#! /bin/bash # # ptgdp - Play the Goddamned Playlist # Copyright (C) 2019 Vintage Salt # # Distributed under terms of the MIT license. # # TODO: # * Provide integration with other players, like mpd # * Add a quick way to append to a playlist # 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_DATA_HOME:-$HOME/.local/share}/$_name" _musiclink="${XDG_MUSIC_DIR:-$HOME/Music}/PTGDP Songs" # Helper functions log() { [ -z "$1" ] && return 1 # Message body printf "%s: %s\\n" \ "$_name" \ "$1" } error() { [ -z "$1" ] && return 1 # Message body [ -z "$2" ] && return 2 # Exit code log "$1" 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 } 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 audacious 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 "JPTGDP Songs" for offline use. -f The playlist file to load. The file should be plaintext containing a YouTube search query on each line -c Clears the cache (which can become quite large) -d Download only; don't queue anything up Conflicts with -p -p Play the playlist after it is enqueued. Conflicts with -d -s Shuffle the playlist -r 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 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 ! [ -f "$_musicdir/.symlink" ]; then if ln -s "$_musicdir" "$_musiclink" > /dev/null 2>&1; then log "Made symlink to music directory" touch "$_musicdir/.symlink" else log "Failed to make symlink to music directory" log "Music can be found at \"$_musicdir\"" fi fi local -i dlexist=0 local -i dlsuccess=0 local -i dlfailure=0 while read line; do [ -z "$line" ] && continue rm "$_tmpfile"* > /dev/null 2>&1 sanitize "$line" filename="$_musicdir/$_return" 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 log "Could not download song \"$line\"" continue fi else dlexist+=1 fi [ -z "$_optdownloadonly" ] && audacious -e "$filename" if [ -n "$_optautoplay" ]; then audacious -p unset _optautoplay fi done < <(if [ -n "$_optshuffle" ]; then shuf "$1"; else cat "$1"; fi) if [ "$dlexist" = "0" ] && [ "$dlsuccess" = "0" ]; 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 else 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 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 ":cdf:pr:sh" opt; do case $opt in c) clearcache exit $? ;; d) _optdownloadonly=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 "$@"