405 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			405 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
#! /bin/bash
 | 
						|
#
 | 
						|
# ptgdp - Play the Goddamned Playlist
 | 
						|
# Copyright (C) 2019 Vintage Salt <rehashedsalt@cock.li>
 | 
						|
#
 | 
						|
# Distributed under terms of the MIT license.
 | 
						|
#
 | 
						|
set -e
 | 
						|
 | 
						|
# Import user-dirs.dirs
 | 
						|
_xdguserdirs="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
 | 
						|
[ -z "$XDG_MUSIC_DIR" ] && [ -r "$_xdguserdirs" ] && source "$_xdguserdirs"
 | 
						|
unset _xdguserdirs
 | 
						|
# Read-only set-once variables
 | 
						|
declare -r _name="$(basename -- "$0")"
 | 
						|
declare -r _musicdir="${XDG_MUSIC_DIR:-$HOME/Music}"
 | 
						|
declare -r _ptgdpmusicdir="$_musicdir/PTGDP Songs"
 | 
						|
declare -ra _supportedbackends=("mpd" "audacious")
 | 
						|
# May-need-amended variables
 | 
						|
declare _mpdroot="$_musicdir"
 | 
						|
# Options
 | 
						|
declare -A _config=(
 | 
						|
	[backend]="mpd"
 | 
						|
	[ytdl_args]="--geo-bypass"
 | 
						|
)
 | 
						|
declare _optconfigfile="${XDG_CONFIG_HOME:-$HOME/.config}/${_name}.conf"
 | 
						|
declare -i _optautoplay=0
 | 
						|
declare -i _optdlonly
 | 
						|
declare -i _opthelp
 | 
						|
declare -i _optmachinemode
 | 
						|
declare -i _optverbose
 | 
						|
# Working variables
 | 
						|
declare -a _queue
 | 
						|
declare -a _args
 | 
						|
declare _return
 | 
						|
 | 
						|
# Helper functions
 | 
						|
log() {
 | 
						|
	# Print a line to the terminal if _optverbose is greater than $2
 | 
						|
	# $2 defaults to 0
 | 
						|
	# loglevel 0: Daily-use messages
 | 
						|
	# loglevel 1: Detailed but not quite debugging
 | 
						|
	# loglevel 2: Definitely debugging
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	local -i outstream=1
 | 
						|
	[ -n "$_optmachinemode" ] && outstream=2
 | 
						|
	if (( _optverbose >= ${2:-0} )); then
 | 
						|
		printf "%s\\n" "$1" >&"$outstream"
 | 
						|
	fi
 | 
						|
}
 | 
						|
warn() {
 | 
						|
	# Print a yellow line to the terminal, respecting _optverbose
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	local -i outstream=1
 | 
						|
	[ -n "$_optmachinemode" ] && outstream=2
 | 
						|
	if (( _optverbose >= ${2:-0} )); then
 | 
						|
		if [ -t "$outstream" ]; then
 | 
						|
			printf "\\e[33m%s\\e[0m\\n" "$1" >&"$outstream"
 | 
						|
		else
 | 
						|
			printf "WARN: %s\\n" "$1" >&"$outstream"
 | 
						|
		fi
 | 
						|
	fi
 | 
						|
}
 | 
						|
error() {
 | 
						|
	# Print a red line to the terminal, exit if $2 is specified
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	if [ -t 2 ]; then
 | 
						|
		printf "\\e[31m%s\\e[0m\\n" "$1" 1>&2
 | 
						|
	else
 | 
						|
		printf "ERROR: %s\\n" "$1" 1>&2
 | 
						|
	fi
 | 
						|
	[ -n "$2" ] && exit "${2:-1}"
 | 
						|
}
 | 
						|
has() {
 | 
						|
	# Parse out all arguments and try to find them in path
 | 
						|
	# If an argument cannot be found, set _return and fail
 | 
						|
	for prog in "$@"; do
 | 
						|
		if ! command -v "$prog" > /dev/null 2>&1; then
 | 
						|
			_return="$prog"
 | 
						|
			return 1
 | 
						|
		fi
 | 
						|
	done
 | 
						|
	return 0
 | 
						|
}
 | 
						|
sanitize() {
 | 
						|
	# Strip a line of special charactersj
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	_return="${1//[^ a-zA-Z0-9\[\]|()_-:]/}"
 | 
						|
}
 | 
						|
 | 
						|
# More complex helper functions
 | 
						|
