Compare commits

...

2 Commits

Author SHA1 Message Date
Philipp Hofer
e1e21f8837 Revert "add test structs"
All checks were successful
CI/CD Pipeline / test (push) Successful in 2m0s
CI/CD Pipeline / deploy (push) Successful in 1m45s
This reverts commit 14ee4d2767.
2025-10-16 11:53:11 +02:00
Philipp Hofer
5ee6a679c5 Revert "add tracing + custom error type"
This reverts commit 4702017914.
2025-10-16 11:53:06 +02:00
5 changed files with 32 additions and 170 deletions

14
Cargo.lock generated
View File

@@ -724,9 +724,7 @@ dependencies = [
"quick-xml", "quick-xml",
"reqwest", "reqwest",
"serde_json", "serde_json",
"thiserror",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
@@ -1292,21 +1290,9 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.34" version = "0.1.34"

View File

@@ -10,5 +10,3 @@ reqwest = { version = "0.12", features = ["stream", "json", "rustls-tls"], defau
serde_json = "1" serde_json = "1"
chrono = "0.4" chrono = "0.4"
quick-xml = "0.38" quick-xml = "0.38"
thiserror = "2"
tracing = "0.1"

View File

@@ -4,9 +4,7 @@ mod web;
use chrono::DateTime; use chrono::DateTime;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error;
use tokio::{net::TcpListener, sync::RwLock}; use tokio::{net::TcpListener, sync::RwLock};
use tracing::warn;
pub async fn start( pub async fn start(
title: String, title: String,
@@ -16,9 +14,7 @@ pub async fn start(
listener: TcpListener, listener: TcpListener,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let state = Arc::new(RwLock::new( let state = Arc::new(RwLock::new(
LiveFeed::new(title, link, desc, filter_titles) Feed::new(title, link, desc, filter_titles).await.unwrap(),
.await
.unwrap(),
)); ));
web::serve(state, listener).await?; web::serve(state, listener).await?;
@@ -26,62 +22,7 @@ pub async fn start(
Ok(()) Ok(())
} }
#[derive(Error, Debug)] struct Feed {
enum FetchError {
#[error("error fetching url")]
Fetching(reqwest::Error),
#[error("error parsing json")]
JsonParsing(reqwest::Error),
}
trait Feed {
async fn fetch(&mut self) -> Result<(), FetchError>;
}
#[cfg(test)]
struct TestFeed {
episodes: Vec<Broadcast>,
pub(crate) amount_fetch_calls: usize,
}
#[cfg(test)]
impl Default for TestFeed {
fn default() -> Self {
Self {
episodes: vec![Broadcast::test()],
amount_fetch_calls: 0,
}
}
}
#[cfg(test)]
impl rss::ToRss for TestFeed {
fn title(&self) -> &str {
"Test RSS Title"
}
fn link(&self) -> &str {
"https://test.rss"
}
fn desc(&self) -> &str {
"Test RSS Desc"
}
fn episodes(&self) -> &Vec<crate::Broadcast> {
&self.episodes
}
}
#[cfg(test)]
impl Feed for TestFeed {
async fn fetch(&mut self) -> Result<(), FetchError> {
self.amount_fetch_calls += 1;
Ok(())
}
}
struct LiveFeed {
episodes: Vec<Broadcast>, episodes: Vec<Broadcast>,
title: String, title: String,
link: String, link: String,
@@ -89,27 +30,7 @@ struct LiveFeed {
filter_titles: Vec<String>, filter_titles: Vec<String>,
} }
impl Feed for LiveFeed { impl Feed {
async fn fetch(&mut self) -> Result<(), FetchError> {
let broadcasts = self.get_all_broadcasts().await?;
for broadcast in broadcasts {
if !self.has_broadcast_url(&broadcast) {
if let Some(broadcast) = Broadcast::from_url(broadcast).await.unwrap() {
self.episodes.push(broadcast);
} else {
return Ok(());
}
}
}
self.only_keep_last_episodes();
Ok(())
}
}
impl LiveFeed {
async fn new( async fn new(
title: String, title: String,
link: String, link: String,
@@ -129,6 +50,24 @@ impl LiveFeed {
Ok(ret) Ok(ret)
} }
async fn fetch(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let broadcasts = self.get_all_broadcasts().await?;
for broadcast in broadcasts {
if !self.has_broadcast_url(&broadcast) {
if let Some(broadcast) = Broadcast::from_url(broadcast).await.unwrap() {
self.episodes.push(broadcast);
} else {
return Ok(());
}
}
}
self.only_keep_last_episodes();
Ok(())
}
fn only_keep_last_episodes(&mut self) { fn only_keep_last_episodes(&mut self) {
self.episodes = self.episodes.clone().into_iter().rev().take(10).collect(); self.episodes = self.episodes.clone().into_iter().rev().take(10).collect();
// only keep last 10 // only keep last 10
@@ -138,17 +77,12 @@ impl LiveFeed {
self.episodes.iter().any(|e| e.url == url) self.episodes.iter().any(|e| e.url == url)
} }
async fn get_all_broadcasts(&self) -> Result<Vec<String>, FetchError> { async fn get_all_broadcasts(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// List of broadcasts: https://audioapi.orf.at/oe1/api/json/current/broadcasts // 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 // ^ 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 url = "https://audioapi.orf.at/oe1/api/json/current/broadcasts";
let data: Value = reqwest::get(url) let data: Value = reqwest::get(url).await?.json().await?;
.await
.map_err(FetchError::Fetching)?
.json()
.await
.map_err(FetchError::JsonParsing)?;
let mut ret: Vec<String> = Vec::new(); let mut ret: Vec<String> = Vec::new();
@@ -156,19 +90,13 @@ impl LiveFeed {
for day in days { for day in days {
if let Some(broadcasts) = day["broadcasts"].as_array() { if let Some(broadcasts) = day["broadcasts"].as_array() {
for broadcast in broadcasts { for broadcast in broadcasts {
let Some(title) = broadcast["title"].as_str() else {
warn!("Broadcast has no 'title' attribute, skipping broadcast");
continue;
};
let Some(href) = broadcast["href"].as_str() else {
warn!("Broadcast has no 'href' attribute, skipping broadcast");
continue;
};
if self.filter_titles.is_empty() if self.filter_titles.is_empty()
|| self.filter_titles.contains(&title.into()) || self
.filter_titles
.contains(&broadcast["title"].as_str().unwrap().into())
{ {
{ {
ret.push(href.into()); ret.push(broadcast["href"].as_str().unwrap().into());
} }
} }
} }
@@ -189,18 +117,6 @@ struct Broadcast {
} }
impl Broadcast { impl Broadcast {
#[cfg(test)]
fn test() -> Self {
use chrono::Local;
Self {
url: "test.url".into(),
media_url: "test.media.url".into(),
title: "Test title".into(),
timestamp: Local::now().into(),
}
}
async fn from_url(url: String) -> Result<Option<Self>, Box<dyn std::error::Error>> { async fn from_url(url: String) -> Result<Option<Self>, Box<dyn std::error::Error>> {
let data: Value = reqwest::get(&url).await?.json().await?; let data: Value = reqwest::get(&url).await?.json().await?;
let Some(streams) = data["streams"].as_array() else { let Some(streams) = data["streams"].as_array() else {

View File

@@ -1,4 +1,4 @@
use crate::{Broadcast, LiveFeed}; use crate::{Broadcast, Feed};
pub(super) trait ToRss { pub(super) trait ToRss {
fn title(&self) -> &str; fn title(&self) -> &str;
@@ -39,7 +39,7 @@ pub(super) trait ToRss {
} }
} }
impl ToRss for LiveFeed { impl ToRss for Feed {
fn title(&self) -> &str { fn title(&self) -> &str {
&self.title &self.title
} }

View File

@@ -1,12 +1,10 @@
use crate::{rss::ToRss, Feed, LiveFeed}; use crate::{rss::ToRss, Feed};
use axum::{extract::State, http::HeaderMap, response::IntoResponse, routing::get, Router}; use axum::{extract::State, http::HeaderMap, response::IntoResponse, routing::get, Router};
use reqwest::header; use reqwest::header;
use std::sync::Arc; use std::sync::Arc;
use tokio::{net::TcpListener, sync::RwLock}; use tokio::{net::TcpListener, sync::RwLock};
async fn stream_handler<T: Feed + ToRss + Send>( async fn stream_handler(State(state): State<Arc<RwLock<Feed>>>) -> impl IntoResponse {
State(state): State<Arc<RwLock<T>>>,
) -> impl IntoResponse {
state.write().await.fetch().await.unwrap(); state.write().await.fetch().await.unwrap();
let content = state.read().await.to_rss(); let content = state.read().await.to_rss();
@@ -17,7 +15,7 @@ async fn stream_handler<T: Feed + ToRss + Send>(
} }
pub(super) async fn serve( pub(super) async fn serve(
state: Arc<RwLock<LiveFeed>>, state: Arc<RwLock<Feed>>,
listener: TcpListener, listener: TcpListener,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new() let app = Router::new()
@@ -28,39 +26,3 @@ pub(super) async fn serve(
Ok(()) Ok(())
} }
//#[cfg(test)]
//mod tests {
// use crate::{rss::ToRss, TestFeed};
// use axum::http::StatusCode;
// use std::sync::Arc;
// use tokio::sync::RwLock;
//
// #[tokio::test]
// async fn serve_serves_rss() {
// let feed = Arc::new(RwLock::new(TestFeed::default()));
//
// let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
// let addr = listener.local_addr().unwrap();
//
// tokio::spawn(super::serve(feed.clone(), listener));
//
// let client = reqwest::Client::new();
// let resp = client.get(format!("http://{}", addr)).send().await.unwrap();
//
// assert_eq!(resp.status(), StatusCode::OK);
// assert_eq!(
// resp.headers()
// .get("content-type")
// .unwrap()
// .to_str()
// .unwrap(),
// "application/rss+xml"
// );
//
// let body = resp.text().await.unwrap();
// assert_eq!(body, feed.read().await.to_rss());
//
// assert_eq!(feed.read().await.amount_fetch_calls, 1);
// }
//}