diff --git a/README.md b/README.md index 80dbaf1..ca8c25f 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,8 @@ Video info shows a thumbnail, title, author/publication details, and a clipped d ## Development +Research for a possible future streaming-service integration: [Miruro API notes](docs/miruro-api.md). Miruro support is not implemented. + ```sh cargo fmt --check cargo test --locked diff --git a/docs/miruro-api.md b/docs/miruro-api.md new file mode 100644 index 0000000..8d06444 --- /dev/null +++ b/docs/miruro-api.md @@ -0,0 +1,430 @@ +# Miruro API research + +Observed **2026-10-12**, scoped to **https://barelystarted.miruro.tv**. These are reverse-engineering notes for a possible in-repository wrapper, not an official API specification. Tubular does not yet implement Miruro support. + +Evidence labels: + +- **Verified:** an actual HTTP response obtained through the research JavaScript `fetch` transport, with bounded timeouts and no supplied authentication, cookies, or protection bypass. +- **Frontend:** behavior read from deployed first-party JavaScript; not necessarily a server-side guarantee. +- **Unknown:** not established by those sources or requests. + +**Transport limitation:** research requests returned JSON, but a local `curl` request to the same source-validation URL returned **403 `text/html`**, a Cloudflare block page. Native-wrapper access is therefore unproven. Inspect HTTP status and Content-Type before decoding; stop on protection pages rather than treating them as empty results or adding bypass machinery. Successful research requests do not establish cross-client accessibility. + +## Endpoint map + +All paths below are relative to the scoped base URL. Each row was live-verified unless explicitly labeled Frontend. Route-data responses require the [SvelteKit decoder](#sveltekit-route-data), even when Content-Type is `application/json`. + +| Purpose | GET path and parameters | Response | +| --- | --- | --- | +| Autocomplete | `/api/search/browse?q=&limit=8&type=ANIME` | Plain JSON `{data, next_cursor, has_more}` | +| Full search / browse | `/search/__data.json?q=&cursor=` | Route data: `{items, entityItems, nextCursor, hasMore, filters, …}` | +| Detail / episode metadata | `/info///__data.json` | Streamed route data: `detail`, promised `episodes` and related metadata | +| Watch metadata + initial sources | `/watch///__data.json?ep=` | Streamed route data: `media`, promised `episodesData` and `sourcesData` | +| Fresh episode sources | `/api/sources?id=&n=` | Plain JSON `{episodeNumber, tracks, config, providerOrder}` | +| Home | `/__data.json` | Streamed route data: `hero`, promised `rails` and `sideLists` | +| Trending | `/trending/__data.json` | Route data: `{items, requestedPage, year, season, years, seasons}` | +| Schedule page | `/schedule/__data.json` | Route data: `{entries, weekStart, weekOffset, now, track}` | +| Schedule events | `/api/schedule?startAt=&track=all` | Plain JSON event array | +| Genre/tag references | `/api/discover/refs` | Plain JSON `{genres, tags}` | +| Discover page | `/discover/__data.json` | Route data: `{lens, lensOffset, spotlights, picks, lensCount}` | +| Personalized discover feed | `/discover/feed` | Plain JSON `{spotlights, picks}`; empty arrays verified without seeds | + +Sources: [route map][routes], [search/layout][layout], [detail][detail-client], [watch][watch-client], [home][home-client], [trending][trending-client], [schedule][schedule-client], [discover][discover-client], and the live URLs in the verification ledger. No plain JSON detail/episodes endpoint was identified in those bundles; none was guessed or probed. This is not proof that none exists. Other Miruro domains and third-party `/api/secure/pipe` wrappers are outside this contract. + +## IDs and the search-to-stream flow + +Use Miruro's **opaque catalog ID**, not an external numeric ID: + +```text +Miruro id: OERe_a0YS4h-DlsBDOeXwX7HOO6316tw +AniList: 195516 +MAL: 61987 +Slug: the-apothecary-diaries-season-3 +``` + +Verified search records expose `external_ids` as a map of **string arrays**, for example `{"anilist":["195516"],"mal":["61987"]}`. Unmapped provider keys may be absent. Watch data separately exposes numeric `anilistId`; `media.id` remains the opaque Miruro ID. Numeric-ID reverse lookup and route aliases are unknown. + +The minimum evidenced flow is: + +1. Search and retain the opaque `items[].id` / `data[].id`, title, and artwork. +2. Fetch detail route data and resolve its `episodes` chunk, or fetch watch route data and resolve `episodesData`. +3. Select an actual returned episode number. Detail `episode_number:1` mapped to `absolute_number:49`; source query uses **1**, not 49 or an array index. +4. Fetch `/api/sources?id=&n=` immediately before playback. +5. Choose a returned track/provider/server with direct streams; preserve that server's headers and runtime configuration. Iframe-only servers are not native media URLs. + +**Frontend:** [link helpers][link-helpers] generate `/info//` and `/watch//?ep=`. The canonical title is the shorter trimmed English/Romaji title when both exist, otherwise the available English/Romaji, then native title/ID. Slugging applies Unicode NFD, removes combining accents, lowercases, removes non-ASCII-alphanumeric/non-whitespace/non-hyphen characters, trims, hyphenates whitespace, and collapses repeated hyphens. Display-language preference is separate. Missing/wrong-slug behavior is untested; preserve the deployed helper's convention rather than assuming an arbitrary slug works. + +## Search and discovery contracts + +### Plain autocomplete + +**Verified:** [this request][browse-live] returned eight summary records, `next_cursor` and `has_more:true`. **Frontend:** layout trims `q`, sends literal `limit=8&type=ANIME`, debounces input 300 ms and aborts older requests. Empty input clears local suggestions without a request. + +An abbreviated observed result, with additional fields omitted: + +```json +{ + "data": [{ + "id": "OERe_a0YS4h-DlsBDOeXwX7HOO6316tw", + "external_ids": {"anilist": ["195516"], "mal": ["61987"]}, + "title": { + "romaji": "Kusuriya no Hitorigoto 3rd Season", + "english": "The Apothecary Diaries Season 3", + "native": "薬屋のひとりごと 第3期" + }, + "format": "TV", + "status": "RELEASING", + "episode_count": 12, + "episode_counts": {"raw": 2, "sub": 2, "dub": null}, + "episode_duration_minutes": 24, + "season": "FALL", + "season_year": 2026, + "average_score": 85 + }], + "next_cursor": "", + "has_more": true +} +``` + +Summary fields also observed: `description`, `cover_url`, `cover_color`, `banner_url`, `background_url`, `logo_url`, `source`, `country_of_origin`, `popularity`, `is_adult`, `next_airing:{episode_number,air_at}`, `show_cluster_id`, `show_cluster_position`. Across records, English title, counts, duration, season/year, score, colors/artwork, next airing, and cluster fields could be null. Null is not zero or an empty string. + +**Pagination trap:** sending this response's `next_cursor` as `cursor` on the same otherwise-valid autocomplete request returned **400**, `detail:"Unsupported browse request."`. `/api/search/browse?limit=2&type=ANIME` also returned 400, but that probe changed both limit and omission of `q`, so it does not isolate the cause. Do not promise plain-API pagination or infer supported limits from these probes. + +### Full search, filters, and pagination + +**Verified:** decoded `/search/__data.json?q=apothecary` yielded: + +```text +{ + mode: "anime", items: MediaSummary[], entityItems: [], + nextCursor: null, hasMore: false, limit: 24, cursor: "", + entityPage: 1, filters: { ...string-valued fields... }, + context: null, error: null +} +``` + +It returned eleven summaries. An unmatched query returned `items:[]`, `entityItems:[]`, `nextCursor:null`, `hasMore:false`, `error:null`, HTTP 200. Empty results are distinct from errors. + +Unfiltered `/search/__data.json` returned 24 summaries and a non-null `nextCursor`; a subsequent request with that token as `cursor` returned another 24 summaries with a different first ID, echoed `cursor`, and `hasMore:true`. Retain the returned token opaquely and URL encode it. Token lifetime, binding to filters/cookies and invalid-token handling are unknown. Do not reuse the autocomplete cursor. + +**Frontend:** [search page][search-client] and route dependency metadata expose these page query keys: + +| Keys | Representation / behavior | +| --- | --- | +| `mode` | `anime`, `studio`, `staff`, `character` | +| `q` | Trimmed query; entity input has a client-only 200-character limit | +| `cursor` | Anime pagination; reset when filters change | +| `page` | Numeric entity pagination, not anime pagination | +| `studios`, `staff`, `characters` | Numeric entity-ID context filters | +| `genres`, `format`, `status`, `tracks`, `season`, `source`, `country` | Single or comma-separated selected values; genre names, not reference IDs | +| `adult` | Empty/unset is labeled Non-adult; `true` is labeled Adult | +| `yearFrom`, `yearTo`, `minScore`, `maxScore` | String-valued range bounds; score slider 0–100 | +| `sort` | One frontend option string | + +Frontend option values ([source][filter-options]), **not guaranteed server enums**: + +- `format`: `TV`, `TV_SHORT`, `MOVIE`, `OVA`, `ONA`, `SPECIAL`, `MUSIC`. +- `status`: `RELEASING`, `FINISHED`, `NOT_YET_RELEASED`, `HIATUS`, `CANCELLED`. +- `tracks`: `raw` (**Sub / Raw**), `dub` (**Dub**), `sub` (**Scheduled Subs**). These discovery labels are not the playback API's `ssub`/`sub` classification. +- `season`: `WINTER`, `SPRING`, `SUMMER`, `FALL`; `country`: `JP`, `KR`, `CN`, `TW`. +- `source`: `ORIGINAL`, `MANGA`, `LIGHT_NOVEL`, `WEB_NOVEL`, `NOVEL`, `VISUAL_NOVEL`, `VIDEO_GAME`, `GAME`, `DOUJINSHI`, `ANIME`, `COMIC`, `LIVE_ACTION`, `MULTIMEDIA_PROJECT`, `PICTURE_BOOK`, `OTHER`. +- `sort`: empty (Most Popular), `-score`, `-season_year`, `season_year`, `title`, `-title`, `-episode_count`, `episode_count`, `-duration`, `duration`, `-started_on`, `-ended_on`, `-updated_at`. + +The grid stores `hestia-page-size` in a cookie, computing 24 or 12/15/18/21 by layout. No full-search query `limit` was observed; 24 is a sampled response, not a guaranteed default. Entity responses/filter semantics, cookie acceptance/ranges and server validation were not exercised. + +### Other discovery surfaces + +**Verified**, with frontend parameter behavior noted separately: + +- **Home:** decoded `{hero, now, rails:Promise, sideLists:Promise}`. Hero records are camelCase (`externalIds`, `coverUrl`, `backdropUrl`, `episodeCounts`, etc.), unlike search summaries. `rails` resolves `raw`/`dub` latest entries with `animeId`, `episode`, `track`, Unix-second `airingAt`; `sideLists` has `airing`/`upcoming` summary arrays. Use `animeId`, not an event ID, for sources. +- **Trending:** `{items, requestedPage, year, season, years, seasons}`. **Frontend** `year`/`season` change the loaded collection; `page` slices it locally into 18-item pages. It is not evidence of server-side cursor pagination. +- **Schedule page:** `{entries, weekStart, weekOffset, now, track}`; dependency metadata reads `week` and `track`. Plain schedule returns `[{id,airingAt,episode,track,media}]`; `startAt` and `airingAt` are Unix **seconds**. Use `media.id`, not event `id`, for sources. **Frontend** fetches `track=all`, shifts weeks by `7*86400`, and filters all/sub/dub locally. API sub/dub variants are untested; returned entries included `track:"raw"`. +- **References:** `/api/discover/refs` returns `genres:[{id,name}]` and `tags:[{id,name,category,parent_id,parent_name}]`; parent fields may be null. Search uses genre names. A tag filter query was not observed in full-search controls. +- **Discover page:** `{lens:{id,label,tagline,recipe}, lensOffset, spotlights, picks, lensCount}`. Spotlights are camelCase; picks use snake_case summaries. Dependency metadata reads `season`, `year`, `cursor`, `d`; **Frontend** refresh sets a random `d`. These are not a proven paginated API. +- **Personalized feed:** unseeded `/discover/feed` returned empty arrays. **Frontend** sends `seeds=&exclude=&provider=`, using up to eight seeds and fallback provider `anilist`. These IDs belong to the external list service, not Miruro's opaque catalog. The UI gates personalization on sign-in; seeded-request authentication and schema are unknown. + +## Detail, episodes, and watch data + +**Verified:** detail route initially decodes to: + +```text +{ + detail: MediaDetail, id, currentSlug, tvdbBackgrounds, + characters: Promise, relations: Promise, recommendations: Promise, + artworks: Promise, show: Promise, episodes: Promise +} +``` + +`detail` extends summary metadata with `alternate_titles:[{value,kind,language}]`, `started_on`, `ended_on`, `broadcast`, `release_schedules`, `mal_score`, `mal_members`, `aired_season`, `trailer_youtube_id`, `genres`, `tags:[{name,rank,category,is_spoiler}]`, `studios:[{id,name,is_main,is_animation_studio}]`, `franchise`, `updated_at`. Dates and some nested identifiers/counts can be null. Broadcast/release schedules describe airing metadata, not stream provider URLs or playback availability. + +The sample title's resolved `episodes` contained two records despite a planned `episode_count:12`. Abbreviated episode with all observed keys: + +```json +{ + "episode_number": 1, "absolute_number": 49, "aired_season_number": 3, + "kind": "regular", "canon_type": null, "title": "Locusts", + "synopsis": "A locust plague is looming, and mysteriously, a book on insects belonging to the Shi clan appears.", + "thumbnail_url": "https://image.tmdb.org/t/p/original/v2k4Nj11mnVwyUhqktOmYr3E4yV.jpg", + "aired_on": "2026-10-02", "duration_seconds": 1500, "finale_type": null, + "skip_times": [ + {"kind":"op","start_seconds":63.313,"end_seconds":153.237}, + {"kind":"ed","start_seconds":1335.252,"end_seconds":1425.384} + ] +} +``` + +Do not synthesize playable episodes from planned counts. These records do not carry per-track availability flags; request actual sources for the selected episode. Null dub count does not conclusively prove no playable dub. + +Watch route initially has `{media, episodesData:Promise, sourcesData:Promise, anilistId, externalIds, episodeNumber, tvdbBackgrounds}`. `media` is adapted camelCase: `episodeCounts`, `duration` (minutes), `seasonYear`, `startDate`, `coverImage`, `nextAiringEpisode`, etc.; it is not interchangeable with `detail`. + +Resolved watch `episodesData` has: + +```text +{ + episodes: [{id:"1",number:1,title,image,airDate,duration:1500,description,filler:false}, ...], + currentEpisode: , prevEpisode: null, nextEpisode: , + aniskip: [ + {episode:1,type:"op",start:63.313,end:153.237}, + {episode:1,type:"ed",start:1335.252,end:1425.384} + ] +} +``` + +Watch `episodes[].id` is the number rendered as a string, **not a provider episode ID**. `aniskip` times are seconds; they differ in naming from detail `skip_times`. `/api/sources` does not carry those fields. **Frontend:** watch converts `op`/`mixed-op` to intro and `ed`/`mixed-ed` to outro, filters wrong episodes/invalid intervals and optional provider/vote/duration constraints, then recomputes skips using player-reported duration. No separate skip endpoint was found. Missing/invalid episode behavior and last-episode boundaries are untested. + +## Source-resolution and playback contract + +### Plain source response + +**Verified:** [fresh sources][sources-live] returned the following observed structure. Fields seen only as null have unknown non-null types; this is not an exhaustive guaranteed schema. + +```text +Sources { episodeNumber:number, tracks:Track[], config:{provider:ProviderConfig}, providerOrder:string[] } +Track { track:string, providers:Provider[] } +Provider { provider:string, servers:Server[], subtitles:Subtitle[], thumbnails:null|{vtt:string,sprite:null}, downloads:Download[] } +Server { server:string, headers:null|{header:string}, embed:null|{url:string}, streams:Stream[] } +Stream { url:string, format:string, quality:null, resolution:null, codec:null, embed:null } +Subtitle { language:string, label:string, file:string, format:string, default:boolean } +Download { url:string, quality:null, size:null, fansub:null } +``` + +- Sample tracks were `ssub` (soft subtitles) and `sub` (hard-subtitled, UI `h-sub`); **Frontend** also supports `dub`. No separate `audio` property was observed. Discovery `raw/sub/dub` counts are a different vocabulary. +- Sample direct streams used `format:"hls"`. **Frontend** recognizes HLS strings/MIME types, DASH/MPD, MP4, WebM and Ogg, and defaults a missing format to HLS. Those are consumer inputs, not verified server formats. +- Quality/resolution/codec were null, yet a fetched master advertised `RESOLUTION=1920x1080`; do not equate null metadata with absent HLS renditions. +- An iframe-only server (`bun`) had `streams:[]`, `headers:null`, `embed:{url}`. An embed page is not a native stream. +- Subtitle language strings were provider-specific (`en`/`eng`, `sp`, `po`); distinct Spanish labels shared `language:"sp"`. Preserve labels, not only language codes. VTT was verified; SRT support is frontend-only. +- Subtitle/thumbnail URLs can end in `.jpeg` while returning `text/vtt`. Classify by format/content, not extension. Relative thumbnail sprite references must be resolved against the VTT URL. +- Download URLs can be download pages; their being raw media files is unproven. +- Source URLs may contain tokens. Fetch fresh before playing; do not persist tokens as catalog identity or log full URLs. Expiry and IP binding are unknown. + +A sanitized projection (URL values deliberately redacted): + +```json +{ + "episodeNumber": 1, + "tracks": [{"track":"ssub","providers":[{ + "provider":"sun", + "servers":[{ + "server":"icarus-2-1", + "headers":{"Origin":"https://ultracloud.cc","Referer":"https://ultracloud.cc/"}, + "embed":null, + "streams":[{"url":"","format":"hls","quality":null,"resolution":null,"codec":null,"embed":null}] + }], + "subtitles":[{"language":"en","label":"English","file":"","format":"vtt","default":true}], + "thumbnails":{"vtt":"","sprite":null} + }]}] +} +``` + +This projects one server/provider and omits config, other tracks/providers and downloads; it is not a complete response. Preserve the actual returned header strings without normalization. + +### Runtime config and selection + +**Verified:** response `config` entries contain `capabilities:{sub,ssub,download,skip_times,thumbnails}`, `visible`, `player` (`native`/`iframe` observed), `parent`, `proxy`, `variantOrder`, `cors`, `fallback`, and optional `hls` / `downloadWorker` fields. `proxy` was false or `{rotate:boolean,segments:boolean}`; `hls` contained `segments:{enabled,origin}` and `query:{enabled,params}`. Non-null fallback shape is unknown. + +Observed `providerOrder`: `sun, bee, pewe, hop, ally, kiwi, moo, bonk, knob, nun, bun, cog, twin, telli`. Config includes providers absent from the selected episode. Treat names/order as runtime data. Capability flags and `cors` were false even where streams/subtitles/downloads/thumbnails were returned; they are not verified availability gates. + +**Frontend** ([watch client][watch-client]): + +1. Use saved sub/dub and provider preferences. For sub, consult each provider's `variantOrder` for `sub`/`ssub`, then append missing variants; iterate `providerOrder` and retain only actual provider/track combinations. +2. Prefer exact `provider:track`, then same provider, then first available. If a requested language has no entries, try the opposite language. This is existing UI behavior, not a recommendation to silently change a native user's language. +3. Flatten selected provider servers' nonempty streams in returned order; start at the first and advance on failure. Retain iframe embeds only for `player:"iframe"`. +4. Browser stream projection extracts Referer case-insensitively, but raw API also returns Origin. A native wrapper should retain the entire header map. + +### Direct resources, proxies, and browser player + +**Verified:** using fresh Sun stream/subtitle/thumbnail URLs and the server's Origin/Referer headers, research GETs obtained an HLS master (200 `application/vnd.apple.mpegurl`) and two VTT bodies (200 `text/vtt`). No nested rendition playlist, media segment, key, sprite, download page or player was exercised. Whether either header is mandatory was not tested. **This proves resource retrieval, not full playback.** + +**Frontend:** deployment HTML [environment][homepage] identifies `https://strm.cx`, proxy bases `https://s1.keeply.top/` and `https://s2.keeply.top/`, and a public proxy obfuscation key. Watch source shows: + +- Sun uses direct streams (`proxy:false` in the sample). Bee/Hop use `{rotate:false,segments:true}`; a bare direct URL is not proven equivalent for these providers. +- Browser proxy paths encode URL/Referer and use `/pl.m3u8`, `/sub.vtt`, `/seg.jpg`. This is a private deployment convention, not `/api/secure/pipe`. No replay of that obfuscation protocol was needed for direct-resource verification. +- Provider `hls` overrides can rewrite segment/init-resource hosts and append query parameters to playlists, including nested playlists. Browser code resolves relative URLs/`URI="…"` attributes, rewrites playlists into `blob:` URLs, drops failed variants, and bounds recursion, size and time. Those blob URLs are browser-local, not reusable native source URLs. +- The browser loads [strm.cx/client.js][strm-client], renders ``, and calls `setSession` with local player state/source/subtitle/skip data over `strmcx/v1` messaging. This was **not required for the tested source or direct-resource GETs**. It is not evidence of an authenticated API session prerequisite. +- strm client contains a best-effort `POST /v1/embeds`; it was source-inspected only, not invoked. Iframe providers and unrelated comment/telemetry endpoints were not exercised. + +For a future native player: preserve fresh query tokens, per-server headers and relevant provider config; correctly resolve nested/relative HLS URLs. Do not apply one provider's headers indiscriminately to another provider's subtitles/thumbnails. Terebi currently receives only `--new-instance --url … --progress-file PATH` in Tubular ([existing usage](../README.md#usage)); custom headers, external subtitles, HLS overrides, native seeking and full Miruro playback remain unverified. + +## SvelteKit route data + +Sources: [deployed fetch/devalue decoder][transport-client], [route map][routes], and live detail/watch/home responses. + +Append `/__data.json` to the page path and retain query parameters. The browser transport can add `x-sveltekit-invalidated=` and `x-sveltekit-trailing-slash=1`; neither was necessary in the research requests. + +An **illustrative**, minimal envelope: + +```json +{"type":"data","nodes":[{"type":"data","data":[{"someKey":1},"someValue"],"uses":{}},null]} +``` + +Actual responses had layout at node 0, route at node 1. Each node's `data` is its own **devalue flattened table**, with root at index 0. Object-property and ordinary-array numeric values are references into that table, not literal payload numbers. Null is a table value; undefined uses sentinel `-1`. Decoder also handles array holes (`-2`), NaN/infinities/negative zero (`-3`…`-6`), sparse arrays (`-7`) and tagged Date/Set/Map/BigInt/other types. A generic recursive integer dereferencer is not a complete decoder. Keep layout configuration separate from route models. + +Promised fields are tagged `['Promise', ]`. Streamed `text/sveltekit-data` is **newline-delimited JSON**, not one JSON object: + +```text +{"type":"data","nodes":[...]} +{"type":"chunk","id":,"data":} +``` + +Resolve by decoded promise ID; each chunk has a new table. Arrival order is not field order: sample detail chunks arrived 2,1,6,3,4,5. IDs are response-local, not constants. Consume the full body or track promises incrementally. Frontend also decodes `error` chunks to reject a promise; that error path was not live-exercised. Top-level redirect envelopes require separate handling. + +**Verified:** `/info/invalid/invalid/__data.json` returned HTTP **200** with `{"type":"redirect","location":"/"}`. Status 200 therefore does not prove a successful detail response. Empty search arrays, redirect envelopes, API problem responses and transport failures must remain distinct. + +## Errors and caching + +**Verified API problem shape:** + +```json +{"type":"about:blank","title":"Bad Request","status":400,"detail":"Unsupported request parameters."} +``` + +`Content-Type: application/problem+json`, `Cache-Control: no-store`. + +- `/api/sources` without parameters: 400, `detail:"Anime and episode are required."`. +- Known ID with `n=0` or `n=not-a-number`: 400, `detail:"Unsupported request parameters."`. +- `id=invalid&n=1`: same 400/detail; this does not establish behavior for a syntactically valid nonexistent ID. +- Unsupported tested browse requests: 400, `detail:"Unsupported browse request."`. +- Local curl of known ID with `n=0`: Cloudflare **403 HTML**, before the JSON validation behavior seen in research transport. The cause/client fingerprint/IP distinction is unknown. + +Source success and source errors had no `Access-Control-Allow-Origin` header. The direct Sun master/subtitle responses had ACAO `https://ultracloud.cc`. Neither observation guarantees general browser cross-origin access. Server rate limits, stable error taxonomy, authentication guarantees, token expiry and protection behavior are unknown; no rate/protection stress tests occurred. + +Observed Cache-Control values: + +| Surface | Cache-Control | +| --- | --- | +| Plain browse success | `public, max-age=300, s-maxage=900, stale-while-revalidate=3600, stale-if-error=86400` | +| Plain schedule | `public, max-age=0, s-maxage=60, stale-while-revalidate=60` | +| Discover references | `public, max-age=0, s-maxage=300, stale-while-revalidate=60` | +| Source success/errors and normal route data | `no-store` | +| Missing-detail redirect; discover feed | `private, no-store` | + +Respect current response headers; these are sampled deployment values, not constants. Metadata cacheability does not make stream URLs durable. + +## Verification ledger + +All research data GETs below used `fetch` with 20–25-second timeouts, no supplied auth/cookie/Origin/Referer unless stated. JS bundles used 15–20-second bounds. These are research-transport results, **not successful local curl/native playback claims**. Stream tokens and pagination cursors are intentionally not recorded. + +| Request | Status / Content-Type | Observed result | +| --- | --- | --- | +| [Autocomplete][browse-live] | 200 / application/json | Eight summaries; next_cursor; has_more | +| Browse without q, changed limit 2 | 400 / application/problem+json | Unsupported browse request | +| Valid browse + own returned cursor as `cursor` | 400 / application/problem+json | Unsupported browse request | +| [Full query search][search-live] | 200 / application/json | Eleven summaries; no nextCursor | +| [Full browse][search-all-live] + its nextCursor request | Both 200 / application/json | Two 24-item pages; different first IDs | +| [Unmatched search][empty-search-live] | 200 / application/json | Empty items; error:null | +| [Detail][detail-live] | 200 / text/sveltekit-data | Initial detail + six promised chunks; two episodes | +| [Invalid detail][missing-detail-live] | 200 / application/json | Redirect envelope to `/` | +| [Watch ep=1][watch-live] | 200 / text/sveltekit-data | Initial media + episodesData/sourcesData chunks | +| [Source ep=1][sources-live] | 200 / application/json | ssub/sub, direct + iframe providers, runtime config | +| Source missing parameters / n=0 / nonnumeric n / invalid ID | Each 400 / application/problem+json | Validation details above | +| [Home][home-live] | 200 / text/sveltekit-data | Hero + rail/side-list chunks | +| [Trending][trending-live] | 200 / application/json | 97 sampled items; year/season choices | +| [Schedule page][schedule-live] / [plain schedule][events-live] | Both 200 / application/json | 240 sampled entries | +| [References][refs-live] | 200 / application/json | 19 genres, 1,742 tags in sample | +| [Discover][discover-live] | 200 / application/json | Lens, 12 spotlights, 12 picks | +| [Unseeded feed][feed-live] | 200 / application/json | Empty spotlights/picks | +| Fresh ssub/Sun first-server master, with server Origin/Referer | 200 / application/vnd.apple.mpegurl | `#EXTM3U`; one 1920x1080 variant; no redirect | +| Same provider default subtitle / thumbnail VTT, same headers | Both 200 / text/vtt | WEBVTT cues; thumbnail sprite coordinates; no redirect | +| [strm client][strm-client] | 200 / text/javascript | Source inspected, not executed | +| Local shell curl, known source ID with n=0 | 403 / text/html | Cloudflare block page | + +One worker's homepage fetch without a trailing slash returned nginx 404, while the trailing-slash HTML read succeeded. This unresolved transport/URL discrepancy is not evidence of an API rule. No browser testing, playback, media-segment downloads, account mutations, protection bypass, or application tests were performed for this documentation-only change. + +## Bounded reproduction requests + +These curl recipes show request syntax; **successful execution with local curl is not established**. One local curl probe was blocked. Check status/Content-Type and stop on a block page. Example IDs/dates describe the observed catalog snapshot; titles and availability can change. + +```sh +BASE='https://barelystarted.miruro.tv' +ID='OERe_a0YS4h-DlsBDOeXwX7HOO6316tw' +SLUG='the-apothecary-diaries-season-3' + +curl --silent --show-error --max-time 25 -i --get "$BASE/api/search/browse" \ + --data-urlencode 'q=apothecary' --data-urlencode 'limit=8' --data-urlencode 'type=ANIME' + +curl --silent --show-error --max-time 25 -i --get "$BASE/search/__data.json" \ + --data-urlencode 'q=apothecary' + +curl --silent --show-error --max-time 25 -i "$BASE/info/$ID/$SLUG/__data.json" + +curl --silent --show-error --max-time 25 -i --get "$BASE/watch/$ID/$SLUG/__data.json" \ + --data-urlencode 'ep=1' + +curl --silent --show-error --max-time 25 -i --get "$BASE/api/sources" \ + --data-urlencode "id=$ID" --data-urlencode 'n=1' +``` + +For full-search pagination, decode `nextCursor` from `/search/__data.json`, then request the same route with `--data-urlencode "cursor=$CURSOR"`, retaining filters/cookie context. For current schedule, decode `weekStart` from `/schedule/__data.json`, then request `/api/schedule` with `startAt=&track=all`. To inspect media resources, select fresh stream/subtitle URLs from the source response and use that server's actual headers, a bounded GET and in-memory results; do not save tokenized URLs to these notes. + +Equivalent plain-JSON research request (runtime/network portability is unproven): + +```js +const response = await fetch( + 'https://barelystarted.miruro.tv/api/search/browse?q=apothecary&limit=8&type=ANIME', + { signal: AbortSignal.timeout(20_000) } +); +console.log(response.status, response.headers.get('content-type')); +console.log(await response.text()); +``` + +## Open questions before implementation + +- Can the real native HTTP client access this deployment without being blocked? Local curl failed; no workaround has been implemented or tested. +- Is there a stable plain metadata/episode API, or must the wrapper own a SvelteKit/devalue decoder tied to this deployment? +- What are server-side filter/enum constraints, multi-value semantics, cursor lifetime, page-size-cookie limits, numeric-ID lookups and slug behavior? +- What happens for unavailable episodes, empty tracks, provider failures, bad cursors, rejected promise chunks and syntactically valid missing IDs? +- Which headers/config transforms are required per provider, and can Terebi propagate them to HLS playlists/segments/keys and external subtitles? +- Are direct streams usable end-to-end, including rendition/segment access and seeking? Only master/subtitle/thumbnail retrieval was verified. +- What are seeded discovery authentication requirements, rate limits, token expiry/IP binding and content-use permissions? Reachable internal endpoints do not imply a supported third-party integration or permission to redistribute content. + +Hashed JavaScript URLs below identify the inspected deployment, not stable versions. For later research, re-read the homepage's entry-script imports and [route map][routes] to locate the current bundles; do not hardcode these hashes in the wrapper. + +[routes]: https://barelystarted.miruro.tv/_app/immutable/entry/app.BSfZufle.js +[layout]: https://barelystarted.miruro.tv/_app/immutable/nodes/0.TgQ_FRp6.js +[search-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/5.B8psgBrv.js +[filter-options]: https://barelystarted.miruro.tv/_app/immutable/chunks/BFtFCiqs.js +[link-helpers]: https://barelystarted.miruro.tv/_app/immutable/chunks/DRUtYxUt.js +[detail-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/6.BPCeS58O.js +[watch-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/11.CVVa7Rmv.js +[home-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/2.D7a6D6w9.js +[trending-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/9.CWTtBRa5.js +[schedule-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/7.BJ4_XLlf.js +[discover-client]: https://barelystarted.miruro.tv/_app/immutable/nodes/12.C2zcxgjP.js +[transport-client]: https://barelystarted.miruro.tv/_app/immutable/chunks/DGYr79W3.js +[homepage]: https://barelystarted.miruro.tv/ +[strm-client]: https://strm.cx/client.js +[browse-live]: https://barelystarted.miruro.tv/api/search/browse?q=apothecary&limit=8&type=ANIME +[search-live]: https://barelystarted.miruro.tv/search/__data.json?q=apothecary +[search-all-live]: https://barelystarted.miruro.tv/search/__data.json +[empty-search-live]: https://barelystarted.miruro.tv/search/__data.json?q=zzzznoanimezzzz +[detail-live]: https://barelystarted.miruro.tv/info/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3/__data.json +[missing-detail-live]: https://barelystarted.miruro.tv/info/invalid/invalid/__data.json +[watch-live]: https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3/__data.json?ep=1 +[sources-live]: https://barelystarted.miruro.tv/api/sources?id=OERe_a0YS4h-DlsBDOeXwX7HOO6316tw&n=1 +[home-live]: https://barelystarted.miruro.tv/__data.json +[trending-live]: https://barelystarted.miruro.tv/trending/__data.json +[schedule-live]: https://barelystarted.miruro.tv/schedule/__data.json +[events-live]: https://barelystarted.miruro.tv/api/schedule?startAt=1791763200&track=all +[refs-live]: https://barelystarted.miruro.tv/api/discover/refs +[discover-live]: https://barelystarted.miruro.tv/discover/__data.json +[feed-live]: https://barelystarted.miruro.tv/discover/feed