38 KiB
Miruro API research
Observed 2026-10-12, scoped to https://barelystarted.miruro.tv. These are reverse-engineering notes, not an official API specification. Tubular now has a native API module and runnable smoke example, but no Miruro TUI or Terebi integration.
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: 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, 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: 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 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.
Source-resolution and playback contract
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.
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); custom headers, external subtitles, HLS overrides, native seeking and full Miruro playback remain unverified.
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 wrapper and verified probe
The local probe succeeded using Rust 1.90.0, ureq 3.4.2, rustls 0.23.45 and the ring TLS provider. Its isolated scratch project was removed after the exercised path was extracted into src/miruro.rs. The client uses ordinary TLS verification, no cookies, no browser impersonation, no clearance tokens and no retry/alternate transport.
Library interface, exposed as tubular::miruro:
| Call | Result / contract |
|---|---|
Client::default() |
Reusable blocking HTTP agent; 25-second global request timeout; 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 |
client.sources(id, episode) |
io::Result<Value>; fresh full response preserving headers/config/subtitles; validates positive episode and returned episodeNumber/tracks |
The wrapper does not choose providers/languages or implement a player. It preserves JSON metadata rather than freezing every deployment-specific field into a second schema. IDs/slugs are validated as nonempty ASCII alphanumeric/hyphen/underscore path segments. HTTP failures remain errors (PermissionDenied for 401/403); HTML and unexpected Content-Types are rejected, not converted to empty results. No tokenized URLs appear in its error messages. Successful bodies are limited to 2 MiB.
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.
Run the retained live smoke:
cargo run --locked --example miruro_probe
Actual output from the extracted wrapper:
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 example deliberately uses the documented title and ssub/sun provider, and limits resource requests to HTTPS *.keeply.top without redirects. It fails explicitly if that sample disappears or changes instead of silently choosing a different provider. It fetches no nested playlist, media segment, key, sprite or download page, and saves no tokens/bodies. This is an explicit network smoke command, not a network-dependent unit test.
Verification: cargo test --locked passed all 164 tests, including the new 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.
Open questions before TUI/player integration
- 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 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 to locate the current bundles; do not hardcode these hashes in the wrapper.