#! /bin/bash
#
# ptgdp - Play the Goddamned Playlist
# Copyright (C) 2019 Vintage Salt <rehashedsalt@cock.li>
#
# Distributed under terms of the MIT license.
#

# Variables
_name="$(basename -- "$0")"
_tmpdir="${XDG_CACHE_HOME:-$HOME/.cache}/$_name"
_tmpfile="$_tmpdir/tmpfile-$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 12)"
_xdguserdirs="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
[ -f "$_xdguserdirs" ] && source "$_xdguserdirs"
_musicdir="${XDG_MUSIC_DIR:-$HOME/Music}/${PTGDP_MUSIC_DIR:-PTGDP Songs}"

# Helper functions
log() {
	[ -z "$1" ] && return 1 # Message body
	printf "%s: %s\\n" \
		"$_name" \
		"$1"
}
error() {
	[ -z "$1" ] && return 1 # Message body
	# 2: Exit code
	printf "%s: \\e[31m%s\\e[0m\\n" \
		"$_name" \
		"$1"
	[ -n "$2" ] && exit "${2:-1}"
}
notify() {
	[ -z "$_optrofi" ] && return 0
	[ -z "$1" ] && return 1 # Title
	[ -z "$2" ] && return 2 # Body
	local urg="${3:-normal}" # Urgency
	local icon="${4:-dialog-information}" # Icon
	local timeout="${5:-3000}" # Timeout in milliseconds
	notify-send \
		-a "$_name" \
		-i "$icon" \
		-u "$urg" \
		-t "$timeout" \
		"$1" \
		"$2" > /dev/null 2>&1
}
checkforsong() {
	# $1: A song name to validate
	[ -z "$1" ] && return 1
	# Very rudimentary implementation
	# TODO: Make this a lot more thorough, maybe ensure MIME is good
	for file in "$_musicdir/$1"*; do
		if [ -f "$file" ]; then
			_return="$file"
			return 0
		fi
	done
	return 1
}
validatedeps() {
	# $@: Dependencies to validate
	for prog in "$@"; do
		if ! command -v "$prog" > /dev/null 2>&1; then
			_return="$prog"
			return 1
		fi
	done
	return 0
}
sanitize() {
	[ -z "$1" ] && return 1 # String to strip special chars from
	_return="${1//[^ a-zA-Z0-9\[\]|()_-]/}"
}

# Traps
trapexit() {
	kill $(jobs -p) > /dev/null 2>&1
	[ -n "$_tmpdir" ] && rm "$_tmpfile"* > /dev/null 2>&1
}

# Critical functions
clearcache() {
	[ -n "$_musicdir" ] && rm "$_musicdir"/* > /dev/null 2>&1
	log "Cache has been emptied"
}
helptext() {
	cat << EOF
Usage: $_name [OPTION]
Use youtube-dl and mpc to queue up a playlist given a file of only search
queries. The first result found is the one that will be downloaded. Downloaded
files are cached in your Music folder under "PTGDP Songs" for offline use.
  -f <file>		The playlist file to load. The file should be plaintext
			containing a YouTube search query on each line.
			Comments are supported and prepended with #

  -c			Clears the cache (which can become quite large)
  -d			Download only; don't queue anything up
			Conflicts with -p
  -D			Dry run; parse out all songs, downloaded or not, and
			print out the resolved names. Useful for testing, as
			YouTube searches can sometimes be finicky.
  -p			Play the playlist after it is enqueued.
			Conflicts with -d
  -s			Shuffle the playlist

  -r <directory>	Start up rofi, if installed, and present a listing of
			all .gdp files in the given directory. If notify-send
			is installed, this will also send notifications
			pertaining to playlist status.

  -h			Print this help text

Environment Variables

In addition to XDG-spec variables (XDG_CACHE_HOME, user-dirs.dirs, etc.), ptgdp
also respects an additional variable:

  PTGDP_MUSIC_DIR	The subdirectory in XDG_MUSIC_DIR to save music to

Copyright (c) 2019 rehashedsalt@cock.li
Licensed under the MIT license
EOF
}
rofimenu() {
	validatedeps rofi || error "$_return is not currently installed" 1
	[ -d "$_optrofi" ] || error "Could not open directory \"$_optrofi\"" 2
	files=$(find "$_optrofi" -type f -name \*.gdp)
	if [ -n "$files" ]; then
		# Strip file suffixes for a cleaner menu
		playlists=""
		while read file; do
			filebase="$(basename -- "$file")"
			filebase="${filebase%.gdp}"
			[ -n "$playlists" ] && playlists+=$'\n'
			playlists+="$filebase"
		done <<< "$files"
	else
		notify "No playlists found" \
			"No playlists could be found in directory \"$_optrofi\"." \
			normal dialog-error 5000
		error "No playlists found" 61
	fi
	choice="$(rofi -dmenu -i -p "$_name" <<< "$playlists" 2>/dev/null)"
	[ -z "$choice" ] && error "User aborted at selection" 62
	playlist "$_optrofi"/"$choice".gdp
	return 0
}
playlist() {
	[ -z "$1" ] && return 1
	[ -e "$1" ] || error "Playlist \"$1\" does not exist" 50
	[ -f "$1" ] || error "Playlist \"$1\" is not a file" 50
	[ -r "$1" ] || error "Cannot read playlist \"$1\"" 51
	if [ -z "$_optdownloadonly" ] && ! mpc status > /dev/null 2>&1; then
		notify "Could not communicate with MPD" \
			"MPD connection was refused. Ensure your configuration is correct and the daemon is currently running." \
			normal dialog-error 3000
		error "Failed to communicate with MPD" 52
	fi
	local -a queue
	local -i dlexist=0
	local -i dlsuccess=0
	local -i dlfailure=0
	while read line; do
		[ -z "$line" ] && continue
		# Ignore comment lines
		if ! [ "${line#\#}" = "$line" ]; then
			continue
		fi
		rm "$_tmpfile"* > /dev/null 2>&1
		sanitize "$line"
		filename="$_musicdir/$_return"
		if [ -z "$_optdryrun" ]; then
			if ! checkforsong "$_return"; then
				log "Finding a song for \"$line\""
				youtube-dl \
					--add-metadata \
					--audio-format "best" \
					--geo-bypass \
					--playlist-items 1 \
					-x \
					-o "$_tmpfile.%(ext)s" \
					ytsearch:"$line" \
					> /dev/null 2>&1 &
				if wait $!; then
					dlsuccess+=1
					for file in "$_tmpfile"*; do
						[ -f "$file" ] && local extension="$file"
						break
					done
					extension="${extension##*/}"
					extension="${extension##*.}"
					filename="$filename.$extension"
					mv "$_tmpfile"* "$filename"
					queue+=("$filename")
				else
					dlfailure+=1
					notify "Could not download song" \
						"youtube-dl did not download a song for \"$line\", either because it is out of date or because it could not find a video to rip from" \
						normal dialog-error 3000
					error "Could not download song \"$line\""
					continue
				fi
			else
				queue+=("$_return")
				dlexist+=1
			fi
		else
			output="$(
				youtube-dl \
					--get-title \
					--geo-bypass \
					--playlist-items 1 \
					ytsearch:"$line" 2>&1
			)"
			exitcode="$?"
			if [ $exitcode -gt 0 ]; then
				error "Could not find song \"$line\""
				dlerror+=1
				continue
			fi
			if ! [ "$output" = "${output#*WARNING}" ]; then
				log "$line parsed, but title could not be extracted"
			else
				log "$line - \"$output\""
			fi
			dlsuccess+=1
		fi
	done < <(if [ -n "$_optshuffle" ]; then shuf "$1"; else cat "$1"; fi)
	if [ "$dlexist" = "0" ] && [ "$dlsuccess" = "0" ] && [ -z "$_optdryrun" ]; then
		notify "Failed to enqueue playlist" \
			"The playlist could not be enqueued. Ensure that youtube-dl is up to date, you have a valid internet connection, and your search queries pull up results" \
			normal dialog-error 10000
	elif [ -z "$_optdryrun" ]; then
		if [ -z "$_optdownloadonly" ]; then
			notify "Finished building queue" \
				"The playlist queue has been built in mpd"
			log "Finished building queue: $dlexist cached, $dlsuccess downloaded, $dlfailure failed"
			mpc update --wait > /dev/null 2>&1
			local xdgmusicdir="${XDG_MUSIC_DIR:-$HOME/Music}"
			for file in "${queue[@]}"; do
				file=${file##$xdgmusicdir/}
				mpc add "$file" > /dev/null 2>&1 || error "Could not add file: \"$file\""
			done
			if [ -n "$_optautoplay" ]; then
				mpc play > /dev/null 2>&1
				unset _optautoplay
			fi
		else
			notify "Finished precaching" \
				"Your songs have been cached and are ready for offline playback"
			log "Finished downloading: $dlexist cached, $dlsuccess downloaded, $dlfailure failed"
		fi
	else
		log "Finished dry run: $dlsuccess succeeded, $dlfailure failed"
	fi
}

# Main
main() {
	# Boostrapping and setup
	validatedeps youtube-dl basename || error "Critical dependency $_return was not met" 1
	mkdir -p "$_tmpdir"
	mkdir -p "$_musicdir"
	trap trapexit EXIT

	# Actual program stuff
	while getopts ":cdDf:pr:sh" opt; do
		case $opt in
			c)
				clearcache
				exit $?
				;;
			d)
				_optdownloadonly=1
				;;
			D)
				_optdryrun=1
				;;
			f)
				_optfile="$OPTARG"
				;;
			p)
				_optautoplay=1
				;;
			r)
				_optrofi="$OPTARG"
				;;
			s)
				_optshuffle=1
				;;
			h)
				helptext
				exit $?
				;;
			:)
				error "Option requires argument: -$OPTARG" 2
				;;
			*)
				error "Invalid option: -$OPTARG" 2
				;;
		esac
	done
	[ -n "$_optfile" ] && [ -n "$_optrofi" ] && error "Flags -f and -r conflict" 2
	[ -n "$_optdownloadonly" ] && [ -n "$_optautoplay" ] && error "Flags -d and -p conflict" 2
	if [ -n "$_optrofi" ]; then rofimenu; exit $?; fi
	if [ -n "$_optfile" ]; then playlist "$_optfile"; exit $?; fi
	error "Nothing to do" 0
}

main "$@"