Files
tubular/docs/miruro-segments.md
T

14 KiB

Miruro video-segment feasibility

Investigated 2026-10-22, scoped to https://barelystarted.miruro.tv. Investigation only: no production changes, media playback, playlist/segment/subtitle downloads, browser testing, or remote mutations.

Conclusion: Miruro supplies episode-relative opening/ending intervals, but Terebi cannot consume them today. Tubular retains them while browsing, then drops them before the source-file handoff; Terebi silently ignores an added segments property. Both sides need an explicit contract change. Availability is sparse, and timing must be checked against the selected stream's actual duration; alignment across different cuts remains unproven.

Evidence labels: Observed = responses obtained today; Frontend = today's deployed first-party JavaScript, not a server guarantee; Earlier = the 2026-10-12 investigation in miruro-api.md; Recommendation = proposed minimum behavior, not implemented.

Live metadata

Observed — native Tubular probe: the actual Client::detail, Client::sources, and SourceDocument::from_sources path ran against the detail route and sources endpoint. The temporary example was removed afterward; cargo run --locked --example miruro_segments_probe describes that investigation, not a permanent runnable command. Safe output projection:

detail: 3 episodes
episode 1: absolute_number=49 duration_seconds=1500
  skip_times=[{kind:op,start_seconds:63.313,end_seconds:153.237},
              {kind:ed,start_seconds:1335.252,end_seconds:1425.384}]
episode 2: absolute_number=50 duration_seconds=1500 skip_times=[]
episode 3: absolute_number=51 duration_seconds=1500 skip_times=[]
sources top-level keys: config episodeNumber providerOrder tracks
projected source-document keys: headers identity subtitles title url version

Those are episode 1 intervals, not absolute episode 49 or a provider-specific episode identifier. Two empty arrays show why metadata must remain optional; they do not establish that those episodes have no opening/ending.

Observed — watch metadata: JavaScript fetch of the watch route returned HTTP 200, text/sveltekit-data, 33,483 text characters. The newline-delimited SvelteKit response contained an episodesData promise chunk; decoding its indexed table yielded:

{
  "episodes": [
    {"number": 1, "duration": 1500},
    {"number": 2, "duration": 1500},
    {"number": 3, "duration": 1500}
  ],
  "currentEpisode": {"number": 1},
  "prevEpisode": null,
  "nextEpisode": {"number": 2},
  "aniskip": [
    {"episode": 1, "type": "op", "start": 63.313, "end": 153.237},
    {"episode": 1, "type": "ed", "start": 1335.252, "end": 1425.384}
  ]
}

This is an abbreviated projection: episode objects have additional descriptive fields. The two aniskip objects are complete and have no provider, votes, or episode_length. Both routes express these intervals in seconds, retaining fractional precision; skip_times uses kind/start_seconds/end_seconds, whereas watch uses type/start/end plus the episode number. The sampled values match across both routes. /api/sources contains no segment field; a source-only request cannot recover these sampled intervals.

Earlier vs current: miruro-api.md recorded two available episodes on 2026-10-12; today's probes found three. Its detail/watch shapes and episode-1 intervals are revalidated above. Its old hashed frontend links are not today's deployment. The current watch Content-Type is text/sveltekit-data, so do not assume every route-data response is application/json. Native ureq success and previous curl/Cloudflare failure are transport-specific observations, not permission or an availability guarantee.

Deployed first-party normalization

Frontend: today's homepage imports app.Bj5KeT2s.js, whose route dictionary maps /watch/[id]/[slug] to node 11, 11.DKyvW9qB.js. In that watch bundle, function wn and sets xn/Sn implement this complete mapping:

Input type Emitted player segment
op, mixed-op {kind:"intro", start, end}
ed, mixed-ed {kind:"outro", start, end}
Any other value, including recap No segment from this normalizer

Only op and ed occurred in today's metadata. The mixed variants are frontend-supported, not live-observed API records. There is no evidenced exhaustive server enum, recap interval, preview interval, provider-specific interval, or mixed-kind decomposition. In particular, mixed-op is treated as one whole intro interval, not split into opening and recap portions.

The algorithm in wn(records, episodeNumber, duration) is:

  1. Return [] when records are empty, the episode is null, or duration is absent/nonpositive.
  2. Reject a record when its optional provider, lowercased, contains crunchyroll; its episode is not strictly equal to the selected episode number; or (votes ?? 0) < 0.
  3. Require finite start and end, start >= 0, end > start, and end <= duration (an end exactly at duration is allowed).
  4. When episode_length is a positive number, require abs(episode_length - duration) / duration <= 0.05. Missing/non-number/nonpositive lengths bypass this comparison. This optional field was absent in the sample, so catalog duration 1500 does not trigger that 5% check by itself.
  5. Sort survivors by descending (votes ?? 0). Take the first opening-kind record and the first ending-kind record. Emit intro first, then outro; at most two segments. It does not merge, scale, shift, trim, or resolve overlapping intro/outro intervals.

