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