From 7572a9f46ff1438d4199306077c1dacd1233176a Mon Sep 17 00:00:00 2001 From: liam4 Date: Mon, 29 May 2017 20:12:33 +0000 Subject: General update --- crawl-itunes.js | 76 +++++++++++++----- play.js | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 266 insertions(+), 44 deletions(-) diff --git a/crawl-itunes.js b/crawl-itunes.js index 7a98be9..3c0f3f7 100644 --- a/crawl-itunes.js +++ b/crawl-itunes.js @@ -1,12 +1,13 @@ const fetch = require('node-fetch') +const MAX_DOWNLOAD_ATTEMPTS = 5 + function parseDirectoryListing(text) { // Matches all links in a directory listing. // Returns an array where each item is in the format [href, label]. if (!(text.includes('Directory listing for'))) { - console.warn("Not a directory listing! Crawl returning empty array.") - return [] + throw 'NOT_DIRECTORY_LISTING' } const regex = /([^>]*)<\/a>/g @@ -18,28 +19,65 @@ function parseDirectoryListing(text) { return output } -function crawl(absURL) { +function crawl(absURL, attempts = 0) { return fetch(absURL) - .then(res => res.text(), err => { - console.warn('FAILED: ' + absURL) - return 'Oops' + .then(res => res.text().then(text => playlistifyParse(text, absURL)), err => { + console.error('Failed to download: ' + absURL) + + if (attempts < MAX_DOWNLOAD_ATTEMPTS) { + console.error( + 'Trying again. Attempt ' + (attempts + 1) + + '/' + MAX_DOWNLOAD_ATTEMPTS + '...' + ) + return crawl(absURL, attempts + 1) + } else { + console.error( + 'We\'ve hit the download attempt limit (' + + MAX_DOWNLOAD_ATTEMPTS + '). Giving up on ' + + 'this path.' + ) + throw 'FAILED_DOWNLOAD' + } + }) + .catch(error => { + if (error === 'FAILED_DOWNLOAD') { + // Debug logging for this is already handled above. + return [] + } else { + throw error + } }) - .then(text => parseDirectoryListing(text)) - .then(links => Promise.all(links.map(link => { - const [ href, title ] = link +} - if (href.endsWith('/')) { - // It's a directory! +function playlistifyParse(text, absURL) { + const links = parseDirectoryListing(text) + return Promise.all(links.map(link => { + const [ href, title ] = link - console.log('[Dir] ' + absURL + href) - return crawl(absURL + href).then(res => [title, res]) - } else { - // It's a file! + const verbose = process.argv.includes('--verbose') - console.log('[File] ' + absURL + href) - return Promise.resolve([title, absURL + href]) - } - }))) + if (href.endsWith('/')) { + // It's a directory! + + if (verbose) console.log('[Dir] ' + absURL + href) + return crawl(absURL + href) + .then(res => [title, res]) + .catch(error => { + if (error === 'NOT_DIRECTORY_LISTING') { + console.error('Not a directory listing: ' + absURL) + return [] + } else { + throw error + } + }) + } else { + // It's a file! + + if (verbose) console.log('[File] ' + absURL + href) + return Promise.resolve([title, absURL + href]) + } + })).catch(error => { + }) } crawl('http://192.168.2.19:1233/') diff --git a/play.js b/play.js index 275bb91..b5a85e6 100644 --- a/play.js +++ b/play.js @@ -1,11 +1,14 @@ // TODO: Get `avconv` working. Oftentimes `play` won't be able to play // some tracks due to an unsupported format; we'll need to use // `avconv` to convert them (to WAV). +// (Done!) // // TODO: Get `play` working. +// (Done!) // // TODO: Get play-next working; probably just act like a shuffle. Will // need to keep an eye out for the `play` process finishing. +// (Done!) // // TODO: Preemptively download and process the next track, while the // current one is playing, to eliminate the silent time between @@ -13,13 +16,16 @@ // // TODO: Delete old tracks! Since we aren't overwriting files, we // need to manually delete files once we're done with them. +// (Done!) // // TODO: Clean up on SIGINT. // // TODO: Get library filter path from stdin. +// (Done!) // // TODO: Show library tree. Do this AFTER filtering, so that people // can e.g. see all albums by a specific artist. +// (Done!) // // TODO: Ignore .DS_Store. // @@ -32,6 +38,17 @@ // itely true; 'Saucey Sounds'[0] === 'S', and 'Unofficial'[0] // === 'U', which are the two "files" it crashes on while playing // -g 'Jake Chudnow'.) +// (Done?) +// +// TODO: A way to exclude a specific group path. +// (Done!) +// +// TODO: Better argv handling. +// (Done!) +// +// TODO: Option to include a specific path from the source playlist. + +'use strict' const fsp = require('fs-promise') const fetch = require('node-fetch') @@ -98,11 +115,13 @@ function loopPlay(fn) { } function filterPlaylistByPathString(playlist, pathString) { - const parts = pathString.split('/') - return filterPlaylistByPath(playlist, parts) + return filterPlaylistByPath(playlist, parsePathString(pathString)) } function filterPlaylistByPath(playlist, pathParts) { + // Note this can be used as a utility function, rather than just as + // a function for use by the argv-handler! + let cur = pathParts[0] if (!(cur.endsWith('/'))) { @@ -117,7 +136,7 @@ function filterPlaylistByPath(playlist, pathParts) { const rest = pathParts.slice(1) return filterPlaylistByPath(groupContents, rest) } else { - return groupContents + return match } } else { console.warn(`Not found: "${cur}"`) @@ -125,39 +144,204 @@ function filterPlaylistByPath(playlist, pathParts) { } } -function getPlaylistTreeString(playlist) { +function ignoreGroupByPathString(playlist, pathString) { + const pathParts = parsePathString(pathString) + return ignoreGroupByPath(playlist, pathParts) +} + +function ignoreGroupByPath(playlist, pathParts) { + // TODO: Ideally this wouldn't mutate the given playlist. + + const groupToRemove = filterPlaylistByPath(playlist, pathParts) + + const parentPath = pathParts.slice(0, pathParts.length - 1) + let parent + + if (parentPath.length === 0) { + parent = playlist + } else { + parent = filterPlaylistByPath(playlist, pathParts.slice(0, -1)) + } + + const index = parent.indexOf(groupToRemove) + + if (index >= 0) { + parent.splice(index, 1) + } else { + console.error( + 'Group ' + pathParts.join('/') + ' doesn\'t exist, so we can\'t ' + + 'explicitly ignore it.' + ) + } +} + +function getPlaylistTreeString(playlist, showTracks = false) { function recursive(group) { const groups = group.filter(x => Array.isArray(x[1])) const nonGroups = group.filter(x => x[1] && !(Array.isArray(x[1]))) - return groups.map( - g => g[0] + recursive(g[1]).map(l => '\n| ' + l).join('') - + (g[1].length ? '\n|' : '') - ) + const childrenString = groups.map(g => { + const groupString = recursive(g[1]) + + if (groupString) { + const indented = groupString.split('\n').map(l => '| ' + l).join('\n') + return '\n' + g[0] + '\n' + indented + } else { + return g[0] + } + }).join('\n') + + const tracksString = (showTracks ? nonGroups.map(g => g[0]).join('\n') : '') + + if (tracksString && childrenString) { + return tracksString + '\n' + childrenString + } else if (childrenString) { + return childrenString + } else if (tracksString) { + return tracksString + } else { + return '' + } } - return recursive(playlist).join('\n') + return recursive(playlist) +} + +function parsePathString(pathString) { + const pathParts = pathString.split('/') + return pathParts +} + +async function processArgv(argv, handlers) { + for (let i = 0; i < argv.length; i++) { + const cur = argv[i] + if (cur.startsWith('-')) { + const opt = cur.slice(1) + if (opt in handlers) { + await handlers[opt]({ + argv, index: i, + nextArg: function() { + i++ + return argv[i] + } + }) + } else { + console.warn('Option not understood: ' + cur) + } + } + } } fsp.readFile('./playlist.json', 'utf-8') .then(plText => JSON.parse(plText)) - .then(playlist => { - if (process.argv.includes('-g')) { - const groupIndex = process.argv.indexOf('-g') - const pathString = process.argv[groupIndex + 1] - console.log( - 'Filtering according to path: ' + pathString - ) - return filterPlaylistByPathString(playlist, pathString) - } else { - return playlist - } - }) - .then(playlist => { - if (process.argv.includes('-l') || process.argv.includes('--list')) { - console.log(getPlaylistTreeString(playlist)) + .then(async playlist => { + let sourcePlaylist = playlist + let curPlaylist = playlist + + // WILL play says whether the user has forced playback via an argument. + // SHOULD play says whether the program has automatically decided to play + // or not, if the user hasn't set WILL play. + let shouldPlay = true + let willPlay = null + + await processArgv(process.argv, { + 'o': async function(util) { + // -o + // Opens a separate playlist file. + // This sets the source playlist. + + const openedPlaylist = JSON.parse(await fsp.readFile(util.nextArg(), 'utf-8')) + sourcePlaylist = openedPlaylist + curPlaylist = openedPlaylist + }, + + 'c': function(util) { + // -c + // Clears the active playlist. This does not affect the source + // playlist. + + curPlaylist = [] + }, + + 'k': function(util) { + // -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 ignored an entire parent group, e.g. `-i foo -k foo/baz`. + + const pathString = util.nextArg() + const group = filterPlaylistByPathString(sourcePlaylist, pathString) + curPlaylist.push(group) + }, + + 'g': function(util) { + // -g + // Filters the playlist so that only the tracks under the passed + // group path will play. + + const pathString = util.nextArg() + console.log('Filtering according to path: ' + pathString) + curPlaylist = filterPlaylistByPathString(curPlaylist, pathString)[1] + }, + + 'i': function(util) { + // -i + // Filters the playlist so that the given path is removed. + + const pathString = util.nextArg() + console.log('Ignoring path: ' + pathString) + ignoreGroupByPathString(curPlaylist, pathString) + }, + + 'l': function(util) { + // -l + // Lists all groups in the playlist. + // Try -L (upper-case L) for a list including tracks. + + console.log(getPlaylistTreeString(curPlaylist)) + + // 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 + } + }, + + 'L': function(util) { + // -L + // Lists all groups AND tracks in the playlist. + // Try -l (lower-case L) for a list that doesn't include tracks. + + console.log(getPlaylistTreeString(curPlaylist, true)) + + // 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 + } + }, + + 'p': function(util) { + // -p + // Forces the playlist to actually play. + + willPlay = true + }, + + 'np': function(util) { + // -np + // Forces the playlist not to play. + + willPlay = false + } + }) + + if (willPlay || (willPlay === null && shouldPlay)) { + return loopPlay(() => pickRandomFromPlaylist(curPlaylist)) } else { - return loopPlay(() => pickRandomFromPlaylist(playlist)) + return curPlaylist } }) .catch(err => console.error(err)) -- cgit 1.3.0-6-gf8a5