« get me outta code hell

General update - 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 20:12:33 +0000
committerliam4 <towerofnix@gmail.com>2017-05-29 20:12:33 +0000
commit7572a9f46ff1438d4199306077c1dacd1233176a (patch)
treec59a007053b4aa1f02f4456f3d1ee205e145ca1d
parentf5e24f0a08c8d0a6ff51213d337944f3e31f8804 (diff)
General update
-rw-r--r--crawl-itunes.js76
-rw-r--r--play.js234
2 files changed, 266 insertions, 44 deletions
diff --git a/crawl-itunes.js b/crawl-itunes.js
index 7a98be9..3c0f3f7 100644
--- a/crawl-itunes.js
+++ b/crawl-itunes.js
@@ -1,12 +1,13 @@
 const fetch = require('node-fetch')
 
+const MAX_DOWNLOAD_ATTEMPTS = 5
+
 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 []
+		throw 'NOT_DIRECTORY_LISTING'
 	}
 
 	const regex = /<a href="([^"]*)">([^>]*)<\/a>/g
@@ -18,28 +19,65 @@ function parseDirectoryListing(text) {
 	return output
 }
 
-function crawl(absURL) {
+function crawl(absURL, attempts = 0) {
 	return fetch(absURL)
-		.then(res => res.text(), err => {
-			console.warn('FAILED: ' + absURL)
-			return 'Oops'
+		.then(res => res.text().then(text => playlistifyParse(text, absURL)), 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
+			}
 		})
-		.then(text => parseDirectoryListing(text))
-		.then(links => Promise.all(links.map(link => {
-			const [ href, title ] = link
+}
 
-			if (href.endsWith('/')) {
-				// It's a directory!
+function playlistifyParse(text, absURL) {
+	const links = parseDirectoryListing(text)
+	return Promise.all(links.map(link => {
+		const [ href, title ] = link
 
-				console.log('[Dir] ' + absURL + href)
-				return crawl(absURL + href).then(res => [title, res])
-			} else {
-				// It's a file!
+		const verbose = process.argv.includes('--verbose')
 
-				console.log('[File] ' + absURL + href)
-				return Promise.resolve([title, absURL + href])
-			}
-		})))
+		if (href.endsWith('/')) {
+			// It's a directory!
+
+			if (verbose) console.log('[Dir] ' + absURL + href)
+			return crawl(absURL + href)
+				.then(res => [title, res])
+				.catch(error => {
+					if (error === 'NOT_DIRECTORY_LISTING') {
+						console.error('Not a directory listing: ' + absURL)
+						return []
+					} else {
+						throw error
+					}
+				})
+		} else {
+			// It's a file!
+
+			if (verbose) console.log('[File] ' + absURL + href)
+			return Promise.resolve([title, absURL + href])
+		}
+	})).catch(error => {
+	})
 }
 
 crawl('http://192.168.2.19:1233/')
diff --git a/play.js b/play.js
index 275bb91..b5a85e6 100644
--- a/play.js
+++ b/play.js
@@ -1,11 +1,14 @@
 // 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).
+//       (Done!)
 //
 // TODO: Get `play` working.
+//       (Done!)
 //
 // TODO: Get play-next working; probably just act like a shuffle. Will
 //       need to keep an eye out for the `play` process finishing.
+//       (Done!)
 //
 // TODO: Preemptively download and process the next track, while the
 //       current one is playing, to eliminate the silent time between
@@ -13,13 +16,16 @@
 //
 // TODO: Delete old tracks! Since we aren't overwriting files, we
 //       need to manually delete files once we're done with them.
+//       (Done!)
 //
 // TODO: Clean up on SIGINT.
 //
 // TODO: Get library filter path from stdin.
+//       (Done!)
 //
 // TODO: Show library tree. Do this AFTER filtering, so that people
 //       can e.g. see all albums by a specific artist.
+//       (Done!)
 //
 // TODO: Ignore .DS_Store.
 //
@@ -32,6 +38,17 @@
 //       itely true; 'Saucey Sounds'[0] === 'S', and 'Unofficial'[0]
 //       === 'U', which are the two "files" it crashes on while playing
 //       -g 'Jake Chudnow'.)
+//       (Done?)
+//
+// TODO: A way to exclude a specific group path.
+//       (Done!)
+//
+// TODO: Better argv handling.
+//       (Done!)
+//
+// TODO: Option to include a specific path from the source playlist.
+
+'use strict'
 
 const fsp = require('fs-promise')
 const fetch = require('node-fetch')
@@ -98,11 +115,13 @@ function loopPlay(fn) {
 }
 
 function filterPlaylistByPathString(playlist, pathString) {
-	const parts = pathString.split('/')
-	return filterPlaylistByPath(playlist, parts)
+	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!
+
 	let cur = pathParts[0]
 
 	if (!(cur.endsWith('/'))) {
@@ -117,7 +136,7 @@ function filterPlaylistByPath(playlist, pathParts) {
 			const rest = pathParts.slice(1)
 			return filterPlaylistByPath(groupContents, rest)
 		} else {
-			return groupContents
+			return match
 		}
 	} else {
 		console.warn(`Not found: "${cur}"`)
@@ -125,39 +144,204 @@ function filterPlaylistByPath(playlist, pathParts) {
 	}
 }
 
-function getPlaylistTreeString(playlist) {
+function ignoreGroupByPathString(playlist, pathString) {
+	const pathParts = parsePathString(pathString)
+	return ignoreGroupByPath(playlist, pathParts)
+}
+
+function ignoreGroupByPath(playlist, pathParts) {
+	// TODO: Ideally this wouldn't mutate the given playlist.
+
+	const groupToRemove = filterPlaylistByPath(playlist, pathParts)
+
+	const parentPath = pathParts.slice(0, pathParts.length - 1)
+	let parent
+
+	if (parentPath.length === 0) {
+		parent = playlist
+	} else {
+		parent = filterPlaylistByPath(playlist, pathParts.slice(0, -1))
+	}
+
+	const index = parent.indexOf(groupToRemove)
+
+	if (index >= 0) {
+		parent.splice(index, 1)
+	} else {
+		console.error(
+			'Group ' + pathParts.join('/') + ' doesn\'t exist, so we can\'t ' +
+			'explicitly ignore it.'
+		)
+	}
+}
+
+function getPlaylistTreeString(playlist, showTracks = false) {
 	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|' : '')
-		)
+		const childrenString = groups.map(g => {
+			const groupString = recursive(g[1])
+
+			if (groupString) {
+				const indented = groupString.split('\n').map(l => '| ' + l).join('\n')
+				return '\n' + g[0] + '\n' + indented
+			} else {
+				return g[0]
+			}
+		}).join('\n')
+
+		const tracksString = (showTracks ? nonGroups.map(g => g[0]).join('\n') : '')
+
+		if (tracksString && childrenString) {
+			return tracksString + '\n' + childrenString
+		} else if (childrenString) {
+			return childrenString
+		} else if (tracksString) {
+			return tracksString
+		} else {
+			return ''
+		}
 	}
 
-	return recursive(playlist).join('\n')
+	return recursive(playlist)
+}
+
+function parsePathString(pathString) {
+	const pathParts = pathString.split('/')
+	return pathParts
+}
+
+async function processArgv(argv, handlers) {
+	for (let i = 0; i < argv.length; i++) {
+		const cur = argv[i]
+		if (cur.startsWith('-')) {
+			const opt = cur.slice(1)
+			if (opt in handlers) {
+				await handlers[opt]({
+					argv, index: i,
+					nextArg: function() {
+						i++
+						return argv[i]
+					}
+				})
+			} else {
+				console.warn('Option not understood: ' + cur)
+			}
+		}
+	}
 }
 
 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))
