OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / f9096aa

Source registry (0.13.0)
Sources were hardcoded: extract_streams named fmovies and vsembed inline and vidcore was bolted on beside them. There was no shape into which a fourth could go, and nowhere to record what a source can actually do. A Source declares three things. The third is the one that cost a day: container hls-fmp4 needs a remux to be seekable; progressive-mp4 is seekable as-is, so ingesting it is a plain fetch with no ffmpeg browser resolving needs xvfb+patchright (~11s), so it cannot sit on a hot path ingestable whether a PLAIN HTTP CLIENT can fetch the bytes ingest_source() treats ingestable as a veto that ranking cannot override, and that is the whole point. vidlink sorts first on every visible merit -- a progressive MP4, resolved in 3s, with exact byte sizes -- and ffmpeg cannot fetch a byte of it (measured 2026-08-11: 428/403/429 across six client/URL combinations, including byte-exact browser headers). Local-first rests on ffmpeg pulling bytes independently of any player, so a browser-only source can never feed the library however good its API looks. Dead and unusable sources are RECORDED, not deleted. The next person to notice vidlink serves progressive MP4 should find the measurement rather than repeat the day it took to make. Resolution still calls _resolve_vidcore directly. Dispatching through src.resolve buys nothing while vidcore is the only ingestable source, and speculative indirection is how the special-casing this replaces began. _run_ingest gates on ingest_source() so it declines outright instead of resolving, downloading, then discovering it could not. Behaviour is unchanged: same rows, same order, same ingest. 19 new tests, 153 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f9096aa19a5fa3fda8d989dc18309ddf98893e7c
by plankatron <fleeky@gmail.com> · 2026-08-11T16:45:22.000Z

Changed files (2) +269 -2

