Files
tubular/docs/miruro-segments.md
T

134 lines
14 KiB
Markdown

# 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](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][detail] and [sources endpoint][sources]. The temporary example was removed afterward; `cargo run --locked --example miruro_segments_probe` describes that investigation, **not a permanent runnable command**. Safe output projection:
```text
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][watch] 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:
```json
{
"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](miruro-api.md#detail-episodes-and-watch-data) 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][home] imports [`app.Bj5KeT2s.js`][app], whose route dictionary maps `/watch/[id]/[slug]` to node 11, [`11.DKyvW9qB.js`][client]. 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][client]: `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`](../src/miruro.rs#L321) decodes raw episode records, including `skip_times`. [`series`](../src/miruro_browser.rs#L238) retains them in `Episode.metadata`; no additional upstream request is needed while browsing an already-loaded series.
2. [`Episode::snapshot`](../src/miruro_browser.rs#L29) projects the selected episode into a generic `Video`, without segment metadata. [`Screen::selected_video`](../src/miruro_ui.rs#L145) uses that snapshot for series playback; offline collections already contain only `Video` snapshots.
3. [`open`](../src/miruro_ui.rs#L522) submits `Request::Source(Video)`. The [worker](../src/miruro_browser.rs#L87) calls [`Client::source_document`](../src/miruro.rs#L362), which fetches only `/api/sources`. It cannot reconstruct the missing intervals from that response.
4. [`SourceDocument`](../src/miruro.rs#L90) has no segment field. [`State::miruro_session`](../src/video_state.rs#L379) 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](../../terebi/docs/miruro-playback-handoff.md#source-document-version-1) contains no semantic intervals and explicitly lists intro/outro controls as out of scope. [`ResolvedSource.Document`](../../terebi/Sources/App/ResolvedSource.swift#L38) decodes only version, URL, headers, identity, title and subtitles. [`validated`](../../terebi/Sources/App/ResolvedSource.swift#L84) 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.
```text
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`](../../terebi/Sources/App/TerebiApp.swift#L690) 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](../../terebi/Sources/App/StreamResolver.swift#L103) 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.
```javascript
// 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.
[home]: https://barelystarted.miruro.tv/
[app]: https://barelystarted.miruro.tv/_app/immutable/entry/app.Bj5KeT2s.js
[client]: https://barelystarted.miruro.tv/_app/immutable/nodes/11.DKyvW9qB.js
[detail]: https://barelystarted.miruro.tv/info/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3/__data.json
[watch]: https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3/__data.json?ep=1
[sources]: https://barelystarted.miruro.tv/api/sources?id=OERe_a0YS4h-DlsBDOeXwX7HOO6316tw&n=1