checksong() {
 | 
						|
	# Check to see if a song exists in the cache
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	# Very rudimentary implementation
 | 
						|
	# TODO: Make this a lot more thorough, maybe ensure MIME is good
 | 
						|
	sanitize "$1"
 | 
						|
	for file in "$_ptgdpmusicdir/$_return"*; do
 | 
						|
		if [ -f "$file" ]; then
 | 
						|
			_return="$file"
 | 
						|
			return 0
 | 
						|
		fi
 | 
						|
	done
 | 
						|
	unset _return
 | 
						|
	return 1
 | 
						|
}
 | 
						|
cachesong() {
 | 
						|
	# Check to see if a song exists in the cache, downloading if not
 | 
						|
	# Sets _returnstatus to either "dl", "cache", or "err"
 | 
						|
	# Sets _return to the song filename
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	sanitize "$1"
 | 
						|
	filename="$_ptgdpmusicdir/$_return"
 | 
						|
	if ! checksong "$_return"; then
 | 
						|
		log "Downloading new song \"$1\"" 2
 | 
						|
		local ytdllog
 | 
						|
		if ytdllog=$(youtube-dl \
 | 
						|
			--add-metadata \
 | 
						|
			--audio-format "best" \
 | 
						|
			--default-search "ytsearch" \
 | 
						|
			--playlist-items 1 \
 | 
						|
			"${_config[ytdl_args]}" \
 | 
						|
			-x \
 | 
						|
			-o "$filename.%(ext)s" \
 | 
						|
			"$1")
 | 
						|
		then
 | 
						|
			# Sanitize removes all periods, so this is safe
 | 
						|
			_return="$filename".*
 | 
						|
			_returnstatus="dl"
 | 
						|
			log "Downloaded song to \"$_return\"" 2
 | 
						|
			log "$ytdllog" 2
 | 
						|
			return 0
 | 
						|
		else
 | 
						|
			log "$ytdllog" 2
 | 
						|
			_returnstatus="err"
 | 
						|
			return 1
 | 
						|
		fi
 | 
						|
	else
 | 
						|
		_returnstatus="cache"
 | 
						|
		return 0
 | 
						|
	fi
 | 
						|
}
 | 
						|
queuesong() {
 | 
						|
	# Add a song to _queue, downloading if necessary
 | 
						|
	# Sets _return to one of: dl, cache, err
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	cachesong "$1" || return 1
 | 
						|
	case $_returnstatus in
 | 
						|
		cache)
 | 
						|
			log "Using cached song \"$1\"" 1
 | 
						|
			log " $_return" 2
 | 
						|
			;;
 | 
						|
		*)
 | 
						|
			log "Downloaded song \"$1\""
 | 
						|
			log " $_return" 2
 | 
						|
			;;
 | 
						|
	esac
 | 
						|
	_queue+=("$_return")
 | 
						|
}
 | 
						|
validateline() {
 | 
						|
	# Takes a line and errors if it's just whitespace or a comment
 | 
						|
	local linenows=${1//[[:space:]]}
 | 
						|
	if ! [ "${1#\#}" = "$1" ] || [ -z "$linenows" ]; then
 | 
						|
		return 1
 | 
						|
	fi
 | 
						|
	return 0
 | 
						|
}
 | 
						|
 | 
						|
# Backend-specific functions
 | 
						|
backend-validate() {
 | 
						|
	# Ensure the backend is even proper
 | 
						|
	# Returns 1 for missing dependencies
 | 
						|
	# Everything after that is backend-specific. See below
 | 
						|
	(
 | 
						|
		for backend in ${_supportedbackends[@]}; do
 | 
						|
			if [ "$backend" = "${_config[backend]}" ]; then
 | 
						|
				return 0
 | 
						|
			fi
 | 
						|
		done
 | 
						|
		return 1
 | 
						|
	) || return $?
 | 
						|
	# Backend-specific checks
 | 
						|
	case ${_config[backend]} in
 | 
						|
		audacious)
 | 
						|
			has audacious audtool || return 1
 | 
						|
			;;
 | 
						|
		mpd)
 | 
						|
			has mpd mpc || return 1
 | 
						|
			pgrep mpd > /dev/null 2>&1 || return 2
 | 
						|
			;;
 | 
						|
	esac
 | 
						|
	return 0
 | 
						|
}
 | 
						|
