Files
tubular/docs/miruro-api.md
T

46 KiB
Raw Blame History

Miruro API research

Observed 2026-10-12, scoped to https://barelystarted.miruro.tv. These are reverse-engineering notes, not an official API specification. Tubular includes a native API module, Miruro catalog/episode browsing, and direct Sun playback through Terebi's implemented source/progress contracts.

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, 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:

  1. Search and retain the opaque items[].id / data[].id, title, and artwork.
  2. Fetch detail route data and resolve its episodes chunk, or fetch watch route data and resolve episodesData.
  3. Select an actual returned episode number. Detail episode_number:1 mapped to absolute_number:49; source query uses 1, not 49 or an array index.
  4. Fetch /api/sources?id=<opaque id>&n=<selected number> immediately before playback.
  5. Choose a returned track/provider/server with direct streams; preserve that server's headers and runtime configuration. Iframe-only servers are not native media URLs.

Frontend: link helpers 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's ssub/sub classification.
  • season: WINTER, SPRING, SUMMER, FALL; country: JP, KR, CN, TW.
  • source: ORIGINAL, MANGA, LIGHT_NOVEL, WEB_NOVEL, NOVEL, VISUAL_NOVEL, VIDEO_GAME, GAME, DOUJINSHI, ANIME, COMIC, LIVE_ACTION, MULTIMEDIA_PROJECT, PICTURE_BOOK, OTHER.
  • sort: empty (Most Popular), -score, -season_year, season_year, title, -title, -episode_count, episode_count, -duration, duration, -started_on, -ended_on, -updated_at.

The grid stores hestia-page-size in a cookie, computing 24 or 12/15/18/21 by layout. No full-search query limit was observed; 24 is a sampled response, not a guaranteed default. Entity responses/filter semantics, cookie acceptance/ranges and server validation were not exercised.

Other discovery surfaces

Verified, with frontend parameter behavior noted separately:

  • Home: decoded {hero, now, rails:Promise, sideLists:Promise}. Hero records are camelCase (externalIds, coverUrl, backdropUrl, episodeCounts, etc.), unlike search summaries. rails resolves raw/dub latest entries with animeId, episode, track, Unix-second airingAt; sideLists has airing/upcoming summary arrays. Use animeId, not an event ID, for sources.
  • Trending: {items, requestedPage, year, season, years, seasons}. Frontend year/season change the loaded collection; page slices it locally into 18-item pages. It is not evidence of server-side cursor pagination.
  • Schedule page: {entries, weekStart, weekOffset, now, track}; dependency metadata reads week and track. Plain schedule returns [{id,airingAt,episode,track,media}]; startAt and airingAt are Unix seconds. Use media.id, not event id, for sources. Frontend fetches track=all, shifts weeks by 7*86400, and filters all/sub/dub locally. API sub/dub variants are untested; returned entries included track:"raw".
  • References: /api/discover/refs returns genres:[{id,name}] and tags:[{id,name,category,parent_id,parent_name}]; parent fields may be null. Search uses genre names. A tag filter query was not observed in full-search controls.
  • Discover page: {lens:{id,label,tagline,recipe}, lensOffset, spotlights, picks, lensCount}. Spotlights are camelCase; picks use snake_case summaries. Dependency metadata reads season, year, cursor, d; Frontend refresh sets a random d. These are not a proven paginated API.
  • Personalized feed: unseeded /discover/feed returned empty arrays. Frontend sends seeds=<comma-separated numeric list IDs>&exclude=<comma-separated IDs>&provider=<list provider>, using up to eight seeds and fallback provider anilist. These IDs belong to the external list service, not Miruro's opaque catalog. The UI gates personalization on sign-in; seeded-request authentication and schema are unknown.

Detail, episodes, and watch data

Verified: detail route initially decodes to:

{
  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) 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):

