OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed commits / 12c5b2c

Preview the stream a running download is copying (0.26.0)
labs#13. Watch the source, judge it, keep the download or Cancel. The preview plays exactly what the job copies -- recorded in _job_info[key]["preview"] at copy start -- not a second resolve. (The .part cannot play: +faststart writes the moov atom last.) Measured on stortron (Inception, moon.zenoak.top): vidcore variant playlists are REFERER-GATED -- 403 bare, 200 with Referer https://vidcore.net/ (6/6, one egress), CORS `*`. A browser cannot set Referer, so vidcore previews play via hls.js 1.6.16 through /web/relay/{playlist,seg}, which adds it and rewrites segment and EXT-X-MAP URIs. The relay only fetches from the live job's own playlist host (not an open proxy) and passes Range through. HdHub files carry no referer and Chromium plays h264 .mkv (measured: aac audible, eac3 silent), so those play straight from the source with the same "no sound" note. Preview links on the downloads page and on title pages while a job runs. Also fixes key slicing in the finished-job redirect (ident = parts[1:-3]). 15 tests. Suite 465. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12c5b2c8ac3f8e6eefd8f00a16df6805ad466929
by plankatron <fleeky@gmail.com> · 2026-10-05T01:09:51.000Z

Changed files (2) +459 -5

