diff options
Diffstat (limited to 'invidious-dl.sh')
-rwxr-xr-x | invidious-dl.sh | 276 |
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 +} |