backend-enqueue() { # Enqueues a song
 | 
						|
	# Note: mpd will assume you've updated the library since!
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	case ${_config[backend]} in
 | 
						|
		audacious)
 | 
						|
			audtool --playlist-addurl "$1"
 | 
						|
			return 0
 | 
						|
			;;
 | 
						|
		mpd)
 | 
						|
			file=${1##$_mpdroot/}
 | 
						|
			if ! mpc add "$file" > /dev/null 2>&1; then
 | 
						|
				error "Could not add file: \"$file\" ($?)"
 | 
						|
			fi
 | 
						|
			return 0
 | 
						|
			;;
 | 
						|
	esac
 | 
						|
	return 1
 | 
						|
}
 | 
						|
backend-play() {
 | 
						|
	# Plays a set up queue
 | 
						|
	case ${_config[backend]} in
 | 
						|
		audacious)
 | 
						|
			audtool --playback-play
 | 
						|
			;;
 | 
						|
		mpd)
 | 
						|
			if ! mpc play > /dev/null 2>&1; then
 | 
						|
				error "Could not play queue ($?)"
 | 
						|
				return 1
 | 
						|
			fi
 | 
						|
			return 0
 | 
						|
			;;
 | 
						|
	esac
 | 
						|
	log "Started playback" 1
 | 
						|
}
 | 
						|
backend-execqueue() {
 | 
						|
	# Executes a queue, enqueueing files and autoplaying if configured
 | 
						|
	case ${_config[backend]} in
 | 
						|
		audacious)
 | 
						|
			if ! pgrep audacious > /dev/null 2>&1; then
 | 
						|
				warn "Audacious is not running; songs will be downloaded but not enqueued"
 | 
						|
			fi
 | 
						|
			;;
 | 
						|
		mpd)
 | 
						|
			if ! mpc update --wait > /dev/null 2>&1; then
 | 
						|
				error "Failed to update mpd library" 51
 | 
						|
			fi
 | 
						|
			;;
 | 
						|
	esac
 | 
						|
	if [ -z "$_optdlonly" ]; then
 | 
						|
		for song in "${_queue[@]}"; do
 | 
						|
			backend-enqueue "$song"
 | 
						|
		done
 | 
						|
		if [ "$_optautoplay" != "0" ]; then
 | 
						|
			backend-play
 | 
						|
		fi
 | 
						|
	fi
 | 
						|
}
 | 
						|
 | 
						|
# Core program functions
 | 
						|
printhelp() {
 | 
						|
	cat << EOF
 | 
						|
Usage: $_name [OPTION]... [FILE]...
 | 
						|
Use youtube-dl and a music player to queue up or download a number of songs
 | 
						|
given plaintext files with only search queries
 | 
						|
 | 
						|
  -c [FILE]		Load the given file in place of the usual config file
 | 
						|
  -d			Download songs but don't queue them up
 | 
						|
  -h			Print this help text
 | 
						|
  -m			Machine-processible mode. Output all log text to STDERR
 | 
						|
			and print only the final list of songs to STDOUT,
 | 
						|
			separated by newlines.
 | 
						|
  -p			Play the queue after it's built
 | 
						|
  -v			Print more status messages. Stacks
 | 
						|
 | 
						|
Copyright (c) 2019 rehashedsalt@cock.li
 | 
						|
Licensed under the MIT license
 | 
						|
EOF
 | 
						|
}
 | 
						|
playlist() {
 | 
						|
	[ -z "$1" ] && return 1
 | 
						|
	[ -e "$1" ] || error "Playlist \"$1\" does not exist" 60
 | 
						|
	[ -r "$1" ] || error "Playlist \"$1\" could not be read" 61
 | 
						|
	local -i dlcache=0
 | 
						|
	local -i dlsuccess=0
 | 
						|
	local -i dlerr=0
 | 
						|
	local -i maxlines=0
 | 
						|
	while read line; do
 | 
						|
		validateline "$line" || continue
 | 
						|
		maxlines+=1
 | 
						|
	done < "$1"
 | 
						|
	log "Parsed playlist \"$1\" with $maxlines songs"
 | 
						|
	while read line; do
 | 
						|
		if validateline "$line"; then
 | 
						|
			queuesong "$line" || error "Failed to enqueue song: \"$line\""
 | 
						|
			case $_returnstatus in
 | 
						|
				dl)
 | 
						|
					dlsuccess+=1
 | 
						|
					;;
 | 
						|
				cache)
 | 
						|
					dlcache+=1
 | 
						|
					;;
 | 
						|
				err)
 | 
						|
					dlerr+=1
 | 
						|
					;;
 | 
						|
			esac
 | 
						|
		else
 | 
						|
			continue
 | 
						|
		fi
 | 
						|
	done < "$1"
 | 
						|
	if [ -n "$_optmachinemode" ]; then
 | 
						|
		for file in "${_queue[@]}"; do
 | 
						|
			printf "$file\\n"
 | 
						|
		done
 | 
						|
	else
 | 
						|
		backend-execqueue
 | 
						|
	fi
 | 
						|
	log "Finished: $dlcache cached, $dlsuccess downloaded, $dlerr failed"
 | 
						|
}
 | 
						|
 | 
						|
