mvw-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Local tooling
13
+ .recall/
mvw_cli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Boettinger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mvw_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: mvw-cli
3
+ Version: 0.1.0
4
+ Summary: Search and download German public-broadcasting media from MediathekViewWeb, with Plex-friendly season downloads.
5
+ Author-email: Max Boettinger <perplexity@bttngr.de>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: ard,cli,download,german,mediathek,mediathekviewweb,plex,television,zdf
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Natural Language :: German
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Multimedia :: Video
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.13
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: platformdirs>=4.2
21
+ Requires-Dist: rich>=13.7
22
+ Requires-Dist: typer>=0.12
23
+ Description-Content-Type: text/markdown
24
+
25
+ # mvw
26
+
27
+ A command-line tool for searching and downloading content from
28
+ [MediathekViewWeb](https://mediathekviewweb.de/) (MVW), the index of German
29
+ public-broadcasting media libraries (ARD, ZDF, WDR, and more). Built for
30
+ automation: the headline feature is reliable, Plex-friendly **season**
31
+ downloads.
32
+
33
+ ## Install
34
+
35
+ Requires Python ≥ 3.13. The distribution is published as **`mvw-cli`** (the
36
+ PyPI name `mvw` was already taken); the installed command is `mvw`.
37
+
38
+ Install as a standalone tool with [uv](https://github.com/astral-sh/uv):
39
+
40
+ ```bash
41
+ uv tool install mvw-cli # adds the `mvw` command to your PATH
42
+ ```
43
+
44
+ Or run it once without installing:
45
+
46
+ ```bash
47
+ uvx --from mvw-cli mvw search "#Tatort"
48
+ ```
49
+
50
+ With pip:
51
+
52
+ ```bash
53
+ pip install mvw-cli
54
+ ```
55
+
56
+ > **Note:** HLS (`.m3u8`) downloads require [ffmpeg](https://ffmpeg.org/download.html)
57
+ > on your `PATH`. It is an external (non-Python) dependency and is not installed
58
+ > automatically.
59
+
60
+ ### From source
61
+
62
+ ```bash
63
+ uv sync # create the dev environment
64
+ uv run mvw --help # run from the working tree
65
+ ```
66
+
67
+ ## Query grammar
68
+
69
+ The query string follows the MediathekViewWeb syntax:
70
+
71
+ | Prefix | Field searched | Example |
72
+ |--------|---------------|---------|
73
+ | `!` | channel | `!ARD` |
74
+ | `#` | topic | `#Tatort` |
75
+ | `+` | title | `+Schokolade` |
76
+ | `*` | description | `*Berlin` |
77
+ | (none) | topic and title | `feuer flamme` |
78
+ | `>N` | duration > N minutes | `>80` |
79
+ | `<N` | duration < N minutes | `<10` |
80
+
81
+ Combination rules:
82
+
83
+ - **Space between different selectors** → AND: `!WDR #Tatort` means channel=WDR
84
+ AND topic=Tatort.
85
+ - **Same selector repeated** → OR: `!ARD !ZDF` means ARD or ZDF.
86
+ - **Comma within a selector's value** → AND of words: `#Olympia,Tokio` matches
87
+ topic containing both "Olympia" and "Tokio".
88
+ - **No negation operator.** Exclusion is done client-side with `--exclude`.
89
+
90
+ > Note: the API is case-insensitive and flexible with umlauts
91
+ > (`ö` ≈ `oe` ≈ `OE`).
92
+
93
+ ## Commands
94
+
95
+ ### `mvw search`
96
+
97
+ Search MVW and display a Rich results table.
98
+
99
+ ```
100
+ mvw search QUERY
101
+ [--channel C] [--topic T] [--title T] [--description D]
102
+ [--min-duration MIN] [--max-duration MAX]
103
+ [--sort timestamp|duration|channel] [--order asc|desc]
104
+ [--future] [--limit N] [--offset N] [--json]
105
+ ```
106
+
107
+ | Option | Default | Description |
108
+ |--------|---------|-------------|
109
+ | `--channel` | — | Filter by channel (structured flag, not query syntax) |
110
+ | `--topic` | — | Filter by topic |
111
+ | `--title` | — | Filter by title |
112
+ | `--description` | — | Filter by description |
113
+ | `--min-duration` | — | Minimum duration in minutes |
114
+ | `--max-duration` | — | Maximum duration in minutes |
115
+ | `--sort` | `timestamp` | Sort field |
116
+ | `--order` | `desc` | Sort order (`asc` or `desc`) |
117
+ | `--future` | off | Include not-yet-aired entries |
118
+ | `--limit` | 15 | Number of results to fetch |
119
+ | `--offset` | 0 | Pagination offset |
120
+ | `--json` | off | Emit raw JSON to stdout (scripting-friendly) |
121
+
122
+ Example:
123
+
124
+ ```bash
125
+ mvw search "#Tatort !ARD >80"
126
+ ```
127
+
128
+ ### `mvw download`
129
+
130
+ Search and download matching entries. Run `--dry-run` first to preview the
131
+ exact file tree before downloading anything.
132
+
133
+ ```
134
+ mvw download QUERY
135
+ [--channel C] [--topic T] [--title T]
136
+ [--min-duration MIN] [--max-duration MAX]
137
+ [--season] [--dry-run]
138
+ [--resolution low|medium|high|best]
139
+ [--output DIR] [-o DIR] [--template STR]
140
+ [--exclude TERM ...] [--dedup] [--latest-season]
141
+ [--season-number N] [--subtitles] [--limit N]
142
+ ```
143
+
144
+ | Option | Default | Description |
145
+ |--------|---------|-------------|
146
+ | `--channel` | — | Filter by channel |
147
+ | `--topic` | — | Filter by topic |
148
+ | `--title` | — | Filter by title |
149
+ | `--min-duration` | — | Minimum duration in minutes |
150
+ | `--max-duration` | — | Maximum duration in minutes |
151
+ | `--season` | off | Group into Plex season folders using `S##E##` numbering |
152
+ | `--dry-run` | off | Preview the file tree and source URLs; download nothing |
153
+ | `--resolution` | `best` | Resolution preference: `low`, `medium`, `high`, or `best` |
154
+ | `--output`, `-o` | config default | Output directory |
155
+ | `--template` | Plex default | Custom filename template (see below) |
156
+ | `--exclude` | — | Regex to exclude entries from title/topic/description (repeatable) |
157
+ | `--dedup` | off | Remove near-duplicate entries, keeping the highest-quality copy |
158
+ | `--latest-season` | off | Keep only entries from the highest detected season |
159
+ | `--season-number` | — | Override detected season number |
160
+ | `--subtitles` | off | Also fetch subtitle files alongside each video |
161
+ | `--limit` | 200 | Maximum number of entries to resolve |
162
+
163
+ #### Filename template
164
+
165
+ The default template produces Plex/Jellyfin-compatible paths:
166
+
167
+ ```
168
+ {series} ({year})/Season {s:02d}/{series} ({year}) - s{s:02d}e{e:02d} - {ep_title} [{res}].{ext}
169
+ ```
170
+
171
+ Override with `--template`. Available tokens:
172
+
173
+ | Token | Value |
174
+ |-------|-------|
175
+ | `{series}` | Topic (show name) |
176
+ | `{year}` | Broadcast year |
177
+ | `{s}` | Season number (supports `:02d` formatting) |
178
+ | `{e}` | Episode number (supports `:02d` formatting) |
179
+ | `{ep_title}` | Cleaned episode title |
180
+ | `{res}` | Resolution label (see note below) |
181
+ | `{channel}` | Broadcaster |
182
+ | `{date}` | Broadcast date (`YYYY-MM-DD`) |
183
+ | `{ext}` | File extension |
184
+
185
+ **`{res}` label note:** MVW exposes only three tiers (`low` / `medium` / `high`),
186
+ not measured pixel heights. The `{res}` token maps these to conventional labels —
187
+ `high → "1080p"`, `medium → "720p"`, `low → "480p"` — because Plex parses these
188
+ and they reflect typical public-broadcast encodes. These are labels, not
189
+ guarantees of exact resolution.
190
+
191
+ #### ffmpeg requirement for HLS
192
+
193
+ Some entries serve `.m3u8` HLS playlists instead of direct `.mp4` files. Those
194
+ are downloaded via `ffmpeg -i <url> -c copy <dest>`. If ffmpeg is not on your
195
+ PATH and an HLS entry is encountered, `mvw` exits with code 4 and prints an
196
+ install hint. Install from <https://ffmpeg.org/download.html>.
197
+
198
+ #### Flagship example: Feuer und Flamme
199
+
200
+ ```bash
201
+ # Preview the newest season, no audio description, deduped
202
+ mvw download "#Feuer und Flamme" --season --latest-season --dedup \
203
+ --exclude Audiodeskription --exclude "Gebärdensprache" \
204
+ --output ~/Media/TV --dry-run
205
+
206
+ # Then download for real in best resolution
207
+ mvw download "#Feuer und Flamme" --season --latest-season --dedup \
208
+ --exclude Audiodeskription --output ~/Media/TV
209
+ ```
210
+
211
+ ### `mvw info`
212
+
213
+ Show a Rich detail panel for the first match of a query.
214
+
215
+ ```
216
+ mvw info QUERY
217
+ ```
218
+
219
+ Displays: topic, title, description, channel, aired datetime, duration, size,
220
+ available resolutions with URLs, subtitle URL, website URL, and detected
221
+ season/episode.
222
+
223
+ ### `mvw config`
224
+
225
+ Manage persistent configuration stored in `config.toml`
226
+ (location: `platformdirs.user_config_dir("mvw")`).
227
+
228
+ ```
229
+ mvw config show # Print the effective config (key = value)
230
+ mvw config set KEY VALUE # Write a key to config.toml
231
+ mvw config path # Print the path to config.toml
232
+ ```
233
+
234
+ Available keys: `download_dir`, `template`, `resolution`, `user_agent`,
235
+ `page_size`, `request_timeout`.
236
+
237
+ CLI flags always override config file values, which override built-in defaults.
238
+
239
+ ## Exit codes
240
+
241
+ | Code | Condition |
242
+ |------|-----------|
243
+ | 0 | Success or no results |
244
+ | 2 | API error (non-null `err`), HTTP error, or network/timeout after retries |
245
+ | 4 | HLS entry encountered but ffmpeg is not installed |
246
+ | 5 | Partial/interrupted download failure |
247
+
248
+ ## Running tests
249
+
250
+ ```bash
251
+ # Unit and mocked tests (default)
252
+ uv run pytest -q
253
+
254
+ # Include the live API test (requires network access)
255
+ uv run pytest -m live tests/test_live.py -v
256
+ ```
@@ -0,0 +1,232 @@
1
+ # mvw
2
+
3
+ A command-line tool for searching and downloading content from
4
+ [MediathekViewWeb](https://mediathekviewweb.de/) (MVW), the index of German
5
+ public-broadcasting media libraries (ARD, ZDF, WDR, and more). Built for
6
+ automation: the headline feature is reliable, Plex-friendly **season**
7
+ downloads.
8
+
9
+ ## Install
10
+
11
+ Requires Python ≥ 3.13. The distribution is published as **`mvw-cli`** (the
12
+ PyPI name `mvw` was already taken); the installed command is `mvw`.
13
+
14
+ Install as a standalone tool with [uv](https://github.com/astral-sh/uv):
15
+
16
+ ```bash
17
+ uv tool install mvw-cli # adds the `mvw` command to your PATH
18
+ ```
19
+
20
+ Or run it once without installing:
21
+
22
+ ```bash
23
+ uvx --from mvw-cli mvw search "#Tatort"
24
+ ```
25
+
26
+ With pip:
27
+
28
+ ```bash
29
+ pip install mvw-cli
30
+ ```
31
+
32
+ > **Note:** HLS (`.m3u8`) downloads require [ffmpeg](https://ffmpeg.org/download.html)
33
+ > on your `PATH`. It is an external (non-Python) dependency and is not installed
34
+ > automatically.
35
+
36
+ ### From source
37
+
38
+ ```bash
39
+ uv sync # create the dev environment
40
+ uv run mvw --help # run from the working tree
41
+ ```
42
+
43
+ ## Query grammar
44
+
45
+ The query string follows the MediathekViewWeb syntax:
46
+
47
+ | Prefix | Field searched | Example |
48
+ |--------|---------------|---------|
49
+ | `!` | channel | `!ARD` |
50
+ | `#` | topic | `#Tatort` |
51
+ | `+` | title | `+Schokolade` |
52
+ | `*` | description | `*Berlin` |
53
+ | (none) | topic and title | `feuer flamme` |
54
+ | `>N` | duration > N minutes | `>80` |
55
+ | `<N` | duration < N minutes | `<10` |
56
+
57
+ Combination rules:
58
+
59
+ - **Space between different selectors** → AND: `!WDR #Tatort` means channel=WDR
60
+ AND topic=Tatort.
61
+ - **Same selector repeated** → OR: `!ARD !ZDF` means ARD or ZDF.
62
+ - **Comma within a selector's value** → AND of words: `#Olympia,Tokio` matches
63
+ topic containing both "Olympia" and "Tokio".
64
+ - **No negation operator.** Exclusion is done client-side with `--exclude`.
65
+
66
+ > Note: the API is case-insensitive and flexible with umlauts
67
+ > (`ö` ≈ `oe` ≈ `OE`).
68
+
69
+ ## Commands
70
+
71
+ ### `mvw search`
72
+
73
+ Search MVW and display a Rich results table.
74
+
75
+ ```
76
+ mvw search QUERY
77
+ [--channel C] [--topic T] [--title T] [--description D]
78
+ [--min-duration MIN] [--max-duration MAX]
79
+ [--sort timestamp|duration|channel] [--order asc|desc]
80
+ [--future] [--limit N] [--offset N] [--json]
81
+ ```
82
+
83
+ | Option | Default | Description |
84
+ |--------|---------|-------------|
85
+ | `--channel` | — | Filter by channel (structured flag, not query syntax) |
86
+ | `--topic` | — | Filter by topic |
87
+ | `--title` | — | Filter by title |
88
+ | `--description` | — | Filter by description |
89
+ | `--min-duration` | — | Minimum duration in minutes |
90
+ | `--max-duration` | — | Maximum duration in minutes |
91
+ | `--sort` | `timestamp` | Sort field |
92
+ | `--order` | `desc` | Sort order (`asc` or `desc`) |
93
+ | `--future` | off | Include not-yet-aired entries |
94
+ | `--limit` | 15 | Number of results to fetch |
95
+ | `--offset` | 0 | Pagination offset |
96
+ | `--json` | off | Emit raw JSON to stdout (scripting-friendly) |
97
+
98
+ Example:
99
+
100
+ ```bash
101
+ mvw search "#Tatort !ARD >80"
102
+ ```
103
+
104
+ ### `mvw download`
105
+
106
+ Search and download matching entries. Run `--dry-run` first to preview the
107
+ exact file tree before downloading anything.
108
+
109
+ ```
110
+ mvw download QUERY
111
+ [--channel C] [--topic T] [--title T]
112
+ [--min-duration MIN] [--max-duration MAX]
113
+ [--season] [--dry-run]
114
+ [--resolution low|medium|high|best]
115
+ [--output DIR] [-o DIR] [--template STR]
116
+ [--exclude TERM ...] [--dedup] [--latest-season]
117
+ [--season-number N] [--subtitles] [--limit N]
118
+ ```
119
+
120
+ | Option | Default | Description |
121
+ |--------|---------|-------------|
122
+ | `--channel` | — | Filter by channel |
123
+ | `--topic` | — | Filter by topic |
124
+ | `--title` | — | Filter by title |
125
+ | `--min-duration` | — | Minimum duration in minutes |
126
+ | `--max-duration` | — | Maximum duration in minutes |
127
+ | `--season` | off | Group into Plex season folders using `S##E##` numbering |
128
+ | `--dry-run` | off | Preview the file tree and source URLs; download nothing |
129
+ | `--resolution` | `best` | Resolution preference: `low`, `medium`, `high`, or `best` |
130
+ | `--output`, `-o` | config default | Output directory |
131
+ | `--template` | Plex default | Custom filename template (see below) |
132
+ | `--exclude` | — | Regex to exclude entries from title/topic/description (repeatable) |
133
+ | `--dedup` | off | Remove near-duplicate entries, keeping the highest-quality copy |
134
+ | `--latest-season` | off | Keep only entries from the highest detected season |
135
+ | `--season-number` | — | Override detected season number |
136
+ | `--subtitles` | off | Also fetch subtitle files alongside each video |
137
+ | `--limit` | 200 | Maximum number of entries to resolve |
138
+
139
+ #### Filename template
140
+
141
+ The default template produces Plex/Jellyfin-compatible paths:
142
+
143
+ ```
144
+ {series} ({year})/Season {s:02d}/{series} ({year}) - s{s:02d}e{e:02d} - {ep_title} [{res}].{ext}
145
+ ```
146
+
147
+ Override with `--template`. Available tokens:
148
+
149
+ | Token | Value |
150
+ |-------|-------|
151
+ | `{series}` | Topic (show name) |
152
+ | `{year}` | Broadcast year |
153
+ | `{s}` | Season number (supports `:02d` formatting) |
154
+ | `{e}` | Episode number (supports `:02d` formatting) |
155
+ | `{ep_title}` | Cleaned episode title |
156
+ | `{res}` | Resolution label (see note below) |
157
+ | `{channel}` | Broadcaster |
158
+ | `{date}` | Broadcast date (`YYYY-MM-DD`) |
159
+ | `{ext}` | File extension |
160
+
161
+ **`{res}` label note:** MVW exposes only three tiers (`low` / `medium` / `high`),
162
+ not measured pixel heights. The `{res}` token maps these to conventional labels —
163
+ `high → "1080p"`, `medium → "720p"`, `low → "480p"` — because Plex parses these
164
+ and they reflect typical public-broadcast encodes. These are labels, not
165
+ guarantees of exact resolution.
166
+
167
+ #### ffmpeg requirement for HLS
168
+
169
+ Some entries serve `.m3u8` HLS playlists instead of direct `.mp4` files. Those
170
+ are downloaded via `ffmpeg -i <url> -c copy <dest>`. If ffmpeg is not on your
171
+ PATH and an HLS entry is encountered, `mvw` exits with code 4 and prints an
172
+ install hint. Install from <https://ffmpeg.org/download.html>.
173
+
174
+ #### Flagship example: Feuer und Flamme
175
+
176
+ ```bash
177
+ # Preview the newest season, no audio description, deduped
178
+ mvw download "#Feuer und Flamme" --season --latest-season --dedup \
179
+ --exclude Audiodeskription --exclude "Gebärdensprache" \
180
+ --output ~/Media/TV --dry-run
181
+
182
+ # Then download for real in best resolution
183
+ mvw download "#Feuer und Flamme" --season --latest-season --dedup \
184
+ --exclude Audiodeskription --output ~/Media/TV
185
+ ```
186
+
187
+ ### `mvw info`
188
+
189
+ Show a Rich detail panel for the first match of a query.
190
+
191
+ ```
192
+ mvw info QUERY
193
+ ```
194
+
195
+ Displays: topic, title, description, channel, aired datetime, duration, size,
196
+ available resolutions with URLs, subtitle URL, website URL, and detected
197
+ season/episode.
198
+
199
+ ### `mvw config`
200
+
201
+ Manage persistent configuration stored in `config.toml`
202
+ (location: `platformdirs.user_config_dir("mvw")`).
203
+
204
+ ```
205
+ mvw config show # Print the effective config (key = value)
206
+ mvw config set KEY VALUE # Write a key to config.toml
207
+ mvw config path # Print the path to config.toml
208
+ ```
209
+
210
+ Available keys: `download_dir`, `template`, `resolution`, `user_agent`,
211
+ `page_size`, `request_timeout`.
212
+
213
+ CLI flags always override config file values, which override built-in defaults.
214
+
215
+ ## Exit codes
216
+
217
+ | Code | Condition |
218
+ |------|-----------|
219
+ | 0 | Success or no results |
220
+ | 2 | API error (non-null `err`), HTTP error, or network/timeout after retries |
221
+ | 4 | HLS entry encountered but ffmpeg is not installed |
222
+ | 5 | Partial/interrupted download failure |
223
+
224
+ ## Running tests
225
+
226
+ ```bash
227
+ # Unit and mocked tests (default)
228
+ uv run pytest -q
229
+
230
+ # Include the live API test (requires network access)
231
+ uv run pytest -m live tests/test_live.py -v
232
+ ```
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "mvw-cli"
3
+ description = "Search and download German public-broadcasting media from MediathekViewWeb, with Plex-friendly season downloads."
4
+ readme = "README.md"
5
+ requires-python = ">=3.13"
6
+ dynamic = ["version"]
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Max Boettinger", email = "perplexity@bttngr.de" }]
10
+ keywords = [
11
+ "mediathek",
12
+ "mediathekviewweb",
13
+ "ard",
14
+ "zdf",
15
+ "german",
16
+ "television",
17
+ "download",
18
+ "cli",
19
+ "plex",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Environment :: Console",
24
+ "Intended Audience :: End Users/Desktop",
25
+ "Natural Language :: German",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Multimedia :: Video",
30
+ "Topic :: Utilities",
31
+ ]
32
+ dependencies = [
33
+ "typer>=0.12",
34
+ "rich>=13.7",
35
+ "httpx>=0.27",
36
+ "platformdirs>=4.2",
37
+ ]
38
+
39
+ [project.scripts]
40
+ mvw = "mvw.cli:app"
41
+
42
+ # Fill these in once the project has a public home, then uncomment.
43
+ # [project.urls]
44
+ # Homepage = "https://github.com/<you>/mvw-cli"
45
+ # Repository = "https://github.com/<you>/mvw-cli"
46
+ # Issues = "https://github.com/<you>/mvw-cli/issues"
47
+
48
+ [dependency-groups]
49
+ dev = [
50
+ "pytest>=8.2",
51
+ "respx>=0.21",
52
+ ]
53
+
54
+ [build-system]
55
+ requires = ["hatchling"]
56
+ build-backend = "hatchling.build"
57
+
58
+ [tool.hatch.version]
59
+ path = "src/mvw/__init__.py"
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["src/mvw"]
63
+
64
+ [tool.hatch.build.targets.sdist]
65
+ include = [
66
+ "src/mvw",
67
+ "tests",
68
+ "README.md",
69
+ "LICENSE",
70
+ ]
71
+
72
+ [tool.pytest.ini_options]
73
+ markers = ["live: hits the real MediathekViewWeb API (opt-in)"]
74
+ addopts = "-m 'not live'"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Iterator
5
+
6
+ import httpx
7
+
8
+ from mvw.models import MediathekResult, QueryInfo, QueryResult
9
+
10
+
11
+ class MediathekError(Exception):
12
+ """Raised on API errors, HTTP failures, or transport errors."""
13
+
14
+
15
+ class MediathekClient:
16
+ def __init__(
17
+ self,
18
+ user_agent: str = "mvw/0.1.0",
19
+ timeout: float = 30.0,
20
+ retries: int = 2,
21
+ base_url: str = "https://mediathekviewweb.de/api/query",
22
+ ) -> None:
23
+ self.user_agent = user_agent
24
+ self.timeout = timeout
25
+ self.retries = retries
26
+ self.base_url = base_url
27
+
28
+ def query(self, payload: dict) -> QueryResult:
29
+ headers = {"Content-Type": "text/plain", "User-Agent": self.user_agent}
30
+ body = json.dumps(payload)
31
+ transport_err: Exception | None = None
32
+ for _ in range(self.retries + 1):
33
+ try:
34
+ resp = httpx.post(
35
+ self.base_url, content=body, headers=headers, timeout=self.timeout
36
+ )
37
+ except httpx.TransportError as exc:
38
+ transport_err = exc
39
+ continue
40
+ if resp.status_code != 200:
41
+ raise MediathekError(f"HTTP {resp.status_code}: {resp.text[:200]}")
42
+ data = resp.json()
43
+ err = data.get("err")
44
+ if err:
45
+ raise MediathekError("; ".join(str(e) for e in err))
46
+ result = data.get("result") or {}
47
+ results = [MediathekResult.from_api(r) for r in result.get("results", [])]
48
+ info = QueryInfo.from_api(result.get("queryInfo", {}))
49
+ return QueryResult(results=results, query_info=info)
50
+ raise MediathekError(f"network error: {transport_err}")
51
+
52
+ def iter_all(
53
+ self, payload: dict, page_size: int = 50, cap: int | None = None
54
+ ) -> Iterator[MediathekResult]:
55
+ offset = int(payload.get("offset", 0))
56
+ yielded = 0
57
+ while True:
58
+ page = dict(payload, offset=offset, size=page_size)
59
+ result = self.query(page)
60
+ if not result.results:
61
+ return
62
+ for row in result.results:
63
+ yield row
64
+ yielded += 1
65
+ if cap is not None and yielded >= cap:
66
+ return
67
+ offset += len(result.results)
68
+ if offset >= result.query_info.total_results:
69
+ return