14 Commits
0.1.0 ... 0.1.1

8 changed files with 170 additions and 48 deletions

View File

@ -1,4 +1,5 @@
# **W**hat **Do** **I** **W**ant **T**o **L**isten **T**o
# What Do I Want To Listen To
## A simple, tag-based music library manager.
This project is born out of a frustration I had managing my music library.
@ -6,16 +7,70 @@ 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)
* Song tagging.
* Playlists.
* Read meta-data from ID3.
* Transcoding on the fly.
* Read songs from Amazon S3.
* Stream from Amazon Cloudfront.
* Bookmarks (temporary song tag marking location in a playlist/album)
I have not found a music manager that gives me all of the above, so I'm going
to write my own.
I have not found a music manager that gives me all of the above, so I'm
writing my own.
## Overview
WDIWTLT is currently made up of two subprojects:
* `core`
* `cli`
### `core`
`core` contains the data layer implementation, built with a lightweight ORM
layer over JDBC, 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.
## 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

@ -12,11 +12,14 @@ allprojects {
}
group = 'com.jdbernard'
version = '0.1.0'
version = '0.1.1'
repositories {
mavenLocal()
mavenCentral()
maven { url "https://dl.bintray.com/ijabz/maven" }
maven { url "http://mvn.jdb-labs.com/repo" }
}
apply plugin: 'maven'
}

View File

