From 26663377fd7ea15a6c3d23a399d1266c8639d42e Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 31 May 2017 18:58:08 -0300 Subject: Progress --- crawl-links.js | 33 ------------- crawl-recursive.js | 118 ++++++++++++++++++++++++++--------------------- src/pickers.js | 6 +++ src/play.js | 7 ++- src/playlist-utils.js | 28 +++++++---- src/process-argv.js | 18 ++++++++ src/promisify-process.js | 3 ++ todo.txt | 4 ++ 8 files changed, 123 insertions(+), 94 deletions(-) delete mode 100644 crawl-links.js diff --git a/crawl-links.js b/crawl-links.js deleted file mode 100644 index 8602ce1..0000000 --- a/crawl-links.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -const fetch = require('node-fetch') -const $ = require('cheerio') -const url = require('url') - -const DEFAULT_EXTENSIONS = [ - 'mp3', 'wav' -] - -function getHTMLLinks(text) { - // Never parse HTML with a regex! - - return $(text).find('a').get().map(a => { - const $a = $(a) - return [$a.text(), $a.attr('href')] - }) -} - -module.exports.getHTMLLinks = getHTMLLinks - -if (require.main === module) { - const urlString = process.argv[2] - const exts = process.argv.length > 3 ? process.argv.slice(3) : DEFAULT_EXTENSIONS - - fetch(urlString) - .then(res => res.text()) - .then(text => getHTMLLinks(text)) - .then(links => links.filter(l => exts.some(e => l[1].endsWith('.' + e)))) - .then(links => links.map(l => [l[0], url.resolve(urlString, l[1])])) - .then(links => console.log(JSON.stringify(links, null, 2))) - .catch(err => console.error(err)) -} diff --git a/crawl-recursive.js b/crawl-recursive.js index 8d33ded..d3b0127 100644 --- a/crawl-recursive.js +++ b/crawl-recursive.js @@ -3,70 +3,84 @@ const MAX_DOWNLOAD_ATTEMPTS = 5 const fetch = require('node-fetch') -const { getHTMLLinks } = require('./crawl-links') function crawl(absURL, attempts = 0) { - return fetch(absURL) - .then(res => res.text().then(text => playlistifyParse(text, absURL)), err => { - console.error('Failed to download: ' + absURL) + // Recursively crawls a given URL, following every link to a deeper path and + // recording all links in a tree (in the same format playlists use). Makes + // multiple attempts to download failed paths. - 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 - } - }) -} + return fetch(absURL) + .then( + res => res.text().then(text => { + const links = getHTMLLinks(text) + const verbose = process.argv.includes('--verbose') + + return Promise.all(links.map(link => { + const [ title, href ] = link + + if (href.endsWith('/')) { + // It's a directory! -function playlistifyParse(text, absURL) { - const links = getHTMLLinks(text) - const verbose = process.argv.includes('--verbose') + if (verbose) console.log('[Dir] ' + absURL + href) + return crawl(absURL + href) + .then(res => [title, res]) + } else { + // It's a file! - return Promise.all(links.map(link => { - const [ title, href ] = link + if (verbose) console.log('[File] ' + absURL + href) + return Promise.resolve([title, absURL + href]) + } + })) + }), - if (href.endsWith('/')) { - // It's a directory! + 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 + } + }) +} - if (verbose) console.log('[Dir] ' + absURL + href) - return crawl(absURL + href) - .then(res => [title, res]) - } else { - // It's a file! +function getHTMLLinks(text) { + // Never parse HTML with a regex! - if (verbose) console.log('[File] ' + absURL + href) - return Promise.resolve([title, absURL + href]) - } - })) + return $(text).find('a').get().map(a => { + const $a = $(a) + return [$a.text(), $a.attr('href')] + }) } if (process.argv.length === 2) { - console.log('Usage: crawl-recursive http://example.com/example/path') + console.log('Usage: crawl-recursive http://example.com/example/path') } else { - let url = process.argv[2] + let url = process.argv[2] - if (!(url.endsWith('/'))) { - url = url + '/' - } + if (!(url.endsWith('/'))) { + url = url + '/' + } - crawl(url) - .then(res => console.log(JSON.stringify(res, null, 2))) - .catch(err => console.error(err)) + crawl(url) + .then(res => console.log(JSON.stringify(res, null, 2))) + .catch(err => console.error(err)) } diff --git a/src/pickers.js b/src/pickers.js index 236f9ea..92a9641 100644 --- a/src/pickers.js +++ b/src/pickers.js @@ -3,6 +3,9 @@ const { flattenPlaylist } = require('./playlist-utils') function makeOrderedPlaylistPicker(playlist) { + // Ordered playlist picker - this plays all the tracks in a playlist in + // order, after flattening it. + const allSongs = flattenPlaylist(playlist) let index = 0 @@ -18,6 +21,9 @@ function makeOrderedPlaylistPicker(playlist) { } function makeShufflePlaylistPicker(playlist) { + // Shuffle playlist picker - this selects a random track at any index in + // the playlist, after flattening it. + const allSongs = flattenPlaylist(playlist) return function() { diff --git a/src/play.js b/src/play.js index b0014f5..9b9a5cf 100644 --- a/src/play.js +++ b/src/play.js @@ -7,6 +7,10 @@ const loopPlay = require('./loop-play') const processArgv = require('./process-argv') const pickers = require('./pickers') +const { + filterPlaylistByPathString, ignoreGroupByPathString, getPlaylistTreeString +} = require('./playlist-utils') + const readFile = promisify(fs.readFile) readFile('./playlist.json', 'utf-8') @@ -29,7 +33,8 @@ readFile('./playlist.json', 'utf-8') // Opens a separate playlist file. // This sets the source playlist. - const openedPlaylist = JSON.parse(await readFile(util.nextArg(), 'utf-8')) + const playlistText = await readFile(util.nextArg(), 'utf-8') + const openedPlaylist = JSON.parse(playlistText) sourcePlaylist = openedPlaylist curPlaylist = openedPlaylist }, diff --git a/src/playlist-utils.js b/src/playlist-utils.js index d853456..5266f1a 100644 --- a/src/playlist-utils.js +++ b/src/playlist-utils.js @@ -1,6 +1,10 @@ 'use strict' function flattenPlaylist(playlist) { + // Flattens a playlist, taking all of the non-group items (tracks) at all + // levels in the playlist tree and returns them as a single-level array of + // tracks. + const groups = playlist.filter(x => Array.isArray(x[1])) const nonGroups = playlist.filter(x => x[1] && !(Array.isArray(x[1]))) return groups.map(g => flattenPlaylist(g[1])) @@ -8,12 +12,16 @@ function flattenPlaylist(playlist) { } function filterPlaylistByPathString(playlist, pathString) { + // Calls filterPlaylistByPath, taking a path string, rather than a parsed + // path. + 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! + // Finds a group by following the given group path and returns it. If the + // function encounters an item in the group path that is not found, it logs + // a warning message and returns the group found up to that point. let cur = pathParts[0] @@ -33,13 +41,14 @@ function filterPlaylistByPath(playlist, pathParts) { } } -function ignoreGroupByPathString(playlist, pathString) { - const pathParts = parsePathString(pathString) - return ignoreGroupByPath(playlist, pathParts) +function removeGroupByPathString(playlist, pathString) { + // Calls removeGroupByPath, taking a path string, rather than a parsed path. + + return removeGroupByPath(playlist, parsePathString(pathString)) } -function ignoreGroupByPath(playlist, pathParts) { - // TODO: Ideally this wouldn't mutate the given playlist. +function removeGroupByPath(playlist, pathParts) { + // Removes the group at the given path from the given playlist. const groupToRemove = filterPlaylistByPath(playlist, pathParts) @@ -80,7 +89,10 @@ function getPlaylistTreeString(playlist, showTracks = false) { } }).join('\n') - const tracksString = (showTracks ? nonGroups.map(g => g[0]).join('\n') : '') + let trackString = '' + if (showTracks) { + trackString = nonGroups.map(g => g[0]).join('\n') + } if (tracksString && childrenString) { return tracksString + '\n' + childrenString diff --git a/src/process-argv.js b/src/process-argv.js index 3193d98..d5f86f9 100644 --- a/src/process-argv.js +++ b/src/process-argv.js @@ -1,17 +1,35 @@ 'use strict' module.exports = async function processArgv(argv, handlers) { + // Basic command line argument list processor. Takes a list of arguments and + // an object, which is used as a mapping of option strings to behavior + // functions. + let i = 0 async function handleOpt(opt) { + // Handles a single option. May be recursive, depending on the user-defined + // handler given to processArgv. If there is no such handler for the given + // option, a warning message is displayed and the option is ignored. + if (opt in handlers) { await handlers[opt]({ + // Util object; stores useful information and methods that the handler + // can access. + argv, index: i, + nextArg: function() { + // Returns the next argument in the argument list, and increments + // the parse index by one. + i++ return argv[i] }, + alias: function(optionToRun) { + // Runs the given option's handler. + handleOpt(optionToRun) } }) diff --git a/src/promisify-process.js b/src/promisify-process.js index 877cb8d..ca49b31 100644 --- a/src/promisify-process.js +++ b/src/promisify-process.js @@ -1,6 +1,9 @@ 'use strict' module.exports = function promisifyProcess(proc, showLogging = true) { + // Takes a process (from child_process) and returns a promise that resolves + // when the process exits. + return new Promise((resolve, reject) => { if (showLogging) { proc.stdout.pipe(process.stdout) diff --git a/todo.txt b/todo.txt index 324df83..cf984fc 100644 --- a/todo.txt +++ b/todo.txt @@ -63,3 +63,7 @@ TODO: Make crawl-itunes.js a bit more general, more command-line TODO: Play-in-order track picker. (Done!) + +TODO: Volume controls. Who knows how to do this? It might have to be an + argument passed to `play`. Being able to change the volume while it's + playing would be nice, but I'm not sure if that's really possible. -- cgit 1.3.0-6-gf8a5