| … | … | @@ -619,7 +619,7 @@ TV_GENRES = [ |
| 619 | 619 | |
| 620 | 620 | MANIFEST = { |
| 621 | 621 | "id": "org.tabertron.vsembed", |
| 622 | | "version": "0.30.0", |
| 622 | "version": "0.31.0", |
| 623 | 623 | "name": "Tabertron VsEmbed", |
| 624 | 624 | "description": "Self-hosted extractor with TMDB browse + search, played via vidcore and HdHub", |
| 625 | 625 | "types": ["movie", "series"], |
| … | … | @@ -2544,7 +2544,61 @@ def _subtitle_summary_focused(dest: str, present: list, records: dict, |
| 2544 | 2544 | return head |
| 2545 | 2545 | |
| 2546 | 2546 | |
| 2547 | | def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int) -> None: |
| 2547 | # OpenSubtitles through its stremio addon -- the host Jacktook already uses, no |
| 2548 | # key. Asked only when a copy has NO English after wyzie and the sync: on |
| 2549 | # 2026-10-18 wyzie listed 9 tracks for Hope and 503'd every file, while this |
| 2550 | # had 4 English tracks, each named for its release, all measuring 0.13 s / |
| 2551 | # 1.000 against our copy. Capped, because each one becomes a menu entry. |
| 2552 | OPENSUBS_URL = "https://opensubtitles-v3.strem.io" |
| 2553 | OPENSUBS_MAX_TRACKS = 3 |
| 2554 | |
| 2555 | |
| 2556 | def _opensubs_tracks(kind: str, imdb_id: str, season: int = 0, |
| 2557 | episode: int = 0) -> list[dict]: |
| 2558 | """English tracks in _write_sidecars' shape, or [] -- never an exception.""" |
| 2559 | path = (f"series/{imdb_id}:{season}:{episode}" if kind == "series" |
| 2560 | else f"movie/{imdb_id}") |
| 2561 | try: |
| 2562 | r = requests.get(f"{OPENSUBS_URL}/subtitles/{path}.json", |
| 2563 | timeout=REQUEST_TIMEOUT, headers={"User-Agent": UA}) |
| 2564 | if r.status_code != 200: |
| 2565 | log.info("opensubs: none for %s (HTTP %s)", imdb_id, r.status_code) |
| 2566 | return [] |
| 2567 | subs = r.json().get("subtitles") or [] |
| 2568 | except Exception as exc: |
| 2569 | log.info("opensubs: none for %s (%s)", imdb_id, exc) |
| 2570 | return [] |
| 2571 | english = [{"language": "en", "url": s["url"]} for s in subs |
| 2572 | if isinstance(s, dict) and s.get("lang") == "eng" and s.get("url")] |
| 2573 | log.info("opensubs: %d English track(s) for %s", len(english), imdb_id) |
| 2574 | return english[:OPENSUBS_MAX_TRACKS] |
| 2575 | |
| 2576 | |
| 2577 | def _opensubs_for(kind: str, tmdb_id: str, season: int = 0, |
| 2578 | episode: int = 0) -> list[dict]: |
| 2579 | """OpenSubtitles is keyed by imdb id only. A seam: conftest refuses it, so |
| 2580 | no test reaches TMDB or OpenSubtitles without a stub.""" |
| 2581 | imdb_id = _imdb_id_for(kind, f"tmdb:{tmdb_id}") |
| 2582 | return _opensubs_tracks(kind, imdb_id, season, episode) if imdb_id else [] |
| 2583 | |
| 2584 | |
| 2585 | def _has_english(dest: str) -> bool: |
| 2586 | return any(_sidecar_lang(dest, p) == "en" for p in _sidecars_of(dest)) |
| 2587 | |
| 2588 | |
| 2589 | def _sync_subtitles_safely(dest: str) -> None: |
| 2590 | # Measured against THIS copy's audio, because subtitle tracks are cut for |
| 2591 | # other releases (labs#13). The copy is already published; this only ever |
| 2592 | # replaces or withdraws sidecars, and never raises. |
| 2593 | _job_update(getattr(_job_ctx, "key", None) or "", phase="syncing subtitles") |
| 2594 | try: |
| 2595 | _sync_subtitles(dest) |
| 2596 | except Exception as exc: |
| 2597 | log.warning("subsync: skipped for %s (%s)", os.path.basename(dest), exc) |
| 2598 | |
| 2599 | |
| 2600 | def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int, |
| 2601 | kind: str = "movie") -> None: |
| 2548 | 2602 | """Put whatever subtitles exist beside a published copy. Never raises. |
| 2549 | 2603 | |
| 2550 | 2604 | Called AFTER the copy is complete and published, so there is nothing a |
| … | … | @@ -2557,14 +2611,19 @@ def _fetch_subtitles_for(dest: str, meta: dict, season: int, episode: int) -> No |
| 2557 | 2611 | except Exception as exc: |
| 2558 | 2612 | log.warning("subtitles: skipped for %s (%s)", |
| 2559 | 2613 | os.path.basename(dest), exc) |
| 2560 | | # Measured against THIS copy's audio, because wyzie's tracks are cut for |
| 2561 | | # other releases (labs#13). The copy is already published; this only ever |
| 2562 | | # replaces or withdraws sidecars, and never raises. |
| 2563 | | _job_update(getattr(_job_ctx, "key", None) or "", phase="syncing subtitles") |
| 2614 | _sync_subtitles_safely(dest) |
| 2615 | # Wyzie gave nothing usable in English (none, a 503, or withdrawn by the |
| 2616 | # sync as a different cut): ask the second source, and measure that too. |
| 2564 | 2617 | try: |
| 2565 | | _sync_subtitles(dest) |
| 2618 | if _has_english(dest): |
| 2619 | return |
| 2620 | if not _write_sidecars(dest, _opensubs_for(kind, meta["tmdb_id"], |
| 2621 | season, episode)): |
| 2622 | return |
| 2566 | 2623 | except Exception as exc: |
| 2567 | | log.warning("subsync: skipped for %s (%s)", os.path.basename(dest), exc) |
| 2624 | log.warning("opensubs: skipped for %s (%s)", os.path.basename(dest), exc) |
| 2625 | return |
| 2626 | _sync_subtitles_safely(dest) |
| 2568 | 2627 | |
| 2569 | 2628 | |
| 2570 | 2629 | def _run_ingest(kind: str, ident: str, season: int, episode: int, |
| … | … | @@ -2607,7 +2666,7 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int, |
| 2607 | 2666 | # costs no browser, and one HTTP call does not change that -- while |
| 2608 | 2667 | # skipping it would mean the library only ever gains subtitles for |
| 2609 | 2668 | # things fetched after today. |
| 2610 | | _fetch_subtitles_for(dest, meta, season, episode) |
| 2669 | _fetch_subtitles_for(dest, meta, season, episode, kind=kind) |
| 2611 | 2670 | return "done" |
| 2612 | 2671 | # vidcore first: it is asked by TMDB id, so what it returns IS this title. |
| 2613 | 2672 | # HdHub is the fallback because its answer is matched by filename -- see |
| … | … | @@ -2667,7 +2726,7 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int, |
| 2667 | 2726 | return f"{failure}; hdhub: {why}" |
| 2668 | 2727 | log.info("ingest done: %s in %.0fs (%.2f GB)", key, |
| 2669 | 2728 | time.time() - started, os.path.getsize(dest) / 1e9) |
| 2670 | | _fetch_subtitles_for(dest, meta, season, episode) |
| 2729 | _fetch_subtitles_for(dest, meta, season, episode, kind=kind) |
| 2671 | 2730 | # After the sidecars, so one notification covers the copy and them. |
| 2672 | 2731 | _notify_jellyfin(dest, "Created") |
| 2673 | 2732 | return "done" |