Clean out previous work (clean slate).

This commit is contained in:
Jonathan Bernard 2021-11-11 11:48:53 -06:00
parent a6371574a7
commit e429d2656e
34 changed files with 12 additions and 3816 deletions

View File

@ -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

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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

View File

@ -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) }
}

View File

@ -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() }
}

View File

@ -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) } }

View File

@ -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"])

View 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

View File

@ -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'
}

View File

@ -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

View File

@ -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 }
}

View File

@ -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

View File

@ -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; }
}

View File

@ -1,7 +0,0 @@
package com.jdbernard.wdiwtlt.db.models;
public class Artist extends Model {
public String name;
public String toString() { return name; }
}

View File

@ -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; }
}

View File

@ -1,7 +0,0 @@
package com.jdbernard.wdiwtlt.db.models;
public class Image extends Model {
public String url;
public String toString() { return url; }
}

View File

@ -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; }
}

View File

@ -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); }
}

View File

@ -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)"; }
}

View File

@ -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; }
}

View File

@ -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;

View File

@ -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)
);

View File

@ -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)

View File

@ -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' }

View File

@ -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 {}

View File

@ -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"));
}
}

View File

@ -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"; } }

View File

@ -1,8 +0,0 @@
package com.jdbernard.nlsongs.servlet
import com.jdbernard.wdiwtlt.db.DbApi
public class WdiwtltContext {
public static DbApi dbapi
}

View File

@ -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') }
}

View File

@ -1,4 +0,0 @@
include 'core', 'cli', 'rest-server'
project(":core").name ="wdiwtlt-core"
project(":cli").name = "wdiwtlt-cli"

View File

@ -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)?