From 27de918cb04a5ce5c067a37b7fce617affe1ab7e Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Mon, 31 Aug 2026 00:24:37 +0000 Subject: [PATCH] Intro skip: parse Jikan v4's actual response shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jikan /anime/{id}/episodes returns the episode list DIRECTLY under 'data' (flat array), not {'episodes': [...]}. The old code called .get() on that list -> AttributeError, swallowed by the endpoint's except -> [] -> the skip button could never appear, even with Jikan up. Accept both shapes now. Verified end-to-end with a mocked /api/intros response: the button is wired into the player, visible exactly while currentTime is inside the intro window (111-188s test), hidden outside it. The live button still awaits Jikan/MAL recovery (504ing since this feature landed); failures are not cached, so it activates on the first successful fetch. Skipped: MAL-direct fallback — official API needs a client key (403 without), and the keyless HTML page carries no intro data. --- server/upstream.py | 8 ++++++-- tests/test_api.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/server/upstream.py b/server/upstream.py index e5154b9..0443f42 100644 --- a/server/upstream.py +++ b/server/upstream.py @@ -132,9 +132,13 @@ def _parse_intros(eps): def _intros(mal_id): - """Jikan (keyless MAL mirror): per-episode intro start/length, seconds""" + """Jikan (keyless MAL mirror): per-episode intro start/length, seconds.""" + # Jikan v4 returns the list directly under 'data'; tolerate a wrapped + # shape too, so a format change doesn't silently kill the button r = _get("https://api.jikan.moe/v4/anime/{}".format(mal_id) + "/episodes") - return _parse_intros(r.json().get("data", {}).get("episodes", [])) + d = r.json().get("data") + eps = d if isinstance(d, list) else (d or {}).get("episodes", []) + return _parse_intros(eps) def _master_m3u8(ep_id, lang="eng"): diff --git a/tests/test_api.py b/tests/test_api.py index 955e0da..6b229c1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -312,3 +312,33 @@ def test_parse_intros(): None, None, ] + + +def test_intros_parses_jikan_shapes(): + # _intros against a stubbed HTTP layer: Jikan v4 returns the episode + # list directly under 'data' (not {'episodes': [...]}) — a regression + # here would silently disable the skip button whenever Jikan is up. + import server.upstream as u + + class R: + def json(self): + return {"data": [ + {"title": "Episode 1", "intro": {"start": 111, "length": 77}}, + {"title": "Episode 2", "intro": {"start": 0, "length": 0}}, + ]} + + u._CACHE.clear() + u._get = lambda url: R() # stub the network + assert u._intros(92573) == [ + {"start": 111, "length": 77}, + {"start": 0, "length": 0}, + ] + # wrapped shape still works + class W: + def json(self): + return {"data": {"episodes": [ + {"intro": {"start": 90, "length": 60}}, + ]}} + u._get = lambda url: W() + assert u._intros(92573)[0]["start"] == 90 + u._CACHE.clear()