riplex 0.6.3__tar.gz → 0.6.4__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.
- {riplex-0.6.3 → riplex-0.6.4}/.github/workflows/release.yml +5 -0
- {riplex-0.6.3 → riplex-0.6.4}/PKG-INFO +1 -1
- {riplex-0.6.3 → riplex-0.6.4}/docs/changelog.md +5 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/disc/provider.py +91 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex.egg-info/PKG-INFO +1 -1
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/release.py +157 -12
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_disc_provider.py +43 -0
- {riplex-0.6.3 → riplex-0.6.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/.github/copilot-instructions.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/.github/workflows/publish.yml +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/.gitignore +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/.vscode/settings.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/CONTRIBUTORS.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/LICENSE +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/README.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/architecture.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/getting-started/configuration.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/getting-started/installation.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/guide/lookup.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/guide/orchestrate.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/guide/organize.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/guide/workflow.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/index.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/naming-rules.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/reference/cli.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/docs/troubleshooting.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/issues/debug-artifacts-consolidation.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/issues/planned-features.md +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/mkdocs.yml +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/pyproject.toml +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/setup.cfg +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/cache.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/config.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/dedup.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/detect.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/formatter.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/lookup.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/manifest.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/matcher.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/models.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/normalize.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/organizer.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/scanner.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/snapshot.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/splitter.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/tagger.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/title.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/ui.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex/updater.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex.egg-info/SOURCES.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex.egg-info/requires.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/bug_report.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/crash_dump.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/keep_awake.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/main.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/progress.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/update.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_app/screens/welcome.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/src/riplex_cli/main.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/__init__.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_cache.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_cli_utils.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_config.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_dedup.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_detect.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_disc_analysis.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_disc_detection_screen.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_formatter.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_makemkv.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_matcher.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_normalize.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_organizer.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_planner.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_rip_guide.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_scanner.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_snapshot.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_splitter.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_tagger.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_ui.py +0 -0
- {riplex-0.6.3 → riplex-0.6.4}/tests/test_updater.py +0 -0
|
@@ -164,6 +164,11 @@ jobs:
|
|
|
164
164
|
- name: Create GitHub Release
|
|
165
165
|
uses: softprops/action-gh-release@v2
|
|
166
166
|
with:
|
|
167
|
+
# Auto-generate notes from commits and PRs since the previous tag,
|
|
168
|
+
# then prepend the tag annotation (if any) as a header. Using just
|
|
169
|
+
# the tag annotation produces single-line releases when the tag
|
|
170
|
+
# was created with `git tag -a -m "<subject>"` and no body.
|
|
171
|
+
generate_release_notes: true
|
|
167
172
|
body: ${{ steps.tag_notes.outputs.body }}
|
|
168
173
|
files: |
|
|
169
174
|
release/riplex-windows.exe
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
10
10
|
|
|
11
11
|
- Bumped `dvdcompare-scraper` pin to `>=0.1.15`, which adds quoted-title disc-header parsing. Boxsets such as *Back to the Future 40th Anniversary Trilogy* that glue six physical discs into a single dvdcompare disc entry are now split correctly into one PlannedDisc per physical disc.
|
|
12
12
|
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Disc Release screen: "View on dvdcompare.net" link showing the auto-selected film page so users can verify region/edition before committing to a long rip.
|
|
16
|
+
- Disc Release screen: manual film-id override input. Paste either a bare fid (e.g. `55540`) or a full URL (`https://www.dvdcompare.net/comparisons/film.php?fid=55540`) and riplex will fetch and use that film page instead. The chosen fid is persisted per `(title, disc_format)` so swapping discs in the same box set keeps the override, with a "Clear saved override" affordance.
|
|
17
|
+
|
|
13
18
|
## 2026-05-12
|
|
14
19
|
|
|
15
20
|
### Added
|
|
@@ -208,6 +208,82 @@ class DiscProvider:
|
|
|
208
208
|
# in screens / external code may still use this; prefer ``fetch_film``.
|
|
209
209
|
_fetch_film_cached = fetch_film
|
|
210
210
|
|
|
211
|
+
async def fetch_film_by_id(self, film_id: int) -> FilmComparison:
|
|
212
|
+
"""Fetch a specific dvdcompare film page by its film id.
|
|
213
|
+
|
|
214
|
+
Used by the UI's manual-override path when our auto-ranking picks
|
|
215
|
+
the wrong film page. Cached on success; HTTP/network failures
|
|
216
|
+
propagate as LookupError after caching a short-TTL negative entry.
|
|
217
|
+
"""
|
|
218
|
+
cache_key = cache.hash_key(f"film-by-id|{film_id}")
|
|
219
|
+
|
|
220
|
+
cached = cache.cache_get(self.cache_ns, cache_key, ttl_days=self.ttl_days)
|
|
221
|
+
if cached is not None and not cached.get("_negative"):
|
|
222
|
+
log.debug("dvdcompare film-by-id cache hit for %s", film_id)
|
|
223
|
+
return _dict_to_film(cached)
|
|
224
|
+
|
|
225
|
+
url = film_url(film_id)
|
|
226
|
+
try:
|
|
227
|
+
film = await _throttled_get_film_by_url(url)
|
|
228
|
+
except httpx.HTTPStatusError as exc:
|
|
229
|
+
status = exc.response.status_code if exc.response is not None else "?"
|
|
230
|
+
log.warning("dvdcompare HTTP %s fetching fid=%s", status, film_id)
|
|
231
|
+
raise LookupError(
|
|
232
|
+
f"dvdcompare returned HTTP {status} for fid={film_id}"
|
|
233
|
+
) from exc
|
|
234
|
+
except httpx.RequestError as exc:
|
|
235
|
+
log.warning("dvdcompare network error fetching fid=%s: %s", film_id, exc)
|
|
236
|
+
raise LookupError(
|
|
237
|
+
f"dvdcompare network error for fid={film_id}: {exc}"
|
|
238
|
+
) from exc
|
|
239
|
+
|
|
240
|
+
if not film.releases:
|
|
241
|
+
raise LookupError(f"dvdcompare film {film_id} has no releases")
|
|
242
|
+
|
|
243
|
+
cache.cache_set(self.cache_ns, cache_key, dataclasses.asdict(film))
|
|
244
|
+
return film
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Public URL / fid helpers
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
from dvdcompare.scraper import BASE_URL as _DVDCOMPARE_BASE_URL # noqa: E402
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def film_url(film_id: int | str) -> str:
|
|
255
|
+
"""Return the public dvdcompare comparison URL for a film id."""
|
|
256
|
+
return f"{_DVDCOMPARE_BASE_URL}/comparisons/film.php?fid={film_id}"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
_FID_RE = re.compile(r"(?:fid=|film\.php\?fid=|^)(\d+)")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def parse_film_id(value: str) -> int | None:
|
|
263
|
+
"""Extract a numeric film id from a bare number or a dvdcompare URL.
|
|
264
|
+
|
|
265
|
+
Returns ``None`` if no fid can be parsed.
|
|
266
|
+
|
|
267
|
+
Accepted forms:
|
|
268
|
+
- ``"55540"``
|
|
269
|
+
- ``"fid=55540"``
|
|
270
|
+
- ``"https://www.dvdcompare.net/comparisons/film.php?fid=55540"``
|
|
271
|
+
- ``"https://www.dvdcompare.net/comparisons/film.php?fid=55540#2"``
|
|
272
|
+
"""
|
|
273
|
+
if not value:
|
|
274
|
+
return None
|
|
275
|
+
s = value.strip()
|
|
276
|
+
if not s:
|
|
277
|
+
return None
|
|
278
|
+
# Pure digits
|
|
279
|
+
if s.isdigit():
|
|
280
|
+
return int(s)
|
|
281
|
+
m = _FID_RE.search(s)
|
|
282
|
+
if m:
|
|
283
|
+
return int(m.group(1))
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
211
287
|
|
|
212
288
|
# ---------------------------------------------------------------------------
|
|
213
289
|
# Internal helpers: negative cache + throttle
|
|
@@ -284,6 +360,21 @@ async def _throttled_find_film(
|
|
|
284
360
|
_last_request_at = time.monotonic()
|
|
285
361
|
|
|
286
362
|
|
|
363
|
+
async def _throttled_get_film_by_url(url: str) -> FilmComparison:
|
|
364
|
+
"""Call ``get_film_by_url`` under the same throttle as ``find_film``."""
|
|
365
|
+
global _last_request_at
|
|
366
|
+
async with _request_lock:
|
|
367
|
+
now = time.monotonic()
|
|
368
|
+
wait = _MIN_INTERVAL_S - (now - _last_request_at)
|
|
369
|
+
if wait > 0:
|
|
370
|
+
log.debug("dvdcompare throttle: waiting %.2fs before request", wait)
|
|
371
|
+
await asyncio.sleep(wait)
|
|
372
|
+
try:
|
|
373
|
+
return await get_film_by_url(url)
|
|
374
|
+
finally:
|
|
375
|
+
_last_request_at = time.monotonic()
|
|
376
|
+
|
|
377
|
+
|
|
287
378
|
async def _find_film_prefer_format(
|
|
288
379
|
title: str, disc_format: str, year: int | None
|
|
289
380
|
) -> FilmComparison:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import threading
|
|
5
|
+
import webbrowser
|
|
5
6
|
|
|
6
7
|
import flet as ft
|
|
7
8
|
|
|
9
|
+
from riplex import cache as _cache
|
|
8
10
|
from riplex.disc.provider import DiscProvider, _convert_release
|
|
9
|
-
from riplex.disc.provider import detect_disc_format, score_releases
|
|
11
|
+
from riplex.disc.provider import detect_disc_format, film_url, parse_film_id, score_releases
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_OVERRIDE_CACHE_NS = "dvdcompare_film_id_override"
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class ReleaseScreen:
|
|
@@ -14,6 +19,8 @@ class ReleaseScreen:
|
|
|
14
19
|
self.app = app
|
|
15
20
|
self.film_comparison = None
|
|
16
21
|
self._search_field: ft.TextField | None = None
|
|
22
|
+
self._fid_field: ft.TextField | None = None
|
|
23
|
+
self._fid_error: ft.Text | None = None
|
|
17
24
|
|
|
18
25
|
@property
|
|
19
26
|
def _next_screen(self) -> str:
|
|
@@ -73,6 +80,114 @@ class ReleaseScreen:
|
|
|
73
80
|
self.app.state.pop("_dvdcompare_error", None)
|
|
74
81
|
self.app.navigate("release")
|
|
75
82
|
|
|
83
|
+
# -- film URL / manual fid override UI --------------------------------
|
|
84
|
+
|
|
85
|
+
def _build_film_link(self, film) -> ft.Control | None:
|
|
86
|
+
"""Build a "View on dvdcompare.net" button for the current film."""
|
|
87
|
+
if film is None or not getattr(film, "film_id", None):
|
|
88
|
+
return None
|
|
89
|
+
url = film_url(film.film_id)
|
|
90
|
+
|
|
91
|
+
def _open(_e, _url=url):
|
|
92
|
+
webbrowser.open(_url)
|
|
93
|
+
|
|
94
|
+
return ft.Row(
|
|
95
|
+
[
|
|
96
|
+
ft.Icon(ft.Icons.OPEN_IN_NEW, size=16, color=ft.Colors.BLUE),
|
|
97
|
+
ft.TextButton(
|
|
98
|
+
f"View on dvdcompare.net (fid={film.film_id})",
|
|
99
|
+
on_click=_open,
|
|
100
|
+
tooltip=url,
|
|
101
|
+
),
|
|
102
|
+
],
|
|
103
|
+
spacing=4,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _build_fid_override_section(self) -> ft.Control:
|
|
107
|
+
"""Editable manual film-id / URL override input."""
|
|
108
|
+
self._fid_field = ft.TextField(
|
|
109
|
+
label="Manual override (paste dvdcompare URL or fid)",
|
|
110
|
+
hint_text="e.g. 55540 or https://www.dvdcompare.net/comparisons/film.php?fid=55540",
|
|
111
|
+
expand=True,
|
|
112
|
+
on_submit=self._on_fid_override_submit,
|
|
113
|
+
)
|
|
114
|
+
self._fid_error = ft.Text("", size=12, color=ft.Colors.RED, visible=False)
|
|
115
|
+
persisted = self._load_persisted_override()
|
|
116
|
+
hint_controls = []
|
|
117
|
+
if persisted is not None:
|
|
118
|
+
hint_controls.append(
|
|
119
|
+
ft.Row([
|
|
120
|
+
ft.Text(
|
|
121
|
+
f"Currently using saved override fid={persisted}.",
|
|
122
|
+
size=12,
|
|
123
|
+
color=ft.Colors.GREY_500,
|
|
124
|
+
),
|
|
125
|
+
ft.TextButton(
|
|
126
|
+
"Clear saved override",
|
|
127
|
+
on_click=self._on_clear_override,
|
|
128
|
+
),
|
|
129
|
+
], spacing=10),
|
|
130
|
+
)
|
|
131
|
+
return ft.Container(
|
|
132
|
+
content=ft.Column(
|
|
133
|
+
[
|
|
134
|
+
ft.Text(
|
|
135
|
+
"Wrong film? Open dvdcompare.net, find the right page, "
|
|
136
|
+
"and paste its URL or fid here:",
|
|
137
|
+
size=12,
|
|
138
|
+
color=ft.Colors.GREY_500,
|
|
139
|
+
),
|
|
140
|
+
ft.Row(
|
|
141
|
+
[
|
|
142
|
+
self._fid_field,
|
|
143
|
+
ft.ElevatedButton(
|
|
144
|
+
"Use this",
|
|
145
|
+
icon=ft.Icons.CHECK,
|
|
146
|
+
on_click=self._on_fid_override_submit,
|
|
147
|
+
),
|
|
148
|
+
],
|
|
149
|
+
spacing=10,
|
|
150
|
+
),
|
|
151
|
+
self._fid_error,
|
|
152
|
+
*hint_controls,
|
|
153
|
+
],
|
|
154
|
+
spacing=6,
|
|
155
|
+
),
|
|
156
|
+
padding=ft.Padding(left=0, top=8, right=0, bottom=8),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _on_fid_override_submit(self, _e):
|
|
160
|
+
if self._fid_field is None:
|
|
161
|
+
return
|
|
162
|
+
raw = (self._fid_field.value or "").strip()
|
|
163
|
+
fid = parse_film_id(raw)
|
|
164
|
+
if fid is None:
|
|
165
|
+
if self._fid_error is not None:
|
|
166
|
+
self._fid_error.value = (
|
|
167
|
+
"Couldn't parse a film id from that input. "
|
|
168
|
+
"Expected a number like 55540 or a "
|
|
169
|
+
"dvdcompare.net/comparisons/film.php?fid=... URL."
|
|
170
|
+
)
|
|
171
|
+
self._fid_error.visible = True
|
|
172
|
+
self.app.page.update()
|
|
173
|
+
return
|
|
174
|
+
# Stash for the next lookup pass and trigger a refresh.
|
|
175
|
+
self.app.state["_dvdcompare_film_id_override"] = fid
|
|
176
|
+
self.app.state["release"] = None
|
|
177
|
+
self.app.state["dvdcompare_discs"] = []
|
|
178
|
+
self.app.state.pop("_dvdcompare_film", None)
|
|
179
|
+
self.app.state.pop("_dvdcompare_error", None)
|
|
180
|
+
self.app.navigate("release")
|
|
181
|
+
|
|
182
|
+
def _on_clear_override(self, _e):
|
|
183
|
+
self._clear_persisted_override()
|
|
184
|
+
self.app.state["release"] = None
|
|
185
|
+
self.app.state["dvdcompare_discs"] = []
|
|
186
|
+
self.app.state.pop("_dvdcompare_film", None)
|
|
187
|
+
self.app.state.pop("_dvdcompare_error", None)
|
|
188
|
+
self.app.navigate("release")
|
|
189
|
+
|
|
190
|
+
|
|
76
191
|
def build(self) -> ft.Control:
|
|
77
192
|
title = self._current_search_title()
|
|
78
193
|
|
|
@@ -166,18 +281,27 @@ class ReleaseScreen:
|
|
|
166
281
|
value=default_value,
|
|
167
282
|
)
|
|
168
283
|
|
|
284
|
+
film_link = self._build_film_link(self.film_comparison)
|
|
285
|
+
header_children = [
|
|
286
|
+
ft.Text("Disc Release", size=24, weight=ft.FontWeight.BOLD),
|
|
287
|
+
ft.Text(
|
|
288
|
+
"Multiple releases were found on dvdcompare.net. Pick the one "
|
|
289
|
+
"that matches your physical disc (region, edition, distributor).",
|
|
290
|
+
size=13,
|
|
291
|
+
color=ft.Colors.GREY_500,
|
|
292
|
+
),
|
|
293
|
+
]
|
|
294
|
+
if film_link is not None:
|
|
295
|
+
header_children.append(film_link)
|
|
296
|
+
header_children.append(ft.Divider(height=20))
|
|
297
|
+
|
|
169
298
|
return ft.Column(
|
|
170
299
|
[
|
|
171
|
-
|
|
172
|
-
ft.Text(
|
|
173
|
-
"Multiple releases were found on dvdcompare.net. Pick the one "
|
|
174
|
-
"that matches your physical disc (region, edition, distributor).",
|
|
175
|
-
size=13,
|
|
176
|
-
color=ft.Colors.GREY_500,
|
|
177
|
-
),
|
|
178
|
-
ft.Divider(height=20),
|
|
300
|
+
*header_children,
|
|
179
301
|
ft.Text("Select your disc release:", size=14),
|
|
180
302
|
self.release_radio_group,
|
|
303
|
+
ft.Divider(height=20),
|
|
304
|
+
self._build_fid_override_section(),
|
|
181
305
|
ft.Container(expand=True),
|
|
182
306
|
ft.Row([
|
|
183
307
|
ft.TextButton("Back", on_click=lambda _: self.app.navigate("metadata")),
|
|
@@ -212,6 +336,8 @@ class ReleaseScreen:
|
|
|
212
336
|
ft.Divider(height=20),
|
|
213
337
|
self._build_search_bar(title, searching=False),
|
|
214
338
|
ft.Text(msg, size=14, color=ft.Colors.ORANGE),
|
|
339
|
+
ft.Divider(height=20),
|
|
340
|
+
self._build_fid_override_section(),
|
|
215
341
|
ft.Container(expand=True),
|
|
216
342
|
ft.Row([
|
|
217
343
|
ft.TextButton("Back", on_click=lambda _: self.app.navigate("metadata")),
|
|
@@ -238,11 +364,30 @@ class ReleaseScreen:
|
|
|
238
364
|
try:
|
|
239
365
|
disc_format = self._detect_disc_format()
|
|
240
366
|
year = tmdb_match.year if tmdb_match else None
|
|
241
|
-
log.info("dvdcompare lookup: title=%r format=%r year=%r", title, disc_format, year)
|
|
242
367
|
provider = DiscProvider()
|
|
243
|
-
|
|
244
|
-
|
|
368
|
+
|
|
369
|
+
# Per-session manual override (just submitted).
|
|
370
|
+
session_fid = self.app.state.pop("_dvdcompare_film_id_override", None)
|
|
371
|
+
# Persisted override (set on a previous run for this title/format).
|
|
372
|
+
persisted_fid = self._load_persisted_override() if session_fid is None else None
|
|
373
|
+
override_fid = session_fid if session_fid is not None else persisted_fid
|
|
374
|
+
|
|
375
|
+
if override_fid is not None:
|
|
376
|
+
log.info("dvdcompare lookup: using film id override fid=%s (source=%s)",
|
|
377
|
+
override_fid, "session" if session_fid is not None else "persisted")
|
|
378
|
+
film = asyncio.run(provider.fetch_film_by_id(override_fid))
|
|
379
|
+
# Persist on success so subsequent navigations / disc swaps
|
|
380
|
+
# auto-use the same fid.
|
|
381
|
+
if session_fid is not None:
|
|
382
|
+
self._save_persisted_override(override_fid)
|
|
383
|
+
else:
|
|
384
|
+
log.info("dvdcompare lookup: title=%r format=%r year=%r",
|
|
385
|
+
title, disc_format, year)
|
|
386
|
+
film = asyncio.run(provider.fetch_film(title, disc_format, year=year))
|
|
387
|
+
|
|
388
|
+
log.info("dvdcompare lookup: found %r (fid=%s, %d releases)",
|
|
245
389
|
film.title if film else None,
|
|
390
|
+
film.film_id if film else None,
|
|
246
391
|
len(film.releases) if film else 0)
|
|
247
392
|
self.app.state["_dvdcompare_film"] = film
|
|
248
393
|
except Exception as exc:
|
|
@@ -253,3 +253,46 @@ class TestBoxsetWithQuotedTitleDiscs:
|
|
|
253
253
|
assert planned[5].is_film is True
|
|
254
254
|
assert planned[6].is_film is False
|
|
255
255
|
assert planned[7].is_film is False
|
|
256
|
+
|
|
257
|
+
class TestFilmUrl:
|
|
258
|
+
def test_film_url_int(self):
|
|
259
|
+
from riplex.disc.provider import film_url
|
|
260
|
+
assert film_url(55540) == "https://www.dvdcompare.net/comparisons/film.php?fid=55540"
|
|
261
|
+
|
|
262
|
+
def test_film_url_str(self):
|
|
263
|
+
from riplex.disc.provider import film_url
|
|
264
|
+
assert film_url("55540") == "https://www.dvdcompare.net/comparisons/film.php?fid=55540"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestParseFilmId:
|
|
268
|
+
def test_bare_digits(self):
|
|
269
|
+
from riplex.disc.provider import parse_film_id
|
|
270
|
+
assert parse_film_id("55540") == 55540
|
|
271
|
+
|
|
272
|
+
def test_full_url(self):
|
|
273
|
+
from riplex.disc.provider import parse_film_id
|
|
274
|
+
assert parse_film_id("https://www.dvdcompare.net/comparisons/film.php?fid=55540") == 55540
|
|
275
|
+
|
|
276
|
+
def test_url_with_anchor(self):
|
|
277
|
+
from riplex.disc.provider import parse_film_id
|
|
278
|
+
assert parse_film_id("https://www.dvdcompare.net/comparisons/film.php?fid=55540#2") == 55540
|
|
279
|
+
|
|
280
|
+
def test_query_fragment(self):
|
|
281
|
+
from riplex.disc.provider import parse_film_id
|
|
282
|
+
assert parse_film_id("fid=12345") == 12345
|
|
283
|
+
|
|
284
|
+
def test_whitespace_stripped(self):
|
|
285
|
+
from riplex.disc.provider import parse_film_id
|
|
286
|
+
assert parse_film_id(" 55540 ") == 55540
|
|
287
|
+
|
|
288
|
+
def test_garbage_returns_none(self):
|
|
289
|
+
from riplex.disc.provider import parse_film_id
|
|
290
|
+
assert parse_film_id("not a film id") is None
|
|
291
|
+
|
|
292
|
+
def test_empty_returns_none(self):
|
|
293
|
+
from riplex.disc.provider import parse_film_id
|
|
294
|
+
assert parse_film_id("") is None
|
|
295
|
+
|
|
296
|
+
def test_whitespace_only_returns_none(self):
|
|
297
|
+
from riplex.disc.provider import parse_film_id
|
|
298
|
+
assert parse_film_id(" ") is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|