use chrono::DateTime; use serde_json::Value; pub struct Feed { episodes: Vec, } impl Feed { pub async fn new() -> Result> { let mut ret = Self { episodes: Vec::new(), }; ret.fetch().await?; Ok(ret) } pub async fn fetch(&mut self) -> Result<(), Box> { let broadcasts = get_all_broadcasts().await?; for broadcast in broadcasts { if !self.has_broadcast_url(&broadcast) { if let Some(broadcast) = Broadcast::from(broadcast).await.unwrap() { self.episodes.push(broadcast); } else { return Ok(()); } } } Ok(()) } fn has_broadcast_url(&self, url: &str) -> bool { self.episodes.iter().any(|e| e.url == url) } pub fn to_rss(&self) -> String { let mut ret = String::new(); ret.push_str(r#""#); ret.push_str(r#""#); ret.push_str(""); ret.push_str("Ö1 Live Journal Feed"); ret.push_str("https://news.hofer.link"); ret.push_str("Feed für Ö1 Journale. Live."); for episode in &self.episodes { ret.push_str(""); ret.push_str(&format!( "{} ({})", episode.title, episode.timestamp.format("%d.%m.") )); ret.push_str(&format!( "\n", quick_xml::escape::escape(&episode.media_url) )); ret.push_str("Journal"); ret.push_str(&format!( "{}", episode.timestamp.to_rfc2822() )); ret.push_str(""); } ret.push_str(" "); ret.push_str(""); ret } } async fn get_all_broadcasts() -> Result, Box> { // List of broadcasts: https://audioapi.orf.at/oe1/api/json/current/broadcasts // // ^ contains link, e.g. https://audioapi.orf.at/oe1/api/json/4.0/broadcast/797577/20250611 let url = "https://audioapi.orf.at/oe1/api/json/current/broadcasts"; let data: Value = reqwest::get(url).await?.json().await?; let mut ret: Vec = Vec::new(); if let Some(days) = data.as_array() { for day in days { if let Some(broadcasts) = day["broadcasts"].as_array() { for broadcast in broadcasts { if ["Ö1 Morgenjournal", "Ö1 Mittagsjournal", "Ö1 Abendjournal"] .contains(&broadcast["title"].as_str().unwrap()) { ret.push(broadcast["href"].as_str().unwrap().into()); } } } } } Ok(ret) } #[derive(Clone)] pub struct Broadcast { pub url: String, pub media_url: String, pub title: String, pub timestamp: DateTime, } impl Broadcast { async fn from(url: String) -> Result, Box> { let data: Value = reqwest::get(&url).await?.json().await?; let Some(streams) = data["streams"].as_array() else { return Err(String::from("No 'streams' found").into()); }; if streams.is_empty() { return Ok(None); } assert_eq!(streams.len(), 1); let Some(id) = streams[0]["loopStreamId"].as_str() else { return Err(String::from("No 'loopStreamId' found").into()); }; let media_url = format!("https://loopstream01.apa.at/?channel=oe1&shoutcast=0&id={id}"); let Some(title) = data["title"].as_str() else { return Err(format!("{url} has no title").into()); }; let Some(timestamp) = data["start"].as_number() else { return Err(format!("{url} has no start").into()); }; let Some(timestamp) = DateTime::from_timestamp(timestamp.as_i64().unwrap() / 1000, 0) else { return Err(format!( "broadcastDay in {url} not in a valid format (unix timestamp): {timestamp}" ) .into()); }; Ok(Some(Self { url, media_url, title: title.into(), timestamp, })) } } impl PartialEq for Broadcast { fn eq(&self, other: &Self) -> bool { self.url == other.url } }