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 org.docopt.Docopt
import jline.console.ConsoleReader
import java.sql.Timestamp
import java.text.SimpleDateFormat
import java.net.URLDecoder
import java.util.regex.Pattern
import java.util.regex.Matcher
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import uk.co.caprica.vlcj.discovery.NativeDiscovery
import uk.co.caprica.vlcj.component.AudioMediaListPlayerComponent
import uk.co.caprica.vlcj.player.MediaPlayerEventListener
import static com.jdbernard.util.AnsiEscapeCodeSequence.*
@ -98,11 +103,13 @@ Configuration:
/// Current play queue and selection data
Selection currentSelection = new Selection()
Playlist playQueue = library.save(new Playlist(
name: "CLI Queue ${sdf.format(new Date())}"))
Playlist playQueue
Bookmark playBookmark
MediaFile curMediaFile
/// VLCJ Player
AudioMediaListPlayerComponent vlcj
public static void main(String[] args) {
def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args)
@ -178,14 +185,27 @@ Configuration:
exitErr("Could not establish a connection to the database:\n\t" +
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
def cliInst = new CommandLineInterface(new MediaLibrary(orm, libRoot),
System.in, System.out, givenCfg)
cliInst.repl()
try {
def cliInst = new CommandLineInterface(new MediaLibrary(orm, libRoot),
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,
OutputStream out, Properties cliConfig) {
public CommandLineInterface(MediaLibrary library,
AudioMediaListPlayerComponent vlcj, InputStream sin, OutputStream out,
Properties cliConfig) {
this.cliConfig = cliConfig
this.library = library
this.inStream = sin
@ -195,7 +215,32 @@ Configuration:
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)
}
void setupTextStyles() {
@ -239,6 +284,10 @@ Configuration:
processInput(line.split(/\s/) as LinkedList)
} else {
drawLeader()
if (curMediaFile) {
playBookmark.playTimeMs =
vlcj.mediaListPlayer.mediaPlayer.time
library.save(playBookmark) }
Thread.sleep(250)
}
}
@ -256,17 +305,18 @@ Configuration:
case 'enqueue': return processEnqueue(line)
case 'tag': return processTag(line)
case 'clear': return processClear(line)
case 'play': return line.size() == 0 ?
transport.play() :
processPlay(line)
case 'pause': return transport.pause()
case 'stop': return transport.stop()
case 'play': return processPlay(line)
case 'pause': return processPause(line)
case 'stop': return processStop(line)
case 'next': return processNext(line)
case 'prev': return processPrev(line)
case 'ff':
case 'fastforward': return processFastForward(line)
case 'rw':
case 'rwd':
case 'rewind': return processRewind(line)
case 'vol':
case 'volume': return processVolume(line)
case 'debug':
outStream.println(
@ -377,9 +427,13 @@ Configuration:
selection.selectedFiles.collect { it.id }) }
private def processEnqueue(LinkedList line) {
Selection selection = processSelect(line)
return library.addToPlaylist(playQueue.id,
selection.selectedFiles.collect { it.id }) }
def selectedFiles = processSelect(line).selectedFiles
selectedFiles.each {
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) {
List<String> parts = line.join(' ').split(' as ').collect { it.trim() }
@ -531,6 +585,183 @@ Configuration:
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) {
if (!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) } }