123 lines
13 KiB
Markdown
123 lines
13 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 (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](terebi-url-playback-handoff.md). 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](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][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**. Source resolution/projection has since been removed from Tubular. 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.
|
|
- **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](terebi-url-playback-handoff.md). 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.
|
|
|
|
```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.
|
|
|
|
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.
|
|
|
|
```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
|