All projects Fermata · Write-up
A downloader whose server only relays bytesEin Downloader, dessen Server nur Bytes weiterreicht
Fermata turns a YouTube link into a tagged MP3, a FLAC or an MP4. Everything from choosing streams to encoding runs in the browser; the server is a relay of about 180 lines.Fermata macht aus einem YouTube-Link eine getaggte MP3, eine FLAC oder eine MP4. Von der Stream-Auswahl bis zum Encoding läuft alles im Browser; der Server ist ein Relay mit rund 180 Zeilen.
- Type
- Web appWeb-App
- Stack
- Svelte · WebCodecs · Node
- Year
- 2026
- Code
- PrivatePrivat
Why there is a server at all
A static site can’t do this on its own. YouTube sends no CORS headers, so a page can’t read its responses, and stream URLs are signed for the IP address that requested them. So the server does exactly one thing: it forwards requests of the form /relay/<host>/<path> to an allowlist of YouTube hosts and pipes the bytes back. It never parses, stores or transforms anything.
- InnerTube client, with a fallback chain
- Byte-range probe before trusting a stream
- Worker: 8 MiB ranges into the private file system
- WebCodecs and WASM: mux, encode, trim, tag, zip
- Allowlisted hosts only
- Streams bytes
- Stores and parses nothing
- youtube.com
- googlevideo.com
- ytimg.com
The details are where a relay like this goes wrong. Browsers silently drop headers such as User-Agent, Cookie, Origin and Referer from fetch(), so the client sends them as x-relay-* headers and the relay restores them. In the other direction, everything that describes the browser’s connection to the relay is dropped, and the Authorization header never leaves: with a password set on the instance, the browser attaches it to every same-origin request, relay calls included.
Trust, but probe
Fermata talks to YouTube’s InnerTube API through youtubei.js. YouTube restricts its API clients independently and changes the rules often, so Fermata tries a chain of them, and a client is only accepted after a real byte-range request for the last 4 KB of the best audio and video streams succeeds. The end is the part worth testing: a client that is about to be refused typically gets the first few hundred kilobytes, and then a 403.
When YouTube changes something, the fix is usually an update of youtubei.js or a new order in the chain, not a rewrite.
Chunks, in a worker, on disk
googlevideo throttles long-lived requests. So a download is split into 8 MiB byte ranges, fetched over three connections by a Web Worker and written to the origin private file system rather than held in memory. That is faster, it can resume per chunk, and a long video doesn’t have to fit in RAM. Behind HTTP/2 the browser isn’t limited to six connections per host, which is one more reason to put an instance behind a proxy.
Processing on the device
The downloaded streams are processed locally with mediabunny, WebCodecs and WebAssembly. What the source already provides is remuxed without re-encoding: M4A, Opus, MP4, WebM, MKV. MP3 at 320 kbps, WAV and FLAC are encoded. Trimming follows YouTube’s “most replayed” curve, tags are parsed from the video title or read from YouTube Music uploads, and a full album upload becomes numbered, tagged tracks, one per chapter, in a ZIP.
WebCodecs is only available in a secure context. On localhost that’s a given; on http://192.168.x.x it isn’t, so Fermata shows a banner there and offers only the remux formats. On the NAS the Cloudflare Tunnel provides the HTTPS.
A note on use
Only download what you have the right to keep: your own uploads, Creative Commons material, or content the rights holder allows. Fermata doesn’t handle accounts, so age-restricted and members-only videos are out of reach by design.
Wozu überhaupt ein Server
Eine statische Seite schafft das nicht allein. YouTube sendet keine CORS-Header, eine Seite kann die Antworten also nicht lesen, und Stream-URLs sind für die IP-Adresse signiert, die sie angefragt hat. Also macht der Server genau eine Sache: Er leitet Anfragen der Form /relay/<host>/<pfad> an eine Allowlist von YouTube-Hosts weiter und reicht die Bytes zurück. Er parst, speichert und verändert nichts.
- InnerTube-Client mit Fallback-Kette
- Byte-Range-Test, bevor ein Stream zählt
- Worker: 8-MiB-Blöcke ins private Dateisystem
- WebCodecs und WASM: muxen, encodieren, schneiden, taggen, zippen
- Nur Hosts auf der Allowlist
- Reicht Bytes durch
- Speichert und parst nichts
- youtube.com
- googlevideo.com
- ytimg.com
Die Details sind es, an denen so ein Relay scheitert. Browser lassen Header wie User-Agent, Cookie, Origin und Referer bei fetch() stillschweigend weg, also schickt der Client sie als x-relay-*-Header, und das Relay setzt sie wieder ein. In die andere Richtung fällt alles weg, was die Verbindung des Browsers zum Relay beschreibt, und der Authorization-Header verlässt den Server nie: Ist für die Instanz ein Passwort gesetzt, hängt der Browser es an jede Anfrage an dieselbe Origin, auch an die Relay-Aufrufe.
Vertrauen ist gut, testen ist besser
Fermata spricht über youtubei.js mit YouTubes InnerTube-API. YouTube schränkt seine API-Clients unabhängig voneinander ein und ändert die Regeln oft, deshalb probiert Fermata eine ganze Kette durch, und ein Client gilt erst, wenn eine echte Byte-Range-Anfrage für die letzten 4 KB des besten Audio- und Video-Streams klappt. Das Ende ist der Teil, der sich zu testen lohnt: Ein Client, der gleich abgewiesen wird, bekommt typischerweise die ersten paar hundert Kilobyte und dann ein 403.
Ändert YouTube etwas, reicht meist ein Update von youtubei.js oder eine neue Reihenfolge in der Kette, kein Umbau.
Blöcke, im Worker, auf der Platte
googlevideo drosselt langlebige Anfragen. Ein Download wird deshalb in Byte-Bereiche von 8 MiB aufgeteilt, von einem Web Worker über drei Verbindungen geladen und ins Origin Private File System geschrieben, statt im Speicher zu liegen. Das ist schneller, lässt sich pro Block fortsetzen, und ein langes Video muss nicht in den Arbeitsspeicher passen. Hinter HTTP/2 ist der Browser nicht auf sechs Verbindungen pro Host beschränkt, ein Grund mehr, eine Instanz hinter einen Proxy zu stellen.
Verarbeitung auf dem Gerät
Die geladenen Streams werden lokal mit mediabunny, WebCodecs und WebAssembly verarbeitet. Was die Quelle schon mitbringt, wird ohne Neucodierung umverpackt: M4A, Opus, MP4, WebM, MKV. MP3 mit 320 kbps, WAV und FLAC werden encodiert. Das Zuschneiden orientiert sich an YouTubes Kurve „Am häufigsten wiedergegeben“, Tags werden aus dem Videotitel gelesen oder von YouTube-Music-Uploads übernommen, und ein kompletter Album-Upload wird zu nummerierten, getaggten Tracks, einer pro Kapitel, in einem ZIP.
WebCodecs gibt es nur in einem sicheren Kontext. Auf localhost ist das gegeben, unter http://192.168.x.x nicht, deshalb zeigt Fermata dort ein Banner und bietet nur die Formate zum Umverpacken an. Auf dem NAS liefert der Cloudflare Tunnel das HTTPS.
Ein Wort zur Nutzung
Lade nur herunter, was du behalten darfst: eigene Uploads, Creative-Commons-Material oder Inhalte, die der Rechteinhaber freigibt. Fermata verwaltet keine Konten, altersbeschränkte Videos und Inhalte nur für Kanalmitglieder bleiben also bewusst außen vor.