refactor: delegate Miruro playback resolution to Terebi
This commit is contained in:
+7
-129
@@ -1,9 +1,8 @@
|
||||
//! Live read-only smoke: cargo run --locked --example miruro_probe
|
||||
//! Uses the documented research title and direct Sun provider; no media segments.
|
||||
use std::{io, time::Duration};
|
||||
//! Exercises native catalog metadata and stable episode URLs; no playback requests.
|
||||
use std::io;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use tubular::miruro::Client;
|
||||
use tubular::miruro::{Client, Identity};
|
||||
|
||||
fn invalid(message: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, message)
|
||||
@@ -28,132 +27,11 @@ fn main() -> io::Result<()> {
|
||||
.and_then(|number| u32::try_from(number).ok())
|
||||
.ok_or_else(|| invalid("No episode metadata available"))?;
|
||||
println!(
|
||||
"detail: {} episodes; selected episode {number}",
|
||||
"detail: {} returned episodes; selected episode {number}",
|
||||
detail.episodes.len()
|
||||
);
|
||||
let sources = client.sources(id, number)?;
|
||||
let tracks = sources["tracks"]
|
||||
.as_array()
|
||||
.ok_or_else(|| invalid("Missing tracks"))?;
|
||||
let provider = tracks
|
||||
.iter()
|
||||
.find(|track| track["track"] == "ssub")
|
||||
.and_then(|track| track["providers"].as_array())
|
||||
.and_then(|providers| {
|
||||
providers
|
||||
.iter()
|
||||
.find(|provider| provider["provider"] == "sun")
|
||||
})
|
||||
.ok_or_else(|| invalid("Direct Sun soft-subtitle provider not returned"))?;
|
||||
let server = provider["servers"]
|
||||
.as_array()
|
||||
.and_then(|servers| {
|
||||
servers.iter().find(|server| {
|
||||
server["streams"]
|
||||
.as_array()
|
||||
.is_some_and(|streams| !streams.is_empty())
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| invalid("No direct Sun stream returned"))?;
|
||||
let headers = server["headers"]
|
||||
.as_object()
|
||||
.ok_or_else(|| invalid("Missing server headers"))?;
|
||||
println!(
|
||||
"sources: {} tracks; selected ssub/sun; {} server headers retained",
|
||||
tracks.len(),
|
||||
headers.len()
|
||||
);
|
||||
let agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(Duration::from_secs(25)))
|
||||
.http_status_as_error(false)
|
||||
.max_redirects(0)
|
||||
.build()
|
||||
.new_agent();
|
||||
let master = resource(
|
||||
&agent,
|
||||
"hls_master",
|
||||
server["streams"][0]["url"]
|
||||
.as_str()
|
||||
.ok_or_else(|| invalid("Missing stream URL"))?,
|
||||
headers,
|
||||
)?;
|
||||
if !master.starts_with("#EXTM3U") || !master.contains("#EXT-X-STREAM-INF:") {
|
||||
return Err(invalid("Expected an HLS master playlist"));
|
||||
}
|
||||
let subtitle = provider["subtitles"]
|
||||
.as_array()
|
||||
.and_then(|subtitles| {
|
||||
subtitles
|
||||
.iter()
|
||||
.find(|subtitle| subtitle["default"] == true)
|
||||
})
|
||||
.ok_or_else(|| invalid("No default subtitle returned"))?;
|
||||
let subtitle = resource(
|
||||
&agent,
|
||||
"subtitle",
|
||||
subtitle["file"]
|
||||
.as_str()
|
||||
.ok_or_else(|| invalid("Missing subtitle URL"))?,
|
||||
headers,
|
||||
)?;
|
||||
if !subtitle.starts_with("WEBVTT") || !subtitle.contains(" --> ") {
|
||||
return Err(invalid("Expected timed VTT subtitles"));
|
||||
}
|
||||
println!(
|
||||
"PASS: native search -> detail/episodes -> sources -> HLS master + subtitle; no media segments or tokens saved"
|
||||
);
|
||||
let identity = Identity::new(id.into(), number)?;
|
||||
println!("watch URL: {}", identity.watch_url()?);
|
||||
println!("PASS: native search -> detail/episodes -> stable watch URL; no playback requests");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resource(
|
||||
agent: &ureq::Agent,
|
||||
label: &str,
|
||||
url: &str,
|
||||
headers: &Map<String, Value>,
|
||||
) -> io::Result<String> {
|
||||
let uri: ureq::http::Uri = url
|
||||
.parse()
|
||||
.map_err(|_| invalid("Invalid Sun resource URL"))?;
|
||||
if uri.scheme_str() != Some("https")
|
||||
|| !uri.host().is_some_and(|host| host.ends_with(".keeply.top"))
|
||||
|| uri
|
||||
.authority()
|
||||
.is_some_and(|authority| authority.as_str().contains('@'))
|
||||
|| uri.port_u16().is_some_and(|port| port != 443)
|
||||
{
|
||||
return Err(invalid(
|
||||
"Smoke only permits the verified Sun HTTPS CDN; no redirects",
|
||||
));
|
||||
}
|
||||
let mut request = agent.get(url);
|
||||
for (name, value) in headers {
|
||||
request = request.header(
|
||||
name.as_str(),
|
||||
value
|
||||
.as_str()
|
||||
.ok_or_else(|| invalid("Invalid server header"))?,
|
||||
);
|
||||
}
|
||||
let mut response = request
|
||||
.call()
|
||||
.map_err(|_| io::Error::other("Sun resource HTTP transport failed"))?;
|
||||
let status = response.status().as_u16();
|
||||
let mime = response.body().mime_type().unwrap_or("missing");
|
||||
println!("{label}: HTTP {status}; Content-Type {mime}");
|
||||
let expected = if label == "subtitle" {
|
||||
"text/vtt"
|
||||
} else {
|
||||
"application/vnd.apple.mpegurl"
|
||||
};
|
||||
if status != 200 || mime != expected {
|
||||
return Err(invalid(
|
||||
"Resource failed or returned unexpected content; stopping without retry",
|
||||
));
|
||||
}
|
||||
response
|
||||
.body_mut()
|
||||
.with_config()
|
||||
.limit(2 * 1024 * 1024)
|
||||
.read_to_string()
|
||||
.map_err(|_| invalid("Resource response unreadable, timed out, or exceeded 2 MiB"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user