summaryrefslogtreecommitdiff
path: root/invidious-dl.sh
diff options
context:
space:
mode:
Diffstat (limited to 'invidious-dl.sh')
-rwxr-xr-xinvidious-dl.sh276
1 files changed, 276 insertions, 0 deletions
diff --git a/invidious-dl.sh b/invidious-dl.sh
new file mode 100755
index 0000000..1991d2b
--- /dev/null
+++ b/invidious-dl.sh
@@ -0,0 +1,276 @@
+#!/bin/sh
+# Copyright (c) 2021 Aleksei Kovura
+#
+# invidious-dl.sh is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# invidious-dl.sh 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with invidious-dl.sh. If not, see <https://www.gnu.org/licenses/>.
+#DBG=1
+CACHE_INST="$HOME/.cache/invidious-dl/instances"
+UAGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0"
+INSTCS_URL="https://api.invidious.io/instances.json"
+OUTDIR=/tmp
+[ -n "$INVDL_OUTD" ] && {
+ mkdir -pv $INVDL_OUTD 2>/dev/null
+ OUTDIR="${INVDL_OUTD%/}"
+}
+get_rnd_inst() {
+ instance_url=""
+ for u in $(shuf $CACHE_INST); do
+ httpc=0
+ printf "Trying instance %s\n" "$u" >&2
+ tst_url="${u}/api/v1/trending"
+ tst_url2="${u}/api/v1/videos/2l6JUNFAJ9o?fields=title,author"
+ httpc=$(curl -k -m 3 -sS -o /dev/null -Iw '%{http_code}' "$tst_url")
+ [ $? -gt 0 ] && continue
+ httpc2=$(curl -k -m 3 -sS -o /dev/null -s -Iw '%{http_code}' "$tst_url2")
+ tst=$(curl -s "$tst_url2")
+ dbg "Test json = $tst"
+ [ $httpc2 -eq 200 ] &&[ $httpc -eq 200 ] && [ "$tst" != "{}" ] && {
+ instance_url="$u"
+ break
+ }
+ done
+ [ -z "$instance_url" ] && printf "No working instance\n" && exit 1
+ dbg "Using instance = $instance_url"
+ printf $instance_url
+}
+dbg() {
+ [ -n "$DBG" ] && printf "%s\n" "$1" >&2
+}
+dbg $OUTDIR
+deps=""
+jq --version 1>/dev/null 2>&1 || deps=$deps",jq"
+ffmpeg -version 1>/dev/null 2>&1 || deps=$deps",ffmpeg"
+curl --version 1>/dev/null 2>&1 || deps=$deps",curl"
+[ -n "$deps" ] && printf "Missing dependencies: %s\n" ${deps#,} && exit 1
+mkdir -p /tmp/.invidious-dl $HOME/.cache/invidious-dl 2>/dev/null
+dl_inst_cache=""
+[ -s $CACHE_INST ] || {
+ dbg "Instances cache is absent/empty, set dl_inst_cache=1"
+ dl_inst_cache=1
+}
+[ -z "$dl_inst_cache" ] && {
+ age=$(( $(date +'%s') - $(stat -c "%Y" --cached=never $CACHE_INST) ))
+ age_days=$(( $age / 3600 / 24 ))
+ [ $age_days -gt 7 ] && {
+ dbg "Instances cache is over 7 days old, set dl_inst_cache=1"
+ dl_inst_cache=1
+ }
+}
+[ -n "$dl_inst_cache" ] && {
+ printf "Getting a fresh list of instances\n"
+ httpc=0
+ httpc=$(curl -m 5 -ss -o /dev/null -iw '%{http_code}' "$INSTCS_URL")
+ [ ! $httpc -eq 200 ] && {
+ printf "Instances API is not accessible: %s\n" "$INSTCS_URL"
+ exit 1
+ }
+ curl -sS "$INSTCS_URL" |
+ jq -rM -c '.[] | select(.[1].type=="https" and .[1].stats.software.version) |
+ .[1].uri | sub("/$"; "")' > $CACHE_INST
+}
+URL=""
+# <Options handling
+while getopts thu:f:Fl name; do
+case $name in
+ u)
+ URL="$OPTARG"
+ case "$URL" in
+ https://*watch*=*)
+ URL_TYPE="video"
+ id=${URL#*=}
+ ;;
+ https://youtu.be/*)
+ URL_TYPE="video"
+ id=${URL#https://youtu.be/}
+ ;;
+ https://www.youtube.com/embed/*)
+ URL_TYPE="video"
+ id=${URL#https://www.youtube.com/embed/}
+ ;;
+ https://*/playlist\?list=*)
+ URL_TYPE="plist"
+ id=${URL#*list=}
+ ;;
+ *)
+ printf "%s doesn't seem like invidious or youtube video url\n" "$url"
+ exit 1
+ ;;
+ esac
+ id=${id%%\&*}
+ id=${id%%\?*}
+ #$url can be:
+ #https://yewtu.be/watch?v=IwPsZOBSQTc
+ #https://invidious.tube/watch?v=IwPsZOBSQTc
+ #https://youtu.be/IwPsZOBSQTc
+ #https://www.youtube.com/watch?v=IwPsZOBSQTc
+ #https://www.youtube.com/watch?v=IwPsZOBSQTc&feature=youtu.be&t=28m4s
+ #https://www.youtube.com/playlist?list=PLsGUcSbWFgXiESsUnTwZXizWixbJvACql
+ #https://yewtu.be/playlist?list=PLBS34APvqtnGth6WHL3L-xGZ6ScJPl6Bg
+ #https://www.youtube.com/embed/CkXbyklunp4
+ ;;
+ l|F) MODE_LIST=1 ;;
+ t) MODE_TITLE=1 ;;
+ f)
+ MODE_DL=1
+ FMT_SPEC="$OPTARG"
+ case "$FMT_SPEC" in
+ [0-9]*[0-9]|[0-9]*[0-9]+[0-9]*[0-9]) ;;
+ *)
+ printf "%s format spec doesn't look legit\n" "$FMT_SPEC"
+ exit 1
+ ;;
+ esac
+ ;;
+ h|?)
+ printf "Usage: %s: [-l/-F]|[-f fmt] [-u URL]\n" ${0##*/}
+ printf " [-l/-F] : List formats\n"
+ printf " [-f] : Download specified formats\n"
+ exit 2
+ ;;
+esac
+done
+[ -z "$URL" ] && {
+ printf "Need at least -u URL and either -l/-F or -f. -h to see usage.\n"
+ exit 1
+}
+[ -n "$MODE_LIST" ] && [ -n "$MODE_DL" ] && {
+ printf "Can't use both -l/-F and -f. -h to see usage.\n"
+ exit 1
+}
+[ "$URL_TYPE" = "plist" ] && [ -z "$MODE_LIST" ] && {
+ printf "Only list mode (-l/-F) is supported for playlists.\n"
+ exit 1
+}
+[ -z "$MODE_DL" ] && [ -z "$MODE_TITLE" ] && [ -z "$MODE_LIST" ] && {
+ printf "Pass at least -l/-F or -f or -t. -h to see usage.\n"
+ exit 1
+}
+# Options handling >
+dbg "vid = $id"
+dbg "MODE_DL = $MODE_DL"
+dbg "URL_TYPE = $URL_TYPE"
+dbg "MODE_LIST = $MODE_LIST"
+DL_JSON=""
+jsonf=/tmp/.invidious-dl/invidious-dl_${id}.json
+[ -s "$jsonf" ] || {
+ dbg "JSON not cached, setting DL_JSON=1"
+ DL_JSON=1
+}
+# vids only: check google URLs expiration date in already downloaded JSON
+[ -z "$DL_JSON" ] && [ -s "$jsonf" ] && [ "$URL_TYPE" = "video" ] && {
+ dbg "found cached ${jsonf}, checking expiration"
+ exp=$(awk '{match($0, /.*expire=([0-9]+).*/, arr); print arr[1]; exit}' $jsonf)
+ [ -z "$exp" ] && {
+ printf "No expiration date in %s, unreleased video? Re-trying\n" $jsonf >&2
+ DL_JSON=1
+ }
+ cur=$(date +'%s')
+ dbg "Cur dt = ${cur}, exp dt = ${exp}"
+ [ -z $DL_JSON ] && [ $exp -le $cur ] && {
+ printf "Cached %s expired, re-downloading\n" $jsonf >&2
+ DL_JSON=1
+ }
+}
+[ -n "$DL_JSON" ] && {
+ # Pick random working instance on every run to spread load
+ instance_url=$(get_rnd_inst)
+ dbg "instance_url = $instance_url"
+ api_url=""
+ dbg "URL_TYPE = $URL_TYPE"
+ case "$URL_TYPE" in
+ video)
+ api_url="${instance_url}/api/v1/videos/"$id"?fields="
+ api_url="$api_url""title,author,adaptiveFormats,formatStreams"
+ ;;
+ plist)
+ api_url="${instance_url}/api/v1/playlists/"$id"?fields=title,author,videos"
+ ;;
+ esac
+ dbg "api_url = $api_url"
+ curl --silent --show-error "$api_url" > $jsonf
+ [ $? -eq 0 ] || exit 1
+}
+[ -n "$MODE_TITLE" ] && [ "$URL_TYPE" = "video" ] && {
+ jq -crM '.title' $jsonf
+ exit 0
+}
+[ -n "$MODE_LIST" ] && [ "$URL_TYPE" = "video" ] && {
+ jq -crM '(.formatStreams[]| ""
+ + .itag + " "
+ + "combo" + " "
+ + (.qualityLabel//"unkn") + " "
+ + ((.bitrate//0|tonumber)/1024|round|tostring) + "_kbps" + " "
+ + ((.clen//0|tonumber)/1024/1024|round|tostring) + "_Mb" + " "
+ + (.type | gsub("\""; ""))),
+ (.adaptiveFormats[]| ""
+ + .itag + " "
+ + "single-" + (if (.type|test("video.*")) then "video" else "audio" end) + " "
+ #+ "single" + .type + " "
+ + (.qualityLabel//"n/a") + " "
+ + ((.bitrate//0|tonumber)/1024|round|tostring) + "_kbps" + " "
+ + ((.clen//0|tonumber)/1024/1024|round|tostring) + "_Mb" + " "
+ + (.type | gsub("\""; "")))' $jsonf
+ exit 0
+}
+[ -n "$MODE_DL" ] && {
+ itag1=${FMT_SPEC%%+*}
+ dbg "itag1 = $itag1"
+ [ "$itag1" != "$FMT_SPEC" ] && {
+ itag2=${FMT_SPEC##*+}
+ dbg "itag2 = $itag2"
+ }
+ title=$(jq -rM '.title' $jsonf)
+ author=$(jq -rM '.author' $jsonf)
+ dbg "Output dir = $OUTDIR"
+ ofname_base="$OUTDIR"/$(printf "%s" "$title" |
+ tr -s "[:space:]" "_" |
+ tr -cd "[:alpha:][:digit:]_")"_id_${id}"
+ dbg "ofname_base = $ofname_base"
+ printf "author = %s\n" "$author"
+ printf "title = %s\n" "$title"
+ fmt1_url=$(jq -r --arg itag1 $itag1 '.formatStreams[],.adaptiveFormats[]|
+ select(.itag == $itag1)|(.type | gsub("\""; "")) +"_"+ .url' $jsonf)
+ printf "%s\n" "${fmt1_url%%_https://*}"
+ case "${fmt1_url%%_https://*}" in # e.g. "video/webm; codecs=vp9"
+ video/*) ofname=$ofname_base".mkv" ;;
+ audio*codecs=mp4a*) ofname=$ofname_base".m4a" ;;
+ audio*codecs=opus*) ofname=$ofname_base".ogg" ;;
+ esac
+ dbg "ofname = $ofname"
+ if [ -z $itag2 ]; then {
+ #I think sh doesn't have lookahead - can't (.*)https://
+ printf "Downloading combo or audio format %s to %s...\n" $itag1 $ofname
+ ffmpeg -loglevel info -user_agent "$UAGENT" -multiple_requests 1 \
+ -i "https://${fmt1_url##*_https://}" \
+ -c:v copy -c:a copy \
+ -metadata title="$title" -metadata artist="$author" \
+ "$ofname"
+ exit $?
+ }
+ else
+ fmt2_url=$(jq -r --arg itag2 $itag2 '.formatStreams[],.adaptiveFormats[]|
+ select(.itag == $itag2)|(.type | gsub("\""; "")) +"_"+ .url' $jsonf)
+ printf "Downloading 2 formats %s + %s to %s\n" $itag1 $itag2 $ofname
+ ffmpeg -loglevel info -user_agent "$UAGENT" -multiple_requests 1 \
+ -i "https://${fmt1_url##*_https://}" \
+ -i "https://${fmt2_url##*_https://}" \
+ -c:v copy -c:a copy \
+ -metadata title="$title" -metadata artist="$author" -multiple_requests 1 \
+ "$ofname"
+ exit $?
+ fi
+}
+[ "$URL_TYPE" = "plist" ] && [ -n "$MODE_LIST" ] && {
+ dbg "Printing vids in plist"
+ jq -r '.videos[]|"https://yewtu.be/watch?v="+ .videoId +" # "+ .title' $jsonf
+}