A command-line tool for searching and downloading content from MediathekViewWeb (MVW), the index of German public-broadcasting media libraries (ARD, ZDF, WDR, and more). Built for automation: the headline feature is reliable, Plex-friendly season downloads.
Requires Python ≥ 3.11. The distribution is published as mvw-cli (the
PyPI name mvw was already taken); the installed command is mvw.
Install as a standalone tool with uv:
uv tool install mvw-cli # adds the `mvw` command to your PATHOr run it once without installing:
uvx --from mvw-cli mvw search "#Tatort"With pip:
pip install mvw-cliNote: HLS (
.m3u8) downloads require ffmpeg on yourPATH. It is an external (non-Python) dependency and is not installed automatically.
uv sync # create the dev environment
uv run mvw --help # run from the working treeThe query string follows the MediathekViewWeb syntax:
| Prefix | Field searched | Example |
|---|---|---|
! |
channel | !ARD |
# |
topic | #Tatort |
+ |
title | +Schokolade |
* |
description | *Berlin |
| (none) | topic and title | feuer flamme |
>N |
duration > N minutes | >80 |
<N |
duration < N minutes | <10 |
Combination rules:
- Space between different selectors → AND:
!WDR #Tatortmeans channel=WDR AND topic=Tatort. - Same selector repeated → OR:
!ARD !ZDFmeans ARD or ZDF. - Comma within a selector's value → AND of words:
#Olympia,Tokiomatches topic containing both "Olympia" and "Tokio". - No negation operator. Exclusion is done client-side with
--exclude.
Note: the API is case-insensitive and flexible with umlauts (
ö≈oe≈OE).
Search MVW and display a Rich results table.
mvw search QUERY
[--channel C] [--topic T] [--title T] [--description D]
[--min-duration MIN] [--max-duration MAX]
[--sort timestamp|duration|channel] [--order asc|desc]
[--future] [--limit N] [--offset N] [--json]
| Option | Default | Description |
|---|---|---|
--channel |
— | Filter by channel (structured flag, not query syntax) |
--topic |
— | Filter by topic |
--title |
— | Filter by title |
--description |
— | Filter by description |
--min-duration |
— | Minimum duration in minutes |
--max-duration |
— | Maximum duration in minutes |
--sort |
timestamp |
Sort field |
--order |
desc |
Sort order (asc or desc) |
--future |
off | Include not-yet-aired entries |
--limit |
15 | Number of results to fetch |
--offset |
0 | Pagination offset |
--json |
off | Emit raw JSON to stdout (scripting-friendly) |
Example:
mvw search "#Tatort !ARD >80"Search and download matching entries. Run --dry-run first to preview the
exact file tree before downloading anything.
mvw download QUERY
[--channel C] [--topic T] [--title T]
[--min-duration MIN] [--max-duration MAX]
[--season] [--dry-run]
[--resolution low|medium|high|best]
[--output DIR] [-o DIR] [--template STR]
[--exclude TERM ...] [--dedup] [--latest-season]
[--season-number N] [--subtitles] [--force] [--limit N]
| Option | Default | Description |
|---|---|---|
--channel |
— | Filter by channel |
--topic |
— | Filter by topic |
--title |
— | Filter by title |
--min-duration |
— | Minimum duration in minutes |
--max-duration |
— | Maximum duration in minutes |
--season |
off | Group into Plex season folders using S##E## numbering |
--dry-run |
off | Preview the file tree and source URLs; download nothing |
--resolution |
best |
Resolution preference: low, medium, high, or best |
--output, -o |
config default | Output directory |
--template |
Plex default | Custom filename template (see below) |
--exclude |
— | Regex to exclude entries from title/topic/description (repeatable) |
--dedup |
off | Remove near-duplicate entries, keeping the highest-quality copy |
--latest-season |
off | Keep only entries from the highest detected season |
--season-number |
— | Override detected season number |
--subtitles |
off | Also fetch subtitles, saved as <video>.de.<ext> sidecars |
--limit |
200 | Maximum number of entries to resolve |
--force |
off | Redownload every matched entry even if a valid file already exists |
By default, download checks each target path before fetching:
- an existing file is checked by probing the actual size of the version that
would be downloaded (an HTTP HEAD, falling back to a ranged request) and
comparing it to the file on disk; a match →
✓ … already exists, skipped, a mismatch →⚠ … corrupted (size mismatch), redownloading; - if the size cannot be determined (or for HLS
.m3u8, which has no single size), any non-empty existing file is treated as complete.
Each fresh download is verified against the server's Content-Length before
the temporary .part file is promoted to its final name (this now applies to
HLS too, which is muxed to a .part and promoted only after ffmpeg succeeds),
so an interrupted transfer never leaves a corrupt final file. A run ends with a
summary line, e.g. Done: 8 downloaded, 5 skipped, 2 redownloaded, 1 failed.
The default template produces Plex/Jellyfin-compatible paths:
{series} ({year})/Season {s:02d}/{series} ({year}) - s{s:02d}e{e:02d} - {ep_title} [{res}].{ext}
Override with --template. Available tokens:
| Token | Value |
|---|---|
{series} |
Topic (show name) |
{year} |
Broadcast year |
{s} |
Season number (supports :02d formatting) |
{e} |
Episode number (supports :02d formatting) |
{ep_title} |
Cleaned episode title |
{res} |
Resolution label (see note below) |
{channel} |
Broadcaster |
{date} |
Broadcast date (YYYY-MM-DD) |
{ext} |
File extension |
Season/episode numbers are parsed from titles (S07/E05, Staffel 7 … Folge 5, Folge 5, trailing (5)); entries without a parseable season fall
back to their broadcast year. Entries with no broadcast date at all land in
Season 00 (Plex's "Specials") with {year} and {date} rendered as
unknown — not the epoch year 1970.
{res} label note: MVW exposes only three tiers (low / medium / high),
not measured pixel heights. The {res} token maps these to conventional labels —
high → "1080p", medium → "720p", low → "480p" — because Plex parses these
and they reflect typical public-broadcast encodes. These are labels, not
guarantees of exact resolution.
Some entries serve .m3u8 HLS playlists instead of direct .mp4 files. Those
are downloaded via ffmpeg -i <url> -c copy <dest>. If ffmpeg is not on your
PATH and an HLS entry is encountered, mvw exits with code 4 and prints an
install hint. Install from https://ffmpeg.org/download.html.
# Preview the newest season, no audio description, deduped
mvw download "#Feuer und Flamme" --season --latest-season --dedup \
--exclude Audiodeskription --exclude "Gebärdensprache" \
--output ~/Media/TV --dry-run
# Then download for real in best resolution
mvw download "#Feuer und Flamme" --season --latest-season --dedup \
--exclude Audiodeskription --output ~/Media/TVShow a Rich detail panel for the first match of a query.
mvw info QUERY
Displays: topic, title, description, channel, aired datetime, duration, size, available resolutions with URLs, subtitle URL, website URL, and detected season/episode.
Manage persistent configuration stored in config.toml
(location: platformdirs.user_config_dir("mvw")).
mvw config show # Print the effective config (key = value)
mvw config set KEY VALUE # Write a key to config.toml
mvw config path # Print the path to config.toml
Available keys: download_dir, template, resolution, user_agent,
page_size, request_timeout.
CLI flags always override config file values, which override built-in defaults.
Print the installed version (-V for short).
| Code | Condition |
|---|---|
| 0 | Success or no results |
| 2 | Invalid input or config (bad template, regex, config file/key/value), API error, HTTP error, or network failure after retries |
| 4 | HLS entry encountered but ffmpeg is not installed |
| 5 | Partial/interrupted download failure |
Development setup, quality checks, and the release process are documented in CONTRIBUTING.md. Notable changes are tracked in CHANGELOG.md.