Files
tubular/examples/miruro_probe.rs
T

160 lines
5.3 KiB
Rust

//! 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};
use serde_json::{Map, Value};
use tubular::miruro::Client;
fn invalid(message: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
fn main() -> io::Result<()> {
let client = Client::default();
let results = client.search("apothecary")?;
let media = results
.iter()
.find(|media| media["id"] == "OERe_a0YS4h-DlsBDOeXwX7HOO6316tw")
.ok_or_else(|| invalid("Research title no longer appears in search"))?;
let id = media["id"]
.as_str()
.ok_or_else(|| invalid("Missing media ID"))?;
println!("search: {} records; selected Miruro ID {id}", results.len());
let detail = client.detail(id, "the-apothecary-diaries-season-3")?;
let number = detail
.episodes
.first()
.and_then(|episode| episode["episode_number"].as_u64())
.and_then(|number| u32::try_from(number).ok())
.ok_or_else(|| invalid("No episode metadata available"))?;
println!(
"detail: {} 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"
);
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"))
}