CLI: Added VLCJ bindings to play through VLC.
This commit is contained in:
parent
9689547c0d
commit
729c27bd72
@ -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.");
|
||||||
|
@ -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) } }
|
Loading…
Reference in New Issue
Block a user