DRM-free at Last: How I Made VRT MAX Watchlist Downloads Work on Raspberry Pi
VRT MAX streams are DRM-protected and notoriously hard to download. After weeks of work involving pywidevine, N_m3u8DL-RE, and a custom _vrt_drm_* field detector, the thuis-v4 downloader now gracefully handles both DRM and non-DRM content. This post covers the full journey — from detecting DRM in the HLS manifest to the fallback strategy that keeps non-DRM downloads fast.
The Problem: Why VRT MAX Refuses to Play Nice
VRT MAX is the streaming platform of Belgium's public broadcaster (VRT). Their content uses Widevine DRM — the same technology Netflix and Spotify use. Standard downloaders like yt-dlp hit a wall: they see the HLS manifest, attempt to download segments, and get encrypted garbage.
The key insight came from inspecting the GraphQL responses: VRT MAX embeds _vrt_drm_* fields that explicitly tell you whether content is protected. No guessing, no failed downloads — just read the field and route accordingly.
The Detection Layer: Classifying Content Before Download
The thuis-v4 downloader uses a two-stage probe in probe.py and drm_decrypt.py:
- GraphQL fetch — retrieve the episode's streaming metadata including
_vrt_drm_key_id,_vrt_drm_license_url, and_vrt_drm_protection_scheme - HLS manifest inspection — if DRM fields exist, parse the manifest for
#EXT-X-KEYtags to confirm encryption method (AES-128 vs SAMPLE-AES)
# Simplified detection logic
def classify_content(episode_data: dict) -> ContentType:
if episode_data.get("_vrt_drm_key_id"):
return ContentType.DRM_PROTECTED
# Fallback: inspect HLS manifest for EXT-X-KEY
manifest = fetch_hls_manifest(episode_data["hls_url"])
if "#EXT-X-KEY:METHOD=SAMPLE-AES" in manifest:
return ContentType.DRM_PROTECTED
return ContentType.CLEAR
This runs in milliseconds and prevents wasting bandwidth on content that needs the heavy pipeline.
The DRM Pipeline: pywidevine + N_m3u8DL-RE
For DRM-protected content, the pipeline is:
- License acquisition — use the
_vrt_drm_license_urlwith a Widevine CDM (via pywidevine) to request a license - Key extraction — parse the license response for the content decryption key
- Download with decryption — feed the key to N_m3u8DL-RE, which handles segment decryption on the fly
# What the downloader runs under the hood
N_m3u8DL-RE "https://vrt.be/manifest.m3u8" \
--key "1234567890abcdef:fedcba0987654321" \
--save-dir "/downloads" \
--ffmpeg-binary-path "/usr/bin/ffmpeg"
The Raspberry Pi 5's 4 ARM Cortex-A76 cores handle this comfortably — a 45-minute episode takes ~3 minutes end-to-end.
Graceful Fallback: The _nodrm_ Path
Here's the part I'm proud of: non-DRM content skips the entire decryption stack. No CDM initialization, no license request, no N_m3u8DL-RE overhead. Just a direct ffmpeg -i <hls_url> -c copy <output> — completes in seconds.
The watchlist feature (see below) automatically routes each episode to the right path based on the detection result. You configure shows, not pipelines.
Watchlist Magic: Set It and Forget It
The real UX win is the watchlist system. Create a text file:
# watchlists/tv.txt
[daily]
Het Journaal
Terzake
[weekly]
De Slimste Mens
Villa Politica
Run ./thuis.sh --watchlist watchlists/tv.txt via cron (or systemd timer) and it:
- Checks for new episodes of each show
- Classifies each episode (DRM vs clear)
- Downloads using the appropriate pipeline
- Respects --dry-run for testing
The [daily] / [weekly] directives control scheduling granularity — no external scheduler needed.
What I Learned: The Reality of CDM Licensing
Three hard-won lessons:
-
CDM licensing is fragile — Widevine CDMs have device limits and expiration. The downloader caches licenses and retries automatically, but some episodes simply won't play if the CDM is revoked.
-
Not all episodes are equal — Even within a series, some episodes are DRM-protected and others aren't (e.g., news vs drama). The per-episode detection handles this automatically.
-
Testing discipline pays off — 3,800+ lines of tests cover the detection logic, mock GraphQL responses, HLS manifest parsing, and end-to-end download flows. Every bug found in production became a regression test.
The Result
What started as "can I download De Slimste Mens for offline viewing?" became a robust, self-hosted pipeline that: - Runs on a €80 Raspberry Pi 5 - Handles both DRM and non-DRM content transparently - Schedules via simple watchlist files - Produces standard MP4/MKV files playable anywhere
The code lives in ~/dev/06-apps-thuis-v4 (submodule in the home-lab monorepo). The watchlist feature is documented in watchlists/README.md.
This post is based on commits dc1f9d6 (DRM detection), 24403d2 (pywidevine integration), bb4036e (watchlist scheduler), and 7bafb52 (fallback optimization) in the thuis-v4 repository. All commits and test runs are verifiable in the ~/dev git history.