Eine Liste von Gründen, aus denen ich doch anfing Youtube-Videos herunterzuladen
Letztens schrieb ich auf Mastodon, dass ich viel Musik auf Youtube konsumiere, da ich vor allem die TV Noir-Aufnahmen von diversen Künstlern teilweise besser finde, als alles, was sie je auf Alben veröffentlicht haben. Normalerweise läuft das so ab, dass ich Youtube öffne, eins der Videos von der Startseite anklicke und dem Algorithmus vertraue, der mich nach und nach durch meine altbekannten und oft-gehörten Songs begleitet.
Mich hat das schon immer etwas gestört. Was wenn eins meiner Lieblingsvideos mal verschwindet. Vielleicht finde ich sie auch gar nicht wieder, wenn der Algorithmus es aussortiert! Außerdem ist es auch ziemlich verschwenderisch immer ganze Videos zu streamen, wenn man eh nur Musik hört und eigentlich würde ich die Tracks auch gerne unterwegs hören.
Ich überlegte mir also, dass es gut wäre, wenn die Tracks einfach in meinem Plex gespeichert sind. Ich suchte nach schönen Youtube-DL-GUIs und Web-Interfaces, die sowas vielleicht schon abbilden, aber das war mir doch alles zu viel.
Ich benutzte also, as you do, Chat GPT und ließ mir ein PHP-Skript generieren, dass alle Videos aus einer Youtube-Playlist herunterläd. Ich liebe diesen Chatbot, so viel getippe für so Quatsch-Aufgaben gespart.
Die Videos lade ich mit -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'
herunter, ich weiß aber noch nicht, ob das Sinn ergibt, aber vom Gefühl her ist h264 kompatibler als dieser Google-Codec von .webm
. Könnte ich aber noch mal recherchieren. Zusätzlich strippe ich noch einmal die Audio-Spur aus dem .mp4
-Container. Das Video kommt in ein Musikvideos-Verzeichnis vom Plex und die .m4a
-Datei in das Musik-Verzeichnis, auf das ich per Plexamp zugreifen kann, so habe ich beides parat!
Das ganze habe ich jetzt als Cronjob, der regelmäßig schaut, ob es was neues in der Playlist gibt und mir per Telegram Bescheid gibt, dass etwas erfolgreich einsortiert wurde.
Mein Workflow ist jetzt, dass ich Videos, die ich gerne gesichert hätte, einfach zu einer Playlist hinzufüge, die ich auf “unlisted” gestellt habe. Damit ist sie immer noch privat, aber mein Dreizeiler kann sie abrufen, ohne, dass ich OAuth-Kram mit der Youtube-API machen muss.
Nach der Implementierung dachte ich natürlich direkt an das grundlegende Hass-Thema bei solchen Sachen: Paginierung. Die Youtube-API liefert maximal 50 PlaylistItems
zurück. Muss ich nun wirklich einbauen, dass er alle Seiten fetcht? Wie nervig wäre das denn. Also, ich könnte den Chatbot bitten, es einzubauen, aber ich fand eine viel bessere Lösung: Wenn man die Sortierung der Playlist im Webinterface von Youtube auf “Added at… (newest first)” einstellt, spiegelt sich das auch in der API-Response wider! Wie geil ist das denn. So lang ich nun zwischen zwei Cronjob-Läufen nicht mehr als 50 Videos hinzufüge, sollte das alles kein Problem sein.
Was verbleibt ist nun leider noch, dass die entstandenen Dateien natürlich völlige Kraut-und-Rüben-Namen haben. Leider werden Youtube-Videos nur selten nach einem vernünftigen Schema benannt, und so werde ich wohl alle paar Wochen mal durchgehen müssen und die Songs in Plex vernünftig vertaggen. Aber naja.
Falls jemand sowas auch braucht, hier der unspannende Code, wahrscheinlich hätte Chat GPT ihn auch komplett selber schreiben können, aber etwas Eigenleistung darf ja schon drin sein!
<?php
// Telegram Bot API-Token
$api_token = '';
// Telegram Chat-ID
$chat_id = '';
// Playlist-ID
$playlist_id = '';
// Google API Key
$api_key = '';
$response = file_get_contents("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=$playlist_id&key=$api_key");
$data = json_decode($response, true);
$known = file_exists('known.txt') ? file('known.txt') : [];
$known = array_map('trim', $known);
foreach($data['items'] as $item) {
$video_id = $item['snippet']['resourceId']['videoId'];
$video_title = $item['snippet']['title'];
if(in_array($video_id, $known)) {
continue;
}
echo "Go: $video_title\n";
$slug = preg_replace( '/[^a-z0-9]+/i', '-', $video_title);
$videoFilename = sprintf('%s.mp4', $slug);
$audioFilename = sprintf('%s.m4a', $slug);
exec(sprintf('yt-dlp -f \'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4\' -o "%s" "%s"', $videoFilename, $video_id));
exec(sprintf('ffmpeg -i "%s" -vn -c:a copy "%s"', $videoFilename, $audioFilename));
// copy files to correct directories
rename('./' . $videoFilename, '/mnt/plex/Plex/Musikvideos/' . $videoFilename);
rename('./' . $audioFilename, '/mnt/plex/Plex/Musik/' . $audioFilename);
file_get_contents("https://api.telegram.org/bot$api_token/sendMessage?chat_id=$chat_id&text=" . urlencode(sprintf('Video heruntergeladen: %s', $video_title)));
$known[] = $video_id;
file_put_contents('known.txt', implode(PHP_EOL, $known));
}
(Immer wieder schön, wie man bei Telegram mit einem kurzen GET
-Request ohne viel Trara Nachrichten verschicken kann)
So, jetzt lasse ich das mal ein Bisschen laufen und melde mich, sobald ich zum ersten Mal auch tatsächlich etwas aus dem Archiv gehört habe…