#!/bin/sh # Don't link mrsh to sh, since it does not allow line continuations in here-documents. # Don't use tcsh, it doesn't allow multiple lines in braced or parenthesized blocks. # Use dash, yash, bash, posh, ksh, mksh, zsh, pbosh # Run entire script in subshell to prevent variable pollution ( # https://unix.stackexchange.com/a/188365 # If and only if bash is in POSIX-mode, which can be forced by setting the # POSIXLY_CORRECT variable (to any value), then the special built-ins, see # https://www.gnu.org/software/bash/manual/html_node/Special-Builtins.html, # are found BEFORE functions during lookup. `unset' is one of these special # built-ins. However, when using `shopt -s expand_aliases', alias expansion in # non-interactive shells is supported, so aliasing unset is a problem. # This can be circumvented by escaping any of the letters of the command, usually the first. # # The COMMANDS_ variable contains the commands, space delimited (therefore the IFS), # which are supposed to be "made safe". IFS=" " POSIXLY_CORRECT='1' COMMANDS_='builtin unalias unset read printf command exit type . tr mkdir wc sed grep xargs ffmpeg jq wget' # shellcheck disable=SC2086 \unset -f -- ${COMMANDS_-} 2>/dev/null # shellcheck disable=SC2086 \unalias -- ${COMMANDS_-} 2>/dev/null || true command -v -- wget >/dev/null 2>&1 || { printf '\033[31m%s\033[m\n' "Please install wget" exit 1 } command -v -- jq >/dev/null 2>&1 || { printf '\033[31m%s\033[m\n' "Please install jq" exit 1 } command -v -- ffmpeg >/dev/null 2>&1 || { printf '\033[31m%s\033[m\n' "Please install ffmpeg" exit 1 } # .env contains a line like # BASE64_BEARER_TOKEN='' # Retrieve that base64 token from a request from the browser # network tab network tabfrom a logged in MS Stream (Classic) tab . './.env' OUTDIR='./videos' read -r API_URL <<- EOM https://euwe-1.api.microsoftstream.com/api/videos?\ \$top=4\ &\ \$skip=0\ &\ \$orderby=metrics/trendingScore asc\ &\ \$filter=\ published \ and \ (\ (\ state eq 'Completed' and contentSource ne 'livestream'\ ) \ or \ (\ contentSource eq 'livestream' \ and not \ (\ state eq 'Completed' \ and not \ liveEvent/LiveEventOptions/OnDemandOptions/EnablePlayback\ )\ )\ )\ &\ api-version=1.4-private EOM API_URL="$( printf '%s' "${API_URL-}" | tr -d -- '\t' )" mkdir -p -- "${OUTDIR-}" || { printf '\033[31m%s\033[m\n' 'Aborting' exit 2 } # '>' without quotes is delimiter, since it is one of the few characters disallowed in a URL. Title must not include this character # https://www.ietf.org/rfc/rfc2396.txt # 2.4.3. Excluded US-ASCII Characters m3u8_manifest_urls_and_metadata="$( wget --no-verbose \ --quiet \ --output-document - \ --header="authorization: Bearer ${BASE64_BEARER_TOKEN-}" \ -- "${API_URL-}" | jq --raw-output \ '.value[] | .name + ">" + .media.duration[2:] + ">" + (.playbackUrls | map(select(.mimeType == "application/vnd.apple.mpegurl")) | .[].playbackUrl)' )" [ -z "${m3u8_manifest_urls_and_metadata-}" ] && { printf '\033[31m%s\033[m\n' 'Error GETting manifest urls (response empty) or Bearer token invalid/expired. Exiting.' exit 3 } idx='1' total="$(printf '%s\n' "${m3u8_manifest_urls_and_metadata-}" | wc -l)" IFS=' ' for m3u8_manifest_url_and_metadata in ${m3u8_manifest_urls_and_metadata-} do IFS='>' read -r title length m3u8_manifest_url <<- EOM ${m3u8_manifest_url_and_metadata-} EOM length="$(printf '%s' "${length-}" | sed -e 's/\..*S/S/g' -e 's/M/:/g' -e 's/H/:/g')" filepath="${OUTDIR-}/${title-}.mp4" printf '\033[32m%s\033[m\n' "Download '${filepath-}' (${idx-}/${total-})" ffmpeg -hide_banner \ -loglevel error \ -headers "authorization: Bearer ${BASE64_BEARER_TOKEN-}" \ -i "${m3u8_manifest_url-}" \ -progress - \ -c copy \ "${filepath-}" | grep --line-buffered 'out_time=' | sed -u -e 's/out_time=//g' -e 's/\..*//g' -e 's/^-//g' | xargs -I{} printf '%s\r' "{} of ${length-} " printf '\n\033[33m%s\033[m\n' "Done" idx="$(( idx + 1 ))" done unset -v POSIXLY_CORRECT \ idx \ total \ API_URL \ filepath \ m3u8_manifest_url \ m3u8_highest_res_url \ m3u8_manifest_url_and_metadata \ m3u8_manifest_urls_and_metadata \ OUTDIR \ BASE64_BEARER_TOKEN IFS=" " )