Files
Sergey Shunko fc09823f80 feat(douyin): whether a hundred live rooms are on air, in one call
GET /{platform}/live/status?live_ids=<a,b,...> now answers on Douyin as well as TikTok.

live.douyin.com answers /webcast/room/check_alive/ with a 404, which is why ab25d6e left douyin.live_status unwired. Like reflow/info, the endpoint lives on the webcast host instead: webcast.amemv.com/webcast/room/check_alive/ answered a guest with HTTP 200 and status_code 0 for up to 200 rooms in one call (measured 2026-10-26).

Two differences from TikTok's check_alive answer:

- Each entry in data carries room_id as a 64-bit JSON integer and alive as a boolean, with no room_id_str sibling; Python's JSON reader keeps every digit, and entry.id('room_id') reads it as the string live_id.

- A broadcast that has ended is alive: false in the order asked, while an id that was never a broadcast is omitted from data rather than returned as false (an ask of only nonexistent ids answers data: []).
2026-10-26 20:12:35 +02:00
..

Replay fixtures

Input for the L2 replay tests in tests/replay/ (see docs/design/13-testing.md). Each file is one decoded JSON response body, exactly as a platform endpoint returns it, fed straight into a parser.

These are hand-built. They must be replaced before release.

Every file here was written by hand from V4's parsing and request code (branch main, commit 8c98fb7), not captured from a live platform. They reproduce the response shapes that code demonstrably worked against, so the parsers are exercised against a realistic structure - but a hand-built fixture can only contain the fields whoever wrote it already knew about. It cannot tell you that the platform renamed a key last week, and it cannot show you the field nobody thought to look for.

Replacing them with real captured responses is the browser task described in docs/design/16-salvage-and-debug.md - the first of the five browser tasks listed there, capturing real responses as fixtures. Until that lands, a green replay suite means "the parsers do what we think the platform does", not "the parsers match the platform".

The capture task, per file:

  1. Open the relevant page in a fresh browser context, find the call in the network panel (/aweme/v1/web/aweme/detail/, /api/item/detail/, and so on).
  2. Save the complete response body - do not trim fields you think are irrelevant. The whole point of a fixture is being a permanent record of what the platform sent.
  3. Run it through the redaction pass below.
  4. Replace the file and re-run uv run pytest tests/replay -q. Every difference the parsers notice is real information: either the fixture was wrong, or the parser is.

Redaction rules

Captured responses contain real people. Before a file lands in this directory:

  • No credentials of any shape. No key whose name resembles cookie, token, msToken, sessionid, sid_tt, passport, ttwid, odin_tt, verifyFp, s_v_web_id, _signature, authorization or credential. tests/replay/test_platform_contract.py asserts this on every fixture, and CI runs it. A bare secret is not on that list on purpose - TikTok's itemStruct.secret is a post visibility flag, and a check that fires on a legitimate field gets switched off.
  • No real personal data. Nicknames, handles, signatures, comment text and avatar paths are replaced with obviously synthetic values ("Synthetic Creator", synthetic.viewer.a). Ids are replaced with reserved-looking runs of zeros.
  • No live CDN hosts. Media URLs point at *.example-cdn.invalid. .invalid is reserved by RFC 2606 and can never resolve, so no test can accidentally reach out to the network and no expired signed URL leaks a real account's media.

Keep those properties when you replace a fixture with a captured one: capture the structure, then substitute the values.

What each file is for

Same set on both platforms, so the cross-platform normalization can be compared directly (docs/design/11-data-contracts.md, acceptance criterion 1). Douyin adds video_live_photo.json, a shape TikTok has no counterpart for.

File Covers
video_normal.json The happy path: a public video with media, stats, music, hashtags and a location
video_image_album.json Douyin image album / TikTok photo mode - kind=image_album, no video stream, duration_ms=None
video_live_photo.json Douyin only: an album whose slides are live photos, each with a clip under images[i].video, around one still. Structure from a capture of 2026-10-20, values synthetic (#792)
video_deleted.json Withdrawn post: metadata survives, media URLs do not. Must parse, not raise
video_private.json Restricted post, same media-free shape but a different flag
video_long_desc.json Multi-line description with emoji and six hashtags - pins the title/description split
user_profile.json Author profile with counters
user_posts_page1.json Paged post list with has_more and a cursor
comments_with_replies.json Top level comments, one pinned, one carrying inline reply previews
comment_replies_page1.json Reply page, last page (has_more false, so no cursor)
risk_control_empty.json Structurally valid envelope with the payload emptied out - must raise UPSTREAM_RISK_CONTROL, never UPSTREAM_CHANGED
risk_control_captcha.json Captcha / verification decision response - same requirement

The two risk-control files are the ones most worth replacing with real captures first. The parsers' risk signatures are inferred from V4's behaviour, and a signature that is too permissive cools down healthy identities every time somebody looks up a deleted video - the failure mode docs/design/02-identity-pool.md warns about.

Adding a fixture

Per docs/design/13-testing.md: when an endpoint breaks in production, the first step is to save that response here and write a failing replay test. Then fix the parser. Every incident leaves a permanent regression guard behind instead of being forgotten.