48 KiB
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.
Mirror update (2026-10-25): the official directory links Miruro 2.0 at https://miruro.cx, https://miruro.pw, https://miruro.ws, https://miruro.to, https://miruro.tv, and https://miruro.bz; www. hosts are labeled Legacy. Tubular now uses these six explicit origins for catalog/detail requests. Local native ureq and curl probes received 403 text/html from all six autocomplete endpoints; curl returned Cloudflare block pages. Thus live schema/ID compatibility and successful browsing on these mirrors remain unverified. The original cargo run --locked --example miruro_probe now exercises all six and reports Miruro request failed on 6/6 mirrors, rather than stopping at the obsolete browsing origin. Endpoint/schema evidence below remains the historical snapshot, not a claim of current mirror access.
Evidence labels:
- Verified: an actual HTTP response obtained through the research JavaScript
fetchtransport, 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 (historical): 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 was proven for that client and sample, not every transport, network, provider or future request. Inspect HTTP status and Content-Type before decoding; never treat protection pages as empty results. The current client can try the next explicitly listed official mirror, but cannot restore access when every mirror blocks it.
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, 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, search/layout, detail, watch, home, trending, schedule, discover, 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:
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:
- Search and retain the opaque
items[].id/data[].id, title, and artwork. - Fetch detail route data and resolve its
episodeschunk, or fetch watch route data and resolveepisodesData. - Select an actual returned episode number. Detail
episode_number:1mapped toabsolute_number:49; source query uses 1, not 49 or an array index. - Fetch
/api/sources?id=<opaque id>&n=<selected number>immediately before playback. - 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 generate /info/<id>/<slug> and /watch/<id>/<slug>?ep=<number>, with title-derived canonical slugs. 2026-10-22: a placeholder watch slug returned a canonical-route redirect envelope; see the URL handoff. Slugs are not episode identity. Tubular does not derive playback routes from display titles.
Search and discovery contracts
Plain autocomplete
Verified: this request 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:
{
"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:
{
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 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), 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'sssub/subclassification.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.railsresolvesraw/dublatest entries withanimeId,episode,track, Unix-secondairingAt;sideListshasairing/upcomingsummary arrays. UseanimeId, not an event ID, for sources. - Trending:
{items, requestedPage, year, season, years, seasons}. Frontendyear/seasonchange the loaded collection;pageslices it locally into 18-item pages. It is not evidence of server-side cursor pagination. - Schedule page:
{entries, weekStart, weekOffset, now, track}; dependency metadata readsweekandtrack. Plain schedule returns[{id,airingAt,episode,track,media}];startAtandairingAtare Unix seconds. Usemedia.id, not eventid, for sources. Frontend fetchestrack=all, shifts weeks by7*86400, and filters all/sub/dub locally. API sub/dub variants are untested; returned entries includedtrack:"raw". - References:
/api/discover/refsreturnsgenres:[{id,name}]andtags:[{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 readsseason,year,cursor,d; Frontend refresh sets a randomd. These are not a proven paginated API. - Personalized feed: unseeded
/discover/feedreturned empty arrays. Frontend sendsseeds=<comma-separated numeric list IDs>&exclude=<comma-separated IDs>&provider=<list provider>, using up to eight seeds and fallback provideranilist. 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:
{
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:
{
"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:
{
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.
Historical source-resolution and playback research (2026-10-12)
Plain source response
Verified: fresh sources returned the following observed structure. Fields seen only as null have unknown non-null types; this is not an exhaustive guaranteed schema.
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) andsub(hard-subtitled, UIh-sub); Frontend also supportsdub. No separateaudioproperty was observed. Discoveryraw/sub/dubcounts 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) hadstreams:[],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 sharedlanguage:"sp". Preserve labels, not only language codes. VTT was verified; SRT support is frontend-only. - Subtitle/thumbnail URLs can end in
.jpegwhile returningtext/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):
{
"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):
- Use saved sub/dub and provider preferences. For sub, consult each provider's
variantOrderforsub/ssub, then append missing variants; iterateproviderOrderand retain only actual provider/track combinations. - 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. - Flatten selected provider servers' nonempty streams in returned order; start at the first and advance on failure. Retain iframe embeds only for
player:"iframe". - 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 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:falsein 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
hlsoverrides can rewrite segment/init-resource hosts and append query parameters to playlists, including nested playlists. Browser code resolves relative URLs/URI="…"attributes, rewrites playlists intoblob: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, renders
<strmcx-embed>, and callssetSessionwith local player state/source/subtitle/skip data overstrmcx/v1messaging. 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.
SvelteKit route data
Sources: deployed fetch/devalue decoder, route map, 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:
{"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:
{"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:
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Unsupported request parameters."}
Content-Type: application/problem+json, Cache-Control: no-store.
/api/sourceswithout parameters: 400,detail:"Anime and episode are required.".- Known ID with
n=0orn=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 | 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 | 200 / application/json | Eleven summaries; no nextCursor |
| Full browse + its nextCursor request | Both 200 / application/json | Two 24-item pages; different first IDs |
| Unmatched search | 200 / application/json | Empty items; error:null |
| Detail | 200 / text/sveltekit-data | Initial detail + six promised chunks; two episodes |
| Invalid detail | 200 / application/json | Redirect envelope to / |
| Watch ep=1 | 200 / text/sveltekit-data | Initial media + episodesData/sourcesData chunks |
| Source ep=1 | 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 | 200 / text/sveltekit-data | Hero + rail/side-list chunks |
| Trending | 200 / application/json | 97 sampled items; year/season choices |
| Schedule page / plain schedule | Both 200 / application/json | 240 sampled entries |
| References | 200 / application/json | 19 genres, 1,742 tags in sample |
| Discover | 200 / application/json | Lens, 12 spotlights, 12 picks |
| Unseeded feed | 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 | 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.
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):
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; six official 2.0 mirrors; sticky successful origin; 25-second total request budget, at most five seconds per attempt; no automatic redirects |
client.search(query) |
io::Result<Vec<serde_json::Value>>; trimmed, nonempty autocomplete query; no cursor pagination |
client.detail(id, slug) |
io::Result<Detail>; decoded media JSON and episodes: Vec<Value>; validates returned media ID |
Identity::new(id, episode), Identity::from_key(key) |
Validated catalog namespace/episode identity; key() is miruro:<opaque ID>:<episode> |
identity.watch_url() |
Validates identity and produces https://barelystarted.miruro.tv/watch/<opaque ID>/episode?ep=<returned episode> |
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.
The client starts at miruro.cx and remembers the last origin whose response passed HTTP, Content-Type and bounded-body checks. On transport/body failures, unexpected Content-Type, HTTP 301–308, 403, 404, 408 or 5xx, it tries the remaining origins once in directory order. Other HTTP errors, including 400, 401 and 429, stop immediately; rotating mirrors must not circumvent rate limits. Exhaustion reports the attempted/total mirror count and last origin/error. JSON/route decoding and detail-ID validation remain mandatory and do not trigger additional fallback. The directory is not scraped at runtime, and requests are not randomized. Playback URLs deliberately retain barelystarted.miruro.tv: current Terebi admits and resolves only that hostname, so playback failover requires a separate player change.
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:
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:
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.
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:
{"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:<opaque ID>:<canonical episode>. 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 to locate the current bundles; do not hardcode these hashes in the wrapper.