From e429d2656e2af2bf73d8cf88d954d43e47ac36e6 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Thu, 11 Nov 2021 11:48:53 -0600 Subject: [PATCH] Clean out previous work (clean slate). --- README.md | 48 +- build.gradle | 25 - cli/build.gradle | 23 - cli/interface.txt | 76 - .../com/jdbernard/wdiwtlt/cli/CliErr.groovy | 9 - .../wdiwtlt/cli/CommandLineInterface.groovy | 1673 ----------------- .../jdbernard/wdiwtlt/cli/ScrollText.groovy | 69 - .../cli/UniversalNoopImplementation.groovy | 7 - cli/src/main/resources/logback.groovy | 19 - core/README.md | 10 - core/build.gradle | 30 - core/database.properties | 5 - .../jdbernard/wdiwtlt/ConfigWrapper.groovy | 93 - .../com/jdbernard/wdiwtlt/MediaLibrary.groovy | 299 --- .../com/jdbernard/wdiwtlt/db/DbApi.groovy | 1004 ---------- .../jdbernard/wdiwtlt/db/models/Album.java | 11 - .../jdbernard/wdiwtlt/db/models/Artist.java | 7 - .../jdbernard/wdiwtlt/db/models/Bookmark.java | 18 - .../jdbernard/wdiwtlt/db/models/Image.java | 7 - .../wdiwtlt/db/models/MediaFile.java | 25 - .../jdbernard/wdiwtlt/db/models/Model.java | 26 - .../jdbernard/wdiwtlt/db/models/Playlist.java | 18 - .../com/jdbernard/wdiwtlt/db/models/Tag.java | 11 - .../20151209054632-initial-schema-down.sql | 14 - .../20151209054632-initial-schema-up.sql | 112 -- core/test.groovy | 35 - rest-server/build.gradle | 24 - .../com/jdbernard/wdiwtlt/rest/AllowCors.java | 12 - .../wdiwtlt/rest/CorsResponseFilter.java | 30 - .../jdbernard/wdiwtlt/rest/PingResource.java | 12 - .../wdiwtlt/servlet/WdiwtltContext.groovy | 8 - .../servlet/WdiwtltContextListener.groovy | 43 - settings.gradle | 4 - worklog.md | 21 - 34 files changed, 12 insertions(+), 3816 deletions(-) delete mode 100644 build.gradle delete mode 100644 cli/build.gradle delete mode 100644 cli/interface.txt delete mode 100644 cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy delete mode 100644 cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy delete mode 100644 cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/ScrollText.groovy delete mode 100644 cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/UniversalNoopImplementation.groovy delete mode 100644 cli/src/main/resources/logback.groovy delete mode 100644 core/README.md delete mode 100644 core/build.gradle delete mode 100644 core/database.properties delete mode 100644 core/src/main/groovy/com/jdbernard/wdiwtlt/ConfigWrapper.groovy delete mode 100644 core/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy delete mode 100644 core/src/main/groovy/com/jdbernard/wdiwtlt/db/DbApi.groovy delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Album.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Artist.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Bookmark.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Image.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/MediaFile.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Model.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Playlist.java delete mode 100644 core/src/main/java/com/jdbernard/wdiwtlt/db/models/Tag.java delete mode 100644 core/src/main/sql/migrations/20151209054632-initial-schema-down.sql delete mode 100644 core/src/main/sql/migrations/20151209054632-initial-schema-up.sql delete mode 100644 core/test.groovy delete mode 100644 rest-server/build.gradle delete mode 100644 rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/AllowCors.java delete mode 100644 rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/CorsResponseFilter.java delete mode 100644 rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/PingResource.java delete mode 100644 rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContext.groovy delete mode 100644 rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContextListener.groovy delete mode 100644 settings.gradle delete mode 100644 worklog.md diff --git a/README.md b/README.md index 18e9eda..dfda20a 100644 --- a/README.md +++ b/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 diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 938d6fd..0000000 --- a/build.gradle +++ /dev/null @@ -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' -} diff --git a/cli/build.gradle b/cli/build.gradle deleted file mode 100644 index 2b77736..0000000 --- a/cli/build.gradle +++ /dev/null @@ -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' -} diff --git a/cli/interface.txt b/cli/interface.txt deleted file mode 100644 index 6e904e4..0000000 --- a/cli/interface.txt +++ /dev/null @@ -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} -select {albums, artists, files, playlists, tags} where -select files tagged as ... and not as .. -select playing {album, artist, file, playlist} - -# Play Queue Management - -enqueue selection -enqueue - -remove selection from queue -remove from queue - -play selection -play bookmark -play - -clear queue - -# Tagging - -tag ... -tag selection as ... -tag as ... - -# List - -list -list selected {albums, artists, files, playlists, tags} -list playing {albums, artists, files, playlists, tags} - -# Playlist management - -create playlist named -create playlist named from {selection, queue} -copy playlist as -rename playlist to - -add selection to playlist -add to playlist - -remove selection from playlist -remove from playlist - -clear playlist - -delete playlist - -# Bookmarking - -create bookmark named -create bookmark named on at -rename bookmark to -delete bookmark - -# Transport Functions - -play -pause -stop -next -prev -ff -rw -jump - -# Misc - -scan -clear diff --git a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy deleted file mode 100644 index b491ed3..0000000 --- a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy +++ /dev/null @@ -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) } -} diff --git a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy deleted file mode 100644 index 2451f86..0000000 --- a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy +++ /dev/null @@ -1,1673 +0,0 @@ -package com.jdbernard.wdiwtlt.cli - -import com.jdbernard.wdiwtlt.ConfigWrapper -import com.jdbernard.wdiwtlt.MediaLibrary -import com.jdbernard.wdiwtlt.db.DbApi -import com.jdbernard.wdiwtlt.db.models.* -import com.jdbernard.io.NonBlockingInputStreamReader -import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI -import com.zaxxer.hikari.HikariConfig -import com.zaxxer.hikari.HikariDataSource -import org.docopt.Docopt -import jline.console.ConsoleReader -import java.sql.Timestamp -import java.text.SimpleDateFormat -import java.net.URLDecoder -import java.util.regex.Pattern -import java.util.regex.Matcher -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import uk.co.caprica.vlcj.discovery.NativeDiscovery -import uk.co.caprica.vlcj.component.AudioMediaListPlayerComponent -import uk.co.caprica.vlcj.player.MediaPlayerEventListener -import uk.co.caprica.vlcj.player.list.MediaListPlayerMode - -import static com.jdbernard.util.AnsiEscapeCodeSequence.* -import static com.jdbernard.wdiwtlt.MediaLibrary.* -import static com.jdbernard.wdiwtlt.cli.CliErr.* - -public class CommandLineInterface { - - public static final VERSION = "0.1.3" - - public static final def DOC = """\ -wdiwtlt v$VERSION - -Usage: - wdiwtlt [options] - wdiwtlt --version - wdiwtlt --help - wdiwtlt --sync - wdiwtlt --sync-only - -Options: - -c --config - - Config file for wdiwtlt CLI. - - -L --library-root - - The path to a local media library directory. Providing a local library - causes wdiwtlt to run in local mode. - - -D --database-config - - The path to a data source configuration file. wdiwtlt cli uses HikariCP - for database connection pooling, so the given configuration file is - expected to be formatted so that it can be fed directly to the - HikariConfig constructor. - (see https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby) - - -R --remote-database-config - - For use when performing a sync: the path to a data source configuration - file for the remote database configuration data. This is processed in - the same manner as the local database configuration file. - - --sync-pull - - When performing a sync, copy data present on the remote database and - missing locally into the local database. - - --sync-push - - When performing a sync, copy data missing on the remote database and - present in the local database into the remote database. - -Configuration: -""" - - private static Logger logger = - LoggerFactory.getLogger(CommandLineInterface) - - private Properties cliConfig - private MediaLibrary library - - /// IO Management - private InputStream inStream - private OutputStream outStream - - private ConsoleReader reader - private Thread consoleReaderThread - private List consoleReadBuffer = - Collections.synchronizedList(new ArrayList()) - private synchronized boolean running - - /// Console output data - private String titleStyle, normalStyle, statusStyle, promptStyle, - artistStyle, albumStyle, fileStyle, errorStyle, playlistStyle, - cmdStyle, optStyle - private String eraseToEnd = new ANSI().eraseLine(Erase.ToEnd).toString() - private String clearLine = new ANSI().eraseLine(Erase.All).toString() - private String afterInput = - new ANSI().eraseLine(Erase.All).scrollUp().cursorUp().toString() - private String beforeLeader = - new ANSI().saveCursor().cursorPrevLine(3).toString() - private String afterLeader = - new ANSI().restoreCursor().toString() - private String eraseLeader = - new ANSI().eraseLine(Erase.All).cursorPrevLine().eraseLine(Erase.All) - .cursorPrevLine().eraseLine(Erase.All) - .cursorPrevLine().eraseLine(Erase.All).toString() - - public final static modelClasses = [ - 'album': Album, 'artist': Artist, 'bookmark': Bookmark, - 'file': MediaFile, 'mediaFile': MediaFile, 'playlist': Playlist, - 'tag': Tag ] - - public final static selectableModels = 'album|artist|file|playlist|tag' - - private int displayWidth = 79 - private long msgTimeout - private ScrollText currentlyPlaying = new ScrollText( - maxWidth: displayWidth - 17, - text: "No media currently playing.") - private ScrollText status = new ScrollText(maxWidth: displayWidth) - private Date dismissMsgDate = new Date() - private SimpleDateFormat sdf = new SimpleDateFormat('EEE-HH-SSS') - private Random rand = new Random() - - /// Current play queue and selection data - List currentSelection = [] - Playlist playQueue - Bookmark playBookmark - MediaFile curMediaFile - - /// VLCJ Player - AudioMediaListPlayerComponent vlcj - - public static void main(String[] args) { - - def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args) - - def exitErr = { msg -> - System.err.println("wdiwtlt: $msg") - System.exit(1) } - - // Look for a given CLI config file. - def givenCfg = new Properties() - File cfgFile - if (opts["--config"]) cfgFile = new File(opts["--config"]) - - if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile()) - cfgFile = new File("wdiwtlt.cli.properties") - - if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile()) { - String userHome = System.getenv().HOME - if (userHome) cfgFile = new File(userHome, ".wdiwtlt.cli.properties") - - if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile()) { - userHome = System.getProperty('user.home') - if (userHome) cfgFile = new File(userHome, 'wdiwtlt.cli.properties') } } - - if (cfgFile.exists() && cfgFile.isFile()) { - try { cfgFile.withInputStream { givenCfg.load(it) } } - catch (Exception e) { givenCfg.clear() } } - - // Just in case, load up the general WDIWTLT default config service - def wdiwtltDefaultConfig = new ConfigWrapper() - - // Find the library root directory (TODO: eventually this should be - // enabled only when local mode is chosen). - File libRoot = new File( - opts["--library-root"] ?: - givenCfg[ConfigWrapper.LIBRARY_DIR_KEY] ?: - wdiwtltDefaultConfig.libraryRootPath ?: - "no libRoot configured") - - if (libRoot && (!libRoot.exists() || !libRoot.isDirectory())) - exitErr("Library root does not exist or is not a directory: " + - libRoot.canonicalPath) - - // Get the library database - HikariConfig hcfg - File dbCfgFile = new File( - opts["--database-config"] ?: - givenCfg[ConfigWrapper.DB_CONFIG_FILE_KEY] ?: - "database.properties") - - // Try to load from the given DB config file - if (dbCfgFile && dbCfgFile.exists() && dbCfgFile.isFile()) { - Properties props = new Properties() - try { dbCfgFile.withInputStream { props.load(it) } } - catch (Exception e) { props.clear() } - - if (props) hcfg = new HikariConfig(props) } - - // Look to see if database connection properties are given in the CLI - // config file. - if (givenCfg && givenCfg["database.config.dataSourceClassName"]) { - Properties props = new Properties() - props.putAll(givenCfg - .findAll { it.key.startsWith("database.config.") } - .collectEntries { [it.key[16..-1], it.value] } ) - if (props) hcfg = new HikariConfig(props) } - - // Fall back to the WDIWTLT general database defaults - if (!hcfg) hcfg = wdiwtltDefaultConfig.hikariConfig - if (!hcfg) exitErr("Cannot load database configuation.") - - // Create our database instance - def dbapi - try { - logger.debug('Using datasource config:\n\t{}', hcfg.dataSourceProperties) - def hds = new HikariDataSource(hcfg) - dbapi = new DbApi(hds) } - catch (Exception e) { - logger.error( - "Could not establish a connection to the database:\n\t{}\n", - e.localizedMessage, e) - exitErr("Could not establish a connection to the database:\n\t" + - e.localizedMessage) } - - // Look to see if we've been asked to sync with a remote DB - if (opts['--sync'] || opts['--sync-only'] || - givenCfg['sync']?.toLowerCase() == 'true') { - - // Get the remote database - HikariConfig remoteHcfg - File remoteDbCfgFile = new File( - opts["--remote-database-config"] ?: - givenCfg['sync.database.config.file'] ?: - "database.remote.properties") - - // Try to load from the given DB config file - if (remoteDbCfgFile && remoteDbCfgFile.exists() && - remoteDbCfgFile.isFile()) { - Properties props = new Properties() - try { remoteDbCfgFile.withInputStream { props.load(it) } } - catch (Exception e) { props.clear() } - - if (props) remoteHcfg = new HikariConfig(props) } - - // Look to see if database connection properties are given in the CLI - // config file. - if (givenCfg && givenCfg["sync.database.config.dataSourceClassName"]) { - Properties props = new Properties() - props.putAll(givenCfg - .findAll { it.key.startsWith("sync.database.config.") } - .collectEntries { [it.key[21..-1], it.value] } ) - if (props) remoteHcfg = new HikariConfig(props) } - - def remoteDbApi - try { - logger.debug('Using remote datasource config:\n\t{}', - remoteHcfg.dataSourceProperties) - def hds = new HikariDataSource(remoteHcfg) - remoteDbApi = new DbApi(hds) } - catch (Exception e) { - logger.error( - "Could not establish a connection to the remote database:\n\t{}\n", - e.localizedMessage, e) - exitErr("Could not establish a connection to the remote database:\n\t" + - e.localizedMessage) } - - // Sync the databases - try { - println 'Syncing database...' - dbapi.syncWith(remoteDbApi, - opts['--sync-pull'] || givenCfg['sync.pull']?.toLowerCase() == 'true', - opts['--sync-push'] || givenCfg['sync.push']?.toLowerCase() == 'true') - println 'Sync completed!' - if (opts['--sync-only']) System.exit(0) } - catch (Exception e) { - logger.error( "Unable to sync with remote database:\n\t{}\n", - e.localizedMessage, e) - exitErr("Unable to sync with remote database:\n\t" + - e.localizedMessage) } } - - // Try to discover the VLC native libraries - def vlcj - try { - - String vlcLibDir = opts['--vlc-lib-dir'] || givenCfg['vlc.lib.dir'] - //if (vlcLibDir) NativeLibrary.addSearchPath('libvlc', vlcLibDir) - new NativeDiscovery().discover() - vlcj = new AudioMediaListPlayerComponent() } - catch (Exception e) { - exitErr("Could not find the VLC libraries. Is VLC installed?\n\t" + - e.localizedMessage) } - - // Create our CLI object - try { - def cliInst = new CommandLineInterface(new MediaLibrary(dbapi, libRoot), - vlcj, System.in, System.out, givenCfg) - if (opts['--sync'] || givenCfg['sync']?.toLowerCase() == 'true') - cliInst.consoleReadBuffer.add('scan') - cliInst.repl() } - catch (Exception e) { e.printStackTrace(); exitErr(e.localizedMessage) } - finally { if (vlcj) vlcj.release() } - } - - public CommandLineInterface(MediaLibrary library, - AudioMediaListPlayerComponent vlcj, InputStream sin, OutputStream out, - Properties cliConfig) { - this.cliConfig = cliConfig - this.library = library - this.inStream = sin - this.outStream = out - - this.msgTimeout = cliConfig["message.timeout"] ?: 5000l - - setupTextStyles() - - library.clean() - - playQueue = library.save(new Playlist( - name: "CLI Queue ${sdf.format(new Date())}", - userCreated: false)) - - String user = System.getProperty('user.name') - String os = System.getProperty('os.name') - def bookmarks = library.getBookmarkByName("Last played for $user on $os.") - if (bookmarks) playBookmark = bookmarks[0] - else playBookmark = new Bookmark(name: "Last played for $user on $os.") - - playBookmark.playlistId = playQueue.id - playBookmark.playIndex = 0 - playBookmark.mediaFileId = null - - this.vlcj = vlcj - - def listener = new UniversalNoopImplementation() - listener.methods.playing = this.&playing - listener.methods.finished = this.&finished - listener.methods.stopped = this.&stopped - vlcj.mediaListPlayer.mediaPlayer.addMediaPlayerEventListener( - listener as MediaPlayerEventListener) - - this.consoleReaderThread = new Thread().start(this.&runReaderThread) - - } - - void setupTextStyles() { - titleStyle = new ANSI().color(Colors.WHITE, Colors.DEFAULT, true).toString() - normalStyle = new ANSI().resetText().toString() - promptStyle = new ANSI().color(Colors.YELLOW, Colors.DEFAULT, true).toString() - cmdStyle = new ANSI().color(Colors.YELLOW, Colors.DEFAULT, true).toString() - optStyle = new ANSI().color(Colors.GREEN, Colors.DEFAULT, true).toString() - statusStyle = new ANSI().color(Colors.CYAN, Colors.DEFAULT, false).toString() - playlistStyle = new ANSI().color(Colors.GREEN, Colors.DEFAULT, false).toString() - artistStyle = new ANSI().color(Colors.RED, Colors.DEFAULT, false).toString() - albumStyle = new ANSI().color(Colors.BLUE, Colors.DEFAULT, false).toString() - fileStyle = new ANSI().color(Colors.GREEN, Colors.DEFAULT, false).toString() - errorStyle = new ANSI().color(Colors.RED, Colors.DEFAULT, true).toString() - } - - private void runReaderThread() { - this.reader = new ConsoleReader(this.inStream, this.outStream) - this.reader.setPrompt(promptStyle + "> " + normalStyle) - - String line - while(this.running && !Thread.currentThread().isInterrupted()) { - line = reader.readLine() - if (line == null || line == "quit" || line == "exit") running = false - else { - consoleReadBuffer.add(line) - outStream.print(afterInput) - outStream.flush() } } } - - public void repl() { - this.running = true - String line = null - long nextBookmarkUpdate = 0 - - outStream.println "\n\n\n" - drawLeader() - - while(this.running) { - - if (new Date() > dismissMsgDate) { - resetStatus() - dismissMsgDate = new Date(Long.MAX_VALUE) } - - if (consoleReadBuffer.size() > 0) { - line = consoleReadBuffer.remove(0) - try { processInput(line) } - catch (CliErr cliErr) { - String errMsg = cliErr.message - logger.error(errMsg) - if (ANSI.strip(errMsg).length() > 80) { - printLongMessage(errorStyle + errMsg + normalStyle) } - else { - status.text = errorStyle + errMsg + normalStyle - dismissMsgDate = new Date(new Date().time + msgTimeout) } } - outStream.print eraseToEnd } - else { - drawLeader() - if (curMediaFile && - vlcj.mediaListPlayer.mediaPlayer.isPlaying() && - System.currentTimeMillis() > nextBookmarkUpdate) { - - playBookmark.playTimeMs = - vlcj.mediaListPlayer.mediaPlayer.time - library.save(playBookmark) - nextBookmarkUpdate = System.currentTimeMillis() + 2000 } - Thread.sleep(250) } } } - - private def processInput(String line) { - line = line.trim() - logger.debug("line: $line") - - if (line.indexOf(' and ') > 0 || line.indexOf(';')> 0) { - String[] cmds = line.split(/ and |;/) - return cmds.collect(this.&processInput) } - - String[] parts = line.split(' ', 2) - String command = parts[0]?.toLowerCase() - String rest = parts.size() == 2 ? parts[1]?.trim() : null - logger.debug("command: ${command}") - switch(command) { - // Misc/utility - case 'scan': return scanMediaLibrary() - case 'list': return processList(rest, currentSelection) - case 'select': return processSelect(rest, currentSelection) - case 'create': return processCreate(rest, currentSelection) - case 'delete': return processDelete(rest) - case 'play': return processPlay(rest, currentSelection) - case 'enqueue': return processEnqueue(rest, currentSelection) - case 'add': return processAdd(rest, currentSelection) - case 'remove': return processRemove(rest, currentSelection) - case 'tag': return processTag(rest, currentSelection) - case 'untag': return processTag(rest, currentSelection, false) - case 'randomize': return processRandomize(rest) - case 'clear': return processClear(rest) - case 'pause': return processPause() - case 'stop': return processStop() - case 'n': - case 'next': return processNext(rest) - case 'p': - case 'prev': return processPrev(rest) - case 'jump': return processJump(rest) - case 'ff': - case 'fastforward': return processFastForward(rest) - case 'rw': case 'rwd': - case 'rewind': return processRewind(rest) - case 'repeat': return processRepeat(rest) - case 'vol': - case 'volume': return processVolume(rest) - case 'help': return printLongMessage(processHelp(rest)) - - case 'debug': - outStream.println( - "\n\nConfig: \n" + - cliConfig.collect { "\t${it.key}: ${it.value}" } - .join("\n") + - "\n\n\n") - drawLeader(true) - return - - case 'q': case 'quit': case ':q': case 'exit': case '\u0004': - running = false - consoleReaderThread.interrupt() - return - - default: - err "Unrecognized command: '$line'" - drawLeader() - Thread.sleep(250) - break - } - } - - public MediaLibrary scanMediaLibrary() { - msg "Scanning media library..." - drawLeader() - def counts = library.rescanLibrary() - String scanResults = "\n\n--------------------\nScan complete:\n\t${counts.total} files total." - - if (counts.new) scanResults += "\n\t${counts.new} new files added." - if (counts.ignored) scanResults += "\n\t${counts.ignored} files ignored." - if (counts.absent) - scanResults += "\n\t${counts.absent} files in the database but not stored locally." - - printLongMessage(scanResults) - resetStatus() - return library } - - private String processList(String options, def selection) { - logger.debug("Listing things. Options: $options") - - if (options == 'bookmarks') selection = library.getBookmarks() - else if (options != 'selection') selection = select(options, selection) - - if (!selection) err "Nothing selected." - else return printLongMessage(makeList(selection, - { "${it.id.toString()[0..<6]}: ${it} " })) } - - private List processSelect(String options, List selection) { - currentSelection = select(options, selection) - if (!currentSelection) msg 'Nothing selected.' - else resetStatus() - logger.debug("currentSelection: $currentSelection") - return currentSelection } - - private List select(String options, List selection = null) { - - logger.debug("Selecting: {}\tselection: {}", options, selection) - List excludedTags = [] - List selectedTags = [] - Class modelClass - - switch (options) { - case ~/selection/: return selection; - case ~/playing ($selectableModels)s?/: - if (!curMediaFile) err "No media is currently playing." - - modelClass = modelClasses[Matcher.lastMatcher[0][1]] - logger.debug("modelClass: {}\tcurMediaFileId: {}", modelClass, curMediaFile.id) - if (modelClass == MediaFile) return [curMediaFile] - else if (modelClass == Playlist) return playQueue - else return library.getWhere(modelClass, - [mediaFileId: curMediaFile.id]) - - case ~/(\d+ )?random ($selectableModels)s?( from (.+$))?/: - modelClass = modelClasses[Matcher.lastMatcher[0][2]] - def sourceCriteria = Matcher.lastMatcher[0][4]?.trim() - int count = (Matcher.lastMatcher[0][1] ?: 1) as int - - def source - if (sourceCriteria) { - source = select(sourceCriteria, selection) - if (modelClass != source[0].class) { - source = source.collectMany { library.getWhere(modelClass, - [(idKeyFor(source[0].class)): it.id]) } - .findAll().unique() } } - - else source = library.getAll(modelClass); - - if (source.size() < count) - err "There are not ${count} ${toEnglish(modelClass)}s to select." - - def selected = [] - (0.. where ... is not yet implemented." - - case ~/($selectableModels)s? from (.+)/: - modelClass = modelClasses[Matcher.lastMatcher[0][1]] - def sourceCriteria = Matcher.lastMatcher[0][2].trim() - def models = select(sourceCriteria, selection) - - if (modelClass != models[0].class) { - models = models.collectMany { library.getWhere(modelClass, - [(idKeyFor(models[0].class)): it.id]) } - .findAll().unique() } - - return models; - - case ~/($selectableModels)s((\s\d+)+)/: - modelClass = modelClasses[Matcher.lastMatcher[0][1]] - return Matcher.lastMatcher[0][2].split(/\s/) - .collect { safeToInteger(it) }.findAll() - .collect { library.getById(modelClass, it) }.findAll() - - case ~/($selectableModels) (.+)/: - modelClass = modelClasses[Matcher.lastMatcher[0][1]] - String nameOrId = Matcher.lastMatcher[0][2] - return [ensureExactlyOne( - library.getByIdOrName(modelClass, nameOrId))] - - case ~/($selectableModels)s/: - modelClass = modelClasses[Matcher.lastMatcher[0][1]] - return library.getAll(modelClass) - - case 'queue': return [playQueue] - case ~/queued ($selectableModels)s?/: - modelClass = modelClasses[Matcher.lastMatcher[0][1]] - return library.getWhere(modelClass, [playlistId: playQueue.id]) - case ~/untagged files/: return library.untaggedFiles - default: invalidOptionsErr('select') } } - - private def processCreate(String options, List selection = null) { - logger.debug("Creating something. Options: $options") - - String name - - switch (options) { - case ~/bookmark named (.+) on playlist (.+) at (.+)/: - Playlist p = getExactlyOne( - Playlist, Matcher.lastMatcher[0][2].trim()) - MediaFile mf = getExactlyOne( - MediaFile, Matcher.lastMatcher[0][3].trim()) - - Bookmark b = new Bookmark(name: Matcher.lastMatcher[0][1].trim(), - playlistId: p.id, mediaFileId: mf.id) - - if (!p.userCreated) { - p.userCreated = true - p = library.save(p) } - b = library.save(b) - msg "New bookmark: ${b.id}: ${b.name}" - return b - - case ~/bookmark named (.+)/: - if (!curMediaFile) err 'Nothing currently playing to bookmark.' - Bookmark b = playBookmark.clone() - b.name = Matcher.lastMatcher[0][1].trim() - b.id = null; - - Playlist p = library.getById(Playlist, b.playlistId) - p.userCreated = true - p = library.save(p) - b = library.save(b) - msg "New bookmark: ${b.id}: ${b.name}" - return b - - case ~/playlist named (.+) from (queue|selection|.+)/: - Playlist p = library.save( - new Playlist( name: Matcher.lastMatcher[0][1].trim())) - - if (Matcher.lastMatcher[0][2] != 'selection') - selection = select(Matcher.lastMatcher[0][2], selection) - - library.addToPlaylist(p.id, - library.collectMediaFiles(selection).collect { it.id }) - - msg "New playlist: ${p.id}: ${p.name}" - return p - - case ~/playlist named (.+)/: - Playlist p = new Playlist( - name: Matcher.lastMatcher[0][1].trim()) - - p = library.save(p) - msg "New playlist: ${p.id}: ${p.name}" - return p - - default: invalidOptionsErr('create') } } - - private def processDelete(String options) { - switch (options) { - case ~/playlist (.+)/: - Playlist p = getExactlyOne( - Playlist, Matcher.lastMatcher[0][1].trim()) - return library.delete(p) - case ~/bookmark (.+)/: - Bookmark b = getExactlyOne( - Bookmark, Matcher.latMatcher[0][1].trim()) - return library.delete(b) - default: invalidOptionsErr('delete') } } - - private Playlist processPlay(String options, List selection) { - - switch (options) { - case null: selection = null - case ~/selection/: break - case ~/bookmark (.+)/: - String nameOrId = Matcher.lastMatcher[0][1] - Bookmark b = ensureExactlyOne( - library.getByIdOrName(Bookmark, nameOrId)) - if (!b) err "No bookmark matches '$nameOrId'." - - Playlist p = library.getPlaylistById(b.playlistId) - if (!p) err 'The playlist for this bookmark no longer exists.' - - setPlayQueue(p) - vlcj.mediaListPlayer.playItem(b.playIndex) - - if (b.playTimeMs > 0) - vlcj.mediaListPlayer.mediaPlayer.time = b.playTimeMs - - b.lastUsed = new Timestamp(new Date().time); - library.update(b) - - return p; - - default: selection = select(options, selection) } - - if (selection) { - List mediaFiles = library.collectMediaFiles(selection) - playQueue = library.removeAllFromPlaylist(playQueue.id) - playQueue = library.addToPlaylist(playQueue.id, mediaFiles.collect { it.id }) - setPlayQueue(playQueue) } - - vlcj.mediaListPlayer.play() - return playQueue } - - private List processEnqueue(String options, - List selection = null) { - if (options != 'selection') selection = select(options, selection) - if (!selection) err "Nothing is selected." - List enqueued = enqueue(selection) - msg "${enqueued.size()} files added to the current play queue." - return enqueued } - - private List enqueue(List items) { - if (!items) return playQueue - - List mediaFiles = library.collectMediaFiles(items) - library.addToPlaylist(playQueue.id, mediaFiles.collect { it.id }) - mediaFiles.each { - vlcj.mediaList.addMedia( - new File(library.libraryRoot, it.filePath).canonicalPath) } - - return mediaFiles } - - private List processAdd(String options, - List selection = null) { - - Playlist p - def m = (options =~ /(.+) to playlist (.+)/) - p = getExactlyOne(Playlist, m[0][2]) - if (!p) err "No playlist for '${Matcher.lastMatcher[0][1]}'." - - if (m[0][1] != "selection") selection = select(options, selection) - - if (!selection) err 'Nothing selected to add.' - - List mediaFiles = library.collectMediaFiles(selection) - library.addToPlaylist(p.id, mediaFiles.collect { it.id }) - msg "${mediaFiles.size()} media files added to '${p.name}'." - return added } - - private def removeFromSelection(List toRemoveSel, - List selection) { - if (!toRemoveSel) err 'Nothing was selected to be removed.' - if (!selection) err 'Selection is already empty.' - - if (selection[0].class == toRemoveSel[0].class) { - return selection - toRemoveSel } - - List selectionFiles = library.collectMediaFiles(selection) - List toRemoveFiles = library.collectMediaFiles(toRemoveSel) - return selectionFiles - toRemoveFiles } - - private def processRemove(String options, List selection = null) { - - def m = (options =~ /(.+) from (.+)/) - String removeFrom = m[0][2] - List toRemoveSel - - if (m[0][1] == 'selection') toRemoveSel = selection - else toRemoveSel = select(m[0][1], selection) - - if (!toRemoveSel) err 'Nothing was selected to be removed.' - - if (removeFrom == 'selection') { - currentSelecton = removeFromSelection(toRemoveSel, selection) - msg "Removed from selection." - return } - - else { - Playlist p - if (removeFrom == 'queue') p = playQueue - else if (removeFrom.startsWith('playlist')) { - String[] parts = removeFrom.split(/\s/, 2) - if (parts.size() < 2) err 'No playlist id or name given.' - p = getExactlyOne(Playlist, parts[1]) } - - List toRemoveFiles = library.collectMediaFiles(toRemoveSel) - toRemoveFiles.each { library.removeFromPlaylist(p.id, it.id) } - - // Reset our queue if we removed from the queue - if (removeFrom == 'queue') { - vlcj.mediaListPlayer.stop() - setPlayQueue(playQueue) - - // Restart playback with the file that was playing before we - // removed stuff (may not be there anymore) - if (playBookmark) { - MediaFile mf = library.getMediaFileById(playBookmark.mediaFileId) - List playlistMFs = library.getMediaFilesWhere( - playlistId: playQueue.id) - int index = playlistMFs.indexOf(mf) - - if (index > 0) { - vlcj.mediaListPlayer.playItem(index) - - if (playBookmark.playTimeMs > 0) { - vlcj.mediaListPlayer.mediaPlayer.time = - playBookmark.playTimeMs } } } } - - msg "Removed ${toRemoveFiles.size()} files." - return toRemoveFiles } } - - private def processRandomize(String options) { - Playlist p - switch (options) { - case 'queue': p = playQueue; break - case ~/playlist (.+)/: - String idOrName = Matcher.lastMatcher[0][1] - p = getExactlyOne(Playlist, idOrName) - break - default: invalidOptionsErr('randomize') } - - library.randomizePlaylist(p) } - - private def processReorder(String options) { - Playlist p - int startingIdx = -1 - - String[] parts = options.split(/ as | starting at /) - - if (parts.size() < 2) invalidOptionsErr('reorder'); - - // Playlist or queue? - switch (parts[0].trim()) { - case 'queue': p = playQueue; break - case ~/playlist (.+)/: - String idOrName = Matcher.lastMatcher[0][1] - p = getExactlyOne(Playlist, idOrName) - break - default: invalidOptionsErr('reorder') } - - // What files in order? - List playlistFiles = library.getMediaFilesWhere(playlistId: p.id) - String[] fileIds = parts[1].split(/[,;\s]/) - List files = fileIds.collect { library.getMediaFileById(it) } - .findAll() - - if (parts.size() == 3) startingIdx = safeToInteger(parts[2]) - } - - private def processTag(String options, List selection, - boolean addTags = true) { - - String[] parts = options.split(' as ', 2) - - List tags - - // Short form: tag ... - if (parts.size() == 1) { - if (!curMediaFile) err 'Nothing currently playing to tag.' - selection = [curMediaFile] - tags = parts[0].split(/\s/).collect { it?.trim() }.findAll() } - - else { - if (parts[0] != 'selection') selection = select(parts[0], selection) - tags = parts[1].split(/\s/).collect { it?.trim() }.findAll() } - - List mediaFiles = library.collectMediaFiles(selection) - - if (addTags) library.tagMediaFiles(mediaFiles.collect { it.id }, tags) - else library.untagMediaFiles(mediaFiles.collect {it.id}, tags) - - msg "${addTags ? 'Tagged' : 'Untagged'} ${mediaFiles.size()} files as $tags." - return mediaFiles } - - private def processClear(String options) { - - switch(options) { - case null: - print(new ANSI().eraseDisplay(Erase.All))//.cursorPosition(4, 0)) - drawLeader(true) - return - case 'queue': - playQueue = library.removeAllFromPlaylist(playQueue.id) - setPlayQueue(playQueue) - return playQueue - case ~/playlist (.+)/: - Playlist p = getExactlyOne(Playlist, Matcher.lastMatcher[0][1]) - if (!p) err "No playlist for '${Matcher.lastMatcher[0][1]}'." - return library.removeAllFromPlaylist(p.id) - case 'selection': currentSelection = []; resetStatus(); break - default: - err "Unrecognized option to the ${cmdStyle}clear" + - "${normalStyle} command. Use ${cmdStyle}help clear" + - "${normalStyle} to see a list of valid options." } } - - private def processPause() { vlcj.mediaListPlayer.pause() } - - private def processStop() { vlcj.mediaListPlayer.stop() } - - private def processNext(String rest) { - def count - try { count = rest ? rest as int : 1 } - catch (Exception e) { err "$count is not a valid number" } - vlcj.mediaListPlayer.stop() - if ((playBookmark.playIndex + count) < vlcj.mediaList.size()) - vlcj.mediaListPlayer.playItem(playBookmark.playIndex + count) } - - private def processPrev(String rest) { - def count - try { count = rest ? rest as int : 1 } - catch (Exception e) { err "$count is not a valid number" } - vlcj.mediaListPlayer.stop() - if ((playBookmark.playIndex - count) >= 0) - vlcj.mediaListPlayer.playItem(playBookmark.playIndex - count) } - - private def processJump(String options) { - MediaFile target = getExactlyOne(MediaFile, options) - if (!target) err "No media file matches '${options}'." - - int index = library.getMediaFilesWhere(playlistId: playQueue.id) - .indexOf(target) - if (index < 0) err "'$target' is not in the current play queue." - - vlcj.mediaListPlayer.mediaPlayer.stop() - vlcj.mediaListPlayer.playItem(index) } - - private def processFastForward(String rest) { - String[] parts = rest.split(' ') - String strAmount = parts.size() > 0 ? parts[0]?.trim() : null - String unit = parts.size() > 1 ? parts[1]?.trim() : null - - if (!strAmount) { strAmount = "10"; unit = "s" } - if (!unit) unit = 's' - - long amount - try { amount = strAmount as long } - catch (Exception e) { err "$strAmount must be an integer." } - - switch (unit) { - case 'ms': case 'millis': case 'millisecond': case 'milliseconds': - vlcj.mediaListPlayer.mediaPlayer.skip(amount) - break - case 's': case 'sec': case 'second': case 'seconds': - vlcj.mediaListPlayer.mediaPlayer.skip(amount * 1000) - break - case 'm': case 'min': case 'minute': case 'minutes': - vlcj.mediaListPlayer.mediaPlayer.skip(amount * 60000) - break - default: err "$unit must be one of 'milliseconds' " + - "(or 'millis' or 'ms'), 'seconds' (or 'sec' or 's'), or " + - "'minutes' (or 'min' or 'm')" } } - - private def processRewind(String rest) { - String[] parts = rest.split(' ') - String strAmount = parts.size() > 0 ? parts[0]?.trim() : null - String unit = parts.size() > 1 ? parts[1]?.trim() : null - - if (!strAmount) { strAmount = "10"; unit = "s" } - if (!unit) unit = 's' - - long amount - try { amount = -(strAmount as int) } - catch (Exception e) { err "$strAmount must be an integer." } - - switch (unit) { - case 'ms': case 'millis': case 'millisecond': case 'milliseconds': - vlcj.mediaListPlayer.mediaPlayer.skip(amount) - break - case 's': case 'sec': case 'second': case 'seconds': - vlcj.mediaListPlayer.mediaPlayer.skip(amount * 1000) - break - case 'm': case 'min': case 'minute': case 'minutes': - vlcj.mediaListPlayer.mediaPlayer.skip(amount * 60000) - break - default: err "$unit must be one of 'milliseconds' " + - "(or 'millis' or 'ms'), 'seconds' (or 'sec' or 's'), or " + - "'minutes' (or 'min' or 'm')" } } - - private def processRepeat(String option) { - switch(option) { - case null: - err 'Reading current repeat mode is not yet implemented.' - /*def mode = vlcj.mediaListPlayer.mode - msg("Repeat mode is: " + - mode == MediaListPlayerMode.LOOP ? 'all.' : - (mode == MediaListPlayerMode.REPEAT ? 'one.' : 'off.'))*/ - return - - case 'off': - vlcj.mediaListPlayer.mode = MediaListPlayerMode.DEFAULT - msg "Repeat set to off." - break - case 'all': - vlcj.mediaListPlayer.mode = MediaListPlayerMode.LOOP - msg "Repeat set to all." - break - case 'one': - vlcj.mediaListPlayer.mode = MediaListPlayerMode.REPEAT - msg "Repeat set to one." - break - default: invalidOptionsErr('repeat') } } - - private def processVolume(String rest) { - int percentage - - if (rest) { - try { percentage = Math.min(Math.max(0, rest as int), 200) } - catch (Exception e) { - err "Volume must be and integer between 0 and 200." - return } - vlcj.mediaListPlayer.mediaPlayer.volume = percentage } - - msg "Volume: ${vlcj.mediaListPlayer.mediaPlayer.volume}" } - - private String processHelp(String options) { - switch(options) { - - // Top-level help - // -------------- - case null: case '': return """\ -Available commands: - - scan Re-scan the media library for new files. - list List items (albums, artists, files, etc). - select Select things (into the select buffer, no the play queue). - play Play requested media. - enqueue Enqueue requested media to the end of the play queue. - create Create a playlist or bookmark. - add Append requested media to the end of a playlist. - remove Remove requested from the play queue or playlist. - randomize Re-order the files in a play queue or a playlist randomly. - reorder Re-order the files in a play queue or a playlist. - tag Associate tags with requested media. - untag Remove the association between the given tags and media. - clear Clear the selection, play queue, playlist, or screen. - pause Pause media playback. - stop Stop media playback. - next Move forward in the play queue. - prev Move backward in the play queue. - jump Jump to a given media file in the play queue. - ff Jump ahead in the playback of the current media. - rw Jump back in the playback of the current media. - volume Set or retrieve the volume of the media player. - help Show help information about commands. - quit Quit this program. - -A new user is advised to read the help section for the ${cmdStyle}select${normalStyle} command. - -For a quick list of options, consider reading the ${cmdStyle}summary${normalStyle} section. -""" - - // SCAN - // -------------- - case 'scan': return """\ -${commandStyle}scan${normalStyle} - - Scan the media library for new files. - -""" - - // LIST - // -------------- - case 'list': return """\ -${cmdStyle}list bookmarks${normalStyle} List all bookmarks. -${cmdStyle}list selection${normalStyle} List the currently selected items. -${cmdStyle}list ${normalStyle} Make a selection using the ${cmdStyle}select${normalStyle} syntax and then - list the selection. - -""" - - // SELECT - // -------------- - case 'select': return """\ -${cmdStyle}select playing { album | artist | file | playlist | tag }${normalStyle} - - Select the currently playing items into the selection buffer. Specifically, - this selects items that are associated with the currently playing media - file. - -${cmdStyle}select queued { albums | artists | files | playlists | tags }${normalStyle} - - Select the items currently in the queue. - -${cmdStyle}select random {albums | artists | files | playlists | tags }${normalStyle} - - Select one or more items randomly. - -${cmdStyle}select random {albums | artists | files | playlists | tags } from ${normalStyle} - - Make a selection, then select one or more items randomly from it. - -${cmdStyle}select selected { album | artist | file | playlist | tag }${normalStyle} - - Select the items associated with the current selection buffer into the - selection buffer. This is useful to change the type of the selection. For - example, the following commands would select all of the albums in the - library by Bob Dylan: - - select artist Bob Dylan - select selected albums - - Another example, to select media files based on a set of tags: - - select tags instrumental orchestral - select selected files - - This example would select all files that are tagged as *either* - instrumental or orchestral - -${cmdStyle}select absent files${normalStyle} - - Select al media files which have entries in the database but are not - actually present locally on disk. - -${cmdStyle}select files tagged as ...${normalStyle} - - Select all media files tagged with the given tags. If multiple tags are - given then only files which have all the given tags are selected. In - contrast to the previous example: - - select files tagged as instrumental orchestral - - This selects all files that are tagged as *both* instrumental and - orchestral. - -${cmdStyle}select untagged files${normalStyle} - - Select all media files that do not have any tags associated with them. - - -${cmdStyle}select { album | artist | file | playlist | tag } where ...${normalStyle} - - ${errorStyle}Not yet implemented.${normalStyle} - -${cmdStyle}select { album | artist | file | playlist | tag } ${normalStyle} - - Select a single item by ID or by name. When selecting by name, the name can - include spaces and can be a substring of the whole name ("Lonely Hearts" - for "Sgt. Pepper's Lonely Hearts Club Band" for example, quotations not - required). - -${cmdStyle}select { albums | artists | files | playlists | tags } ...${normalStyle} - - Select multiple items by ID. Multiple IDs can be given, separated by - spaces. If no IDs are given, all of the items are returned. - -${cmdStyle}queue${normalStyle} - - Select the current play queue - -""" - - // CREATE - // -------------- - case 'create': return """\ -${cmdStyle}create bookmark named ${normalStyle} - - Create a new bookmark at the current play position in the currently playing - playlist. - -${cmdStyle}create bookmark named on playlist at ${normalStyle} - - Create a bookmark on the named media file in the named playlist. - -${cmdStyle}create playlist named ${normalStyle} - - Create a new playlist. - -${cmdStyle}create playlist named from { queue | selection}${normalStyle} - - Create a new playlist and populate it with the contents of either the - current play queue or the current selection. -""" - - // DELETE - // -------------- - case 'delete': return """\ -${cmdStyle}delete playlist -delete bookmark ${normalStyle} - - Delete a playlist or bookmark. -""" - - // PLAY - // -------------- - case 'play': return """\ -${cmdStyle}play${normalStyle} - - With no options, play the current file (inverse of pause). - -${cmdStyle}play selection${normalStyle} - - Clear the play queue, enqueue the current selection, and begin playback. - -${cmdStyle}play bookmark ${normalStyle} - - Load the bookmarked playlist as the play queue and begin playback at the - bookmarked media file. - -${cmdStyle}play ${normalStyle} - - Make a selection using the ${cmdStyle}select${normalStyle} syntax and then play the selection. - -""" - - // ENQUEUE - // -------------- - case 'enqueue': return """\ -enqueue selection - - Add the media files for the selected items to the end of the current play - queue. - -enqueue - - Make a selection using the ${cmdStyle}select${normalStyle} syntax and then enqueue - the selection. - -""" - - // ADD - // -------------- - case 'add': return """\ -add selection to playlist - - Lookup a playlist by id or name (including partial match) then add the - media files for the selected items to the end of that playlist. - -add to playlist - - Lookup a playlist by id or name (including partial match), select a set of - media files using the ${cmdStyle}select${normalStyle} syntax, then add the media files - for the selected items to the end of that playlist. - -""" - - // REMOVE - // -------------- - case 'remove': return """\ -remove selection from { queue | selection } -remove selection from playlist - - Remove the media files for the current selection from the current - selection, the current play queue, or from a playlist looked up by ID or - name (including partial match). - -remove from { queue | selection } -remove from playlist - - Make a selection using the ${cmdStyle}select${normalStyle} syntax then remove those media - files from either the current selection, the play queue, or from a - playlist looked up by ID or name (including partial match). - -""" - - // RANDOMIZE - // -------------- - case 'randomize': return """\ -randomize queue -randomize playlist - - Randomize the order of all elements in either the current play queue or the - named playlist. - -""" - - // REORDER - // -------------- - case 'reorder': return """\ -reorder queue move to -reorder playlist move to - - Move a single file from its current position to the named position in the - play queue/playlist. - -reorder queue as ... starting at -reorder playlist as ... starting at - - Take a set of files in the playlist, reorder them into given order, and - move them to the given starting position (defaults to the end of the - playlist) - -""" - - // TAG - // -------------- - case 'tag': return """\ -tag ... - - Tag the currently playing file with the given tags. Multiple tags may be - provided, separated by spaces (tags cannot include spaces). - -tag selection as ... - - Tag all of the media files in the current selection with the given tags. - Multiple tags may be provided separated by spaces (tags cannot include - spaces). - -tag as ... - - Make a selection using the ${cmdStyle}select${normalStyle} syntax then tag all of the - media files in the selection with the given tags. Multiple tags may be - provided separated by spaces (tags cannot include spaces). - -""" - // UNTAG - // -------------- - case 'untag': return """\ -untag ... - - Remove the given tags from the currently playing file. Multiple tags may be - provided, separated by spaces (tags cannot include spaces). - -tunag selection as ... - - Remove the given tags from all of the media files in the current selection. - Multiple tags may be provided separated by spaces (tags cannot include - spaces). - -taung as ... - - Make a selection using the ${cmdStyle}select${normalStyle} syntax then remove the - given tags from all of the media files in the selection. Multiple tags may - be provided separated by spaces (tags cannot include spaces). - -""" - - // CLEAR - // -------------- - case 'clear': """\ -clear Clear the terminal display. -clear queue Clear the play queue. -clear selection Clear the selection buffer. -clear playlist Clear the given playlist - -""" - case 'pause': return 'pause Pause playback.' - case 'stop': return 'stop Stop playback' - case 'next': return """\ -next Move forward in the play queue by items. is - optional and defaults to 1 - -""" - - case 'prev': return """\ -prev Move backward in the play queue by items. is - optional and defaults to 1 - -""" - - case 'jump': return """\ -jump to - - Find the given media file by ID or name in the current play queue and - resume playback starting from that file. - -""" - - case 'ff': case 'fastforward': return """\ -ff - - Jump forward in the playback of the current media by specified in - s. must be an integer. may be one of: 'millisecons', - 'seconds', or 'minutes'. The following abbreviations are allowed: 'ms', - 'millis', 's', 'sec', 'm', 'min'. - -""" - - case 'rw': case 'rwd': case 'rewind': return """\ -rw - - Jump backward in the playback of the current media by specified in - s. must be an integer. may be one of: 'millisecons', - 'seconds', or 'minutes'. The following abbreviations are allowed: 'ms', - 'millis', 's', 'sec', 'm', 'min'. - -""" - - case 'repeat': return """\ -repeat { all | one | none } - - Set the playlist repeat mode to: - all: repeat the entire play queue - one: loop the currently playing song - none: do not repeat. -""" - - case 'vol': case 'volume': return """\ -volume Display the current volume setting. -volume Set the volume. may be any value from 0 to 200. - -""" - - case 'help': return """\ -help summary Display a quick summary of available commands. -help Display detailed information about the given command.""" - - - case 'summary': return """\ -Selecting files: - - select { album | artist | file | playlist | tag } - select { albums | artists | files | playlists | tags } where - select files tagged as ... and not as ... - select playing { albums | artists | files | playlists | tags } - select queued { albums | artists | files | playlists | tags } - select absent files - - list selection - list - list selected { albums | artists | files | playlists | tags } - -Play and Controlling Media: - - play - play selection - play bookmark - play - - next (alias: n) - prev (alias: p) - - repeat { all | one | off } - stop - pause - fasforward (aliases: ff, fwd) - rewind (aliases: rw, rwd) - jump - -Playlist and Queue Management: - - enqueue selection - enqueue - - add selection to playlist to playlist - add to playlist - - remove selection from playlist - remove from playlist - remove selection from { queue | selection } - remove from { queue | selection } - - create bookmark named - create bookmark named on playlist at - create playlist named from { selection | queue } - create playlist named - - copy playlist as - create bookmark - update bookmark - - delete playlist - delete bookmark - - randomize { queue | playlist | selection } - - reorder queue move to - reorder playlist move to - reorder queue as ... - reorder playlist as ... - - clear - clear queue - clear selection - clear playlist - -Library Management: - - scan -""" - - default: - err "Unrecognized command: '$options'" - drawLeader() - Thread.sleep(250) - break } } - - private String invalidOptionsErr(String commandName) { - err "Invalid options to the ${cmdStyle}${commandName}${normalStyle}" + - " command. Use ${cmdStyle}help ${commandName}${normalStyle} to " + - "see a list of valid options." } - - private void playing(def player) { - try { - def mediaFiles = library.getMediaFilesWhere(playlistId: playQueue.id) - String absFilePath = - URLDecoder.decode(vlcj.mediaListPlayer.currentMrl()[7..-1]) - - def currentIdx = mediaFiles.findIndexOf { - def mrlPath = new File(absFilePath).canonicalPath - def mfPath = new File(library.libraryRoot, it.filePath).canonicalPath - return mfPath == mrlPath } - - curMediaFile = mediaFiles[currentIdx] - - currentlyPlaying.text = makeFullMediaFileDescription(curMediaFile) - - playBookmark.playlistId = playQueue.id - playBookmark.playIndex = currentIdx - playBookmark.mediaFileId = curMediaFile.id - - library.save(playBookmark) } - catch (Exception e) { printLongMessage(e.printStackTrace()) } } - - private void finished(def player) { - curMediaFile = library.incrementPlayCount(curMediaFile) - library.save(curMediaFile) } - - private void stopped(def player) { - curMediaFile = null - currentlyPlaying.text = "No media currently playing." } - - public void setPlayQueue(Playlist p) { - if (vlcj.mediaListPlayer.isPlaying()) vlcj.mediaListPlayer.stop() - p.lastUsed = new Timestamp(new Date().time) - playQueue = library.update(p) - - vlcj.mediaListPlayer.stop() - vlcj.mediaList.clear() - - library.getMediaFilesWhere(playlistId: playQueue.id).each { - vlcj.mediaList.addMedia( - new File(library.libraryRoot, it.filePath).canonicalPath) } } - - public def ensureExactlyOne(def matches) { - if (!matches) err "Nothing matches." - - String englishName = toEnglish(matches[0].class) - if (matches.size() > 1) err "Multiple ${englishName}s match:\n\t" + - matches.collect { "${it.id}: ${it.name}" }.join('\n\t') - - return matches[0] } - - public def getExactlyOne(Class modelClass, String nameOrId) { - return ensureExactlyOne(library.getByIdOrName(modelClass, nameOrId)) } - - private void drawLeader(afterOutput = false) { - - String leader = beforeLeader + getLeader() + - (afterOutput ? - (promptStyle + "> " + normalStyle) : - afterLeader) - - outStream.print(leader) - outStream.flush() } - - private String printLongMessage(String msg) { - String result = new StringBuilder() - .append(eraseLeader) - .append(msg) - .append("\n\n\n\n") - .toString() - - outStream.println result - drawLeader(true) - return result } - - private String getLeader() { - StringBuilder leader = new StringBuilder() - .append(clearLine) - .append(titleStyle) - .append("WDIWTLT - v") - .append(VERSION) - .append('\n') - - StringBuilder statusLine = new StringBuilder() - .append(statusStyle) - - if (playBookmark) statusLine.append('(') - .append(playBookmark.playIndex + 1) - .append('/') - .append(vlcj.mediaList.size()) - .append(') ') - - statusLine.append(currentlyPlaying) - - if (curMediaFile) { - long playTime = Math.floor(vlcj.mediaListPlayer.mediaPlayer.time / 1000) as long - int statusLength = ANSI.strip(statusLine.toString()).length() - statusLine.append((' ' * (displayWidth - (statusLength + 7)))) - .append(promptStyle) - .append(String.format('%02d', Math.floor(playTime / 60) as int)) - .append(':') - .append(String.format('%02d', Math.floor(playTime % 60) as int)) - .append(' ')} - - leader.append(clearLine) - .append(statusLine) - .append("\n") - .append(clearLine) - .append(normalStyle) - .append(status) - .append("\n") - - return leader.toString() } - - private String msg(String msg) { - status.text = msg - dismissMsgDate = new Date(new Date().time + msgTimeout) } - - private String makeFullMediaFileDescription(MediaFile mf) { - def artist = library.getArtistsWhere( mediaFileId: mf.id) - def album = library.getAlbumsWhere(mediaFileId: mf.id) - - StringBuilder s = new StringBuilder() - - if (artist) s.append(artistStyle) - .append(artist[0]) - .append(normalStyle) - .append(" / ") - - if (album) s.append(albumStyle) - .append(album[0]) - .append(normalStyle) - .append(" / ") - - s.append(fileStyle) - .append(mf) - .append(normalStyle) - .append(' ') - - return s.toString() } - - private String makeList(List items, Closure toString = null) { - - if (!items) return "No items to list." - - if (!toString) toString = { it.toString() } - - Class modelClass = items[0].class - def currentCollection - - if (curMediaFile) { - if (modelClass == MediaFile) currentCollection = [curMediaFile] - else currentCollection = library.getWhere( - modelClass, [mediaFileId: curMediaFile.id]) } - - def highlightSelected = { item -> - if (currentCollection && currentCollection.contains(item)) - return "${promptStyle}${toString(item)}${normalStyle}" - else return toString(item) } - - def result = new StringBuilder() - .append("--------------------\n${modelClass.simpleName}s:\n\n") - - result.append(items.collect(highlightSelected).join("\n")) - .append("\n") - - return result.toString() } - - private String resetStatus() { - - if (currentSelection) { - if (currentSelection.size() == 1) { - String s - Model m = currentSelection[0] - switch (m.class) { - case Album: s = "$albumStyle$m"; break - case Artist: s = "$artistStyle$m"; break - case Playlist: s = "$playlistStyle$m"; break - case MediaFile: - s = makeFullMediaFileDescription(currentSelection[0]) - break - case Tag: s = "Tag: $m" } - - status.text = s } - else status.text = "${currentSelection.size()} " + - "${toEnglish(currentSelection[0].class)}s selected." } - else status.text = "" - return status.text } - -} diff --git a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/ScrollText.groovy b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/ScrollText.groovy deleted file mode 100644 index e8f54d7..0000000 --- a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/ScrollText.groovy +++ /dev/null @@ -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.. 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 } -} diff --git a/core/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy b/core/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy deleted file mode 100644 index e7dca11..0000000 --- a/core/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy +++ /dev/null @@ -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 missingFiles = dbapi.getMediaFiles() - List 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 getArtistsByName(String name) { - return [dbapi.getArtistByName(name)] ?: dbapi.getArtistsLikeName(name) } - - public List getAlbumsByName(String name) { - return dbapi.getAlbumsWhere(name: name) ?: dbapi.getAlbumsLikeName(name) } - - public List collectMediaFiles(List 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 splitArtist(Artist toSplit, Pattern splitPattern) { - return splitArtist(toSplit, pattern.split(toSplit.name)) } - - public List splitArtist(Artist toSplit, List newNames) { - def albums = dbapi.getAlbumsWhere(artistId: toSplit.id) - def mediaFiles = dbapi.getMediaFilesByArtistId(toSplit.id) - - toSplit.name = newNames[0] - List 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 artistNames, List 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.. List getAll(Class modelClass, String orderClause = null) { - def query = new StringBuilder() - query.append('SELECT * FROM ') - .append(pluralize(nameFromModel(modelClass.simpleName))) - - if (orderClause) query.append(' ORDER BY ').append(orderClause) - - query = query.toString() - - logger.debug('Selecting models.\n\tSQL: {}', query) - return sql.rows(query).collect { recordToModel(modelClass, it) } } - - public List getAllIds(Class modelClass) { - String query = 'SELECT id FROM ' + - pluralize(nameFromModel(modelClass.simpleName)) - - logger.debug('Selecting model ids.\n\tSQL: {}', query) - return sql.rows(query).collect { it.id } } - - public M getById(Class modelClass, UUID id) { - def model = modelClass.newInstance() - model.id = id - return refresh(model) } - - public List getByIdLike(Class modelClass, - String partialId) { - String likeVal = partialId + '%' - String query = new StringBuilder() - .append('SELECT * FROM ') - .append(pluralize(nameFromModel(modelClass.simpleName))) - .append(' WHERE id LIKE ?') - .toString() - - logger.debug('Selecting models like id.\n\tSQL: {}\n\tPARAMS: {}', - query, likeVal) - return sql.rows(query, likeVal) - .collect { recordToModel(modelClass, it) } } - - public M refresh(M model) { - def query = new StringBuilder() - .append('SELECT * FROM ') - .append(pluralize(nameFromModel(model.class.simpleName))) - .append(' WHERE id = ?') - .toString() - - logger.debug('Selecting model.\n\tSQL: {}\n\tPARAMS: {}', query, model.id) - def result = sql.firstRow(query, [model.id]) - if (!result) return null - return updateModel(result, model) } - - public List getByName(Class modelClass, String name) { - def query = new StringBuilder() - .append('SELECT * FROM ') - .append(pluralize(nameFromModel(modelClass.simpleName))) - .append(' WHERE name = ?') - .toString() - - logger.debug('Selecting model.\n\tSQL: {}\n\tPARAMS: {}', query, name) - return sql.rows(query, [name]).collect { recordToModel(modelClass, it) } } - - public List getBy(Class modelClass, - List columns, List values) { - def query = new StringBuilder() - .append('SELECT * FROM ') - .append(pluralize(nameFromModel(modelClass.simpleName))) - .append(' WHERE ') - .append(columns.collect { it + ' = ?' }.join(' AND ')) - .toString() - - logger.debug('Selecting models.\n\tSQL: {}\n\tPARAMS: {}', query, values) - return sql.rows(query, values) - .collect { recordToModel(modelClass, it) } } - - public List getLike(Class modelClass, - List columns, List values) { - values = values.collect { "%$it%".toString() } - String query = new StringBuilder() - .append('SELECT * FROM ') - .append(pluralize(nameFromModel(modelClass.simpleName))) - .append(' WHERE ') - .append(columns.collect { """"$it" LIKE ?"""}.join(' AND ')) - .toString() - - logger.debug('Selecting models.\n\tSQL: {}\n\tPARAMS: {}', query, values) - return sql.rows(query, values) - .collect { recordToModel(modelClass, it) } } - - public List getWhere(Class modelClass, Map criteria) { - switch(modelClass) { - case Album: return getAlbumsWhere(criteria) - case Artist: return getArtistsWhere(criteria) - case Bookmark: return getBookmarksWhere(criteria) - case MediaFile: return getMediaFilesWhere(criteria) - case Playlist: return getPlaylistsWhere(criteria) - case Tag: return getTagsWhere(criteria) } } - - public Model save(Model model) { - if (model.id) return update(model) - else return create(model) } - - public Model update(Model model) { - def setClauses = [] - def params = [] - - getInstanceFields(model.class) - .findAll { it.name != 'id' } - .each { field -> - setClauses << '"' + nameFromModel(field.name) + '"= ?' - params << field.get(model) } - - String query = new StringBuilder() - .append('UPDATE ') - .append(pluralize(nameFromModel(model.class.simpleName))) - .append(' SET ') - .append(setClauses.join(', ')) - .append(' WHERE id = ?') - .toString() - - params << model.id - - return withTransaction { - logger.debug('Updating model.\n\tSQL: {}\n\tPARAMS: {}', query, params) - sql.executeUpdate(query, params) - return refresh(model) } } - - public Model create(Model model) { - def columns = [] - def params = [] - - if (!model.id) model.id = UUID.randomUUID() - getInstanceFields(model.class).each { field -> - //if (field.class.getAnnotation(Entity)) // check to see if we - // have nested models - columns << '"' + nameFromModel(field.name) + '"' - params << field.get(model) } - - def query= new StringBuilder() - .append('INSERT INTO ') - .append(pluralize(nameFromModel(model.class.simpleName))) - .append(' (') - .append(columns.join(', ')) - .append(') VALUES (') - .append((1..columns.size()).collect { '?' }.join(', ')) - .append(')').toString() - - logger.debug('Creating model.\n\tSQL: {}\n\tPARAMS: {}', query, params) - sql.executeInsert(query, params) - return model } - - public int delete(Model model) { - def query = new StringBuilder() - .append('DELETE FROM ') - .append(pluralize(nameFromModel(model.class.simpleName))) - .append(' WHERE id = ?') - .toString() - - logger.debug('Deleting model.\n\tSQL: {}\n\tPARAMS: {}', - query, model.id) - sql.execute(query, [model.id]) - return sql.updateCount } - - public def associate(Class modelClass1, - Class modelClass2 , UUID firstId, UUID secondId) { - String linkTable = pluralize(nameFromModel(modelClass1.simpleName)) + - '_' + pluralize(nameFromModel(modelClass2.simpleName)) - String col1 = nameFromModel(modelClass1.simpleName) + '_id' - String col2 = nameFromModel(modelClass2.simpleName) + '_id' - - return withTransaction { - def query = """\ - SELECT * FROM $linkTable - WHERE "${col1}" = ? AND "${col2}" = ?""" - def params = [firstId, secondId] - - // Look first for an existing association before creating one. - logger.debug('Selecting association.\n\tSQL: {}\n\tPARAMS: {}', - query, params) - if (sql.firstRow(query, params)) { return 0 } - else { - query = """INSERT INTO $linkTable ("$col1", "$col2") VALUES (?, ?)""" - logger.debug('Creating association.\n\tSQL: {}\n\tPARAMS: {}', - query, params) - return sql.execute(query, params) } } } - - public def associate(Model m1, Model m2) { - return associate(m1.class, m2.class, m1.id, m2.id) } - - /// ### Album-specific methods - // ======================================================================= - public Album getAlbumById(UUID id) { return getById(Album, id) } - - public List getAlbums() { return getAll(Album) } - - public List getAlbumsOrderedByName() { - return getAll(Album, 'name ASC') } - - public List getAlbumsLikeName(String name) { - return getLike(Album, ['name'], [name]) } - - public List getAlbumsWhere(Map params) { - def query = new StringBuilder() - def sqlParams = [] - - query.append('SELECT DISTINCT al.* FROM albums al ') - - if (params.artistId) { - query.append(' JOIN artists_albums aa ON al.id = aa.album_id ') - query.append(' AND aa.artist_id = ? ') - sqlParams << params.artistId } - - if (params.mediaFileId || params.playlistId) { - query.append(' JOIN albums_media_files amf ON ') - query.append(' al.id = amf.album_id ') - - if (params.mediaFileId) { - query.append(' AND amf.media_file_id = ? ') - sqlParams << params.mediaFileId } } - - if (params.playlistId) { - query.append(' JOIN playlists_media_files pmf ON ') - query.append(' amf.media_file_id = pmf.media_file_id AND ') - query.append(' pmf.playlist_id = ?') - sqlParams << params.playlistId } - - if (params.name || params.year) { query.append(' WHERE ') } - - if (params.year) { - query.append(' al.year = ? ') - sqlParams << params.year } - - if (params.name) { - if (params.year) query.append(' AND ') - query.append(' al.name = ? ') - sqlParams << params.name } - - query = query.toString() - logger.debug('Selecting albums.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - return sql.rows(query, sqlParams).collect { recordToModel(Album, it) } } - - public void removeEmptyAlbums() { - - String query = """\ - DELETE FROM albums WHERE id IN ( - SELECT DISTINCT al.id - FROM albums al LEFT OUTER JOIN albums_media_files almf ON - al.id = almf.album_id - WHERE almf.album_id IS NULL)""" - - logger.debug('Deleting empty albums.\n\tSQL: {}', query) - sql.execute(query) } - - /// ### Artist-specific methods - public List getArtists() { return getAll(Artist) } - public Artist getArtistById(UUID id) { return getById(Artist, id) } - - public Artist getArtistByName(String name) { - def artists = getByName(Artist, name) - if (artists) return artists[0] } - - public List getArtistsOrderedByName() { - return getAll(Artist, 'name ASC') } - - public List getArtistsWhere(Map params) { - def query = new StringBuilder() - def sqlParams = [] - - query.append('SELECT DISTINCT ar.* FROM artists ar ') - - if (params.albumId) { - query.append(' JOIN artists_albums aa ON ar.id = aa.artist_id ') - query.append(' AND aa.album_id = ? ') - sqlParams << params.albumId } - - if (params.mediaFileId || params.playlistId) { - query.append(' JOIN artists_media_files amf ON ') - query.append(' ar.id = amf.artist_id ') - - if (params.mediaFileId) { - query.append(' AND amf.media_file_id = ? ') - sqlParams << params.mediaFileId } } - - if (params.playlistId) { - query.append(' JOIN playlists_media_files pmf ON ') - query.append(' amf.media_file_id = pmf.media_file_id AND ') - query.append(' pmf.playlist_id = ?') - sqlParams << params.playlistId } - - if (params.name) { - query.append(' WHERE ar.name = ? ') - sqlParams << params.name } - - query = query.toString() - logger.debug('Selecting artists.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - return sql.rows(query, sqlParams).collect { recordToModel(Artist, it) } } - - public List getArtistsLikeName(String name) { - return getLike(Artist, ['name'], [name]) } - - public def addAlbumArtist(UUID albumId, UUID artistId) { - return associate(Artist, Album, artistId, albumId) } - - public void removeEmptyArtists() { - - String query = """\ - DELETE FROM artists WHERE id IN ( - SELECT DISTINCT ar.id - FROM artists ar LEFT OUTER JOIN artists_media_files armf ON - ar.id = armf.artist_id - WHERE armf.artist_id IS NULL)""" - - logger.debug('Deleting empty artists.\n\tSQL: {}', query) - sql.execute(query) } - - /// ### Bookmark-specific methods - public Bookmark getBookmarkById(UUID id) { return getById(Bookmark, id) } - - public List getBookmarkByName(String name) { - return getByName(Bookmark, name) } - - public List getBookmarks() { return getAll(Bookmark) } - public List getBookmarksOrderedByName() { - return getAll(Bookmark, 'name ASC') } - - public List getBookmarksWhere(Map params) { - def query = new StringBuilder() - def sqlParams = [] - def orderClauses = [] - def whereClauses = [] - - query.append('SELECT b.* FROM bookmarks b ') - - if (params.playlistId) { - whereClauses << 'b.playlist_id = ?' - sqlParams << params.playlistId } - - if (params.userCreated != null) { - whereClauses << 'b.user_created = ?' - sqlParams << params.userCreated } - - if (params.name) { - whereClauses << 'b.name = ?' - sqlParams << params.name } - - if (params.mediaFileId) { - whereClauses << 'b.media_file_id = ?' - sqlParams << params.mediaFileId } - - if (params.playIndex) { - whereClauses << 'b.play_index = ?' - sqlParams << params.playIndex } - - if (params.lastUsedBefore) { - whereClauses << 'b.last_used < ?' - sqlParams << params.lastUsedBefore } - - if (params.lastUsedAfter) { - whereClauses << 'b.last_used > ?' - sqlParams << params.lastUsedAfter } - - if (params.lastUsedBetween) { - whereClauses << 'b.last_used BETWEEN ? AND ?' - sqlParams.addAll(params.lastUsedBetween) } - - orderClauses << 'b.last_used DESC' - - if (whereClauses) - query.append(' WHERE ').append(whereClauses.join(' AND ')) - - if (orderClauses) - query.append(' ORDER BY ').append(orderClauses.join(', ')) - - query = query.toString() - logger.debug('Selecting bookmarks.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - return sql.rows(query, sqlParams) - .collect { recordToModel(Bookmark, it) } } - - /// ### Image-specific methods - public Image getImageById(UUID id) { return getById(Image, id) } - - /// ### MediaFile-specific methods - public MediaFile getMediaFileById(UUID id) { return getById(MediaFile, id) } - - public List getMediaFileByName(String name) { - return getByName(MediaFile, name) } - - public List getMediaFiles() { return getAll(MediaFile) } - - public List getMediaFilesLikeName(String name) { - return getLike(MediaFile, ['name'], [name]) } - - public MediaFile getMediaFileByFilePath(String filePath) { - def files = getBy(MediaFile, ['file_path'], [filePath]) - return files ? files[0] : null } - - public MediaFile getMediaFileByFileHash(String fileHash) { - def files = getBy(MediaFile, ['file_hash'], [fileHash]) - return files ? files[0] : null } - - public List getMediaFilesWhere(Map params) { - def query = new StringBuilder() - def sqlParams = [] - def joins = [] - def orderClauses = [] as LinkedList - def whereClauses = [] - - def newJoin - query.append('SELECT mf.* FROM media_files mf ') - - if (params.artistId || params.playlistId) { - joins << (newJoin = [ - table: 'artists_media_files armf', - clauses: ['mf.id = armf.media_file_id']]) - - if (params.artistId) { - newJoin.clauses << 'armf.artist_id = ?' - sqlParams << params.artistId } - else newJoin.type = 'LEFT' - - orderClauses << 'armf.artist_id ASC' } - - if (params.albumId || params.artistId || params.playlistId) { - joins << (newJoin = [ - table: 'albums_media_files almf', - clauses: ['mf.id = almf.media_file_id']]) - - if (params.albumId) { - newJoin.clauses << 'almf.album_id = ?' - sqlParams << params.albumId } - else newJoin.type = 'LEFT' - orderClauses << 'almf.album_id ASC' } - - if (params.playlistId) { - joins << (newJoin = [ - table: 'playlists_media_files pmf', - clauses: ['pmf.media_file_id = mf.id', 'pmf.playlist_id = ?']]) - orderClauses.addFirst('pmf.position') - sqlParams << params.playlistId } - - if (params.name) { - whereClauses << 'mf.name = ?' - sqlParams << params.name } - - if (params.discNumber) { - whereClauses << 'mf.disc_number = ?' - sqlParams << params.discNumber } - - if (params.trackNumber) { - whereClauses << 'mf.track_number = ?' - sqlParams << params.trackNumber } - - if (params.filePath) { - whereClauses << 'mf.file_path = ?' - sqlParams << params.filePath } - - if (params.fileHash) { - whereClauses << 'mf.file_hash = ?' - sqlParams << params.fileHash } - - if (params.containsKey('presentLocally')) { - whereClauses << 'mf.present_locally = ?' - sqlParams << params.presentLocally } - - if (params.tags) { - params.tags.eachWithIndex { tag, idx -> - String L = "mft${idx}"; - joins << (newJoin = [ - table: "media_files_tags $L", - clauses: ["${L}.media_file_id = mf.id", - "${L}.tag_id = ?"]]) - sqlParams << tag.id } } - - orderClauses << 'mf.disc_number' - orderClauses << 'mf.track_number' - - joins.each { j -> - if (j.type) query.append(' ').append(j.type).append(' ') - - query.append(' JOIN ') - .append(j.table) - .append(' ON ') - .append(j.clauses.join(' AND ')) } - - if (whereClauses) - query.append(' WHERE ').append(whereClauses.join(' AND ')) - - if (orderClauses) - query.append(' ORDER BY ').append(orderClauses.join(', ')) - - query = query.toString() - logger.debug('Selecting media files.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - return sql.rows(query, sqlParams).collect { - recordToModel(MediaFile, it) }.unique() } - - public List getUntaggedFiles() { - String query = """ - SELECT mf.* - FROM media_files mf LEFT JOIN - media_files_tags mft ON - mf.id = mft.media_file_id - WHERE mft.media_file_id IS NULL""" - - logger.debug('Finding untagged media files.\n\tSQL: {}', query) - return sql.rows(query).collect { recordToModel(MediaFile, it) } } - - public def associateMediaFileWithAlbum(UUID mediaFileId, UUID albumId) { - return associate(Album, MediaFile, albumId, mediaFileId) } - - public def associateMediaFileWithArtist(UUID mediaFileId, UUID artistId) { - return associate(Album, MediaFile, artistId, mediaFileId) } - - public def incrementPlayCount(UUID mediaFileId) { - def query = 'UPDATE media_files SET play_count = play_count + 1 WHERE ID = ?' - def params = [mediaFileId] - - logger.debug('Updating media file.\n\tSQL: {}\n\tPARAMS: {}', query, params) - sql.executeUpdate(query, params) - - query = 'SELECT play_count FROM media_files WHERE id = ?' - logger.debug('Selecting media file play count.\n\tSQL: {}\n\tPARAMS: {}', query, params) - return sql.firstRow(query, params)[0] } - - public MediaFile incrementPlayCount(MediaFile mf) { - mf.playCount = incrementPlayCount(mf.id) - return mf } - - /// ### Playlist-specific methods - public List getPlaylists() { return getAll(Playlist) } - public Playlist getPlaylistById(UUID id) { return getById(Playlist, id) } - - public List getPlaylistByName(String name) { - return getByName(Playlist, name) } - - public List getPlaylistsWhere(Map params) { - def query = new StringBuilder() - def sqlParams = [] - def orderClauses = [] - def whereClauses = [] - - query.append('SELECT DISTINCT p.* FROM playlists p ') - - if (params.albumId || params.artistId || params.mediaFileId) { - query.append(' JOIN playlists_media_files pmf ON ') - .append(' pmf.playlist_id = p.id ') - .append(' JOIN media_files mf ON ') - .append(' pmf.media_file_id = mf.id ') } - - if (params.mediaFileId) { - query.append(' AND mf.id = ? ') - sqlParams << params.mediaFileId } - - if (params.albumId) { - query.append(' JOIN albums_media_files almf ON ') - .append(' mf.album_id = almf.album_id AND almf.album_id = ? ') - sqlParams << params.albumId } - - if (params.artistId) { - query.append(' JOIN arists_media_files armf ON ') - .append(' mf.artist_id = armf.artist_id AND ') - .append(' armf.artist_id = ? ') - sqlParams << params.artistId } - - if (params.userCreated != null) { - whereClauses << 'p.user_created = ?' - sqlParams << params.userCreated } - - if (params.name) { - whereClauses << 'p.name = ?' - sqlParams << params.name } - - if (params.copiedFromId) { - whereClauses << 'p.copied_from_id = ?' - sqlParams << params.copiedFromId } - - if (params.lastUsedBefore) { - whereClauses << 'p.last_used < ?' - sqlParams << params.lastUsedBefore } - - if (params.lastUsedAfter) { - whereClauses << 'p.last_used > ?' - sqlParams << params.lastUsedAfter } - - if (params.lastUsedBetween) { - whereClauses << 'p.last_used BETWEEN ? AND ?' - sqlParams.addAll(params.lastUsedBetween) } - - orderClauses << 'p.last_used DESC' - - if (whereClauses) - query.append(' WHERE ').append(whereClauses.join(' AND ')) - - if (orderClauses) - query.append(' ORDER BY ').append(orderClauses.join(', ')) - - query = query.toString() - logger.debug('Selecting playlists.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - return sql.rows(query, sqlParams) - .collect { recordToModel(Playlist, it) } } - - public int delete(Playlist p) { - return withTransaction { - removeAllFromPlaylist(p.id) - delete((Model) p) } } - - public int getNextPlaylistPosition(UUID playlistId) { - String query = """\ - SELECT COALESCE(MAX(position), 0) + 1 - FROM playlists_media_files - WHERE playlist_id = ?""" - - logger.debug('Finding next playlist position.\n\tSQL: {}\n\tPARAMS: {}', - query, playlistId) - - return sql.firstRow(query, playlistId)[0] } - - private int incrementPlaylistPositions(UUID playlistId, int startPosition, - int incrementAmount) { - String query = """\ - UPDATE playlists_media_files - SET position = position + ? - WHERE playlist_id = ? AND position > ?""" - def params = [incrementAmount, playlistId, startPosition] - - logger.debug('Incrementing playlist positions.\n\tSQL: {}\n\tPARAMS: {}', - query, params) - return sql.executeUpdate(query, params) } - - public Playlist addToPlaylist(UUID playlistId, - List mediaFileIds, int startPosition = -1) { - - String query - def params - - return withTransaction { - def p = getById(Playlist, playlistId) - if (!p) return null - - if (startPosition < 0) - startPosition = getNextPlaylistPosition(playlistId) - else - incrementPlaylistPositions(playlistId, startPosition, - mediaFileIds.size()) - - def position = startPosition - mediaFileIds.each { mediaFileId -> - query = 'INSERT INTO playlists_media_files VALUES (?, ?, ?)' - params = [playlistId, mediaFileId, position++] - logger.debug( - 'Adding a new playlist/media file relation.\n\tSQL: {}\n\tPARAMS: {}', - query, params) - sql.executeInsert(query, params) } - - p.mediaFileCount += mediaFileIds.size() - p.lastUsed = new Timestamp(new Date().time) - return update(p) } } - - public Playlist addToPlaylist(UUID playlistId, UUID mediaFileId, - int position = -1) { - - String query - def params - - return withTransaction { - def p = getById(Playlist, playlistId) - if (!p) return null - - if (position < 0) position = getNextPlaylistPosition(playlistId) - else incrementPlaylistPositions(playlistId, position, 1) - - query = 'INSERT INTO playlists_media_files VALUES (?, ?, ?)' - params = [playlistId, mediaFileId, position] - - logger.debug( - 'Adding a new playlist/media file relation.\n\tSQL: {}\n\tPARAMS: {}', - query, params) - sql.executeInsert(query, params) - - p.mediaFileCount++ - p.lastUsed = new Timestamp(new Date().time) - return update(p) } } - - public Playlist removeFromPlaylist(UUID playlistId, UUID mediaFileId) { - String getPositionQuery = """\ - SELECT position FROM playlists_media_files - WHERE playlist_id = ? AND media_file_id = ?""" - - String delQuery = """\ - DELETE FROM playlists_media_files - WHERE playlist_id = ? AND media_file_id = ?""" - - String reorderPlaylistQuery = """\ - UPDATE playlists_media_files SET position = position - 1 - WHERE playlist_id = ? AND position > ?""" - - String updatePlaylistQuery = """\ - UPDATE playlists SET - media_file_count = (SELECT count(*) - FROM playlists_media_files pmf - WHERE pmf.playlist_id = ?), - last_used = NOW() - WHERE id = ?""" - - withTransaction { - def params = [playlistId, mediaFileId] - logger.debug( - 'Finding media file playlist position.\n\tSQL: {}\n\tPARAMS: {}', - getPositionQuery, params) - def row = sql.firstRow(getPositionQuery, params) - int position = row ? row[0] : 0 - - logger.debug( - 'Removing media file from playlist.\n\tSQL: {}\n\tPARAMS: {}', - delQuery, params) - sql.execute(delQuery, params) - - params = [playlistId, position] - logger.debug('Reording playlist items.\n\tSQL: {}\n\tPARAMS: {}', - reorderPlaylistQuery, params) - sql.execute(reorderPlaylistQuery, params) - - params = [playlistId, playlistId] - logger.debug('Updating playlist.\n\tSQL: {}\n\tPARAMS: {}', - updatePlaylistQuery, params) - sql.executeUpdate(updatePlaylistQuery, params) - - return getById(Playlist, playlistId) } } - - public Playlist removeAllFromPlaylist(UUID playlistId) { - return withTransaction { - def p = getById(Playlist, playlistId) - if (!p) return null - String query = - 'DELETE FROM playlists_media_files WHERE playlist_id = ?' - def sqlParams = [playlistId] - logger.debug('Clearing playlist.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - sql.execute(query, sqlParams) - p.mediaFileCount = 0 - p.lastUsed = new Timestamp(new Date().time) - return update(p) } } - - public void removeEmptyPlaylists() { - - String query = """\ - DELETE FROM playlists WHERE id IN ( - SELECT DISTINCT p.id - FROM playlists p LEFT OUTER JOIN playlists_media_files pmf ON - p.id = pmf.playlist_id - WHERE pmf.playlist_id IS NULL)""" - - logger.debug('Deleting empty playlists.\n\tSQL: {}', query) - sql.execute(query) } - - /// ### Tag-specific methods - public Tag getTagById(UUID id) { return getById(Tag, id) } - public Tag getTagByName(String name) { return getByName(Tag, name)[0] } - public List getTags() { return getAll(Tag, 'name ASC') } - - public List getTagsWhere(Map params) { - def query = new StringBuilder() - def sqlParams = [] - - query.append('SELECT DISTINCT t.* FROM tags t ') - - if (params.mediaFileId || params.artistId || params.albumId || - params.playlistId) - query.append(' JOIN media_files_tags mft ON t.id = mft.tag_id') - - if (params.mediaFileId) { - query.append(' AND mft.media_file_id = ? ') - sqlParams << params.mediaFileId } - - if (params.playlistId) { - query.append(' JOIN playlists_media_files pmf ON ') - .append(' mft.media_file_id = pmf.media_file_id AND ') - .append(' pmf.playlist_id = ? ') - sqlParams << params.playlistId } - - if (params.artistId) { - query.append(' JOIN artists_media_files armf ON ') - .append(' mft.media_file_id = armf.media_file_id AND ') - .append(' armf.artist_id = ? ') - sqlParams << params.artistId } - - if (params.albumId) { - query.append(' JOIN albums_media_files almf ON ') - .append(' mft.media_file_id = almf.media_file_id AND ') - .append(' almf.album_id = ? ') - sqlParams << params.albumId } - - if (params.name) { - query.append(' WHERE t.name = ? ') - sqlParams << params.name } - - query = query.toString() - logger.debug('Selecting tags.\n\tSQL: {}\n\tPARAMS: {}', - query, sqlParams) - return sql.rows(query, sqlParams).collect { recordToModel(Tag, it) } } - - public def tagMediaFiles(List mediaFileIds, List tagNames) { - String insertQuery = 'INSERT INTO media_files_tags VALUES (?, ?)' - String checkQuery = 'SELECT * FROM media_files_tags WHERE media_file_id = ? AND tag_id = ?' - def params - - withTransaction { - List tags = tagNames.collect { name -> - getTagByName(name) ?: create(new Tag(name: name)) } - - tags.each { tag -> - params = [null, tag.id] - mediaFileIds.each { mediaFileId -> - params[0] = mediaFileId - logger.debug('Checking to see if a media file is ' + - 'already tagged.\n\tSQL: {}\n\tPARAMS: {}', params) - if (sql.rows(checkQuery, params).size() == 0) { - logger.debug( - 'Tagging a media file.\n\tSQL: {}\n\tPARAMS: {}', - params) - sql.executeInsert(insertQuery, params) } } } } } - - public def untagMediaFiles(List mediaFileIds, List tagNames) { - - withTransaction { - List tags = tagNames.collect(this.&getTagByName).findAll() - def tagPlaceHolders = tags.collect { '?' }.join(', ') - def tagIds = tags.collect { it.id } - - String unassociateTagQuery = """ - DELETE FROM media_files_tags - WHERE tag_id IN ($tagPlaceHolders) AND media_file_id = ?""" - - String checkTagUseQuery = - 'SELECT * FROM media_files_tags WHERE tag_id = ?' - - String deleteTagQuery = 'DELETE FROM tags WHERE id = ?' - - mediaFileIds.each { mfId -> - def params = tagIds + mfId - logger.debug('Removing tags.\n\tSQL: {}\n\tPARAMS: {}', - unassociateTagQuery, params) - sql.execute(unassociateTagQuery, params) } - - tags.each { tag -> - def params = [tag.id] - logger.debug( - 'Checking if tag is still in use.\n\tSQL: {}\n\tPARAMS: {}', - checkTagUseQuery, params) - if (sql.rows(checkTagUseQuery, params).size() == 0) { - logger.debug('Deleting unused tag.\n\tSQL: {}, PARAMS: {}', - deleteTagQuery, params) - sql.execute(deleteTagQuery, params) } } } } - - /// ### Utility functions - public def withTransaction(Closure c) { - try { sql.execute('BEGIN TRANSACTION'); return c() } - finally { sql.execute('COMMIT') } } - - public static String nameToModel(String name) { - def pts = name.toLowerCase().split('_') - return pts.length == 1 ? pts[0] : - pts[0] + pts[1..-1].collect { it.capitalize() }.join() } - - public static String nameFromModel(String name) { - return UPPERCASE_PATTERN.matcher(name). - replaceAll(/$1_$2/).toLowerCase() } - - public static String pluralize(String name) { return name + 's' } - - static def updateModel(def record, def model) { - getInstanceFields(model.class).each { field -> - field.set(model, record[nameFromModel(field.name)]) } - return model } - - static M recordToModel(Class modelClass, def record) { - if (record == null) return null - - def model = modelClass.newInstance() - - getInstanceFields(model.class).each { field -> - field.set(model, record[nameFromModel(field.name)]) } - - return model } - - static def modelToRecord(Model model) { - if (model == null) return null - - def record = [:] - - getInstanceFields(model.class).each { field -> - record[nameFromModel(field.name)] = field.get(model) } - - return record } - - static def getInstanceFields(Class modelClass) { - return modelClass.fields.findAll { !Modifier.isStatic(it.modifiers) } } - - /// ### DB Sync/Replication - - public def diffWith(DbApi that) { - - def results = [ - ours: [ modelIds: [:], associations: [:] ], - theirs:[ modelIds: [:], associations: [:] ] ] - - [Album, Artist, Image, MediaFile, Playlist, Bookmark, - Tag].each { modelClass -> - - List ourIds = this.getAllIds(modelClass) - List theirIds = that.getAllIds(modelClass) - - results.ours.modelIds[modelClass] = ourIds - theirIds - results.theirs.modelIds[modelClass] = theirIds - ourIds } - - ['albums_images', 'albums_media_files', 'artists_albums', - 'artists_images', 'artists_media_files', 'media_files_tags', - 'playlists_media_files'].each { tableName -> - - def query = 'SELECT * FROM ' + tableName; - def allOurRows = this.sql.rows(query) - def allTheirRows = that.sql.rows(query) - - def ourRows = allOurRows.clone() - def theirRows = allTheirRows.clone() - - allOurRows.each { ourRow -> - allTheirRows.each { theirRow -> - if (ourRow[0] == theirRow[0]) { - ourRows.remove(ourRow) - theirRows.remove(theirRow) } } } - - results.ours.associations[tableName] = ourRows - results.theirs.associations[tableName] = theirRows } - - return results } - - public def ingestDiff(DbApi that, def diff) { - - diff.modelIds.each { modelClass, ids -> - ids.each { id -> this.create(that.getById(modelClass, id)) } } - - diff.associations.each { tableName, rows -> - String placeholders = null - String query = null - rows.each { row -> - if (!placeholders) - placeholders = (1..row.size()).collect { '?' }.join(', ') - - if (!query) - query = "INSERT INTO ${tableName} VALUES (${placeholders})" - - logger.debug("Adding association.\n\tSQL: {}\n\tPARAMS: {}", - query, row.values() as List) - this.sql.executeInsert(query, row.values() as List) } } } - - public def syncWith(DbApi that, boolean pull = true, - boolean push = false) { - def diff = this.diffWith(that) - - if (pull) this.ingestDiff(that, diff.theirs) - if (push) that.ingestDiff(this, diff.ours) - - return diff } - -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Album.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Album.java deleted file mode 100644 index d5b39f0..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Album.java +++ /dev/null @@ -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; } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Artist.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Artist.java deleted file mode 100644 index 4358a98..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Artist.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.jdbernard.wdiwtlt.db.models; - -public class Artist extends Model { - public String name; - - public String toString() { return name; } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Bookmark.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Bookmark.java deleted file mode 100644 index 32910ee..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Bookmark.java +++ /dev/null @@ -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; } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Image.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Image.java deleted file mode 100644 index a0d2d42..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Image.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.jdbernard.wdiwtlt.db.models; - -public class Image extends Model { - public String url; - - public String toString() { return url; } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/MediaFile.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/MediaFile.java deleted file mode 100644 index d30f0b4..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/MediaFile.java +++ /dev/null @@ -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; } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Model.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Model.java deleted file mode 100644 index 5fb86b5..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Model.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.jdbernard.wdiwtlt.db.models; - -import java.util.UUID; -import javax.persistence.Entity; - -@Entity -public class Model implements Comparable, 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); } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Playlist.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Playlist.java deleted file mode 100644 index d79ce3a..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Playlist.java +++ /dev/null @@ -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)"; } -} diff --git a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Tag.java b/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Tag.java deleted file mode 100644 index 06201df..0000000 --- a/core/src/main/java/com/jdbernard/wdiwtlt/db/models/Tag.java +++ /dev/null @@ -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; } -} diff --git a/core/src/main/sql/migrations/20151209054632-initial-schema-down.sql b/core/src/main/sql/migrations/20151209054632-initial-schema-down.sql deleted file mode 100644 index f0a2fb3..0000000 --- a/core/src/main/sql/migrations/20151209054632-initial-schema-down.sql +++ /dev/null @@ -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; diff --git a/core/src/main/sql/migrations/20151209054632-initial-schema-up.sql b/core/src/main/sql/migrations/20151209054632-initial-schema-up.sql deleted file mode 100644 index dfd4211..0000000 --- a/core/src/main/sql/migrations/20151209054632-initial-schema-up.sql +++ /dev/null @@ -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) -); diff --git a/core/test.groovy b/core/test.groovy deleted file mode 100644 index ff1fecb..0000000 --- a/core/test.groovy +++ /dev/null @@ -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) diff --git a/rest-server/build.gradle b/rest-server/build.gradle deleted file mode 100644 index 33a5bc2..0000000 --- a/rest-server/build.gradle +++ /dev/null @@ -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' } diff --git a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/AllowCors.java b/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/AllowCors.java deleted file mode 100644 index 4fe8dff..0000000 --- a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/AllowCors.java +++ /dev/null @@ -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 {} diff --git a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/CorsResponseFilter.java b/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/CorsResponseFilter.java deleted file mode 100644 index 415bb9a..0000000 --- a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/CorsResponseFilter.java +++ /dev/null @@ -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 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")); - - } -} diff --git a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/PingResource.java b/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/PingResource.java deleted file mode 100644 index f54a750..0000000 --- a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/rest/PingResource.java +++ /dev/null @@ -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"; } } diff --git a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContext.groovy b/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContext.groovy deleted file mode 100644 index 0fdb41b..0000000 --- a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContext.groovy +++ /dev/null @@ -1,8 +0,0 @@ -package com.jdbernard.nlsongs.servlet - -import com.jdbernard.wdiwtlt.db.DbApi - -public class WdiwtltContext { - - public static DbApi dbapi -} diff --git a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContextListener.groovy b/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContextListener.groovy deleted file mode 100644 index 0a0a633..0000000 --- a/rest-server/src/main/groovy/com/jdbernard/wdiwtlt/servlet/WdiwtltContextListener.groovy +++ /dev/null @@ -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') } -} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index bc57af0..0000000 --- a/settings.gradle +++ /dev/null @@ -1,4 +0,0 @@ -include 'core', 'cli', 'rest-server' - -project(":core").name ="wdiwtlt-core" -project(":cli").name = "wdiwtlt-cli" diff --git a/worklog.md b/worklog.md deleted file mode 100644 index 642139d..0000000 --- a/worklog.md +++ /dev/null @@ -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)?