|
#!/usr/bin/env bash |
|
|
|
########## Zeroteams ########## _ |
|
## Download videos from Teams. ## _______ ____ ___ | |_ ___ __ _ _ __ ___ ___ |
|
# Author: Midblyte (Mario A.) # |_ / _ \ ___/ _ \| __/ _ \/ _` | '_ ` _ \/ __| |
|
# License: MIT License # / / __/ | | (_) | || __/ (_| | | | | | \__ \ |
|
## Last-update: 2025-12-08 ## /___\___|_| \___/ \__\___|\__,_|_| |_| |_|___/ |
|
############################### |
|
|
|
set -e -u -o pipefail |
|
|
|
VERSION=0.1.0-beta4 |
|
USER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' |
|
|
|
usage() { |
|
# <required argument: type = default value> |
|
# [optional argument: type = default value] |
|
|
|
cat << __EOF__ |
|
Usage: $(basename -- "${1:-zeroteams}") [options...] |
|
|
|
--help View this page |
|
(alias: -h). |
|
|
|
--version Print current version, $VERSION |
|
(alias: -v). |
|
|
|
download Download a recording |
|
(aliases: d, dl). |
|
|
|
--url <URL> Video URL to download |
|
(alias: -u). |
|
|
|
[--file <FILENAME = auto>] Target output file |
|
(alias: -f). |
|
|
|
--from-clipboard Retrieve the video URL to download |
|
from the clipboard (alias: -c). |
|
|
|
[--no-check] Skip the check phase after |
|
downloading. |
|
|
|
--segments Write raw segments, do not |
|
transcode (alias: -s) |
|
|
|
check Check the frames of a recording |
|
to verify its integrity |
|
(alias: c). |
|
|
|
--file <URL> Target input file |
|
(alias: -f). |
|
__EOF__ |
|
} |
|
|
|
requirements() { |
|
# Keep track of missing dependencies |
|
declare -a dependencies=() |
|
|
|
# Let's thank all the people behind these projects for their work. |
|
# All the following dependencies are required for this software to run. |
|
# Additional conditional dependencies: loginctl, awk, xclip, wl-paste. |
|
# Optional development dependencies: shellcheck. |
|
# Runtime dependencies: |
|
for command in basename cat ffmpeg ffprobe getopt grep mv sed; do |
|
if ! command -v "$command" > /dev/null; then |
|
dependencies+=(\ "$command") |
|
fi |
|
done |
|
|
|
if ! test ${#dependencies[@]} -eq 0; then |
|
missing=$(IFS=, ; echo "${dependencies[*]}") |
|
missing="${missing:1}" |
|
fi |
|
} |
|
|
|
filter_by_subcommand() { |
|
# It works for this use case (extracting a subsection of the help page), although it took a few attempts to come up with it. |
|
grep --color=never --only-matching --perl-regexp --null-data --regexp '^.+?\n\n|(?<=\n) '"$1"'(?:.|\n)+?(?:(?=\n\n ?\S)\n|$)' |
|
} |
|
|
|
transform_url() { |
|
# As it stands, it appears to be robust enough to not require a full fledged parsing of the URL query parameters. |
|
test $# -eq 1 && sed --sandbox -E 's/([?]?)[&]?(enableCdn=1|pretranscode=0|hybridPlayback=false)/\1/g' <<< "$1" |
|
} |
|
|
|
download() { |
|
local executable="$1" |
|
local file |
|
local segments=false |
|
shift |
|
|
|
local options |
|
options=$(getopt -l "help,url:,from-clipboard,file:,segments,no-check" -o "hu:cf:s" -- "$@") |
|
|
|
eval set -- "$options" |
|
|
|
while true; do |
|
case "$1" in |
|
-h|--help) |
|
usage "$executable" | filter_by_subcommand download |
|
|
|
return 0 |
|
;; |
|
-u|--url) |
|
shift |
|
url="$(transform_url "$1")" |
|
;; |
|
-c|--from-clipboard) |
|
# The following code operates on a best-effort basis, it might be further improved. |
|
|
|
if ! command -v loginctl > /dev/null; then |
|
echo "Warning: loginctl is unavailable, can't identify the display manager in use." |
|
shift |
|
continue |
|
elif ! command -v awk > /dev/null; then |
|
echo "Warning: awk is unavailable, can't identify the display manager in use." |
|
shift |
|
continue |
|
fi |
|
|
|
local display_manager |
|
# shellcheck disable=SC2046 |
|
display_manager="$(loginctl show-session $(awk '/tty/ {print $1}' <(loginctl)) -p Type | awk -F= 'NR==1 {print $2}')" |
|
|
|
if test "$display_manager" = x11; then |
|
if command -v xclip > /dev/null; then |
|
pasted="$(xclip -selection clipboard -o)" |
|
else |
|
echo "Warning: xclip is unavailable." |
|
shift |
|
continue |
|
fi |
|
elif test "$display_manager" = wayland; then |
|
if command -v wl-paste > /dev/null; then |
|
pasted="$(wl-paste)" |
|
else |
|
echo "Warning: wl-paste is unavailable." |
|
shift |
|
continue |
|
fi |
|
else |
|
echo "Warning: couldn't recognise display manager." |
|
shift |
|
continue |
|
fi |
|
|
|
if test -v pasted; then |
|
url="$(transform_url "${pasted}")" |
|
unset pasted |
|
else |
|
echo "Warning: couldn't paste from clipboard." |
|
fi |
|
|
|
;; |
|
-f|--file) |
|
shift |
|
file="$1" |
|
;; |
|
-s|--segments) |
|
segments=true |
|
;; |
|
--no-check) |
|
# shellcheck disable=SC2034 |
|
no_check=true |
|
;; |
|
--) |
|
shift |
|
break;; |
|
esac |
|
shift |
|
done |
|
|
|
if ! test -v url; then |
|
echo "$executable"': missing --url parameter.' |
|
return 1 |
|
elif ! test -v file; then |
|
file="video-${EPOCHSECONDS}-${RANDOM}.mp4" |
|
fi |
|
|
|
_download "$url" "$file" "$segments" |
|
|
|
local rc=$? |
|
if test $rc -ne 0; then |
|
return $rc |
|
elif test -v no_check; then |
|
return 0 |
|
fi |
|
|
|
_check "$file" "$segments" |
|
|
|
return $? |
|
} |
|
|
|
_download() { |
|
test $# -eq 3 || return 1 |
|
|
|
local url="$1" |
|
local file="$2" |
|
local segments="$3" |
|
|
|
local ffmpeg_arguments=( |
|
# display stuff |
|
-hide_banner |
|
-loglevel level+info |
|
|
|
# not interactive |
|
-y |
|
|
|
# abort decoding on minor error detection |
|
-err_detect explode |
|
|
|
# custom user_agent, it should help a bit more with the spoofing |
|
-user_agent "$USER_AGENT" |
|
|
|
# should the streaming stop for 30s, ffmpeg will detect it and save the video file before it exits |
|
-rw_timeout 30000000 |
|
|
|
# generic timeout |
|
-timeout 30 |
|
|
|
# input |
|
-i "$url" |
|
|
|
# disable re-encoding, copy the streams as they are |
|
-c copy |
|
|
|
# although not strictly needed, make sure to copy any other stream, too |
|
-map 0 |
|
) |
|
|
|
if test "$segments" = true; then |
|
ffmpeg_arguments+=(-f segment "${file%.mp4}_%05d.mp4") |
|
else |
|
if test -e "${file%.mp4}.mp4"; then |
|
mv --backup=numbered "${file%.mp4}.mp4" "${file%.mp4}.mp4.bak" |
|
fi |
|
|
|
ffmpeg_arguments+=("${file%.mp4}.mp4") |
|
fi |
|
|
|
ffmpeg "${ffmpeg_arguments[@]}" |
|
} |
|
|
|
check() { |
|
local executable="$1" |
|
shift |
|
|
|
local options |
|
options="$(getopt -l "help,file:" -o "hf:" -- "$@")" |
|
|
|
eval set -- "$options" |
|
|
|
while true; do |
|
case "$1" in |
|
-h|--help) |
|
usage "$executable" | filter_by_subcommand check |
|
|
|
return 0 |
|
;; |
|
-f|--file) |
|
shift |
|
file="$1" |
|
;; |
|
--) |
|
shift |
|
break;; |
|
esac |
|
shift |
|
done |
|
|
|
if ! test -v file; then |
|
echo "$executable"': missing --file parameter.' |
|
return 1 |
|
fi |
|
|
|
_check "$file" "" |
|
|
|
return $? |
|
} |
|
|
|
_check() { |
|
test $# -eq 1 -o $# -eq 2 || return 1 |
|
|
|
local file="$1" |
|
local segments="${2:-}" |
|
|
|
if test "$segments" = true; then |
|
echo "Video check is disabled for the segments downloading mode." |
|
elif ffprobe -v error -- "$file" 2>/dev/null; then |
|
echo "Video check: passing test, everything seems fine." |
|
else |
|
echo "Video check: fail - consider re-downloading the video again." |
|
fi |
|
} |
|
|
|
main() { |
|
local executable="$0" |
|
|
|
requirements |
|
|
|
if test -v missing; then |
|
echo "Missing dependencies: $missing" |
|
return 1 |
|
elif test $# -eq 0; then |
|
usage "$executable" |
|
return 1 |
|
fi |
|
|
|
case "$1" in |
|
d|dl|download) |
|
shift |
|
download "$executable" "$@" |
|
return $? |
|
;; |
|
c|check) |
|
shift |
|
check "$executable" "$@" |
|
return $? |
|
;; |
|
esac |
|
|
|
# Fast-fail for clearly wrong syntax. |
|
if test "${1::1}" != -; then |
|
usage "$executable" |
|
return 1 |
|
fi |
|
|
|
local options |
|
options=$(getopt -l "help,version" -o "hv" -- "$@") |
|
|
|
eval set -- "$options" |
|
|
|
while true; do |
|
case "$1" in |
|
-h|--help) |
|
usage "$executable" |
|
return 0 |
|
;; |
|
-v|--version) |
|
echo -n "$VERSION" |
|
return 0 |
|
;; |
|
--) |
|
shift |
|
break;; |
|
esac |
|
shift |
|
done |
|
} |
|
|
|
main "$@" |