diff --git a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy new file mode 100644 index 0000000..b491ed3 --- /dev/null +++ b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CliErr.groovy @@ -0,0 +1,9 @@ +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) } +} diff --git a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy index 2842918..52f7fbf 100644 --- a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy +++ b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy @@ -22,6 +22,8 @@ import uk.co.caprica.vlcj.component.AudioMediaListPlayerComponent import uk.co.caprica.vlcj.player.MediaPlayerEventListener import static com.jdbernard.util.AnsiEscapeCodeSequence.* +import static com.jdbernard.wdiwtlt.MediaLibrary.* +import static com.jdbernard.wdiwtlt.cli.CliErr.* public class CommandLineInterface { @@ -59,8 +61,6 @@ Configuration: private static Logger logger = LoggerFactory.getLogger(CommandLineInterface) - private static UPPERCASE_PATTERN = Pattern.compile(/(.)(\p{javaUpperCase})/) - private Properties cliConfig private MediaLibrary library @@ -90,9 +90,12 @@ Configuration: .cursorPrevLine().eraseLine(Erase.All) .cursorPrevLine().eraseLine(Erase.All).toString() - public final static modelClass = [ + public final static modelClasses = [ 'album': Album, 'artist': Artist, 'bookmark': Bookmark, - 'mediaFile': MediaFile, 'playlist': Playlist, 'tag': Tag ] + 'file': MediaFile, 'mediaFile': MediaFile, 'playlist': Playlist, + 'tag': Tag ] + + public final static selectableModels = 'album|artist|file|playlist|tag' private int displayWidth = 79 private long msgTimeout @@ -104,7 +107,7 @@ Configuration: private SimpleDateFormat sdf = new SimpleDateFormat('EEE-HH-SSS') /// Current play queue and selection data - Selection currentSelection = new Selection() + List currentSelection = [] Playlist playQueue Bookmark playBookmark MediaFile curMediaFile @@ -282,45 +285,51 @@ Configuration: if (new Date() > dismissMsgDate) resetStatus() if (consoleReadBuffer.size() > 0) { line = consoleReadBuffer.remove(0) - line.split(';').each { - processInput(it.trim().split(/\s/) as LinkedList) } - } else { + try { processInput(line) } + catch (CliErr cliErr) { + String errMsg = cliErr.message + if (ANSI.strip(errMsg).length() > 80) { + printLongMessage(errorStyle + errMsg + normalStyle) } + else { + status.text = errorStyle + errMsg + normalStyle + dismissMsgDate = new Date(new Date().time + msgTimeout) } } } + else { drawLeader() if (curMediaFile) { playBookmark.playTimeMs = vlcj.mediaListPlayer.mediaPlayer.time library.save(playBookmark) } - Thread.sleep(250) - } - } - } + Thread.sleep(250) } } } - private def processInput(LinkedList line) { + private def processInput(String line) { logger.debug("line: $line") - String command = line.poll() - logger.debug("command: $command") - switch(command?.toLowerCase()) { + + String[] parts = line.split(' ', 2) + String command = parts[0]?.toLowerCase() + String rest = parts.size() == 2 ? parts[1]?.trim() : null + logger.debug("command: ${command}") + switch(command) { + // Misc/utility case 'scan': return scanMediaLibrary() - case 'select': return processSelect(line, currentSelection) - case 'list': return processList(line, currentSelection) - case 'add': return processAdd(line) - case 'enqueue': return processEnqueue(line) - case 'remove': return processRemove(line) - case 'tag': return processTag(line) - case 'clear': return processClear(line) - 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 'jump': return processJump(line) + case 'list': return processList(rest, currentSelection) + case 'select': return processSelect(rest, currentSelection) + case 'play': return processPlay(rest, currentSelection) + case 'enqueue': return processEnqueue(rest, currentSelection) + case 'add': return processAdd(rest, currentSelection) + case 'remove': return processRemove(rest, currentSelection) + case 'tag': return processTag(rest, currentSelection) + case 'clear': return processClear(rest) + case 'pause': return processPause() + case 'stop': return processStop() + case 'next': return processNext(rest) + case 'prev': return processPrev(rest) + case 'jump': return processJump(rest) case 'ff': - case 'fastforward': return processFastForward(line) - case 'rw': - case 'rwd': - case 'rewind': return processRewind(line) + case 'fastforward': return processFastForward(rest) + case 'rw': case 'rwd': + case 'rewind': return processRewind(rest) case 'vol': - case 'volume': return processVolume(line) + case 'volume': return processVolume(rest) case 'debug': outStream.println( @@ -337,9 +346,7 @@ Configuration: return default: - status.text = errorStyle + - "Unrecognized command: '$line'${normalStyle}" - dismissMsgDate = new Date(new Date().time + msgTimeout) + err "Unrecognized command: '$line'" drawLeader() Thread.sleep(250) break @@ -349,137 +356,228 @@ Configuration: public MediaLibrary scanMediaLibrary() { status.text = "Scanning media library..." def counts = library.rescanLibrary() - status.text = "Scanned ${counts.total} files. " + - "Added ${counts.new} and ignored ${counts.ignored} files." - dismissMsgDate = new Date(new Date().time + msgTimeout) + msg("Scanned ${counts.total} files. Added ${counts.new} and " + + "ignored ${counts.ignored} files.") return library } - private def processSelect(LinkedList line, def sel = null) { - String option = line.poll() - boolean current = option == "current" - if (!sel) sel = new Selection() - def items + private String processList(String options, def selection) { + logger.debug("Listing things. Options: $options") - if (current) { - if (!curMediaFile) { - setErr "No media is currently playing." - return null } + if (options == 'bookmarks') selection = library.getBookmarks() + else if (options != 'selection') selection = select(options, selection) - option = line.poll() - switch (option) { - case 'album': - sel.album = ensureExactlyOne( - library.getAlbumsWhere({ mediaFileId: curMediaFile.id})) - return sel - case 'artist': - sel.artist = ensureExactlyOne( - library.getArtistsWhere({ mediaFileId: curMediaFile.id})) - return sel - case 'playlist': - sel.playlist = playQueue; return sel - case 'file': case 'mediaFile': - sel.mediaFile = curMediaFile; return sel - case 'tags': - sel.tags = library.getTagsWhere({ - mediaFileId: curMediaFile.id}) - return sel - default: - setErr("Unrecognized option to ${promptStyle}select " + - "current${errorStyle}.") - return null - } - } + if (!selection) msg "Nothing selected." + else return printLongMessage(makeList(selection, { "${it.id}: ${it} " })) } - switch (option) { - case 'file': - sel.mediaFile = ensureExactlyOne( - library.getByIdOrName(MediaFile, line.join(' '))) - return sel - case 'playlist': - sel.playlist = ensureExactlyOne( - library.getByIdOrName(Playlist, line.join(' '))) - sel.playlist.lastUsed = new Timestamp(new Date().time) - sel.playlist = library.save(sel.playlist) - return sel - case 'mediaFile': case 'album': case 'artist': - sel[option] = ensureExactlyOne( - library.getByIdOrName(modelClass[option], line.join(' '))) - return sel - case 'tags': - sel.tags = line.collect { library.getByIdOrName(Tag, it) } - .findAll().flatten() - return sel + private List processSelect(String options, List selection) { + currentSelection = select(options, selection) + if (!currentSelection) msg 'Nothing selected.' + else resetStatus() + logger.debug("currentSelection: $currentSelection") + return currentSelection } - default: - setErr("Unrecognized option to ${promptStyle}select${errorStyle}") - return null - } - } + private List select(String options, List selection = null) { - private def processAdd(LinkedList line) { + logger.debug("Selecting: {}\tselection: {}", options, selection) + List excludedTags = [] + List selectedTags = [] + Class modelClass - List parts = line.join(' ').split(' to playlist ').collect { it.trim() } + switch (options) { + case ~/playing ($selectableModels)s?/: + if (!curMediaFile) err "No media is currently playing." - if (parts.size() != 2) { - printLongMessage( - "Invalid options to the ${promptStyle}add${normalStyle}" + - " command. Use ${promptStyle}help add${normalStyle} to see a " + - "list of valid options.") - return } + modelClass = modelClasses[Matcher.lastMatcher[0][1]] + if (modelClass == MediaFile) return [curMediaFile] + else if (modelClass == Playlist) return playQueue + else return library.getWhere(modelClass, + [mediaFileId: curMediaFile.id]) - Selection selectionToAdd - Playlist targetPlaylist + /* TODO + case ~/files tagged as((\s\w+)+?) and not as((\s\w+)+)/: + excludedTags = lastMatcher[0][3].split(/\s/) + */ - if (parts[0] == "selection") { selectionToAdd = currentSelection } - else selectionToAdd = processSelect(parts[0].split(' ') as LinkedList) + case ~/selected ($selectableModels)s?/: - targetPlaylist = ensureExactlyOne( - library.getByIdOrName(Playlist, parts[1])) + if (!selection) err "Nothing is selected." - return library.addToPlaylist(targetPlaylist.id, - selection.selectedFiles.collect { it.id }) } + modelClass = modelClasses[Matcher.lastMatcher[0][1]] + Class selectionClass = selection[0].class - private def processEnqueue(LinkedList line) { - 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 }) + if (modelClass == selectionClass) return selection + return selection.collectMany { library.getWhere(modelClass, + [(idKeyFor(selectionClass)): it.id]) }.findAll() + + case ~/files tagged( as)?((\s\w+)+?)/: + selectedTags = Matcher.lastMatcher[0][2].split(/\s/) + .collect { it?.trim() }.findAll() + .collect { logger.debug("tag name: {}", it); library.getTagByName(it) }.findAll() + if (!selectedTags) err 'Nothing is selected.' + return library.getMediaFilesWhere(tags: selectedTags) + + case ~/($selectableModels) where (.+)/: + // TODO + err "select where ... is not yet implemented." + + case ~/($selectableModels)s((\s\d+)+)/: + modelClass = modelClasses[Matcher.lastMatcher[0][1]] + return Matcher.lastMatcher[0][2].split(/\s/) + .collect { safeToInteger(it) }.findAll() + .collect { library.getById(modelClass, it) }.findAll() + + case ~/($selectableModels) (.+)/: + modelClass = modelClasses[Matcher.lastMatcher[0][1]] + String nameOrId = Matcher.lastMatcher[0][2] + return [ensureExactlyOne( + library.getByIdOrName(modelClass, nameOrId))] + + case ~/($selectableModels)s/: + modelClass = modelClasses[Matcher.lastMatcher[0][1]] + return library.getAll(modelClass) + + case 'queue': return [playQueue] + case ~/queued ($selectableModels)s?/: + modelClass = modelClasses[Matcher.lastMatcher[0][1]] + return library.getWhere(modelClass, [playlistId: playQueue.id]) + default: + logger.debug("Invalid select options: $options") + err "Invalid options to the ${promptStyle}select${normalStyle}" + + " command. Use ${promptStyle}help select${normalStyle} to " + + "see a list of valid options." } } + + private Playlist processPlay(String options, List selection) { + + switch (options) { + case null: selection = null + case ~/selection/: break + case ~/bookmark (.+)/: + String nameOrId = Matcher.lastMatcher[0][1] + Bookmark b = ensureExactlyOne( + library.getByIdOrName(Bookmark, nameOrId)) + if (!b) err "No bookmark matches '$nameOrId'." + + Playlist p = library.getPlaylistById(b.playlistId) + if (!p) err 'The playlist for this bookmark no longer exists.' + + setPlayQueue(p) + vlcj.mediaListPlayer.playItem(b.playIndex) + + if (b.playTimeMs > 0) + vlcj.mediaListPlayer.mediaPlayer.time = b.playTimeMs + + return p; + + default: selection = select(options, selection) } + + if (selection) { + List mediaFiles = library.collectMediaFiles(selection) + playQueue = library.removeAllFromPlaylist(playQueue.id) + library.addToPlaylist(playQueue.id, mediaFiles.collect { it.id }) + setPlayQueue(playQueue) } + + vlcj.mediaListPlayer.play() return playQueue } - private def processRemove(LinkedList line) { - String[] parts = line.join(' ').split(' from ') - Playlist playlist - def selection + private List processEnqueue(String options, + List selection = null) { + if (options != 'selection') selection = select(options, selection) + if (!selection) err "Nothing is selected." + List enqueued = enqueue(selection) + msg "${enqueued.size()} files added to the current play queue." + return enqueued } - if (parts.length == 1 || parts[1] == 'queue') playlist = playQueue - else if (parts.length == 2) // TODO: shoule ensure starts with 'playlist' - playlist = processSelect(parts[1].split(' ') as LinkedList) + private List enqueue(List items) { + if (!items) return playQueue - selection = processSelect(parts[0].split(' ') as LinkedList) + List mediaFiles = library.collectMediaFiles(items) + library.addToPlaylist(playQueue.id, mediaFiles.collect { it.id }) + mediaFiles.each { + vlcj.mediaList.addMedia( + new File(library.libraryRoot, it.filePath).canonicalPath) } - if (selection) selection.selectedFiles.each { file -> - library.removeFromPlaylist(playlist.id, file.id) } } + return mediaFiles } - private def processTag(LinkedList line) { - List parts = line.join(' ').split(' as ').collect { it.trim() } + private List processAdd(String options, + List selection = null) { - if (parts.size() != 2) { - printLongMessage( - "Invalid options to the ${promptStyle}tag${normalStyle}" + - " command. Use ${promptStyle}help tag${normalStyle} to see a " + - "list of valid options.") - return } + Playlist p + def m = (options =~ /(.+) to playlist (.+)/) + p = getExactlyOne(m[0][2]) + if (!p) err "No playlist for '${Matcher.lastMatcher[0][1]}'." + if (m[0][1] != "selection") selection = select(options, selection) - Selection selection = processSelect(parts[0].split(' ') as LinkedList) - library.tagMediaFiles(selection.selectedFiles.collect { it.id }, - parts[1].split(' ') as List) } + if (!selection) err 'Nothing selected to add.' - private def processClear(LinkedList line) { - def option = line.poll() - switch(option) { + List mediaFiles = library.collectMediaFiles(selection) + library.addToPlaylist(p.id, mediaFiles.collect { it.id }) + msg "${mediaFiles.size()} media files added to '${p.name}'." + return added } + + private def processRemove(String options, List selection = null) { + + def m = (options =~ /(.+) from (.+)/) + String removeFrom = m[0][2] + + if (m[0][1] != 'selection') selection = select(m[0][1], selection) + if (!selection) err 'Nothing was selected to be removed.' + + if (removeFrom == 'queue') p = playQueue + else if (removeFrom.startsWith('playlist')) { + String[] parts = removeFrom.split(/\s/, 2) + if (parts.size() < 2) err 'No playlist id or name given.' + p = getExactlyOne(Playlist, parts[1]) } + + List toRemove = library.collectMediaFiles(selection) + toRemove.each { library.removeFromPlaylist(p.id, it.id) } + + // Reset our queue if we removed from the queue + if (removeFrom == 'queue') { + vlcj.mediaListPlayer.stop() + setPlayQueue(playQueue) + + // Restart playback with the file that was playing before we + // removed stuff (may not be there anymore) + if (playBookmark) { + MediaFile mf = library.getMediaFileById(playBookmark.mediaFileId) + List playlistMFs = library.getMediaFilesWhere( + playlistId: playQueue.id) + int index = playlistMFs.indexOf(mf) + if (index > 0) { + vlcj.mediaListPlayer.playItem(index) + if (playBookmark.playTimeMs > 0) { + vlcj.mediaListPlayer.mediaPlayer.time = + playBookmark.playTimeMs } } } } + + msg "Removed ${toRemove.size()} files." + return toRemove } + + private def processTag(String options, List selection) { + + String[] parts = options.split(' as ', 2) + + List tags + + // Short form: tag ... + if (parts.size() == 1) { + if (!curMediaFile) err 'Nothing currently playing to tag.' + selection = [curMediaFile] + tags = parts[0].split(/\s/) } + + else { + if (parts[0] != 'selection') selection = select(parts[0], selection) + tags = parts[1].split(/\s/) } + + List mediaFiles = library.collectMediaFiles(selection) + library.tagMediaFiles(mediaFiles.collect { it.id }, tags) + + msg "Tagged '${mediaFiles.size()}' files as $tags." + return mediaFiles } + + private def processClear(String options) { + + switch(options) { case null: print(new ANSI().eraseDisplay(Erase.All))//.cursorPosition(4, 0)) drawLeader(true) @@ -488,260 +586,56 @@ Configuration: playQueue = library.removeAllFromPlaylist(playQueue.id) setPlayQueue(playQueue) return playQueue - case 'selected playlist': - if (!currentSelection.playlist) { - printLongMessage("No playlist currently selected.") - return null } - return library.removeAllFromPlaylist(selected.playlist.id) - case 'playlist': - def criteria = line.poll() - if (!criteria) { - printLongMessage("Missing playlist id or name. Use " + - "${promptStyle}help clear${normalStyle} to see a " + - "list of valid options.") - return null } - def playlist = library.getByIdOrName(Playlist, criteria) - if (!playlist) { - printLongMessage("No playlist matches '${criteria}'.") - return null } - return library.removeAllFromPlaylist(playlist.id) - case 'selection': - currentSelection = new Selection() - break + case ~/playlist (.+)/: + Playlist p = getExactlyOne(Playlist, Matcher.lastMatcher[0][1]) + if (!p) err "No playlist for '${Matcher.lastMatcher[0][1]}'." + return library.removeAllFromPlaylist(p.id) + case 'selection': currentSelection = []; resetStatus(); break default: - printLongMessage("Unrecognized option to the ${promptStyle}" + - "clear${normalStyle} command. Use ${promptStyle}help clear" + - "${normalStyle} to see a list of valid options.") - return null - } - } + err "Unrecognized option to the ${promptStyle}clear" + + "${normalStyle} command. Use ${promptStyle}help clear" + + "${normalStyle} to see a list of valid options." } } - private def processList(LinkedList options, def selection) { - logger.debug("Listing things. Options: $options") - if (!selection) selection = new Selection() - def option = options.poll() - boolean all = option == 'all' - boolean current = option == 'current' - if (all || current) option = options.poll() + private def processPause() { vlcj.mediaListPlayer.pause() } - logger.debug("Option: $option") + private def processStop() { vlcj.mediaListPlayer.stop() } - def list - - switch(option) { - case 'albums': - if (all) list = library.getAlbums() - else if (current) list = library.getAlbumsWhere( - playlistId: playQueue.id) - else if (selection.album) list = [selection.album] - else list = library.getAlbumsWhere( - playlistId: selection.playlist?.id, - artistId: selection.artist?.id, - mediaFileId: selection.mediaFile?.id) - - String albumMatch = options?.join(" ")?.trim() - if (albumMatch) list = list.findAll { it.name =~ albumMatch } - printLongMessage(makeList(selection, Album, list, all, - { "${it.id}: ${it}" })) - break - - case 'artists': - if (all) list = library.getArtists() - else if (current) list = library.getArtistsWhere( - playlistId: playQueue.id) - else if (selection.artist) list = [selection.artist] - else list = library.getArtistsWhere( - playlistId: selection.playlist?.id, - albumId: selection.album?.id, - mediaFileId: selection.mediaFile?.id) - - String artistMatch = options?.join(" ")?.trim() - if (artistMatch) list = list.findAll { it.name =~ artistMatch } - printLongMessage(makeList(selection, Artist, list, all, - { "${it.id}: ${it}" })) - break - - case 'files': - case 'selection': - if (all) list = library.getMediaFiles() - else if (current) list = library.getMediaFilesWhere( - playlistId: playQueue.id) - else list = selection.selectedFiles - - if (selection.album) list = list.sort { it.trackNumber } - String mediaFileMatch = options?.join(" ")?.trim() - if (mediaFileMatch) list = list.findAll { - it.name =~ mediaFileMatch } - printLongMessage(makeList(selection, MediaFile, list, all, - { "${it.id}: ${it.trackNumber} - ${it}" })) - break - - case 'bookmarks': - if (all) list = library.getBookmarks() - else if (current) list = library.getBookmarksWhere( - playlistId: playQueue.id) - else list = library.getBookmarksWhere( - playlistId: selection?.playlist?.id) - - String bookmarkMatch = options?.join(" ")?.trim() - if (bookmarkMatch) - list = list.findAll { it.name =~ bookmarkMatch } - printLongMessage(makeList(selection, Bookmark, list, all, - { "${it.id}: ${it} ${it.userCreated ? '' : ' (auto)'}" })) - break - - case 'playlists': - if (all) list = library.getPlaylists() - else if (current) list = [playQueue] - else list = library.getPlaylistsWhere( - artistId: selection?.artist?.id, - albumId: selection?.album?.id, - mediaFileId: selection?.mediaFile?.id) - - String playlistMatch = options?.join(" ")?.trim() - if (playlistMatch) - list = list.findAll { it.name =~ playlistMatch } - printLongMessage(makeList(selection, Playlist, list, all, - { "${it.id}: ${it} ${it.userCreated ? '' : ' (auto)'}" })) - break - - case 'tags': - if (all) list = library.getTags() - else if (current) list = library.getTagsWhere( - playlistId: playQueue.id) - else list = library.getTagsWhere( - playlistId: selection?.playlist?.id, - artistId: selection?.artist?.id, - albumId: selection?.album?.id) - - String tagMatch = options?.join(" ")?.trim() - if (tagMatch) list = list.findAll { it.name =~ tagMatch } - printLongMessage(makeList(selection, Tag, list, all, - { "${it.id}: ${it}" })) - break - - default: - printLongMessage("Unrecognized option to the ${promptStyle}" + - "list${normalStyle} command. Use ${promptStyle}help list" + - "${normalStyle} to see a list of valid options.") - return null - } - - resetStatus() - 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 } + private def processNext(String rest) { + def count + try { count = rest ? rest as int : 1 } + catch (Exception e) { err "$count is not a valid number" } 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 } + private def processPrev(String rest) { + try { count = rest ? rest as int : 1 } + catch (Exception e) { err "$count is not a valid number" } vlcj.mediaListPlayer.stop() if ((playBookmark.playIndex - count) >= 0) vlcj.mediaListPlayer.playItem(playBookmark.playIndex - count) } - private def processJump(LinkedList line) { - String errMsg = - "Invalid options to the ${promptStyle}jump${normalStyle}" + - " command. Use ${promptStyle}help jump${normalStyle} to see a " + - "list of valid options." - - String to = line.poll() - if (!to || to.trim() != "to") { printLongMessage(errMsg); return } - - def criteria = line.poll() - if (!criteria) { printLongMessage(errMsg); return } - - def target = getExactlyOne(MediaFile, criteria) - if (!target) return + private def processJump(String options) { + MediaFile target = getExactlyOne(MediaFile, options) + if (!target) err "No media file matches '${options}'." int index = library.getMediaFilesWhere(playlistId: playQueue.id) - .findIndexOf { it.id == target.id } - if (index < 0) { - setErr "'$target' is not in the current play queue."; return } - - printLongMessage(""" - target: $target - index: $index""") + .indexOf(target) + if (index < 0) err "'$target' is not in the current play queue." vlcj.mediaListPlayer.mediaPlayer.stop() vlcj.mediaListPlayer.playItem(index) } - private def processFastForward(LinkedList line) { - def amount = line.poll() - def unit = line.poll()?.trim() + private def processFastForward(String rest) { + String[] parts = rest.split(' ') + String amount = parts.size() > 0 ? parts[0]?.trim() : null + String unit = parts.size() > 1 ? parts[1]?.trim() : null 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 } + catch (Exception e) { err "$amount must be an integer." } switch (unit) { case 'ms': case 'millis': case 'millisecond': case 'milliseconds': @@ -753,20 +647,20 @@ Configuration: case 'm': case 'min': case 'minute': case 'minutes': vlcj.mediaListPlayer.mediaPlayer.skip(amount * 60000) break - default: setErr "$unit must be one of 'milliseconds' " + + default: err "$unit must be one of 'milliseconds' " + "(or 'millis' or 'ms'), 'seconds' (or 'sec' or 's'), or " + - "'minutes' (or 'min' or 'm')" - return } } + "'minutes' (or 'min' or 'm')" } } - private def processRewind(LinkedList line) { - def amount = line.poll() - def unit = line.poll()?.trim() + private def processRewind(String rest) { + String[] parts = rest.split(' ') + String amount = parts.size() > 0 ? parts[0]?.trim() : null + String unit = parts.size() > 1 ? parts[1]?.trim() : null 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 } + catch (Exception e) { err "$amount must be an integer." } switch (unit) { case 'ms': case 'millis': case 'millisecond': case 'milliseconds': @@ -778,22 +672,21 @@ Configuration: case 'm': case 'min': case 'minute': case 'minutes': vlcj.mediaListPlayer.mediaPlayer.skip(amount * 60000) break - default: setErr "$unit must be one of 'milliseconds' " + + default: err "$unit must be one of 'milliseconds' " + "(or 'millis' or 'ms'), 'seconds' (or 'sec' or 's'), or " + - "'minutes' (or 'min' or 'm')" - return } } + "'minutes' (or 'min' or 'm')" } } - private def processVolume(LinkedList line) { - def percentage = line.poll() - if (!percentage) - setMsg("Volume: ${vlcj.mediaListPlayer.mediaPlayer.volume}") + private def processVolume(String rest) { + int percentage - else { - try { percentage = Math.min(Math.max(0, percentage as int), 200) } + if (rest) { + try { percentage = Math.min(Math.max(0, rest as int), 200) } catch (Exception e) { - setErr("Volume must be and integer between 0 and 200.") + err "Volume must be and integer between 0 and 200." return } - vlcj.mediaListPlayer.mediaPlayer.volume = percentage } } + vlcj.mediaListPlayer.mediaPlayer.volume = percentage } + + msg "Volume: ${vlcj.mediaListPlayer.mediaPlayer.volume}" } private void playing(def player) { try { @@ -827,7 +720,8 @@ Configuration: public void setPlayQueue(Playlist p) { if (vlcj.mediaListPlayer.isPlaying()) vlcj.mediaListPlayer.stop() - playQueue = p + p.lastUsed = new Timestamp(new Date().time) + playQueue = library.update(p) vlcj.mediaList.clear() @@ -836,19 +730,15 @@ Configuration: new File(library.libraryRoot, it.filePath).canonicalPath) } } public def ensureExactlyOne(def matches) { - if (!matches) { - setErr("Nothing matches."); - return null } + if (!matches) err "Nothing matches." - String englishName = toEnglish(matches[0].class.simpleName) - if (matches.size() > 1) { - setErr("Multiple ${englishName}s match: " + - matches.collect { "${it.id}: ${it.name}" }.join(', ')) - return null } + String englishName = toEnglish(matches[0].class) + if (matches.size() > 1) err "Multiple ${englishName}s match: " + + matches.collect { "${it.id}: ${it.name}" }.join(', ') return matches[0] } - public def getExactlyOne(Class modelClass, def criteria) { + public def getExactlyOne(Class modelClass, String nameOrId) { return ensureExactlyOne(library.getByIdOrName(modelClass, criteria)) } private void drawLeader(afterOutput = false) { @@ -861,7 +751,7 @@ Configuration: outStream.print(leader) outStream.flush() } - private void printLongMessage(String msg) { + private String printLongMessage(String msg) { String result = new StringBuilder() .append(eraseLeader) .append(msg) @@ -869,7 +759,8 @@ Configuration: .toString() outStream.println result - drawLeader(true) } + drawLeader(true) + return result } private String getLeader() { StringBuilder leader = new StringBuilder() @@ -910,14 +801,10 @@ Configuration: return leader.toString() } - private String setMsg(String msg) { + private String msg(String msg) { status.text = msg dismissMsgDate = new Date(new Date().time + msgTimeout) } - private String setErr(String errMsg) { - status.text = errorStyle + errMsg - dismissMsgDate = new Date(new Date().time + msgTimeout) } - private String makeFullMediaFileDescription(MediaFile mf) { def artist = library.getArtistsWhere( mediaFileId: mf.id) def album = library.getAlbumsWhere(mediaFileId: mf.id) @@ -941,121 +828,51 @@ Configuration: return s.toString() } - private String makeList(Selection selection, Class modelClass, - def items, boolean listAll = false, Closure toString = null) { + private String makeList(List items, Closure toString = null) { + + if (!items) return "No items to list." if (!toString) toString = { it.toString() } + Class modelClass = items[0].class def currentCollection if (curMediaFile) { - def lookupFun - - switch (modelClass) { - case Album: lookupFun = library.&getAlbumsWhere; break - case Artist: lookupFun = library.&getArtistsWhere; break - case Playlist: lookupFun = library.&getPlaylistsWhere; break - case MediaFile: lookupFun = { ign -> [curMediaFile] }; break - case Tag: lookupFun = library.&getTagsWhere; break - default: lookupFun = { ign -> [] } } - - currentCollection = lookupFun([mediaFileId: curMediaFile.id]) } + if (modelClass == MediaFile) currentCollection = [curMediaFile] + else currentCollection = library.getWhere( + modelClass, [mediaFileId: curMediaFile.id]) } def highlightSelected = { item -> - if (currentCollection && currentCollection.find {it.id == item.id}) + if (currentCollection && currentCollection.contains(item)) return "${promptStyle}${toString(item)}${normalStyle}" else return toString(item) } def result = new StringBuilder() - .append("--------------------\n${modelClass.simpleName}s") + .append("--------------------\n${modelClass.simpleName}s:\n\n") - if (!listAll && (selection.playlist || selection.artist || - selection.mediaFile)) - result.append("\n(for selection: ") - .append(selection.toString()) - .append(normalStyle) - .append(")") - - result.append(":\n\n") result.append(items.collect(highlightSelected).join("\n")) .append("\n\n") return result.toString() } private String resetStatus() { - String s = currentSelection.toString() - if (s.size() == 0) status.text = "No current media selections." - else status.text = s + if (currentSelection) { + if (currentSelection.size() == 1) { + String s + Model m = currentSelection[0] + switch (m.class) { + case Album: s = "$albumStyle$m"; break + case Artist: s = "$artistStyle$m"; break + case Playlist: s = "$playlistStyle$m"; break + case MediaFile: + s = makeFullMediaFileDescription(currentSelection[0]) + break + case Tag: s = "Tag: $m" } + status.text = s } + else status.text = "${currentSelection.size()} " + + "${toEnglish(currentSelection[0].class)}s selected." } return status.text } - private static String uncapitalize(String s) { - if (s == null) return null; - if (s.length() < 2) return s.toLowerCase(); - return s[0].toLowerCase() + s[1..-1] } - - private static String toEnglish(String modelName) { - return UPPERCASE_PATTERN.matcher(modelName). - replaceAll(/$1 $2/).toLowerCase() } - - public class Selection { - Album album - Artist artist - MediaFile mediaFile - Playlist playlist - List tags - - public def unselect(Class modelClass) { - String key = uncapitalize(modelClass.simpleName) - def value = this[key] - this[key] = null - return value } - - public Selection select(Map s) { - ['artist', 'album', 'playlist', 'file', 'tags'].each { - this[it] = s[it] } - return this - } - - public def select(def value) { - if (value) this[uncapitalize(match.class.simpleName)] = value - return value } - - public List getSelectedFiles() { - if (album || artist || mediaFile || playlist || tags) - return library.getMediaFilesWhere( - playlistId: playlist?.id, - artistId: artist?.id, - albumId: album?.id, - tags: tags) - return null } - - public String toString() { - StringBuilder s = new StringBuilder() - - if (playlist) s.append(playlistStyle) - .append(playlist) - .append(normalStyle) - .append(": ") - - if (artist) s.append(artistStyle) - .append(artist) - .append(normalStyle) - .append(" / ") - - if (album) s.append(albumStyle) - .append(album) - .append(normalStyle) - .append(" / ") - - if (mediaFile) s.append(fileStyle) - .append(mediaFile) - .append(normalStyle) - - return s.toString() - } - - - } }