{
  "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):

  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 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, renders <strmcx-embed>, and calls setSession with local player state/source/subtitle/skip data over strmcx/v1 messaging. This was not required for the tested source or direct-resource GETs. It is not evidence of an authenticated API session prerequisite.
  • strm client contains a best-effort POST /v1/embeds; it was source-inspected only, not invoked. Iframe providers and unrelated comment/telemetry endpoints were not exercised.

Tubular's direct Sun integration preserves fresh URLs and explicitly supplies the selected server's headers to both its stream and verified VTT subtitle resources. Terebi owns native HLS resolution and subtitle loading; other providers' proxies, headers and playlist transforms remain unverified and unsupported. See the adopted integration contract.

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/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 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
client.source_document(id, episode, title) io::Result<SourceDocument>; fresh validated direct ssub/sun HLS/VTT projection for Terebi
Identity::new(id, episode), Identity::from_key(key) Validated catalog namespace/episode identity; key() is miruro:<opaque ID>:<episode>

The raw methods preserve JSON metadata rather than freezing every deployment-specific field into a second schema. source_document deliberately selects only direct Sun soft-subtitle playback; it does not guess another provider or implement a player. 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.

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

tubular offers a centered YouTube/Miruro startup selector; an initial query follows the chosen provider. tubular --miruro "apothecary" bypasses it and starts the permanent Miruro search tab directly. Catalog Enter opens actual returned episodes; episode Enter resolves fresh sources off-thread. Autocomplete is limited to eight titles, without invented pagination. One worker handles bounded network work; changing views/queries invalidates stale responses rather than launching obsolete selections. Canonical title/slug rules follow the deployed helper, including Unicode NFD normalization.

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.

Source document and launch

Client::source_document chooses the first nonempty server/first direct HLS stream from the returned ssub track's sun provider. It requires native playback, proxy:false, and no enabled HLS host/query rewriting. Missing/unsupported sources are errors; no alternate language/provider, embed, proxy or refresh fallback is added.

Terebi source-v1 field Projection
url Selected stream URL, unchanged, including its query tokens
headers Selected server map, without trimming or rewriting
identity {provider:"miruro", media_id:<opaque catalog ID>, episode:<returned episode_number>}; not slug/external ID/absolute number/array index/playback provider
title Episode display title
subtitles Selected Sun provider's VTT tracks; other external formats excluded
Track url, language, label, default Returned file and subtitle metadata
Track headers Explicit selected Sun server map, verified for Sun resources only; no general inheritance rule

Projection validates HTTP(S) resource URLs, header syntax/values, case-insensitive duplicate headers, identities, control characters, 32-track/64-header limits and at most one default subtitle. Session creation also enforces the 1 MiB serialized source-document limit. Miruro media IDs allow at most 512 ASCII letters/digits/-/_; episodes range from 1 through 2147483647.

State::miruro_session binds the source identity to the selected episode, creates a unique private session directory (0700), writes immutable source.json (0600) and metadata/identity snapshots, and returns a nonexistent progress destination. PlayerLaunch::start_source passes absolute filesystem paths, not resource URLs/header values:

terebi --new-instance --source-file '/private/session/source.json' \
  --progress-file '/private/session/progress.jsonl'

Terebi exclusively creates the report (0600). CLI exit acknowledges only launch acceptance, not successful media opening or completion. Tubular retains source/report files across launcher exit, final events, and Tubular shutdown because the independent GUI may still need them. The existing 4,096-session cap and explicit inactive-session cleanup apply; close the corresponding players before removing their session directories. Private source documents contain expiring URLs/headers; watched/collection state contains only stable identity and display metadata.

Terebi's implemented transport preserves headers on master/variant/segment/key/init requests and probes Sun's image-suffixed media/subtitle resources by content/declared VTT format. All media redirects and foreign-origin HLS resources are rejected; subtitle redirects must stay on their initial origin. Terebi does not refresh expired sources. These restrictions remain visible errors, not silent provider switches.

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.

Watch later, saved videos, currently watching and watched state accept stable episode keys miruro:<opaque ID>:<canonical episode>. Catalog rows (miruro-series:<ID>) are not saveable/playable episode identities. Saved episode playback resolves fresh sources; CDN addresses/tokens never become collection keys. Miruro has no YouTube channel identity, so category/tag/channel actions and YouTube extraction/thumbnails are excluded; its episodes use combined collections.

Integration verification

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.

Final 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.

This run proves 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.

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.