From f5e24f0a08c8d0a6ff51213d337944f3e31f8804 Mon Sep 17 00:00:00 2001 From: liam4 Date: Mon, 29 May 2017 00:23:33 +0000 Subject: Initial commit --- .gitignore | 4 ++ crawl-itunes.js | 47 ++++++++++++++++ package.json | 7 +++ play.js | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 94 +++++++++++++++++++++++++++++++ 5 files changed, 320 insertions(+) create mode 100644 .gitignore create mode 100644 crawl-itunes.js create mode 100644 package.json create mode 100644 play.js create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1a06bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.*.wav +/.temp-track +node_modules +playlist.json diff --git a/crawl-itunes.js b/crawl-itunes.js new file mode 100644 index 0000000..7a98be9 --- /dev/null +++ b/crawl-itunes.js @@ -0,0 +1,47 @@ +const fetch = require('node-fetch') + +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 [] + } + + const regex = /([^>]*)<\/a>/g + + let matches, output = [] + while (matches = regex.exec(text)) { + output.push([matches[1], matches[2]]) + } + return output +} + +function crawl(absURL) { + return fetch(absURL) + .then(res => res.text(), err => { + console.warn('FAILED: ' + absURL) + return 'Oops' + }) + .then(text => parseDirectoryListing(text)) + .then(links => Promise.all(links.map(link => { + const [ href, title ] = link + + if (href.endsWith('/')) { + // It's a directory! + + console.log('[Dir] ' + absURL + href) + return crawl(absURL + href).then(res => [title, res]) + } else { + // It's a file! + + console.log('[File] ' + absURL + href) + return Promise.resolve([title, absURL + href]) + } + }))) +} + +crawl('http://192.168.2.19:1233/') + .then(res => console.log(JSON.stringify(res, null, 2))) + .catch(err => console.error(err)) diff --git a/package.json b/package.json new file mode 100644 index 0000000..c1e8450 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "fs-promise": "^2.0.3", + "node-fetch": "^1.7.0", + "sanitize-filename": "^1.6.1" + } +} diff --git a/play.js b/play.js new file mode 100644 index 0000000..275bb91 --- /dev/null +++ b/play.js @@ -0,0 +1,168 @@ +// 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). +// +// TODO: Get `play` working. +// +// TODO: Get play-next working; probably just act like a shuffle. Will +// need to keep an eye out for the `play` process finishing. +// +// TODO: Preemptively download and process the next track, while the +// current one is playing, to eliminate the silent time between +// tracks. +// +// TODO: Delete old tracks! Since we aren't overwriting files, we +// need to manually delete files once we're done with them. +// +// TODO: Clean up on SIGINT. +// +// TODO: Get library filter path from stdin. +// +// TODO: Show library tree. Do this AFTER filtering, so that people +// can e.g. see all albums by a specific artist. +// +// TODO: Ignore .DS_Store. +// +// TODO: Have a download timeout, somehow. +// +// TODO: Fix the actual group format. Often times we get single-letter +// files being downloaded (which don't exist); I'm guessing that's +// related to folder names (which are just strings, not title-href +// arrays) still being in the group array. (Update: that's defin- +// itely true; 'Saucey Sounds'[0] === 'S', and 'Unofficial'[0] +// === 'U', which are the two "files" it crashes on while playing +// -g 'Jake Chudnow'.) + +const fsp = require('fs-promise') +const fetch = require('node-fetch') +const sanitize = require('sanitize-filename') +const { spawn } = require('child_process') + +function promisifyProcess(proc, showLogging = true) { + return new Promise((resolve, reject) => { + if (showLogging) { + proc.stdout.pipe(process.stdout) + proc.stderr.pipe(process.stderr) + } + + proc.on('exit', code => { + if (code === 0) { + resolve() + } else { + reject(code) + } + }) + }) +} + +function flattenPlaylist(playlist) { + 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)) + .reduce((a, b) => a.concat(b), nonGroups) +} + +function convert(fromFile, toFile) { + const avconv = spawn('avconv', ['-y', '-i', fromFile, toFile]) + return promisifyProcess(avconv, false) +} + +function playFile(file) { + const play = spawn('play', [file]) + return promisifyProcess(play) +} + +function pickRandomFromPlaylist(playlist) { + const allSongs = flattenPlaylist(playlist) + const index = Math.floor(Math.random() * allSongs.length) + const picked = allSongs[index] + return picked +} + +function loopPlay(fn) { + const picked = fn() + const [ title, href ] = picked + + console.log(`Downloading ${title}..\n${href}`) + + const outWav = `.${sanitize(title)}.wav` + + return fetch(href) + .then(res => res.buffer()) + .then(buf => fsp.writeFile('./.temp-track', buf)) + .then(() => convert('./.temp-track', outWav)) + .then(() => fsp.unlink('./.temp-track')) + .then(() => playFile(outWav), () => console.warn('Failed to convert ' + title + '\n' + href)) + .then(() => fsp.unlink(outWav)) + .then(() => loopPlay(fn)) +} + +function filterPlaylistByPathString(playlist, pathString) { + const parts = pathString.split('/') + return filterPlaylistByPath(playlist, parts) +} + +function filterPlaylistByPath(playlist, pathParts) { + let cur = pathParts[0] + + if (!(cur.endsWith('/'))) { + cur = cur + '/' + } + + const match = playlist.find(g => g[0] === cur && Array.isArray(g[1])) + + if (match) { + const groupContents = match[1] + if (pathParts.length > 1) { + const rest = pathParts.slice(1) + return filterPlaylistByPath(groupContents, rest) + } else { + return groupContents + } + } else { + console.warn(`Not found: "${cur}"`) + return playlist + } +} + +function getPlaylistTreeString(playlist) { + 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|' : '') + ) + } + + return recursive(playlist).join('\n') +} + +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)) + } else { + return loopPlay(() => pickRandomFromPlaylist(playlist)) + } + }) + .catch(err => console.error(err)) + +/* +loopPlay(() => ['blah', 'http://192.168.2.19:1233/Koichi%20Sugiyama/Dragon%20Quest%205/34%2034%20Dragon%20Quest%205%20-%20Bonus%20Fight.mp3']) + .catch(err => console.error(err)) +*/ diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..db1fddd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,94 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +any-promise@^1.0.0, any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +fs-extra@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + +fs-promise@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-2.0.3.tgz#f64e4f854bcf689aa8bddcba268916db3db46854" + dependencies: + any-promise "^1.3.0" + fs-extra "^2.0.0" + mz "^2.6.0" + thenify-all "^1.6.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +iconv-lite@~0.4.13: + version "0.4.17" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.17.tgz#4fdaa3b38acbc2c031b045d0edcdfe1ecab18c8d" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +mz@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.6.0.tgz#c8b8521d958df0a4f2768025db69c719ee4ef1ce" + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +node-fetch@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.0.tgz#3ff6c56544f9b7fb00682338bb55ee6f54a8a0ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +sanitize-filename@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a" + dependencies: + truncate-utf8-bytes "^1.0.0" + +thenify-all@^1.0.0, thenify-all@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + dependencies: + any-promise "^1.0.0" + +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + dependencies: + utf8-byte-length "^1.0.1" + +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" -- cgit 1.3.0-6-gf8a5