360 lines
8.3 KiB
Bash
Executable File
360 lines
8.3 KiB
Bash
Executable File
#!/bin/sh
|
||
|
||
# Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
|
||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify it under
|
||
# the terms of the GNU Affero General Public License as published by the Free
|
||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||
# later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||
# details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License along
|
||
# with this program. If not, see https://www.gnu.org/licenses/.
|
||
|
||
set -e
|
||
|
||
test -z "$DEBUG" || set -x
|
||
|
||
test -z "$XDG_CACHE_HOME" && cachefile="$HOME/.cache/yt.cache" \
|
||
|| cachefile="$XDG_CACHE_HOME/yt.cache"
|
||
|
||
test -n "$YT_PL_DIR" \
|
||
|| test -n "$XDG_DATA_HOME" && YT_PL_DIR="$XDG_DATA_HOME/yt" \
|
||
|| YT_PL_DIR="$HOME/.local/share/yt"
|
||
|
||
test -d "$YT_PL_DIR" \
|
||
|| mkdir -p "$YT_PL_DIR"
|
||
|
||
if test -z "$YTPICK"; then
|
||
printf "%s: Please set \$YTPICK to your preferred picking tool." \
|
||
"$argv0" 1>&2
|
||
exit 78 # sysexits.h(3) EX_CONFIG
|
||
fi
|
||
|
||
if test -z "$PLAYER"; then
|
||
printf "%s: Please set \$PLAYER to your preferred video player." \
|
||
"$argv0" 1>&2
|
||
exit 78 # sysexits.h(3) EX_CONFIG
|
||
fi
|
||
|
||
# formatted $YT_PL_DIR for use with sed
|
||
P="$(printf '%s\n' "$YT_PL_DIR" | sed 's;\/;\\/;g')"
|
||
|
||
argv0="$0"
|
||
com="$1"
|
||
u='add | archive | clone | list | localsearch | new | pick | play | search | sync | verify'
|
||
FMT='%(title)s – %(channel)s (%(duration>%H:%M:%S)s) [%(webpage_url)s]'
|
||
WBAPI='https://archive.org/wayback/available?url='
|
||
|
||
|
||
add() { # adds a video to a playlist file
|
||
test -z "$2" || test -n "$3" && usage 'add uri playlist'
|
||
|
||
while test -f "$YT_PL_DIR/$2.m3u"; do
|
||
file="$YT_PL_DIR/$2.m3u"
|
||
video="$(printf '%s\n' "$1" \
|
||
| sed -e 's/youtu\.be\//www.youtube\.com\/watch?v=/g' \
|
||
-e 's/\?[^v].*$//g')"
|
||
|
||
if test -n "$(grep -e "$video" "$file")"
|
||
then
|
||
printf "%s: %s: %s: Video already in playlist.\n" \
|
||
"$argv0" "$file" "$video"
|
||
exit
|
||
else
|
||
cache "$video" &
|
||
archive "$video" &
|
||
printf "%s\n" "$video" >> "$file"
|
||
fi
|
||
|
||
shift
|
||
done
|
||
}
|
||
|
||
archive() { # archives a video to the Wayback Machine
|
||
test -z "$1" && usage 'archive uri...'
|
||
|
||
while test -n "$1"; do
|
||
if test -n "$(printf '%s\n' "$1" | grep -e 'web\.archive\.org')"
|
||
then
|
||
shift
|
||
continue
|
||
else
|
||
wayback_url="$(curl -s "$WBAPI$1" \
|
||
| jq .archived_snapshots.closest.url \
|
||
| tr -d '"' | sed 's/^http:/https:/g')"
|
||
|
||
# skips archiving on the Wayback Machine if the video has a snapshot
|
||
if [ "$wayback_url" != "null" ]; then shift; continue; fi
|
||
|
||
printf "%s: %s: Saving video to the Wayback Machine.\n" "$argv0" "$1" 1>&2
|
||
curl -s -d "url=$1" 'https://web.archive.org/{url}' >/dev/null
|
||
fi
|
||
shift
|
||
done
|
||
}
|
||
|
||
cache() { # cache the video title for faster retrieval
|
||
test -z "$1" && usage 'cache video...'
|
||
|
||
test -e "$cachefile" || touch "$cachefile"
|
||
|
||
while test -n "$1"; do
|
||
grep "$1" "$cachefile" 2>/dev/null 1>&2 || printf '%s\n' \
|
||
"$(yt-dlp -s "$1" --print "$FMT")" >>"$cachefile"
|
||
shift
|
||
done
|
||
}
|
||
|
||
clone() { # clones a YouTube playlist to a file
|
||
test -z "$1" && usage 'clone uri [playlist]'
|
||
|
||
n=1
|
||
|
||
while test -z "$title"; do
|
||
title="$(yt-dlp -s -I "$n" --print '%(playlist)s' "$1" 2>/dev/null || true)"
|
||
n="$((n + 1))"
|
||
done
|
||
|
||
creator="$(yt-dlp -s -I "$n" --print '%(playlist_uploader)s' "$1")"
|
||
filename="$(printf '%s\n' "$title" | sed 's/ /_/g')"
|
||
|
||
file="$YT_PL_DIR/$filename.m3u"
|
||
|
||
videos="$(yt-dlp --flat-playlist "$1" --print url)"
|
||
prefix="$(printf "#EXTM3U\n#EXTART:%s\n#PLAYLIST:%s [%s]\n" \
|
||
"$creator" "$title" "$1")"
|
||
|
||
printf "%s: %s: Saving playlist to %s.\n" "$argv0" "$title" "$file" 1>&2
|
||
printf '%s\n\n%s\n' "$prefix" "$videos" >"$file"
|
||
|
||
verify "$filename"
|
||
}
|
||
|
||
lines() {
|
||
while test -n "$1"; do
|
||
sed 's/#.*$//g' "$1"
|
||
shift
|
||
done
|
||
}
|
||
|
||
list() {
|
||
test -n "$1" && usage 'list'
|
||
choices="$(menu)"
|
||
test -z "$choices" || pick "$choices"
|
||
}
|
||
|
||
localsearch() {
|
||
test -n "$1" && usage 'localsearch'
|
||
$YTPICK <"$cachefile"
|
||
}
|
||
|
||
menu() {
|
||
playlist="$(ls "$YT_PL_DIR" | sed 's/\.m3u//g' | sed -n '/[^.old]/p' \
|
||
| $YTPICK)"
|
||
|
||
while test -d "$YT_PL_DIR/$playlist"; do
|
||
dir="$playlist"
|
||
playlist="$(ls "$YT_PL_DIR/$playlist" | sed 's/\.m3u//g' | $YTPICK \
|
||
|| printf '')"
|
||
done
|
||
|
||
printf '%s/%s\n' "$dir" "$playlist"
|
||
}
|
||
|
||
new() {
|
||
test -z "$1" && usage 'new playlist...'
|
||
|
||
file="$YT_PL_DIR/$1.m3u"
|
||
|
||
if test -f "$file"; then
|
||
printf "%s: %s: File exists." "$argv0" "$file"
|
||
elif test -d "$file"; then
|
||
printf "%s: %s: Is a directory." "$argv0" "$file"
|
||
else
|
||
touch "$file"
|
||
fi
|
||
}
|
||
|
||
pick() { # Pick a video to play from a playlist of videos
|
||
test -z "$1" && usage 'pick playlist...'
|
||
|
||
if test -f "$YT_PL_DIR/$1.m3u"; then
|
||
file="$YT_PL_DIR/$1.m3u"
|
||
|
||
for line in $(lines "$file"); do
|
||
if test -n "$(printf '%s\n' "$line" | sed -n '/^\#/p')"
|
||
then
|
||
continue
|
||
else
|
||
cache "$line" &
|
||
fi
|
||
|
||
if test -z "$list"; then
|
||
list="$(grep "$line" "$cachefile")"
|
||
else
|
||
list="$(printf '%s\n%s' "$list" "$(grep "$line" "$cachefile" | uniq)")"
|
||
fi
|
||
done
|
||
|
||
chosen="$(printf '%s\n' "$list" \
|
||
| $YTPICK | sed -e 's/.*\[//g' -e 's/\]//g')"
|
||
|
||
test -z "$chosen" \
|
||
|| printf "%s: %s: Playing stream.\n" "$argv0" "$chosen" 1>&2 \
|
||
&& play "$chosen"
|
||
fi
|
||
}
|
||
|
||
play() { # play a video after caching its title
|
||
test -z "$1" && usage 'play uri...'
|
||
|
||
cache "$@" &
|
||
"$PLAYER" "$@"
|
||
}
|
||
|
||
queue() {
|
||
test -z "$1" && usage 'queue playlist...'
|
||
|
||
while test -f "$YT_PL_DIR/$1.m3u"; do
|
||
"$PLAYER" "$YT_PL_DIR/$1.m3u"
|
||
shift
|
||
done
|
||
}
|
||
|
||
search() {
|
||
test -z "$1" && usage 'search term [count]'
|
||
|
||
results="$(yt-dlp "ytsearch$2:$1" --print "$FMT")"
|
||
|
||
cache "$(printf '%s\n' "$results" | sed -e 's/.*\[//g' -e 's/\]/ /g' \
|
||
| tr -d '\n')" &
|
||
|
||
selection="$(printf '%s\n' "$results" \
|
||
| $YTPICK \
|
||
| sed 's/.*\[//g' \
|
||
| tr -d ']')"
|
||
|
||
if test -n "$selection"; then
|
||
option="$(printf 'copy\nmusic\nplay\nsave\n' | $YTPICK)"
|
||
|
||
case "$option" in
|
||
"copy")
|
||
wl-copy "$selection"
|
||
;;
|
||
"music")
|
||
music "$selection"
|
||
;;
|
||
"play")
|
||
play "$selection"
|
||
;;
|
||
"save")
|
||
add "$selection" "$(menu)"
|
||
;;
|
||
esac
|
||
fi
|
||
}
|
||
|
||
sync() {
|
||
if test -z "$1"
|
||
then
|
||
set -- "$YT_PL_DIR"/*.m3u
|
||
else
|
||
set -- $(printf "$@" | sed "s/.* /$P\/&\.m3u /g")
|
||
fi
|
||
|
||
while test -n "$1"; do
|
||
file="$1"
|
||
|
||
if test -f "$file"; then
|
||
URL="$(sed -n 's/^\#PLAYLIST:.* \[//p' <"$file" | tr -d ']')"
|
||
|
||
if test -n "$URL"; then
|
||
clone "$URL" && mv "$file" "$file.old"
|
||
fi
|
||
fi
|
||
|
||
shift
|
||
done
|
||
}
|
||
|
||
usage() {
|
||
printf "Usage: %s %s\n" "$argv0" "$@" 1>&2
|
||
exit 64 # sysexits.h(3) EX_USAGE
|
||
}
|
||
|
||
verify() { # replaces videos with archived versions if they are not available
|
||
test -z "$1" && usage 'verify playlist...'
|
||
|
||
set -- $(printf '%s\n' "$@" | sed "s/[^ ]*/$P\/&.m3u /g")
|
||
|
||
while test -n "$1"; do
|
||
if ! test -f "$1"; then
|
||
printf '%s: %s: No such playlist.\n' \ "$argv0" "$1" 1>&2
|
||
exit 69 # syexits.h(3) EX_UNAVAILABLE
|
||
fi
|
||
|
||
printf "%s: %s: Verifying playlist.\n" "$argv0" "$1" 1>&2
|
||
|
||
for video in $(lines "$1"); do
|
||
if test -n "$(yt-dlp -s "$video" 2>&1 \
|
||
| grep -i -e 'video unavailable' -e 'private video' -e 'been removed')"
|
||
then
|
||
printf "%s: %s: Video unavailable.\n" "$argv0" "$video" 1>&2
|
||
|
||
wayback_url="$(curl -s "$WBAPI$video" \
|
||
| jq -r .archived_snapshots.closest.url \
|
||
| sed 's/^http:/https:/g')"
|
||
|
||
if [ "$wayback_url" = "null" ] \
|
||
|| test -n "$(yt-dlp -s "$wayback_url" 2>&1 | grep -e 'not archived')"
|
||
then
|
||
printf "%s: %s: Video not available on the Wayback Machine.\n" \
|
||
"$argv0" "$video" 1>&2
|
||
# replace the video link with a comment containing link
|
||
content="$(sed "s;^$video;\# $video;g" "$1")"
|
||
|
||
# write the new content buffer to file
|
||
printf '%s\n' "$content" > "$1"
|
||
else
|
||
printf "%s: %s: Replacing link with Wayback link.\n" \
|
||
"$argv0" "$video" 1>&2
|
||
|
||
content="$(sed "s;^$video;$wayback_url;g" "$1")"
|
||
printf '%s\n' "$content" >"$1"
|
||
cache "$wayback_url" &
|
||
fi
|
||
else
|
||
printf '%s: %s: Video available.\n' "$argv0" "$video" 1>&2
|
||
cache "$video" &
|
||
archive "$video" &
|
||
fi
|
||
done
|
||
|
||
shift
|
||
done
|
||
}
|
||
|
||
for dep in \
|
||
curl \
|
||
jq \
|
||
yt-dlp
|
||
do
|
||
if ! command -v "$dep" >/dev/null
|
||
then
|
||
printf "%s: %s: Missing dependency.\n" "$0" "$dep" 1>&2
|
||
exit 69 # syexits.h(3) EX_UNAVAILABLE
|
||
fi
|
||
done
|
||
|
||
test -n "$com" && shift || usage "$u"
|
||
|
||
printf '%s\n' "$u" | grep "$com" >/dev/null || usage "$u"
|
||
|
||
"$com" "$@"
|