yt/yt

360 lines
8.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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" "$@"