# Main
 | 
						|
main() {
 | 
						|
	# Getopts before anything else
 | 
						|
	while getopts ":c:dhmpv" opt; do
 | 
						|
		case $opt in
 | 
						|
			c)
 | 
						|
				_optconfigfile="$OPTARG"
 | 
						|
				;;
 | 
						|
			d)
 | 
						|
				_optdlonly=1
 | 
						|
				;;
 | 
						|
			h)
 | 
						|
				_opthelp=1
 | 
						|
				;;
 | 
						|
			m)
 | 
						|
				_optmachinemode=1
 | 
						|
				;;
 | 
						|
			p)
 | 
						|
				_optautoplay=1
 | 
						|
				;;
 | 
						|
			v)
 | 
						|
				_optverbose+=1
 | 
						|
				;;
 | 
						|
			:)
 | 
						|
				error "Option requires argument: -$OPTARG" 2
 | 
						|
				;;
 | 
						|
			*)
 | 
						|
				error "Invalid option: -$OPTARG" 2
 | 
						|
				;;
 | 
						|
		esac
 | 
						|
	done
 | 
						|
	# Parse out a config file if it exists
 | 
						|
	if [ -f "$_optconfigfile" ]; then
 | 
						|
		log "Loading config file" 2
 | 
						|
		while read line; do
 | 
						|
			# If the line has an equals sign and isn't a comment
 | 
						|
			if [ "$line" != "${line#*=}" ] && validateline "$line"; then
 | 
						|
				local varname="${line%=*}"
 | 
						|
				local value="${line#*=}"
 | 
						|
				_config[$varname]="$value"
 | 
						|
				log "Setting $varname to $value" 2
 | 
						|
			fi
 | 
						|
		done < "$_optconfigfile"
 | 
						|
	else
 | 
						|
		warn "Could not find configuration file" 2
 | 
						|
	fi
 | 
						|
	# Store arguments
 | 
						|
	shift $((OPTIND - 1))
 | 
						|
	for arg in "$@"; do
 | 
						|
		_args+=("$arg")
 | 
						|
	done
 | 
						|
	# Validate critical options
 | 
						|
	(
 | 
						|
		for backend in ${_supportedbackends[@]}; do
 | 
						|
			if [ "$backend" = "${_config[backend]}" ]; then
 | 
						|
				return 0
 | 
						|
			fi
 | 
						|
		done
 | 
						|
		return 1
 | 
						|
	) || error "Unsupported backend: ${_config[backend]}" 50
 | 
						|
	if [ -z "$_optmachinemode" ] && ! backend-validate; then
 | 
						|
		errorcode=$?
 | 
						|
		case $errorcode in
 | 
						|
			1)
 | 
						|
				error "Missing dependency for backend ${_config[backend]}: $_return" 50
 | 
						|
				;;
 | 
						|
			*)
 | 
						|
				error "Backend error: $errorcode: $_return" 50
 | 
						|
				;;
 | 
						|
		esac
 | 
						|
	fi
 | 
						|
 | 
						|
	# Pre-really-do-stuff hooks like help text
 | 
						|
	[ -n "$_opthelp" ] && printhelp && exit 0
 | 
						|
 | 
						|
	# Do the do
 | 
						|
	log "Validating dependencies" 2
 | 
						|
	if ! has youtube-dl pgrep; then
 | 
						|
		error "Failed to find program: $_return" 1
 | 
						|
	fi
 | 
						|
	if [ -n "${_args[*]}" ]; then
 | 
						|
		for arg in "${_args[@]}"; do
 | 
						|
			playlist "$arg"
 | 
						|
		done
 | 
						|
	else
 | 
						|
		warn "Nothing to do"
 | 
						|
		exit 0
 | 
						|
	fi
 | 
						|
}
 | 
						|
 | 
						|
main "$@"
 | 
						|
 |