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