-
-
Save andrewmackrodt/e1020095a871f97660667553db88b79c to your computer and use it in GitHub Desktop.
#!/bin/bash | |
IFS=$'\n\t' | |
set -eou pipefail | |
# configure credentials | |
BBOD_API_KEY="" # bbConfig.configData.bbVideoDataMode | |
BBOD_PROFILE="" # bbConfig.profileID | |
BBOD_USER_AGENT="" # can probably be anything | |
# check whether the content id has been specified | |
if [[ $# == 0 ]] || [[ "$1" == "" ]]; then | |
echo "ERR the first argument is required" >&2 | |
echo "Usage: ./bbod-dl <ContentGUID>" >&2 | |
exit 1 | |
fi | |
CONTENT_GUID=$1 | |
OUTPUT_FILENAME=${2:-${CONTENT_GUID}.mp4} | |
echo "Downloading $CONTENT_GUID to $OUTPUT_FILENAME" >&2 | |
if [[ -f "$OUTPUT_FILENAME" ]]; then | |
echo "ERR file exists $OUTPUT_FILENAME" >&2 | |
exit 1 | |
fi | |
fetch_media_data () { | |
curl -sS --no-keepalive --http1.1 \ | |
-o mediaData.json \ | |
-H "x-api-key: $BBOD_API_KEY" \ | |
-H "Content-Type: application/json" \ | |
-H "User-Agent: $BBOD_USER_AGENT" \ | |
"https://video-data.api.beachbodyondemand.com/v1/mediaData?platform=android&guid=$CONTENT_GUID&fields=default,thumbnail,audio,ads&profile=$BBOD_PROFILE" | |
} | |
fetch_streams () { | |
curl -sS --no-keepalive --http1.1 \ | |
-o "${CONTENT_GUID}.m3u8" \ | |
-L \ | |
-H "User-Agent: $BBOD_USER_AGENT" \ | |
"$1" | |
} | |
fetch_file () { | |
curl -sS --no-keepalive --http1.1 \ | |
-o "$2" \ | |
-L \ | |
-H "User-Agent: $BBOD_USER_AGENT" \ | |
"$1" | |
} | |
fetch_aes_key () { | |
local CONTENT_HOST=$(echo "$1" | sed -E 's#^https?://([^/]+).*#\1#') | |
local VIDEO_ID=$(echo "$1" | sed -E 's#^.+?/([^/]+)/[^/.]\.m3u8.*#\1#') | |
local PBS=$(echo "$1" | sed -E 's#^.+?&pbs=([^&]+).*#\1#') | |
curl -sS --no-keepalive --http1.1 \ | |
-o "$2.key" \ | |
-H "User-Agent: $BBOD_USER_AGENT" \ | |
"https://$CONTENT_HOST/check2?b=$VIDEO_ID&v=$VIDEO_ID&r=$2&pbs=$PBS" | |
} | |
WORKSPACE_PATH=$PWD/$CONTENT_GUID.tmp | |
[[ -d "$WORKSPACE_PATH" ]] || mkdir "$WORKSPACE_PATH" | |
pushd "$WORKSPACE_PATH" >/dev/null | |
# fetch the mediaData to extract urls.playbackStandard | |
if [[ ! -f mediaData.json ]]; then | |
echo "Fetching mediaData.json" >&2 | |
fetch_media_data | |
fi | |
streamcount=1 | |
# fetch first english caption | |
CAPTIONS_URL=$(jq -r '.captions[]|select((.language=="") or (.language=="en"))|.url' mediaData.json | sort | uniq | head -n1 || true) | |
if [[ "$CAPTIONS_URL" != "" ]]; then | |
echo "Fetching subtitle k.srt" >&2 | |
if [[ ! -f k.srt ]]; then | |
fetch_file "$CAPTIONS_URL" k.srt | |
fi | |
streamcount=$(($streamcount + 1)) | |
fi | |
# fetch streams metadata using the extracted url | |
if [[ ! -f "${CONTENT_GUID}.m3u8" ]]; then | |
echo "Fetching stream metadata ${CONTENT_GUID}.m3u8" >&2 | |
fetch_streams $(grep 'playbackStandard' mediaData.json | sed -E 's/^.+?"playbackStandard": "([^"]+)".*$/\1/') | |
fi | |
PLAYLIST_META_LINE=$(grep -nE 'RESOLUTION=1920x1080' "${CONTENT_GUID}.m3u8" | cut -f1 -d:) | |
PLAYLIST_URL=$(sed $(($PLAYLIST_META_LINE + 1))'!d' "${CONTENT_GUID}.m3u8") | |
STREAM_CODE="$(echo $PLAYLIST_URL | sed -E 's#^.+?/([^./]+)\.m3u8\b.*$#\1#')" | |
# fetch the playlist for the 1080p stream | |
if [[ ! -f k.m3u8 ]]; then | |
echo "Fetching playlist k.m3u8" >&2 | |
fetch_file "$PLAYLIST_URL" k.m3u8 | |
fi | |
# fetch the decryption key | |
if [[ ! -f "$STREAM_CODE.key" ]]; then | |
echo "Fetching aes key $STREAM_CODE.key" >&2 | |
fetch_aes_key "$PLAYLIST_URL" "$STREAM_CODE" | |
fi | |
# download each video slice | |
echo "Downloading video chunks" >&2 | |
[[ -d chunks/k ]] || mkdir -p chunks/k | |
for url in $(cat k.m3u8 | grep -E '^https?:[^?]+\.ts' | sed -E 's/\?.+//'); do | |
filename=$(basename "$url" | sed -E 's/\?.*$//') | |
if [[ ! -f "chunks/k/$filename" ]]; then | |
while [[ $(jobs -p | wc -l) -ge 8 ]]; do | |
sleep 0.1 | |
done | |
curl -sS -o "chunks/k/$filename" "$url" & | |
fi | |
done | |
# combine the ts file | |
echo "Combining video chunks -> k.ts" >&2 | |
if [[ ! -f k.ts ]]; then | |
sed -E 's/^(https?:[^?]+)\?.*/\1/' 'k.m3u8' \ | |
| sed -E 's/^(#EXT-X-KEY:.*URI)="[^"]+"/\1="'$STREAM_CODE'.key"/' \ | |
| sed -E 's/^https?:.+?\/([A-Za-z0-9]+\.ts)$/chunks\/k\/\1/' > 'k-local.m3u8' | |
ffmpeg -loglevel warning -allowed_extensions key,ts -i 'k-local.m3u8' -c copy 'k.ts' | |
fi | |
# fetch alt audio playlists | |
alttracks=$(grep -E '^#EXT-X-MEDIA:' "${CONTENT_GUID}.m3u8" | grep 'TYPE=AUDIO' | grep 'LANGUAGE="en"' | grep 'DEFAULT=NO' | grep 'URI="' || true) | |
altcount=0 | |
for alt in $alttracks; do | |
ALT_PLAYLIST_NAME=$(echo "$alt" | sed -E 's/.+NAME="([^"]+)".*/\1/') | |
ALT_PLAYLIST_URL=$(echo "$alt" | sed -E 's/.+URI="([^"]+)".*/\1/') | |
altcount=$(($altcount + 1)) | |
streamcount=$(($streamcount + 1)) | |
if [[ ! -f "a${altcount}.m3u8" ]]; then | |
echo "Fetching alt audio playlist a${altcount}.m3u8" >&2 | |
fetch_file "$ALT_PLAYLIST_URL" "a${altcount}.m3u8" | |
fi | |
done | |
# fetch alt audio decryption key | |
if [[ "$alttracks" != "" ]] && [[ ! -f a.key ]]; then | |
echo "Fetching alt audio aes key a.key" >&2 | |
fetch_aes_key "$PLAYLIST_URL" a | |
fi | |
# download alt audio chunks | |
for i in $(seq 1 $altcount); do | |
ALT_PLAYLIST_NAME=$(echo "$alttracks" | tail -n+$i | head -n1 | sed -E 's/.+NAME="([^"]+)".*/\1/') | |
echo "Downloading alt audio chunks [$i] $ALT_PLAYLIST_NAME" >&2 | |
[[ -d "chunks/a$i" ]] || mkdir -p "chunks/a$i" | |
for url in $(cat "a$i.m3u8" | grep -E '^https?:[^?]+\.aac' | sed -E 's/\?.+//'); do | |
filename=$(basename "$url" | sed -E 's/\?.*$//') | |
if [[ ! -f "chunks/a$i/$filename" ]]; then | |
while [[ $(jobs -p | wc -l) -ge 8 ]]; do | |
sleep 0.1 | |
done | |
curl -sS -o "chunks/a$i/$filename" "$url" | |
fi | |
done | |
done | |
# combine alt audio files | |
for i in $(seq 1 $altcount); do | |
if [[ -f "a$i.aac" ]]; then | |
continue | |
fi | |
sed -E 's/^(https?:[^?]+)\?.*/\1/' "a$i.m3u8" \ | |
| sed -E 's/^(#EXT-X-KEY:.*URI)="[^"]+"/\1="a.key"/' \ | |
| sed -E 's/^https?:.+?\/([A-Za-z0-9]+\.aac)$/chunks\/a'$i'\/\1/' > "a$i-local.m3u8" | |
ffmpeg -loglevel warning -allowed_extensions key,aac -i "a$i-local.m3u8" -c copy "a$i.aac" | |
done | |
popd >/dev/null | |
# create the combined mp4 file | |
echo "Creating mp4 file $OUTPUT_FILENAME" >&2 | |
command=( "ffmpeg" "-i" "${WORKSPACE_PATH}/k.ts" ) | |
for i in $(seq 1 $altcount); do | |
command=( "${command[@]}" "-i" "${WORKSPACE_PATH}/a$i.aac" ) | |
done | |
if [[ "$CAPTIONS_URL" != "" ]]; then | |
command=( "${command[@]}" "-i" "${WORKSPACE_PATH}/k.srt" ) | |
fi | |
for i in $(seq 0 $altcount); do | |
command=( "${command[@]}" "-map" "$i" ) | |
command=( "${command[@]}" "-metadata:s:a:$i" "language=eng" ) | |
done | |
if [[ "$CAPTIONS_URL" != "" ]]; then | |
command=( "${command[@]}" "-map" "$(($streamcount - 1))" ) | |
command=( "${command[@]}" "-metadata:s:s:0" "language=eng" ) | |
command=( "${command[@]}" "-metadata:s:s:0" "handler=English" ) | |
fi | |
DEFAULT_TRACK_NAME=$(grep -E '^#EXT-X-MEDIA:' "${WORKSPACE_PATH}/${CONTENT_GUID}.m3u8" | grep 'TYPE=AUDIO' | grep 'DEFAULT=YES' | grep -v 'URI="' | sed -E 's/.+NAME="([^"]+)".*/\1/' || true) | |
if [[ "$DEFAULT_TRACK_NAME" != "" ]]; then | |
command=( "${command[@]}" "-metadata:s:a:0" "handler=$DEFAULT_TRACK_NAME" ) | |
fi | |
for i in $(seq 1 $altcount); do | |
ALT_PLAYLIST_NAME=$(echo "$alttracks" | tail -n+$i | head -n1 | sed -E 's/.+NAME="([^"]+)".*/\1/') | |
command=( "${command[@]}" "-metadata:s:a:$i" "handler=$ALT_PLAYLIST_NAME" ) | |
done | |
command=( "${command[@]}" "-c" "copy" ) | |
if [[ "$CAPTIONS_URL" != "" ]]; then | |
command=( "${command[@]}" "-c:s" "mov_text" ) | |
fi | |
command=( "${command[@]}" "$OUTPUT_FILENAME" ) | |
"${command[@]}" | |
# perform clean up | |
echo "Removing working directory" >&2 | |
rm -rf "$WORKSPACE_PATH/" |
I too want to download some videos before my sub runs out. I don't have the slightest clue where to begin. What program do I need to run this code?
Does this code work?
I'm about 50% to a generic solution. Your api-key, the current bearer token, and transaction_id(not sure if this matters) can be found via the developer tools. I can automate the full flow if I can figure out the oauth flow to get the bearer token.
for x in {04..31}; do
curl -s "https://cocoa-prod.prod.cd.beachbodyondemand.com/v1.0/playback/CWCW00${x}" \
-X 'OPTIONS' \
-H 'authority: cocoa-prod.prod.cd.beachbodyondemand.com' \
-H 'accept: */*' \
-H 'accept-language: en-US,en;q=0.9' \
-H 'access-control-request-headers: accept-language,authorization,bodweb,platform,platform-version,x-api-key,x-transaction-id' \
-H 'access-control-request-method: GET' \
-H 'dnt: 1' \
-H 'origin: https://www.beachbodyondemand.com' \
-H 'referer: https://www.beachbodyondemand.com/' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-site' \
-H 'sec-gpc: 1' \
-H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' \
--compressed
meta=$(curl -s "https://cocoa-prod.prod.cd.beachbodyondemand.com/v1.0/playback/CWCW00${x}" \
-H 'authority: cocoa-prod.prod.cd.beachbodyondemand.com' \
-H 'accept: */*' \
-H 'accept-language: en_us' \
-H 'authorization: Bearer $bearer' \
-H 'bodweb: true' \
-H 'dnt: 1' \
-H 'origin: https://www.beachbodyondemand.com' \
-H 'platform: webvideoplayer' \
-H 'platform-version: v2.47.0-RC2' \
-H 'referer: https://www.beachbodyondemand.com/' \
-H 'sec-ch-ua: "Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "Linux"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-site' \
-H 'sec-gpc: 1' \
-H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' \
-H 'x-api-key: $api-key' \
-H 'x-transaction-id: $tid' \
--compressed)
guid=$(echo $meta | jq -r ".metadata.guid")
title=$(echo $meta | jq -r ".metadata.title")
playbackStandard=$(echo $meta| jq -r ".urls.playbackStandard")
echo $title - $guid - $playbackStandard
test ! -f "$title.mp4" && youtube-dl -o "$title.mp4" $playbackStandard \
--add-header 'authority: d197pzlrcwv1zr.cloudfront.net' \
--add-header 'referer: https://www.beachbodyondemand.com/' \
--user-agent 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
done
I'm about 50% to a generic solution. Your api-key, the current bearer token, and transaction_id(not sure if this matters) can be found via the developer tools. I can automate the full flow if I can figure out the oauth flow to get the bearer token.
for x in {04..31}; do curl -s "https://cocoa-prod.prod.cd.beachbodyondemand.com/v1.0/playback/CWCW00${x}" \ -X 'OPTIONS' \ -H 'authority: cocoa-prod.prod.cd.beachbodyondemand.com' \ -H 'accept: */*' \ -H 'accept-language: en-US,en;q=0.9' \ -H 'access-control-request-headers: accept-language,authorization,bodweb,platform,platform-version,x-api-key,x-transaction-id' \ -H 'access-control-request-method: GET' \ -H 'dnt: 1' \ -H 'origin: https://www.beachbodyondemand.com' \ -H 'referer: https://www.beachbodyondemand.com/' \ -H 'sec-fetch-dest: empty' \ -H 'sec-fetch-mode: cors' \ -H 'sec-fetch-site: same-site' \ -H 'sec-gpc: 1' \ -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' \ --compressed meta=$(curl -s "https://cocoa-prod.prod.cd.beachbodyondemand.com/v1.0/playback/CWCW00${x}" \ -H 'authority: cocoa-prod.prod.cd.beachbodyondemand.com' \ -H 'accept: */*' \ -H 'accept-language: en_us' \ -H 'authorization: Bearer $bearer' \ -H 'bodweb: true' \ -H 'dnt: 1' \ -H 'origin: https://www.beachbodyondemand.com' \ -H 'platform: webvideoplayer' \ -H 'platform-version: v2.47.0-RC2' \ -H 'referer: https://www.beachbodyondemand.com/' \ -H 'sec-ch-ua: "Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "Linux"' \ -H 'sec-fetch-dest: empty' \ -H 'sec-fetch-mode: cors' \ -H 'sec-fetch-site: same-site' \ -H 'sec-gpc: 1' \ -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' \ -H 'x-api-key: $api-key' \ -H 'x-transaction-id: $tid' \ --compressed) guid=$(echo $meta | jq -r ".metadata.guid") title=$(echo $meta | jq -r ".metadata.title") playbackStandard=$(echo $meta| jq -r ".urls.playbackStandard") echo $title - $guid - $playbackStandard test ! -f "$title.mp4" && youtube-dl -o "$title.mp4" $playbackStandard \ --add-header 'authority: d197pzlrcwv1zr.cloudfront.net' \ --add-header 'referer: https://www.beachbodyondemand.com/' \ --user-agent 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' done
Looks interesting. I managed a very manual workaround. I would be interested in working this through with you. If you had telegram or discord for a quick message or two?
BBOD_PROFILE="" # bbConfig.profileID
What is the ID, the username, Login email ...?