2019-06-13 07:34:00 -05:00
#! /bin/bash
#
2019-06-19 06:21:20 -05:00
# ptgdp - Play the Goddamned Playlist
2019-06-13 07:34:00 -05:00
# Copyright (C) 2019 Vintage Salt <rehashedsalt@cock.li>
#
# Distributed under terms of the MIT license.
#
2019-08-13 04:34:22 -05:00
# TODO:
# * Reorganize mpd integration into abstract functions
2019-06-13 07:34:00 -05:00
# Variables
_name="$(basename -- "$0")"
_tmpdir="${XDG_CACHE_HOME:-$HOME/.cache}/$_name"
2019-07-23 14:08:55 -05:00
_tmpfile="$_tmpdir/tmpfile-$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 12)"
2019-06-13 07:50:58 -05:00
_xdguserdirs="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
2019-08-14 04:15:28 -05:00
[ -z "$XDG_MUSIC_DIR" ] && [ -f "$_xdguserdirs" ] && source "$_xdguserdirs"
2019-08-14 04:09:14 -05:00
_musicdir="${XDG_MUSIC_DIR:-$HOME/Music}"
_ptgdpmusicdir="$_musicdir/${PTGDP_MUSIC_DIR:-PTGDP Songs}"
2019-08-18 01:26:53 -05:00
declare -a _queue
2019-09-01 23:33:04 -05:00
declare -a _playlists
2019-06-13 07:34:00 -05:00
2019-09-13 22:00:34 -05:00
# Preprocessor-accessible vars
declare p_ytdlargs="--geo-bypass"
declare p_search="ytsearch:"
2019-06-13 07:34:00 -05:00
# Helper functions
log() {
2019-07-04 03:59:02 -05:00
[ -z "$1" ] && return 1 # Message body
2019-06-13 07:34:00 -05:00
printf "%s: %s\\n" \
"$_name" \
"$1"
}
error() {
2019-07-04 03:59:02 -05:00
[ -z "$1" ] && return 1 # Message body
2019-07-28 17:46:30 -05:00
# 2: Exit code
printf "%s: \\e[31m%s\\e[0m\\n" \
"$_name" \
"$1"
[ -n "$2" ] && exit "${2:-1}"
2019-06-13 07:34:00 -05:00
}
2019-07-04 03:59:02 -05:00
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
}
2019-09-13 21:25:18 -05:00
execpreprocessor() {
# Execute a preprocessor line
# $1: The line, either stripped or not of the leading #:
line="${1#\#:}"
2019-09-13 22:00:34 -05:00
read -r -a args <<< "$line"
case ${args[0]} in
ytdlargs)
local old="$p_ytdlargs"
p_ytdlargs="${args[@]:1}"
if ! [ "$p_ytdlargs" = "$old" ]; then
log "Setting youtube-dl arguments \"$p_ytdlargs\""
fi
;;
search)
local old="$p_search"
p_search="${args[@]:1}"
if [ "$p_search" = "${p_search%:}" ]; then
p_search="${p_search}:"
fi
if ! [ "$p_search" = "$old" ]; then
log "Setting default search method \"$p_search\""
fi
;;
*)
error "Preprocessor command \"${args[0]}\" not implemented"
;;
esac
2019-09-13 21:25:18 -05:00
}
2019-08-13 04:00:52 -05:00
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
2019-09-13 21:13:48 -05:00
sanitize "$1"
for file in "$_ptgdpmusicdir/$_return"*; do
2019-08-13 04:33:55 -05:00
if [ -f "$file" ]; then
_return="$file"
return 0
fi
done
2019-09-13 21:13:48 -05:00
unset _return
2019-08-13 04:33:55 -05:00
return 1
2019-08-13 04:00:52 -05:00
}
2019-08-14 04:09:14 -05:00
cachesong() {
# $1: A song query to download
# $_return: The relative file path to song from XDG_MUSIC_DIR
# If in a dry run, $_return is the title of the video, if extracted
# Note that this will return 100 if the file is cached, so do not assume only 0 is success
[ -z "$1" ] && return 1
# Clean up tmpfile
rm "$_tmpfile"* > /dev/null 2>&1
sanitize "$1"
filename="$_ptgdpmusicdir/$_return"
if [ -z "$_optdryrun" ]; then
if ! checkforsong "$_return"; then
# We need to download the song
youtube-dl \
--add-metadata \
--audio-format "best" \
2019-09-13 22:00:34 -05:00
--default-search "$p_search" \
2019-08-14 04:09:14 -05:00
--playlist-items 1 \
2019-09-13 22:00:34 -05:00
"$p_ytdlargs" \
2019-08-14 04:09:14 -05:00
-x \
-o "$_tmpfile.%(ext)s" \
2019-09-13 20:29:54 -05:00
"$1" \
2019-08-14 04:09:14 -05:00
> /dev/null 2>&1 &
if wait $!; then
for file in "$_tmpfile"*; do
[ -f "$file" ] && local extension="$file"
break
done
extension="${extension##*/}"
extension="${extension##*.}"
filename="$filename.$extension"
2019-09-13 22:31:27 -05:00
[ -f "$_tmpfile"* ] || return 1
2019-08-14 04:09:14 -05:00
mv "$_tmpfile"* "$filename"
_return="$filename"
return 0
else
return 1
fi
else
#_return is already an existing cached file, so just die
return 100
fi
else
output="$(
youtube-dl \
2019-09-13 21:13:48 -05:00
--default-search "ytsearch:" \
2019-08-14 04:09:14 -05:00
--get-title \
--geo-bypass \
--playlist-items 1 \
2019-09-13 21:13:48 -05:00
"$1" 2>&1
2019-08-14 04:09:14 -05:00
)"
exitcode="$?"
if [ $exitcode -gt 0 ]; then
return 1
fi
if ! [ "$output" = "${output#*WARNING}" ]; then
# Title could not be extracted
unset _return
else
# Title was found
_return="$output"
fi
return 0
fi
}
2019-08-18 01:26:53 -05:00
queuesong() {
# $1: A song query to queue
# returns 0 if successful, 100 if cached, 1 if error
# Adds a song to $_queue
[ -z "$1" ] && return 2
cachesong "$1"
local errorcode=$?
if [ -z "$_optdryrun" ]; then
case $errorcode in
0)
# Downloaded
log "Downloaded song \"$1\""
;;
100)
# Cached
log "Using cached song \"$1\""
;;
*)
# Failed
dlfailure+=1
notify "Could not download song" \
"youtube-dl did not download a song for \"$1\"" \
normal dialog-error 3000
error "Could not download song \"$1\""
;;
esac
_queue+=("$_return")
else
case $errorcode in
0)
# Success
if [ -n "$_return" ]; then
log "$1 - \"$output\""
else
log "$1 parsed, but title could not be extracted"
fi
;;
*)
# Failure
error "Could not find song \"$1\""
;;
esac
fi
return $errorcode
}
execqueue() {
# Plays all songs in _queue
mpc update --wait > /dev/null 2>&1
for file in "${_queue[@]}"; do
file=${file##$_musicdir/}
mpc add "$file" > /dev/null 2>&1 || error "Could not add file: \"$file\""
done
2019-09-01 23:33:04 -05:00
}
execplaylists() {
# Plays all playlists in _playlists
for list in "${_playlists[@]}"; do
playlist "$list" || error "Failed to play playlist: \"$list\""
done
2019-09-06 02:56:00 -05:00
if [ -n "$_optautoplay" ] && [ -z "$_optdryrun" ]; then
2019-08-18 01:26:53 -05:00
mpc play > /dev/null 2>&1
2019-09-01 23:33:04 -05:00
log "Started playback"
2019-08-18 01:26:53 -05:00
unset _optautoplay
fi
}
2019-06-13 07:34:00 -05:00
validatedeps() {
2019-07-04 03:59:02 -05:00
# $@: Dependencies to validate
2019-06-13 07:34:00 -05:00
for prog in "$@"; do
if ! command -v "$prog" > /dev/null 2>&1; then
_return="$prog"
return 1
fi
done
return 0
}
2019-08-14 04:34:32 -05:00
validateline() {
# $1: A line to check for comments or whitespace
2019-08-18 01:26:53 -05:00
# Strictly speaking, this removes all whitespace from the line
# While not *exactly* what I'm looking for, it's sufficient for trimming whitespace lines
2019-08-14 04:34:32 -05:00
local linenows=${1//[[:space:]]}
2019-09-13 21:25:18 -05:00
# Preprocessor commands
if ! [ "${1#\#:}" = "$1" ]; then
return 100
fi
# Comments and whitespace-only lines
2019-08-14 04:34:32 -05:00
if ! [ "${1#\#}" = "$1" ] || [ -z "$linenows" ]; then
return 1
fi
return 0
}
2019-08-18 01:26:53 -05:00
validateplaylistenv() {
[ -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
}
2019-06-14 06:11:02 -05:00
sanitize() {
2019-07-04 03:59:02 -05:00
[ -z "$1" ] && return 1 # String to strip special chars from
2019-09-13 21:13:48 -05:00
_return="${1//[^ a-zA-Z0-9\[\]|()_-:]/}"
2019-06-14 06:11:02 -05:00
}
2019-06-13 07:34:00 -05:00
# Traps
trapexit() {
kill $(jobs -p) > /dev/null 2>&1
2019-06-13 07:43:21 -05:00
[ -n "$_tmpdir" ] && rm "$_tmpfile"* > /dev/null 2>&1
2019-06-13 07:34:00 -05:00
}
# Critical functions
clearcache() {
2019-08-14 04:09:14 -05:00
[ -n "$_ptgdpmusicdir" ] && rm "$_ptgdpmusicdir"/* > /dev/null 2>&1
2019-06-13 07:34:00 -05:00
log "Cache has been emptied"
}
helptext() {
cat << EOF
2019-09-01 23:33:04 -05:00
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.
2019-06-13 07:58:48 -05:00
2019-06-13 07:34:00 -05:00
-c Clears the cache (which can become quite large)
2019-06-13 07:37:02 -05:00
-d Download only; don't queue anything up
2019-07-28 17:46:30 -05:00
-D Dry run; parse out all songs, downloaded or not, and
2019-09-06 02:56:00 -05:00
print out the resolved names
-p Play the playlist after it is enqueued
-s Shuffle the playlist before enqueueing
2019-06-13 07:34:00 -05:00
-r <directory> Start up rofi, if installed, and present a listing of
2019-07-04 03:59:02 -05:00
all .gdp files in the given directory. If notify-send
is installed, this will also send notifications
pertaining to playlist status.
2019-08-18 01:26:53 -05:00
-R <directory|file> As above, but also parse out the selected playlist and
present a listing of individual songs. The argument can
be a file in this mode
2019-06-13 07:34:00 -05:00
-h Print this help text
2019-09-13 22:00:34 -05:00
Preprocessor Commands
Any line prefixed with #: is interpreted as a preprocessor command. Commands
are IFS-separated with everything after the first word interpreted as arguments.
search Set the default search prefix, with or without a
trailing colon.
ytdlargs A set of arguments to pass to youtube-dl
2019-08-13 03:47:37 -05:00
Environment Variables
2019-09-13 22:00:34 -05:00
In addition to XDG-spec variables (XDG_CACHE_HOME, user-dirs.dirs, etc.),
$_name also respects an additional variable:
2019-08-13 03:47:37 -05:00
PTGDP_MUSIC_DIR The subdirectory in XDG_MUSIC_DIR to save music to
2019-06-13 07:34:00 -05:00
Copyright (c) 2019 rehashedsalt@cock.li
Licensed under the MIT license
EOF
}
rofimenu() {
validatedeps rofi || error "$_return is not currently installed" 1
2019-08-18 01:26:53 -05:00
if ! [ -d "$_optrofi" ]; then
if [ -z "$_optrofisong" ]; then
error "Could not open directory \"$_optrofi\"" 2
else
[ -f "$_optrofi" ] || error "Could not open file \"$_optrofi\"" 2
2019-08-18 01:31:26 -05:00
rofiplaysong "$_optrofi"
return $?
2019-08-18 01:26:53 -05:00
fi
fi
2019-06-14 06:16:43 -05:00
files=$(find "$_optrofi" -type f -name \*.gdp)
2019-06-14 06:11:02 -05:00
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
2019-07-04 03:59:02 -05:00
notify "No playlists found" \
"No playlists could be found in directory \"$_optrofi\"." \
normal dialog-error 5000
2019-06-14 06:11:02 -05:00
error "No playlists found" 61
fi
2019-08-18 01:26:53 -05:00
prompt="$_name list"
[ -n "$_optautplay" ] && prompt="$prompt ap"
choice="$(rofi -dmenu -i -p "$prompt" <<< "$playlists" 2>/dev/null)"
2019-06-14 06:11:02 -05:00
[ -z "$choice" ] && error "User aborted at selection" 62
2019-08-18 01:26:53 -05:00
if [ -z "$_optrofisong" ]; then
2019-09-01 23:33:04 -05:00
_playlists+=("$_optrofi"/"$choice".gdp)
execplaylists
2019-08-18 01:26:53 -05:00
else
rofiplaysong "$_optrofi"/"$choice".gdp
fi
2019-06-13 07:34:00 -05:00
return 0
}
2019-08-18 01:26:53 -05:00
rofiplaysong() {
validateplaylistenv "$1" || return $?
local songs=""
while read line; do
validateline "$line" || continue
if [ -z "$songs" ]; then
songs="$line"
else
songs="$songs"$'\n'"$line"
fi
done < "$1"
IFS=$'\n'
prompt="$_name song"
[ -n "$_optautplay" ] && prompt="$prompt ap"
choice="$(rofi -dmenu -i -p "$prompt" <<< "$songs" 2>/dev/null)"
if [ -z "$choice" ]; then
# Assume user is looking for another list
2019-08-18 01:31:26 -05:00
# Kick him back to the main menu, unless he picked a file
if ! [ "$_optrofi" = "$1" ]; then
rofimenu
else
error "User aborted at selection" 62
fi
2019-08-13 03:39:19 -05:00
fi
2019-08-18 01:26:53 -05:00
queuesong "$choice"
[ "$?" = "1" ] && return 62
execqueue
return 0
}
playlist() {
validateplaylistenv "$1" || return $?
2019-08-14 04:09:14 -05:00
[ -z "$_optdryrun" ] && local -a queue # An array of songs to later enqueue into mpd
2019-07-04 03:59:02 -05:00
local -i dlexist=0
local -i dlsuccess=0
local -i dlfailure=0
2019-08-14 04:31:57 -05:00
local -i maxlines=0
while read line; do
2019-08-14 04:34:32 -05:00
validateline "$line" || continue
2019-08-14 04:31:57 -05:00
maxlines+=1
done < "$1"
2019-08-19 01:49:05 -05:00
log "Parsed playlist \"$1\" with $maxlines songs"
2019-09-13 22:00:34 -05:00
# Store old preprocessor settings
declare -a oldsettings
while read var; do
oldsettings+=("$(declare -p "$var")")
done < <(compgen -v "p_")
# Parse out the playlist
2019-06-13 07:34:00 -05:00
while read line; do
2019-09-13 21:25:18 -05:00
validateline "$line"
case $? in
2019-08-18 01:26:53 -05:00
0)
2019-09-13 21:25:18 -05:00
rm "$_tmpfile"* > /dev/null 2>&1
# Do the do
queuesong "$line"
# What did we do?
local errorcode=$?
case $errorcode in
0)
dlsuccess+=1
;;
100)
dlexist+=1
;;
*)
dlfailure+=1
;;
esac
2019-08-18 01:26:53 -05:00
;;
100)
2019-09-13 21:25:18 -05:00
execpreprocessor "$line"
2019-08-18 01:26:53 -05:00
;;
*)
2019-09-13 21:25:18 -05:00
continue
2019-08-18 01:26:53 -05:00
;;
esac
2019-06-13 07:34:00 -05:00
done < <(if [ -n "$_optshuffle" ]; then shuf "$1"; else cat "$1"; fi)
2019-09-13 22:00:34 -05:00
# Return to normality
for var in "${oldsettings[@]}"; do
eval "$var"
done
# Dump some stats
2019-07-28 17:50:23 -05:00
if [ "$dlexist" = "0" ] && [ "$dlsuccess" = "0" ] && [ -z "$_optdryrun" ]; then
2019-08-14 04:09:14 -05:00
notify "Failed to download playlist" \
"The playlist in its entirety could not be downloaded. Ensure that youtube-dl is up to date, you have a valid internet connection, and your search queries pull up results" \
2019-07-04 03:59:02 -05:00
normal dialog-error 10000
2019-07-28 17:50:23 -05:00
elif [ -z "$_optdryrun" ]; then
2019-08-19 02:12:47 -05:00
if ! [ "$dlfailure" = "0" ]; then
error "Some songs failed to download" 0
fi
2019-07-04 03:59:02 -05:00
if [ -z "$_optdownloadonly" ]; then
2019-08-14 04:09:14 -05:00
notify "Finished downloading playlist" \
"The playlist has been downloaded and will be added to mpd shortly"
log "Finished downloading: $dlexist cached, $dlsuccess downloaded, $dlfailure failed"
2019-08-18 01:26:53 -05:00
execqueue
2019-07-04 03:59:02 -05:00
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
2019-07-28 17:50:23 -05:00
else
log "Finished dry run: $dlsuccess succeeded, $dlfailure failed"
2019-06-13 07:37:59 -05:00
fi
2019-06-13 07:34:00 -05:00
}
# Main
main() {
# Boostrapping and setup
2019-06-13 17:17:44 -05:00
validatedeps youtube-dl basename || error "Critical dependency $_return was not met" 1
2019-06-13 07:34:00 -05:00
mkdir -p "$_tmpdir"
2019-08-14 04:09:14 -05:00
mkdir -p "$_ptgdpmusicdir"
2019-06-13 07:34:00 -05:00
trap trapexit EXIT
2019-09-01 23:33:04 -05:00
# Get options
2019-08-18 01:26:53 -05:00
while getopts ":cdDf:pr:R:sh" opt; do
2019-06-13 07:34:00 -05:00
case $opt in
c)
clearcache
exit $?
;;
2019-06-13 07:37:02 -05:00
d)
_optdownloadonly=1
;;
2019-07-28 17:46:30 -05:00
D)
_optdryrun=1
;;
2019-06-14 06:11:02 -05:00
p)
_optautoplay=1
;;
2019-06-13 07:34:00 -05:00
r)
2019-06-14 06:16:43 -05:00
_optrofi="$OPTARG"
2019-06-13 07:34:00 -05:00
;;
2019-08-18 01:26:53 -05:00
R)
_optrofi="$OPTARG"
_optrofisong=1
;;
2019-06-13 07:34:00 -05:00
s)
_optshuffle=1
;;
h)
helptext
exit $?
;;
:)
error "Option requires argument: -$OPTARG" 2
;;
*)
error "Invalid option: -$OPTARG" 2
;;
esac
done
2019-09-01 23:33:04 -05:00
# Get program arguments (playlists)
shift $((OPTIND - 1))
for list in "$@"; do
if ! [ -f "$list" ]; then
error "Failed to find playlist: $list"
continue
fi
_playlists+=("$list")
done
2019-08-19 01:45:25 -05:00
[ -z "$_optdownloadonly" ] && [ -z "$_optdryrun" ] && ! validatedeps mpc && error "$_return is required outside of dry- and download-only runs"
2019-09-01 23:33:04 -05:00
[ -n "$_playlists" ] && [ -n "$_optrofi" ] && error "Flag -r cannot be used with playlist arguments" 2
2019-06-13 07:34:00 -05:00
if [ -n "$_optrofi" ]; then rofimenu; exit $?; fi
2019-09-01 23:33:04 -05:00
if [ -n "$_playlists" ]; then
execplaylists
exit $?
else
error "Nothing to do" 1
fi
2019-06-13 07:34:00 -05:00
}
main "$@"