Useful anchors in the minified bundle: Cn=.05, function wn, provider?.toLowerCase().includes(\crunchyroll`), episode_length, and strmcx-duration-change`. The provider check concerns the metadata record's provenance, not equality with the currently selected stream provider; the algorithm has no selected-provider argument. Vote/provider/duration-length rules are consumer behavior, not evidence that Miruro currently returns those optional fields.

Frontend timing/lifecycle: the player component starts with empty segments and resets them, together with duration, during source resolution. A strmcx-duration-change event with positive numeric duration invokes wn using player-reported duration, the selected episode number, and watch aniskip. The normalized array is passed as segments to the strm.cx session, separately from the options.autoskip preference. Catalog episode.duration is not used as the normalization duration. Iframe playback takes a separate embedded-URL path rather than this direct-player session; the inspected code does not establish segment handoff to an arbitrary iframe provider.

Limits and minimum recommendation

  • Observed: metadata exists and is not tied to an expiring source URL. Unknown: coverage across the catalog, a stable/public metadata contract, how timings were generated, non-opening/ending availability, and alignment across providers/dubs/cuts. Merely being in bounds does not prove synchronization with a particular cut.
  • Recommendation: reuse the existing detail episode metadata; retain selected episode identity, optional raw kinds, and fractional second boundaries alongside it. Normalize only the four evidenced opening/ending kinds for initial intro/outro behavior. Do not relabel recap or unknown kinds as intro/outro or invent missing intervals.
  • Recommendation: use the existing Terebi source-session handoff if its contract can carry the metadata, validating finite/nonnegative/ordered intervals and actual media-duration bounds at the playback boundary. Preserve optional provenance/reference-length fields if they become available; do not claim the sample has them or that the deployed frontend's optional checks guarantee cross-provider synchronization. No new endpoint, media download, clock-based heuristic, or separate segment lookup service is justified by this evidence.
  • Recommendation: address every launch path, including saved/watch-later/currently-watching reopening, rather than enriching only a currently open catalog episode.

Tubular → Terebi boundary

Where Tubular loses the metadata

  1. Client::detail decodes raw episode records, including skip_times. series retains them in Episode.metadata; no additional upstream request is needed while browsing an already-loaded series.
  2. Episode::snapshot projects the selected episode into a generic Video, without segment metadata. Screen::selected_video uses that snapshot for series playback; offline collections already contain only Video snapshots.
  3. open submits Request::Source(Video). The worker calls Client::source_document, which fetches only /api/sources. It cannot reconstruct the missing intervals from that response.
  4. SourceDocument has no segment field. State::miruro_session serializes it into the existing private source.json; that transport can be reused once both contracts change. No new CLI argument or separate segment file is needed.

Recommendation: preserve episode-bound metadata through the shared source-resolution path, not a series-screen-only patch. Offline episode snapshots lack both skip metadata and a canonical route slug; adoption must retain enough metadata/route information to refresh them or deliberately store the episode metadata for those launches. Do not guess a slug from the combined episode display title or confuse episode_number with absolute_number.

What Terebi needs

The current source-v1 contract contains no semantic intervals and explicitly lists intro/outro controls as out of scope. ResolvedSource.Document decodes only version, URL, headers, identity, title and subtitles. validated requires version 1 and constructs a runtime source without segments.

Observed — actual decoder smoke: compiled the existing ResolvedSource.swift with a temporary Foundation-only harness, then called ResolvedSource.load(from:) on synthetic local source documents. No player/network/GUI was started.

interval-array: accepted; segments absent from ResolvedSource
wrong-type: accepted; segments absent from ResolvedSource
version-2: rejected

Thus merely adding segments to a version-1 JSON file would silently discard it, even with a malformed value. Changing the version alone also does not work. This is a source-document version, independent of the existing version-2 progress records.

Minimum implementation sequence — recommendation, not implemented:

  1. Agree and implement a new source-document version in Terebi with optional episode-relative intervals: kind and fractional-second start/end. Define missing/empty metadata, allowed kinds, limits and invalid-interval handling. A version gate prevents old players from silently claiming support; keep the existing private file and identity contract.
  2. Retain intervals in Terebi's resolved runtime source and playback lifecycle; clear them on media replacement and check them against actual duration. For skip controls, reuse existing acknowledged seeking rather than creating another playback clock. finishSeeking and seekRelative already route seeks through native/Cast behavior; there is no semantic intro/outro action today. Automatic skipping is a separate behavior decision, not required to transport metadata.
  3. Update Tubular's projection/validation and every launch path to emit the agreed contract. Preserve unknown metadata without turning it into a skip action; empty intervals must not block ordinary playback. Add consumer-visible checks for wrong-episode metadata, invalid bounds and media replacement when implementing.

Terebi's own episode-URL resolver also uses only /api/sources and would need metadata acquisition separately if that entry point is included in a future feature. External source-file support alone would not make native URL resolution segment-aware.

Fetch record

All requests were read-only. Static source was inspected, not executed as a webpage. No expiring source URL/token was printed or saved.

// Research eval JavaScript transport; each response's status/type was inspected.
await fetch("https://barelystarted.miruro.tv/");
await fetch("https://barelystarted.miruro.tv/_app/immutable/entry/app.Bj5KeT2s.js");
await fetch("https://barelystarted.miruro.tv/_app/immutable/nodes/11.DKyvW9qB.js");
await fetch("https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3/__data.json?ep=1");

The homepage/app were first fetched with the static read tool (:raw for imports); the watch bundle was read and then fetched with eval: 200 text/javascript, 84,045 characters. Watch route: 200 text/sveltekit-data, 33,483 characters; only decoded episode/segment metadata was printed, never sourcesData. The native detail/source probe is documented above. The decoder smoke used swiftc -parse-as-library ../terebi/Sources/App/ResolvedSource.swift <temporary harness> -o <temporary binary>, followed by running that binary. Both temporary source harnesses and the decoder's generated fixture/binary were removed. No test suites, linters, formatters, full application builds or UI smoke tests were run; only the native metadata probe and isolated decoder harness were compiled and executed.