Clean out previous work (clean slate).
This commit is contained in:
parent
a6371574a7
commit
e429d2656e
48
README.md
48
README.md
@ -7,15 +7,10 @@ I have found playlists, genres, and other ways of organizing my music too
|
||||
restrictive and cumbersome to keep up with. Here are the main features I want
|
||||
out of music player, in order of priority:
|
||||
|
||||
### Implemented
|
||||
|
||||
* Song tagging.
|
||||
* Playlists.
|
||||
* Bookmarks (temporary song tag marking location in a playlist/album)
|
||||
* Read meta-data from ID3.
|
||||
|
||||
### TODO
|
||||
|
||||
* Web-based interface.
|
||||
* Mobile interface (could implement the Subsonic API on the back-end to be able
|
||||
to use pre-made mobile apps)
|
||||
@ -28,49 +23,30 @@ writing my own.
|
||||
|
||||
## Overview
|
||||
|
||||
WDIWTLT is currently made up of two subprojects:
|
||||
WDIWTLT will be made up of multiple subprojects:
|
||||
|
||||
* `core`
|
||||
* `cli`
|
||||
* `api`
|
||||
* `web`
|
||||
|
||||
### `core`
|
||||
|
||||
`core` contains the data layer implementation, built with a lightweight ORM
|
||||
layer over JDBC, and common functionality for managing a media library.
|
||||
`core` contains the data layer implementation, built with the fiber-orm
|
||||
layer over PostgreSQL, and common functionality for managing a media library.
|
||||
|
||||
### `cli`
|
||||
|
||||
`cli` is a command-line interface built using the WDIWTLT core and VLC for
|
||||
media playback.
|
||||
|
||||
### `api`
|
||||
|
||||
`api` be a REST API implemented on top of the WDIWTLT core providing access to
|
||||
the WDIWTLT database (exposed by the core)
|
||||
|
||||
### `web`
|
||||
|
||||
## Install
|
||||
|
||||
The current version, 0.1.0, is an alpha version. The CLI client depends on
|
||||
[VLC](http://www.videolan.org/vlc/) and expects it to already be installed on
|
||||
the system.
|
||||
|
||||
To install the CLI client:
|
||||
|
||||
$ wget http://mvn.jdb-labs.com/repo/com/jdbernard/wdiwtlt-cli/0.1.0/wdiwtlt-cli-0.1.0.zip
|
||||
$ unzip wdiwtlt-cli-0.1.0.zip
|
||||
|
||||
The `wdiwtlt-cli` binary is located in `wdiwtlt-cli/bin`, which you can add to
|
||||
you `PATH` environment variable.
|
||||
|
||||
## Building From Source
|
||||
|
||||
The `wdiwtlt` project is written in [Groovy](http://www.groovy-lang.org/) uses
|
||||
the [Gradle](http://gradle.org) build tool. If you do not already have a Groovy
|
||||
environment installed you can do:
|
||||
|
||||
$ curl -s https://get.sdkman.io | bash
|
||||
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
$ yes | sdkman install groovy
|
||||
$ yes | sdkman install gradle
|
||||
|
||||
Once you have Groovy and Gradle, the `wdiwtlt` project can be built by invoking
|
||||
`gradle assembleDist`.
|
||||
|
||||
$ git clone https://git.jdb-labs.com/jdb/wdiwtlt.git
|
||||
$ cd wdiwtlt
|
||||
$ gradle assembleDist
|
||||
|
25
build.gradle
25
build.gradle
@ -1,25 +0,0 @@
|
||||
allprojects {
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'ch.raffael.pegdown-doclet:pegdown-doclet:1.2'
|
||||
}
|
||||
}
|
||||
|
||||
group = 'com.jdbernard'
|
||||
version = '0.1.3'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url "https://dl.bintray.com/ijabz/maven" }
|
||||
maven { url "http://mvn.jdb-labs.com/repo" }
|
||||
}
|
||||
|
||||
apply plugin: 'maven'
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'ch.raffael.pegdown-doclet'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName = 'com.jdbernard.wdiwtlt.cli.CommandLineInterface'
|
||||
|
||||
dependencies {
|
||||
compile localGroovy()
|
||||
compile 'ch.qos.logback:logback-classic:1.1.3'
|
||||
compile 'ch.qos.logback:logback-core:1.1.3'
|
||||
compile 'org.slf4j:slf4j-api:1.7.14'
|
||||
compile 'com.offbytwo:docopt:0.6.0.20150202'
|
||||
compile 'com.jdbernard:jdb-util:4.4'
|
||||
compile 'jline:jline:2.12'
|
||||
compile project(":wdiwtlt-core")
|
||||
compile 'uk.co.caprica:vlcj:3.10.1'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
||||
runtime 'net.java.dev.jna:jna:4.2.1'
|
||||
runtime 'net.java.dev.jna:jna-platform:4.2.1'
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
# Selection
|
||||
|
||||
A selection can be a set of Artists, Albums, or songs. It is only ever one of
|
||||
these things. So you can select multiple artists, or multiple albums, or
|
||||
multiple songs.
|
||||
|
||||
select {album, artist, file, playlist, tag} <id | name>
|
||||
select {albums, artists, files, playlists, tags} where <criteria>
|
||||
select files tagged as <tag>... and not as <tag>..
|
||||
select playing {album, artist, file, playlist}
|
||||
|
||||
# Play Queue Management
|
||||
|
||||
enqueue selection
|
||||
enqueue <selection-criteria>
|
||||
|
||||
remove selection from queue
|
||||
remove <selection-criteria> from queue
|
||||
|
||||
play selection
|
||||
play bookmark <id | name>
|
||||
play <selection-criteria>
|
||||
|
||||
clear queue
|
||||
|
||||
# Tagging
|
||||
|
||||
tag <tag>...
|
||||
tag selection as <tag>...
|
||||
tag <selection-criteria> as <tag>...
|
||||
|
||||
# List
|
||||
|
||||
list <selection-criteria>
|
||||
list selected {albums, artists, files, playlists, tags}
|
||||
list playing {albums, artists, files, playlists, tags}
|
||||
|
||||
# Playlist management
|
||||
|
||||
create playlist named <playlist name>
|
||||
create playlist named <playlist name> from {selection, queue}
|
||||
copy playlist <id | name> as <new name>
|
||||
rename playlist <old name> to <new name>
|
||||
|
||||
add selection to playlist <id | name>
|
||||
add <selection-criteria> to playlist <id | name>
|
||||
|
||||
remove selection from playlist <id | name>
|
||||
remove <selection-criteria> from playlist <id | name>
|
||||
|
||||
clear playlist <id | name>
|
||||
|
||||
delete playlist <id | name>
|
||||
|
||||
# Bookmarking
|
||||
|
||||
create bookmark named <name>
|
||||
create bookmark named <name> on <playlist id | name> at <media file id | name>
|
||||
rename bookmark <old name> to <new name>
|
||||
delete bookmark <name | id>
|
||||
|
||||
# Transport Functions
|
||||
|
||||
play
|
||||
pause
|
||||
stop
|
||||
next <count>
|
||||
prev <count>
|
||||
ff <amount> <unit>
|
||||
rw <amount> <unit>
|
||||
jump <media file id | name>
|
||||
|
||||
# Misc
|
||||
|
||||
scan
|
||||
clear
|
@ -1,9 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.cli
|
||||
|
||||
public class CliErr extends Exception {
|
||||
public CliErr(String message) { super(message) }
|
||||
public CliErr(String message, Throwable t) { super(message, t) }
|
||||
public CliErr(Throwable t) { super(t) }
|
||||
|
||||
public static err(String msg) { throw new CliErr(msg) }
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,69 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.cli
|
||||
|
||||
import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI
|
||||
|
||||
public class ScrollText {
|
||||
|
||||
public String text = ""
|
||||
public int maxWidth
|
||||
|
||||
private int scrollIdx
|
||||
private String curAnsiPrefix = ""
|
||||
|
||||
public void setMaxWidth(int max) {
|
||||
this.maxWidth = Math.max(max, 1) }
|
||||
|
||||
public void setText(String t) {
|
||||
this.text = t
|
||||
this.scrollIdx = Math.max(0, text.size() - 10) }
|
||||
|
||||
public String getNextScroll() {
|
||||
if (ANSI.strip(text).size() < maxWidth) return text
|
||||
|
||||
scrollIdx = (scrollIdx + 1) % text.size()
|
||||
|
||||
// If we're on the start of an ANSI escape sequence, skip past the end
|
||||
// of it.
|
||||
while (text[scrollIdx] == '\u001b') {
|
||||
def endIdx = text.findIndexOf(scrollIdx) {
|
||||
Character.isLetter(it.charAt(0)) }
|
||||
curAnsiPrefix = text[scrollIdx..endIdx]
|
||||
scrollIdx = (endIdx + 1) % text.size() }
|
||||
|
||||
int toWalk = maxWidth
|
||||
int endIdx = scrollIdx + 1
|
||||
int wrapIdx = 0
|
||||
|
||||
boolean inAnsiSeq = false
|
||||
while (toWalk > 0 && wrapIdx < scrollIdx) {
|
||||
def cur
|
||||
if (endIdx == text.size()) {
|
||||
cur = text[wrapIdx]
|
||||
|
||||
if (cur == '\u001b') inAnsiSeq = true
|
||||
|
||||
if (inAnsiSeq && Character.isLetter(cur.charAt(0)))
|
||||
inAnsiSeq = false
|
||||
|
||||
if (!inAnsiSeq) toWalk--
|
||||
wrapIdx++ }
|
||||
|
||||
else {
|
||||
cur = text[endIdx]
|
||||
|
||||
if (cur == '\u001b') inAnsiSeq = true
|
||||
|
||||
if (inAnsiSeq && Character.isLetter(cur.charAt(0)))
|
||||
inAnsiSeq = false
|
||||
|
||||
if (!inAnsiSeq) toWalk--
|
||||
endIdx++ } }
|
||||
|
||||
if (wrapIdx == 0) return curAnsiPrefix + text[scrollIdx..<endIdx]
|
||||
return new StringBuilder()
|
||||
.append(curAnsiPrefix)
|
||||
.append(text[scrollIdx..<endIdx])
|
||||
.append(" ").append(text[0..<wrapIdx]) }
|
||||
|
||||
public String toString() { return getNextScroll() }
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.cli
|
||||
|
||||
public class UniversalNoopImplementation {
|
||||
Map methods = [:]
|
||||
|
||||
public def methodMissing(String name, def args) {
|
||||
if (methods[name]) return methods[name](*args) } }
|
@ -1,19 +0,0 @@
|
||||
import ch.qos.logback.core.*;
|
||||
import ch.qos.logback.core.encoder.*;
|
||||
import ch.qos.logback.core.read.*;
|
||||
import ch.qos.logback.core.rolling.*;
|
||||
import ch.qos.logback.core.status.*;
|
||||
import ch.qos.logback.classic.net.*;
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||
|
||||
|
||||
appender("FILE", FileAppender) {
|
||||
file = "wdiwtlt.log"
|
||||
append = true
|
||||
encoder(PatternLayoutEncoder) {
|
||||
pattern = "%level %logger - %msg%n"
|
||||
}
|
||||
}
|
||||
|
||||
root(OFF)
|
||||
logger("com.jdbernard.wdiwtlt", WARN, ["FILE"])
|
@ -1,10 +0,0 @@
|
||||
## Database
|
||||
|
||||
Uses [db-migrate][http://db-migrate.readthedocs.org/en/latest/] to manage
|
||||
database migrations. Migration scripts live if `src/main/db`.
|
||||
|
||||
Database environment configuration lives in `database.json`.
|
||||
|
||||
To initialize a new database do:
|
||||
|
||||
db-migrate -m src/main/db up
|
@ -1,30 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'ch.raffael.pegdown-doclet:pegdown-doclet:1.2'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'ch.raffael.pegdown-doclet'
|
||||
|
||||
dependencies {
|
||||
compile localGroovy()
|
||||
compile 'ch.qos.logback:logback-classic:1.1.3'
|
||||
compile 'ch.qos.logback:logback-core:1.1.3'
|
||||
compile 'org.slf4j:slf4j-api:1.7.14'
|
||||
compile 'com.zaxxer:HikariCP:2.4.3'
|
||||
compile 'net.jthink:jaudiotagger:2.2.3'
|
||||
compile 'commons-codec:commons-codec:1.10'
|
||||
compile 'javax.persistence:persistence-api:1.0.2'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
||||
runtime 'com.h2database:h2:1.4.192'
|
||||
runtime 'org.postgresql:postgresql:9.4.1207.jre7'
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
dataSourceClassName=org.h2.jdbcx.JdbcDataSource
|
||||
dataSource.url=jdbc:h2:/Users/jonathan/programs/wdiwtlt/db;DATABASE_TO_UPPER=FALSE;AUTO_SERVER=TRUE
|
||||
dataSource.user=sa
|
||||
dataSource.password=
|
||||
migrations.dir=src/main/sql/migrations
|
@ -1,93 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
|
||||
public class ConfigWrapper {
|
||||
|
||||
public static final String LIBRARY_DIR_KEY = "library.dir"
|
||||
public static final String DB_CONFIG_FILE_KEY = "database.config.file"
|
||||
|
||||
Properties configProps
|
||||
Map<String, String> env = System.getenv()
|
||||
|
||||
public ConfigWrapper() {
|
||||
if (env.WDIWTLT_CONFIG_FILE) {
|
||||
if (tryLoadConfigFile(env.WDIWTLT_CONFIG_FILE)) {
|
||||
return } }
|
||||
|
||||
if (env.HOME &&
|
||||
tryLoadConfigFile(new File(env.HOME, ".wdiwtlt.properties"))) return
|
||||
|
||||
String userHome = System.getProperty('user.home')
|
||||
if (userHome &&
|
||||
tryLoadConfigFile(new File(userHome, '.wdiwtlt.properties'))) return
|
||||
|
||||
try {
|
||||
ConfigWrapper.getResourceAsStream("/com/jdbernard/wdiwtlt/db/default.properties")
|
||||
.withStream { tryLoadConfig(is) } }
|
||||
catch (Exception e) {} }
|
||||
|
||||
public ConfigWrapper(File propertiesFile) {
|
||||
this.configPropertiesFile = configPropertiesFile }
|
||||
|
||||
public String getLibraryRootPath() {
|
||||
// 1. Check config properties
|
||||
if (configProps && configProps[LIBRARY_DIR_KEY])
|
||||
return configProps[LIBRARY_DIR_KEY]
|
||||
|
||||
// 2. Check environment variable
|
||||
if (env.WDIWTLT_LIBRARY_DIR)
|
||||
return env.WDIWTLT_LIBRARY_DIR
|
||||
|
||||
return null }
|
||||
|
||||
public HikariConfig getHikariConfig() {
|
||||
Properties props = new Properties()
|
||||
|
||||
// 1. Check config properties for database config file
|
||||
if (configProps) {
|
||||
|
||||
// 1.1 Look for a reference to a dedicated config file
|
||||
if (configProps[DB_CONFIG_FILE_KEY]) {
|
||||
File cf = new File(configProps[DB_CONFIG_FILE_KEY])
|
||||
if (cf.exists() && cf.isFile())
|
||||
cf.withInputStream { props.load(it) } }
|
||||
|
||||
// 1.2 Look for prefixed properties in this config file
|
||||
else if (configProps["database.config.dataSourceClassName"]) {
|
||||
props.putAll(configProps
|
||||
.findAll { it.key.startsWith("database.config.") }
|
||||
.collectEntries { [it.key[16..-1], it.value] } ) } }
|
||||
|
||||
// 2. Look for environment variables
|
||||
if (!props) {
|
||||
if (env.WDIWTLT_DATASOURCE_CLASSNAME)
|
||||
props["dataSourceClassName"] = env.WDIWTLT_DATABASE_DATASOURCECLASSNAME
|
||||
|
||||
if (env.WDIWTLT_DATASOURCE_PROPERTIES) {
|
||||
props.putAll(env.WDIWTLT_DATASOURCE_PROPERTIES.split(":")
|
||||
.collect { it.split("=") }
|
||||
.collectEntries { ["dataSource.${it[0]}", it[1]] }) } }
|
||||
|
||||
// If properties exist, create and return HikariConfig
|
||||
if (props) return new HikariConfig(props)
|
||||
else return null }
|
||||
|
||||
private boolean tryLoadConfigFile(String configFilePath) {
|
||||
if (!configFilePath) return false
|
||||
return tryLoadConfigFile(new File(configFilePath)) }
|
||||
|
||||
private boolean tryLoadConfigFile(File configFile) {
|
||||
if (!configFile.exists() || !configFile.isFile()) return false
|
||||
|
||||
return configFile.withInputStream { tryLoadConfig(it) } }
|
||||
|
||||
private boolean tryLoadConfig(InputStream is) {
|
||||
try {
|
||||
Properties props = new Properties()
|
||||
props.load(is)
|
||||
this.configProps = props }
|
||||
catch (Exception e) { return false }
|
||||
|
||||
return true }
|
||||
}
|
@ -1,299 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt
|
||||
|
||||
import com.jdbernard.wdiwtlt.db.DbApi
|
||||
import com.jdbernard.wdiwtlt.db.models.*
|
||||
|
||||
import java.sql.Timestamp
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.jaudiotagger.audio.AudioFile
|
||||
import org.jaudiotagger.audio.AudioFileIO
|
||||
import org.jaudiotagger.tag.Tag as JATag
|
||||
import org.jaudiotagger.tag.FieldKey
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import static org.jaudiotagger.tag.FieldKey.*
|
||||
|
||||
public class MediaLibrary {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(MediaLibrary)
|
||||
|
||||
private static UPPERCASE_PATTERN = Pattern.compile(/(.)(\p{javaUpperCase})/)
|
||||
|
||||
@Delegate DbApi dbapi
|
||||
private File libraryRoot
|
||||
|
||||
public long autoDeletePeriod = 1000 * 60 * 60 * 24 * 7 // one week
|
||||
|
||||
public MediaLibrary(DbApi dbapi, File rootDir) {
|
||||
logger.debug("Creating a MediaLibrary rooted at: {}",
|
||||
rootDir.canonicalPath)
|
||||
|
||||
this.dbapi = dbapi
|
||||
this.libraryRoot = rootDir }
|
||||
|
||||
public void clean() {
|
||||
dbapi.removeEmptyAlbums()
|
||||
dbapi.removeEmptyArtists()
|
||||
dbapi.removeEmptyPlaylists()
|
||||
|
||||
Timestamp staleTS = new Timestamp(new Date().time - autoDeletePeriod)
|
||||
dbapi.withTransaction {
|
||||
def stalePlaylists = dbapi.getPlaylistsWhere(
|
||||
userCreated: false, lastUsedBefore: staleTS)
|
||||
stalePlaylists.each { dbapi.delete(it) } }
|
||||
dbapi.withTransaction {
|
||||
def staleBookmarks = dbapi.getBookmarksWhere(
|
||||
userCreated: false, lastUsedBefore: staleTS)
|
||||
staleBookmarks.each { dbapi.delete(it) } } }
|
||||
|
||||
public def rescanLibrary() {
|
||||
def results = [ total: 0, ignored: 0, new: 0, present: 0, absent: 0]
|
||||
|
||||
List<MediaFile> missingFiles = dbapi.getMediaFiles()
|
||||
List<MediaFile> foundFiles = []
|
||||
|
||||
Date startDate = new Date()
|
||||
libraryRoot.eachFileRecurse { file ->
|
||||
|
||||
if (!file.isFile()) return
|
||||
def mf = addFile(file)
|
||||
|
||||
results.total++
|
||||
if (!mf) results.ignored++
|
||||
else {
|
||||
foundFiles << mf
|
||||
if (missingFiles.contains(mf)) missingFiles.remove(mf)
|
||||
if (mf.dateAdded > startDate) results.new++ } }
|
||||
|
||||
foundFiles.each { mf ->
|
||||
if (!mf.presentLocally) {
|
||||
mf.presentLocally = true
|
||||
dbapi.update(mf) } }
|
||||
|
||||
missingFiles.each { mf ->
|
||||
if (mf.presentLocally) {
|
||||
mf.presentLocally = false
|
||||
dbapi.update(mf) } }
|
||||
|
||||
results.present = foundFiles.size()
|
||||
results.absent = missingFiles.size()
|
||||
|
||||
return results }
|
||||
|
||||
public MediaFile addFile(File f) {
|
||||
if (!f.exists() || !f.isFile()) {
|
||||
logger.info("Ignoring non-existant file: {}", f.canonicalPath)
|
||||
return null }
|
||||
|
||||
def relPath = getRelativePath(libraryRoot, f)
|
||||
MediaFile found = dbapi.getMediaFileByFilePath(relPath)
|
||||
|
||||
if (found) {
|
||||
logger.info(
|
||||
"Ignoring a media file I already know about: {}", relPath)
|
||||
return found }
|
||||
|
||||
MediaFile mf = new MediaFile()
|
||||
mf.filePath = relPath
|
||||
|
||||
// Hash the file
|
||||
mf.fileHash = f.withInputStream { DigestUtils.md5Hex(it) }
|
||||
|
||||
// Look for an entry for that hash
|
||||
found = dbapi.getMediaFileByFileHash(mf.fileHash)
|
||||
|
||||
if (found) {
|
||||
logger.info('Found a media file by hash in a new location. ' +
|
||||
"I'm updating my relative path to this file:\n\t{}\n\t\t--> {}.",
|
||||
found.filePath, mf.filePath)
|
||||
found.filePath = mf.filePath
|
||||
return dbapi.update(found) }
|
||||
|
||||
// Read in the media's tags
|
||||
def af
|
||||
try { af = AudioFileIO.read(f) }
|
||||
catch (Exception e) {
|
||||
logger.info("Ignoring a file because I can't " +
|
||||
"read the media tag info:\n\t{}\n\t{}",
|
||||
f.canonicalPath, e.localizedMessage)
|
||||
return null }
|
||||
|
||||
def fileTag = af.tag
|
||||
|
||||
mf.name = fileTag?.getFirst(TITLE)?.trim() ?: f.name
|
||||
mf.comment = fileTag?.getAll(COMMENT)?.collect { it.trim() }?.join('\n\n')
|
||||
mf.discNumber = fileTag?.getFirst(DISC_NO) ?: '1'
|
||||
mf.trackNumber = safeToInteger(fileTag?.getFirst(TRACK))
|
||||
|
||||
def folderParts = mf.filePath.split("[\\\\/]")[1..<-1] as LinkedList
|
||||
|
||||
// Find artist and album names (if any)
|
||||
def artistNames = fileTag?.getAll(ARTIST)?.collect { it.trim() }
|
||||
def albumNames = fileTag?.getAll(ALBUM)?.collect { it.trim() }
|
||||
|
||||
if (!artistNames) {
|
||||
mf.metaInfoSource = MediaFile.FILE_LOCATION
|
||||
artistNames = folderParts.size() >= 2 ? [folderParts[0]] : [] }
|
||||
|
||||
if (!albumNames) {
|
||||
mf.metaInfoSource = MediaFile.FILE_LOCATION
|
||||
albumNames = [folderParts.peekLast()] }
|
||||
|
||||
dbapi.withTransaction {
|
||||
dbapi.create(mf)
|
||||
associateWithArtistsAndAlbums(mf, artistNames, albumNames,
|
||||
safeToInteger(fileTag?.getFirst(YEAR))) }
|
||||
|
||||
return mf
|
||||
}
|
||||
|
||||
public def getByIdOrName(Class modelClass, String input) {
|
||||
def match
|
||||
|
||||
if (safeToUUID(input)) {
|
||||
match = dbapi.getById(modelClass, safeToUUID(input))
|
||||
if (match) match = [match] }
|
||||
|
||||
else {
|
||||
match = dbapi.getByIdLike(modelClass, input)
|
||||
if (!match) match = dbapi.getByName(modelClass, input)
|
||||
if (!match) match = dbapi.getLike(modelClass, ["name"], [input]) }
|
||||
return match }
|
||||
|
||||
public List<Artist> getArtistsByName(String name) {
|
||||
return [dbapi.getArtistByName(name)] ?: dbapi.getArtistsLikeName(name) }
|
||||
|
||||
public List<Album> getAlbumsByName(String name) {
|
||||
return dbapi.getAlbumsWhere(name: name) ?: dbapi.getAlbumsLikeName(name) }
|
||||
|
||||
public List<MediaFile> collectMediaFiles(List<Model> models) {
|
||||
if (!models) return []
|
||||
|
||||
return models.collectMany { m ->
|
||||
if (m.class == MediaFile) return [m]
|
||||
return dbapi.getMediaFilesWhere((idKeyFor(m.class)): m.id) }.findAll() }
|
||||
|
||||
public List<Artist> splitArtist(Artist toSplit, Pattern splitPattern) {
|
||||
return splitArtist(toSplit, pattern.split(toSplit.name)) }
|
||||
|
||||
public List<Artist> splitArtist(Artist toSplit, List<String> newNames) {
|
||||
def albums = dbapi.getAlbumsWhere(artistId: toSplit.id)
|
||||
def mediaFiles = dbapi.getMediaFilesByArtistId(toSplit.id)
|
||||
|
||||
toSplit.name = newNames[0]
|
||||
List<Artist> newArtists = newNames[1..-1].collect { new Artist(it) }
|
||||
newArtists.each { newArtist ->
|
||||
albums.each { dbapi.associate(newArtist, it) }
|
||||
mediaFiles.each { dbapi.associate(newArtist, it) } }
|
||||
|
||||
return [toSplit] + newArtists }
|
||||
|
||||
private void associateWithArtistsAndAlbums(MediaFile mf,
|
||||
List<String> artistNames, List<String> albumNames, Integer albumYear) {
|
||||
|
||||
// Find or create artists.
|
||||
def artists = artistNames.collect { artistName ->
|
||||
Artist artist = dbapi.getArtistByName(artistName)
|
||||
if (!artist) artist = dbapi.create(new Artist(name: artistName))
|
||||
return artist }
|
||||
|
||||
// Associate file with artists.
|
||||
artists.each { dbapi.associate(it, mf) }
|
||||
|
||||
// Find or create albums
|
||||
def albums = albumNames.collect { albumName ->
|
||||
Album album
|
||||
|
||||
// If we know what year the album was released we can use that to
|
||||
// narrow down the list of matching albums.
|
||||
if (albumYear != null) {
|
||||
// Look first to see if we already know about this album
|
||||
// associated with one of the artists for this piece.
|
||||
album = artists.inject(null, { foundAlbum, artist ->
|
||||
if (foundAlbum) return foundAlbum
|
||||
def cur = dbapi.getAlbumsWhere(name: albumName,
|
||||
year: albumYear, artistId: artist.id)
|
||||
return cur ? cur[0] : null })
|
||||
|
||||
// If we don't have it with one of the artists, see if we have
|
||||
// one that matches the name and year.
|
||||
if (!album) {
|
||||
def cur = dbapi.getAlbumsWhere(
|
||||
name: albumName, year: albumYear)
|
||||
album = cur ? cur[0] : null } }
|
||||
|
||||
else {
|
||||
album = artists.inject(null, { foundAlbum, artist ->
|
||||
if (foundAlbum) return foundAlbum
|
||||
def cur = dbapi.getAlbumsWhere(
|
||||
name: albumName, artistId: artist.id)
|
||||
return cur ? cur[0] : null })
|
||||
|
||||
if (!album) {
|
||||
def cur = dbapi.getAlbumsWhere(name: albumName)
|
||||
album = cur ? cur[0] : null } }
|
||||
|
||||
// We still can't find the album at all. We'll need to create it
|
||||
if (!album)
|
||||
album = dbapi.create(new Album(name: albumName, year: albumYear))
|
||||
|
||||
return album }
|
||||
|
||||
// Associate file with albums
|
||||
albums.each { dbapi.associate(it, mf) }
|
||||
|
||||
// Make sure we have association between all of the artists and albums.
|
||||
artists.each { artist ->
|
||||
def albumsForArtist = dbapi.getAlbumsWhere(artistId: artist.id)
|
||||
def albumsMissing = albums - albumsForArtist
|
||||
albumsMissing.each { album -> dbapi.associate(artist, album) } } }
|
||||
|
||||
/** #### `getRelativePath`
|
||||
* Given a parent path and a child path, assuming the child path is
|
||||
* contained within the parent path, return the relative path from the
|
||||
* parent to the child. */
|
||||
public static String getRelativePath(File parent, File child) {
|
||||
def parentPath = parent.canonicalPath.split("[\\\\/]")
|
||||
def childPath = child.canonicalPath.split("[\\\\/]")
|
||||
|
||||
/// If the parent path is longer it cannot contain the child path and
|
||||
/// we cannot construct a relative path without backtracking.
|
||||
if (parentPath.length > childPath.length) return ""
|
||||
|
||||
/// Compare the parent and child path up until the end of the parent
|
||||
/// path.
|
||||
int b = 0
|
||||
while (b < parentPath.length && parentPath[b] == childPath[b] ) b++
|
||||
|
||||
/// If we stopped before reaching the end of the parent path it must be
|
||||
/// that the paths do not match. The parent cannot contain the child and
|
||||
/// we cannot build a relative path without backtracking.
|
||||
if (b != parentPath.length) return ""
|
||||
return (['.'] + childPath[b..<childPath.length]).join('/') }
|
||||
|
||||
public static UUID safeToUUID(def val) {
|
||||
if (val == null) return null
|
||||
try { return UUID.fromString(val as String) }
|
||||
catch (IllegalArgumentException iae) { return null } }
|
||||
|
||||
public static Integer safeToInteger(def val) {
|
||||
if (val == null) return null
|
||||
try { return val.trim() as Integer }
|
||||
catch (NumberFormatException nfe) { return null } }
|
||||
|
||||
public static String uncapitalize(String s) {
|
||||
if (s == null) return null;
|
||||
if (s.length() < 2) return s.toLowerCase();
|
||||
return s[0].toLowerCase() + s[1..-1] }
|
||||
|
||||
public static String toEnglish(Class c) {
|
||||
return UPPERCASE_PATTERN.matcher(c.simpleName).
|
||||
replaceAll(/$1 $2/).toLowerCase() }
|
||||
|
||||
public static String idKeyFor(Class c) {
|
||||
return uncapitalize(c.simpleName) + 'Id'; }
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
public class Album extends Model {
|
||||
public String name;
|
||||
public Integer trackTotal;
|
||||
public Integer year;
|
||||
|
||||
public String toString() {
|
||||
if (year != null) return name + " (" + year + ")";
|
||||
else return name; }
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
public class Artist extends Model {
|
||||
public String name;
|
||||
|
||||
public String toString() { return name; }
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Bookmark extends Model {
|
||||
public String name;
|
||||
public UUID playlistId;
|
||||
public UUID mediaFileId;
|
||||
public int playIndex = 0;
|
||||
public int playTimeMs = 0;
|
||||
public boolean userCreated;
|
||||
public Timestamp createdAt = new Timestamp(new Date().getTime());
|
||||
public Timestamp lastUsed = new Timestamp(new Date().getTime());
|
||||
|
||||
public String toString() { return name; }
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
public class Image extends Model {
|
||||
public String url;
|
||||
|
||||
public String toString() { return url; }
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
import java.util.Date;
|
||||
import java.sql.Timestamp;
|
||||
|
||||
public class MediaFile extends Model {
|
||||
public static final String TAG_INFO = "tag info";
|
||||
public static final String FILE_LOCATION = "file location";
|
||||
|
||||
public String name;
|
||||
public String discNumber;
|
||||
public Integer trackNumber;
|
||||
public int playCount = 0;
|
||||
public String filePath;
|
||||
public String fileHash;
|
||||
public String metaInfoSource = TAG_INFO;
|
||||
public Timestamp dateAdded = new Timestamp(new Date().getTime());
|
||||
public Timestamp lastPlayed;
|
||||
public boolean presentLocally = true;
|
||||
public String comment;
|
||||
|
||||
public String toString() {
|
||||
if (trackNumber != null) return trackNumber + " - " + name;
|
||||
else return name; }
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
import java.util.UUID;
|
||||
import javax.persistence.Entity;
|
||||
|
||||
@Entity
|
||||
public class Model implements Comparable<Model>, Cloneable {
|
||||
public UUID id;
|
||||
|
||||
public boolean equals(Object thatObj) {
|
||||
if (thatObj == null) return false;
|
||||
if (this.getClass() != thatObj.getClass()) return false;
|
||||
|
||||
Model that = (Model) thatObj;
|
||||
|
||||
if (this.id == null || that.id == null) return false;
|
||||
return this.id.equals(that.id); }
|
||||
|
||||
public int compareTo(Model that) {
|
||||
if (this.id == null) return -1;
|
||||
if (this.getClass() != that.getClass()) {
|
||||
return this.getClass().getSimpleName().compareTo(
|
||||
that.getClass().getSimpleName()); }
|
||||
|
||||
return this.id.compareTo(that.id); }
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Playlist extends Model {
|
||||
public boolean userCreated = true;
|
||||
public String name;
|
||||
public int mediaFileCount = 0;
|
||||
public UUID copiedFromId = null;
|
||||
public Timestamp createdAt = new Timestamp(new Date().getTime());
|
||||
public Timestamp lastUsed = new Timestamp(new Date().getTime());
|
||||
|
||||
public String toString() {
|
||||
if (userCreated) return name;
|
||||
return name + " (auto)"; }
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package com.jdbernard.wdiwtlt.db.models;
|
||||
|
||||
public class Tag extends Model {
|
||||
public String name;
|
||||
public String description = "";
|
||||
|
||||
public String toString() {
|
||||
if (description != null && description.length() > 0)
|
||||
return name + " (" + description + ")";
|
||||
else return name; }
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
DROP TABLE artists_albums;
|
||||
DROP TABLE albums_images;
|
||||
DROP TABLE artists_images;
|
||||
DROP TABLE images;
|
||||
DROP TABLE media_files_tags;
|
||||
DROP TABLE bookmarks;
|
||||
DROP TABLE playlists_media_files;
|
||||
DROP TABLE playlists;
|
||||
DROP TABLE tags;
|
||||
DROP TABLE artists_media_files;
|
||||
DROP TABLE albums_media_files;
|
||||
DROP TABLE media_files;
|
||||
DROP TABLE albums;
|
||||
DROP TABLE artists;
|
@ -1,112 +0,0 @@
|
||||
CREATE TABLE artists (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX artists_name_idx ON artists(name);
|
||||
|
||||
CREATE TABLE albums (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
year INTEGER,
|
||||
track_total INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX albums_name_idx ON albums(name);
|
||||
|
||||
CREATE TABLE media_files (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
disc_number VARCHAR NOT NULL DEFAULT '1',
|
||||
track_number INTEGER,
|
||||
play_count INTEGER NOT NULL DEFAULT 0,
|
||||
file_path VARCHAR NOT NULL,
|
||||
file_hash VARCHAR NOT NULL,
|
||||
meta_info_source VARCHAR NOT NULL, -- 'tag' or 'filesystem'
|
||||
date_added TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_played TIMESTAMP,
|
||||
present_locally BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
comment VARCHAR DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX media_files_name_idx ON media_files(name);
|
||||
|
||||
CREATE TABLE artists_media_files (
|
||||
artist_id UUID NOT NULL REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
media_file_id UUID NOT NULL REFERENCES media_files(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (artist_id, media_file_id)
|
||||
);
|
||||
|
||||
CREATE TABLE albums_media_files (
|
||||
album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
media_file_id UUID NOT NULL REFERENCES media_files(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (album_id, media_file_id)
|
||||
);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR UNIQUE NOT NULL,
|
||||
description VARCHAR NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE playlists (
|
||||
id UUID PRIMARY KEY,
|
||||
user_created BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
name VARCHAR NOT NULL,
|
||||
media_file_count INTEGER NOT NULL DEFAULT 0,
|
||||
copied_from_id UUID DEFAULT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_used TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE playlists_media_files (
|
||||
playlist_id UUID NOT NULL REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
media_file_id UUID NOT NULL REFERENCES media_files(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (playlist_id, media_file_id, position),
|
||||
UNIQUE (playlist_id, position)
|
||||
);
|
||||
|
||||
CREATE TABLE bookmarks (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR,
|
||||
user_created BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
playlist_id UUID NOT NULL REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
media_file_id UUID NOT NULL REFERENCES media_files(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
play_index INTEGER NOT NULL,
|
||||
play_time_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_used TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX bookmarks_playlist_id_idx ON bookmarks (playlist_id);
|
||||
CREATE INDEX bookmarks_media_file_id_idx ON bookmarks (media_file_id);
|
||||
|
||||
CREATE TABLE media_files_tags (
|
||||
media_file_id UUID REFERENCES media_files(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
tag_id UUID REFERENCES tags(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (media_file_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE images (
|
||||
id UUID PRIMARY KEY,
|
||||
url VARCHAR
|
||||
);
|
||||
|
||||
CREATE TABLE artists_images (
|
||||
artist_id UUID REFERENCES artists (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
image_id UUID REFERENCES images (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (artist_id, image_id)
|
||||
);
|
||||
|
||||
CREATE TABLE albums_images (
|
||||
album_id UUID REFERENCES albums (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
image_id UUID REFERENCES images (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (album_id, image_id)
|
||||
);
|
||||
|
||||
CREATE TABLE artists_albums (
|
||||
artist_id UUID NOT NULL REFERENCES artists (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
album_id UUID NOT NULL REFERENCES albums (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (artist_id, album_id)
|
||||
);
|
@ -1,35 +0,0 @@
|
||||
import groovy.grape.Grape
|
||||
import groovy.sql.Sql
|
||||
Grape.grab(group: 'com.zaxxer', module: 'HikariCP', version: '2.4.3')
|
||||
Grape.grab(group: 'org.postgresql', module: 'postgresql', version: '9.4.1207.jre7')
|
||||
Grape.grab(group: 'commons-codec', module: 'commons-codec', version: '1.10')
|
||||
|
||||
import com.jdbernard.wdiwtlt.MediaLibrary
|
||||
import com.jdbernard.wdiwtlt.db.ORM
|
||||
import com.jdbernard.wdiwtlt.db.models.*
|
||||
import com.zaxxer.hikari.*
|
||||
import org.jaudiotagger.audio.*
|
||||
import org.jaudiotagger.tag.*
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)
|
||||
rootLogger.level = Level.INFO
|
||||
|
||||
// myLogger = (Logger) LoggerFactory.getLogger("com.jdbernard.wdiwtlt")
|
||||
// myLogger.level = Level.DEBUG
|
||||
|
||||
config = new Properties()
|
||||
config.dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
|
||||
config."dataSource.databaseName" = "wdiwtlt"
|
||||
config."dataSource.user" = "jonathan"
|
||||
config."dataSource.password" = ""
|
||||
|
||||
hcfg = new HikariConfig(config)
|
||||
hds = new HikariDataSource(hcfg)
|
||||
|
||||
db = new ORM(hds)
|
||||
|
||||
musicDir = new File('/Users/jonathan/Music')
|
||||
library = new MediaLibrary(db, musicDir)
|
@ -1,24 +0,0 @@
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'war'
|
||||
apply plugin: "jetty"
|
||||
|
||||
dependencies {
|
||||
compile localGroovy()
|
||||
compile 'ch.qos.logback:logback-classic:1.1.3'
|
||||
compile 'ch.qos.logback:logback-core:1.1.3'
|
||||
compile 'org.slf4j:slf4j-api:1.7.14'
|
||||
compile 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.3'
|
||||
compile 'com.lambdaworks:scrypt:1.4.0'
|
||||
compile 'com.zaxxer:HikariCP-java6:2.3.2'
|
||||
compile 'javax:javaee-api:7.0'
|
||||
compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
|
||||
compile project(":wdiwtlt-core")
|
||||
|
||||
runtime 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.3.2'
|
||||
runtime 'org.glassfish.jersey.containers:jersey-container-servlet:2.16'
|
||||
runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.16'
|
||||
|
||||
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testRuntime 'com.h2database:h2:1.4.186' }
|
@ -1,12 +0,0 @@
|
||||
package com.jdbernard.nlsongs.rest;
|
||||
|
||||
import javax.ws.rs.NameBinding;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@NameBinding
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
public @interface AllowCors {}
|
@ -1,30 +0,0 @@
|
||||
package com.jdbernard.nlsongs.rest;
|
||||
|
||||
import javax.annotation.Priority;
|
||||
import javax.ws.rs.Priorities;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerResponseContext;
|
||||
import javax.ws.rs.container.ContainerResponseFilter;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider @AllowCors @Priority(Priorities.HEADER_DECORATOR)
|
||||
public class CorsResponseFilter implements ContainerResponseFilter {
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext reqCtx,
|
||||
ContainerResponseContext respCtx) {
|
||||
|
||||
MultivaluedMap<String, Object> headers = respCtx.getHeaders();
|
||||
|
||||
headers.add("Access-Control-Allow-Origin",
|
||||
reqCtx.getHeaderString("Origin"));
|
||||
|
||||
headers.add("Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS");
|
||||
|
||||
headers.add("Access-Control-Allow-Headers",
|
||||
reqCtx.getHeaderString("Access-Control-Request-Headers"));
|
||||
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package com.jdbernard.nlsongs.rest;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
|
||||
@Path("v1/ping") @AllowCors
|
||||
public class PingResource {
|
||||
|
||||
@GET
|
||||
@Produces("text/plain")
|
||||
public String ping() { return "pong"; } }
|
@ -1,8 +0,0 @@
|
||||
package com.jdbernard.nlsongs.servlet
|
||||
|
||||
import com.jdbernard.wdiwtlt.db.DbApi
|
||||
|
||||
public class WdiwtltContext {
|
||||
|
||||
public static DbApi dbapi
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package com.jdbernard.nlsongs.servlet
|
||||
|
||||
import javax.servlet.ServletContext
|
||||
import javax.servlet.ServletContextEvent
|
||||
import javax.servlet.ServletContextListener
|
||||
|
||||
import com.jdbernard.wdiwtlt.db.DbApi
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
|
||||
public final class WdiwtltContextListener implements ServletContextListener {
|
||||
|
||||
public void contextInitialized(ServletContextEvent event) {
|
||||
def context = event.servletContext
|
||||
|
||||
// Load the context configuration.
|
||||
Properties props = new Properties()
|
||||
WdiwtltContextListener.getResourceAsStream(
|
||||
context.getInitParameter('context.config.file')).withStream { is ->
|
||||
props.load(is) }
|
||||
|
||||
// Create the pooled data source
|
||||
HikariConfig hcfg = new HikariConfig(
|
||||
context.getInitParameter('datasource.config.file'))
|
||||
|
||||
HikariDataSource hds = new HikariDataSource(hcfg)
|
||||
|
||||
// Create the NLSonsDB instance.
|
||||
DbApi dbapi = new DbApi(hds)
|
||||
|
||||
context.setAttribute('dbapi', dbapi)
|
||||
WdiwtltContext.dbapi = dbapi }
|
||||
|
||||
public void contextDestroyed(ServletContextEvent event) {
|
||||
def context = event.servletContext
|
||||
|
||||
// Shutdown the DB API instance (it will shut down the data source).
|
||||
DbApi dbapi = context.getAttribute('dbapi')
|
||||
if (dbapi) dbapi.shutdown()
|
||||
|
||||
context.removeAttribute('dbapi') }
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
include 'core', 'cli', 'rest-server'
|
||||
|
||||
project(":core").name ="wdiwtlt-core"
|
||||
project(":cli").name = "wdiwtlt-cli"
|
21
worklog.md
21
worklog.md
@ -1,21 +0,0 @@
|
||||
WDIWTLT
|
||||
========================================
|
||||
|
||||
* Local library
|
||||
* CLI interface
|
||||
* REST API
|
||||
|
||||
|
||||
Current track
|
||||
----------------------------------------
|
||||
|
||||
Building CLI:
|
||||
* More clarity around selection
|
||||
management.
|
||||
* Tagging
|
||||
* Bookmark management
|
||||
* Playlist managment
|
||||
* Online command help.
|
||||
* Ability to actually play music.
|
||||
* Library management (split artists,
|
||||
change other metadata)?
|
Loading…
Reference in New Issue
Block a user