@ -28,7 +28,7 @@ import static com.jdbernard.wdiwtlt.cli.CliErr.*
public class CommandLineInterface {
public static final VERSION = "ALPHA"
public static final VERSION = "0.1.1"
public static final def DOC = """\
wdiwtlt v$VERSION
@ -125,6 +125,7 @@ Configuration:
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<Model> currentSelection = []
@ -151,8 +152,13 @@ Configuration:
if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile())
cfgFile = new File("wdiwtlt.cli.properties")
if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile())
cfgFile = new File(System.getenv().HOME, ".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) } }
@ -273,6 +279,9 @@ Configuration:
// 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) {
@ -305,7 +314,8 @@ Configuration:
library.clean()
playQueue = library.save(new Playlist(
name: "CLI Queue ${sdf.format(new Date())}"))
name: "CLI Queue ${sdf.format(new Date())}",
userCreated: false))
String user = System.getProperty('user.name')
String os = System.getProperty('os.name')
@ -384,7 +394,10 @@ Configuration:
dismissMsgDate = new Date(new Date().time + msgTimeout) } } }
else {
drawLeader()
if (curMediaFile && System.currentTimeMillis() > nextBookmarkUpdate) {
if (curMediaFile &&
vlcj.mediaListPlayer.mediaPlayer.isPlaying() &&
System.currentTimeMillis() > nextBookmarkUpdate) {
playBookmark.playTimeMs =
vlcj.mediaListPlayer.mediaPlayer.time
library.save(playBookmark)
@ -491,6 +504,7 @@ Configuration:
Class modelClass
switch (options) {
case ~/selection/: return selection;
case ~/playing ($selectableModels)s?/:
if (!curMediaFile) err "No media is currently playing."
@ -501,10 +515,29 @@ Configuration:
else return library.getWhere(modelClass,
[mediaFileId: curMediaFile.id])
/* TODO
case ~/files tagged as((\s\w+)+?) and not as((\s\w+)+)/:
excludedTags = lastMatcher[0][3].split(/\s/)
*/
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..<count).each {
selected << source.remove(rand.nextInt(source.size())) }
return selected
case ~/selected ($selectableModels)s?/:
@ -531,6 +564,18 @@ Configuration:
// TODO
err "select <thing> 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/)
@ -561,12 +606,17 @@ Configuration:
switch (options) {
case ~/bookmark named (.+) on playlist (.+) at (.+)/:
Playlist p = getExactlyOne(Matcher.lastMatcher[0][2].trim())
MediaFile mf = getExactlyOne(Matcher.lastMatcher[0][3].trim())
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
@ -577,33 +627,29 @@ Configuration:
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
if (Matcher.lastMatcher[0][2] == 'queue') {
p = playQueue.clone()
p.name = Matcher.lastMatcher[0][1].trim()
p.id = null
p = library.save(p) }
else {
p = new Playlist(name: Matcher.lastMatcher[0][1])
p = library.save(p)
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)
if (Matcher.lastMatcher[0][2] != 'selection')
selection = select(Matcher.lastMatcher[0][2], selection)
library.addToPlaylist(p.id,
library.collectMediaFiles(selection).collect { it.id }) }
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(),
userCreated: true)
name: Matcher.lastMatcher[0][1].trim())
p = library.save(p)
msg "New playlist: ${p.id}: ${p.name}"
@ -653,7 +699,7 @@ Configuration:
if (selection) {
List<MediaFile> mediaFiles = library.collectMediaFiles(selection)
playQueue = library.removeAllFromPlaylist(playQueue.id)
library.addToPlaylist(playQueue.id, mediaFiles.collect { it.id })
playQueue = library.addToPlaylist(playQueue.id, mediaFiles.collect { it.id })
setPlayQueue(playQueue) }
vlcj.mediaListPlayer.play()
@ -927,10 +973,11 @@ Configuration:
private def processRepeat(String option) {
switch(option) {
case null:
def mode = vlcj.mediaListPlayer.mode
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.'))
(mode == MediaListPlayerMode.REPEAT ? 'one.' : 'off.'))*/
return
case 'off':
@ -1024,10 +1071,18 @@ ${cmdStyle}select playing { album | artist | file | playlist | tag }${normalStyl
this selects items that are associated with the currently playing media
file.
${cmdStyle}queued { albums | artists | files | playlists | tags }${normalStyle}
${cmdStyle}select queued { albums | artists | files | playlists | tags }${normalStyle}
Select the items currently in the queue.
${cmdStyle}select <count> random {albums | artists | files | playlists | tags }${normalStyle}
Select one or more items randomly.
${cmdStyle}select <count> random {albums | artists | files | playlists | tags } from <select-criteria>${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
@ -1409,7 +1464,7 @@ Library Management:
"""
default:
err "Unrecognized command: '$line'"
err "Unrecognized command: '$options'"
drawLeader()
Thread.sleep(250)
break } }
@ -1454,6 +1509,7 @@ Library Management:
p.lastUsed = new Timestamp(new Date().time)
playQueue = library.update(p)
vlcj.mediaListPlayer.stop()
vlcj.mediaList.clear()
library.getMediaFilesWhere(playlistId: playQueue.id).each {

View File

@ -12,7 +12,6 @@ buildscript {
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'ch.raffael.pegdown-doclet'
apply plugin: 'war'
dependencies {
compile localGroovy()

View File

@ -1,5 +0,0 @@
{
"driver": "postgres",
"sqlDir": "src/main/sql/migrations",
"connectionString": "host=localhost port=5432 dbname=wdiwtlt user=jdbernard password="
}

5
core/database.properties Normal file
View File

@ -0,0 +1,5 @@
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

@ -12,10 +12,15 @@ public class ConfigWrapper {
public ConfigWrapper() {
if (env.WDIWTLT_CONFIG_FILE) {
if (tryLoadConfigFile(new File(env.WDIWTLT_CONFIG_FILE))) {
if (tryLoadConfigFile(env.WDIWTLT_CONFIG_FILE)) {
return } }
if (tryLoadConfigFile(new File(env.HOME, ".wdiwtlt.properties"))) 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")
@ -68,6 +73,10 @@ public class ConfigWrapper {
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

View File

@ -5,7 +5,7 @@ import java.util.Date;
import java.util.UUID;
public class Playlist extends Model {
public boolean userCreated = false;
public boolean userCreated = true;
public String name;
public int mediaFileCount = 0;
public UUID copiedFromId = null;