//! 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, ) -> io::Result { 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")) }