OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 11a07e5

HdHub: a second source that hands over files, not a player (0.19.0)
2026-09-28 Cloudflare blocked vidcore's CDN host moon.quietridge.top for "Terms of Service violations" -- the whole host, every master 403 from any referer, vidcore's own player stuck on "LOADING THINGS UP". With vidcore the only live source, nothing outside the library could play (MobLand, Tony). HdHub came out of cinewave's server list: a real Stremio addon returning direct mkv files on Cloudflare R2 and pixeldrain. Plain HTTP, ~10 MB/s, seekable, no browser. Measured before writing a line: - asked by IMDb id it answers in 0.2-0.4s; asked by tmdb: it finds nothing, so tmdb: titles are translated through TMDB external_ids first - only R2/pixeldrain serve bytes to a plain client; hubcloud "10Gbps" rows are HTML/400, so hosts are an allowlist - matching is fuzzy (Tony 2026 returned NCIS: Tony & Ziva), so the title must LEAD the filename, followed by the year (+/-1) or the exact SxxEyy - HEVC/4K/10-bit/DTS rows are refused by name, and ffprobe has the final word: h264 <=1080 with English (or untagged) audio the MP4 can carry - R2 links are pre-signed for 8h from when HdHub cached them, so expiry is read from the URL: 10 min margin to play, 60 min to start a copy Rows: one per resolution, after the local row, before vidcore. The play route probes and falls through to the next file within a 12s budget. /mp4/ "play & keep" now answers with an HdHub file when there is no copy, so rows Jacktook already cached start working too. Library: vidcore is still tried FIRST (identity asserted by TMDB id); on any failure HdHub copies the file -- English audio first and default, since these files flag Hindi -- measured against the source's duration (0.99) and refused unless it runs 85-115% of TMDB's runtime. The registry gains `identity` and ingest_sources() orders by it. conftest.py stubs the HdHub query suite-wide: the first full run found test_vidcore_prefetch reaching the real service through /mp4/. Verified on the Pi through Jacktook, not curl: MobLand S02E01 and Tony (2026) both play h264 with English E-AC3, clock advancing, seek to 50% lands (24:27/48:54, 53:12/1:46), pause works. Suite 339 passed x2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11a07e5a1727a9a555f0a94634bb82b722f6a402
by plankatron <fleeky@gmail.com> · 2026-09-28T22:44:32.000Z

Changed files (3) +1130 -29

