fix: support current official Miruro mirrors

This commit is contained in:
2026-10-25 12:42:10 +01:00
parent ae87bb8b36
commit baa91d4091
4 changed files with 206 additions and 30 deletions
+6 -2
View File
@@ -2,13 +2,15 @@
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.
**Mirror update (2026-10-25):** the [official directory](https://www.miruro.com/#domains) links Miruro 2.0 at `https://miruro.cx`, `https://miruro.pw`, `https://miruro.ws`, `https://miruro.to`, `https://miruro.tv`, and `https://miruro.bz`; `www.` hosts are labeled Legacy. Tubular now uses these six explicit origins for catalog/detail requests. Local native `ureq` and curl probes received **403 `text/html`** from all six autocomplete endpoints; curl returned Cloudflare block pages. Thus live schema/ID compatibility and successful browsing on these mirrors remain unverified. The original `cargo run --locked --example miruro_probe` now exercises all six and reports `Miruro request failed on 6/6 mirrors`, rather than stopping at the obsolete browsing origin. Endpoint/schema evidence below remains the historical snapshot, not a claim of current mirror access.
Evidence labels:
- **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 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.
**Transport distinction (historical):** 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 was proven for that client and sample, not every transport, network, provider or future request. Inspect HTTP status and Content-Type before decoding; never treat protection pages as empty results. The current client can try the next explicitly listed official mirror, but cannot restore access when every mirror blocks it.
## Endpoint map
@@ -395,7 +397,7 @@ Current library interface:
| Call | Result / contract |
| --- | --- |
| `Client::default()` | Reusable blocking HTTP agent; 25-second global request timeout; no automatic redirects |
| `Client::default()` | Reusable blocking HTTP agent; six official 2.0 mirrors; sticky successful origin; 25-second total request budget, at most five seconds per attempt; 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 |
| `Identity::new(id, episode)`, `Identity::from_key(key)` | Validated catalog namespace/episode identity; `key()` is `miruro:<opaque ID>:<episode>` |
@@ -403,6 +405,8 @@ Current library interface:
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.
The client starts at `miruro.cx` and remembers the last origin whose response passed HTTP, Content-Type and bounded-body checks. On transport/body failures, unexpected Content-Type, HTTP 301–308, 403, 404, 408 or 5xx, it tries the remaining origins once in directory order. Other HTTP errors, including 400, 401 and 429, stop immediately; rotating mirrors must not circumvent rate limits. Exhaustion reports the attempted/total mirror count and last origin/error. JSON/route decoding and detail-ID validation remain mandatory and do not trigger additional fallback. The directory is not scraped at runtime, and requests are not randomized. Playback URLs deliberately retain `barelystarted.miruro.tv`: current Terebi admits and resolves only that hostname, so playback failover requires a separate player change.
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.
The retained `cargo run --locked --example miruro_probe` smoke now covers search → detail/episodes only, not sources or playback.