Files
tubular/docs/terebi-url-playback-handoff.md
T

135 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Terebi episode-URL playback handoff
Approved ownership split, 2026-10-22. This document hands work to the Terebi team; it does not claim Terebi implementation or visible-player proof. Tubular cutover checks are recorded below. Primary metadata evidence, sampled intervals, frontend normalization and investigation limits are in [Miruro segment research](miruro-segments.md). Earlier source/CDN evidence is retained in [Miruro API research](miruro-api.md).
## Ownership and current support
- **Tubular:** catalog/detail browsing, selecting actual returned episodes, collections, reporting-session identity and watched decisions.
- **Terebi:** episode-URL parsing, fresh source resolution, headers, subtitles, playback title/metadata, resume state and intro/outro visualization.
Terebi already accepts native Miruro episode URLs, resolves sources and supports stable version-2 episode progress. Its current title is `Miruro episode N`; enriching that title and displaying intro/outro intervals remain work for Terebi. A successful URL launch is not proof of either feature. Direct Terebi URLs and Tubular launches must use the same resolver and metadata lifecycle; do not add a caller-specific source-file fallback.
## URL and canonical-route handling
Tubular uses `PlayerLaunch::start(&videos, player, &mut state, enqueue)` for both providers; the launcher allocates and binds private reporting sessions through `State`. Plain Enter passes `false`, retaining a separate player; `video.enqueue` passes `true`. For Miruro, `Identity::from_key(id)?.watch_url()?` validates the stable identity and constructs:
```sh
terebi --new-instance \
--url 'https://barelystarted.miruro.tv/watch/OPAQUE_ID/episode?ep=RETURNED_EPISODE' \
--progress-file '/private/session/progress.jsonl'
```
`episode` is a deliberate non-identifying placeholder slug. The existing Terebi `StreamURL.parse` accepts the slug and discards it, retaining opaque ID and episode. Never parse Tubular's combined display title to obtain a slug, identity or episode number. Use the returned episode number, not `absolute_number`, external AniList/MAL ID, array index or source-provider identifier.
**Observed read-only route probe, 2026-10-22:** `https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/episode/__data.json?ep=1` returned HTTP 200 `application/json` with a redirect envelope to `/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3?ep=1`. This is route-data canonicalization, not necessarily an HTTP redirect. Terebi metadata acquisition must recognize the envelope, safely validate the destination against the expected Miruro origin, opaque ID and selected episode, and request the canonical route's `__data.json` while preserving `ep`. Bound redirect handling and reject loops or identity/origin changes; do not mistake HTTP 200 for decoded metadata success. Canonical watch data can be newline-delimited `text/sveltekit-data`, not just one JSON object. Decode response-local value tables and promised chunks as documented in the evidence.
A failed title/segment lookup or unsafe/unusable metadata redirect must not prevent otherwise resolvable ordinary playback. Retain a usable generic title when enrichment is unavailable; never change media identity to make metadata fit.
## Stable identity, sessions and progress remain unchanged
The collection key remains `miruro:<opaque ID>:<canonical episode>`. Runtime/report identity remains `{provider:"miruro", media_id:<opaque ID>, episode:<returned episode>}`. Titles, slugs and expiring CDN URLs are not identity. Metadata title enrichment must not create a new resume key or split an episode's history.
Tubular creates a private reporting session and registers its expected identity before launching. Terebi exclusively creates and appends the private progress report; Tubular consumes it independently of launcher lifetime. Existing retained sessions and collections, including old snapshots and reports recovered after restart, must remain readable. Do not migrate collections or add persistent `Video` fields to transport metadata. Existing session retention, explicit inactive-session cleanup and the 4,096-session cap remain in force; launcher exit, a final event or Tubular shutdown does not imply the independent player has stopped.
Progress-v2 is unchanged, for example:
```json
{"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 checks the complete identity against the launch session. Acknowledged positive positions admit currently-watching entries. Watched completion remains `completion_eligible && duration_seconds > 0 && position_seconds / duration_seconds >= 0.95`; null duration is ineligible. Terebi owns resume positions. Existing partial-line/restart handling stays intact. YouTube canonical URLs, progress-v1 compatibility and watched policy are unchanged.
## Terebi metadata and timeline work
Acquire title and optional episode-bound intervals in the native URL resolver, not from `/api/sources` alone: the sampled sources response has no intervals. Watch `episodesData.aniskip` and detail `skip_times` contain equivalent sampled timings with different field names; use the existing route metadata surface rather than inventing another lookup service. Preserve episode binding and fractional seconds. Obtain a descriptive series/episode title from returned metadata, not a guessed title derived from the placeholder slug.
Requested visualization is **labeled opening/ending bands in the controls progress bar**, leaving the playhead and played/buffered indicators readable; do not distinguish the bands by color alone. No auto-skip, skip buttons or new seek behavior is requested. Normalize only evidenced kinds: `op`/`mixed-op` → intro, `ed`/`mixed-ed` → outro. Do not relabel `recap` or unknown kinds, invent absent intervals, or decompose mixed kinds. Raw unknown kinds may remain metadata but do not become intro/outro bands.
Validate finite start/end, `start >= 0`, `end > start`, selected episode match and `end <= actual media duration` (equality allowed). Wait for positive player-reported duration, not catalog duration; revalidate on duration changes. Invalid/out-of-bounds intervals should not be scaled, shifted or trimmed into plausible bands. Do not invent absent provenance, votes or reference-length fields; the frontend's optional checks and coverage/alignment limits are recorded in the primary evidence. In-bounds timings alone do not establish synchronization across different providers/cuts.
Start each media lifecycle with no intervals. Clear title/interval metadata on media replacement, bind asynchronous results to the current stable identity and reject stale responses, including results arriving after a different episode starts. Missing, empty, malformed or unavailable optional metadata leaves ordinary playback usable and the progress bar without those bands. No source-v2 contract, `--title` option, segment file, caller distinction or Tubular source projection is needed.
## Acceptance scenarios for the Terebi implementation
1. **Series selection:** launch a returned episode from Tubular's series screen using the placeholder URL; source resolution, headers/subtitles and canonical-route metadata use the native resolver. Compare a direct Terebi launch of the same URL: same descriptive title, identity, resume position and matching progress-v2 records.
2. **Offline collections:** launch the same identity from saved, watch-later and currently-watching collections without first opening its series. Include an old snapshot with no slug, new metadata fields or intervals. All paths resolve fresh sources and acquire the same title/optional intervals without migration; offline collection browsing itself does not require network availability. Playback still requires resolvable resources.
3. **Identity continuity:** canonical slug/title changes do not fork resume/progress state; wrong-episode/provider/media records cannot admit or complete another episode. Existing retained reports remain consumable after Tubular restarts; the unchanged 95% rule applies.
4. **Optional metadata:** absent/empty metadata, malformed intervals, unknown kinds and failed metadata requests leave normal playback usable. Unknown kinds are not intro/outro. A valid sampled opening/ending appears as bands only, with no automatic seeking or added skip buttons.
5. **Bounds and replacement:** with controlled metadata and player duration, reject negative/nonfinite/reversed/out-of-bounds or wrong-episode intervals; allow an end exactly at duration. Revalidate on actual-duration changes. Replace media while metadata is pending and confirm no old title/bands leak into the replacement; absent metadata clears earlier bands.
6. **Canonicalization:** exercise the placeholder's 200 redirect envelope and canonical streamed route response. Unsafe origin/identity changes, loops or unavailable metadata fail only optional enrichment, not an independently resolvable source.
7. **YouTube regression:** canonical YouTube URL launch, independent player lifetime, existing title/resume handling, progress-v1 ingestion and watched decisions remain unchanged.
These are acceptance requirements, not reported passes. Terebi implementation and visible progress-bar/title confirmation remain the receiving team's responsibility; no Terebi UI or browser verification was performed.
## Tubular cutover verification
Exercised locally on 2026-10-22:
- `cargo fmt --all --check`, `cargo test --locked` (**220 passed**), `cargo build --locked`, and the built executable's `--help` passed.
- `cargo run --locked --example miruro_probe` returned eight catalog records, three episodes for the documented title, and an episode-1 URL. The example makes no source/CDN requests.
- The actual terminal application launched a live catalog episode and a legacy snapshot from saved, watch-later and currently-watching views through `--new-instance --url ... --progress-file ...`. A capture/report launcher fixture confirmed matching stable identities, no `source.json`, private session/report modes, positive v2 watching admission, rejection of a wrong-episode completion, and matching completion recovered after restart. YouTube's actual terminal launch retained its canonical URL and v1 progress admission.
- `cargo clippy --locked --all-targets -- -D warnings` is blocked by findings in untouched files: `src/youtube/catalog.rs:184` (`collapsible_if`) and `src/titles.rs:492` (`reversed_empty_ranges`).
The launcher fixture did not run Terebi. Native Terebi playback, title enrichment and visible timeline bands are not verified by these Tubular checks. Temporary smoke fixtures were removed; the user-facing catalog probe remains.
## Ordered stream queue handoff
Multiple-video selection extends the URL handoff without transporting resolved sources or changing progress records. Tubular sends checked rows in displayed order; it does not combine providers' browsing sessions into one selection. Terebi accepts either provider in a queue and preserves the supplied order instead of applying local-file filename sorting.
```sh
terebi --new-instance --queue-file '/private/session/queue.json'
```
The version-1 manifest is:
```json
{
"version": 1,
"items": [
{
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"progress_file": "/private/playback-sessions/session-A/progress.jsonl"
},
{
"url": "https://barelystarted.miruro.tv/watch/opaque_Show-1/episode?ep=3",
"progress_file": "/private/playback-sessions/session-B/progress.jsonl"
}
]
}
```
The manifest contains 1–4,096 entries. Plain Enter uses it for batches of two or more; a single item retains `--new-instance --url` and `--progress-file`. Enqueue uses a manifest even for one item. `--queue-file` requires either `--new-instance` or `--enqueue`, an absolute manifest path, and no other media argument or global `--progress-file`; those two launch modes cannot be combined. Terebi validates the entire manifest before delivery and again in the receiving application: supported page URLs, distinct stable identities, distinct absolute new report destinations, and plain owner-only report directories. POSIX-canonical report paths deduplicate macOS `/var` and `/private/var` aliases. Invalid batches fail rather than silently playing only their first entry.
Tubular validates all identities, snapshots and available session capacity before allocating a batch. It creates one existing-format snapshot/reporting session and identity marker per item; it never creates Terebi's report files. The manifest is `0600` inside the first retained `0700` session directory and contains no source URLs or transport credentials. Retention and the shared 4,096-session cap are unchanged; inactive queue entries count toward the cap too.
Terebi resolves only the selected queue item, through its ordinary YouTube/Miruro resolver and metadata lifecycle. Next/previous, sidebar selection, reordering/removal and natural-end advancement reuse the existing queue. Switching entries cancels stale resolution/metadata work without discarding the queue; failed playback does not mark an item watched. Local-file queues retain natural filename ordering. A submitted batch opens one independent player, not one window per checked item.
Each selected entry reports only to its own registered destination. Switching drains the previous writer; returning to an entry appends only to the original owned regular-file inode, never truncating or adopting an unrelated existing file. Resume keys and version-1 YouTube/version-2 Miruro identities remain stable. Tubular's watching admission, 95% completion rule, manual-status precedence and restart recovery consume these independent reports unchanged.
### Queue verification status
- Tubular: `cargo fmt --all --check`, `cargo test --locked` (**230 passed**), `cargo build --locked` and the built CLI's `--help` passed.
- Both actual terminal interfaces were exercised with offline saved rows and a capture launcher: checking in reverse order produced a private two-item manifest in displayed order; successful submission cleared checks and the next Enter used single-video URL playback. This fixture did not run Terebi.
- Terebi: `make check-cli check-queue check-reporting check-resolved all` passed. Headless native playback exercised mixed YouTube/Miruro queues, stale-result cancellation, manual navigation, natural-end advancement, fresh resolution/resume on revisits and isolated per-item reports.
- The packaged GUI smoke did not complete: native state snapshots stalled. Temporary tracing and probe files were removed. Further UI testing was stopped at the user's request and remains user-owned; no complete GUI smoke or visual-layout pass is claimed. The TODO remains open for that confirmation.
## Append to a running player
```sh
terebi --enqueue --queue-file '/private/session/queue.json'
```
`video.enqueue` defaults to Ctrl+Return. It shares plain Enter's checked-display-order selection and hovered-row fallback in video/episode lists, previews and offline collections. Miruro catalog/show rows and text inputs do not enqueue. Plain Enter remains a new-instance launch.
The CLI delivers a manifest reference through an explicit-app native URL-open event, so later submissions reach an already running player rather than relying on startup arguments. The receiver appends in supplied order without restarting current playback, changing paused/playing intent, cancelling current resolution or resolving appended items early. An empty or newly launched player starts the first queued item. OS-level delivery to a running packaged app still needs user confirmation; headless receiver checks are not LaunchServices proof.
The complete request is rejected on an identity already in the current queue, a retained report identity, a retained report path, or the original single-video report identity. Removing a row does not release its registration. Registrations are atomic; failed batches do not reserve their valid prefix. The original single-video report remains independently navigable, and unregistered identities cannot leak into it once queue reporting is active. Errors after OS handoff appear in Terebi; Tubular's checks retain the existing clear-on-successful-launcher-spawn policy.
### Enqueue verification
- Tubular: `cargo fmt --all`, `cargo test --locked` (**233 passed**), `cargo build --locked` and built `--help`; Rust launcher diagnostics were clean.
- Actual YouTube and Miruro terminal interfaces: reverse-checked rows produced display-ordered enqueue manifests, cleared checks fell back to the hovered row, Ctrl/Super+Return and an F12 override worked, and plain Return still used `--new-instance --url`. Six handoffs used private `0600` manifests in `0700` session directories with distinct report destinations. The capture launcher did not run Terebi.
- Terebi: `check-cli`, `check-queue`, `check-reporting` and `make all` passed. A regression check first failed on rebinding the original single-video identity and passed after binding it independently of appended reports.
- Headless native enqueue smoke with local video-only HLS passed: repeated delivery while paused/playing, unchanged current selection/revision, lazy mixed-provider append, natural-end advance, navigation/revisits, atomic rejection/rollback, isolated original/appended reports and empty-player startup.
- Full `make check-resolved` stopped in the pre-existing mixed-queue natural-end scenario at `Could not start Core Audio (560227702)` before reaching enqueue. No full A/V-suite pass is claimed. No packaged-GUI/LaunchServices smoke or visual-layout confirmation was performed; the app was built, not installed. Temporary terminal/server fixtures were removed.