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-06-13 08:00:22 -05:00
# TODO:
# * Provide integration with other players, like mpd
# * Add a quick way to append to a playlist
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"
[ -f "$_xdguserdirs" ] && source "$_xdguserdirs"
2019-06-19 03:39:18 -05:00
_musicdir="${XDG_DATA_HOME:-$HOME/.local/share}/$_name"
_musiclink="${XDG_MUSIC_DIR:-$HOME/Music}/PTGDP Songs"
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-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-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-06-14 06:11:02 -05:00
_return="${1//[^ a-zA-Z0-9\[\]|()_-]/}"
}
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-06-13 07:58:48 -05:00
[ -n "$_musicdir" ] && rm "$_musicdir"/* > /dev/null 2>&1
2019-06-13 07:34:00 -05:00
log "Cache has been emptied"
}
helptext() {
cat << EOF
Usage: $_name [OPTION]
2019-08-13 03:35:50 -05:00
Use youtube-dl and mpc to queue up a playlist given a file of only search
2019-06-13 17:17:44 -05:00
queries. The first result found is the one that will be downloaded. Downloaded
2019-08-13 03:35:50 -05:00
files are cached in your Music folder under "PTGDP Songs" for offline use.
2019-06-13 17:17:44 -05:00
-f <file> The playlist file to load. The file should be plaintext
2019-08-08 18:32:47 -05:00
containing a YouTube search query on each line.
Comments are supported and prepended with #
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-06-14 06:16:43 -05:00
Conflicts with -p
2019-07-28 17:46:30 -05:00
-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.
2019-06-14 06:16:43 -05:00
-p Play the playlist after it is enqueued.
Conflicts with -d
2019-06-13 07:58:48 -05:00
-s Shuffle the playlist
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-06-13 07:58:48 -05:00
2019-06-13 07:34:00 -05:00
-h Print this help text
Copyright (c) 2019 rehashedsalt@cock.li
Licensed under the MIT license
EOF
}
rofimenu() {
validatedeps rofi || error "$_return is not currently installed" 1
2019-06-14 06:17:14 -05:00
[ -d "$_optrofi" ] || error "Could not open directory \"$_optrofi\"" 2
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-07-04 03:59:54 -05:00
choice="$(rofi -dmenu -i -p "$_name" <<< "$playlists" 2>/dev/null)"
2019-06-14 06:11:02 -05:00
[ -z "$choice" ] && error "User aborted at selection" 62
2019-06-14 06:16:43 -05:00
playlist "$_optrofi"/"$choice".gdp
2019-06-13 07:34:00 -05:00
return 0
}
playlist() {
[ -z "$1" ] && return 1
2019-07-04 04:07:33 -05:00
[ -e "$1" ] || error "Playlist \"$1\" does not exist" 50
[ -f "$1" ] || error "Playlist \"$1\" is not a file" 50
2019-06-13 07:34:00 -05:00
[ -r "$1" ] || error "Cannot read playlist \"$1\"" 51
2019-06-19 03:39:18 -05:00
if ! [ -f "$_musicdir/.symlink" ]; then
if ln -s "$_musicdir" "$_musiclink" > /dev/null 2>&1; then
log "Made symlink to music directory"
touch "$_musicdir/.symlink"
else
2019-07-28 17:50:23 -05:00
error "Failed to make symlink to music directory"
2019-06-19 03:39:18 -05:00
log "Music can be found at \"$_musicdir\""
fi
fi
2019-07-04 03:59:02 -05:00
local -i dlexist=0
local -i dlsuccess=0
local -i dlfailure=0
2019-06-13 07:34:00 -05:00
while read line; do
[ -z "$line" ] && continue
2019-08-04 00:04:15 -05:00
# Ignore comment lines
if ! [ "${line#\#}" = "$line" ]; then
continue
fi
2019-06-13 07:34:00 -05:00
rm "$_tmpfile"* > /dev/null 2>&1
2019-06-14 06:11:02 -05:00
sanitize "$line"
filename="$_musicdir/$_return"
2019-07-28 17:46:30 -05:00
if [ -z "$_optdryrun" ]; then
if ! [ -f "$filename" ]; 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
mv "$_tmpfile"* "$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
2019-06-13 07:50:58 -05:00
else
2019-07-28 17:46:30 -05:00
dlexist+=1
fi
2019-08-13 03:35:50 -05:00
validatedeps mpc || continue
[ -z "$_optdownloadonly" ] && mpc add "$filename"
2019-07-28 17:46:30 -05:00
if [ -n "$_optautoplay" ]; then
2019-08-13 03:35:50 -05:00
mpc play
2019-07-28 17:46:30 -05:00
unset _optautoplay
2019-06-13 07:50:58 -05:00
fi
2019-07-04 03:59:02 -05:00
else
2019-07-28 17:46:30 -05:00
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\""
2019-07-28 17:50:23 -05:00
dlerror+=1
2019-07-28 17:46:30 -05:00
continue
fi
if ! [ "$output" = "${output#*WARNING}" ]; then
log "$line parsed, but title could not be extracted"
else
log "$line - \"$output\""
fi
2019-07-28 17:50:23 -05:00
dlsuccess+=1
2019-06-14 06:11:02 -05:00
fi
2019-06-13 07:34:00 -05:00
done < <(if [ -n "$_optshuffle" ]; then shuf "$1"; else cat "$1"; fi)
2019-07-28 17:50:23 -05:00
if [ "$dlexist" = "0" ] && [ "$dlsuccess" = "0" ] && [ -z "$_optdryrun" ]; then
2019-07-04 03:59:02 -05:00
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
2019-07-28 17:50:23 -05:00
elif [ -z "$_optdryrun" ]; then
2019-07-04 03:59:02 -05:00
if [ -z "$_optdownloadonly" ]; then
notify "Finished building queue" \
"The playlist queue has been built in Audacious"
log "Finished building queue: $dlexist cached, $dlsuccess downloaded, $dlfailure failed"
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-06-13 07:58:48 -05:00
mkdir -p "$_musicdir"
2019-06-13 07:34:00 -05:00
trap trapexit EXIT
# Actual program stuff
2019-07-28 17:46:30 -05:00
while getopts ":cdDf:pr: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-13 07:34:00 -05:00
f)
_optfile="$OPTARG"
;;
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
;;
s)
_optshuffle=1
;;
h)
helptext
exit $?
;;
:)
error "Option requires argument: -$OPTARG" 2
;;
*)
error "Invalid option: -$OPTARG" 2
;;
esac
done
2019-06-14 06:22:00 -05:00
[ -n "$_optfile" ] && [ -n "$_optrofi" ] && error "Flags -f and -r conflict" 2
2019-06-14 06:16:43 -05:00
[ -n "$_optdownloadonly" ] && [ -n "$_optautoplay" ] && error "Flags -d and -p conflict" 2
2019-06-13 07:34:00 -05:00
if [ -n "$_optrofi" ]; then rofimenu; exit $?; fi
if [ -n "$_optfile" ]; then playlist "$_optfile"; exit $?; fi
error "Nothing to do" 0
}
main "$@"