Befreiung vom (digitalen) Überfluss

Datenschutzfreundliche Youtube-Thumbnails für HUGO

Ein Skript um Youtube-Thumbnails automatisch abzurufen und lokal zu cachen – als maximal datenschutzfreundliche Lösung, da keine Userdaten zu Youtube weitergeleitet werden.

Das direkte Einbinden von externen Ressourcen ist aus Datenschutzgründen problematisch. Lösungen mit einem Cookie-Opt-In lehne ich grundsätzlich ab. Ich stelle hier eine Lösung vor, die ohne externe API auskommt und das automatisch generierte Youtube-Thumbnail abruft und lokal cached.

Diese Lösung funktioniert mit geringen Anpassungen auch für “normale” statische Websites. Ich habe sie für HUGO entwickelt. Sie besteht aus zwei Komponenten:

  1. Eine php-Funktion, die anhand der Youtube-ID das Thumbnail abruft und lokal speichert.
  2. Ein Shortcode, der das Thumbnail mit einem Play-Button anzeigt und auf Youtube verlinkt.

youtube_thumb.php

Zuerst wird versucht, das große Thumbnail (maxresdefault.jpg) bei Youtube abzurufen. Wenn das fehlschlägt wird ein kleiners Thumbnail abgerufen, das es eigentlich immer gibt. Das Bild wird lokal in den Ordner “cache/” gespeichert. Wenn das Bild schon im Cache vorhanden ist, wird es nicht erneut abgerufen. Am Ende gibt es einen Redirect auf dieses lokal abgelegte Bild. Falls es gar kein Bild gibt (bzw. im Fehlerfall) wird ein Default-Bild angezeigt.

Der Code ist relativ übersichtlich. Ich habe curl für den Abruf der Daten genutzt, weil ich das für den zweistufigen Abruf der Bilder benötige und weil es einen zuverlässigen Timeout gibt. Mit file_get_contents habe ich im Zusamenhang mit Timeout schlechte Erfahrungen gemacht.

Um zu verhindern, dass das Skript missbraucht wird, prüfe ich den Referer ab.

Ich gehe davon aus, dass das Skript im HUGO-Verzeichnis static/ liegt und das es dort ein Unterverzeichnis cache/ gibt. Das Verzeichnis muss manuell angelegt werden. Es wird nicht vom Skript erstellt. Je nach Provider und Art des Uploads muss das Verzeichnis cache möglicherweise großzügigere Schreibrechte haben. Das muss ggf. nach dem Upload korrigiert werden. (Ich veröffentliche HUGO Websites mit einem Shell-Skript per rsync und setze nach dem Aufruf von hugo Schreibrechte im Verzeichnis public/cache/ und synce sie anschließend mit rsync -avz.)

<?php
/* youtube_thumb.php: Fetches the Youtube thumbnail and saves it locally
   Usage: youtube_thumb.php?v=<Youtube-ID> Returns the cached image URL
   author: Erhard Maria Klein, https://www.weitblick.de
*/

$whitelist = '/(yourdomain.com|localhost)/'; // Prevention of abuse
$defaultImage = 'images/default-image.jpg'; // Displayed when no image could be retrieved
$serverPath = './';
$v = '';
if (isset($_GET['v'])) $v = htmlspecialchars($_GET['v']);
$imageData = '';
$cacheFolder = 'cache/';
$fileName = $cacheFolder . $v . '.jpg';
$referer = $_SERVER['HTTP_REFERER'];

if (preg_match($whitelist, $referer, $match)) {

    // Check if the file already exists
    if (!file_exists($fileName)) {
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_HEADER, 0);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
	curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
	curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
	curl_setopt($ch, CURLOPT_URL, 'https://img.youtube.com/vi/' . $v . '/maxresdefault.jpg');
	$imageData = curl_exec($ch);
	$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        // If maxresdefault.jpg doesn't exist, retrieve 0.jpg
        if ($httpCode != 200) {
            curl_setopt($ch, CURLOPT_URL, 'https://img.youtube.com/vi/' . $v . '/0.jpg');
            $imageData = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if ($httpCode != 200) $imageData = '';
        }
        curl_close($ch);
        
        if (!empty($imageData)) {
            file_put_contents($serverPath . $fileName, $imageData);
            header("Location: /$fileName");
        } else {
            header("Location: /$defaultImage");
        }
    } else {
        header("Location: /$fileName");
    }
    exit;
}
?>

