From 943b3447c7dc6743ec7c6d1c644f2eb9cc4669cb Mon Sep 17 00:00:00 2001 From: liam4 Date: Tue, 25 Jul 2017 11:03:03 -0300 Subject: Was I even awake --- src/downloaders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/downloaders.js b/src/downloaders.js index c3dc43d..aa0bc44 100644 --- a/src/downloaders.js +++ b/src/downloaders.js @@ -107,7 +107,7 @@ function makeConverterDownloader(downloader, type) { const inFile = await downloader(arg) const base = path.basename(inFile, path.extname(inFile)) const tempDir = tempy.directory() - const outFile = tempDir + base + '.' + type + const outFile = `${tempDir}/${base}.${type}` await promisifyProcess(spawn('avconv', ['-i', inFile, outFile]), false) -- cgit 1.3.0-6-gf8a5 From 07c9f7622567d822670c1021280c4dba4d161794 Mon Sep 17 00:00:00 2001 From: liam4 Date: Wed, 26 Jul 2017 12:43:44 -0300 Subject: Let downloader be specified manually, per-track (e.g. use youtube-dl for non-YT links) --- src/downloaders.js | 8 ++++++++ src/loop-play.js | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/downloaders.js b/src/downloaders.js index aa0bc44..b9cc33d 100644 --- a/src/downloaders.js +++ b/src/downloaders.js @@ -122,6 +122,14 @@ module.exports = { makePowerfulDownloader, makeConverterDownloader, + byName: { + 'http': makeHTTPDownloader, + 'local': makeLocalDownloader, + 'file': makeLocalDownloader, + 'youtube': makeYouTubeDownloader, + 'youtube-dl': makeYouTubeDownloader + }, + getDownloaderFor(arg) { if (arg.startsWith('http://') || arg.startsWith('https://')) { if (arg.includes('youtube.com')) { diff --git a/src/loop-play.js b/src/loop-play.js index b0bb4dd..9328073 100644 --- a/src/loop-play.js +++ b/src/loop-play.js @@ -5,7 +5,10 @@ const { spawn } = require('child_process') const FIFO = require('fifo-js') const EventEmitter = require('events') -const { getDownloaderFor, makeConverterDownloader } = require('./downloaders') +const { + getDownloaderFor, makeConverterDownloader, + byName: downloadersByName +} = require('./downloaders') const { getItemPathString } = require('./playlist-utils') const promisifyProcess = require('./promisify-process') @@ -114,7 +117,22 @@ class PlayController { if (picked === null) { return null } else { - let downloader = getDownloaderFor(picked.downloaderArg) + let downloader + + if (picked.downloader) { + downloader = downloadersByName[picked.downloader]() + + if (!downloader) { + console.error( + `Invalid downloader for track ${picked.name}:`, downloader + ) + + return false + } + } else { + downloader = getDownloaderFor(picked.downloaderArg) + } + downloader = makeConverterDownloader(downloader, 'wav') this.downloadController.download(downloader, picked.downloaderArg) return picked -- cgit 1.3.0-6-gf8a5 From e02f90272a805f85f94adec8d790eaf45dcf9c3f Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 27 Jul 2017 00:05:21 -0300 Subject: Add new "apply" feature to playlists See https://gist.github.com/liam4/cd7465a82c8b367eef221e61c3b6186e. Though not tested, this should work even on old/mixed playlists, e.g.: https://gist.github.com/liam4/2cad8630d5df4cf0014cb9acd0d76115 --- src/playlist-utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src') diff --git a/src/playlist-utils.js b/src/playlist-utils.js index 55638ed..5efd478 100644 --- a/src/playlist-utils.js +++ b/src/playlist-utils.js @@ -56,6 +56,16 @@ function updateGroupFormat(group) { // isn't a string.. if (typeof item[1] === 'string' || item.downloaderArg) { item = updateTrackFormat(item) + + // TODO: Should this also apply to groups? Is recursion good? Probably + // not! + // + // TODO: How should saving/serializing handle this? For now it just saves + // the result, after applying. (I.e., "apply": {"foo": "baz"} will save + // child tracks with {"foo": "baz"}.) + if (groupObj.apply) { + Object.assign(item, groupObj.apply) + } } else { item = updateGroupFormat(item) } -- cgit 1.3.0-6-gf8a5 From 6f4ce94467db4e95cab117007e3724695b7d9533 Mon Sep 17 00:00:00 2001 From: liam4 Date: Tue, 1 Aug 2017 11:33:19 -0300 Subject: Smart playlists --- src/cli.js | 29 +++++++++++++++++------------ src/crawl-http.js | 2 +- src/crawl-itunes.js | 32 +++++++++++++++++++++----------- src/crawl-local.js | 2 +- src/crawl-youtube.js | 2 +- src/crawlers.js | 13 +++++++++++++ src/smart-playlist.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 src/crawlers.js create mode 100644 src/smart-playlist.js (limited to 'src') diff --git a/src/cli.js b/src/cli.js index 4bc64ab..9b21395 100755 --- a/src/cli.js +++ b/src/cli.js @@ -4,6 +4,8 @@ // maxlistenersexceededwarning. process.on('warning', e => console.warn(e.stack)) +const { getCrawlerByName } = require('./crawlers') + async function main(args) { let script @@ -13,18 +15,21 @@ async function main(args) { return } - switch (args[0]) { - case 'play': script = require('./play'); break - case 'crawl-http': script = require('./crawl-http'); break - case 'crawl-local': script = require('./crawl-local'); break - case 'crawl-itunes': script = require('./crawl-itunes'); break - case 'crawl-youtube': script = require('./crawl-youtube'); break - case 'download-playlist': script = require('./download-playlist'); break - - default: - console.error(`Invalid command "${args[0]}" provided.`) - console.error("Try 'man http-music'?") - return + const module = getCrawlerByName(args[0]) + + if (module) { + script = module.main + } else { + switch (args[0]) { + case 'play': script = require('./play'); break + case 'download-playlist': script = require('./download-playlist'); break + case 'smart-playlist': script = require('./smart-playlist'); break + + default: + console.error(`Invalid command "${args[0]}" provided.`) + console.error("Try 'man http-music'?") + return + } } await script(args.slice(1)) diff --git a/src/crawl-http.js b/src/crawl-http.js index e776b9c..7553e85 100755 --- a/src/crawl-http.js +++ b/src/crawl-http.js @@ -195,7 +195,7 @@ async function main(args) { console.log(JSON.stringify(downloadedPlaylist, null, 2)) } -module.exports = main +module.exports = {main, crawl} if (require.main === module) { main(process.argv.slice(2)) diff --git a/src/crawl-itunes.js b/src/crawl-itunes.js index 6060ffa..e6b63b3 100755 --- a/src/crawl-itunes.js +++ b/src/crawl-itunes.js @@ -30,7 +30,23 @@ function findChild(grouplike, name) { return grouplike.items.find(x => x.name === name) } -async function crawl(libraryXML) { +let NO_LIBRARY_SYMBOL = Symbol('No library') + +async function crawl( + libraryPath = `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml` +) { + let libraryXML + + try { + libraryXML = await readFile(libraryPath) + } catch (err) { + if (err.code === 'ENOENT') { + throw NO_LIBRARY_SYMBOL + } else { + throw err + } + } + const document = new xmldoc.XmlDocument(libraryXML) const libraryDict = document.children.find(child => child.name === 'dict') @@ -94,16 +110,12 @@ async function crawl(libraryXML) { } async function main(args) { - const libraryPath = args[0] || ( - `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml` - ) - - let library + let playlist try { - library = await readFile(libraryPath) + playlist = await crawl(args[0]) } catch(err) { - if (err.code === 'ENOENT') { + if (err === NO_LIBRARY_SYMBOL) { console.error( "It looks like you aren't sharing the iTunes Library XML file." ) @@ -125,12 +137,10 @@ async function main(args) { } } - const playlist = await crawl(library) - console.log(JSON.stringify(playlist, null, 2)) } -module.exports = main +module.exports = {main, crawl} if (require.main === module) { main(process.argv.slice(2)) diff --git a/src/crawl-local.js b/src/crawl-local.js index 629e015..d4176ed 100755 --- a/src/crawl-local.js +++ b/src/crawl-local.js @@ -41,7 +41,7 @@ async function main(args) { } } -module.exports = main +module.exports = {main, crawl} if (require.main === module) { main(process.argv.slice(2)) diff --git a/src/crawl-youtube.js b/src/crawl-youtube.js index 823fef7..4b4c66c 100644 --- a/src/crawl-youtube.js +++ b/src/crawl-youtube.js @@ -41,7 +41,7 @@ async function main(args) { } } -module.exports = main +module.exports = {main, crawl} if (require.main === module) { main(process.argv.slice(2)) diff --git a/src/crawlers.js b/src/crawlers.js new file mode 100644 index 0000000..5ad7fb4 --- /dev/null +++ b/src/crawlers.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports = { + getCrawlerByName: function(name) { + switch (name) { + case 'crawl-http': return require('./crawl-http') + case 'crawl-local': return require('./crawl-local') + case 'crawl-itunes': return require('./crawl-itunes') + case 'crawl-youtube': return require('./crawl-youtube') + default: return null + } + } +} diff --git a/src/smart-playlist.js b/src/smart-playlist.js new file mode 100644 index 0000000..e65ff1f --- /dev/null +++ b/src/smart-playlist.js @@ -0,0 +1,49 @@ +'use strict' + +const fs = require('fs') +const { getCrawlerByName } = require('./crawlers') + +const { promisify } = require('util') +const readFile = promisify(fs.readFile) + +async function processItem(item) { + // Object.assign is used so that we keep original properties, e.g. "name" + // or "apply". + + if ('items' in item) { + return Object.assign(item, { + items: await Promise.all(item.items.map(processItem)) + }) + } else if ('source' in item) { + const [ name, ...args ] = item.source + + const crawlModule = getCrawlerByName(name) + + if (crawlModule === null) { + console.error(`No crawler by name ${name} - skipped item:`, item) + return Object.assign(item, {failed: true}) + } + + const { crawl } = crawlModule + + return Object.assign(item, await crawl(...args)) + } else { + return item + } +} + +async function main(opts) { + // TODO: Error when no file is given + + if (opts.length === 0) { + console.log("Usage: smart-playlist /path/to/playlist") + } else { + const playlist = JSON.parse(await readFile(opts[0])) + console.log(JSON.stringify(await processItem(playlist), null, 2)) + } +} + +if (require.main === module) { + main(process.argv.slice(2)) + .catch(err => console.error(err)) +} -- cgit 1.3.0-6-gf8a5