Skip to content

Instantly share code, notes, and snippets.

@marco79cgn
Last active June 4, 2025 10:49
Show Gist options
  • Save marco79cgn/b09e26beaaf466cb04f9d74122866048 to your computer and use it in GitHub Desktop.
Save marco79cgn/b09e26beaaf466cb04f9d74122866048 to your computer and use it in GitHub Desktop.
Das Skript lädt entweder einen einzelnen Film, Staffeln einer Serie oder alle Tatort Folgen einer Stadt von ARD Plus
#!/bin/bash
curlBin=$(which curl)
# use snap curl version if your OS is outdated
#curlBin=/snap/bin/curl
FILE=ard-plus-token
# parse input parameter
ardPlusUrl=$1
username=$2
password=$3
skip=$4
movieId=''
token=''
showPath=$(echo $ardPlusUrl | rev | cut -d "/" -f1 | rev)
showId=$(echo $showPath | cut -d "-" -f1)
if [[ -z "$username" || -z "$password" ]]
then
echo "Credentials missing! Please start the script with 3 parameters: "
echo "./ard-plus-dl <ard-plus-url> <username> <password>"
exit 1
fi
if [[ -z "$skip" ]]
then
skip=1
fi
# login only if necessary
login() {
token=$("$curlBin" -is 'https://auth.ardplus.de/auth/login?plainRedirect=true&redirectURL=https%3A%2F%2Fwww.ardplus.de%2Flogin%2Fcallback&errorRedirectURL=https%3A%2F%2Fwww.ardplus.de%2Fanmeldung%3Ferror%3Dtrue' \
-H 'authority: auth.ardplus.de' \
-H 'content-type: application/x-www-form-urlencoded' \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' \
--data-raw "username=${username}&password=${password}" | grep -i authorization | awk '{print $3}')
tokenType=$(echo $token | cut -f1 -d "." | base64 -d | jq -r '.typ')
if [[ "$tokenType" == "JWT" ]]; then
echo $token > $FILE
else
echo "Login not possible! Please check credentials and subscription for user $username."
exit 1
fi
}
# cleanup after each episode and at the end
cleanup() {
deleteToken=$("$curlBin" -s 'https://token.ardplus.de/token/session/playback/delete' \
-H 'authority: token.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' \
--data-raw "{\"contentId\":\"$movieId\",\"contentType\":\"CmsMovie\"}" \
--compressed)
}
# get authorization for content
auth() {
auth=$("$curlBin" -s 'https://token.ardplus.de/token/session' \
-H 'authority: token.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' \
--data-raw "{\"contentId\":\"$movieId\",\"contentType\":\"CmsEpisode\",\"download\":false,\"appInfo\":{\"platform\":\"web\",\"appVersion\":\"1.0.0\",\"build\":\"web\",\"bundleIdentifier\":\"web\"},\"deviceInfo\":{\"isTouchDevice\":false,\"isTablet\":false,\"isFireOS\":false,\"appPlatform\":\"web\",\"isIOS\":false,\"isCastReceiver\":false,\"isSafari\":false,\"isFirefox\":false}}" \
--compressed)
urlParam=$(echo ${auth} | jq -r '.authorizationParams')
echo "$urlParam"
}
# intercept CTRL+C click to clean up before exit
term() {
echo "CTRL+C pressed. Cleanup and exit!"
cleanup
exit 0
}
trap term SIGINT
# perform login
if [ -f "$FILE" ]; then
# Using cached token
token=$(<$FILE)
else
# Log in once
login $username $password
fi
# check if token is valid
movieId="a0S010000007GcX"
urlParam=$( auth )
if [[ "$urlParam" == null ]]; then
login $username $password
token=$(<$FILE)
if [[ -z "$token" ]]; then
echo "Login not possible! Please check credentials and subscription for user $username."
exit 0
fi
fi
cleanup
# get requested content
contentUrl="https://data.ardplus.de/ard/graphql?extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%2240d7cbfb79e6675c80aae2d44da2a7f74e4a4ee913b5c31b37cf9522fa64d63b%22%7D%7D&variables=%7B%22movieId%22%3A%22$showId%22%2C%22externalId%22%3A%22%22%2C%22slug%22%3A%22%22%2C%22potentialMovieId%22%3A%22%22%7D"
seasonsStatus=$("$curlBin" -s -o content-result.txt -w "%{http_code}" "${contentUrl}" \
-H 'authority: data.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
if [[ $seasonsStatus != "200" ]]; then
#retry once
echo "Couldn't get season details. Trying again!"
sleep 2
seasonsStatus=$("$curlBin" -s -o content-result.txt -w "%{http_code}" "${contentUrl}" \
-H 'authority: data.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
contentResult=$(cat content-result.txt)
else
contentResult=$(cat content-result.txt)
fi
# check whether content is movie or series
movie=$(echo "$contentResult" | jq '.data.movie')
tvshow=$(echo "$contentResult" | jq '.data.series')
if [[ "$movie" != null ]]; then
movieId=$(echo "$movie" | jq -r '.id')
name=$(echo "$movie" | jq -r '.title')
videoUrl=$(echo "$movie" | jq -r '.videoSource.dashUrl')
year=$(echo "$movie" | jq -r '.productionYear')
filename="${name} (${year})"
urlParam=$( auth )
downloadUrl=${videoUrl}?${urlParam}
echo "Lade Film ${filename}..."
yt-dlp --quiet --progress --no-warnings --audio-multistreams -f "bv+mergeall[vcodec=none]" --sub-langs "en.*,de.*" --embed-subs --merge-output-format mp4 ${downloadUrl} -o "$filename"
cleanup
elif [[ "$tvshow" != null ]]; then
requestedShow=$(echo "$contentResult" | jq '.data.series.title')
seasonIds=$(echo "$contentResult" | jq '[.data.series.seasons.nodes[] | { season: .seasonInSeries, seasonId: .id, title: .title }]')
seasonOutput=$(echo "$seasonIds" | jq '[.[] | { Option: .season, Titel: .title }]' | jq -r '(.[0]|keys_unsorted|(.,map(length*"-"))),.[]|map(.)|@tsv'|column -ts $'\t')
echo -e "\nGewünschte Serie: $requestedShow\n"
echo -e "$seasonOutput\n"
echo -n "Welche Staffel möchtest du runterladen? "
read -r selectedSeason
selectedSeasonId=$(echo "$seasonIds" | jq -r --argjson index 1 ".[$((selectedSeason - 1))].seasonId")
seasonData=$("$curlBin" -s "https://data.ardplus.de/ard/graphql?extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22134d75e1e68a9599d1cdccf790839d9d71d2e7d7dca57d96f95285fcfd02b2ae%22%7D%7D&variables=%7B%22seasonId%22%3A%22$selectedSeasonId%22%7D&operationName=EpisodesInSeasonData" \
-H 'authority: data.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
episodes=$(echo $seasonData | jq '[.data.episodes.nodes[] | { id: .id, episodeNo: .episodeInSeason, title: .title, videoUrl: .videoSource.dashUrl }]')
amount=$(echo $episodes | jq '. | length')
echo -e "\nStaffel $selectedSeason hat $amount Folgen."
selectedSeasonFormatted=$(printf '%02d\n' "$selectedSeason")
if [[ $skip != "1" ]]; then
echo "Überspringe $skip Episode(n)."
skip=$((skip + 1))
fi
# loop over all episodes and download each
while read episode
do
movieId=$(echo "$episode" | jq -r '.id')
name=$(echo "$episode" | jq -r '.title')
videoUrl=$(echo "$episode" | jq -r '.videoUrl')
episode=$(echo "$episode" | jq -r '.episodeNo')
filename="S${selectedSeasonFormatted}E$(printf '%02d\n' $episode) - ${name}"
urlParam=$( auth )
downloadUrl=${videoUrl}?${urlParam}
echo "Lade ${filename}..."
yt-dlp --quiet --progress --no-warnings --audio-multistreams -f "bv+mergeall[vcodec=none]" --sub-langs "en.*,de.*" --embed-subs --merge-output-format mp4 ${downloadUrl} -o "$filename"
cleanup
done < <(echo "$episodes" | sed 's/\\"//g' | jq -c '.[]' | tail -n +$skip)
elif [[ "$ardPlusUrl" == *"tatort"* ]]; then
tatortCity=$(echo $showPath | cut -d "-" -f2)
# get all episodes per city
tatortCityEpisodes=$("$curlBin" -s "https://data.ardplus.de/ard/graphql?operationName=CategoryDataBySlug&variables=%7B%22slug%22%3A%22tatort-$tatortCity%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%221bf6e600fa91aa72bba35ee95a53677cf21b994a4c2afbcd01127259c7e88612%22%7D%7D" \
--header 'authority: data.ardplus.de' \
--header 'content-type: application/json' \
--header "cookie: sid=$token" \
--header 'origin: https://www.ardplus.de' \
--header 'referer: https://www.ardplus.de/' \
--header 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
episodeIds=$(echo "$tatortCityEpisodes" | jq '[.data.category.content.nodes[] | { id: .contentMovie.id }]')
amount=$(echo $episodeIds | jq '. | length')
cityCapitalized=$(echo ${tatortCity} | awk '{$1=toupper(substr($1,0,1))substr($1,2)}1')
echo "Der Tatort ${cityCapitalized} hat $amount Episoden."
echo -n "Wie viele Episoden möchtest du überspringen? (0=alle laden) "
read -r skip
echo "Überspringe $skip Episode(n)."
skip=$((skip + 1))
# loop over all episodes and download each
while read episode
do
movieId=$(echo "$episode" | jq -r '.id')
episodeUrl="https://data.ardplus.de/ard/graphql?operationName=MovieDetails&variables=%7B%22movieId%22%3A%22$movieId%22%2C%22externalId%22%3A%22%22%2C%22slug%22%3A%22%22%2C%22potentialMovieId%22%3A%22%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%226a791c24fd9716b154a3d68f9b5213eb0bf25828a5e633cdbbd2e35aa5b9a984%22%7D%7D"
episodeDetailsStatus=$("$curlBin" -s -o current-tatort-episode.txt -w "%{http_code}" "${episodeUrl}" \
-H 'authority: data.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
if [[ $episodeDetailsStatus != "200" ]]; then
#retry once
echo "Couldn't get episode details. Trying again!"
sleep 2
episodeDetailsStatus=$("$curlBin" -s -o current-tatort-episode.txt -w "%{http_code}" $episodeUrl \
-H 'authority: data.ardplus.de' \
-H 'content-type: application/json' \
-H "cookie: sid=$token" \
-H 'origin: https://www.ardplus.de' \
-H 'referer: https://www.ardplus.de/' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' \
--compressed)
episodeDetails=$(cat current-tatort-episode.txt)
else
episodeDetails=$(cat current-tatort-episode.txt)
fi
name=$(echo "$episodeDetails" | jq -r '.data.movie.title')
videoUrl=$(echo "$episodeDetails" | jq -r '.data.movie.videoSource.dashUrl')
year=$(echo "$episodeDetails" | jq -r '.data.movie.productionYear')
customData=$(echo "$episodeDetails" | jq -r '.data.movie.customData')
episode=$(echo "$customData" | jq -r '.episodeProductionNumber')
team=$(echo "$customData" | jq -r '.team')
city=$(echo "$customData" | jq -r '.location')
filename="Tatort ${city}"
if [[ -n "$team" ]];
then
filename="$filename (${team})"
fi
if [[ "$episode" != null ]];
then
filename="$filename - Folge ${episode}"
fi
filename="$filename - ${name} (${year})"
urlParam=$( auth )
downloadUrl=${videoUrl}?${urlParam}
echo "Lade ${filename}..."
yt-dlp --quiet --progress --no-warnings --audio-multistreams -f "bv+mergeall[vcodec=none]" --sub-langs "en.*,de.*" --embed-subs --merge-output-format mp4 ${downloadUrl} -o "$filename"
cleanup
sleep 1
done < <(echo "$episodeIds" | jq -c '.[]' | tail -n +$skip )
else
echo "invalid content"
fi
cleanup
@marco79cgn
Copy link
Author

marco79cgn commented Nov 29, 2023

Bildschirmfoto 2023-12-29 um 17 25 27

Anforderungen

  • Shell/Bash (z.B. macOS Terminal)
  • jq
  • yt-dlp
  • Gnu-Tools: curl, grep, awk, echo, cut, sed, base64
  • ARD Plus Mitgliedschaft (14 Tage kostenlos)

Benutzung

Skript downloaden und ausführbar machen: chmod 755 ard-plus-dl.sh

Anschließend das Skript aufrufen und drei Parameter mitgeben:
./ard-plus-dl.sh <url> <username> <password>

Die URL ist die Übersichtsseite eines Films oder einer Serie bei ARD Plus, zum Beispiel
Gegen den Wind (Serie):
https://www.ardplus.de/details/a0T0100000064DB-gegen-den-wind
Lola rennt (Film):
https://www.ardplus.de/details/a0S01000000EWYi-lola-rennt

Das Skript erkennt automatisch, ob es sich um einen Film oder eine Serie handelt. Filme werden unmittelbar geladen. Im Falle einer Serie werden alle gefundenen Staffeln aufgelistet und zur Auswahl angeboten.

Filme und Serien werden automatisch mit mehreren Tonspuren geladen (z.B. deutsch & englisch), sofern verfügbar. Auch die Untertitel werden berücksichtigt.

Es können zusätzlich zu Filmen und Serien auch ganze Tatort Ausgaben pro Stadt geladen werden, z.B. alle Folgen aus Bremen mit der URL:
https://www.ardplus.de/kategorie/tatort-bremen

Die Zieldateien werden sinnvoll benannt, z.B.:
S01E01 - Schönes Wochenende.mp4
oder
Lola rennt (1998).mp4

@gh01mojmnet
Copy link

gh01mojmnet commented Feb 29, 2024

Hallo!
Hast du eine Idee, ob sich in der ARDplus Mediathek etwas geändert hat?
Ich bekomme oft 'invalid content' für Filme und Serien werden auch nicht runtergeladen...

folgendes Beispiel - dl.sh:

~/git/ard-plus-dl/ard-plus-dl.sh https://www.ardplus.de/details/a0S01000000zF11-halleluja-amigo <user> <password>
~/git/ard-plus-dl/ard-plus-dl.sh https://www.ardplus.de/details/a0T01000000s7sX-don-matteo <user> <password>
~/git/ard-plus-dl/ard-plus-dl.sh https://www.ardplus.de/details/a0S010000015IyU-alfred-die-knallerbse <user> <password>
~/git/ard-plus-dl/ard-plus-dl.sh https://www.ardplus.de/details/a0S010000015IyY-der-zerstreute <user> <password>
~/git/ard-plus-dl/ard-plus-dl.sh https://www.ardplus.de/details/a0S010000015Iyf-der-sanfte-mit-den-schnellen-beinen <user> <password>
~/git/ard-plus-dl/ard-plus-dl.sh https://www.ardplus.de/details/a0S010000015RYQ-ein-tolpatsch-auf-abwegen <user> <password>

ergibt folgenden Output:

> ./dl.sh
invalid content

Gewünschte Serie: "Don Matteo"

Option  Titel
------  -----
1       Don Matteo - Staffel 1
2       Don Matteo - Staffel 2

Welche Staffel möchtest du runterladen? 2
jq: error (at <stdin>:1): Cannot iterate over null (null)

Staffel 2 hat  Folgen.
Lade Film Alfred, die Knallerbse (1972)...
invalid content
invalid content
Lade Film Ein Tolpatsch auf Abwegen (1978)...
Lade Film Eine Wolke zwischen den Zähnen (1974)...

Vielen Dank für das ansonsten tolle Script!

@marco79cgn
Copy link
Author

marco79cgn commented Oct 24, 2024

Update 24.10.2024: fixed API calls

@gh01mojmnet
Sorry, hatte dein Feedback komplett übersehen. Das Skript sollte jetzt wieder einwandfrei funktionieren, auch mit deinen Beispielen.

@marco79cgn
Copy link
Author

marco79cgn commented Jan 8, 2025

Update 08.01.2025: multi-language support (automatic)

Das Skript lädt automatisch mehrere Sprachen und Untertitel (falls verfügbar).

@ipo0x
Copy link

ipo0x commented May 24, 2025

Was mache ich falsch, wenn mir das Skript z. B. die Serie „Sherlock“ (https://www.ardplus.de/details/a0T01000002PRMu-sherlock) nur in englischer Sprachausgabe und ohne Untertitel herunterlädt? Aufgerufen wie oben beschrieben, mit den Zugangsdaten, und anschließend die Staffel bzw. Option ausgewählt. VLC media player gibt mir das Heruntergeladene englisch wieder, und hat augenscheinlich keine zweite Tonspur und auch keine Untertitel zur Auswahl. Auf ardplus.de lassen sich aber Audio-Ausgabe und Untertitel einstellen.

@ipo0x
Copy link

ipo0x commented May 24, 2025

Bei folgendem Inhalt ist mir außerdem eine Ungereimtheit (Option bzw. Staffel 5, doch nur mit Eingabe 1 läuft das Skript ordnungsgemäß – ist hier die Zeilennummer relevant?) sowie eine Fehlermeldung aufgefallen:

~/Downloads/ard-plus-dl.sh https://www.ardplus.de/details/a0T010000001io5-es-war-einmal <user> <password>

Gewünschte Serie: "Es war einmal..."

Option  Titel
------  -----
5       Es war einmal... Forscher und Erfinder

Welche Staffel möchtest du runterladen? 1

Staffel 1 hat 26 Folgen.
Lade S01E01 - Die Chinesen – die ersten Erfinder...
Lade S01E02 - Archimedes – Griechenland im Auftrieb...
Lade S01E03 - Ptolemäus und Heron – Zwei Genies aus Alexandria...
Lade S01E04 - Die Zeitmessung – Der weite Weg zur Quarzuhr...
Lade S01E05 - Heinrich der Seefahrer – Auf der Suche nach dem Ende der Welt...
Lade S01E06 - Gutenberg – Die Erfindung der Druckkunst...
Lade S01E07 - Leonardo da Vinci – Das Multitalent aus Italien...
Lade S01E08 - Die Medizin – die Väter der Heilkunst...
jq: parse error: Invalid numeric literal at line 1, column 64
jq: parse error: Invalid numeric literal at line 1, column 64
jq: parse error: Invalid numeric literal at line 1, column 64
jq: parse error: Invalid numeric literal at line 1, column 64
Lade S01E00 - ...
ERROR: [generic] '?' is not a valid URL. Set --default-search "ytsearch" (or run  yt-dlp "ytsearch:?" ) to search YouTube
Lade S01E10 - Newton – Das Gesetz der Schwerkraft...
Lade S01E11 - Buffon – Die Geschichte der Natur...
Lade S01E12 - Lavoisier – Die Wunderwelt der Chemie...
Lade S01E13 - Stephenson – Mit Volldampf voraus...
Lade S01E14 - Faraday – Die Entdeckung des Elektromagnetismus...
Lade S01E15 - Darwin – Die Entstehung der Arten...
Lade S01E16 - Mendel – Die Regeln der Vererbung...
Lade S01E17 - Pasteur – Die Welt der Mikroorganismen...
Lade S01E18 - Edison – Ein Licht geht auf...
Lade S01E19 - Marconi – Ein neues Medium wird geboren...
Lade S01E20 - Ford – Automobile vom laufenden Band...
Lade S01E21 - Lindbergh – Der Traum vom Fliegen...
Lade S01E22 - Madame Curie – Das Geheimnis der Radioaktivität...
Lade S01E23 - Einstein – Alles ist relativ...
Lade S01E24 - Lorenz – Der Vater der Gänse...
Lade S01E25 - Armstrong – Der erste Mensch auf dem Mond...
Lade S01E26 - Die Zukunft – Der Aufbruch ins 21. Jahrhundert...

Ich habe den Aufruf zweimal wiederholt, mit gleichem Ergebnis. Die „S01E09“ lässt sich nicht herunterladen, obwohl auf ardplus.de enthalten & anschaubar.

Dein Skript funktioniert ansonsten sehr gut! Vielen Dank dafür!

@marco79cgn
Copy link
Author

marco79cgn commented May 24, 2025

Was mache ich falsch, wenn mir das Skript z. B. die Serie „Sherlock“ (https://www.ardplus.de/details/a0T01000002PRMu-sherlock) nur in englischer Sprachausgabe und ohne Untertitel herunterlädt? Aufgerufen wie oben beschrieben, mit den Zugangsdaten, und anschließend die Staffel bzw. Option ausgewählt. VLC media player gibt mir das Heruntergeladene englisch wieder, und hat augenscheinlich keine zweite Tonspur und auch keine Untertitel zur Auswahl. Auf ardplus.de lassen sich aber Audio-Ausgabe und Untertitel einstellen.

Du hast höchstwahrscheinlich eine alte Version des Skripts. Ich habe eben gesehen, dass in der Installationsanleitung noch eine alte Version verlinkt war. Ist jetzt angepasst. Bitte neu downloaden und darauf achten, dass der Parameter --audio-multistreams im Skript vorkommt - dann ist es die neueste Version.

//EDIT: Gerade mit einer Folge getestet. Es werden beide Audiospuren bei Sherlock geladen und auch beide Untertitel (deutsch/englisch).

@marco79cgn
Copy link
Author

marco79cgn commented May 24, 2025

Bei folgendem Inhalt ist mir außerdem eine Ungereimtheit (Option bzw. Staffel 5, doch nur mit Eingabe 1 läuft das Skript ordnungsgemäß – ist hier die Zeilennummer relevant?) ...

Korrekt, hier werden die verschiedenen Staffeln normalerweise durchnummeriert und ich nehme die Zeile. als Index für die Staffel. Das funktionierte bisher immer, weil es normalerweise nur gesamte Serien mit allen Staffeln gab und Staffel 1 immer Index 1 war usw. Hier ist es ein Sonderfall, es gibt nur eine Staffel und die hat aber Nummer 5. Schönheitsfehler, schau ich mir an.

jq: parse error: Invalid numeric literal at line 1, column 64
jq: parse error: Invalid numeric literal at line 1, column 64
jq: parse error: Invalid numeric literal at line 1, column 64
jq: parse error: Invalid numeric literal at line 1, column 64
Lade S01E00 - ...
ERROR: [generic] '?' is not a valid URL. Set --default-search "ytsearch" (or run yt-dlp "ytsearch:?" ) to search YouTube
Ich habe den Aufruf zweimal wiederholt, mit gleichem Ergebnis. Die „S01E09“ lässt sich nicht herunterladen, obwohl auf ardplus.de enthalten & anschaubar.

Da war tatsächlich ein Fehler beim parsen des JSON. Verantwortlich dafür waren zusätzliche Quotes im Dateinamen "Galilei – \"und sie dreht sich doch\"", deren Escape Zeichen \ beim parsen verloren gingen. Das ist jetzt gefixt in der neuesten Version des Skripts. Bitte nochmal aktualisieren.

Dein Skript funktioniert ansonsten sehr gut! Vielen Dank dafür!

Danke schön.

@marco79cgn
Copy link
Author

Update 24.05.2025: Bugfix

Spezielle Zeichen im Dateinamen werden jetzt gefiltert (z.B. "Galilei – \"und sie dreht sich doch\""). Bitte auf die neueste Version aktualisieren.

@kobeegh
Copy link

kobeegh commented May 24, 2025

Habe die aktuelle Version des Skripts geladen. Bekomme allerdings bei jedem Versuch nur folgende Ausgabe:
Login not possible! Please check credentials and subscription for user [email protected].

Die ard-plus-token Datei wird angelegt, ist jedoch leer (bzw. nur 1 Zeilenumbruch drin)
In der Geräteliste bei ardplus.de taucht allerdings bei jedem Versuch ein neues angemeldetes Gerät (Chrome) auf.
Der Login scheint also zu klappen, nur landet das auth-token irgendwie nicht in der Datei. Eine Idee, wo es haken könnte?

edit:
habe den login-curl manuell mit verbose ausgeführt und den auth-token (authorization: Bearer) dann manuell in die Datei ard-plus-token gepackt.
Damit gehts jetzt. Wieso das automatisch nicht geklappt hat, versteh ich jetzt aber nicht so wirklich

@marco79cgn
Copy link
Author

marco79cgn commented May 25, 2025

@kobeegh
Das könnte eventuell an Sonderzeichen im Password gelegen haben. Ich habe das Skript eben nochmal angepasst. Könntest du bitte bei dir einmal die Datei ard-plus-token löschen und es dann nochmal mit der neuen Version des Skripts versuchen? Thx.

@kobeegh
Copy link

kobeegh commented May 25, 2025

Nein hat leider nichts geändert. Hatte auch schon alle Sonderzeichen aus dem Kennwort rausgeworfen zum Testen. Das einzige Sonderzeichen ist das @ in der E-Mail-Adresse als Loginname

edit:
Der Vollständigkeit halber:
Ubuntu 22.04.05
curl 7.81.0
jq 1.6
yt-dlp 2025.05.22

@ipo0x
Copy link

ipo0x commented May 25, 2025

@marco79cgn
Vielen Dank für die Anpassungen – hat alles geklappt, konnte „Sherlock“ als auch die andere Serie entsprechend laden! :)

Das einzige Problem ist, dass es mir die .vtt-Dateien nebendran legt – oder ist das so gedacht/richtig? Gleichzeitig kommt eine Postprocessing-Fehlermeldung und „encoder not found“ o. ä., obwohl z. B. ffmpeg (7.1.1) installiert ist.

@kobeegh
Ich habe das Problem bislang nicht, hatte nur einmal eine Fehlermeldung, ab dem 2. oder 3. Versuch lief alles wie erwartet. Sonderzeichen habe ich nicht im Passwort. Fedora 42 (curl 8.11.1, jq-1.7.1, yt-dlp 2025.03.31).

@marco79cgn
Copy link
Author

Nein hat leider nichts geändert. Hatte auch schon alle Sonderzeichen aus dem Kennwort rausgeworfen zum Testen. Das einzige Sonderzeichen ist das @ in der E-Mail-Adresse als Loginname

edit: Der Vollständigkeit halber: Ubuntu 22.04.05 curl 7.81.0 jq 1.6 yt-dlp 2025.05.22

Es liegt an deiner curl Version, wie ich eben rausgefunden habe. Der Parameter -w '%header{authorization}' in curl dient dazu, den Wert des Authorization-Headers aus der HTTP-Antwort auszugeben. Das wurde leider erst mit curl Version 7.84.0 eingeführt. Schade, denn das ist tatsächlich sehr praktisch, um an das Token zu kommen (ohne -v und fehleranfälliges sed/awk/grep). Gibt es keine Möglichkeit, die Version zu aktualisieren? Am Mac habe ich bereits curl 8.7.1.

@kobeegh
Copy link

kobeegh commented May 26, 2025

Tatsächlich, mit aktuellerer Version von curl läufts direkt.
Aktuell kann ich noch nicht vom 22.04 hochmigrieren, und dort ist Schluss bei curl 7.81.0. Habe mir via snap parallel noch curl 8.13.0 installiert.

Dein Skript habe ich entsprechend angepasst, oben ergänzt:

#curlVersion=/usr/bin/curl     # system default
curlVersion=/snap/bin/curl     # snap version 8.13.0

Und dann unten die ganzen einzelnen Aufrufe von curl angepasst:
z.B.
loginResult=$(curl -s -o /dev/null ... loginResult=$("$curlVersion" -s -o /dev/null ...

Geht bestimmt auch noch eleganter, aber so läufts erstmal.
Danke für deine Unterstüztung!

@marco79cgn
Copy link
Author

marco79cgn commented May 26, 2025

@kobeegh
Ich habe gerade das parsen des Tokens angepasst im Skript. Es funktioniert jetzt auch mit alten Versionen von curl. Zudem prüft das Skript jetzt, ob überhaupt ein Token zurück kommt beim Login und ob er vom Typ JWT ist. Falls nicht, beendet sich das Skript und es wird keine leere Datei gespeichert geschweigedenn versucht, ohne Token mit deren API zu kommunizieren.

@profhccaesar
Copy link

Hallo, bin gerade auf dein Skript gestoßen.

Leider schaffe ich es nicht, Videos herunter zu laden, ich bekomme immer folgende Meldung:

$ ./ard-plus-dl.sh 'https://www.ardplus.de/details/a0S010000037hjZ-kommissar-dupin-bretonischer-ruhm' 'meinuser' 'meinpw'
Couldn't get season details. Trying again!
cat: content-result.txt: Datei oder Verzeichnis nicht gefunden
Lade Film  ()...
ERROR: [generic] '?' is not a valid URL. Set --default-search "ytsearch" (or run  yt-dlp "ytsearch:?" ) to search YouTube

Ich habe das einmal weiter analysiert und festgestellt, dass der HTTP-Request per curl immer den Code 000 (anstatt 200) zurückliefert.

Mache ich etwas falsch oder hat ARD schon wieder Änderungen vorgenommen?

P. S. Ich habe folgende Änderungen an deinem Skript vorgenommen:

  1. Beim wiederholten Versuch, die Filmdaten per curl zu holen, fehlt das Fehler-Handling, weshalb die seltsame Meldung content-result.txt: Datei oder Verzeichnis nicht gefunden auftaucht.
  2. Ausgabe des HTTP-Status-Codes in der Meldung.
--- ard-plus-dl.sh	2025-06-02 19:07:34.513796353 +0200
+++ ard-plus-dl.sh-patched	2025-06-02 19:10:02.053509897 +0200
@@ -109,7 +109,7 @@
     -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
 if [[ $seasonsStatus != "200" ]]; then
     #retry once
-    echo "Couldn't get season details. Trying again!"
+    echo "Couldn't get season details (HTTP-Status $seasonsStatus). Trying again!"
     sleep 2
     seasonsStatus=$(curl -s -o content-result.txt -w "%{http_code}" "${contentUrl}" \
     -H 'authority: data.ardplus.de' \
@@ -118,6 +118,10 @@
     -H 'origin: https://www.ardplus.de' \
     -H 'referer: https://www.ardplus.de/' \
     -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36')
+    if [[ $seasonsStatus != "200" ]]; then
+        echo "Couldn't get season details (HTTP-Status $seasonsStatus). Giving up."
+        exit 1
+    fi
     contentResult=$(cat content-result.txt)
 else
     contentResult=$(cat content-result.txt)

Schöne Grüße
Raimund

@marco79cgn
Copy link
Author

Ich habe das einmal weiter analysiert und festgestellt, dass der HTTP-Request per curl immer den Code 000 (anstatt 200)zurückliefert.

Das Skript bzw. der curl Befehl versucht, die Metadaten über das ARD Plus API zu bekommen und speichert diese temporär in einer Datei namens content-result.txt.

curl -s -o content-result.txt -w "%{http_code}" "${contentUrl}"

Der Grund hierfür ist, dass ich primär an den http status code kommen will, um zu prüfen, dass der 200 (=success) ist. Denn dann kann man davon ausgehen, dass der Aufruf auch erfolgreich war. Der Grund hierfür ist das Fehlerhandling. Wenn da bei dir stattdessen 000 zurück kommt, dann ist genau das das Problem. Welche curl Version benutzt du denn?

@marco79cgn
Copy link
Author

@profhccaesar
Habe es eben auf einem alten Raspberry Pi OS bullseye nachgestellt. Die neueste curl-Version via apt ist dort 7.74.0 und die hat auch diesen Bug, der zu 000 führt. Scheint ein Problem zu sein mit https im Zusammenhang mit -w "%{http_code}" in alten Curl Versionen. (Das Problem ist seit langem gefixt.)

Workaround:
neue curl Version per snap installieren. Beim Raspberry Pi klappt das wie hier beschrieben. Damit konnte ich das Problem direkt lösen!

Ich habe gerade das Skript angepasst und in der ersten Zeile die Möglichkeit ergänzt, einen alternativen Pfad für das curl binary anzugeben. Man muss es entsprechend nur an dieser einen Stelle ändern.

Vielleicht sollte ich das Ganze als Docker Container anbieten. Eigentlich habe ich gedacht, bei den wenigen Abhängigkeiten kann nicht viel schief gehen. Aber offensichtlich sind viele unterschiedliche curl Versionen im Umlauf, die oft veraltet sind und/oder Bugs enthalten.

@profhccaesar
Copy link

@marco79cgn
Vielen Dank für den Typ - obwohl ich täglich mit curl arbeite, ist mir dieser Bug noch nicht in die Quere gekommen.

Leider gestaltete es sich etwas schwierig, eine aktuelle curl-Version auf meinem System zum Laufen zu bekommen - snapd ist aus diversen Gründen deaktiviert. Ich habe deshalb deine Idee des Docker-Containers aufgegriffen und einen gebaut, liegt hier auf Github im Projekt docker-ard-plus-dl. Bitte nach Belieben verwenden und anpassen - das Ding unterliegt der "Unlicense"-Lizenz.

Die Verwendung ist - eine lauffähige Docker-Installation vorausgesetzt - sehr einfach:

# Projekt herunterladen:
git clone https://github.com/profhccaesar/docker-ard-plus-dl.git ./docker-ard-plus-dl

# Video herunterladen:
cd docker-ard-plus-dl
./ard-plus-dl-docker.sh --dir ~/Downloads 'https://www.ardplus.de/details/a0S010000037hjZ-kommissar-dupin-bretonischer-ruhm' 'myuser' 'mypassword'

Wenn nötig, wird das Image gebaut, yt-dlp und ard-plus-dl.sh werden in der jeweils aktuellsten Version heruntergeladen. Anschliessend wird ard-plus-dl.sh im Container gestartet. Die heruntergeladenen Dateien landen im angegebenen Verzeichnis (--dir, im Beispiel also ~/Downloads).

Werden Änderungen am Image vorgenommen (Anpassung Dockerfile / Startskript) oder gibt es eine neue Version von yt-dlp oder ard-plus-dl.sh, muss lediglich der Parameter --force angegeben werden, dann wird das vorhandene Image gelöscht und mit den neuesten Versionen der Skripte neu gebaut:

./ard-plus-dl-docker.sh --force --dir ~/Downloads 'https://www.ardplus.de/details/a0S010000037hjZ-kommissar-dupin-bretonischer-ruhm' 'myuser' 'mypassword'

@marco79cgn
Copy link
Author

@profhccaesar
Super, vielen Dank! Ich hatte die gleiche Idee und bin beim über-optimieren an Alpine gescheitert (wollte das Image so klein wie möglich haben, aber ist eigentlich auch nicht so extrem wichtig).

Ich werde es zeitnah ausprobieren. Was ich bei einem anderen Projekt festgestellt habe: die mwader/static-ffmpeg binaries beschleunigen nicht nur das initiale bauen des Containers, sie machen ihn vor allem wesentlich kleiner, was die Größe anbelangt.

@profhccaesar
Copy link

profhccaesar commented Jun 4, 2025

P. S. Fehler im Docker-Image behoben, bitte von Github aktualisieren (z. B. git pull) und einmal mit Parameter --force aufrufen.

* (Bugfix) Missing dependency bsdextrautils.
* (Bugfix) Container now interactive - ard-plus-dl.sh sometimes requires user interaction, e. g. for season selection.

@marco79cgn
Um ehrlich zu sein, habe ich mich um die Größe des Images herzlich wenig gekümmert. Es ist 970 MB groß, der Container benötigt nur ein paar Bytes. Jedes heruntergeladene Video ist um ein Mehrfaches größer - eine Optimierung der Größe bringt also in Relation kaum etwas.

$ docker system df -v
Images space usage:

REPOSITORY                            TAG              IMAGE ID       CREATED         SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
ard-plus-dl                           latest           423ff031ddb0   13 hours ago    971MB     0B            970.9MB       1
...
Containers space usage:

CONTAINER ID   IMAGE                        COMMAND                  LOCAL VOLUMES   SIZE      CREATED              STATUS                NAMES
629bbfd5db5e   ard-plus-dl                  "bash"                   0               0B        About a minute ago   Up About a minute     ard-plus-dl
...

Die Zeit zum Bauen des Images hat mich auch nicht viel mehr interessiert, das es nur ein einziges Mal stattfindet; außerdem muss man auch hier die Verhältnismäßigkeit sehen: jeder Video-Download benötigt mehrere Minuten; da fällt die einmalige "Investition" von etwa 20 Sekunden für den Bau des Image kaum ins Gewicht.

Zudem wird der größte Zeitanteil beim Bauen des Images sowieso durch das Herunterladen der benötigten Programme per apt-get und das Klonen des yt-dlp-Repositories beansprucht; da bringen eine geschicktere Auswahl des Images und der verwendeten Pakete in der Relation nicht viel.

@marco79cgn
Copy link
Author

marco79cgn commented Jun 4, 2025

@profhccaesar
Ja, da hast du natürlich vollkommen Recht. Es ist der innere Monk. ;) Ich habe die Größe jetzt dennoch runter bekommen auf 223 MB. Deine Variante ist robuster, speziell was die ganzen Berechtigungen angeht. Ich habe jetzt dennoch alles mal in ein richtiges Github Repository geworfen. Das macht Änderungen am Skript einfacher und du kannst auch leichter die aktuelle Version verlinken.

https://github.com/marco79cgn/ard-plus-dl/tree/main

Dort wird auch automatisch ein Docker image gebaut via Github Actions zur direkten Verwendung (optional).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment