Files
tubular/docs/miruro-segments.md
T

13 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 (updated after the approved ownership split): Miruro supplies optional episode-relative opening/ending intervals. Terebi must acquire and display them through its native episode-URL resolver; Tubular owns catalog/selection/collections/watched decisions only. Current Terebi does not display these intervals. The former source-file recommendation below is superseded by the URL-playback handoff. Availability is sparse; actual media-duration bounds are necessary but do not prove alignment across cuts.

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 — historical native Tubular probe before URL cutover: the then-existing 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. Source resolution/projection has since been removed from Tubular. 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.
  • Approved seam (supersedes the earlier source-session recommendation): Terebi's existing episode-URL resolver owns optional metadata acquisition and validation against actual media duration. Direct and Tubular launches must use the same path. No source-document version change, separate segment file, new endpoint or Tubular metadata persistence is required. Preserve optional provenance/reference-length fields if available; do not invent fields absent from the sample or claim bounds prove synchronization.
  • Recommendation: address every launch path, including saved/watch-later/currently-watching reopening, rather than enriching only a currently open catalog episode.

Tubular → Terebi seam

The approved implementation contract and acceptance scenarios are consolidated in Terebi URL-playback handoff. Tubular launches the stable opaque ID and returned episode number through PlayerLaunch::start, using a non-identifying episode watch slug. Old/offline snapshots need no new fields or migration. Terebi owns canonical-route resolution, title enrichment and optional timeline metadata; title text is not identity.

The requested UI is intro/outro bands in the controls progress bar only, not automatic skipping or skip buttons. Validate episode binding, finite ordered nonnegative times and actual-duration bounds; clear metadata on media replacement. Empty, unavailable or invalid metadata must not prevent ordinary playback, and unknown kinds must not be relabeled.

Historical source-file decoder evidence (superseded transport)

Before this cutover, Tubular retained skip_times in browsing metadata but omitted it from generic Video snapshots and source documents. Terebi's source-v1 decoder did not retain intervals. The following isolated decoder observation explains why adding JSON fields alone would not have implemented timeline display; it is not a recommendation to restore source files.

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.

The earlier recommendation to introduce a new source-document version and carry intervals through Tubular is superseded. No source-v2 or segment file is required. Terebi's native URL resolver already owns /api/sources; it must additionally acquire title and optional episode metadata via canonical route data for all callers. See the handoff for lifecycle and acceptance requirements. Existing source-v1 decoder observations above remain historical evidence only.

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.