« get me outta code hell

Initial commit - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorliam4 <towerofnix@gmail.com>2017-05-29 00:23:33 +0000
committerliam4 <towerofnix@gmail.com>2017-05-29 00:23:33 +0000
commitf5e24f0a08c8d0a6ff51213d337944f3e31f8804 (patch)
tree9c3abb584d2e40af369cfcb47605dc6cbed18782
Initial commit
-rw-r--r--.gitignore4
-rw-r--r--crawl-itunes.js47
-rw-r--r--package.json7
-rw-r--r--play.js168
-rw-r--r--yarn.lock94
5 files changed, 320 insertions, 0 deletions
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 href="([^"]*)">([^>]*)<\/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"