addon.py +184 -5
……@@ -21,7 +21,7 @@ import time
2121import unicodedata
2222from dataclasses import dataclass, field
2323from typing import Optional
24from urllib.parse import urlparse, unquote, urljoin, parse_qs
24from urllib.parse import urlparse, unquote, urljoin, parse_qs, quote
2525
2626import requests
2727from flask import Flask, jsonify, request, redirect, abort, Response, send_file
……@@ -619,7 +619,7 @@ TV_GENRES = [
619619
620620MANIFEST = {
621621 "id": "org.tabertron.vsembed",
622 "version": "0.25.0",
622 "version": "0.26.0",
623623 "name": "Tabertron VsEmbed",
624624 "description": "Self-hosted extractor with TMDB browse + search, played via vidcore and HdHub",
625625 "types": ["movie", "series"],
……@@ -2250,6 +2250,11 @@ def _run_ingest(kind: str, ident: str, season: int, episode: int,
22502250 return "cancelled"
22512251 if url:
22522252 variant = _cap_variant(url, height)
2253 # The preview plays exactly what is being copied (labs#13).
2254 picked = next(((h, bw) for h, bw, uri, _ in _variants(url)
2255 if urljoin(url, uri) == variant), (None, None))
2256 _job_update(key, preview={"type": "hls", "url": variant,
2257 "height": picked[0], "bandwidth": picked[1]})
22532258 log.info("ingest start: %s -> %s", key, dest)
22542259 _job_update(key, phase="downloading", source="vidcore",
22552260 downloading_since=time.time())
……@@ -3129,6 +3134,9 @@ def _ingest_from_hdhub(kind: str, ident: str, season: int, episode: int,
31293134 if not picked:
31303135 return "no usable file"
31313136 f, p = picked
3137 _job_update(_mp4_key(kind, ident, season, episode, height),
3138 preview={"type": "file", "url": f.url, "filename": f.filename,
3139 "height": f.height, "size": f.size, "probe": p})
31323140 try:
31333141 seconds = float((p.get("format") or {}).get("duration") or 0)
31343142 expected = float(meta.get("runtime") or 0) * 60
……@@ -4061,7 +4069,9 @@ def _web_state(kind: str, tmdb: str, season: int = 0,
40614069 states = [_mp4_jobs.get(_mp4_key(kind, ident, season, episode, h))
40624070 for h in VIDCORE_HEIGHTS]
40634071 if "running" in states:
4064 return {"status": "running",
4072 key = next(_mp4_key(kind, ident, season, episode, h)
4073 for h, st in zip(VIDCORE_HEIGHTS, states) if st == "running")
4074 return {"status": "running", "key": key,
40654075 "bytes": _ingest_progress(kind, ident, season, episode)}
40664076 failed = next((s for s in states if s and s.startswith("failed")), None)
40674077 if failed:
……@@ -4079,7 +4089,8 @@ def _web_state_html(state: dict, watch: Optional[str] = None) -> str:
40794089 if s == "running":
40804090 n = state.get("bytes")
40814091 got = f" · {n / 1e6:.0f} MB" if n else " · starting"
4082 return f"<span class='st running'>downloading{got}</span>"
4092 return (f"<span class='st running'>downloading{got}</span>"
4093 + _preview_link(state.get("key")))
40834094 if s == "failed":
40844095 return f"<span class='st failed'>{html.escape(state['why'])}</span>"
40854096 return ""
……@@ -4347,7 +4358,8 @@ def _downloads_active_html() -> tuple[str, bool]:
43474358 f"<td>{j.get('height')}p</td>"
43484359 f"<td>{html.escape(j.get('source') or '')}</td>"
43494360 f"<td>{_human_secs(now - (j.get('started') or now))}</td>"
4350 f"<td class=act>{_web_cancel_form(key, 'Cancel')}</td></tr>")
4361 f"<td class=act>{_preview_link(key)} "
4362 f"{_web_cancel_form(key, 'Cancel')}</td></tr>")
43514363 if not rows:
43524364 return "<p class=mute>Nothing downloading.</p>", False
43534365 return ("<table><tr><th>Title</th><th>Status</th><th>Quality</th>"
……@@ -4777,6 +4789,173 @@ def web_watch_series(tmdb: str, season: int, episode: int):
47774789 return _web_watch("series", tmdb, season, episode)
47784790
47794791
4792# --------------------------------- preview ----------------------------------
4793# Watch the stream a RUNNING download is copying, judge it, then let it finish
4794# or Cancel. It plays the very source the job copies -- recorded in
4795# _job_info[key]["preview"] when the copy starts -- not a second resolve. The
4796# .part cannot be played instead: +faststart writes the moov atom last.
4797#
4798# Measured 2026-10-04 (labs#13):
4799# vidcore variant playlists are REFERER-GATED -- 403 bare, 200 with
4800# `Referer: https://vidcore.net/` (6/6 on one egress), CORS `*`.
4801# A browser cannot set Referer, so they play through the relay
4802# below, which adds it.
4803# HdHub direct files, no referer. Chromium plays h264 in .mkv as well as
4804# .mp4, so the browser plays them straight from the source.
4805HLS_JS = "https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.6.16/hls.min.js"
4806VIDCORE_REFERER = "https://vidcore.net/"
4807
4808
4809def _preview_link(key: Optional[str]) -> str:
4810 if not key or not (_job_info.get(key) or {}).get("preview"):
4811 return ""
4812 return (f" <a class=watch href='/web/preview?key={quote(key, safe='')}'>"
4813 "Preview</a>")
4814
4815
4816def _live_preview(key: str) -> Optional[dict]:
4817 return (_job_info.get(key) or {}).get("preview")
4818
4819
4820def _preview_quality_html(pv: dict) -> str:
4821 if pv["type"] == "hls":
4822 rate = f"{pv['bandwidth'] / 1e6:.1f} Mbps" if pv.get("bandwidth") else "?"
4823 rows = [("Source", "vidcore (HLS)"),
4824 ("Variant", f"{pv.get('height') or '?'}p · {rate}")]
4825 return "<table>" + "".join(
4826 f"<tr><td class=mute>{k}</td><td>{html.escape(v)}</td></tr>"
4827 for k, v in rows) + "</table>"
4828 probe = pv.get("probe") or {}
4829 streams = probe.get("streams") or []
4830 video = next((s for s in streams if s.get("codec_type") == "video"), {})
4831 audio = [s for s in streams if s.get("codec_type") == "audio"]
4832 rows = [("Source", "HdHub (direct file)"),
4833 ("File", pv.get("filename") or ""),
4834 ("Picture", f"{video.get('codec_name') or '?'} · "
4835 f"{video.get('height') or pv.get('height') or '?'}p"),
4836 ("Size", _human_bytes(pv.get("size")))]
4837 for i, a in enumerate(audio, 1):
4838 lang = (a.get("tags") or {}).get("language") or "und"
4839 rows.append((f"Audio {i}", f"{a.get('codec_name')} · {lang}"))
4840 warn = ""
4841 if audio and audio[0].get("codec_name") in BROWSER_SILENT_AUDIO:
4842 warn = ("<p class=warn>This preview plays without sound: the first "
4843 f"audio track is {html.escape(audio[0].get('codec_name'))}, "
4844 "which browsers cannot decode. The copy keeps it.</p>")
4845 return warn + "<table>" + "".join(
4846 f"<tr><td class=mute>{k}</td><td>{html.escape(str(v))}</td></tr>"
4847 for k, v in rows) + "</table>"
4848
4849
4850@app.route("/web/preview")
4851def web_preview():
4852 key = request.args.get("key") or ""
4853 info = _job_info.get(key)
4854 if info is None:
4855 state = _mp4_jobs.get(key) or ""
4856 parts = key.split(":")
4857 if state and len(parts) >= 5 and parts[0] in ("movie", "series"):
4858 # Finished (or stopped) since the link was rendered: the title page
4859 # has the outcome, and Watch if it landed.
4860 href = _title_href(parts[0], ":".join(parts[1:-3]), int(parts[-3]))
4861 if href:
4862 return redirect(href, code=303)
4863 abort(404)
4864 title = html.escape(str(info.get("title") or info.get("ident")))
4865 head = (f"<h1>Preview: {title} <span class=mute>"
4866 f"{html.escape(str(info.get('label') or ''))}</span></h1>")
4867 pv = info.get("preview")
4868 if not pv:
4869 return _web_page("Preview", head
4870 + f"<p class=mute>{html.escape(info.get('phase') or '')}"
4871 " — the stream appears here once the download starts "
4872 "copying.</p>" + _web_cancel_form(key, "Cancel download"),
4873 refresh=True)
4874 k = quote(key, safe="")
4875 if pv["type"] == "hls":
4876 player = (
4877 "<video id=pv controls playsinline style='width:100%;max-height:70vh;"
4878 "background:#000'></video>"
4879 f"<script src='{HLS_JS}'></script><script>"
4880 "(function(){var v=document.getElementById('pv'),"
4881 f"src='/web/relay/playlist?key={k}';"
4882 "if(window.Hls&&Hls.isSupported()){var h=new Hls();h.loadSource(src);"
4883 "h.attachMedia(v);}else{v.src=src;}})();</script>")
4884 else:
4885 player = (f"<video controls playsinline preload=metadata src='{html.escape(pv['url'])}' "
4886 "style='width:100%;max-height:70vh;background:#000'></video>")
4887 body = (head + player
4888 + "<p>Downloading in the background. Keep it, or "
4889 + _web_cancel_form(key, "Cancel download") + "</p>"
4890 + "<h2>What is being copied</h2>" + _preview_quality_html(pv))
4891 return _web_page(f"Preview — {info.get('title') or ''}", body)
4892
4893
4894def _relay_target(key: str) -> dict:
4895 pv = _live_preview(key)
4896 if not pv or pv.get("type") != "hls":
4897 abort(404)
4898 return pv
4899
4900
4901@app.route("/web/relay/playlist")
4902def web_relay_playlist():
4903 key = request.args.get("key") or ""
4904 pv = _relay_target(key)
4905 try:
4906 r = requests.get(pv["url"], headers={"User-Agent": UA,
4907 "Referer": VIDCORE_REFERER},
4908 timeout=REQUEST_TIMEOUT)
4909 r.raise_for_status()
4910 except Exception as exc:
4911 log.warning("preview: playlist fetch failed for %s (%s)", key, exc)
4912 abort(502)
4913 k = quote(key, safe="")
4914
4915 def relayed(uri: str) -> str:
4916 return f"/web/relay/seg?key={k}&u={quote(urljoin(pv['url'], uri), safe='')}"
4917
4918 out = []
4919 for line in r.text.splitlines():
4920 if line and not line.startswith("#"):
4921 out.append(relayed(line.strip()))
4922 elif 'URI="' in line:
4923 out.append(re.sub(r'URI="([^"]+)"',
4924 lambda m: f'URI="{relayed(m.group(1))}"', line))
4925 else:
4926 out.append(line)
4927 return Response("\n".join(out) + "\n",
4928 mimetype="application/vnd.apple.mpegurl")
4929
4930
4931@app.route("/web/relay/seg")
4932def web_relay_seg():
4933 """One segment, fetched with the referer the CDN demands. Only from the
4934 host of this job's own playlist -- never an open proxy."""
4935 key = request.args.get("key") or ""
4936 pv = _relay_target(key)
4937 u = request.args.get("u") or ""
4938 if urlparse(u).scheme != "https" or urlparse(u).netloc != urlparse(pv["url"]).netloc:
4939 abort(403)
4940 headers = {"User-Agent": UA, "Referer": VIDCORE_REFERER}
4941 if request.headers.get("Range"):
4942 headers["Range"] = request.headers["Range"]
4943 try:
4944 up = requests.get(u, headers=headers, timeout=REQUEST_TIMEOUT, stream=True)
4945 except Exception as exc:
4946 log.warning("preview: segment fetch failed for %s (%s)", key, exc)
4947 abort(502)
4948 if up.status_code >= 400:
4949 up.close()
4950 abort(502)
4951 passed = {h: up.headers[h] for h in ("Content-Length", "Content-Range",
4952 "Accept-Ranges") if h in up.headers}
4953 # Segments arrive disguised as text/html; the player only needs bytes.
4954 return Response(up.iter_content(64 * 1024), status=up.status_code,
4955 headers=passed, mimetype="video/mp4",
4956 direct_passthrough=True)
4957
4958
47804959# The sources this addon knows about. Declared here, at the end, so each entry
47814960# can point at the resolver it uses. Dead and unusable sources are RECORDED
47824961# rather than deleted: the next person to notice that vidlink serves progressive
test_web_preview.py +275 -0
……@@ -0,0 +1,275 @@
1"""Preview the stream a running download is copying (labs#13).
2
3Watch it, judge the quality, then let the download finish or Cancel. The
4preview plays the SAME source the job copies -- not a second resolve -- because
5the job already holds the URL. (The .part itself is unplayable: +faststart
6writes the moov atom last.)
7
8Measured 2026-10-04 on stortron (Inception, moon.zenoak.top):
9 - vidcore variant playlists are REFERER-GATED: 403 bare, 200 with
10 `Referer: https://vidcore.net/` -- 6/6 on one egress. CORS is `*`. A
11 browser cannot set Referer, so vidcore previews go through a relay here
12 that adds it.
13 - HdHub hands out direct files (no referer). Chromium plays h264 .mkv as
14 well as .mp4 (aac audible; eac3 silent, as for Watch), so the browser
15 plays those straight from the source.
16
17The relay is NOT an open proxy: it only fetches from the host of the live
18job's own playlist.
19
20Run: .venv/bin/python3 -m pytest test_web_preview.py -q
21"""
22
23import os
24import threading
25from urllib.parse import quote
26
27import pytest
28
29import addon
30
31
32KEY = "series:tmdb:1:2:6:1080"
33VARIANT = "https://cdn.example/vd/abc/sd/720.m3u8"
34PLAYLIST = """#EXTM3U
35#EXT-X-VERSION:7
36#EXT-X-MAP:URI="init-720.mp4"
37#EXTINF:6.0,
38seg-1.html?token=a
39#EXTINF:6.0,
40https://cdn.example/vd/abc/sd/seg-2.html?token=b
41#EXT-X-ENDLIST
42"""
43
44
45@pytest.fixture(autouse=True)
46def isolated(tmp_path, monkeypatch):
47 monkeypatch.setattr(addon, "LIBRARY_ROOT", str(tmp_path / "lib"))
48 monkeypatch.setattr(addon, "INGEST_QUEUE_FILE", str(tmp_path / "q.json"))
49 monkeypatch.setattr(addon, "_job_info", {})
50 monkeypatch.setattr(addon, "_mp4_jobs", {})
51 monkeypatch.setattr(addon, "_enough_space", lambda: True)
52 yield
53
54
55@pytest.fixture
56def client():
57 return addon.app.test_client()
58
59
60def _live(preview=None, phase="downloading"):
61 addon._job_info[KEY] = {
62 "phase": phase, "source": "vidcore", "dest": None, "started": 0,
63 "kind": "series", "ident": "tmdb:1", "season": 2, "episode": 6,
64 "height": 1080, "label": "S02E06", "title": "Show"}
65 if preview:
66 addon._job_info[KEY]["preview"] = preview
67 addon._mp4_jobs[KEY] = "running"
68
69
70HLS = {"type": "hls", "url": VARIANT, "height": 720, "bandwidth": 3121132}
71FILE = {"type": "file", "url": "https://pub-x.r2.dev/Show.S02E06.mkv?X-Amz=1",
72 "filename": "Show.S02E06.1080p.mkv", "height": 1080, "size": 2_000_000_000,
73 "probe": {"format": {"duration": "1800"}, "streams": [
74 {"codec_type": "video", "codec_name": "h264", "height": 1080},
75 {"codec_type": "audio", "codec_name": "eac3", "tags": {"language": "eng"}}]}}
76
77
78class FakeResp:
79 def __init__(self, body=b"", status=200, headers=None):
80 self.content = body
81 self.text = body.decode() if isinstance(body, bytes) else body
82 self.status_code = status
83 self.headers = headers or {}
84
85 def raise_for_status(self):
86 if self.status_code >= 400:
87 raise addon.requests.HTTPError(str(self.status_code))
88
89 def iter_content(self, chunk_size=1):
90 yield self.content
91
92 def close(self):
93 pass
94
95
96# --- the job records what it copies -----------------------------------------
97
98META = {"tmdb_id": "1", "title": "Show", "year": "2020",
99 "episode_title": "Six", "runtime": 30}
100
101
102def _ingest_env(monkeypatch, tmp_path):
103 monkeypatch.setattr(addon, "_library_meta", lambda *a, **k: dict(META))
104 monkeypatch.setattr(addon, "_library_has", lambda *a, **k: False)
105 monkeypatch.setattr(addon, "_fetch_subtitles_for", lambda *a, **k: None)
106 monkeypatch.setattr(addon, "_notify_jellyfin", lambda *a, **k: None)
107 addon._job_info[KEY] = {}
108
109
110def test_a_vidcore_job_records_the_variant_it_copies(monkeypatch, tmp_path):
111 _ingest_env(monkeypatch, tmp_path)
112 master = "https://cdn.example/vd/abc/ma"
113 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: master)
114 monkeypatch.setattr(addon, "_variants", lambda m: [
115 (1080, 6152556, "sd/1080.m3u8", ""), (720, 3121132, "sd/720.m3u8", "")])
116 monkeypatch.setattr(addon, "_cap_variant", lambda m, h=None: VARIANT)
117 seen = {}
118
119 def remux(url, dest):
120 seen.update(addon._job_info[KEY].get("preview") or {})
121 os.makedirs(os.path.dirname(dest), exist_ok=True)
122 open(dest, "wb").write(b"x")
123 monkeypatch.setattr(addon, "_remux_to_mp4", remux)
124 addon._run_ingest("series", "tmdb:1", 2, 6, 1080)
125 assert seen == {"type": "hls", "url": VARIANT, "height": 720,
126 "bandwidth": 3121132}
127
128
129def test_an_hdhub_job_records_the_file_it_copies(monkeypatch, tmp_path):
130 _ingest_env(monkeypatch, tmp_path)
131 monkeypatch.setattr(addon, "_resolve_vidcore", lambda *a, **k: None)
132 monkeypatch.setattr(addon, "_hdhub_enabled", lambda: True)
133 f = addon.HdHubFile(url=FILE["url"], filename=FILE["filename"], height=1080,
134 size=FILE["size"], host="pub-x.r2.dev", english_only=True)
135 monkeypatch.setattr(addon, "_hdhub_pick", lambda *a, **k: (f, FILE["probe"]))
136 seen = {}
137
138 def copy(url, probe, dest):
139 seen.update(addon._job_info[KEY].get("preview") or {})
140 os.makedirs(os.path.dirname(dest), exist_ok=True)
141 open(dest, "wb").write(b"x")
142 monkeypatch.setattr(addon, "_copy_progressive", copy)
143 addon._run_ingest("series", "tmdb:1", 2, 6, 1080)
144 assert seen["type"] == "file" and seen["url"] == FILE["url"]
145 assert seen["filename"] == FILE["filename"] and seen["probe"] == FILE["probe"]
146
147
148# --- the page ----------------------------------------------------------------
149
150def test_an_hls_preview_plays_through_the_relay_with_hls_js(client):
151 _live(HLS)
152 body = client.get(f"/web/preview?key={quote(KEY)}").get_data(as_text=True)
153 assert "hls.js/1.6.16/hls.min.js" in body
154 assert "/web/relay/playlist?key=" + quote(KEY, safe="") in body
155 assert "720p" in body and "3.1 Mbps" in body
156
157
158def test_a_file_preview_plays_the_source_directly(client):
159 _live(FILE)
160 body = client.get(f"/web/preview?key={quote(KEY)}").get_data(as_text=True)
161 assert "src='https://pub-x.r2.dev/Show.S02E06.mkv?X-Amz=1'" in body
162 assert "hls.min.js" not in body
163 assert "without sound" in body # eac3 first track
164 assert "Show.S02E06.1080p.mkv" in body
165
166
167def test_the_preview_page_has_cancel_and_does_not_auto_refresh(client):
168 """A refresh would restart the video under the viewer."""
169 _live(HLS)
170 body = client.get(f"/web/preview?key={quote(KEY)}").get_data(as_text=True)
171 assert "action='/web/cancel'" in body and f"value='{KEY}'" in body
172 assert "http-equiv=refresh" not in body
173
174
175def test_before_the_source_is_known_the_page_waits_and_refreshes(client):
176 _live(None, phase="resolving")
177 body = client.get(f"/web/preview?key={quote(KEY)}").get_data(as_text=True)
178 assert "resolving" in body and "http-equiv=refresh" in body
179 assert "<video" not in body
180
181
182def test_a_finished_job_sends_you_to_the_title(client):
183 addon._mp4_jobs[KEY] = "done"
184 r = client.get(f"/web/preview?key={quote(KEY)}")
185 assert r.status_code == 303
186 assert r.headers["Location"].endswith("/web/title/series/1?s=2")
187
188
189def test_an_unknown_job_is_404(client):
190 assert client.get("/web/preview?key=nope").status_code == 404
191
192
193def test_the_downloads_page_links_a_previewable_job(client):
194 _live(HLS)
195 body = client.get("/web/downloads").get_data(as_text=True)
196 assert f"/web/preview?key={quote(KEY, safe='')}" in body
197
198
199def test_the_title_page_links_a_previewable_job(client, monkeypatch):
200 monkeypatch.setattr(addon, "_tmdb_get", lambda path, params=None: {
201 "tv/1": {"name": "Show", "first_air_date": "2020-01-01",
202 "seasons": [{"season_number": 2}]},
203 "tv/1/season/2": {"episodes": [
204 {"episode_number": 6, "name": "Six", "air_date": "2020-01-01"}]},
205 "tv/1/season/2/episode/6": {"name": "Six", "runtime": 30}}[path])
206 _live(HLS)
207 body = client.get("/web/title/series/1?s=2").get_data(as_text=True)
208 assert f"/web/preview?key={quote(KEY, safe='')}" in body
209
210
211# --- the relay ---------------------------------------------------------------
212
213def test_the_playlist_is_fetched_with_the_vidcore_referer_and_rewritten(client, monkeypatch):
214 _live(HLS)
215 calls = []
216
217 def get(url, headers=None, timeout=None, stream=False):
218 calls.append((url, headers))
219 return FakeResp(PLAYLIST.encode())
220 monkeypatch.setattr(addon.requests, "get", get)
221 r = client.get(f"/web/relay/playlist?key={quote(KEY)}")
222 assert r.status_code == 200
223 assert r.mimetype == "application/vnd.apple.mpegurl"
224 assert calls[0][0] == VARIANT
225 assert calls[0][1]["Referer"] == "https://vidcore.net/"
226 body = r.get_data(as_text=True)
227 k = quote(KEY, safe="")
228 assert f'URI="/web/relay/seg?key={k}&u=' + quote("https://cdn.example/vd/abc/sd/init-720.mp4", safe="") + '"' in body
229 assert f"/web/relay/seg?key={k}&u=" + quote("https://cdn.example/vd/abc/sd/seg-1.html?token=a", safe="") in body
230 assert f"/web/relay/seg?key={k}&u=" + quote("https://cdn.example/vd/abc/sd/seg-2.html?token=b", safe="") in body
231 assert "#EXT-X-ENDLIST" in body
232
233
234def test_a_segment_is_relayed_with_the_referer_and_range(client, monkeypatch):
235 _live(HLS)
236 calls = []
237
238 def get(url, headers=None, timeout=None, stream=False):
239 calls.append((url, headers))
240 return FakeResp(b"MOOF", status=206,
241 headers={"Content-Type": "text/html",
242 "Content-Range": "bytes 0-3/10"})
243 monkeypatch.setattr(addon.requests, "get", get)
244 u = "https://cdn.example/vd/abc/sd/seg-1.html?token=a"
245 r = client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(u, safe='')}",
246 headers={"Range": "bytes=0-3"})
247 assert r.status_code == 206
248 assert r.get_data() == b"MOOF"
249 assert r.headers["Content-Range"] == "bytes 0-3/10"
250 assert calls[0][0] == u
251 assert calls[0][1]["Referer"] == "https://vidcore.net/"
252 assert calls[0][1]["Range"] == "bytes=0-3"
253
254
255def test_the_relay_is_not_an_open_proxy(client, monkeypatch):
256 _live(HLS)
257 monkeypatch.setattr(addon.requests, "get",
258 lambda *a, **k: pytest.fail("fetched a foreign host"))
259 u = "https://evil.example/x"
260 assert client.get(f"/web/relay/seg?key={quote(KEY)}&u={quote(u, safe='')}").status_code == 403
261
262
263def test_the_relay_needs_a_live_hls_job(client, monkeypatch):
264 monkeypatch.setattr(addon.requests, "get",
265 lambda *a, **k: pytest.fail("fetched without a job"))
266 assert client.get(f"/web/relay/playlist?key={quote(KEY)}").status_code == 404
267 _live(FILE)
268 assert client.get(f"/web/relay/playlist?key={quote(KEY)}").status_code == 404
269
270
271def test_an_upstream_error_is_passed_on_not_crashed(client, monkeypatch):
272 _live(HLS)
273 monkeypatch.setattr(addon.requests, "get",
274 lambda *a, **k: FakeResp(b"no", status=403))
275 assert client.get(f"/web/relay/playlist?key={quote(KEY)}").status_code == 502