HUGO Shortcode youtube.html

{{- $src := .Get "src" | default (.Get 0) -}}
{{- $caption := .Get "caption" | default (.Get 1) | safeHTML -}}
<a href="https://youtu.be/{{ $src }}" target="_blank" title="externes Video (Link öffnet Youtube)" style="display: block; text-align: center; position: relative;">
	<figure>
		<svg width="100" height="100" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
			<path fill="#ec322a" stroke="#fff" d="M15,4.1c1,0.1,2.3,0,3,0.8c0.8,0.8,0.9,2.1,0.9,3.1C19,9.2,19,10.9,19,12c-0.1,1.1,0,2.4-0.5,3.4c-0.5,1.1-1.4,1.5-2.5,1.6 c-1.2,0.1-8.6,0.1-11,0c-1.1-0.1-2.4-0.1-3.2-1c-0.7-0.8-0.7-2-0.8-3C1,11.8,1,10.1,1,8.9c0-1.1,0-2.4,0.5-3.4C2,4.5,3,4.3,4.1,4.2 C5.3,4.1,12.6,4,15,4.1z M8,7.5v6l5.5-3L8,7.5z"></path>
		</svg>
		<img src="/youtube_thumb.php?v={{ $src }}" alt="{{ $caption | default $.Page.Title }}" loading="lazy">
		{{ with $caption }}<figcaption style="text-align: left;">{{ . }} (Link öffnet Youtube)</figcaption>{{ end }}
	</figure>
</a>

Anwendung

Innerhalb des Contents werden Youtube-Videos mit {{< youtube ID "caption">}} eingebunden - z.B. mit
{{< youtube 0RKpf3rK57I "HUGO in 100 Seconds" >}}.

Alternativ können auch die Parameter src und caption verwendet werden:
{{< youtube src="0RKpf3rK57I" caption="HUGO in 100 seconds" >}}

HUGO in 100 Seconds
HUGO in 100 Seconds (Link öffnet Youtube)

Tipps und Hinweise

Die php-Funktion wird in einem normalen <img>-Tag aufgerufen – z.B.
<img src="/youtube_thumb.php?v=0RKpf3rK57I" >.
Das Skript macht einen Redirect auf das gecachte Bild. Das funktioniert daher auch gutinnerhalb von statischem HTML-Code. Das einzige dynamische Element in dieser Lösung ist das freistehende php-Skript. Das könnte sogar auf einem anderen Server installiert werden und für mehrere Websites als API fungieren ($whitelist und Redirect-Location entsprechend anpassen).

Bei jedem HUGO Build wird das cache-Verzeichnis neu erzeugt – und zwar ohne Schreibrechte. Wer der Website per FTP hochlädt, muss also daran denken, dem Ordner im public-Verzeichnis Schreibrechte zu geben. Ich habe das in mein deploy-Skript integriert (s.o.).

Außerdem müssen die Pfade korrekt sein. Mein Skript geht davon aus, dass die Website im root-Verzeichnis einer Domain läuft und verwendet eine relative Pfad-Angabe für das Schreiben der Image-Datei in den Cache. In die Variable $serverPath kann aber auch ein absoluter Serverpfad eingetragen werden. Das php-Skript und das cache-Verzeichnis liegen im Ordner static/ und werden beim HUGO Build nach public/ übertragen.

Die Variable $whitelist enthält einen regulären Ausdruck, der auf den Domainnamen matchen muss. Das verhindert, dass jemand das Skript für seine Zwecke missbraucht. Also: nicht vergessen, den anzupassen!

$defaultImage enthält einen Pfad auf ein Default-Bild, das angezeigt wird, wenn kein Bild von Youtube geholt werden konnte – also auch dann, wenn die Youtube-ID ungültig war.

Die gecachten Bilder werden nicht automatisch aktualisiert und verfallen nicht. Ich habe das Skript bewusst einfach gehalten. Beim nächsten HUGO Build wird das Cache-Verzeichnis sowieso neu (leer) erzeugt. Ansonsten muss das Verzeichnis bei Bedarf manuell gelöscht werden.


« Lowtech-TV: Antennenfernsehen ist das neue Streaming 😉 | OnePage Website mit HUGO »