feat: add verified native Miruro API wrapper
This commit is contained in:
+47
-7
@@ -1,14 +1,14 @@
|
||||
# Miruro API research
|
||||
|
||||
Observed **2026-10-12**, scoped to **https://barelystarted.miruro.tv**. These are reverse-engineering notes for a possible in-repository wrapper, not an official API specification. Tubular does not yet implement Miruro support.
|
||||
Observed **2026-10-12**, scoped to **https://barelystarted.miruro.tv**. These are reverse-engineering notes, not an official API specification. Tubular now has a native API module and runnable smoke example, but no Miruro TUI or Terebi integration.
|
||||
|
||||
Evidence labels:
|
||||
|
||||
- **Verified:** an actual HTTP response obtained through the research JavaScript `fetch` transport, with bounded timeouts and no supplied authentication, cookies, or protection bypass.
|
||||
- **Verified:** an actual HTTP response obtained through the research JavaScript `fetch` transport, or the explicitly identified local Rust probe below, with bounded timeouts and no supplied authentication, cookies, or protection bypass.
|
||||
- **Frontend:** behavior read from deployed first-party JavaScript; not necessarily a server-side guarantee.
|
||||
- **Unknown:** not established by those sources or requests.
|
||||
|
||||
**Transport limitation:** research requests returned JSON, but a local `curl` request to the same source-validation URL returned **403 `text/html`**, a Cloudflare block page. Native-wrapper access is therefore unproven. Inspect HTTP status and Content-Type before decoding; stop on protection pages rather than treating them as empty results or adding bypass machinery. Successful research requests do not establish cross-client accessibility.
|
||||
**Transport distinction:** research requests returned JSON, while a local `curl` source-validation request returned **403 `text/html`**, a Cloudflare block page. A subsequent local Rust `ureq` probe passed the search → detail/episodes → sources → HLS master/subtitle path without bypass machinery. Native access is proven for that client and sample, not every transport, network, provider or future request. Inspect HTTP status and Content-Type before decoding; stop on protection pages rather than treating them as empty results.
|
||||
|
||||
## Endpoint map
|
||||
|
||||
@@ -348,7 +348,7 @@ All research data GETs below used `fetch` with 20–25-second timeouts, no suppl
|
||||
| [strm client][strm-client] | 200 / text/javascript | Source inspected, not executed |
|
||||
| Local shell curl, known source ID with n=0 | 403 / text/html | Cloudflare block page |
|
||||
|
||||
One worker's homepage fetch without a trailing slash returned nginx 404, while the trailing-slash HTML read succeeded. This unresolved transport/URL discrepancy is not evidence of an API rule. No browser testing, playback, media-segment downloads, account mutations, protection bypass, or application tests were performed for this documentation-only change.
|
||||
One worker's homepage fetch without a trailing slash returned nginx 404, while the trailing-slash HTML read succeeded. This unresolved transport/URL discrepancy is not evidence of an API rule. The initial documentation research did not run application tests; the later native-wrapper verification is recorded below. Neither investigation used a browser, playback, media-segment downloads, account mutations or protection bypass.
|
||||
|
||||
## Bounded reproduction requests
|
||||
|
||||
@@ -387,10 +387,50 @@ console.log(response.status, response.headers.get('content-type'));
|
||||
console.log(await response.text());
|
||||
```
|
||||
|
||||
## Open questions before implementation
|
||||
## Native Rust wrapper and verified probe
|
||||
|
||||
- Can the real native HTTP client access this deployment without being blocked? Local curl failed; no workaround has been implemented or tested.
|
||||
- Is there a stable plain metadata/episode API, or must the wrapper own a SvelteKit/devalue decoder tied to this deployment?
|
||||
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.
|
||||
|
||||
Library interface, exposed as `tubular::miruro`:
|
||||
|
||||
| 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 |
|
||||
|
||||
The wrapper does not choose providers/languages or implement a player. It preserves JSON metadata rather than freezing every deployment-specific field into a second schema. 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.
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
cargo run --locked --example miruro_probe
|
||||
```
|
||||
|
||||
Actual output from the extracted wrapper:
|
||||
|
||||
```text
|
||||
search: 8 records; selected Miruro ID OERe_a0YS4h-DlsBDOeXwX7HOO6316tw
|
||||
detail: 2 episodes; selected episode 1
|
||||
sources: 2 tracks; selected ssub/sun; 2 server headers retained
|
||||
hls_master: HTTP 200; Content-Type application/vnd.apple.mpegurl
|
||||
subtitle: HTTP 200; Content-Type text/vtt
|
||||
PASS: native search -> detail/episodes -> sources -> HLS master + subtitle; no media segments or tokens saved
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Verification: `cargo test --locked` passed all 164 tests, including the new 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.
|
||||
|
||||
## Open questions before TUI/player integration
|
||||
|
||||
- Will this same native client remain accessible on other networks/deployments? Its sampled path passed; local curl previously failed, and no workaround is implemented.
|
||||
- Is there a stable plain metadata/episode API? The wrapper currently owns the bounded JSON-shaped SvelteKit decoder for the observed deployment.
|
||||
- What are server-side filter/enum constraints, multi-value semantics, cursor lifetime, page-size-cookie limits, numeric-ID lookups and slug behavior?
|
||||
- What happens for unavailable episodes, empty tracks, provider failures, bad cursors, rejected promise chunks and syntactically valid missing IDs?
|
||||
- Which headers/config transforms are required per provider, and can Terebi propagate them to HLS playlists/segments/keys and external subtitles?
|
||||
|
||||
Reference in New Issue
Block a user