diff --git a/src/lib.rs b/src/lib.rs index 1918b4b..118741b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,48 @@ +mod rss; +mod web; + use chrono::DateTime; use serde_json::Value; +use std::sync::Arc; +use tokio::{net::TcpListener, sync::RwLock}; -pub struct Feed { +pub async fn start( + title: String, + link: String, + desc: String, + filter_titles: Vec, + listener: TcpListener, +) -> Result<(), Box> { + let state = Arc::new(RwLock::new( + Feed::new(title, link, desc, filter_titles).await.unwrap(), + )); + + web::serve(state, listener).await?; + + Ok(()) +} + +struct Feed { episodes: Vec, + title: String, + link: String, + desc: String, + filter_titles: Vec, } impl Feed { - pub async fn new() -> Result> { + async fn new( + title: String, + link: String, + desc: String, + filter_titles: Vec, + ) -> Result> { let mut ret = Self { episodes: Vec::new(), + title, + link, + desc, + filter_titles, }; ret.fetch().await?; @@ -16,12 +50,12 @@ impl Feed { Ok(ret) } - pub async fn fetch(&mut self) -> Result<(), Box> { - let broadcasts = get_all_broadcasts().await?; + async fn fetch(&mut self) -> Result<(), Box> { + let broadcasts = self.get_all_broadcasts().await?; for broadcast in broadcasts { if !self.has_broadcast_url(&broadcast) { - if let Some(broadcast) = Broadcast::from(broadcast).await.unwrap() { + if let Some(broadcast) = Broadcast::from_url(broadcast).await.unwrap() { self.episodes.push(broadcast); } else { return Ok(()); @@ -43,77 +77,47 @@ impl Feed { 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."); + async fn get_all_broadcasts(&self) -> 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?; - 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(""); - } + let mut ret: Vec = Vec::new(); - 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()); + if let Some(days) = data.as_array() { + for day in days { + if let Some(broadcasts) = day["broadcasts"].as_array() { + for broadcast in broadcasts { + if self.filter_titles.is_empty() + || self + .filter_titles + .contains(&broadcast["title"].as_str().unwrap().into()) + { + { + ret.push(broadcast["href"].as_str().unwrap().into()); + } + } } } } } - } - Ok(ret) + Ok(ret) + } } #[derive(Clone)] -pub struct Broadcast { - pub url: String, - pub media_url: String, - pub title: String, - pub timestamp: DateTime, +struct Broadcast { + url: String, + media_url: String, + title: String, + timestamp: DateTime, } impl Broadcast { - async fn from(url: String) -> Result, Box> { + async fn from_url(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()); diff --git a/src/main.rs b/src/main.rs index 64fb97b..cf619ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,19 @@ -use axum::{extract::State, http::HeaderMap, response::IntoResponse, routing::get, Router}; -use player::Feed; -use reqwest::header; -use std::sync::Arc; -use tokio::sync::RwLock; - -pub async fn stream_handler(State(state): State>>) -> impl IntoResponse { - state.write().await.fetch().await.unwrap(); - - let content = state.read().await.to_rss(); - - let mut headers = HeaderMap::new(); - headers.insert(header::CONTENT_TYPE, "application/rss+xml".parse().unwrap()); - (headers, content) -} - #[tokio::main] async fn main() -> Result<(), Box> { - let state = Arc::new(RwLock::new(Feed::new().await.unwrap())); - - let app = Router::new() - .route("/", get(stream_handler)) - .with_state(state.clone()); - - println!("Streaming server running on http://localhost:3029"); - let listener = tokio::net::TcpListener::bind("0.0.0.0:3029").await?; - axum::serve(listener, app).await?; + + player::start( + "Ö1 Live Journal Feed".into(), + "https://news.hofer.link".into(), + "Feed für Ö1 Journale. Live.".into(), + vec![ + "Ö1 Morgenjournal".into(), + "Ö1 Mittagsjournal".into(), + "Ö1 Abendjournal".into(), + ], + listener, + ) + .await?; Ok(()) } diff --git a/src/rss.rs b/src/rss.rs new file mode 100644 index 0000000..fc507e2 --- /dev/null +++ b/src/rss.rs @@ -0,0 +1,58 @@ +use crate::{Broadcast, Feed}; + +pub(super) trait ToRss { + fn title(&self) -> &str; + fn link(&self) -> &str; + fn desc(&self) -> &str; + fn episodes(&self) -> &Vec; + + fn to_rss(&self) -> String { + let mut ret = String::new(); + ret.push_str(r#""#); + ret.push_str(r#""#); + ret.push_str(""); + ret.push_str(&format!("{}", self.title())); + ret.push_str(&format!("{}", self.link())); + ret.push_str(&format!("{}", self.desc())); + + for episode in self.episodes() { + let desc = format!("{} ({})", episode.title, episode.timestamp.format("%d.%m.")); + + ret.push_str(""); + ret.push_str(&format!("{desc}",)); + ret.push_str(&format!( + "\n", + quick_xml::escape::escape(&episode.media_url) + )); + ret.push_str(&format!("{desc}")); + ret.push_str(&format!( + "{}", + episode.timestamp.to_rfc2822() + )); + ret.push_str(""); + } + + ret.push_str(" "); + ret.push_str(""); + + ret + } +} + +impl ToRss for Feed { + fn title(&self) -> &str { + &self.title + } + + fn link(&self) -> &str { + &self.link + } + + fn desc(&self) -> &str { + &self.desc + } + + fn episodes(&self) -> &Vec { + &self.episodes + } +} diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 0000000..940a1a2 --- /dev/null +++ b/src/web.rs @@ -0,0 +1,28 @@ +use crate::{rss::ToRss, Feed}; +use axum::{extract::State, http::HeaderMap, response::IntoResponse, routing::get, Router}; +use reqwest::header; +use std::sync::Arc; +use tokio::{net::TcpListener, sync::RwLock}; + +async fn stream_handler(State(state): State>>) -> impl IntoResponse { + state.write().await.fetch().await.unwrap(); + + let content = state.read().await.to_rss(); + + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, "application/rss+xml".parse().unwrap()); + (headers, content) +} + +pub(super) async fn serve( + state: Arc>, + listener: TcpListener, +) -> Result<(), Box> { + let app = Router::new() + .route("/", get(stream_handler)) + .with_state(state.clone()); + + axum::serve(listener, app).await?; + + Ok(()) +}