addon.py +110 -2
……@@ -258,6 +258,68 @@ def _try_source(name: str, fn, *args, **kwargs) -> list[Stream]:
258258 return out
259259
260260
261# ------------------------------- source registry ----------------------------
262# Sources used to be hardcoded: extract_streams named fmovies and vsembed
263# inline, and vidcore was bolted on beside them as a special case. There was no
264# shape into which a fourth could go, and nowhere to record what a source can
265# actually do.
266#
267# Three properties matter. The third is the one that cost a day to learn.
268@dataclass(frozen=True)
269class Source:
270 id: str
271 name: str # display label on the row
272 container: str # "hls-fmp4" | "progressive-mp4"
273 ingestable: bool # can a PLAIN HTTP CLIENT fetch the bytes?
274 browser: bool # resolving needs xvfb+patchright (~11s)
275 heights: tuple = () # qualities offered as rows
276 resolve: Optional[object] = None
277 enabled: bool = True
278 note: str = ""
279
280
281SOURCES: dict[str, Source] = {}
282
283# A progressive MP4 seeks natively; HLS on this stack does not (that is the
284# whole subject of #4), so progressive sorts first.
285_CONTAINER_RANK = {"progressive-mp4": 0, "hls-fmp4": 1}
286
287
288def register_source(src: Source) -> None:
289 if src.id in SOURCES:
290 # Two sources sharing an id would make row order and breaker state
291 # silently ambiguous — exactly the kind of thing that is discovered
292 # months later as "sometimes the rows come out in a different order".
293 raise ValueError(f"duplicate source id: {src.id}")
294 SOURCES[src.id] = src
295
296
297def get_source(sid: str) -> Optional[Source]:
298 return SOURCES.get(sid)
299
300
301def ordered_sources() -> list[Source]:
302 """Enabled sources, best container first. `sorted` is stable, so sources
303 within a container class keep registration order."""
304 return sorted((s for s in SOURCES.values() if s.enabled),
305 key=lambda s: _CONTAINER_RANK.get(s.container, 9))
306
307
308def ingest_source() -> Optional[Source]:
309 """The source the library should be fed from, or None.
310
311 `ingestable` is a veto that ranking cannot override, and that is the entire
312 point of this function. vidlink sorts first on every visible merit — a
313 progressive MP4, resolved in 3s, with exact byte sizes — and ffmpeg cannot
314 fetch a single byte of it (measured 2026-08-11: 428/403/429 across six
315 client/URL combinations, including byte-exact browser headers). The
316 local-first design rests on ffmpeg pulling bytes independently of any
317 player, so a browser-only source can never feed the library however good its
318 API looks.
319 """
320 return next((s for s in ordered_sources() if s.ingestable), None)
321
322
261323def extract_streams(imdb_id: str, tv: bool = False, season: int = 0, episode: int = 0) -> list[Stream]:
262324 """Gather streams from both fmovies (srv-1, netoda/voxzer direct) and vsembed
263325 (srv-2, cloudnestra embed). Return both so Jacktook shows two source rows —
……@@ -533,7 +595,7 @@ TV_GENRES = [
533595
534596MANIFEST = {
535597 "id": "org.tabertron.vsembed",
536 "version": "0.12.2",
598 "version": "0.13.0",
537599 "name": "Tabertron VsEmbed",
538600 "description": "Self-hosted HLS extractor with TMDB browse + search, played via vidcore",
539601 "types": ["movie", "series"],
……@@ -1380,6 +1442,12 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int,
13801442 otherwise start against a dead link.
13811443 """
13821444 key = _mp4_key(kind, ident, season, episode, height)
1445 # The registry decides whether ANYTHING may feed the library. Resolution is
1446 # still vidcore's directly, because vidcore is the only ingestable source —
1447 # dispatching through src.resolve buys nothing until there are two, and
1448 # speculative indirection is how the special-casing this replaces began.
1449 if not ingest_source():
1450 return "failed: no ingestable source"
13831451 meta = _library_meta(kind, ident, season, episode)
13841452 if not meta:
13851453 return f"failed: cannot identify {ident}"
……@@ -1695,7 +1763,8 @@ def _append_vidcore_row(out: list, kind: str, ident: Optional[str],
16951763 # (request 50%, stay at 1.2%), while ISA seeks correctly. On this source the
16961764 # two are mutually exclusive, and audio wins. Seek needs a source that is not
16971765 # muxed fMP4 HLS -- a progressive MP4 (vidlink) seeks natively under ffmpeg.
1698 for height in VIDCORE_HEIGHTS:
1766 src = get_source("vidcore")
1767 for height in (src.heights if src else VIDCORE_HEIGHTS):
16991768 out.append(_stream_dict(
17001769 _vidcore_row(request.host, kind, ident, season, episode, height=height)))
17011770
……@@ -1968,6 +2037,45 @@ def index():
19682037 )
19692038
19702039
2040# The sources this addon knows about. Declared here, at the end, so each entry
2041# can point at the resolver it uses. Dead and unusable sources are RECORDED
2042# rather than deleted: the next person to notice that vidlink serves progressive
2043# MP4 should find the measurement, not repeat the day it took to make it.
2044SOURCES_DEFAULT: dict[str, Source] = {s.id: s for s in (
2045 Source(
2046 id="vidcore", name="vidcore (flixlab)",
2047 container="hls-fmp4", # muxed fMP4 — why every copy needs a remux
2048 ingestable=True, browser=True, heights=VIDCORE_HEIGHTS,
2049 resolve=_resolve_vidcore,
2050 note="The only live source. Resolve ~11s via xvfb+patchright. Muxed "
2051 "fMP4: ffmpeg plays it with audio but ignores seeks, ISA seeks but "
2052 "starves the audio track — see labs#4.",
2053 ),
2054 Source(
2055 id="vidlink", name="vidlink", container="progressive-mp4",
2056 ingestable=False, browser=True, heights=(1080, 480, 360), enabled=False,
2057 note="Measured 2026-08-11. The API is excellent: "
2058 "vidlink.pro/api/b/tv/<id>/<s>/<e> returns every quality with "
2059 "signed URLs, exact byte sizes and codec in ~3s. But the bytes sit "
2060 "behind a proxy (requiresProxy:true) that rejects curl AND ffmpeg "
2061 "with 428 even given byte-exact browser headers — TLS fingerprint "
2062 "or session binding, not headers. Unusable for ingest without "
2063 "curl-impersonate. Media is HEVC; Pi decode unverified.",
2064 ),
2065 Source(
2066 id="fmovies", name="fmovies", container="hls-fmp4",
2067 ingestable=True, browser=True, enabled=False,
2068 note="Breaker-open since 2026-07-28 (Cloudflare Turnstile).",
2069 ),
2070 Source(
2071 id="vsembed", name="CloudStream Pro", container="hls-fmp4",
2072 ingestable=True, browser=False, enabled=False,
2073 note="Breaker-open since 2026-07-28 (Cloudflare Turnstile).",
2074 ),
2075)}
2076SOURCES.update(SOURCES_DEFAULT)
2077
2078
19712079if __name__ == "__main__":
19722080 import argparse
19732081
test_source_registry.py +159 -0
……@@ -0,0 +1,159 @@
1"""The source registry: what a source is, and which ones may feed the library.
2
3Sources were hardcoded -- extract_streams named fmovies and vsembed inline, and
4vidcore was bolted on beside them as a special case. There was no shape into
5which a fourth could be added, and no place to record what a source can and
6cannot do.
7
8Three properties matter, and the third is the one that cost a day to learn:
9
10 container hls-fmp4 needs a remux to become seekable; progressive-mp4 is
11 seekable as-is, so ingesting it is a plain fetch with no ffmpeg.
12 browser resolving needs a real browser (~11s, xvfb+patchright) rather than
13 plain HTTP, which decides whether it can sit on a hot path.
14 ingestable whether a plain HTTP client can fetch the BYTES.
15
16`ingestable` is not a detail. vidlink hands over a signed progressive-MP4 URL in
173s with exact sizes -- strictly better than vidcore on every axis that shows up
18in an API response -- and is still useless to us, because its proxy rejects curl
19AND ffmpeg even with byte-exact browser headers (measured 2026-08-11: 428/403/429
20across six combinations). The local-first design rests on ffmpeg pulling bytes
21independently of any player, so a browser-only source cannot feed the library at
22any quality.
23
24Encoding that here means the next candidate cannot be silently wired into ingest
25on the strength of a good-looking API.
26
27Run: .venv/bin/python3 -m pytest test_source_registry.py -q
28"""
29
30import pytest
31
32import addon
33
34
35@pytest.fixture(autouse=True)
36def clean_registry(monkeypatch):
37 monkeypatch.setattr(addon, "SOURCES", {})
38 yield
39
40
41def _src(sid, **kw):
42 defaults = dict(name=sid, container="hls-fmp4", ingestable=True,
43 browser=False, heights=(1080,), resolve=None, enabled=True)
44 defaults.update(kw)
45 return addon.Source(id=sid, **defaults)
46
47
48# --- registration -----------------------------------------------------------
49
50def test_a_source_can_be_registered_and_looked_up():
51 addon.register_source(_src("alpha"))
52 assert addon.get_source("alpha").name == "alpha"
53
54
55def test_an_unknown_source_is_none_rather_than_a_crash():
56 assert addon.get_source("nope") is None
57
58
59def test_a_duplicate_id_is_refused():
60 """Two sources sharing an id would make row ordering and breaker state
61 silently ambiguous."""
62 addon.register_source(_src("alpha"))
63 with pytest.raises(ValueError):
64 addon.register_source(_src("alpha"))
65
66
67# --- ordering ---------------------------------------------------------------
68
69def test_progressive_sources_are_offered_before_hls():
70 """A progressive MP4 seeks natively. HLS on this stack does not -- that is
71 the whole subject of #4 -- so it goes second."""
72 addon.register_source(_src("hls-one", container="hls-fmp4"))
73 addon.register_source(_src("prog-one", container="progressive-mp4"))
74 assert [s.id for s in addon.ordered_sources()] == ["prog-one", "hls-one"]
75
76
77def test_a_disabled_source_is_not_offered():
78 addon.register_source(_src("off", enabled=False))
79 assert addon.ordered_sources() == []
80
81
82def test_ordering_is_stable_within_a_container_class():
83 for sid in ("a", "b", "c"):
84 addon.register_source(_src(sid))
85 assert [s.id for s in addon.ordered_sources()] == ["a", "b", "c"]
86
87
88# --- the ingest guarantee ---------------------------------------------------
89
90def test_a_browser_only_source_is_never_chosen_for_ingest():
91 """THE point of this registry. vidlink sorts first on every visible merit --
92 progressive MP4, 3s resolve, exact sizes -- and ffmpeg cannot fetch a byte
93 of it. Ranking must not be able to override that."""
94 addon.register_source(_src("vidlink-like", container="progressive-mp4",
95 ingestable=False))
96 addon.register_source(_src("vidcore-like", container="hls-fmp4",
97 ingestable=True))
98 assert addon.ingest_source().id == "vidcore-like"
99
100
101def test_no_ingestable_source_yields_none_rather_than_a_bad_choice():
102 addon.register_source(_src("browser-only", ingestable=False))
103 assert addon.ingest_source() is None
104
105
106def test_a_non_ingestable_source_is_still_offered_for_playback():
107 """It cannot feed the library, but it can still be watched -- withholding it
108 would remove a working way to play something."""
109 addon.register_source(_src("browser-only", ingestable=False))
110 assert [s.id for s in addon.ordered_sources()] == ["browser-only"]
111
112
113def test_a_disabled_source_is_never_chosen_for_ingest():
114 addon.register_source(_src("off", ingestable=True, enabled=False))
115 assert addon.ingest_source() is None
116
117
118# --- the real registry ------------------------------------------------------
119# Not fixture-isolated: these assert what the addon actually ships with.
120
121def test_vidcore_is_registered_as_the_live_source():
122 src = addon.SOURCES_DEFAULT["vidcore"]
123 assert src.enabled and src.ingestable
124 assert src.container == "hls-fmp4" # why every copy needs a remux
125 assert src.browser # ~11s, xvfb+patchright
126
127
128def test_vidlink_is_recorded_as_measured_and_disabled():
129 """Recorded rather than omitted: the next person to notice vidlink serves
130 progressive MP4 should find the measurement, not repeat it."""
131 src = addon.SOURCES_DEFAULT["vidlink"]
132 assert src.container == "progressive-mp4"
133 assert src.ingestable is False
134 assert not src.enabled
135 assert "428" in src.note or "proxy" in src.note.lower()
136
137
138def test_the_dead_scrapers_are_recorded_as_disabled():
139 for sid in ("fmovies", "vsembed"):
140 assert addon.SOURCES_DEFAULT[sid].enabled is False
141
142
143def test_the_shipped_registry_chooses_vidcore_for_ingest(monkeypatch):
144 monkeypatch.setattr(addon, "SOURCES", dict(addon.SOURCES_DEFAULT))
145 assert addon.ingest_source().id == "vidcore"
146
147
148# --- the registry gates ingest ----------------------------------------------
149
150def test_ingest_refuses_when_no_source_may_feed_the_library(monkeypatch, tmp_path):
151 """If every ingestable source were disabled, ingest must decline outright
152 rather than resolve, download and then discover it cannot."""
153 monkeypatch.setattr(addon, "SOURCES", {})
154 called = []
155 monkeypatch.setattr(addon, "_library_meta",
156 lambda *a, **k: called.append(a) or None)
157 state = addon._run_ingest("series", "tmdb:1", 1, 1, 720)
158 assert state == "failed: no ingestable source"
159 assert called == [], "it must not even look the title up"