diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/general-util.js | 286 | ||||
-rwxr-xr-x | src/play.js | 349 |
2 files changed, 349 insertions, 286 deletions
diff --git a/src/general-util.js b/src/general-util.js index 825dd90..278eea6 100644 --- a/src/general-util.js +++ b/src/general-util.js @@ -1,8 +1,20 @@ const { promisify } = require('util') const fs = require('fs') const fetch = require('node-fetch') +const clone = require('clone') +const processArgv = require('./process-argv') +const { processSmartPlaylist } = require('./smart-playlist') const readFile = promisify(fs.readFile) +const writeFile = promisify(fs.writeFile) + +// TODO: Check which of these are actually used. For now stolen from play.js, +// along with the zillion functions that use at least some of these. +const { + filterPlaylistByPathString, removeGroupByPathString, getPlaylistTreeString, + updatePlaylistFormat, collapseGrouplike, filterGrouplikeByProperty, isTrack, + flattenGrouplike +} = require('./playlist-utils') module.exports.showTrackProcessStatus = function( total, doneCount, noLineBreak = false @@ -29,7 +41,7 @@ function downloadPlaylistFromLocalPath(path) { return readFile(path).then(buf => buf.toString()) } -module.exports.downloadPlaylistFromOptionValue = function(arg) { +function downloadPlaylistFromOptionValue (arg) { // TODO: Verify things! if (arg.startsWith('http://') || arg.startsWith('https://')) { return downloadPlaylistFromURL(arg) @@ -37,3 +49,275 @@ module.exports.downloadPlaylistFromOptionValue = function(arg) { return downloadPlaylistFromLocalPath(arg) } } + +Object.assign(module.exports, { + downloadPlaylistFromOptionValue +}) + +module.exports.makePlaylistOptions = function() { + let sourcePlaylist = null + let activePlaylist = null + + // Whether or not a playlist has been opened yet. This is just used to + // decide when exactly to load the default playlist. (We don't want to load + // it as soon as the process starts, since there might be an --open-playlist + // option that specifies opening a *different* playlist! But if we encounter + // an action that requires a playlist, and no playlist has yet been opened, + // we assume that the user probably wants to do something with the default + // playlist, and that's when we open it. See requiresOpenPlaylist for the + // implementation of this.) + let hasOpenedPlaylist = false + + const openPlaylist = async function (arg, silent = false) { + // Takes a playlist download argument and loads it as the source and + // active playlist. + + let playlistText + + if (!silent) { + console.log("Opening playlist from: " + arg) + } + + try { + playlistText = await downloadPlaylistFromOptionValue(arg) + } catch(err) { + if (!silent) { + console.error("Failed to open playlist file: " + arg) + console.error(err) + } + + return false + } + + const importedPlaylist = JSON.parse(playlistText) + + hasOpenedPlaylist = true + + await loadPlaylist(importedPlaylist) + } + + const loadPlaylist = async function (importedPlaylist) { + // Takes an actual playlist object and sets it up as the source and active + // playlist. + + const openedPlaylist = updatePlaylistFormat(importedPlaylist) + + // We also want to de-smart-ify (stupidify? - simplify?) the playlist. + const processedPlaylist = await processSmartPlaylist(openedPlaylist) + + // ..And finally, we have to update the playlist format again, since + // processSmartPlaylist might have added new (un-updated) items: + const finalPlaylist = updatePlaylistFormat(processedPlaylist, true) + // We also pass true so that the playlist-format-updater knows that this + // is the source playlist. + + sourcePlaylist = finalPlaylist + + // The active playlist is a clone of the source playlist; after all it's + // quite possible we'll be messing with the value of the active playlist, + // and we don't want to reflect those changes in the source playlist. + activePlaylist = clone(sourcePlaylist) + + await processArgv(processedPlaylist.options, optionFunctions) + } + + const requiresOpenPlaylist = async function() { + if (activePlaylist === null) { + if (hasOpenedPlaylist === false) { + await openDefaultPlaylist() + } else { + throw new Error( + "This action requires an open playlist - try --open (file)" + ) + } + } + } + + const openDefaultPlaylist = function() { + return openPlaylist('./playlist.json', true) + } + + const optionFunctions = { + '-open-playlist': async function(util) { + // --open-playlist <file> (alias: --open, -o) + // Opens a separate playlist file. + // This sets the source playlist. + + await openPlaylist(util.nextArg()) + }, + + '-open': util => util.alias('-open-playlist'), + 'o': util => util.alias('-open-playlist'), + + '-open-playlist-string': async function(util) { + // --open-playlist-string <string> + // Opens a playlist, using the given string as the JSON text of the + // playlist. This sets the source playlist. + + await loadPlaylist(JSON.parse(util.nextArg())) + }, + + '-playlist-string': util => util.alias('-open-playlist-string'), + + '-write-playlist': async function(util) { + // --write-playlist <file> (alias: --write, -w, --save) + // Writes the active playlist to a file. This file can later be used + // with --open <file>; you won't need to stick in all the filtering + // options again. + + await requiresOpenPlaylist() + + const playlistString = JSON.stringify(activePlaylist, null, 2) + const file = util.nextArg() + + console.log(`Saving playlist to file ${file}...`) + + await writeFile(file, playlistString) + + console.log("Saved.") + }, + + '-write': util => util.alias('-write-playlist'), + 'w': util => util.alias('-write-playlist'), + '-save': util => util.alias('-write-playlist'), + + '-print-playlist': async function(util) { + // --print-playlist (alias: --log-playlist, --json) + // Prints out the JSON representation of the active playlist. + + await requiresOpenPlaylist() + + console.log(JSON.stringify(activePlaylist, null, 2)) + }, + + '-log-playlist': util => util.alias('-print-playlist'), + '-json': util => util.alias('-print-playlist'), + + '-clear': async function(util) { + // --clear (alias: -c) + // Clears the active playlist. This does not affect the source + // playlist. + + await requiresOpenPlaylist() + + activePlaylist.items = [] + }, + + 'c': util => util.alias('-clear'), + + '-keep': async function(util) { + // --keep <groupPath> (alias: -k) + // Keeps a group by loading it from the source playlist into the + // active playlist. This is usually useful after clearing the + // active playlist; it can also be used to keep a subgroup when + // you've removed an entire parent group, e.g. `-r foo -k foo/baz`. + + await requiresOpenPlaylist() + + const pathString = util.nextArg() + const group = filterPlaylistByPathString(sourcePlaylist, pathString) + + if (group) { + activePlaylist.items.push(group) + } + }, + + 'k': util => util.alias('-keep'), + + '-remove': async function(util) { + // --remove <groupPath> (alias: -r, -x) + // Filters the playlist so that the given path is removed. + + await requiresOpenPlaylist() + + const pathString = util.nextArg() + console.log("Ignoring path: " + pathString) + removeGroupByPathString(activePlaylist, pathString) + }, + + 'r': util => util.alias('-remove'), + 'x': util => util.alias('-remove'), + + '-filter': async function(util) { + // --filter <filterJSON> + // Filters the playlist so that only tracks that match the given filter + // are kept. FilterJSON should be a JSON object as described in the + // man page section "filters". + + const filterJSON = util.nextArg() + + let filterObj + try { + filterObj = JSON.parse(filterJSON) + } catch (error) { + console.error('Invalid JSON for filter:', filterJSON) + return + } + + activePlaylist.filters = [filterObj] + activePlaylist = await processSmartPlaylist(activePlaylist) + activePlaylist = updatePlaylistFormat(activePlaylist) + }, + + 'f': util => util.alias('-filter'), + + '-collapse-groups': async function() { + // --collapse-groups (alias: --collapse) + // Collapses groups in the active playlist so that there is only one + // level of sub-groups. Handy for shuffling the order groups play in; + // try `--collapse-groups --sort shuffle-groups`. + + await requiresOpenPlaylist() + + activePlaylist = updatePlaylistFormat(collapseGrouplike(activePlaylist)) + }, + + '-collapse': util => util.alias('-collapse-groups'), + + '-flatten-tracks': async function() { + // --flatten-tracks (alias: --flatten) + // Flattens the entire active playlist, so that only tracks remain, + // and there are no groups. + + await requiresOpenPlaylist() + + activePlaylist = updatePlaylistFormat(flattenGrouplike(activePlaylist)) + }, + + '-flatten': util => util.alias('-flatten-tracks'), + + '-list-groups': async function(util) { + // --list-groups (alias: -l, --list) + // Lists all groups in the playlist. + + await requiresOpenPlaylist() + + console.log(getPlaylistTreeString(activePlaylist)) + }, + + '-list': util => util.alias('-list-groups'), + 'l': util => util.alias('-list-groups'), + + '-list-all': async function(util) { + // --list-all (alias: --list-tracks, -L) + // Lists all groups and tracks in the playlist. + + await requiresOpenPlaylist() + + console.log(getPlaylistTreeString(activePlaylist, true)) + + }, + + '-list-tracks': util => util.alias('-list-all'), + 'L': util => util.alias('-list-all'), + } + + return { + optionFunctions, + openDefaultPlaylist, + getStuff: { + get hasOpenedPlaylist() { return hasOpenedPlaylist }, + get activePlaylist() { return activePlaylist } + } + } +} diff --git a/src/play.js b/src/play.js index 30a151e..dd49afe 100755 --- a/src/play.js +++ b/src/play.js @@ -4,7 +4,6 @@ const { promisify } = require('util') const { spawn } = require('child_process') -const clone = require('clone') const fs = require('fs') const fetch = require('node-fetch') const commandExists = require('./command-exists') @@ -12,20 +11,9 @@ const startLoopPlay = require('./loop-play') const processArgv = require('./process-argv') const promisifyProcess = require('./promisify-process') const { processSmartPlaylist } = require('./smart-playlist') - -const { - filterPlaylistByPathString, removeGroupByPathString, getPlaylistTreeString, - updatePlaylistFormat, collapseGrouplike, filterGrouplikeByProperty, isTrack, - flattenGrouplike -} = require('./playlist-utils') - -const { - downloadPlaylistFromOptionValue -} = require('./general-util') - -const { - compileKeybindings, getComboForCommand, stringifyCombo -} = require('./keybinder') +const { filterPlaylistByPathString, isTrack, flattenGrouplike } = require('./playlist-utils') +const { compileKeybindings, getComboForCommand, stringifyCombo } = require('./keybinder') +const { makePlaylistOptions } = require('./general-util') const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) @@ -60,7 +48,6 @@ async function determineDefaultConverter() { async function main(args) { let sourcePlaylist = null - let activePlaylist = null let pickerSortMode = 'shuffle' let pickerLoopMode = 'loop-regenerate' @@ -93,16 +80,6 @@ async function main(args) { // The file to output the playlist path to the current file. let trackDisplayFile - // Whether or not a playlist has been opened yet. This is just used to - // decide when exactly to load the default playlist. (We don't want to load - // it as soon as the process starts, since there might be an --open-playlist - // option that specifies opening a *different* playlist! But if we encounter - // an action that requires a playlist, and no playlist has yet been opened, - // we assume that the user probably wants to do something with the default - // playlist, and that's when we open it. See requiresOpenPlaylist for the - // implementation of this.) - let hasOpenedPlaylist = false - const keybindings = [ [['space'], 'togglePause'], [['left'], 'seek', -5], @@ -119,59 +96,6 @@ async function main(args) { [['q'], 'quit'], [['Q'], 'quit'] ] - async function openPlaylist(arg, silent = false) { - // Takes a playlist download argument and loads it as the source and - // active playlist. - - let playlistText - - if (!silent) { - console.log("Opening playlist from: " + arg) - } - - try { - playlistText = await downloadPlaylistFromOptionValue(arg) - } catch(err) { - if (!silent) { - console.error("Failed to open playlist file: " + arg) - console.error(err) - } - - return false - } - - const importedPlaylist = JSON.parse(playlistText) - - hasOpenedPlaylist = true - - await loadPlaylist(importedPlaylist) - } - - async function loadPlaylist(importedPlaylist) { - // Takes an actual playlist object and sets it up as the source and active - // playlist. - - const openedPlaylist = updatePlaylistFormat(importedPlaylist) - - // We also want to de-smart-ify (stupidify? - simplify?) the playlist. - const processedPlaylist = await processSmartPlaylist(openedPlaylist) - - // ..And finally, we have to update the playlist format again, since - // processSmartPlaylist might have added new (un-updated) items: - const finalPlaylist = updatePlaylistFormat(processedPlaylist, true) - // We also pass true so that the playlist-format-updater knows that this - // is the source playlist. - - sourcePlaylist = finalPlaylist - - // The active playlist is a clone of the source playlist; after all it's - // quite possible we'll be messing with the value of the active playlist, - // and we don't want to reflect those changes in the source playlist. - activePlaylist = clone(sourcePlaylist) - - await processArgv(processedPlaylist.options, optionFunctions) - } - async function openKeybindings(arg, add = true) { console.log("Opening keybindings from: " + arg) @@ -198,95 +122,36 @@ async function main(args) { keybindings.unshift(...openedKeybindings) } - async function requiresOpenPlaylist() { - if (activePlaylist === null) { - if (hasOpenedPlaylist === false) { - await openDefaultPlaylist() - } else { - throw new Error( - "This action requires an open playlist - try --open (file)" - ) - } - } - } + const { + optionFunctions, getStuff, + openDefaultPlaylist + } = await makePlaylistOptions() - function openDefaultPlaylist() { - return openPlaylist('./playlist.json', true) - } + const { + '-write-playlist': originalWritePlaylist, + '-print-playlist': originalPrintPlaylist, + '-list-groups': originalListGroups, + '-list-all': originalListAll + } = optionFunctions - const optionFunctions = { - '-help': function(util) { - // --help (alias: -h, -?) - // Presents a help message. + Object.assign(optionFunctions, { - console.log('http-music\nTry man http-music!') - - if (util.index === util.argv.length - 1) { - shouldPlay = false - } - }, - - 'h': util => util.alias('-help'), - '?': util => util.alias('-help'), - - '-open-playlist': async function(util) { - // --open-playlist <file> (alias: --open, -o) - // Opens a separate playlist file. - // This sets the source playlist. - - await openPlaylist(util.nextArg()) - }, - - '-open': util => util.alias('-open-playlist'), - 'o': util => util.alias('-open-playlist'), - - '-open-playlist-string': async function(util) { - // --open-playlist-string <string> - // Opens a playlist, using the given string as the JSON text of the - // playlist. This sets the source playlist. - - await loadPlaylist(JSON.parse(util.nextArg())) - }, - - '-playlist-string': util => util.alias('-open-playlist-string'), + // Extra play-specific behavior ------------------------------------------- '-write-playlist': async function(util) { - // --write-playlist <file> (alias: --write, -w, --save) - // Writes the active playlist to a file. This file can later be used - // with --open <file>; you won't need to stick in all the filtering - // options again. - - await requiresOpenPlaylist() - - const playlistString = JSON.stringify(activePlaylist, null, 2) - const file = util.nextArg() - - console.log(`Saving playlist to file ${file}...`) + await originalWritePlaylist(util) - return writeFile(file, playlistString).then(() => { - console.log("Saved.") - - // If this is the last option, the user probably doesn't actually - // want to play the playlist. (We need to check if this is len - 2 - // rather than len - 1, because of the <file> option that comes - // after --write-playlist.) - if (util.index === util.argv.length - 2) { - shouldPlay = false - } - }) + // If this is the last option, the user probably doesn't actually + // want to play the playlist. (We need to check if this is len - 2 + // rather than len - 1, because of the <file> option that comes + // after --write-playlist.) + if (util.index === util.argv.length - 2) { + shouldPlay = false + } }, - '-write': util => util.alias('-write-playlist'), - 'w': util => util.alias('-write-playlist'), - '-save': util => util.alias('-write-playlist'), - '-print-playlist': async function(util) { - // --print-playlist (alias: --log-playlist, --json) - // Prints out the JSON representation of the active playlist. - - await requiresOpenPlaylist() - - console.log(JSON.stringify(activePlaylist, null, 2)) + await originalPrintPlaylist(util) // As with --write-playlist, the user probably doesn't want to actually // play anything if this is the last option. @@ -295,152 +160,57 @@ async function main(args) { } }, - '-log-playlist': util => util.alias('-print-playlist'), - '-json': util => util.alias('-print-playlist'), - - // Add appends the keybindings to the existing keybindings; import replaces - // the current ones with the opened ones. - - '-add-keybindings': async function(util) { - await openKeybindings(util.nextArg()) - }, - - '-open-keybindings': util => util.alias('-add-keybindings'), - - '-import-keybindings': async function(util) { - await openKeybindings(util.nextArg(), false) - }, - - '-clear': async function(util) { - // --clear (alias: -c) - // Clears the active playlist. This does not affect the source - // playlist. - - await requiresOpenPlaylist() - - activePlaylist.items = [] - }, - - 'c': util => util.alias('-clear'), - - '-keep': async function(util) { - // --keep <groupPath> (alias: -k) - // Keeps a group by loading it from the source playlist into the - // active playlist. This is usually useful after clearing the - // active playlist; it can also be used to keep a subgroup when - // you've removed an entire parent group, e.g. `-r foo -k foo/baz`. - - await requiresOpenPlaylist() - - const pathString = util.nextArg() - const group = filterPlaylistByPathString(sourcePlaylist, pathString) + '-list-groups': async function(util) { + await originalListGroups(util) - if (group) { - activePlaylist.items.push(group) + // If this is the last item in the argument list, the user probably + // only wants to get the list, so we'll mark the 'should run' flag + // as false. + if (util.index === util.argv.length - 1) { + shouldPlay = false } }, - 'k': util => util.alias('-keep'), - - '-remove': async function(util) { - // --remove <groupPath> (alias: -r, -x) - // Filters the playlist so that the given path is removed. - - await requiresOpenPlaylist() - - const pathString = util.nextArg() - console.log("Ignoring path: " + pathString) - removeGroupByPathString(activePlaylist, pathString) - }, - - 'r': util => util.alias('-remove'), - 'x': util => util.alias('-remove'), - - '-filter': async function(util) { - // --filter <filterJSON> - // Filters the playlist so that only tracks that match the given filter - // are kept. FilterJSON should be a JSON object as described in the - // man page section "filters". - const filterJSON = util.nextArg() + '-list-all': async function(util) { + await originalListAll(util) - let filterObj - try { - filterObj = JSON.parse(filterJSON) - } catch (error) { - console.error('Invalid JSON for filter:', filterJSON) - return + // As with -l, if this is the last item in the argument list, we + // won't actually be playing the playlist. + if (util.index === util.argv.length - 1) { + shouldPlay = false } - - activePlaylist.filters = [filterObj] - activePlaylist = await processSmartPlaylist(activePlaylist) - activePlaylist = updatePlaylistFormat(activePlaylist) - }, - - 'f': util => util.alias('-filter'), - - '-collapse-groups': async function() { - // --collapse-groups (alias: --collapse) - // Collapses groups in the active playlist so that there is only one - // level of sub-groups. Handy for shuffling the order groups play in; - // try `--collapse-groups --sort shuffle-groups`. - - await requiresOpenPlaylist() - - activePlaylist = updatePlaylistFormat(collapseGrouplike(activePlaylist)) }, - '-collapse': util => util.alias('-collapse-groups'), + // Other options, specific to play ---------------------------------------- - '-flatten-tracks': async function() { - // --flatten-tracks (alias: --flatten) - // Flattens the entire active playlist, so that only tracks remain, - // and there are no groups. - - await requiresOpenPlaylist() - - activePlaylist = updatePlaylistFormat(flattenGrouplike(activePlaylist)) - }, - - '-flatten': util => util.alias('-flatten-tracks'), - - '-list-groups': async function(util) { - // --list-groups (alias: -l, --list) - // Lists all groups in the playlist. - - await requiresOpenPlaylist() + '-help': function(util) { + // --help (alias: -h, -?) + // Presents a help message. - console.log(getPlaylistTreeString(activePlaylist)) + console.log('http-music\nTry man http-music!') - // If this is the last item in the argument list, the user probably - // only wants to get the list, so we'll mark the 'should run' flag - // as false. if (util.index === util.argv.length - 1) { shouldPlay = false } }, - '-list': util => util.alias('-list-groups'), - 'l': util => util.alias('-list-groups'), + 'h': util => util.alias('-help'), + '?': util => util.alias('-help'), - '-list-all': async function(util) { - // --list-all (alias: --list-tracks, -L) - // Lists all groups and tracks in the playlist. + // Add appends the keybindings to the existing keybindings; import replaces + // the current ones with the opened ones. - await requiresOpenPlaylist() + '-add-keybindings': async function(util) { + await openKeybindings(util.nextArg()) + }, - console.log(getPlaylistTreeString(activePlaylist, true)) + '-open-keybindings': util => util.alias('-add-keybindings'), - // As with -l, if this is the last item in the argument list, we - // won't actually be playing the playlist. - if (util.index === util.argv.length - 1) { - shouldPlay = false - } + '-import-keybindings': async function(util) { + await openKeybindings(util.nextArg(), false) }, - '-list-tracks': util => util.alias('-list-all'), - 'L': util => util.alias('-list-all'), - '-list-keybindings': function() { console.log('Keybindings:') @@ -513,8 +283,13 @@ async function main(args) { // Sets the first track to be played. // This is especially useful when using an ordered sort; this option // could be used to start a long album part way through. + const pathString = util.nextArg() - const track = filterPlaylistByPathString(activePlaylist, pathString) + + const track = filterPlaylistByPathString( + getStuff.activePlaylist, pathString + ) + if (isTrack(track)) { startTrack = track console.log('Starting on track', pathString) @@ -647,14 +422,18 @@ async function main(args) { }, '-trust': util => util.alias('-trust-shell-commands') - } + }) await processArgv(args, optionFunctions) - if (!hasOpenedPlaylist) { + if (!getStuff.hasOpenedPlaylist) { await openDefaultPlaylist() } + // All done processing: let's actually grab the active playlist, which + // we'll quickly validate and then play (if it contains tracks). + const activePlaylist = getStuff.activePlaylist + if (activePlaylist === null) { console.error( "Cannot play - no open playlist. Try --open <playlist file>?" |