+	.then(async playlist => {
+		let sourcePlaylist = playlist
+		let curPlaylist = playlist
+
+		// WILL play says whether the user has forced playback via an argument.
+		// SHOULD play says whether the program has automatically decided to play
+		// or not, if the user hasn't set WILL play.
+		let shouldPlay = true
+		let willPlay = null
+
+		await processArgv(process.argv, {
+			'o': async function(util) {
+				// -o <file>
+				// Opens a separate playlist file.
+				// This sets the source playlist.
+
+				const openedPlaylist = JSON.parse(await fsp.readFile(util.nextArg(), 'utf-8'))
+				sourcePlaylist = openedPlaylist
+				curPlaylist = openedPlaylist
+			},
+
+			'c': function(util) {
+				// -c
+				// Clears the active playlist. This does not affect the source
+				// playlist.
+
+				curPlaylist = []
+			},
+
+			'k': function(util) {
+				// -k <groupPath>
+				// Keeps a group by loading it from the source playlist into the
+				// active playlist. This is usually useful after clearing the
+				// active playlist; it can also be used to keep a subgroup when
+				// you've ignored an entire parent group, e.g. `-i foo -k foo/baz`.
+
+				const pathString = util.nextArg()
+				const group = filterPlaylistByPathString(sourcePlaylist, pathString)
+				curPlaylist.push(group)
+			},
+
+			'g': function(util) {
+				// -g <groupPath>
+				// Filters the playlist so that only the tracks under the passed
+				// group path will play.
+
+				const pathString = util.nextArg()
+				console.log('Filtering according to path: ' + pathString)
+				curPlaylist = filterPlaylistByPathString(curPlaylist, pathString)[1]
+			},
+
+			'i': function(util) {
+				// -i <groupPath>
+				// Filters the playlist so that the given path is removed.
+
+				const pathString = util.nextArg()
+				console.log('Ignoring path: ' + pathString)
+				ignoreGroupByPathString(curPlaylist, pathString)
+			},
+
+			'l': function(util) {
+				// -l
+				// Lists all groups in the playlist.
+				// Try -L (upper-case L) for a list including tracks.
+
+				console.log(getPlaylistTreeString(curPlaylist))
+
+				// If this is the last item in the argument list, the user probably
+				// only wants to get the list, so we'll mark the 'should run' flag
+				// as false.
+				if (util.index === util.argv.length - 1) {
+					shouldPlay = false
+				}
+			},
+
+			'L': function(util) {
+				// -L
+				// Lists all groups AND tracks in the playlist.
+				// Try -l (lower-case L) for a list that doesn't include tracks.
+
+				console.log(getPlaylistTreeString(curPlaylist, true))
+
+				// As with -l, if this is the last item in the argument list, we
+				// won't actually be playing the playlist.
+				if (util.index === util.argv.length - 1) {
+					shouldPlay = false
+				}
+			},
+
+			'p': function(util) {
+				// -p
+				// Forces the playlist to actually play.
+
+				willPlay = true
+			},
+
+			'np': function(util) {
+				// -np
+				// Forces the playlist not to play.
+
+				willPlay = false
+			}
+		})
+
+		if (willPlay || (willPlay === null && shouldPlay)) {
+			return loopPlay(() => pickRandomFromPlaylist(curPlaylist))
 		} else {
-			return loopPlay(() => pickRandomFromPlaylist(playlist))
+			return curPlaylist
 		}
 	})
 	.catch(err => console.error(err))