ptgdp: Move to its own repo, rewrite
This commit is contained in:
parent
2d62cfabbf
commit
109273c805
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "repo-ptgdp"]
|
||||
path = repo-ptgdp
|
||||
url = git@gitlab.com:rehashedsalt/ptgdp
|
553
ptgdp
553
ptgdp
@ -1,553 +0,0 @@
|
||||
#! /bin/bash
|
||||
#
|
||||
# ptgdp - Play the Goddamned Playlist
|
||||
# Copyright (C) 2019 Vintage Salt <rehashedsalt@cock.li>
|
||||
#
|
||||
# Distributed under terms of the MIT license.
|
||||
#
|
||||
|
||||
# TODO:
|
||||
# * Reorganize mpd integration into abstract functions
|
||||
# * Add support for other music players
|
||||
# * Abandon bash in favor of maybe Python or something
|
||||
|
||||
# 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)"
|
||||
_tmpplaylist="$_tmpdir/tmpplaylist-$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 12)"
|
||||
_xdguserdirs="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
|
||||
[ -z "$XDG_MUSIC_DIR" ] && [ -f "$_xdguserdirs" ] && source "$_xdguserdirs"
|
||||
_musicdir="${XDG_MUSIC_DIR:-$HOME/Music}"
|
||||
_ptgdpmusicdir="$_musicdir/${PTGDP_MUSIC_DIR:-PTGDP Songs}"
|
||||
declare -a _queue
|
||||
declare -a _playlists
|
||||
|
||||
# Preprocessor-accessible vars
|
||||
declare p_ytdlargs="--geo-bypass"
|
||||
declare p_search="ytsearch:"
|
||||
|
||||
# Helper functions
|
||||
log() {
|
||||
[ -z "$1" ] && return 1 # Message body
|
||||
printf "%s\\n" \
|
||||
"$1"
|
||||
}
|
||||
error() {
|
||||
[ -z "$1" ] && return 1 # Message body
|
||||
# 2: Exit code
|
||||
printf "\\e[31m%s\\e[0m\\n" \
|
||||
"$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
|
||||
}
|
||||
execpreprocessor() {
|
||||
# Execute a preprocessor line
|
||||
# $1: The line, either stripped or not of the leading #:
|
||||
line="${1#\#:}"
|
||||
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
|
||||
}
|
||||
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
|
||||
sanitize "$1"
|
||||
for file in "$_ptgdpmusicdir/$_return"*; do
|
||||
if [ -f "$file" ]; then
|
||||
_return="$file"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
unset _return
|
||||
return 1
|
||||
}
|
||||
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" \
|
||||
--default-search "$p_search" \
|
||||
--playlist-items 1 \
|
||||
"$p_ytdlargs" \
|
||||
-x \
|
||||
-o "$_tmpfile.%(ext)s" \
|
||||
"$1" \
|
||||
> /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"
|
||||
[ -f "$_tmpfile"* ] || return 1
|
||||
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 \
|
||||
--default-search "ytsearch:" \
|
||||
--get-title \
|
||||
--geo-bypass \
|
||||
--playlist-items 1 \
|
||||
"$1" 2>&1
|
||||
)"
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
execplaylists() {
|
||||
# Plays all playlists in _playlists
|
||||
if [ -n "$_optquickplay" ]; then
|
||||
for line in "${_playlists[@]}"; do
|
||||
printf "%s\\n" "$line" >> "$_tmpplaylist"
|
||||
done
|
||||
_playlists=("$_tmpplaylist")
|
||||
fi
|
||||
for list in "${_playlists[@]}"; do
|
||||
playlist "$list" || error "Failed to play playlist: \"$list\""
|
||||
done
|
||||
if [ -n "$_optautoplay" ] && [ -z "$_optdryrun" ]; then
|
||||
mpc play > /dev/null 2>&1
|
||||
log "Started playback"
|
||||
unset _optautoplay
|
||||
fi
|
||||
}
|
||||
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
|
||||
}
|
||||
validateline() {
|
||||
# $1: A line to check for comments or whitespace
|
||||
# Strictly speaking, this removes all whitespace from the line
|
||||
# While not *exactly* what I'm looking for, it's sufficient for trimming whitespace lines
|
||||
local linenows=${1//[[:space:]]}
|
||||
# Preprocessor commands
|
||||
if ! [ "${1#\#:}" = "$1" ]; then
|
||||
return 100
|
||||
fi
|
||||
# Comments and whitespace-only lines
|
||||
if ! [ "${1#\#}" = "$1" ] || [ -z "$linenows" ]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
[ -f "$_tmpplaylist" ] && rm "$_tmpplaylist" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Critical functions
|
||||
clearcache() {
|
||||
[ -n "$_ptgdpmusicdir" ] && rm "$_ptgdpmusicdir"/* > /dev/null 2>&1
|
||||
log "Cache has been emptied"
|
||||
}
|
||||
helptext() {
|
||||
cat << EOF
|
||||
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.
|
||||
|
||||
-c Clears the cache (which can become quite large)
|
||||
-d Download only; don't queue anything up
|
||||
-D Dry run; parse out all songs, downloaded or not, and
|
||||
print out the resolved names
|
||||
-p Play the playlist after it is enqueued
|
||||
-q Interpret all arguments as songs, not playlists. A
|
||||
temporary playlist will be created with all arguments
|
||||
and executed as normal
|
||||
-s Shuffle the playlist before enqueueing
|
||||
-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.
|
||||
-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
|
||||
-h Print this help text
|
||||
|
||||
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
|
||||
|
||||
Environment Variables
|
||||
|
||||
In addition to XDG-spec variables (XDG_CACHE_HOME, user-dirs.dirs, etc.),
|
||||
$_name 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
|
||||
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
|
||||
rofiplaysong "$_optrofi"
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
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
|
||||
prompt="$_name list"
|
||||
[ -n "$_optautplay" ] && prompt="$prompt ap"
|
||||
choice="$(rofi -dmenu -i -p "$prompt" <<< "$playlists" 2>/dev/null)"
|
||||
[ -z "$choice" ] && error "User aborted at selection" 62
|
||||
if [ -z "$_optrofisong" ]; then
|
||||
_playlists+=("$_optrofi"/"$choice".gdp)
|
||||
execplaylists
|
||||
else
|
||||
rofiplaysong "$_optrofi"/"$choice".gdp
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
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
|
||||
# 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
|
||||
fi
|
||||
queuesong "$choice"
|
||||
[ "$?" = "1" ] && return 62
|
||||
execqueue
|
||||
return 0
|
||||
}
|
||||
playlist() {
|
||||
validateplaylistenv "$1" || return $?
|
||||
[ -z "$_optdryrun" ] && local -a queue # An array of songs to later enqueue into mpd
|
||||
local -i dlexist=0
|
||||
local -i dlsuccess=0
|
||||
local -i dlfailure=0
|
||||
local -i maxlines=0
|
||||
while read line; do
|
||||
validateline "$line" || continue
|
||||
maxlines+=1
|
||||
done < "$1"
|
||||
[ -z "$_optquickplay" ] && log "Parsed playlist \"$1\" with $maxlines songs"
|
||||
# Store old preprocessor settings
|
||||
declare -a oldsettings
|
||||
while read var; do
|
||||
oldsettings+=("$(declare -p "$var")")
|
||||
done < <(compgen -v "p_")
|
||||
# Parse out the playlist
|
||||
while read line; do
|
||||
validateline "$line"
|
||||
case $? in
|
||||
0)
|
||||
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
|
||||
;;
|
||||
100)
|
||||
execpreprocessor "$line"
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
done < <(
|
||||
if [ -n "$_optshuffle" ]; then
|
||||
shuf "$1"
|
||||
else
|
||||
cat "$1"
|
||||
fi
|
||||
)
|
||||
# Return to normality
|
||||
for var in "${oldsettings[@]}"; do
|
||||
eval "$var"
|
||||
done
|
||||
# Dump some stats
|
||||
if [ "$dlexist" = "0" ] && [ "$dlsuccess" = "0" ] && [ -z "$_optdryrun" ]; then
|
||||
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" \
|
||||
normal dialog-error 10000
|
||||
elif [ -z "$_optdryrun" ]; then
|
||||
if ! [ "$dlfailure" = "0" ]; then
|
||||
error "Some songs failed to download" 0
|
||||
fi
|
||||
if [ -z "$_optdownloadonly" ]; then
|
||||
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"
|
||||
execqueue
|
||||
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 "$_ptgdpmusicdir"
|
||||
trap trapexit EXIT
|
||||
|
||||
# Get options
|
||||
while getopts ":cdDf:pqr:R:sh" opt; do
|
||||
case $opt in
|
||||
c)
|
||||
clearcache
|
||||
exit $?
|
||||
;;
|
||||
d)
|
||||
_optdownloadonly=1
|
||||
;;
|
||||
D)
|
||||
_optdryrun=1
|
||||
;;
|
||||
p)
|
||||
_optautoplay=1
|
||||
;;
|
||||
q)
|
||||
_optquickplay=1
|
||||
;;
|
||||
r)
|
||||
_optrofi="$OPTARG"
|
||||
;;
|
||||
R)
|
||||
_optrofi="$OPTARG"
|
||||
_optrofisong=1
|
||||
;;
|
||||
s)
|
||||
_optshuffle=1
|
||||
;;
|
||||
h)
|
||||
helptext
|
||||
exit $?
|
||||
;;
|
||||
:)
|
||||
error "Option requires argument: -$OPTARG" 2
|
||||
;;
|
||||
*)
|
||||
error "Invalid option: -$OPTARG" 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# Get program arguments (playlists)
|
||||
shift $((OPTIND - 1))
|
||||
for list in "$@"; do
|
||||
if ! [ -f "$list" ] && [ -z "$_optquickplay" ]; then
|
||||
error "Failed to find playlist: $list"
|
||||
continue
|
||||
fi
|
||||
_playlists+=("$list")
|
||||
done
|
||||
[ -z "$_optdownloadonly" ] && [ -z "$_optdryrun" ] && ! validatedeps mpc && error "$_return is required outside of dry- and download-only runs"
|
||||
[ -n "$_playlists" ] && [ -n "$_optrofi" ] && error "Flag -r cannot be used with playlist arguments" 2
|
||||
if [ -n "$_optrofi" ]; then rofimenu; exit $?; fi
|
||||
if [ -n "$_playlists" ]; then
|
||||
execplaylists
|
||||
exit $?
|
||||
else
|
||||
error "Nothing to do" 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
1
ptgdp-rofi
Symbolic link
1
ptgdp-rofi
Symbolic link
@ -0,0 +1 @@
|
||||
repo-ptgdp/ptgdp-rofi
|
1
repo-ptgdp
Submodule
1
repo-ptgdp
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 31042910057fffa93f904e09b51f39fc44ad8090
|
Loading…
Reference in New Issue
Block a user