"""test_animekai_live.py — LIVE smoke tests for the animekai provider. Skipped by default so the suite never touches animekai.be (politeness: the provider must not be spammed by CI or local test runs). Enable with: ANI_LIVE=1 .venv/bin/pytest tests/test_animekai_live.py -q -s Every call rides the provider's _cached TTL cache, so one run is a handful of requests, not a crawl. """ import base64 import os import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) live = pytest.mark.skipif( os.environ.get("ANI_LIVE") != "1", reason="live upstream test: run with ANI_LIVE=1") @live def test_search_shape(): from server.providers import animekai as a r = a.search("one piece") assert r and all(set(x) >= {"id", "slug_id", "name"} for x in r), r[:2] print("\nsearch:", [(x["name"], x["slug_id"]) for x in r[:3]]) @live def test_details_episodes_languages(): from server.providers import animekai as a r = a.search("one piece") top = next(x for x in r if x["name"].lower().startswith("one piece")) d = a.details(top["slug_id"]) print("\ndetails:", {k: (str(v)[:60] if v else v) for k, v in d.items() if k != "seasons"}) assert d["name"] and d["id"], d eps = a.episodes(d["id"]) assert eps and eps[0]["number"] == 1, eps[:2] print("episodes:", len(eps), "first:", eps[0]) langs = a.languages(eps[0]["id"]) print("langs ep1:", langs) assert langs, "ep1 should have at least sub" @live def test_master_resolves_and_stream_proxy_serves_range(monkeypatch): """Full path: provider resolves a stream, and the in-process /stream proxy serves it with Range (206 + chunk). server.app import is safe here: the CDN probe is idle-gated (no upstream fetch until a play).""" # stream_meta's allowlist walks the ANI_SOURCE chain; point it at # animekai (as run.sh does) so the resolved host is permitted monkeypatch.setenv("ANI_SOURCE", "animekai") from server.providers import animekai as a # Sparks of Tomorrow's early eps were observed on 1anime servers # (independent CDN); the browse sort is most_viewed, so pick the title # by name, not position, then scan its first episodes for a resolver. r = a.search("sparks of tomorrow") top = next((x for x in r if "sparks" in x["name"].lower()), r[0]) d = a.details(top["slug_id"]) eps = a.episodes(d["id"]) got = None for e in eps[:5]: for lang in ("jpn", "eng"): try: got = a.master_m3u8(e["id"], lang) except Exception as ex: print(f"\nep{e['number']} {lang}: {ex}") if got: break if got: break assert got, "no server resolved for ep1-3 (both backends dead?)" print("\nresolved:", got) # host must be allowlisted (that IS the /stream permit) from urllib.parse import urlparse meta = a.stream_meta(got) assert meta is not None, f"provider did not register host for {got}" # in-process /stream Range check (direct handler call, no server spawn) import asyncio import types import server.app as app_mod token = base64.urlsafe_b64encode(got.encode()).decode() rng = None if got.endswith(".m3u8") else "bytes=0-1023" req = types.SimpleNamespace(headers={"range": rng} if rng else {}) resp = app_mod.stream(token, request=req) async def _drain(it): return b"".join([c async for c in it]) body = (asyncio.run(_drain(resp.body_iterator)) if getattr(resp, "body_iterator", None) is not None else resp.body) if got.endswith(".m3u8"): assert resp.status_code == 200 and b"#EXTM3U" in body else: assert resp.status_code == 206 and len(body) == 1024, resp.status_code print("proxy:", resp.status_code, len(body), "b")