# Miruro API research Observed **2026-10-12**, scoped to **https://barelystarted.miruro.tv**; ownership split updated **2026-10-22**. These are reverse-engineering notes, not an official API specification. Tubular now owns catalog/detail retrieval and stable identity; Terebi resolves episode URLs. Source/CDN investigations below are historical evidence, not the current launch path. Evidence labels: - **Verified:** an actual HTTP response obtained through the research JavaScript `fetch` transport, or the explicitly identified local Rust probe below, 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 distinction:** research requests returned JSON, while a local `curl` source-validation request returned **403 `text/html`**, a Cloudflare block page. A subsequent local Rust `ureq` probe passed the search → detail/episodes → sources → HLS master/subtitle path without bypass machinery. Native access is proven for that client and sample, not every transport, network, provider or future request. Inspect HTTP status and Content-Type before decoding; stop on protection pages rather than treating them as empty results. ## 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 (2026-10-12):** [link helpers][link-helpers] generate `/info//` and `/watch//?ep=`, with title-derived canonical slugs. **2026-10-22:** a placeholder watch slug returned a canonical-route redirect envelope; see the [URL handoff](terebi-url-playback-handoff.md#url-and-canonical-route-handling). Slugs are not episode identity. Tubular does not derive playback routes from display titles. ## 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. ## Historical source-resolution and playback research (2026-10-12) ### 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. The former Tubular direct Sun integration supplied fresh URLs and headers to stream and VTT resources. That projection has been removed; Terebi now owns native URL resolution, HLS and subtitles. Other providers' transforms remain unverified by this research. See [the current integration](#tubular--terebi-integration). ## 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. The initial documentation research did not run application tests; the later native-wrapper verification is recorded below. Neither investigation used a browser, playback, media-segment downloads, account mutations or protection bypass. ## 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()); ``` ## Native Rust catalog wrapper The historical native probe succeeded using Rust 1.90.0, `ureq` 3.4.2, rustls 0.23.45 and the `ring` TLS provider. The current `tubular::miruro` module retains catalog/detail retrieval, the bounded route decoder and stable identity, not source resolution. It uses ordinary TLS verification, no cookies, impersonation, clearance tokens or alternate transport. Current library interface: | Call | Result / contract | | --- | --- | | `Client::default()` | Reusable blocking HTTP agent; 25-second global request timeout; no automatic redirects | | `client.search(query)` | `io::Result>`; trimmed, nonempty autocomplete query; no cursor pagination | | `client.detail(id, slug)` | `io::Result`; decoded `media` JSON and `episodes: Vec`; validates returned media ID | | `Identity::new(id, episode)`, `Identity::from_key(key)` | Validated catalog namespace/episode identity; `key()` is `miruro::` | | `identity.watch_url()` | Validates identity and produces `https://barelystarted.miruro.tv/watch//episode?ep=` | Raw catalog methods preserve JSON metadata without a second deployment-specific schema. IDs/slugs are validated path segments; HTTP/HTML/Content-Type failures remain errors, not empty results. Successful bodies are limited to 2 MiB. Source selection, resource headers and subtitles belong to Terebi. Its private route decoder resolves only the needed detail value and episode promise, using separate value tables and promise IDs. Unrelated sidebar promises are not returned. It supports the JSON-shaped tables verified on this path, **not arbitrary devalue**: tagged special types and negative sentinels are explicit errors, never silently coerced to null. Out-of-range references, rejected/missing/duplicate episode chunks, cycles/excessive depth and decoded-size amplification are rejected. Depth is bounded to 64 and expanded values share a 2 MiB size budget. These are wrapper safety limits, not Miruro server limits. The retained `cargo run --locked --example miruro_probe` smoke now covers search → detail/episodes only, not sources or playback. ### Historical wrapper/source probe evidence (before URL cutover) The output and verification below describe the former source-capable wrapper, not the current example: ```text search: 8 records; selected Miruro ID OERe_a0YS4h-DlsBDOeXwX7HOO6316tw detail: 2 episodes; selected episode 1 sources: 2 tracks; selected ssub/sun; 2 server headers retained hls_master: HTTP 200; Content-Type application/vnd.apple.mpegurl subtitle: HTTP 200; Content-Type text/vtt PASS: native search -> detail/episodes -> sources -> HLS master + subtitle; no media segments or tokens saved ``` The preceding scratch probe also reported HTTP 200 for search (`application/json`), detail (`text/sveltekit-data`, seven records), and sources (`application/json`). Detail decoding found two episodes and selected `episode_number:1`; source decoding found two tracks. The master and default subtitle were fetched using the actual returned `Origin: https://ultracloud.cc` and `Referer: https://ultracloud.cc/`. URL/header necessity and full playback were not tested. The former source-capable example used the documented title and `ssub/sun`, limiting requests to HTTPS `*.keeply.top` without redirects. It fetched no nested playlist, media segment, key, sprite or download page and saved no tokens/bodies. Those resource requests are no longer in the catalog example. Wrapper-extraction verification, before TUI integration: `cargo test --locked` passed all 164 tests, including the episode-promise regression; `cargo clippy --locked --all-targets -- -D warnings` passed; `rustfmt --edition 2024 --check src/lib.rs src/miruro.rs examples/miruro_probe.rs` passed. The decoder regression covers independent tables, out-of-order unrelated chunks, preserved episode/absolute numbers and nulls, unresolved/rejected promises, bad references/cycles, unsupported tags and route redirects. ## Tubular → Terebi integration The startup selector and `--miruro` open the dedicated catalog → series → episodes interface. `miruro_ui` owns navigation, metadata presentation, cached history and collections; `miruro_browser` supplies catalog/episode models and catalog/detail worker requests. Autocomplete returns at most eight titles. Episode Enter passes a stable URL to Terebi; no Tubular source request or projection remains. Both browsers share `src/ui.rs` for layout, search editing/cursor scrolling, selection styling and loading animation. Miruro uses the same full-width results/status chrome and centered metadata/help/removal popups as YouTube, while preserving its own catalog/episode rows and navigation. Press `d` for metadata; arrows/Ctrl-j/k scroll it, while plain j/k change the selected row. Titles normalizing to an empty slug fail explicitly; the wrapper does not guess an alternative detail route. This does not reject accented Latin titles, which use NFD decomposition. ### URL launch and ownership Both providers use `State::progress_file(video)` and `PlayerLaunch::start(&video.id, player, progress_file)`. Miruro's `Identity::from_key(id)?.watch_url()?` produces: ```sh terebi --new-instance \ --url 'https://barelystarted.miruro.tv/watch/OPAQUE_ID/episode?ep=RETURNED_EPISODE' \ --progress-file '/private/session/progress.jsonl' ``` The literal `episode` slug is non-identifying. Terebi's parser uses opaque ID plus episode; metadata acquisition must safely resolve canonical-route redirect envelopes. See the actionable [Terebi URL-playback handoff](terebi-url-playback-handoff.md). Tubular registers the expected stable identity in a private reporting session before launch. Terebi exclusively creates the report (`0600`); session directories remain private (`0700`). CLI exit acknowledges launch acceptance, not media readiness. Retention, the 4,096-session cap, explicit inactive-session cleanup and restart recovery remain unchanged; close corresponding players before removing sessions. No new `source.json`, source-v2 contract, segment file, persistent `Video` fields or collection migration is required. Terebi owns fresh sources, headers, subtitles, playback titles, resume and optional timeline metadata for both direct and Tubular URL launches. Current native URL/progress support is not title/timeline completion: the title is currently `Miruro episode N`; title enrichment and intro/outro progress-bar bands remain Terebi work. Optional metadata failure must not prevent playback. No caller-specific source fallback, automatic skipping or skip buttons are requested. ### Progress and collections Miruro JSONL reports use version 2: ```json {"version":2,"event":"progress","media_identity":{"provider":"miruro","media_id":"opaque_ID","episode":1},"position_seconds":95.0,"duration_seconds":100.0,"completion_eligible":true} ``` Tubular validates the version, namespace, media ID, episode, event and numeric fields, then compares the complete identity against that launch's private session marker. Cross-episode/provider/media records cannot admit or complete another episode. YouTube's canonical `media_url` and version-1 validation remain supported, including its existing unknown-field tolerance. Matching acknowledged positive positions admit currently-watching entries. Watched status retains `completion_eligible && duration_seconds > 0 && position_seconds / duration_seconds >= 0.95`; null duration is ineligible. Seeking near the end can satisfy it. Launch/window closure alone cannot. Duplicate records, partial trailing lines and later reports recovered after Tubular restart retain the existing session-consumption behavior. Collections and watched state use `miruro::`. Catalog rows are not playable episode identities. Saved and old snapshot playback needs only that key, not a canonical slug, title-derived route or stored source URL. Terebi resolves fresh resources; CDN tokens never become collection keys. Each interface filters shared collections by provider. Only actual returned episode numbers are selectable; planned counts do not guarantee source availability. ### Historical source-file integration verification (before URL cutover) The actual Tubular terminal workflow was exercised with isolated `$XDG_CONFIG_HOME` state and the updated Terebi developer bundle selected by `--player`: autocomplete returned eight titles; the selected title returned two episodes; episode 1 was saved to both collections, freshly resolved, and launched through `State::miruro_session` / `PlayerLaunch::start_source`. The private source document retained Origin/Referer headers and nine VTT tracks with English declared default. Session mode `0700` and source/report modes `0600` were checked without logging resource URLs or header values. Actual Terebi v2 records matched the selected opaque ID/episode and acknowledged position advanced from about 7 to 141 seconds, with positive duration and completion eligibility. Tubular admitted that identity to its visible Currently watching list and persisted source-free collection metadata. After Tubular exited successfully, its independent GUI continued appending matching v2 progress and the private source file remained byte-for-byte unchanged. That player/session was intentionally retained for the user's visible subtitle/seek check; no source cleanup was attempted while playback remained active. Initial integrated checks passed: `cargo fmt --check`, all **175 tests**, `cargo clippy --locked --all-targets -- -D warnings`, `cargo build --locked`, and the built executable's `--help`. Regressions cover v1 compatibility, v2 identity/95%/null-duration boundaries, restart/partial-report recovery, source/session permissions and retention, collection routing, stale responses and canonical Unicode slugging. The installed Terebi CLI was older and lacked `--source-file`; the explicit developer-bundle override avoided changing that installation. Startup provider-selection verification: **176 tests**, formatting, Clippy and build passed. Actual terminal runs showed the centered selector, routed both provider choices into their existing interfaces, carried an initial query into Miruro autocomplete, honored remapped selector keys, canceled on an encoded terminal Escape event, and preserved direct `--miruro` startup and noninteractive help/default-config. Bare Escape-byte injection in the automated terminal remained ambiguous; the complete terminal key encoding was exercised instead. Those initial integration runs prove Tubular-to-Terebi launch, acknowledged playback/progress consumption and independent lifetime—not visible subtitle rendering, perceived A/V synchronization, or seeks through the user's installed release. English subtitle appearance/synchronization before and after seeking still requires a visible-app check. ### Dedicated-interface verification The cutover passed **181 tests**, formatting, Clippy with warnings denied and build. Regressions cover cached catalog/series navigation, real episode identities, stale source rejection, provider-scoped collections and filtered removal, matching v2 completion, validated private source sessions, sanitized input/errors and configured controls. Actual terminal runs exercised both chooser routes and direct `--miruro` startup. Miruro showed eight catalog titles with anime metadata, two returned episodes rather than the twelve planned episodes, selected-episode synopsis/date/duration above series metadata, cached back/forward selection and dedicated keyboard help. YouTube retained Search/Feed and its existing video/collection controls. Mixed-provider fixtures showed only the selected provider's entries; removing two Miruro saved episodes left the YouTube sentinel and both Miruro watch-later entries intact, including after restarting into YouTube. Configured F8/F9 controls were exercised; replaced default quit/interrupt keys no longer forced an exit. A freshly resolved episode-2 source retained nine VTT tracks with English default, explicit stream/subtitle header names and the expected opaque episode identity. Terebi's existing `resolved-playback-check` decoded video/audio, advanced playback and passed default-VTT/forward/backward acknowledged cue checks. It also emitted three redacted FFmpeg error lines; their cause was not established. Session/source/report modes were `0700`/`0600`/`0600`; the source hash remained unchanged after Tubular exited. The independent GUI accepted that launch and opened its private report, but the report remained empty: no GUI ready state, advancing GUI progress or current-watching admission is claimed for this run. The visible window needs a human check for a startup prompt or playback error, plus subtitle/seek synchronization. The native diagnostic's pass is not a GUI sign-off. Its active private session was retained rather than deleted. ## Remaining research questions - Will this same native client remain accessible on other networks/deployments? Its sampled path passed; local curl previously failed, and no workaround is implemented. - Is there a stable plain metadata/episode API? The wrapper currently owns the bounded JSON-shaped SvelteKit decoder for the observed 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 by providers other than the verified direct Sun path? - Will other streams require redirects or cross-origin HLS resources that Terebi deliberately rejects? The Terebi team's handoff exercised direct Sun native decode, subtitles and acknowledged seeks, not every provider or comprehensive visible synchronization. - 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