431 lines
34 KiB
Markdown
431 lines
34 KiB
Markdown
# 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=<query>&limit=8&type=ANIME` | Plain JSON `{data, next_cursor, has_more}` |
|
||
| Full search / browse | `/search/__data.json?q=<query>&cursor=<returned cursor>` | Route data: `{items, entityItems, nextCursor, hasMore, filters, …}` |
|
||
| Detail / episode metadata | `/info/<id>/<slug>/__data.json` | Streamed route data: `detail`, promised `episodes` and related metadata |
|
||
| Watch metadata + initial sources | `/watch/<id>/<slug>/__data.json?ep=<number>` | Streamed route data: `media`, promised `episodesData` and `sourcesData` |
|
||
| Fresh episode sources | `/api/sources?id=<id>&n=<number>` | 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=<Unix seconds>&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=<opaque id>&n=<selected number>` 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/<id>/<slug>` and `/watch/<id>/<slug>?ep=<number>`. 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": "<redacted returned 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=<comma-separated numeric list IDs>&exclude=<comma-separated IDs>&provider=<list 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: <episode 1>, prevEpisode: null, nextEpisode: <episode 2>,
|
||
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":"<fresh HLS URL>","format":"hls","quality":null,"resolution":null,"codec":null,"embed":null}]
|
||
}],
|
||
"subtitles":[{"language":"en","label":"English","file":"<fresh VTT URL>","format":"vtt","default":true}],
|
||
"thumbnails":{"vtt":"<fresh thumbnail VTT URL>","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 `<strmcx-embed>`, 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=<bit string>` 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', <table index referencing promise ID>]`. Streamed `text/sveltekit-data` is **newline-delimited JSON**, not one JSON object:
|
||
|
||
```text
|
||
{"type":"data","nodes":[...]}
|
||
{"type":"chunk","id":<promise ID>,"data":<independent devalue table>}
|
||
```
|
||
|
||
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=<that value>&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
|