From f35f8867f69bac0c87ab5ccebcaaf4e019cc9248 Mon Sep 17 00:00:00 2001
From: Salt <rehashedsalt@cock.li>
Date: Thu, 26 Sep 2019 16:34:31 -0500
Subject: [PATCH] Add basic functional implementation Mostly copied from
 another attempt at this Time to bug hunt

---
 README.md |  14 ++++
 ptgdp     | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 224 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index 0802afd..a0e16ea 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,20 @@ See below for more intricate usage.
 
 Move `ptgdp.conf` from this repo to `~/.config/ptgdp.conf` and change the values as you see fit. Alternatively, invoke `ptgdp` with `-c` and point it at your config file.
 
+## Return Codes
+
+| Code | Meaning |
+| --: | :-- |
+| `1` | Syntax error |
+| `50` | Misconfigured backend |
+| `51` | Backend could not be set up (i.e. mpd failed to update) |
+| `60` | Playlist does not exist |
+| `61` | Playlist is unreadable |
+
+## Idiosyncracies
+
+* Because I haven't quite figured out how to into mpd sockets, this tool assumes that `$XDG_MUSIC_DIR` (`~/Music`) is the root of your library.
+
 ## License
 
 See `LICENSE` (hint: it's MIT, just like the header says).
diff --git a/ptgdp b/ptgdp
index 18ba39a..5dc51f7 100755
--- a/ptgdp
+++ b/ptgdp
@@ -16,15 +16,19 @@ declare -r _name="$(basename -- "$0")"
 declare -r _musicdir="${XDG_MUSIC_DIR:-$HOME/Music}"
 declare -r _ptgdpmusicdir="$_musicdir/PTGDP Songs"
 declare -ra _supportedbackends=("mpd")
+# 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
 declare -i _opthelp
-declare -i _optverbose=0
+declare -i _optverbose
 # Working variables
+declare -a _queue
 declare -a _args
 declare _return
 
@@ -32,8 +36,11 @@ declare _return
 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
-	if [ "$_optverbose" -ge "${2:-0}" ]; then
+	if (( _optverbose >= ${2:-0} )); then
 		printf "%s\\n" "$1"
 	fi
 }
@@ -50,7 +57,6 @@ error() {
 has() {
 	# Parse out all arguments and try to find them in path
 	# If an argument cannot be found, set _return and fail
-	[ -z "$@" ] && return 0
 	for prog in $@; do
 		if ! command -v "$prog" > /dev/null 2>&1; then
 			_return="$prog"
@@ -59,9 +65,148 @@ has() {
 	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
+		youtube-dl \
+			--add-metadata \
+			--audio-format "best" \
+			--default-search "ytsearch" \
+			--playlist-items 1 \
+			"${_config[ytdl_args]}" \
+			-x \
+			-o "$filename.%(ext)s" \
+			"$1" \
+			> /dev/null 2>&1 &
+		if wait $!; then
+			# Sanitize removes all periods, so this is safe
+			_return="$filename".*
+			_returnstatus="dl"
+			log "Downloaded song to \"$_return\"" 2
+			return 0
+		else
+			_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 "Downloaded song \"$1\"" 1
+			;;
+	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
+		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
+		mpd)
+			file=${1##$_mpdroot/}
+			if ! mpc add "$file"; then
+				error "Could not add file: \"$file\" ($?)"
+			fi
+			return 0
+			;;
+	esac
+	return 1
+}
+backend-play() {
+	# Plays a set up queue
+	case ${_config[backend]} in
+		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
+		mpd)
+			if ! mpc update --wait > /dev/null 2>&1; then
+				error "Failed to update mpd library" 51
+			fi
+			;;
+	esac
+	for song in "${_queue[@]}"; do
+		backend-enqueue "$song"
+	done
+	[ "$_optautoplay" != "0" ] && backend-play
+}
 
 # Core program functions
 printhelp() {
@@ -72,17 +217,52 @@ given plaintext files with only search queries
 
   -c [FILE]		Load the given file in place of the usual config file
   -h			Print this help text
+  -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"
+	backend-execqueue
+	log "Finished: $dlcache cached, $dlsuccess downloaded, $dlerr failed"
+}
 
 # Main
 main() {
 	# Getopts before anything else
-	while getopts ":c:hv" opt; do
+	while getopts ":c:hpv" opt; do
 		case $opt in
 			c)
 				_optconfigfile="$OPTARG"
@@ -90,6 +270,9 @@ main() {
 			h)
 				_opthelp=1
 				;;
+			p)
+				_optautoplay=1
+				;;
 			v)
 				_optverbose+=1
 				;;
@@ -103,14 +286,14 @@ main() {
 	done
 	# Parse out a config file if it exists
 	if [ -f "$_optconfigfile" ]; then
-		log "Loading config file" 1
+		log "Loading config file" 2
 		while read line; do
 			# If the line has an equals sign and isn't a comment
-			if [ "$line" != "${line#*=}" ] && [ "$line" = "${line#\#}" ]; then
+			if [ "$line" != "${line#*=}" ] && validateline "$line"; then
 				local varname="${line%=*}"
 				local value="${line#*=}"
 				_config[$varname]="$value"
-				log "Setting $varname to $value" 1
+				log "Setting $varname to $value" 2
 			fi
 		done < "$_optconfigfile"
 	fi
@@ -128,16 +311,34 @@ main() {
 		done
 		return 1
 	) || error "Unsupported backend: ${_config[backend]}" 50
+	if ! backend-validate; then
+		errorcode=$?
+		case $errorcode in
+			1)
+				error "Missing dependency for backend ${_config[backend]}: $_return" 50
+				;;
+			*)
+				error "Dependency 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" 1
+	log "Validating dependencies" 2
 	if ! has youtube-dl; then
 		error "Failed to validate dependency on program: $_return" 1
 	fi
-	exit 0
+	if [ -n "${_args[@]}" ]; then
+		for arg in ${_args[@]}; do
+			playlist "$arg"
+		done
+	else
+		log "Nothing to do"
+		exit 0
+	fi
 }
 
 main "$@"