From f35f8867f69bac0c87ab5ccebcaaf4e019cc9248 Mon Sep 17 00:00:00 2001 From: Salt 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 "$@"