CLI: Added VLCJ bindings to play through VLC.

This commit is contained in:
Jonathan Bernard 2016-03-12 21:11:24 -06:00
parent 9689547c0d
commit 729c27bd72
2 changed files with 253 additions and 15 deletions

View File

@ -10,11 +10,16 @@ import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import org.docopt.Docopt import org.docopt.Docopt
import jline.console.ConsoleReader import jline.console.ConsoleReader
import java.sql.Timestamp
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.net.URLDecoder
import java.util.regex.Pattern import java.util.regex.Pattern
import java.util.regex.Matcher import java.util.regex.Matcher
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import uk.co.caprica.vlcj.discovery.NativeDiscovery
import uk.co.caprica.vlcj.component.AudioMediaListPlayerComponent
import uk.co.caprica.vlcj.player.MediaPlayerEventListener
import static com.jdbernard.util.AnsiEscapeCodeSequence.* import static com.jdbernard.util.AnsiEscapeCodeSequence.*
@ -98,11 +103,13 @@ Configuration:
/// Current play queue and selection data /// Current play queue and selection data
Selection currentSelection = new Selection() Selection currentSelection = new Selection()
Playlist playQueue = library.save(new Playlist( Playlist playQueue
name: "CLI Queue ${sdf.format(new Date())}"))
Bookmark playBookmark Bookmark playBookmark
MediaFile curMediaFile MediaFile curMediaFile
/// VLCJ Player
AudioMediaListPlayerComponent vlcj
public static void main(String[] args) { public static void main(String[] args) {
def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args) def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args)
@ -178,14 +185,27 @@ Configuration:
exitErr("Could not establish a connection to the database:\n\t" + exitErr("Could not establish a connection to the database:\n\t" +
e.localizedMessage) } e.localizedMessage) }
// Try to discover the VLC native libraries
def vlcj
try {
new NativeDiscovery().discover()
vlcj = new AudioMediaListPlayerComponent() }
catch (Exception e) {
exitErr("Could not find the VLC libraries. Is VLC installed?\n\t" +
e.localizedMessage) }
// Create our CLI object // Create our CLI object
def cliInst = new CommandLineInterface(new MediaLibrary(orm, libRoot), try {
System.in, System.out, givenCfg) def cliInst = new CommandLineInterface(new MediaLibrary(orm, libRoot),
cliInst.repl() vlcj, System.in, System.out, givenCfg)
cliInst.repl() }
catch (Exception e) { e.printStackTrace(); exitErr(e.localizedMessage) }
finally { if (vlcj) vlcj.release() }
} }
public CommandLineInterface(MediaLibrary library, InputStream sin, public CommandLineInterface(MediaLibrary library,
OutputStream out, Properties cliConfig) { AudioMediaListPlayerComponent vlcj, InputStream sin, OutputStream out,
Properties cliConfig) {
this.cliConfig = cliConfig this.cliConfig = cliConfig
this.library = library this.library = library
this.inStream = sin this.inStream = sin
@ -195,7 +215,32 @@ Configuration:
setupTextStyles() setupTextStyles()
library.clean()
playQueue = library.save(new Playlist(
name: "CLI Queue ${sdf.format(new Date())}"))
String user = System.getProperty('user.name')
String os = System.getProperty('os.name')
def bookmarks = library.getBookmarkByName("Last played for $user on $os.")
if (bookmarks) playBookmark = bookmarks[0]
else playBookmark = new Bookmark(name: "Last played for $user on $os.")
playBookmark.playlistId = playQueue.id
playBookmark.playIndex = 0
playBookmark.mediaFileId = 0
this.vlcj = vlcj
def listener = new UniversalNoopImplementation()
listener.methods.playing = this.&playing
listener.methods.finished = this.&finished
listener.methods.stopped = this.&stopped
vlcj.mediaListPlayer.mediaPlayer.addMediaPlayerEventListener(
listener as MediaPlayerEventListener)
this.consoleReaderThread = new Thread().start(this.&runReaderThread) this.consoleReaderThread = new Thread().start(this.&runReaderThread)
} }
void setupTextStyles() { void setupTextStyles() {
@ -239,6 +284,10 @@ Configuration:
processInput(line.split(/\s/) as LinkedList) processInput(line.split(/\s/) as LinkedList)
} else { } else {
drawLeader() drawLeader()
if (curMediaFile) {
playBookmark.playTimeMs =
vlcj.mediaListPlayer.mediaPlayer.time
library.save(playBookmark) }
Thread.sleep(250) Thread.sleep(250)
} }
} }
@ -256,17 +305,18 @@ Configuration:
case 'enqueue': return processEnqueue(line) case 'enqueue': return processEnqueue(line)
case 'tag': return processTag(line) case 'tag': return processTag(line)
case 'clear': return processClear(line) case 'clear': return processClear(line)
case 'play': return line.size() == 0 ? case 'play': return processPlay(line)
transport.play() : case 'pause': return processPause(line)
processPlay(line) case 'stop': return processStop(line)
case 'pause': return transport.pause()
case 'stop': return transport.stop()
case 'next': return processNext(line) case 'next': return processNext(line)
case 'prev': return processPrev(line) case 'prev': return processPrev(line)
case 'ff': case 'ff':
case 'fastforward': return processFastForward(line) case 'fastforward': return processFastForward(line)
case 'rw':
case 'rwd': case 'rwd':
case 'rewind': return processRewind(line) case 'rewind': return processRewind(line)
case 'vol':
case 'volume': return processVolume(line)
case 'debug': case 'debug':
outStream.println( outStream.println(
@ -377,9 +427,13 @@ Configuration:
selection.selectedFiles.collect { it.id }) } selection.selectedFiles.collect { it.id }) }
private def processEnqueue(LinkedList line) { private def processEnqueue(LinkedList line) {
Selection selection = processSelect(line) def selectedFiles = processSelect(line).selectedFiles
return library.addToPlaylist(playQueue.id, selectedFiles.each {
selection.selectedFiles.collect { it.id }) } vlcj.mediaList.addMedia(
new File(library.libraryRoot, it.filePath).canonicalPath) }
playQueue = library.addToPlaylist(playQueue.id,
selectedFiles.collect { it.id })
return playQueue }
private def processTag(LinkedList line) { private def processTag(LinkedList line) {
List<String> parts = line.join(' ').split(' as ').collect { it.trim() } List<String> parts = line.join(' ').split(' as ').collect { it.trim() }
@ -531,6 +585,183 @@ Configuration:
return list return list
} }
private def processPlay(LinkedList line) {
def option = line.poll()
if(option) switch(option) {
case 'playlist':
Playlist p = ensureExactlyOne(
library.getByIdOrName(Playlist, line.join(' ')))
if (!p) { setErr('No matching playlist found.'); return }
setPlayQueue(p)
break
case 'bookmark':
Bookmark b = ensureExactlyOne(
library.getByIdOrName(Bookmark, line.join(' ')))
if (!b) { setErr('No matching bookmark found.'); return }
Playlist p = library.getPlaylistById(b.playlistId)
if (!p) {
setErr('The playlist no longer exists for this bookmark')
return }
setPlayQueue(p)
vlcj.mediaListPlayer.playItem(b.playIndex)
if (b.playTimeMs > 0)
vlcj.mediaListPlayer.mediaPlayer.time = b.playTimeMs
break
case 'album': case 'artist': case 'file': case 'tags':
if (vlcj.mediaListPlayer.isPlaying())
vlcj.mediaListPlayer.stop()
vlcj.mediaList.clear()
// If we're currently playing a user's playlist, don't change
// it, just create a new system playlist.
if (playQueue.userCreated) {
playQueue = library.save(new Playlist(
name: "CLI Queue ${sdf.format(new Date())}")) }
else library.removeAllFromPlaylist(playQueue.id)
line.addFirst(option)
processEnqueue(line)
default:
printLongMessage(
"Invalid options to the ${promptStyle}play${normalStyle}" +
" command. Use ${promptStyle}help play${normalStyle} to see a " +
"list of valid options.") }
vlcj.mediaListPlayer.play() }
private def processPause(LinkedList line) { vlcj.mediaListPlayer.pause() }
private def processStop(LinkedList line) {
// TODO
vlcj.mediaListPlayer.stop() }
private def processNext(LinkedList line) {
def count = line.poll()
try { count = count ? count as int : 1 }
catch (Exception e) { setErr("$count is not a valid number"); count = 1 }
vlcj.mediaListPlayer.stop()
if ((playBookmark.playIndex + count) < vlcj.mediaList.size())
vlcj.mediaListPlayer.playItem(playBookmark.playIndex + count) }
private def processPrev(LinkedList line) {
def count = line.poll()
try { count = count ? count as int : 1 }
catch (Exception e) { setErr("$count is not a valid number"); count = 1 }
vlcj.mediaListPlayer.stop()
if ((playBookmark.playIndex - count) >= 0)
vlcj.mediaListPlayer.playItem(playBookmark.playIndex - count) }
private def processFastForward(LinkedList line) {
def amount = line.poll()
def unit = line.poll()?.trim()
if (!amount) { amount = "10"; unit = "s" }
if (!unit) unit = 's'
try { amount = amount as int }
catch (Exception e) { setErr "$amount must be an integer."; return }
switch (unit) {
case 'ms': case 'millis': case 'millisecond': case 'milliseconds':
vlcj.mediaListPlayer.mediaPlayer.skip(amount)
break
case 's': case 'sec': case 'second': case 'seconds':
vlcj.mediaListPlayer.mediaPlayer.skip(amount * 1000)
break
case 'm': case 'min': case 'minute': case 'minutes':
vlcj.mediaListPlayer.mediaPlayer.skip(amount * 60000)
break
default: setErr "$unit must be one of 'milliseconds' " +
"(or 'millis' or 'ms'), 'seconds' (or 'sec' or 's'), or " +
"'minutes' (or 'min' or 'm')"
return } }
private def processRewind(LinkedList line) {
def amount = line.poll()
def unit = line.poll()?.trim()
if (!amount) { amount = "10"; unit = "s" }
if (!unit) unit = 's'
try { amount = -(amount as int) }
catch (Exception e) { setErr "$amount must be an integer."; return }
switch (unit) {
case 'ms': case 'millis': case 'millisecond': case 'milliseconds':
vlcj.mediaListPlayer.mediaPlayer.skip(amount)
break
case 's': case 'sec': case 'second': case 'seconds':
vlcj.mediaListPlayer.mediaPlayer.skip(amount * 1000)
break
case 'm': case 'min': case 'minute': case 'minutes':
vlcj.mediaListPlayer.mediaPlayer.skip(amount * 60000)
break
default: setErr "$unit must be one of 'milliseconds' " +
"(or 'millis' or 'ms'), 'seconds' (or 'sec' or 's'), or " +
"'minutes' (or 'min' or 'm')"
return } }
private def processVolume(LinkedList line) {
def percentage = line.poll()
if (!percentage)
setMsg("Volume: ${vlcj.mediaListPlayer.mediaPlayer.volume}")
else {
try { percentage = Math.min(Math.max(0, percentage as int), 200) }
catch (Exception e) {
setErr("Volume must be and integer between 0 and 200.")
return }
vlcj.mediaListPlayer.mediaPlayer.volume = percentage } }
private void playing(def player) {
try {
def mediaFiles = library.getMediaFilesWhere(playlistId: playQueue.id)
String absFilePath =
URLDecoder.decode(vlcj.mediaListPlayer.currentMrl()[7..-1])
def currentIdx = mediaFiles.findIndexOf {
def mrlPath = new File(absFilePath).canonicalPath
def mfPath = new File(library.libraryRoot, it.filePath).canonicalPath
return mfPath == mrlPath }
curMediaFile = mediaFiles[currentIdx]
currentlyPlaying.text = makeFullMediaFileDescription(curMediaFile)
playBookmark.playlistId = playQueue.id
playBookmark.playIndex = currentIdx
playBookmark.mediaFileId = curMediaFile.id
library.save(playBookmark) }
catch (Exception e) { printLongMessage(e.printStackTrace()) } }
private void finished(def player) {
curMediaFile = library.incrementPlayCount(curMediaFile)
library.save(curMediaFile) }
private void stopped(def player) {
curMediaFile = null
currentlyPlaying.text = "No media currently playing." }
public void setPlayQueue(Playlist p) {
if (vlcj.mediaListPlayer.isPlaying()) vlcj.mediaListPlayer.stop()
playQueue = p
vlcj.mediaList.clear()
library.getMediaFilesWhere(playlistId: playQueue.id).each {
vlcj.mediaList.addMedia(
new File(library.libraryRoot, it.filePath).canonicalPath) } }
public def ensureExactlyOne(def matches) { public def ensureExactlyOne(def matches) {
if (!matches) { if (!matches) {
setErr("Nothing matches."); setErr("Nothing matches.");

View File

@ -0,0 +1,7 @@
package com.jdbernard.wdiwtlt.cli
public class UniversalNoopImplementation {
Map methods = [:]
public def methodMissing(String name, def args) {
if (methods[name]) return methods[name](*args) } }