diff --git a/src/lib.rs b/src/lib.rs index 484a82e..c26b88b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,59 +1,142 @@ use chrono::NaiveDate; use serde_json::Value; -#[derive(Clone)] -pub struct Episode { - pub url: String, - pub date: NaiveDate, +pub struct Feed { + episodes: Vec, } -pub async fn newest_morning_journal_streaming_url() -> Result> { - let (date, url) = get_newest_morning_journal().await?; - let url = get_streaming_url(url).await?; +impl Feed { + pub async fn new() -> Result> { + let mut ret = Self { + episodes: Vec::new(), + }; - Ok(Episode { url, date }) + ret.fetch().await?; + + Ok(ret) + } + + pub async fn fetch(&mut self) -> Result<(), Box> { + let broadcasts = get_all_broadcasts().await?; + println!("hea"); + + for broadcast in broadcasts { + if !self.has_broadcast_url(&broadcast) { + if let Ok(Some(broadcast)) = Broadcast::from(broadcast).await { + self.episodes.push(broadcast); + } + } + } + + 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.date.format("%d. %m.") + )); + ret.push_str(&format!( + "\n", + quick_xml::escape::escape(&episode.media_url) + )); + ret.push_str("Journal"); + ret.push_str(""); + } + + ret.push_str(" "); + ret.push_str(""); + + ret + } } -// 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 -async fn get_newest_morning_journal() -> Result<(NaiveDate, String), Box> { +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.iter().rev() { + for day in days { if let Some(broadcasts) = day["broadcasts"].as_array() { - for broadcast in broadcasts.iter().rev() { - if broadcast["title"] == "Einladen, ausladen, boykottieren" - && let Some(href) = broadcast["href"].as_str() - { - let date = broadcast["broadcastDay"].as_number().unwrap_or_else(|| { - panic!("There needs to be a broadcastDay! {}", &broadcast) - }); - let date = NaiveDate::parse_from_str(&date.to_string(), "%Y%m%d").expect("broadcastDay in https://audioapi.orf.at/oe1/api/json/current/broadcasts not in a valid format"); - return Ok((date, href.into())); - } + for broadcast in broadcasts { + // if broadcast["title"] in []; + ret.push(broadcast["href"].to_string()); } } } } - Err(String::from("No Ö1 Morgenjournal found").into()) + Ok(ret) } -async fn get_streaming_url(url: String) -> Result> { - 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()); - }; - assert_eq!(streams.len(), 1); - - let Some(id) = streams[0]["loopStreamId"].as_str() else { - return Err(String::from("No 'loopStreamId' found").into()); - }; - - Ok(format!( - "https://loopstream01.apa.at/?channel=oe1&shoutcast=0&id={id}" - )) +#[derive(Clone)] +pub struct Broadcast { + pub url: String, + pub media_url: String, + pub title: String, + pub date: NaiveDate, +} + +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(date) = data["broadcastDay"].as_number() else { + return Err(format!("{url} has no broadcastDay").into()); + }; + let Ok(date) = NaiveDate::parse_from_str(&date.to_string(), "%Y%m%d") else { + return Err( + format!("broadcastDay in {url} not in a valid format (%Y%m%d): {date}").into(), + ); + }; + + Ok(Some(Self { + url, + media_url, + title: title.into(), + date, + })) + } +} + +impl PartialEq for Broadcast { + fn eq(&self, other: &Self) -> bool { + self.url == other.url + } } diff --git a/src/main.rs b/src/main.rs index 43494f0..a1ec8df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box> { - let state = Arc::new(AppState::new()); + let state = Arc::new(AppState::new().await.unwrap()); let app = Router::new() .route("/", get(streamer::stream_handler)) diff --git a/src/state.rs b/src/state.rs index 3936a3b..d3f6e77 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,45 +1,19 @@ -use chrono::Local; -use player::Episode; +use player::Feed; use std::sync::Arc; use tokio::sync::RwLock; pub struct AppState { - pub episodes: RwLock>, + pub feed: RwLock, } impl AppState { - pub fn new() -> Self { - Self { - episodes: RwLock::new(Vec::new()), - } + pub async fn new() -> Result> { + Ok(Self { + feed: RwLock::new(Feed::new().await?), + }) } - pub async fn check_update(self: Arc) { - if self.already_downloaded_today().await { - return; - } - - let latest_episode = player::newest_morning_journal_streaming_url() - .await - .unwrap(); - - if self.already_downloaded_url(&latest_episode.url).await { - return; - } - - let mut old = self.episodes.read().await.clone(); - old.push(latest_episode); - let new = old.into_iter().rev().take(10).collect(); // only keep last 10 - - *self.episodes.write().await = new; - } - - async fn already_downloaded_today(self: &Arc) -> bool { - let today = Local::now().date_naive(); - self.episodes.read().await.iter().any(|x| x.date == today) - } - - async fn already_downloaded_url(self: &Arc, url: &str) -> bool { - self.episodes.read().await.iter().any(|x| x.url == url) + pub async fn update(self: Arc) { + self.feed.write().await.fetch().await.unwrap(); } } diff --git a/src/streamer.rs b/src/streamer.rs index 5edfca6..8bee76b 100644 --- a/src/streamer.rs +++ b/src/streamer.rs @@ -1,44 +1,14 @@ use crate::state::AppState; use axum::{extract::State, http::HeaderMap, response::IntoResponse}; -use player::Episode; use reqwest::header; use std::sync::Arc; pub async fn stream_handler(State(state): State>) -> impl IntoResponse { - state.clone().check_update().await; + state.clone().update().await; - let content = feed(&state.episodes.read().await.to_vec()); + let content = state.feed.read().await.to_rss(); let mut headers = HeaderMap::new(); headers.insert(header::CONTENT_TYPE, "application/rss+xml".parse().unwrap()); (headers, content) } - -fn feed(episodes: &Vec) -> String { - let mut ret = String::new(); - ret.push_str(r#""#); - ret.push_str(r#""#); - ret.push_str(""); - ret.push_str("Ö1 Morgenjournal Feed"); - ret.push_str("https://news.hofer.link"); - ret.push_str("Feed für Ö1 Morgenjournal. Live."); - - for episode in episodes { - ret.push_str(""); - ret.push_str(&format!( - "Morgenjournal {}", - &episode.date.format("%d. %m.") - )); - ret.push_str(&format!( - "\n", - quick_xml::escape::escape(&episode.url) - )); - ret.push_str("Morgenjournal"); - ret.push_str(""); - } - - ret.push_str(" "); - ret.push_str(""); - - ret -}