« get me outta code hell

Modularize it all - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
path: root/play.js
diff options
context:
space:
mode:
authorLiam <towerofnix@gmail.com>2017-05-31 14:42:22 -0300
committerLiam <towerofnix@gmail.com>2017-05-31 14:42:22 -0300
commit42ec01bb91c517067a9eba901272c1248ed52261 (patch)
treed28bdaa85f9f02028de5f3aad76873a183e0c157 /play.js
parent07626fe82986214d4ef56a5790d864c4bf19b27e (diff)
Modularize it all
Diffstat (limited to 'play.js')
-rw-r--r--play.js457
1 files changed, 0 insertions, 457 deletions
diff --git a/play.js b/play.js
deleted file mode 100644
index bdee7f4..0000000
--- a/play.js
+++ /dev/null
@@ -1,457 +0,0 @@
-// 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
-//       tracks.
-//       (Done!)
-//
-// 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.
-//       (Done!)
-//
-// 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'.)
-//       (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.
-//       (Done!)
-//
-// TODO: Make a playlist generator that parses http://billwurtz.com
-//       instrumentals.html.
-//       (Done!)
-//
-// TODO: Make crawl-itunes.js a bit more general, more command-line
-//       friendly (i.e. don't require editing the script itself), and
-//       make it use the getHTMLLinks function defined in the new
-//       crawl-links.js script.
-//       (Done!)
-//
-// TODO: Play-in-order track picker.
-//       (Done!)
-
-'use strict'
-
-const fs = require('fs')
-const util = require('util')
-const { spawn } = require('child_process')
-
-const fetch = require('node-fetch')
-const sanitize = require('sanitize-filename')
-
-const writeFile = util.promisify(fs.writeFile)
-const readFile = util.promisify(fs.readFile)
-const unlink = util.promisify(fs.unlink)
-
-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 {
-				console.error('Process failed!', proc.spawnargs)
-				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[1]))
-		.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 makeOrderedPlaylistPicker(playlist) {
-	const allSongs = flattenPlaylist(playlist)
-	let index = 0
-
-	return function() {
-		if (index < allSongs.length) {
-			const picked = allSongs[index]
-			index++
-			return picked
-		} else {
-			return null
-		}
-	}
-}
-
-function makeShufflePlaylistPicker(playlist) {
-	const allSongs = flattenPlaylist(playlist)
-
-	return function() {
-		const index = Math.floor(Math.random() * allSongs.length)
-		const picked = allSongs[index]
-		return picked
-	}
-}
-
-async function loopPlay(fn) {
-	// Looping play function. Takes one argument, the "pick" function,
-	// which returns a track to play. Preemptively downloads the next
-	// track while the current one is playing for seamless continuation
-	// from one song to the next. Stops when the result of the pick
-	// function is null (or similar).
-
-	async function downloadNext() {
-		const picked = fn()
-
-		if (picked == null) {
-			return false
-		}
-
-		const [ title, href ] = picked
-		console.log(`Downloading ${title}..\n${href}`)
-
-		const wavFile = `.${sanitize(title)}.wav`
-
-		const res = await fetch(href)
-		const buffer = await res.buffer()
-		await writeFile('./.temp-track', buffer)
-
-		try {
-			await convert('./.temp-track', wavFile)
-		} catch(err) {
-			console.warn('Failed to convert ' + title)
-			console.warn('Selecting a new track\n')
-
-			return await downloadNext()
-		}
-
-		await unlink('./.temp-track')
-
-		return wavFile
-	}
-
-	let wavFile = await downloadNext()
-
-	while (wavFile) {
-		const nextPromise = downloadNext()
-		await playFile(wavFile)
-		await unlink(wavFile)
-		wavFile = await nextPromise
-	}
-}
-
-function filterPlaylistByPathString(playlist, pathString) {
-	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]
-
-	const match = playlist.find(g => g[0] === cur || g[0] === cur + '/')
-
-	if (match) {
-		const groupContents = match[1]
-		if (pathParts.length > 1) {
-			const rest = pathParts.slice(1)
-			return filterPlaylistByPath(groupContents, rest)
-		} else {
-			return match
-		}
-	} else {
-		console.warn(`Not found: "${cur}"`)
-		return 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])))
-
-		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)
-}
-
-function parsePathString(pathString) {
-	const pathParts = pathString.split('/')
-	return pathParts
-}
-
-async function processArgv(argv, handlers) {
-	let i = 0
-
-	async function handleOpt(opt) {
-		if (opt in handlers) {
-			await handlers[opt]({
-				argv, index: i,
-				nextArg: function() {
-					i++
-					return argv[i]
-				},
-				alias: function(optionToRun) {
-					handleOpt(optionToRun)
-				}
-			})
-		} else {
-			console.warn('Option not understood: ' + opt)
-		}
-	}
-
-	for (; i < argv.length; i++) {
-		const cur = argv[i]
-		if (cur.startsWith('-')) {
-			const opt = cur.slice(1)
-			await handleOpt(opt)
-		}
-	}
-}
-
-readFile('./playlist.json', 'utf-8')
-	.then(plText => JSON.parse(plText))
-	.then(async playlist => {
-		let sourcePlaylist = playlist
-		let curPlaylist = playlist
-
-		let pickerType = 'shuffle'
-
-		// 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, {
-			'-open': async function(util) {
-				// --open <file>  (alias: -o)
-				// Opens a separate playlist file.
-				// This sets the source playlist.
-
-				const openedPlaylist = JSON.parse(await readFile(util.nextArg(), 'utf-8'))
-				sourcePlaylist = openedPlaylist
-				curPlaylist = openedPlaylist
-			},
-
-			'o': util => util.alias('-open'),
-
-			'-clear': function(util) {
-				// --clear  (alias: -c)
-				// Clears the active playlist. This does not affect the source
-				// playlist.
-
-				curPlaylist = []
-			},
-
-			'c': util => util.alias('-clear'),
-
-			'-keep': function(util) {
-				// --keep <groupPath>  (alias: -k)
-				// 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)
-			},
-
-			'k': util => util.alias('-keep'),
-
-			'-ignore': function(util) {
-				// --ignore <groupPath>  (alias: -i)
-				// Filters the playlist so that the given path is removed.
-
-				const pathString = util.nextArg()
-				console.log('Ignoring path: ' + pathString)
-				ignoreGroupByPathString(curPlaylist, pathString)
-			},
-
-			'i': util => util.alias('-ignore'),
-
-			'-list-groups': function(util) {
-				// --list-groups  (alias: -l, --list)
-				// Lists all groups in the playlist.
-
-				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
-				}
-			},
-
-			'-list': util => util.alias('-list-groups'),
-			'l': util => util.alias('-list-groups'),
-
-			'-list-all': function(util) {
-				// --list-all  (alias: --list-tracks, -L)
-				// Lists all groups and tracks in the playlist.
-
-				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
-				}
-			},
-
-			'-list-tracks': util => util.alias('-list-all'),
-			'L': util => util.alias('-list-all'),
-
-			'-play': function(util) {
-				// --play  (alias: -p)
-				// Forces the playlist to actually play.
-
-				willPlay = true
-			},
-
-			'p': util => util.alias('-play'),
-
-			'-no-play': function(util) {
-				// --no-play  (alias: -np)
-				// Forces the playlist not to play.
-
-				willPlay = false
-			},
-
-			'np': util => util.alias('-no-play'),
-
-			'-debug-list': function(util) {
-				// --debug-list
-				// Prints out the JSON representation of the active playlist.
-
-				console.log(JSON.stringify(curPlaylist, null, 2))
-			},
-
-			'-picker': function(util) {
-				// --picker <shuffle|ordered>
-				// Selects the mode that the song to play is picked.
-				// This should be used after finishing modifying the active
-				// playlist.
-
-				pickerType = util.nextArg()
-			}
-		})
-
-		if (willPlay || (willPlay === null && shouldPlay)) {
-			let picker
-			if (pickerType === 'shuffle') {
-				console.log('Using shuffle picker')
-				picker = makeShufflePlaylistPicker(curPlaylist)
-			} else if (pickerType === 'ordered') {
-				console.log('Using ordered picker')
-				picker = makeOrderedPlaylistPicker(curPlaylist)
-			} else {
-				console.error('Invalid picker type: ' + pickerType)
-			}
-
-			return loopPlay(picker)
-		} else {
-			return curPlaylist
-		}
-	})
-	.catch(err => console.error(err))