mvw
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.
Install
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 PATH
Or run it once without installing:
uvx --from mvw-cli mvw search "#Tatort"
With pip:
pip install mvw-cli
Note: HLS (
.m3u8) downloads require ffmpeg on yourPATH. It is an external (non-Python) dependency and is not installed automatically.
From source
uv sync # create the dev environment
uv run mvw --help # run from the working tree
Query grammar
The 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).
Commands
mvw search
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"
mvw download
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 |
Download behavior and verification
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.
Filename template
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.
ffmpeg requirement for HLS
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.
Flagship example: Feuer und Flamme
# 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/TV
mvw info
Show 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.
mvw config
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.
mvw --version
Print the installed version (-V for short).
Exit codes
| 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 |
Contributing and releasing
Development setup, quality checks, and the release process are documented in CONTRIBUTING.md. Notable changes are tracked in CHANGELOG.md.
Metadata
Release files for mvw-cli 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mvw_cli-0.3.0.tar.gz | 33.7 kB | Without provenance | Details for mvw_cli-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mvw_cli-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for mvw_cli-0.3.0-py3-none-any.whl |
Total release size: 61.3 kB
Release files / mvw_cli-0.3.0.tar.gz
| Download URL | mvw_cli-0.3.0.tar.gz |
|---|---|
| Size | 33.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
913356baec67ecd46b30e562e3686e71cb976fededf42dd5e37c79a7fc662ab6
|
|
BLAKE2b-256 checksum How to use checksums |
d405dcb66bbedaa074cd4073382f4ff619d0b9cf361da26b5b6f548da482b17c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / mvw_cli-0.3.0-py3-none-any.whl
| Download URL | mvw_cli-0.3.0-py3-none-any.whl |
|---|---|
| Size | 27.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5914da23341075cb37ff21a6ffcef5af3e8aee54eb83100f57df0097cb7b5846
|
|
BLAKE2b-256 checksum How to use checksums |
0727ffbbf0ae2df2edde0c8cf9de89ebd7669ca7fba628908f506cdacf15a952
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for mvw-cli 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.