#!/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 . #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="" # 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 }