refactor: delegate Miruro playback resolution to Terebi
This commit is contained in:
+24
-38
@@ -1,6 +1,6 @@
|
||||
# Miruro API research
|
||||
|
||||
Observed **2026-10-12**, scoped to **https://barelystarted.miruro.tv**. These are reverse-engineering notes, not an official API specification. Tubular includes a native API module, Miruro catalog/episode browsing, and direct Sun playback through Terebi's implemented source/progress contracts.
|
||||
Observed **2026-10-12**, scoped to **https://barelystarted.miruro.tv**; ownership split updated **2026-10-22**. These are reverse-engineering notes, not an official API specification. Tubular now owns catalog/detail retrieval and stable identity; Terebi resolves episode URLs. Source/CDN investigations below are historical evidence, not the current launch path.
|
||||
|
||||
Evidence labels:
|
||||
|
||||
@@ -52,7 +52,7 @@ The minimum evidenced flow is:
|
||||
4. Fetch `/api/sources?id=<opaque id>&n=<selected number>` immediately before playback.
|
||||
5. Choose a returned track/provider/server with direct streams; preserve that server's headers and runtime configuration. Iframe-only servers are not native media URLs.
|
||||
|
||||
**Frontend:** [link helpers][link-helpers] generate `/info/<id>/<slug>` and `/watch/<id>/<slug>?ep=<number>`. The canonical title is the shorter trimmed English/Romaji title when both exist, otherwise the available English/Romaji, then native title/ID. Slugging applies Unicode NFD, removes combining accents, lowercases, removes non-ASCII-alphanumeric/non-whitespace/non-hyphen characters, trims, hyphenates whitespace, and collapses repeated hyphens. Display-language preference is separate. Missing/wrong-slug behavior is untested; preserve the deployed helper's convention rather than assuming an arbitrary slug works.
|
||||
**Frontend (2026-10-12):** [link helpers][link-helpers] generate `/info/<id>/<slug>` and `/watch/<id>/<slug>?ep=<number>`, with title-derived canonical slugs. **2026-10-22:** a placeholder watch slug returned a canonical-route redirect envelope; see the [URL handoff](terebi-url-playback-handoff.md#url-and-canonical-route-handling). Slugs are not episode identity. Tubular does not derive playback routes from display titles.
|
||||
|
||||
## Search and discovery contracts
|
||||
|
||||
@@ -192,7 +192,7 @@ Resolved watch `episodesData` has:
|
||||
|
||||
Watch `episodes[].id` is the number rendered as a string, **not a provider episode ID**. `aniskip` times are seconds; they differ in naming from detail `skip_times`. `/api/sources` does not carry those fields. **Frontend:** watch converts `op`/`mixed-op` to intro and `ed`/`mixed-ed` to outro, filters wrong episodes/invalid intervals and optional provider/vote/duration constraints, then recomputes skips using player-reported duration. No separate skip endpoint was found. Missing/invalid episode behavior and last-episode boundaries are untested.
|
||||
|
||||
## Source-resolution and playback contract
|
||||
## Historical source-resolution and playback research (2026-10-12)
|
||||
|
||||
### Plain source response
|
||||
|
||||
@@ -263,7 +263,7 @@ Observed `providerOrder`: `sun, bee, pewe, hop, ally, kiwi, moo, bonk, knob, nun
|
||||
- The browser loads [strm.cx/client.js][strm-client], renders `<strmcx-embed>`, and calls `setSession` with local player state/source/subtitle/skip data over `strmcx/v1` messaging. This was **not required for the tested source or direct-resource GETs**. It is not evidence of an authenticated API session prerequisite.
|
||||
- strm client contains a best-effort `POST /v1/embeds`; it was source-inspected only, not invoked. Iframe providers and unrelated comment/telemetry endpoints were not exercised.
|
||||
|
||||
Tubular's direct Sun integration preserves fresh URLs and explicitly supplies the selected server's headers to both its stream and verified VTT subtitle resources. Terebi owns native HLS resolution and subtitle loading; other providers' proxies, headers and playlist transforms remain unverified and unsupported. See [the adopted integration contract](#tubular--terebi-integration).
|
||||
The former Tubular direct Sun integration supplied fresh URLs and headers to stream and VTT resources. That projection has been removed; Terebi now owns native URL resolution, HLS and subtitles. Other providers' transforms remain unverified by this research. See [the current integration](#tubular--terebi-integration).
|
||||
|
||||
## SvelteKit route data
|
||||
|
||||
@@ -387,32 +387,29 @@ console.log(response.status, response.headers.get('content-type'));
|
||||
console.log(await response.text());
|
||||
```
|
||||
|
||||
## Native Rust wrapper and verified probe
|
||||
## Native Rust catalog wrapper
|
||||
|
||||
The local probe succeeded using Rust 1.90.0, `ureq` 3.4.2, rustls 0.23.45 and the `ring` TLS provider. Its isolated scratch project was removed after the exercised path was extracted into [src/miruro.rs](../src/miruro.rs). The client uses ordinary TLS verification, no cookies, no browser impersonation, no clearance tokens and no retry/alternate transport.
|
||||
The historical native probe succeeded using Rust 1.90.0, `ureq` 3.4.2, rustls 0.23.45 and the `ring` TLS provider. The current `tubular::miruro` module retains catalog/detail retrieval, the bounded route decoder and stable identity, not source resolution. It uses ordinary TLS verification, no cookies, impersonation, clearance tokens or alternate transport.
|
||||
|
||||
Library interface, exposed as `tubular::miruro`:
|
||||
Current library interface:
|
||||
|
||||
| Call | Result / contract |
|
||||
| --- | --- |
|
||||
| `Client::default()` | Reusable blocking HTTP agent; 25-second global request timeout; no automatic redirects |
|
||||
| `client.search(query)` | `io::Result<Vec<serde_json::Value>>`; trimmed, nonempty autocomplete query; no cursor pagination |
|
||||
| `client.detail(id, slug)` | `io::Result<Detail>`; decoded `media` JSON and `episodes: Vec<Value>`; validates returned media ID |
|
||||
| `client.sources(id, episode)` | `io::Result<Value>`; fresh full response preserving headers/config/subtitles; validates positive episode and returned episodeNumber/tracks |
|
||||
| `client.source_document(id, episode, title)` | `io::Result<SourceDocument>`; fresh validated direct `ssub/sun` HLS/VTT projection for Terebi |
|
||||
| `Identity::new(id, episode)`, `Identity::from_key(key)` | Validated catalog namespace/episode identity; `key()` is `miruro:<opaque ID>:<episode>` |
|
||||
| `identity.watch_url()` | Validates identity and produces `https://barelystarted.miruro.tv/watch/<opaque ID>/episode?ep=<returned episode>` |
|
||||
|
||||
The raw methods preserve JSON metadata rather than freezing every deployment-specific field into a second schema. `source_document` deliberately selects only direct Sun soft-subtitle playback; it does not guess another provider or implement a player. IDs/slugs are validated as nonempty ASCII alphanumeric/hyphen/underscore path segments. HTTP failures remain errors (`PermissionDenied` for 401/403); HTML and unexpected Content-Types are rejected, not converted to empty results. No tokenized URLs appear in its error messages. Successful bodies are limited to 2 MiB.
|
||||
Raw catalog methods preserve JSON metadata without a second deployment-specific schema. IDs/slugs are validated path segments; HTTP/HTML/Content-Type failures remain errors, not empty results. Successful bodies are limited to 2 MiB. Source selection, resource headers and subtitles belong to Terebi.
|
||||
|
||||
Its private route decoder resolves only the needed detail value and episode promise, using separate value tables and promise IDs. Unrelated sidebar promises are not returned. It supports the JSON-shaped tables verified on this path, **not arbitrary devalue**: tagged special types and negative sentinels are explicit errors, never silently coerced to null. Out-of-range references, rejected/missing/duplicate episode chunks, cycles/excessive depth and decoded-size amplification are rejected. Depth is bounded to 64 and expanded values share a 2 MiB size budget. These are wrapper safety limits, not Miruro server limits.
|
||||
|
||||
Run the retained live smoke:
|
||||
The retained `cargo run --locked --example miruro_probe` smoke now covers search → detail/episodes only, not sources or playback.
|
||||
|
||||
```sh
|
||||
cargo run --locked --example miruro_probe
|
||||
```
|
||||
### Historical wrapper/source probe evidence (before URL cutover)
|
||||
|
||||
Actual output from the extracted wrapper:
|
||||
The output and verification below describe the former source-capable wrapper, not the current example:
|
||||
|
||||
```text
|
||||
search: 8 records; selected Miruro ID OERe_a0YS4h-DlsBDOeXwX7HOO6316tw
|
||||
@@ -425,44 +422,33 @@ PASS: native search -> detail/episodes -> sources -> HLS master + subtitle; no m
|
||||
|
||||
The preceding scratch probe also reported HTTP 200 for search (`application/json`), detail (`text/sveltekit-data`, seven records), and sources (`application/json`). Detail decoding found two episodes and selected `episode_number:1`; source decoding found two tracks. The master and default subtitle were fetched using the actual returned `Origin: https://ultracloud.cc` and `Referer: https://ultracloud.cc/`. URL/header necessity and full playback were not tested.
|
||||
|
||||
The [example](../examples/miruro_probe.rs) deliberately uses the documented title and `ssub/sun` provider, and limits resource requests to HTTPS `*.keeply.top` without redirects. It fails explicitly if that sample disappears or changes instead of silently choosing a different provider. It fetches no nested playlist, media segment, key, sprite or download page, and saves no tokens/bodies. This is an explicit network smoke command, not a network-dependent unit test.
|
||||
The former source-capable example used the documented title and `ssub/sun`, limiting requests to HTTPS `*.keeply.top` without redirects. It fetched no nested playlist, media segment, key, sprite or download page and saved no tokens/bodies. Those resource requests are no longer in the catalog example.
|
||||
|
||||
Wrapper-extraction verification, before TUI integration: `cargo test --locked` passed all 164 tests, including the episode-promise regression; `cargo clippy --locked --all-targets -- -D warnings` passed; `rustfmt --edition 2024 --check src/lib.rs src/miruro.rs examples/miruro_probe.rs` passed. The decoder regression covers independent tables, out-of-order unrelated chunks, preserved episode/absolute numbers and nulls, unresolved/rejected promises, bad references/cycles, unsupported tags and route redirects.
|
||||
|
||||
## Tubular → Terebi integration
|
||||
|
||||
`tubular` offers a centered YouTube/Miruro startup selector; an initial query follows the chosen provider. `tubular --miruro "apothecary"` bypasses it and starts the dedicated Miruro interface directly. `src/miruro_ui.rs` owns catalog → series → episodes navigation, synopsis/metadata presentation, cached screen history and flat episode collections; YouTube retains its separate Search/Feed/category interface. Catalog Enter opens actual returned episodes; episode Enter resolves fresh sources off-thread. Autocomplete is limited to eight titles, without invented pagination. `src/miruro_browser.rs` supplies native catalog/series/episode models and one worker for bounded network work; changing views/queries or episode selections invalidates stale source responses rather than launching obsolete selections. Canonical title/slug rules follow the [deployed helper][link-helpers], including Unicode NFD normalization.
|
||||
The startup selector and `--miruro` open the dedicated catalog → series → episodes interface. `miruro_ui` owns navigation, metadata presentation, cached history and collections; `miruro_browser` supplies catalog/episode models and catalog/detail worker requests. Autocomplete returns at most eight titles. Episode Enter passes a stable URL to Terebi; no Tubular source request or projection remains.
|
||||
|
||||
Both browsers share `src/ui.rs` for layout, search editing/cursor scrolling, selection styling and loading animation. Miruro uses the same full-width results/status chrome and centered metadata/help/removal popups as YouTube, while preserving its own catalog/episode rows and navigation. Press `d` for metadata; arrows/Ctrl-j/k scroll it, while plain j/k change the selected row.
|
||||
|
||||
Titles normalizing to an empty slug fail explicitly; the wrapper does not guess an alternative detail route. This does not reject accented Latin titles, which use NFD decomposition.
|
||||
|
||||
### Source document and launch
|
||||
### URL launch and ownership
|
||||
|
||||
`Client::source_document` chooses the first nonempty server/first direct HLS stream from the returned `ssub` track's `sun` provider. It requires native playback, `proxy:false`, and no enabled HLS host/query rewriting. Missing/unsupported sources are errors; no alternate language/provider, embed, proxy or refresh fallback is added.
|
||||
|
||||
| Terebi source-v1 field | Projection |
|
||||
| --- | --- |
|
||||
| `url` | Selected stream URL, unchanged, including its query tokens |
|
||||
| `headers` | Selected server map, without trimming or rewriting |
|
||||
| `identity` | `{provider:"miruro", media_id:<opaque catalog ID>, episode:<returned episode_number>}`; not slug/external ID/absolute number/array index/playback provider |
|
||||
| `title` | Episode display title |
|
||||
| `subtitles` | Selected Sun provider's VTT tracks; other external formats excluded |
|
||||
| Track `url`, `language`, `label`, `default` | Returned `file` and subtitle metadata |
|
||||
| Track `headers` | Explicit selected Sun server map, verified for Sun resources only; no general inheritance rule |
|
||||
|
||||
Projection validates HTTP(S) resource URLs, header syntax/values, case-insensitive duplicate headers, identities, control characters, 32-track/64-header limits and at most one default subtitle. Session creation also enforces the 1 MiB serialized source-document limit. Miruro media IDs allow at most 512 ASCII letters/digits/`-`/`_`; episodes range from 1 through 2147483647.
|
||||
|
||||
`State::miruro_session` binds the source identity to the selected episode, creates a unique private session directory (`0700`), writes immutable `source.json` (`0600`) and metadata/identity snapshots, and returns a **nonexistent** progress destination. `PlayerLaunch::start_source` passes absolute filesystem paths, not resource URLs/header values:
|
||||
Both providers use `State::progress_file(video)` and `PlayerLaunch::start(&video.id, player, progress_file)`. Miruro's `Identity::from_key(id)?.watch_url()?` produces:
|
||||
|
||||
```sh
|
||||
terebi --new-instance --source-file '/private/session/source.json' \
|
||||
terebi --new-instance \
|
||||
--url 'https://barelystarted.miruro.tv/watch/OPAQUE_ID/episode?ep=RETURNED_EPISODE' \
|
||||
--progress-file '/private/session/progress.jsonl'
|
||||
```
|
||||
|
||||
Terebi exclusively creates the report (`0600`). CLI exit acknowledges only launch acceptance, not successful media opening or completion. Tubular retains source/report files across launcher exit, final events, and Tubular shutdown because the independent GUI may still need them. The existing 4,096-session cap and explicit inactive-session cleanup apply; close the corresponding players before removing their session directories. Private source documents contain expiring URLs/headers; watched/collection state contains only stable identity and display metadata.
|
||||
The literal `episode` slug is non-identifying. Terebi's parser uses opaque ID plus episode; metadata acquisition must safely resolve canonical-route redirect envelopes. See the actionable [Terebi URL-playback handoff](terebi-url-playback-handoff.md).
|
||||
|
||||
Terebi's implemented transport preserves headers on master/variant/segment/key/init requests and probes Sun's image-suffixed media/subtitle resources by content/declared VTT format. All media redirects and foreign-origin HLS resources are rejected; subtitle redirects must stay on their initial origin. Terebi does not refresh expired sources. These restrictions remain visible errors, not silent provider switches.
|
||||
Tubular registers the expected stable identity in a private reporting session before launch. Terebi exclusively creates the report (`0600`); session directories remain private (`0700`). CLI exit acknowledges launch acceptance, not media readiness. Retention, the 4,096-session cap, explicit inactive-session cleanup and restart recovery remain unchanged; close corresponding players before removing sessions. No new `source.json`, source-v2 contract, segment file, persistent `Video` fields or collection migration is required.
|
||||
|
||||
Terebi owns fresh sources, headers, subtitles, playback titles, resume and optional timeline metadata for both direct and Tubular URL launches. Current native URL/progress support is not title/timeline completion: the title is currently `Miruro episode N`; title enrichment and intro/outro progress-bar bands remain Terebi work. Optional metadata failure must not prevent playback. No caller-specific source fallback, automatic skipping or skip buttons are requested.
|
||||
|
||||
### Progress and collections
|
||||
|
||||
@@ -476,9 +462,9 @@ Tubular validates the version, namespace, media ID, episode, event and numeric f
|
||||
|
||||
Matching acknowledged positive positions admit currently-watching entries. Watched status retains `completion_eligible && duration_seconds > 0 && position_seconds / duration_seconds >= 0.95`; null duration is ineligible. Seeking near the end can satisfy it. Launch/window closure alone cannot. Duplicate records, partial trailing lines and later reports recovered after Tubular restart retain the existing session-consumption behavior.
|
||||
|
||||
Watch later, saved videos, currently watching and watched state accept stable episode keys `miruro:<opaque ID>:<canonical episode>`. Catalog results use opaque media IDs in native `Media` rows, not fabricated video/episode keys; they are not saveable/playable episode identities. Saved episode playback resolves fresh sources; CDN addresses/tokens never become collection keys. Playback and persisted state remain shared, but each interface filters collections before display, selection, counts and removal. Miruro has no YouTube channel identity, so its flat episode lists exclude category/tag/channel actions and YouTube extraction/thumbnails. Planned/listed counts are metadata, not a claim of supported source availability; only actual returned episode numbers are offered for playback.
|
||||
Collections and watched state use `miruro:<opaque ID>:<canonical episode>`. Catalog rows are not playable episode identities. Saved and old snapshot playback needs only that key, not a canonical slug, title-derived route or stored source URL. Terebi resolves fresh resources; CDN tokens never become collection keys. Each interface filters shared collections by provider. Only actual returned episode numbers are selectable; planned counts do not guarantee source availability.
|
||||
|
||||
### Integration verification
|
||||
### Historical source-file integration verification (before URL cutover)
|
||||
|
||||
The actual Tubular terminal workflow was exercised with isolated `$XDG_CONFIG_HOME` state and the updated Terebi developer bundle selected by `--player`: autocomplete returned eight titles; the selected title returned two episodes; episode 1 was saved to both collections, freshly resolved, and launched through `State::miruro_session` / `PlayerLaunch::start_source`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user