Files
subs/subs

299 lines
9.9 KiB
Bash
Executable File

#!/bin/sh
# █████
# ▒▒███
# █████ █████ ████ ▒███████ █████
# ███▒▒ ▒▒███ ▒███ ▒███▒▒███ ███▒▒
# ▒▒█████ ▒███ ▒███ ▒███ ▒███▒▒█████
# ▒▒▒▒███ ▒███ ▒███ ▒███ ▒███ ▒▒▒▒███
# ██████ ▒▒████████ ████████ ██████
# ▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒
#
# Watch your youtube subscriptions without a youtube account
# via curl, dmenu, mpv and basic unix commands.
#
# The $SUBS_FILE is a text file containing usernames or channel IDs
# comments and blank lines are ignored.
#
# For more information and examples, see:
# http://github.com/mitchweaver/subs
#
# >> note: this is highly experimental / janky, it can and will break <<
#
# -/-/-/-/- Settings -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
: "${SUBS_FILE:=~/files/subs.txt}"
: "${SUBS_MENU_PROG:=dmenu -l 18 -p Subs:}"
: "${SUBS:=${XDG_CACHE_HOME:-~/.cache}/subs}"
: "${SUBS_LINKS:=$SUBS/links}"
: "${SUBS_CACHE:=$SUBS/cache}"
: "${SUBS_SLEEP_VALUE:=1}" # raise this if you experience problems
: "${SUBS_DAEMON_INTERVAL:=600}"
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SEP=^^^^^ # shouldn't need to change this
# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
export LC_ALL=C # this speeds things up a bit but can cause
# issues for titles in foreign languages
# -/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/
die() {
>&2 printf '%s\n' "$*"
exit 1
}
usage() {
die 'Usage: subs [-m no-video] [-g gen-links] [-u update-cache] [-d daemonize] [-n dont-play]'
}
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
# Synopsis: $SUBS_FILE [txt] -> $SUBS_LINKS [xml links]
#
# Updates local cache of xml subscription links from the
# subscription file containing either usernames or channel ids.
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
gen_links() {
:>"$SUBS_LINKS"
count=0
total=$(sed -e '/^$/d' -e '/^#/d' <"$SUBS_FILE" | wc -l)
total=${total##* }
while read -r line ; do
# ignore comments and blank lines
case $line in ''|' '|'#'*) continue ;; esac
# strip off in-line comments and any trailing whitespace
line=${line%%#*}
line=${line%% *}
count=$(( count + 1 ))
case $line in
UC*)
# YT channel IDs always begin with 'UC' and are 24 chars long
if [ ${#line} -eq 24 ] ; then
printf "[%s/%s] using channel ID '%s' for xml link\n" "$count" "$total" "$line"
printf 'https://youtube.com/feeds/videos.xml?%s\n' \
"channel_id=$line" >>"$SUBS_LINKS"
else
>&2 printf 'Error: cannot determine channel for %s\n' "$line"
fi
;;
*)
# otherwise we are given a username, we must find out its channel ID
printf "Fetching channel ID for %s..." "$line"
data=$(curl -sL --retry 10 "https://youtube.com/user/$line/about")
if printf '%s\n' "$data" | grep '404 Not Found' >/dev/null ; then
>&2 printf '\n[ERROR]: Could not determine channel for %s... 404\n' "$line"
>&2 printf '[%s] %s\n' "$(date)" "$line" >> "$SUBS"/ERRORS.log
return 1
fi
printf '%s\n' "$data" | \
while read -r line ; do
case $line in
*channel/UC??????????????????????*)
printf ' Found!\n'
line=${line##*channel/}
line=${line%%\"*}
printf "[%s/%s] using channel ID '%s' for xml link\n" "$count" "$total" "$line"
printf 'https://youtube.com/feeds/videos.xml?channel_id=%s\n' \
"$line" >>"$SUBS_LINKS"
break
esac
done
esac
done <"$SUBS_FILE"
}
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
# Synopsis: $1 [LINK] -> $SUBS_CACHE/$chan_name/concat [CHANNEL INFO]
#
# Takes a channel rss feed link and creates a file
# with a line of its videos dates, titles, and urls.
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
get_vids() {
data=$(curl -sL --retry 15 "$1")
if printf '%s\n' "$data" | grep '404 Not Found' >/dev/null ; then
>&2 printf '[ERROR]: Could not get vids for %s... 404\n' "$1"
>&2 printf '[%s] %s\n' "$(date)" "$1" >> "$SUBS"/ERRORS.log
return 1
fi
# hide the first <published> tag which is the channel
# creation date
data=${data#*\<\/published\>}
# trim off outer <name> tags
chan=${data%%</name*}
chan=${chan##*name>}
printf "%s\n" "$data" | \
while read -r line ; do
case $line in
*'link rel='*)
line=${line#*href=\"}
line=${line%\"/\>}
line=https://${line#*www.}
url=$line
;;
*'<published>'*)
line=${line%+00:*}
line=${line#*<published>}
date=$line
;;
*'<media:title>'*)
line=${line%</*}
line=${line#*:title>}
title=$line
printf '%s\n' \
"${date}${SEP}${chan}${SEP}${title}${SEP}${url}" \
>>"$SUBS_CACHE/$chan"
esac
done
}
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
# Updates the local cache of subscriptions. ([-u] flag)
# shellcheck disable=2086
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
update_subs() {
[ -f "$SUBS_LINKS" ] || die 'Subs links have not been generated.'
rm -r "${SUBS_CACHE:-?}" 2>/dev/null ||:
mkdir -p "$SUBS_CACHE"
total=$(wc -l <"$SUBS_LINKS")
total=${total##* }
count=0
while read -r link ; do
count=$(( count + 1 ))
printf 'starting job [%s/%s] for %s\n' "$count" "$total" "$link"
get_vids "$link" &
sleep "${SUBS_SLEEP_VALUE:-0}"
done <"$SUBS_LINKS"
count=0
max_retries=$total
while [ "$count" -ne "$total" ] && [ "$max_retries" -ne "$total" ] ; do
count=$(printf '%s\n' "$SUBS_CACHE"/* | wc -l)
count=${count##* }
printf "[%s/%s] waiting for fetch jobs to complete...\n" "$count" "$total"
sleep 1
max_retries=$(( max_retries + 1 ))
done
printf '%s\n\n' 'done!'
}
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
# Grab current cache of subscriptions, sort by date uploaded
# -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
cat_subs() {
sort -r "$SUBS_CACHE"/* | \
while read -r line ; do
chan=${line#*$SEP}
chan=${chan%%$SEP*}
title=${line#*$chan$SEP}
title=${title%%$SEP*}
date=${line%%$SEP*}
date=${date#*-}
date=${date%T*}
printf '[%s %s] %s\n' "$date" "$chan" "$title"
done
}
# Split the concatenated lines into entities, send to menu program.
# Finally, play the result with mpv.
get_sel() {
if [ -d "$SUBS_CACHE" ] ; then
# Pipe your subs feed into your desired menu and replace HTML escape codes (&quot; -> ")
sel=$(cat_subs | sed -e 's/&quot;/"/g' -e 's/&amp;/\&/g' -e 's/&lt;/</g' -e 's/&gt;/>/g' | $SUBS_MENU_PROG)
else
die 'Subs cache has not been retrieved.'
fi
[ "$sel" ] || die Interrupted
oldchan="${sel#* }"
chan=$(printf '%s' "${oldchan%%] *}" | sed -e 's/\&/&amp;/g' -e 's/"/\&quot;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' )
title=$(printf '%s' "${sel#*"${oldchan%%] *}"\] }" | sed -e 's/\&/&amp;/g' -e 's/"/\&quot;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' )
while read -r line ; do
case $line in
*"$SEP$title$SEP"*)
url=${line##*$SEP}
if [ "$url" ] ; then
if [ "$DONT_PLAY" ] ; then
# just print the url
# (useful for piping to other programs)
printf '%s\n' "$url"
else
printf 'playing: %s\n' "$url"
# shellcheck disable=2086
exec mpv $MPV_OPTS "$url"
fi
fi
break
esac
done <"$SUBS_CACHE/$chan"
}
daemonize() {
# create a cached copy of the subs file to check for changes
# if changes occur, re-generate links automatically
daemon_file=${XDG_CACHE_HOME:-~/.cache}/subs_daemon.cache
if [ ! -f "$daemon_file" ] ; then
cp -f "${SUBS_FILE:=~/files/subs.txt}" "$daemon_file"
gen_links
fi
while true ; do
if ! cmp "${SUBS_FILE:=~/files/subs.txt}" "$daemon_file" ; then
gen_links
cp -f "${SUBS_FILE:=~/files/subs.txt}" "$daemon_file"
fi
update_subs
printf 'Sleeping for %s seconds...\n' "$SUBS_DAEMON_INTERVAL"
sleep "$SUBS_DAEMON_INTERVAL"
done
}
main() {
mkdir -p "$SUBS"
if [ "$1" ] ; then
case ${1#-} in
g)
gen_links
;;
u)
update_subs
;;
c)
cat_subs
;;
d)
daemonize
;;
m)
MPV_OPTS="$MPV_OPTS --no-video" \
get_sel
;;
n)
DONT_PLAY=true \
get_sel
;;
*)
usage
esac
else
get_sel
fi
}
main "$@"