Skip to content

Instantly share code, notes, and snippets.

@Midblyte
Last active December 30, 2025 20:17
Show Gist options
  • Select an option

  • Save Midblyte/e33ffbe7b10577cb7ed85d68d85f8ec6 to your computer and use it in GitHub Desktop.

Select an option

Save Midblyte/e33ffbe7b10577cb7ed85d68d85f8ec6 to your computer and use it in GitHub Desktop.
Download Teams recordings using Bash

zeroteams

zeroteams is a command-line tool to download video recordings from Microsoft Teams.

Usage

# It's as easy as that (keep the URL in the clipboard):
zeroteams download --from-clipboard
# Specifying the file name:
zeroteams download --from-clipboard --file "filename.mp4"
# Specifying the url in the command
zeroteams download --url "https://northeurope1-mediap.svc.ms/[... omitted for brevity ...]"
# The absolute shortest form (equivalent to download --from-clipboard)
zeroteams d -c

For the full help page, please specify --help.

Getting the video URL

Please note that the URL you need is not the one in the browser address bar.

To get it, do the following steps (beware, exact wording may vary depending on the browser):

  • Navigate to the video page hosted on Microsoft Sharepoint (or go to sharepoint.com, login and find it);
  • Open the browser's dev tools (try F12 or Ctrl + Shift + C);
  • Click on the Network tab;
  • Type videomanifest where it says "Filter URLs";
  • Refresh the page (or press F5).

When the page reloads, pick the first result and copy the file URL (right click, then "Copy URL"). That's it. Now you can use zeroteams to download the video.

Install & Uninstall

This script is supposed to work on Linux systems only (currently). Windows (via WSL2/Cygwin) and MacOS might be supported, but require additional tweaks.

Installation

# You can change the installation directory to the one you want (check $PATH)
install <(curl -fsSLo - https://gist.github.com/Midblyte/e33ffbe7b10577cb7ed85d68d85f8ec6/raw) ~/.local/bin/zeroteams

Uninstallation

command -v zeroteams | xargs -i rm -vi {}
#!/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 "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment