diff options
-rw-r--r-- | README.md | 48 | ||||
-rwxr-xr-x | src/cli.js | 29 | ||||
-rwxr-xr-x | src/crawl-http.js | 2 | ||||
-rwxr-xr-x | src/crawl-itunes.js | 32 | ||||
-rwxr-xr-x | src/crawl-local.js | 2 | ||||
-rw-r--r-- | src/crawl-youtube.js | 2 | ||||
-rw-r--r-- | src/crawlers.js | 13 | ||||
-rw-r--r-- | src/downloaders.js | 10 | ||||
-rw-r--r-- | src/loop-play.js | 22 | ||||
-rw-r--r-- | src/playlist-utils.js | 10 | ||||
-rw-r--r-- | src/smart-playlist.js | 49 | ||||
-rw-r--r-- | todo.txt | 20 |
12 files changed, 180 insertions, 59 deletions
diff --git a/README.md b/README.md index 5956d43..c95ce6e 100644 --- a/README.md +++ b/README.md @@ -3,43 +3,31 @@ A command line program that lets you download music from places and play it. It's also decently powerful. -## Using the thing +## Installation ```bash -# On the server; that is, the device that holds the media: -$ cd my_music_folder -$ python3 -m http.server <some_port> - -# On the client; that is, the device with http-music: +$ git clone https://github.com/liam4/http-music $ cd http-music -$ yarn # to install Node.js dependencies; you'll also need `avconv` and `mpv`. -$ npm run crawl-http -- <server_ip> > playlist.json -$ node . play # Go! +$ npm install + +# Installs http-music GLOBALLY, i.e., so you can use from in any directory. +$ npm link # (You might need sudo here.) ``` -**Zomg command line arguments documentation????** — Yes; read the docs! There's -a man page for a reason: `man man/http-music.1` (or `man http-music`). +## Usage -There's actually three proper ways to run `http-music`: +``` +# Generate a playlist file, using one of these shell commands.. +$ http-music crawl-http http://some.directory.listing.server/ > playlist.json +$ http-music crawl-local ~/Music/ > playlist.json -* **Run `$ npm link` and then use `$ http-music`.** This gives you the - advantage of having a proper command you can use anywhere; however it does - mean installing to /usr/bin (or wherever your `npm-link` command puts - things). +# Then play it: +$ http-music play -* **Run `$ node .` while `cd`'d into `http-music`.** This is essentially the - same as using `npm-link`, but it requires you to be in the repository folder. - That's alright if you're developing, or just directly downloaded the entire - repository, but probably isn't otherwise useful. +# (You can use `python3 -m http.server` or `python2 -m SimpleHTTPServer` to +# run a quick and easy directory listing, to pass into crawl-http!) +``` -* **Run `$ npm run play`.** (You might need to do `$ npm run http-music play`.) - This way *works*, but it's not suggested; command line arguments need to be - passed after `--`, e.g. `npm run play -- -c -k CoolArtist123` instead of - `node . -c -k CoolArtist123` or `http-music -c -k CoolArtist123`. Use - whatever you prefer, I guess. +## Documentation -**If you're running with `npm run`,** you need to use `--` before any of your -own options, e.g. `npm run play -- -c -k CoolArtist123`. I know, it looks -stupid; but it's really just the way `npm run` works. You're probably better -off with `node .` while `cd`'d into the `http-music` directory, or maybe you'd -rather `npm link` it so you can use it anywhere. +Check out [the man pages](man/). (Or view them with `man http-music`.) 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 7308f3f..76f3941 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/downloaders.js b/src/downloaders.js index c3dc43d..b9cc33d 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) @@ -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 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) } 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)) +} diff --git a/todo.txt b/todo.txt index c16f680..9239493 100644 --- a/todo.txt +++ b/todo.txt @@ -235,3 +235,23 @@ TODO: The filter utility function shouldn't work at all if it fails to find TODO: Make the filter/remove/keep options do a search of some sort. TODO: Make those options also work with tracks! + +TODO: The URL 'http://somesite.com/youtube.com.mp3' would probably + automatically assume the YouTube downloader. Instead of checking for the + string 'youtube.com' included in the downloader arg, check if it is a + valid URL and that the URL's domain is 'youtube.com'. + +TODO: Figure out when to process.exit(1). In cli.js? + +TODO: Change usages of "/example/path" to a more specific "/path/to/playlist" + (for example). + +TODO: Support smart playlists right inside of play - and ideally any other + usage, e.g. download-playlist. For now the user can just run + smart-playlist, save the result, and load that in whatever command + they're using. + +TODO: Markdown documentation? Man pages are nice, but aren't really all that + user-friendly (citation needed); for example you can't easily read them + online. (Whereas Markdown documents are easily viewed online, and aren't + hard to read by hand, e.g. with `less doc/foo.md`.) |