addon.py +557 -29
……@@ -4,6 +4,7 @@ with a vsembed.ru/CloudStream Pro fallback (currently broken — Cloudflare Turn
44The fmovies path runs node + Playwright via subprocess to handle the JS-driven extraction.
55"""
66
7import calendar
78import html
89import json
910import logging
……@@ -16,9 +17,10 @@ import tempfile
1617from concurrent.futures import ThreadPoolExecutor
1718import threading
1819import time
20import unicodedata
1921from dataclasses import dataclass, field
2022from typing import Optional
21from urllib.parse import urlparse, unquote, urljoin
23from urllib.parse import urlparse, unquote, urljoin, parse_qs
2224
2325import requests
2426from flask import Flask, jsonify, request, redirect, abort, Response, send_file
……@@ -280,13 +282,17 @@ class Source:
280282 resolve: Optional[object] = None
281283 enabled: bool = True
282284 note: str = ""
285 # How a source knows it has the RIGHT title. "tmdb": it is asked by id, so
286 # identity is asserted. "filename": it answers a search and we match the
287 # file's name, so identity is inferred. The library prefers the former.
288 identity: str = "tmdb"
283289
284290
285291SOURCES: dict[str, Source] = {}
286292
287# A progressive MP4 seeks natively; HLS on this stack does not (that is the
293# A progressive file seeks natively; HLS on this stack does not (that is the
288294# whole subject of #4), so progressive sorts first.
289_CONTAINER_RANK = {"progressive-mp4": 0, "hls-fmp4": 1}
295_CONTAINER_RANK = {"progressive-mp4": 0, "progressive-mkv": 0, "hls-fmp4": 1}
290296
291297
292298def register_source(src: Source) -> None:
……@@ -321,7 +327,19 @@ def ingest_source() -> Optional[Source]:
321327 player, so a browser-only source can never feed the library however good its
322328 API looks.
323329 """
324 return next((s for s in ordered_sources() if s.ingestable), None)
330 return next(iter(ingest_sources()), None)
331
332
333def ingest_sources() -> list[Source]:
334 """Every source that may feed the library, in the order to try them.
335
336 Not the playback order. For playback a progressive file wins because it
337 seeks; for the LIBRARY the question is whether the file is the right title,
338 because a wrongly-named copy is permanent. So a source asked by TMDB id goes
339 before one whose answer we matched by filename, whatever its container.
340 """
341 return sorted((s for s in SOURCES.values() if s.enabled and s.ingestable),
342 key=lambda s: 0 if s.identity == "tmdb" else 1)
325343
326344
327345def extract_streams(imdb_id: str, tv: bool = False, season: int = 0, episode: int = 0) -> list[Stream]:
……@@ -599,9 +617,9 @@ TV_GENRES = [
599617
600618MANIFEST = {
601619 "id": "org.tabertron.vsembed",
602 "version": "0.18.1",
620 "version": "0.19.0",
603621 "name": "Tabertron VsEmbed",
604 "description": "Self-hosted HLS extractor with TMDB browse + search, played via vidcore",
622 "description": "Self-hosted extractor with TMDB browse + search, played via vidcore and HdHub",
605623 "types": ["movie", "series"],
606624 # stream ids: tt (cinemeta-driven), fmv (fmovies-native catalog),
607625 # fxl (flixlab/vidcore-native catalog, TMDB-keyed).
……@@ -2034,10 +2052,8 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int,
20342052 otherwise start against a dead link.
20352053 """
20362054 key = _mp4_key(kind, ident, season, episode, height)
2037 # The registry decides whether ANYTHING may feed the library. Resolution is
2038 # still vidcore's directly, because vidcore is the only ingestable source —
2039 # dispatching through src.resolve buys nothing until there are two, and
2040 # speculative indirection is how the special-casing this replaces began.
2055 # The registry decides whether ANYTHING may feed the library; the order
2056 # below (vidcore, then HdHub) is ingest_sources()'s order, written out.
20412057 if not ingest_source():
20422058 return "failed: no ingestable source"
20432059 # Checked before the resolve, so a full volume costs no browser at all.
……@@ -2064,17 +2080,29 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int,
20642080 # things fetched after today.
20652081 _fetch_subtitles_for(dest, meta, season, episode)
20662082 return "done"
2067 url = _resolve_vidcore(kind, ident, season, episode)
2068 if not url:
2069 return "failed: could not resolve title"
2070 variant = _cap_variant(url, height)
2071 log.info("ingest start: %s -> %s", key, dest)
2083 # vidcore first: it is asked by TMDB id, so what it returns IS this title.
2084 # HdHub is the fallback because its answer is matched by filename -- see
2085 # ingest_sources(). A failure of either kind (no resolve, or a resolve onto
2086 # a CDN that refuses the bytes, as on 2026-09-28) falls through.
20722087 started = time.time()
2073 try:
2074 _remux_to_mp4(variant, dest)
2075 except Exception as exc:
2076 log.warning("ingest failed for %s: %s", key, exc)
2077 return f"failed: {exc}"
2088 failure = None
2089 url = _resolve_vidcore(kind, ident, season, episode)
2090 if url:
2091 variant = _cap_variant(url, height)
2092 log.info("ingest start: %s -> %s", key, dest)
2093 try:
2094 _remux_to_mp4(variant, dest)
2095 except Exception as exc:
2096 log.warning("ingest failed for %s: %s", key, exc)
2097 failure = f"failed: {exc}"
2098 else:
2099 failure = "failed: could not resolve title"
2100 if failure:
2101 started = time.time()
2102 why = _ingest_from_hdhub(kind, ident, season, episode, height, dest, meta)
2103 if why:
2104 log.info("ingest: hdhub fallback for %s declined: %s", key, why)
2105 return f"{failure}; hdhub: {why}"
20782106 log.info("ingest done: %s in %.0fs (%.2f GB)", key,
20792107 time.time() - started, os.path.getsize(dest) / 1e9)
20802108 _fetch_subtitles_for(dest, meta, season, episode)
……@@ -2337,6 +2365,14 @@ def _serve_mp4(kind: str, ident: str, season: int, episode: int):
23372365 # surface as a playback failure while the copy silently proceeded. The same
23382366 # URL serves the local file once it lands, so the row upgrades itself.
23392367 _ensure_ingest(kind, ident, season, episode, height)
2368 # A progressive file first when HdHub has one: it seeks, and it is not on
2369 # vidcore's CDN -- which on 2026-09-28 was banned outright, turning every
2370 # vidcore redirect from here into a dead link.
2371 if _hdhub_enabled():
2372 picked = _hdhub_pick(kind, ident, season, episode, height)
2373 if picked:
2374 log.info("mp4: no copy yet — serving hdhub %s", picked[0].filename)
2375 return redirect(picked[0].url, code=302)
23402376 url = _resolve_vidcore(kind, ident, season, episode,
23412377 wait=VIDCORE_PLAY_DEADLINE)
23422378 if not url:
……@@ -2359,6 +2395,486 @@ def mp4_series(ident: str):
23592395 return _serve_mp4("series", m.group(1), int(m.group(2)), int(m.group(3)))
23602396
23612397
2398# ------------------------------ HdHub (direct files) ------------------------
2399# The second live source, and the first that hands over FILES rather than a
2400# player. Found 2026-09-28 in cinewave's bundle (watch.cinewave.qzz.io), where it
2401# is one of 22 servers and the only one that is not an embed: a real Stremio
2402# addon whose streams are plain mkv links on Cloudflare R2 and pixeldrain.
2403#
2404# It exists because one source is one outage. That evening Cloudflare blocked
2405# vidcore's CDN host `moon.quietridge.top` outright ("restricted ... due to Terms
2406# of Service violations", even on `/`), every master 403'd from every referer,
2407# vidcore's own player sat on "LOADING THINGS UP...", and nothing not already in
2408# the library could play.
2409#
2410# Measured from stortron before any of this was written:
2411# * asked by IMDb id it answers in 0.2-0.4s; asked by `tmdb:` it says "No
2412# streams found" (after up to 21s). So it is always asked by IMDb id.
2413# * R2 and pixeldrain rows answer a plain Range GET with 206, ffprobe reads
2414# them, and they download at ~10 MB/s -- ffmpeg can copy them, which is the
2415# property vidlink never had. Everything else ("10Gbps Download Only" on
2416# hubcloud, hubdrive) is HTML, a 400, or unmeasured, and is not used.
2417# * matching is fuzzy: Tony (2026) came back with "NCIS: Tony & Ziva" rows.
2418# * many rows are HEVC Main10 or 4K, which the Pi's H.264 decoder cannot play.
2419# * dual-audio files flag HINDI as the default track.
2420# * R2 links are pre-signed for 8h from when HdHub CACHED them, not from when
2421# we ask; one handed over at 22:50 UTC expired at 23:25.
2422HDHUB_BASE = ("https://hdhub.thevolecitor.qzz.io/"
2423 # base64 of {"torbox":"unset","qualities":"2160p,1080p,720p",
2424 # "sort":"desc"} -- the configuration cinewave itself sends.
2425 "eyJ0b3Jib3giOiJ1bnNldCIsInF1YWxpdGllcyI6IjIxNjBwLDEwODBwLDcyMHAi"
2426 "LCJzb3J0IjoiZGVzYyJ9")
2427HDHUB_TIMEOUT = 10
2428HDHUB_TTL = 20 * 60
2429# Hosts measured to serve bytes to a plain HTTP client. An allowlist rather than
2430# a blocklist: an unmeasured host is exactly how a dead row gets offered.
2431HDHUB_HOSTS = (".r2.cloudflarestorage.com", ".r2.dev", "pixeldrain.dev",
2432 "pixeldrain.com")
2433# What cinewave filters out too, matched against the row's own text.
2434HDHUB_REJECT = ("10gbps", "hubcloud", "hubcdn", "castle", "file not found")
2435# A link must outlive what it is used for. Pressing play needs minutes; a copy
2436# of a 3.7 GB file at ~10 MB/s needs ~6 minutes plus reconnects, and a reconnect
2437# after expiry is a 403 that leaves a partial behind.
2438HDHUB_PLAY_MARGIN = 10 * 60
2439HDHUB_INGEST_MARGIN = 60 * 60
2440# A filename match is inferred identity. The last check before a file is named
2441# into the library is its length against TMDB's runtime: a different film or
2442# episode is almost never within 15% of this one.
2443HDHUB_RUNTIME_WINDOW = (0.85, 1.15)
2444# The play path has ~20s before Kodi gives up on an open. Probing is ~1-2s a
2445# file, so a handful of tries fits; an unbounded walk down a dead list does not.
2446HDHUB_PICK_BUDGET = 12
2447HDHUB_PICK_TRIES = 4
2448HDHUB_PROBE_TIMEOUT = 15
2449# Audio the MP4 container holds with a stream copy. DTS and TrueHD do not go in
2450# without re-encoding, so a file whose only audio is one of them is refused.
2451MP4_AUDIO = {"aac", "ac3", "eac3", "mp3"}
2452_FOREIGN = ("hindi", "tamil", "telugu", "malayalam", "kannada", "bengali",
2453 "punjabi", "marathi", "dual", "multi")
2454_UNDECODABLE = re.compile(
2455 r"x265|h\.?265|hevc|10.?bit|\bav1\b|\bdts\b|truehd|2160p|\b4k\b|\buhd\b", re.I)
2456
2457
2458@dataclass(frozen=True)
2459class HdHubFile:
2460 url: str
2461 filename: str
2462 height: int
2463 size: int
2464 host: str
2465 english_only: bool
2466
2467
2468_hdhub_cache: dict[tuple, tuple[float, list]] = {}
2469_hdhub_probe_cache: dict[str, tuple[float, Optional[dict]]] = {}
2470_hdhub_pick_cache: dict[tuple, tuple[float, tuple]] = {}
2471
2472
2473def _hdhub_query(kind: str, imdb: str, season: int = 0, episode: int = 0) -> list:
2474 """HdHub's raw stream rows for one title. Raises on any failure."""
2475 key = (kind, imdb, season, episode)
2476 hit = _hdhub_cache.get(key)
2477 if hit and hit[0] > time.time():
2478 return hit[1]
2479 path = (f"series/{imdb}:{season}:{episode}" if kind == "series"
2480 else f"movie/{imdb}")
2481 r = requests.get(f"{HDHUB_BASE}/stream/{path}.json",
2482 headers={"User-Agent": UA}, timeout=HDHUB_TIMEOUT)
2483 r.raise_for_status()
2484 rows = r.json().get("streams") or []
2485 _hdhub_cache[key] = (time.time() + HDHUB_TTL, rows)
2486 return rows
2487
2488
2489def _imdb_id_for(kind: str, ident: str) -> Optional[str]:
2490 """`tt<n>` verbatim, or a `tmdb:<n>` translated through TMDB."""
2491 if ident.startswith("tt"):
2492 return ident
2493 tmdb_id = ident.split(":", 1)[1]
2494 data = _tmdb_get(f"{'tv' if kind == 'series' else 'movie'}/{tmdb_id}/external_ids")
2495 return data.get("imdb_id") or None
2496
2497
2498def _norm(text: str) -> str:
2499 """Lowercase words only: diacritics folded, punctuation to spaces."""
2500 text = unicodedata.normalize("NFKD", text or "")
2501 text = "".join(c for c in text if not unicodedata.combining(c)).lower()
2502 text = text.replace("&", " and ")
2503 return re.sub(r"[^a-z0-9]+", " ", text).strip()
2504
2505
2506def _hdhub_filename(description: str) -> str:
2507 """The file name: first line of the description, minus its [tags]."""
2508 first = (description or "").split("\n", 1)[0]
2509 return re.sub(r"^(\s*\[[^\]]*\])+\s*", "", first).strip()
2510
2511
2512def _hdhub_title_ok(filename: str, title: str, year: str, kind: str,
2513 season: int = 0, episode: int = 0) -> bool:
2514 """Is this file the title asked for?
2515
2516 The title must LEAD the name -- which is what refuses "Ncis.Tony.&.Ziva"
2517 for Tony -- and what follows it must be the year (movies, +/-1) or the exact
2518 SxxEyy (series, optionally after a year). A season pack or a neighbouring
2519 episode is a different file, not a close match.
2520 """
2521 f, t = _norm(filename), _norm(title)
2522 if not t or not f.startswith(t + " "):
2523 return False
2524 rest = f[len(t):].split()
2525 if kind == "series":
2526 tag = f"s{season:02d}e{episode:02d}"
2527 i = 1 if re.fullmatch(r"(19|20)\d\d", rest[0]) else 0
2528 return len(rest) > i and rest[i] == tag
2529 if any(re.fullmatch(r"s\d{1,2}e\d{1,3}.*", tok) for tok in rest):
2530 return False
2531 if not (year and re.fullmatch(r"(19|20)\d\d", rest[0])):
2532 return False
2533 return abs(int(rest[0]) - int(year)) <= 1
2534
2535
2536def _hdhub_height(text: str) -> int:
2537 """Vertical resolution a row declares, or 0. `4KHDHub 1080p` is 1080: the
2538 site's name starts with 4K, the row does not."""
2539 m = re.search(r"(\d{3,4})p\b", text or "", re.I)
2540 if m:
2541 return int(m.group(1))
2542 return 2160 if re.search(r"\b4k\b", text or "", re.I) else 0
2543
2544
2545def _hdhub_decodable(name: str, filename: str) -> bool:
2546 """Could the Pi's H.264 decoder play this, judging by what it says?"""
2547 height = _hdhub_height(name) or _hdhub_height(filename)
2548 if not height or height > VIDCORE_MAX_HEIGHT:
2549 return False
2550 return not _UNDECODABLE.search(filename or "")
2551
2552
2553def _hdhub_languages(filename: str) -> tuple[bool, bool]:
2554 """(has English audio, has ONLY English audio), from the name.
2555
2556 A name that mentions no language at all is taken as English: that is how
2557 ordinary release names read. The probe checks the real tags later.
2558 """
2559 words = _norm(filename).split()
2560 foreign = any(w in words for w in _FOREIGN)
2561 english = "english" in words or "eng" in words
2562 return (english or not foreign, not foreign)
2563
2564
2565def _url_seconds_left(url: str, now: Optional[float] = None) -> Optional[float]:
2566 """Seconds until a pre-signed (AWS-style) URL expires; None if unsigned."""
2567 q = parse_qs(urlparse(url).query)
2568 try:
2569 signed = calendar.timegm(time.strptime(q["X-Amz-Date"][0], "%Y%m%dT%H%M%SZ"))
2570 lifetime = int(q["X-Amz-Expires"][0])
2571 except (KeyError, IndexError, ValueError):
2572 return None
2573 return signed + lifetime - (time.time() if now is None else now)
2574
2575
2576def _hdhub_files(kind: str, ident: str, season: int = 0, episode: int = 0,
2577 max_height: int = VIDCORE_MAX_HEIGHT,
2578 margin: float = HDHUB_PLAY_MARGIN) -> list[HdHubFile]:
2579 """Files HdHub has for this title that we could use, best first.
2580
2581 Never raises: HdHub is a third party, and its being down must cost a row,
2582 not the source list.
2583 """
2584 try:
2585 meta = _library_meta(kind, ident, season, episode)
2586 if not meta:
2587 return []
2588 imdb = _imdb_id_for(kind, ident)
2589 if not imdb:
2590 return []
2591 rows = _hdhub_query(kind, imdb, season, episode)
2592 except Exception as exc:
2593 log.warning("hdhub: lookup failed for %s %s: %s", kind, ident, exc)
2594 return []
2595 files = []
2596 for r in rows:
2597 url = r.get("url") or ""
2598 name = r.get("name") or ""
2599 desc = r.get("description") or ""
2600 if not url or any(x in (name + " " + desc).lower() for x in HDHUB_REJECT):
2601 continue
2602 host = (urlparse(url).hostname or "").lower()
2603 if not any(host == h.lstrip(".") or host.endswith(h) for h in HDHUB_HOSTS):
2604 continue
2605 filename = _hdhub_filename(desc)
2606 if not _hdhub_title_ok(filename, meta["title"], meta["year"], kind,
2607 season, episode):
2608 continue
2609 if not _hdhub_decodable(name, filename):
2610 continue
2611 height = _hdhub_height(name) or _hdhub_height(filename)
2612 if height > max_height:
2613 continue
2614 has_english, english_only = _hdhub_languages(filename)
2615 if not has_english:
2616 continue
2617 left = _url_seconds_left(url)
2618 if left is not None and left < margin:
2619 continue
2620 size = int((r.get("behaviorHints") or {}).get("videoSize") or 0)
2621 files.append(HdHubFile(url=url, filename=filename, height=height,
2622 size=size, host=host, english_only=english_only))
2623 # English-only first (no track juggling needed), then R2 (pixeldrain limits
2624 # hotlinked downloads), then resolution, then size as the quality tiebreak.
2625 files.sort(key=lambda x: (not x.english_only, ".r2." not in x.host + ".",
2626 -x.height, -x.size))
2627 return files
2628
2629
2630def _hdhub_probe(url: str, filename: Optional[str] = None) -> Optional[dict]:
2631 """ffprobe's view of a remote file, or None. Cached, including failures
2632 (briefly), so a dead link is not re-probed on every click."""
2633 hit = _hdhub_probe_cache.get(url)
2634 if hit and hit[0] > time.time():
2635 return hit[1]
2636 result = None
2637 try:
2638 out = subprocess.run(
2639 ["ffprobe", "-v", "error", "-rw_timeout", "10000000",
2640 "-show_entries",
2641 "stream=index,codec_type,codec_name,height:stream_tags=language"
2642 ":stream_disposition=attached_pic:format=duration",
2643 "-of", "json", url],
2644 capture_output=True, text=True, timeout=HDHUB_PROBE_TIMEOUT)
2645 if out.returncode == 0:
2646 result = json.loads(out.stdout)
2647 except Exception as exc:
2648 log.info("hdhub: probe failed for %s (%s)", filename or url[:60], exc)
2649 _hdhub_probe_cache[url] = (time.time() + (HDHUB_TTL if result else 300), result)
2650 return result
2651
2652
2653def _probe_video(p: Optional[dict]) -> Optional[dict]:
2654 """The real video stream -- not the cover art some releases attach."""
2655 for s in (p or {}).get("streams") or []:
2656 if s.get("codec_type") == "video" and not (s.get("disposition") or {}).get("attached_pic"):
2657 return s
2658 return None
2659
2660
2661def _probe_audio(p: Optional[dict]) -> list[dict]:
2662 """Audio streams an MP4 can carry with a stream copy."""
2663 return [s for s in (p or {}).get("streams") or []
2664 if s.get("codec_type") == "audio" and s.get("codec_name") in MP4_AUDIO]
2665
2666
2667def _audio_lang(s: dict) -> str:
2668 return ((s.get("tags") or {}).get("language") or "").lower()
2669
2670
2671def _probe_usable(p: Optional[dict]) -> bool:
2672 """Can the Pi play this file, with English sound? The file decides, not
2673 its name: a name can omit HEVC, the stream cannot."""
2674 v = _probe_video(p)
2675 if not v or v.get("codec_name") != "h264" or (v.get("height") or 0) > 1088:
2676 return False
2677 audio = _probe_audio(p)
2678 if not audio:
2679 return False
2680 tagged = [a for a in audio if _audio_lang(a) not in ("", "und")]
2681 return not tagged or any(_audio_lang(a) in ("eng", "en") for a in tagged)
2682
2683
2684def _hdhub_pick(kind: str, ident: str, season: int = 0, episode: int = 0,
2685 height: int = VIDCORE_MAX_HEIGHT,
2686 margin: float = HDHUB_PLAY_MARGIN) -> Optional[tuple]:
2687 """(file, probe) for the best file that actually checks out, or None."""
2688 key = (kind, ident, season, episode, height)
2689 hit = _hdhub_pick_cache.get(key)
2690 if hit and hit[0] > time.time():
2691 left = _url_seconds_left(hit[1][0].url)
2692 if left is None or left >= margin:
2693 return hit[1]
2694 deadline = time.time() + HDHUB_PICK_BUDGET
2695 for f in _hdhub_files(kind, ident, season, episode, max_height=height,
2696 margin=margin)[:HDHUB_PICK_TRIES]:
2697 if time.time() > deadline:
2698 log.info("hdhub: pick budget spent for %s", ident)
2699 break
2700 p = _hdhub_probe(f.url, f.filename)
2701 if _probe_usable(p):
2702 _hdhub_pick_cache[key] = (time.time() + HDHUB_TTL, (f, p))
2703 return f, p
2704 log.info("hdhub: %s not usable, trying the next", f.filename)
2705 return None
2706
2707
2708_hdhub_prefetch_sem = threading.Semaphore(2)
2709
2710
2711def _hdhub_prefetch(kind: str, ident: str, season: int = 0,
2712 episode: int = 0) -> None:
2713 """Probe the best file while the source list is being read, so pressing
2714 play is a cache hit. Skipped, not queued, when already busy."""
2715 if not _hdhub_prefetch_sem.acquire(blocking=False):
2716 return
2717
2718 def run() -> None:
2719 try:
2720 _hdhub_pick(kind, ident, season, episode)
2721 except Exception as exc:
2722 log.warning("hdhub prefetch failed for %s: %s", ident, exc)
2723 finally:
2724 _hdhub_prefetch_sem.release()
2725
2726 threading.Thread(target=run, name=f"hdhub-{ident}", daemon=True).start()
2727
2728
2729def _hdhub_enabled() -> bool:
2730 src = get_source("hdhub")
2731 return bool(src and src.enabled)
2732
2733
2734def _append_hdhub_rows(out: list, kind: str, ident: Optional[str],
2735 season: int = 0, episode: int = 0) -> None:
2736 """One row per resolution HdHub has a usable-looking file at.
2737
2738 Built from HdHub's listing (0.2-0.4s) without probing: probing is seconds
2739 per file and belongs off the source list. The play route probes and falls
2740 through to the next file, so a row whose top file is bad still plays.
2741 """
2742 if not ident or not _hdhub_enabled():
2743 return
2744 files = _hdhub_files(kind, ident, season, episode)
2745 if not files:
2746 return
2747 try:
2748 _hdhub_prefetch(kind, ident, season, episode)
2749 except Exception as exc: # never let a warm-up break /stream
2750 log.warning("hdhub prefetch could not start: %s", exc)
2751 if kind == "series":
2752 path = f"/hdhub/series/{ident}:{season}:{episode}.mkv"
2753 tag = f"S{season:02d}E{episode:02d} " # Jacktook drops TV rows without it
2754 else:
2755 path = f"/hdhub/movie/{ident}.mkv"
2756 tag = ""
2757 seen = set()
2758 for f in files:
2759 if f.height in seen:
2760 continue
2761 seen.add(f.height)
2762 size = f" · {f.size / 1e9:.1f} GB" if f.size else ""
2763 audio = "English" if f.english_only else "English + other audio"
2764 out.append(_stream_dict(Stream(
2765 url=f"http://{request.host}{path}?h={f.height}",
2766 quality=f"{f.height}p", name="HdHub", referer="",
2767 title_extra=f"{tag}{f.height}p{size}\nHdHub · {audio} · seekable")))
2768
2769
2770def _serve_hdhub(kind: str, ident: str, season: int, episode: int):
2771 height = _requested_height()
2772 local = _local_redirect(kind, ident, season, episode, height)
2773 if local:
2774 return local
2775 picked = _hdhub_pick(kind, ident, season, episode, height)
2776 if not picked:
2777 abort(404)
2778 _ensure_ingest(kind, ident, season, episode, height=height)
2779 if kind == "series":
2780 _queue_next_episode(ident, season, episode, height)
2781 log.info("hdhub: serving %s", picked[0].filename)
2782 return redirect(picked[0].url, code=302)
2783
2784
2785@app.route("/hdhub/movie/<path:ident>.mkv")
2786def hdhub_movie(ident: str):
2787 if not re.fullmatch(VIDCORE_IDENT, ident):
2788 abort(404)
2789 return _serve_hdhub("movie", ident, 0, 0)
2790
2791
2792@app.route("/hdhub/series/<path:ident>.mkv")
2793def hdhub_series(ident: str):
2794 m = re.fullmatch(rf"({VIDCORE_IDENT}):(\d+):(\d+)", ident)
2795 if not m:
2796 abort(404)
2797 return _serve_hdhub("series", m.group(1), int(m.group(2)), int(m.group(3)))
2798
2799
2800def _copy_progressive(url: str, probe: dict, out_path: str) -> None:
2801 """Stream-copy a remote file into a progressive MP4, English audio first.
2802
2803 English goes first AND is flagged default because Kodi plays the default
2804 track, and these dual-audio files flag Hindi. Subtitle streams are not
2805 copied: the library gets wyzie sidecars beside every copy, and SRT does not
2806 stream-copy into MP4. Measured against the source's own duration before it
2807 is published, for the reason _remux_to_mp4 is: ffmpeg exits 0 on an input
2808 that stops early.
2809 """
2810 tmp = _part_path(out_path)
2811 os.makedirs(os.path.dirname(out_path), exist_ok=True)
2812 video = _probe_video(probe)
2813 audio = _probe_audio(probe)
2814 if not video or not audio:
2815 raise RuntimeError("no copyable video/audio in source")
2816 audio = ([a for a in audio if _audio_lang(a) in ("eng", "en")]
2817 + [a for a in audio if _audio_lang(a) not in ("eng", "en")])
2818 cmd = ["ffmpeg", "-v", "error", "-y",
2819 "-rw_timeout", "30000000",
2820 "-reconnect", "1",
2821 "-reconnect_streamed", "1",
2822 "-reconnect_on_network_error", "1",
2823 "-reconnect_delay_max", "30",
2824 "-i", url, "-map", f"0:{video['index']}"]
2825 for a in audio:
2826 cmd += ["-map", f"0:{a['index']}"]
2827 cmd += ["-c", "copy"]
2828 for i in range(len(audio)):
2829 cmd += [f"-disposition:a:{i}", "default" if i == 0 else "0"]
2830 # -f mp4 is REQUIRED, as in _remux_to_mp4: the output is `<name>.part`.
2831 cmd += ["-movflags", "+faststart", "-f", "mp4", tmp]
2832 proc = _run_remux(cmd, tmp, stall_timeout=MP4_STALL_TIMEOUT,
2833 hard_timeout=MP4_REMUX_TIMEOUT)
2834 if proc.returncode != 0 or not os.path.exists(tmp) or os.path.getsize(tmp) == 0:
2835 raise RuntimeError(f"ffmpeg exit {proc.returncode}: {(proc.stderr or '')[-300:]}")
2836 try:
2837 expected = float(((probe or {}).get("format") or {}).get("duration") or 0) or None
2838 except (TypeError, ValueError):
2839 expected = None
2840 if expected:
2841 actual = _media_duration(tmp)
2842 if actual is None or actual < expected * REMUX_MIN_COMPLETENESS:
2843 raise RuntimeError(
2844 f"incomplete copy: got {actual if actual else 0:.0f}s of "
2845 f"{expected:.0f}s — not published")
2846 log.info("copy verified: %.0fs of %.0fs", actual, expected)
2847 _record_verified(out_path, actual, expected)
2848 os.replace(tmp, out_path)
2849
2850
2851def _ingest_from_hdhub(kind: str, ident: str, season: int, episode: int,
2852 height: int, dest: str, meta: dict) -> Optional[str]:
2853 """Copy the title from HdHub into `dest`. None on success, else why not."""
2854 if not _hdhub_enabled():
2855 return "disabled"
2856 picked = _hdhub_pick(kind, ident, season, episode, height,
2857 margin=HDHUB_INGEST_MARGIN)
2858 if not picked:
2859 return "no usable file"
2860 f, p = picked
2861 try:
2862 seconds = float((p.get("format") or {}).get("duration") or 0)
2863 expected = float(meta.get("runtime") or 0) * 60
2864 except (TypeError, ValueError):
2865 seconds = expected = 0
2866 lo, hi = HDHUB_RUNTIME_WINDOW
2867 if seconds and expected and not (lo * expected <= seconds <= hi * expected):
2868 return (f"{f.filename} runs {seconds / 60:.0f} min but TMDB says "
2869 f"{expected / 60:.0f} — not taken as this title")
2870 log.info("ingest start (hdhub): %s -> %s", f.filename, dest)
2871 try:
2872 _copy_progressive(f.url, p, dest)
2873 except Exception as exc:
2874 return str(exc)
2875 return None
2876
2877
23622878def _vidcore_ident_for_fmv(ctype: str, slug: str) -> Optional[str]:
23632879 """fmovies carries no imdb/tmdb id — bridge its slug to TMDB so the (lazy)
23642880 vidcore row can be offered on catalog entries too."""
……@@ -2566,6 +3082,7 @@ def stream_movie(ident: str):
25663082 if fxl: # flixlab-native catalog: vidcore only
25673083 out: list = []
25683084 _append_mp4_row(out, "movie", f"tmdb:{fxl.group(1)}")
3085 _append_hdhub_rows(out, "movie", f"tmdb:{fxl.group(1)}")
25693086 _append_vidcore_row(out, "movie", f"tmdb:{fxl.group(1)}")
25703087 _append_action_rows(out, "movie", f"tmdb:{fxl.group(1)}")
25713088 return jsonify({"streams": out})
……@@ -2588,6 +3105,7 @@ def stream_movie(ident: str):
25883105 log.exception("extract failed for %s: %s", ident, exc)
25893106 streams = []
25903107 out = [_stream_dict(s) for s in streams]
3108 _append_hdhub_rows(out, "movie", ident)
25913109 _append_vidcore_row(out, "movie", ident) # additive gap-filler
25923110 return jsonify({"streams": out})
25933111
……@@ -2599,6 +3117,7 @@ def stream_series(ident: str):
25993117 tmdb, season, episode = fxl.group(1), int(fxl.group(2)), int(fxl.group(3))
26003118 out: list = []
26013119 _append_mp4_row(out, "series", f"tmdb:{tmdb}", season, episode)
3120 _append_hdhub_rows(out, "series", f"tmdb:{tmdb}", season, episode)
26023121 _append_vidcore_row(out, "series", f"tmdb:{tmdb}", season, episode)
26033122 _append_action_rows(out, "series", f"tmdb:{tmdb}", season, episode)
26043123 return jsonify({"streams": out})
……@@ -2635,6 +3154,7 @@ def stream_series(ident: str):
26353154 streams = []
26363155 se_tag = f"S{season:02d}E{episode:02d}"
26373156 out = [_stream_dict(s, se_tag=se_tag) for s in streams]
3157 _append_hdhub_rows(out, "series", imdb_id, season, episode)
26383158 _append_vidcore_row(out, "series", imdb_id, season, episode) # additive
26393159 return jsonify({"streams": out})
26403160
……@@ -2646,19 +3166,17 @@ def _stream_dict(s: Stream, se_tag: str = "") -> dict:
26463166 title = f"{se_tag} {s.quality}\n{s.name}"
26473167 else:
26483168 title = f"{s.quality}\n{s.name}"
3169 hints: dict = {"notWebReady": True}
3170 # No referer, no proxyHeaders: a plain file host needs none, and sending a
3171 # made-up one only gives it something to refuse.
3172 if s.referer:
3173 hints["proxyHeaders"] = {"request": {"User-Agent": UA,
3174 "Referer": s.referer}}
26493175 return {
26503176 "name": s.name,
26513177 "title": title,
26523178 "url": s.url,
2653 "behaviorHints": {
2654 "notWebReady": True,
2655 "proxyHeaders": {
2656 "request": {
2657 "User-Agent": UA,
2658 "Referer": s.referer,
2659 }
2660 },
2661 },
3179 "behaviorHints": hints,
26623180 }
26633181
26643182
……@@ -3195,6 +3713,16 @@ SOURCES_DEFAULT: dict[str, Source] = {s.id: s for s in (
31953713 "or session binding, not headers. Unusable for ingest without "
31963714 "curl-impersonate. Media is HEVC; Pi decode unverified.",
31973715 ),
3716 Source(
3717 id="hdhub", name="HdHub", container="progressive-mkv",
3718 ingestable=True, browser=False, heights=(1080, 720, 480),
3719 resolve=_hdhub_pick, identity="filename",
3720 note="Added 2026-09-28 when Cloudflare banned vidcore's CDN host. A "
3721 "Stremio addon (found in cinewave's server list) returning direct "
3722 "mkv files on R2/pixeldrain: plain HTTP, ~10 MB/s, seekable. Asked "
3723 "by IMDb id (tmdb: finds nothing). Identity is a FILENAME match, "
3724 "so the library tries vidcore first and checks runtime.",
3725 ),
31983726 Source(
31993727 id="fmovies", name="fmovies", container="hls-fmp4",
32003728 ingestable=True, browser=True, enabled=False,
conftest.py +24 -0
……@@ -0,0 +1,24 @@
1"""Suite-wide guards.
2
3HdHub is consulted on the /mp4/ play path and in ingest, so without this any
4test that exercises those paths without thinking about HdHub reaches the real
5service -- on 2026-09-28 test_vidcore_prefetch did exactly that, found a real
6Supergirl file and redirected to it. Unit tests must never reach the network.
7
8A test that wants HdHub installs its own `_hdhub_query` (test_hdhub.py's
9`hdhub` fixture), which replaces this one.
10"""
11
12import pytest
13
14import addon
15
16
17@pytest.fixture(autouse=True)
18def no_hdhub_network(monkeypatch):
19 def refuse(*a, **k):
20 raise RuntimeError("HdHub reached from a test without a stub")
21 monkeypatch.setattr(addon, "_hdhub_query", refuse)
22 monkeypatch.setattr(addon, "_hdhub_cache", {})
23 monkeypatch.setattr(addon, "_hdhub_pick_cache", {})
24 yield
test_hdhub.py +549 -0
……@@ -0,0 +1,549 @@
1"""HdHub: a second source that hands over plain FILES, not a player.
2
32026-09-28, MobLand and Tony would not play. Nothing of ours was broken:
4Cloudflare had blocked vidcore's CDN host `moon.quietridge.top` for "Terms of
5Service violations" -- the whole host, even `/` -- so every master 403'd, from
6any referer, and vidcore's OWN player sat on "LOADING THINGS UP...". With vidcore
7the only live source, that was every title not already in the library.
8
9cinewave (watch.cinewave.qzz.io) lists HdHub among 22 servers, and it is the one
10that is not an embed: a real Stremio addon returning direct mkv links on
11Cloudflare R2 and pixeldrain. Measured the same evening from stortron:
12 * by IMDb id it answers in 0.2-0.4s (by `tmdb:` it answers "No streams found",
13 after up to 21s) -- MobLand S2E1 27 rows, Tony 33 rows;
14 * R2 and pixeldrain rows return 206 to a plain Range GET and ffprobe reads
15 them, at ~10 MB/s -- so ffmpeg can copy them, which vidlink could never do;
16 * "[10Gbps Download Only]" rows (hubcloud) are HTML/400 -- useless;
17 * results are fuzzy: Tony (2026) came back with "NCIS: Tony & Ziva" rows;
18 * many rows are HEVC Main10 or 4K, which the Pi's H.264 decoder cannot play;
19 * dual-audio files flag HINDI as the default track;
20 * R2 links are pre-signed for 8h from when HdHub cached them, not from when
21 we ask -- one handed over at 22:50 UTC expired at 23:25.
22
23Every rule below is one of those facts.
24
25Run: .venv/bin/python3 -m pytest test_hdhub.py -q
26"""
27
28import os
29import time
30
31import pytest
32
33import addon
34
35
36# --- fixtures ---------------------------------------------------------------
37
38R2 = ("https://abc.r2.cloudflarestorage.com/hub/x?X-Amz-Date={date}"
39 "&X-Amz-Expires=28800&X-Amz-Signature=s")
40PD = "https://pixeldrain.dev/api/file/{fid}?download"
41
42
43def _amz_now(offset=0):
44 return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(time.time() + offset))
45
46
47def row(filename, name="HdHub 1080p", host="FSL", size=1_000_000_000, url=None):
48 if url is None:
49 url = (R2.format(date=_amz_now()) if host == "FSL"
50 else PD.format(fid=abs(hash(filename)) % 10 ** 8))
51 return {"name": name,
52 "description": f"[{host}] [💾 1 GB] {filename}\nHindi\n{host} | HdHub",
53 "url": url, "behaviorHints": {"videoSize": size}}
54
55
56@pytest.fixture(autouse=True)
57def isolated(monkeypatch, tmp_path):
58 monkeypatch.setattr(addon, "LIBRARY_ROOT", str(tmp_path))
59 monkeypatch.setattr(addon, "LIBRARY_VERIFIED_FILE",
60 os.path.join(str(tmp_path), "verified.json"))
61 monkeypatch.setattr(addon, "_duration_cache", {})
62 monkeypatch.setattr(addon, "_library_meta_cache", {})
63 monkeypatch.setattr(addon, "_hdhub_cache", {})
64 monkeypatch.setattr(addon, "_hdhub_probe_cache", {})
65 monkeypatch.setattr(addon, "_hdhub_pick_cache", {})
66 monkeypatch.setattr(addon, "_mp4_jobs", {})
67 yield
68
69
70@pytest.fixture
71def tmdb(monkeypatch):
72 """Stand in for TMDB. Tests must never reach the network."""
73 responses = {
74 "tv/247718": {"name": "MobLand", "first_air_date": "2025-03-30"},
75 "tv/247718/season/2/episode/1": {"name": "I Wanna Be Your Dog",
76 "runtime": 55},
77 "tv/247718/external_ids": {"imdb_id": "tt31510819"},
78 "movie/1329016": {"title": "Tony", "release_date": "2026-05-01",
79 "runtime": 100},
80 "movie/1329016/external_ids": {"imdb_id": "tt33095251"},
81 }
82
83 def fake(path, params=None):
84 if path not in responses:
85 raise RuntimeError(f"unexpected TMDB call: {path}")
86 return responses[path]
87
88 monkeypatch.setattr(addon, "_tmdb_get", fake)
89 return responses
90
91
92@pytest.fixture
93def hdhub(monkeypatch):
94 """Stand in for the HdHub addon: {(kind, imdb, s, e): [rows]}."""
95 listing, calls = {}, []
96
97 def fake(kind, imdb, season=0, episode=0):
98 calls.append((kind, imdb, season, episode))
99 return listing.get((kind, imdb, season, episode), [])
100
101 monkeypatch.setattr(addon, "_hdhub_query", fake)
102 return type("H", (), {"listing": listing, "calls": calls})()
103
104
105def probe(codec="h264", height=1080, audio=(("aac", "eng"),), duration=3300.0,
106 png=False):
107 streams = [{"index": 0, "codec_type": "video", "codec_name": codec,
108 "height": height, "disposition": {"attached_pic": 0}}]
109 for i, (c, lang) in enumerate(audio, start=1):
110 streams.append({"index": i, "codec_type": "audio", "codec_name": c,
111 "tags": {"language": lang} if lang else {},
112 "disposition": {"attached_pic": 0}})
113 if png:
114 streams.append({"index": len(streams), "codec_type": "video",
115 "codec_name": "png", "disposition": {"attached_pic": 1}})
116 return {"streams": streams, "format": {"duration": str(duration)}}
117
118
119# --- identity: the right title, or nothing ----------------------------------
120
121@pytest.mark.parametrize("filename", [
122 "MobLand.S02E01.I.Wanna.Be.Your.Dog.1080p.ATV.WEB-DL.x264.mkv",
123 "MobLand.2025.S02E01.1080p.WEB-DL.x264.mkv",
124 "MobLand S02E01 1080p WEB-DL x264.mkv",
125])
126def test_a_series_file_is_accepted_on_title_and_exact_episode(filename):
127 assert addon._hdhub_title_ok(filename, "MobLand", "2025", "series", 2, 1)
128
129
130@pytest.mark.parametrize("filename", [
131 "MobLand.S02E02.Song.2.1080p.x264.mkv", # wrong episode
132 "MobLand.S01E01.Stick.or.Twist.1080p.x264.mkv", # wrong season
133 "MobLand.S02.Complete.1080p.x264.mkv", # a pack, not the episode
134 "Not.MobLand.S02E01.1080p.x264.mkv", # title must LEAD
135])
136def test_a_series_file_for_another_episode_or_show_is_refused(filename):
137 assert not addon._hdhub_title_ok(filename, "MobLand", "2025", "series", 2, 1)
138
139
140def test_ncis_tony_and_ziva_is_not_tony_2026():
141 """The actual rows HdHub returned for Tony (2026)."""
142 for f in ("Ncis.Tony.&.Ziva.S01E07.1080P.Hindi.English-Vegamovies.is.mkv",
143 "NCIS Tony and Ziva S01 1080p WEB-DL H264",
144 "Tony.and.Ziva.2025.1080p.x264.mkv"):
145 assert not addon._hdhub_title_ok(f, "Tony", "2026", "movie", 0, 0), f
146
147
148def test_the_real_tony_is_accepted():
149 f = "Tony.2026.1080p.AMZN.WEB-DL.English.DDP.5.1.Atmos.x264-4kHDHub.com.mkv"
150 assert addon._hdhub_title_ok(f, "Tony", "2026", "movie", 0, 0)
151
152
153def test_a_movie_year_may_be_off_by_one_but_not_more():
154 """Festival vs wide release straddle a new year; a remake is decades off."""
155 assert addon._hdhub_title_ok("Tony.2025.1080p.x264.mkv", "Tony", "2026", "movie")
156 assert not addon._hdhub_title_ok("Tony.2019.1080p.x264.mkv", "Tony", "2026", "movie")
157
158
159def test_a_movie_request_refuses_an_episode_file():
160 assert not addon._hdhub_title_ok("Tony.2026.S01E01.1080p.mkv", "Tony", "2026", "movie")
161
162
163def test_punctuation_and_diacritics_do_not_break_matching():
164 assert addon._hdhub_title_ok("X-Men.97.S02E08.1080p.x264.mkv",
165 "X-Men '97", "2024", "series", 2, 8)
166 assert addon._hdhub_title_ok("Marama.2025.1080p.x264.mkv",
167 "Mārama", "2025", "movie")
168 assert addon._hdhub_title_ok("Star.Trek.Strange.New.Worlds.S04E01.1080p.mkv",
169 "Star Trek: Strange New Worlds", "2022",
170 "series", 4, 1)
171
172
173# --- what the Pi can decode -------------------------------------------------
174
175@pytest.mark.parametrize("name,filename", [
176 ("HdHub 1080p", "MobLand.S02E01.1080p.10Bit.WEB-DL.HEVC.x265.mkv"),
177 ("HdHub 1080p", "MobLand.S02E01.1080p.WEB-DL.H.265.mkv"),
178 ("HdHub 2160p", "MobLand.S02E01.2160p.WEB-DL.x264.mkv"),
179 ("4KHDHub 4K", "MobLand.S02E01.2160p.ATV.WEB-DL.mkv"),
180 ("HdHub 1080p", "Film.2020.1080p.BluRay.DTS-HD.MA.x264.mkv"),
181 ("HdHub 1080p", "Film.2020.1080p.BluRay.TrueHD.x264.mkv"),
182 ("HdHub 1080p", "Film.2020.1080p.AV1.mkv"),
183])
184def test_what_the_pi_cannot_decode_is_refused_by_name(name, filename):
185 assert not addon._hdhub_decodable(name, filename)
186
187
188def test_the_4khdhub_brand_is_not_mistaken_for_a_4k_file():
189 """'4KHDHub 1080p' is a 1080p row from a site whose name starts with 4K."""
190 assert addon._hdhub_height("4KHDHub 1080p") == 1080
191 assert addon._hdhub_decodable("4KHDHub 1080p",
192 "Tony.2026.1080p.AMZN.WEB-DL.DDP.Atmos.x264-4kHDHub.com.mkv")
193
194
195# --- hosts ------------------------------------------------------------------
196
197def test_only_hosts_measured_to_serve_bytes_are_used(tmdb, hdhub):
198 f = "Tony.2026.1080p.AMZN.WEB-DL.English.x264.mkv"
199 hdhub.listing[("movie", "tt33095251", 0, 0)] = [
200 row(f, host="10Gbps Download Only", url="https://pixel.hubcloud.ist/x"),
201 row(f, host="HubDrive", url="https://hubdrive.pics/file/1"),
202 row(f, host="PixelDrain"),
203 row(f, host="FSL"),
204 ]
205 files = addon._hdhub_files("movie", "tmdb:1329016")
206 hosts = sorted(x.host for x in files)
207 assert hosts == ["abc.r2.cloudflarestorage.com", "pixeldrain.dev"]
208
209
210def test_r2_is_preferred_over_pixeldrain_for_the_same_file(tmdb, hdhub):
211 """pixeldrain rate-limits hotlinked downloads; R2 does not."""
212 f = "Tony.2026.1080p.AMZN.WEB-DL.English.x264.mkv"
213 hdhub.listing[("movie", "tt33095251", 0, 0)] = [row(f, host="PixelDrain"),
214 row(f, host="FSL")]
215 assert addon._hdhub_files("movie", "tmdb:1329016")[0].host.endswith("r2.cloudflarestorage.com")
216
217
218# --- expiry -----------------------------------------------------------------
219
220def test_the_expiry_of_a_presigned_r2_url_is_read_from_the_url():
221 url = R2.format(date=_amz_now(-8 * 3600 + 600)) # signed 7h50m ago
222 left = addon._url_seconds_left(url)
223 assert 500 < left < 700
224
225
226def test_a_url_without_a_signature_has_no_known_expiry():
227 assert addon._url_seconds_left(PD.format(fid="abc")) is None
228
229
230def test_a_nearly_expired_link_is_not_offered(tmdb, hdhub):
231 f = "Tony.2026.1080p.AMZN.WEB-DL.English.x264.mkv"
232 dying = R2.format(date=_amz_now(-8 * 3600 + 120)) # 2 minutes left
233 hdhub.listing[("movie", "tt33095251", 0, 0)] = [row(f, url=dying)]
234 assert addon._hdhub_files("movie", "tmdb:1329016") == []
235
236
237def test_ingest_demands_a_wider_margin_than_playback(tmdb, hdhub):
238 """A 3.7 GB copy at ~10 MB/s takes ~6 minutes, and a reconnect after expiry
239 is a 403. 30 minutes left is fine to press play on, not to start a copy."""
240 f = "Tony.2026.1080p.AMZN.WEB-DL.English.x264.mkv"
241 url = R2.format(date=_amz_now(-8 * 3600 + 1800))
242 hdhub.listing[("movie", "tt33095251", 0, 0)] = [row(f, url=url)]
243 assert addon._hdhub_files("movie", "tmdb:1329016",
244 margin=addon.HDHUB_PLAY_MARGIN)
245 assert not addon._hdhub_files("movie", "tmdb:1329016",
246 margin=addon.HDHUB_INGEST_MARGIN)
247
248
249# --- language ---------------------------------------------------------------
250
251def test_a_file_with_no_english_audio_is_refused(tmdb, hdhub):
252 hdhub.listing[("series", "tt31510819", 2, 1)] = [
253 row("MobLand.S02E01.1080p.WEB-DL.Hindi.AAC.x264.mkv")]
254 assert addon._hdhub_files("series", "tmdb:247718", 2, 1) == []
255
256
257def test_english_only_files_rank_above_dual_audio(tmdb, hdhub):
258 dual = "MobLand.S02E01.1080p.WEB-DL.Hindi.2.0-English.5.1.x264.mkv"
259 eng = "MobLand.S02E01.1080p.WEB-DL.English.DDP5.1.x264.mkv"
260 hdhub.listing[("series", "tt31510819", 2, 1)] = [row(dual, size=9e9),
261 row(eng, size=1e9)]
262 assert [x.filename for x in addon._hdhub_files("series", "tmdb:247718", 2, 1)] == [eng, dual]
263
264
265def test_higher_resolution_ranks_first_but_never_above_the_ask(tmdb, hdhub):
266 hd = "MobLand.S02E01.1080p.WEB-DL.English.x264.mkv"
267 sd = "MobLand.S02E01.720p.WEB-DL.English.x264.mkv"
268 hdhub.listing[("series", "tt31510819", 2, 1)] = [row(sd, "HdHub 720p"),
269 row(hd, "HdHub 1080p")]
270 assert [x.height for x in addon._hdhub_files("series", "tmdb:247718", 2, 1)] == [1080, 720]
271 assert [x.height for x in addon._hdhub_files("series", "tmdb:247718", 2, 1,
272 max_height=720)] == [720]
273
274
275# --- lookup -----------------------------------------------------------------
276
277def test_hdhub_is_asked_by_imdb_id_even_for_a_tmdb_title(tmdb, hdhub):
278 """By `tmdb:` HdHub answers 'No streams found'; by IMDb id it has them."""
279 addon._hdhub_files("series", "tmdb:247718", 2, 1)
280 assert hdhub.calls == [("series", "tt31510819", 2, 1)]
281
282
283def test_an_imdb_title_is_passed_straight_through(tmdb, hdhub, monkeypatch):
284 monkeypatch.setattr(addon, "_library_meta",
285 lambda *a, **k: {"tmdb_id": "1329016", "title": "Tony",
286 "year": "2026", "episode_title": "",
287 "runtime": 100})
288 addon._hdhub_files("movie", "tt33095251")
289 assert hdhub.calls == [("movie", "tt33095251", 0, 0)]
290
291
292def test_no_imdb_id_means_hdhub_is_not_asked(tmdb, hdhub):
293 tmdb["movie/1329016/external_ids"] = {"imdb_id": None}
294 assert addon._hdhub_files("movie", "tmdb:1329016") == []
295 assert hdhub.calls == []
296
297
298def test_an_hdhub_outage_yields_nothing_rather_than_an_error(tmdb, monkeypatch):
299 def boom(*a, **k):
300 raise RuntimeError("503")
301 monkeypatch.setattr(addon, "_hdhub_query", boom)
302 assert addon._hdhub_files("movie", "tmdb:1329016") == []
303
304
305# --- the probe: the file itself decides -------------------------------------
306
307def test_an_h264_file_with_english_audio_is_usable():
308 assert addon._probe_usable(probe())
309
310
311def test_hevc_is_refused_even_when_the_name_did_not_say_so():
312 assert not addon._probe_usable(probe(codec="hevc"))
313
314
315def test_a_file_whose_audio_is_all_tagged_hindi_is_refused():
316 assert not addon._probe_usable(probe(audio=(("aac", "hin"),)))
317
318
319def test_untagged_audio_is_given_the_benefit_of_the_doubt():
320 assert addon._probe_usable(probe(audio=(("aac", None),)))
321
322
323def test_cover_art_is_not_mistaken_for_the_video():
324 assert addon._probe_usable(probe(png=True))
325
326
327def test_audio_the_mp4_container_cannot_hold_does_not_count():
328 assert not addon._probe_usable(probe(audio=(("dts", "eng"),)))
329
330
331def test_the_pick_skips_a_file_that_fails_its_probe(tmdb, hdhub, monkeypatch):
332 bad = "MobLand.S02E01.1080p.WEB-DL.English.mkv"
333 good = "MobLand.S02E01.720p.WEB-DL.English.x264.mkv"
334 hdhub.listing[("series", "tt31510819", 2, 1)] = [row(bad, "HdHub 1080p"),
335 row(good, "HdHub 720p")]
336 monkeypatch.setattr(addon, "_hdhub_probe",
337 lambda url, f=None: probe(codec="hevc") if "1080" in (f or "") else probe())
338 picked = addon._hdhub_pick("series", "tmdb:247718", 2, 1, 1080)
339 assert picked and picked[0].filename == good
340
341
342def test_no_usable_file_is_none(tmdb, hdhub, monkeypatch):
343 hdhub.listing[("series", "tt31510819", 2, 1)] = [
344 row("MobLand.S02E01.1080p.WEB-DL.English.mkv")]
345 monkeypatch.setattr(addon, "_hdhub_probe", lambda url, f=None: None)
346 assert addon._hdhub_pick("series", "tmdb:247718", 2, 1, 1080) is None
347
348
349# --- playback route ---------------------------------------------------------
350
351@pytest.fixture
352def client(monkeypatch):
353 started = []
354 monkeypatch.setattr(addon, "_ensure_ingest",
355 lambda *a, **k: started.append(a))
356 monkeypatch.setattr(addon, "_queue_next_episode", lambda *a, **k: None)
357 c = addon.app.test_client()
358 c.started = started
359 return c
360
361
362def test_the_route_redirects_to_the_chosen_file(client, monkeypatch):
363 f = addon.HdHubFile(url="https://x.r2.dev/f.mkv", filename="f", height=1080,
364 size=1, host="x.r2.dev", english_only=True)
365 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: None)
366 monkeypatch.setattr(addon, "_hdhub_pick", lambda *a, **k: (f, probe()))
367 r = client.get("/hdhub/series/tmdb:247718:2:1.mkv?h=1080")
368 assert r.status_code == 302 and r.headers["Location"] == "https://x.r2.dev/f.mkv"
369 assert client.started, "playing is the signal to keep a copy"
370
371
372def test_a_local_copy_wins_over_the_network(client, monkeypatch, tmp_path):
373 p = tmp_path / "x.mp4"
374 p.write_bytes(b"x")
375 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: str(p))
376 monkeypatch.setattr(addon, "_hdhub_pick",
377 lambda *a, **k: pytest.fail("must not look upstream"))
378 r = client.get("/hdhub/movie/tmdb:1329016.mkv?h=1080")
379 assert r.status_code == 302 and "/mp4/movie/tmdb:1329016.mp4" in r.headers["Location"]
380
381
382def test_nothing_usable_is_a_404(client, monkeypatch):
383 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: None)
384 monkeypatch.setattr(addon, "_hdhub_pick", lambda *a, **k: None)
385 assert client.get("/hdhub/movie/tmdb:1329016.mkv").status_code == 404
386
387
388def test_a_hostile_ident_is_refused(client):
389 assert client.get("/hdhub/movie/..%2Fetc.mkv").status_code == 404
390 assert client.get("/hdhub/series/tmdb:1.mkv").status_code == 404
391
392
393def test_play_and_keep_plays_hdhub_when_there_is_no_copy_yet(client, monkeypatch):
394 """The cached 'play & keep' row goes through /mp4/. With vidcore's CDN
395 banned, a vidcore redirect there is a dead link; a progressive file is the
396 better live answer anyway -- it seeks."""
397 f = addon.HdHubFile(url="https://x.r2.dev/f.mkv", filename="f", height=1080,
398 size=1, host="x.r2.dev", english_only=True)
399 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: None)
400 monkeypatch.setattr(addon, "_hdhub_pick", lambda *a, **k: (f, probe()))
401 monkeypatch.setattr(addon, "_resolve_vidcore",
402 lambda *a, **k: pytest.fail("hdhub answered first"))
403 r = client.get("/mp4/movie/tmdb:1329016.mp4?h=1080")
404 assert r.status_code == 302 and r.headers["Location"] == "https://x.r2.dev/f.mkv"
405
406
407# --- rows -------------------------------------------------------------------
408
409def test_an_episode_row_carries_the_episode_tag_and_sits_above_vidcore(
410 client, tmdb, hdhub, monkeypatch):
411 """Jacktook drops a TV row without SxxEyy in its title."""
412 hdhub.listing[("series", "tt31510819", 2, 1)] = [
413 row("MobLand.S02E01.1080p.WEB-DL.English.x264.mkv")]
414 monkeypatch.setattr(addon, "_prefetch_vidcore", lambda *a, **k: None)
415 monkeypatch.setattr(addon, "_hdhub_prefetch", lambda *a, **k: None)
416 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: None)
417 rows = client.get("/stream/series/fxl:247718:2:1.json").get_json()["streams"]
418 names = [r["name"] for r in rows]
419 assert "HdHub" in names
420 hd = rows[names.index("HdHub")]
421 assert hd["title"].startswith("S02E01 ")
422 assert "/hdhub/series/tmdb:247718:2:1.mkv" in hd["url"]
423 assert names.index("HdHub") < names.index("vidcore")
424
425
426def test_no_hdhub_files_means_no_hdhub_row(client, tmdb, hdhub, monkeypatch):
427 monkeypatch.setattr(addon, "_prefetch_vidcore", lambda *a, **k: None)
428 monkeypatch.setattr(addon, "_playable_path", lambda *a, **k: None)
429 rows = client.get("/stream/movie/fxl:1329016.json").get_json()["streams"]
430 assert "HdHub" not in [r["name"] for r in rows]
431
432
433# --- library ingest ---------------------------------------------------------
434
435@pytest.fixture
436def ingest(tmdb, hdhub, monkeypatch):
437 """vidcore resolves to a banned CDN; HdHub has the file."""
438 monkeypatch.setattr(addon, "_enough_space", lambda: True)
439 monkeypatch.setattr(addon, "_fetch_subtitles_for", lambda *a, **k: None)
440 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: "http://banned/m.m3u8")
441 monkeypatch.setattr(addon, "_cap_variant", lambda url, h: url)
442
443 def banned(url, dest):
444 raise RuntimeError("ffmpeg exit 8: Server returned 403 Forbidden")
445 monkeypatch.setattr(addon, "_remux_to_mp4", banned)
446 hdhub.listing[("movie", "tt33095251", 0, 0)] = [
447 row("Tony.2026.1080p.AMZN.WEB-DL.English.x264.mkv")]
448 monkeypatch.setattr(addon, "_hdhub_probe", lambda url, f=None: probe(duration=6000))
449 copied = []
450
451 def fake_copy(url, p, dest):
452 copied.append((url, dest))
453 os.makedirs(os.path.dirname(dest), exist_ok=True)
454 open(dest, "wb").write(b"x")
455 monkeypatch.setattr(addon, "_copy_progressive", fake_copy)
456 return copied
457
458
459def test_a_banned_vidcore_falls_back_to_hdhub(ingest):
460 assert addon._run_ingest("movie", "tmdb:1329016", 0, 0, 1080) == "done"
461 assert ingest and ingest[0][1].endswith("Tony (2026).mp4")
462
463
464def test_an_unresolvable_vidcore_falls_back_too(ingest, monkeypatch):
465 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: None)
466 assert addon._run_ingest("movie", "tmdb:1329016", 0, 0, 1080) == "done"
467
468
469def test_vidcore_is_still_tried_first(ingest, monkeypatch):
470 """vidcore's identity is ASSERTED by TMDB id; HdHub's is matched from a
471 filename. When both can, the asserted one feeds the library."""
472 done = []
473 monkeypatch.setattr(addon, "_remux_to_mp4", lambda url, dest: done.append(url)
474 or open(dest, "wb").write(b"x"))
475 os.makedirs(os.path.dirname(addon._library_path(
476 "movie", "Tony", "2026", "1329016")), exist_ok=True)
477 assert addon._run_ingest("movie", "tmdb:1329016", 0, 0, 1080) == "done"
478 assert done and ingest == []
479
480
481@pytest.mark.parametrize("seconds", [3000, 9000]) # 50% and 150% of 100 min
482def test_a_file_the_wrong_length_for_the_title_is_not_ingested(ingest, monkeypatch, seconds):
483 """The last guard on a filename match: a different film or episode is
484 almost never within 15% of this one's runtime."""
485 monkeypatch.setattr(addon, "_hdhub_probe", lambda url, f=None: probe(duration=seconds))
486 state = addon._run_ingest("movie", "tmdb:1329016", 0, 0, 1080)
487 assert state.startswith("failed") and ingest == []
488
489
490def test_when_both_fail_the_vidcore_reason_is_kept(ingest, hdhub):
491 hdhub.listing.clear()
492 state = addon._run_ingest("movie", "tmdb:1329016", 0, 0, 1080)
493 assert state.startswith("failed: ffmpeg exit 8") and "hdhub" in state
494
495
496def test_a_disabled_hdhub_is_not_consulted(ingest, hdhub, monkeypatch):
497 monkeypatch.setattr(addon, "SOURCES", {k: v for k, v in addon.SOURCES.items()
498 if k != "hdhub"})
499 state = addon._run_ingest("movie", "tmdb:1329016", 0, 0, 1080)
500 assert state.startswith("failed") and hdhub.calls == []
501
502
503# --- the copy itself --------------------------------------------------------
504
505def test_the_copy_puts_english_audio_first_and_default(monkeypatch, tmp_path):
506 """Kodi plays the DEFAULT track, and these files flag Hindi as default."""
507 cmds = []
508
509 def fake_run(cmd, out, stall_timeout, hard_timeout):
510 cmds.append(cmd)
511 open(out, "wb").write(b"x")
512 return addon._RemuxResult(0, "")
513 monkeypatch.setattr(addon, "_run_remux", fake_run)
514 monkeypatch.setattr(addon, "_media_duration", lambda p: 3300.0)
515 p = probe(audio=(("aac", "hin"), ("eac3", "eng")), png=True)
516 dest = str(tmp_path / "Show" / "ep.mp4")
517 addon._copy_progressive("https://x.r2.dev/f.mkv", p, dest)
518 cmd = cmds[0]
519 maps = [cmd[i + 1] for i, a in enumerate(cmd) if a == "-map"]
520 assert maps == ["0:0", "0:2", "0:1"], "video, then English, then the rest"
521 assert "-disposition:a:0" in cmd and cmd[cmd.index("-disposition:a:0") + 1] == "default"
522 assert cmd[cmd.index("-disposition:a:1") + 1] == "0"
523 assert cmd[cmd.index("-f") + 1] == "mp4"
524 assert os.path.exists(dest) and not os.path.exists(dest + ".part")
525
526
527def test_a_short_copy_is_not_published(monkeypatch, tmp_path):
528 """ffmpeg exits 0 on an input that stops early; measure, as the HLS path does."""
529 monkeypatch.setattr(addon, "_run_remux", lambda cmd, out, **k: (
530 open(out, "wb").write(b"x"), addon._RemuxResult(0, ""))[1])
531 monkeypatch.setattr(addon, "_media_duration", lambda p: 600.0)
532 dest = str(tmp_path / "ep.mp4")
533 with pytest.raises(RuntimeError, match="incomplete"):
534 addon._copy_progressive("https://x.r2.dev/f.mkv", probe(duration=3300), dest)
535 assert not os.path.exists(dest)
536
537
538# --- the registry -----------------------------------------------------------
539
540def test_hdhub_is_registered_as_an_ingestable_plain_http_source():
541 src = addon.SOURCES_DEFAULT["hdhub"]
542 assert src.enabled and src.ingestable and not src.browser
543 assert src.container == "progressive-mkv"
544
545
546def test_vidcore_remains_the_first_choice_for_the_library(monkeypatch):
547 monkeypatch.setattr(addon, "SOURCES", dict(addon.SOURCES_DEFAULT))
548 assert [s.id for s in addon.ingest_sources()] == ["vidcore", "hdhub"]
549 assert addon.ingest_source().id == "vidcore"