Compare commits
2 Commits
4702017914
...
e1e21f8837
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1e21f8837 | ||
|
|
5ee6a679c5 |
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -724,9 +724,7 @@ dependencies = [
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1292,21 +1290,9 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"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]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
|
||||
@@ -10,5 +10,3 @@ reqwest = { version = "0.12", features = ["stream", "json", "rustls-tls"], defau
|
||||
serde_json = "1"
|
||||
chrono = "0.4"
|
||||
quick-xml = "0.38"
|
||||
thiserror = "2"
|
||||
tracing = "0.1"
|
||||
|
||||
138
src/lib.rs
138
src/lib.rs
@@ -4,9 +4,7 @@ mod web;
|
||||
use chrono::DateTime;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::{net::TcpListener, sync::RwLock};
|
||||
use tracing::warn;
|
||||
|
||||
pub async fn start(
|
||||
title: String,
|
||||
@@ -16,9 +14,7 @@ pub async fn start(
|
||||
listener: TcpListener,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let state = Arc::new(RwLock::new(
|
||||
LiveFeed::new(title, link, desc, filter_titles)
|
||||
.await
|
||||
.unwrap(),
|
||||
Feed::new(title, link, desc, filter_titles).await.unwrap(),
|
||||
));
|
||||
|
||||
web::serve(state, listener).await?;
|
||||
@@ -26,62 +22,7 @@ pub async fn start(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
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 {
|
||||
struct Feed {
|
||||
episodes: Vec<Broadcast>,
|
||||
title: String,
|
||||
link: String,
|
||||
@@ -89,27 +30,7 @@ struct LiveFeed {
|
||||
filter_titles: Vec<String>,
|
||||
}
|
||||
|
||||
impl Feed for LiveFeed {
|
||||
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 {
|
||||
impl Feed {
|
||||
async fn new(
|
||||
title: String,
|
||||
link: String,
|
||||
@@ -129,6 +50,24 @@ impl LiveFeed {
|
||||
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) {
|
||||
self.episodes = self.episodes.clone().into_iter().rev().take(10).collect();
|
||||
// only keep last 10
|
||||
@@ -138,17 +77,12 @@ impl LiveFeed {
|
||||
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
|
||||
//
|
||||
// ^ 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
|
||||
.map_err(FetchError::Fetching)?
|
||||
.json()
|
||||
.await
|
||||
.map_err(FetchError::JsonParsing)?;
|
||||
let data: Value = reqwest::get(url).await?.json().await?;
|
||||
|
||||
let mut ret: Vec<String> = Vec::new();
|
||||
|
||||
@@ -156,19 +90,13 @@ impl LiveFeed {
|
||||
for day in days {
|
||||
if let Some(broadcasts) = day["broadcasts"].as_array() {
|
||||
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()
|
||||
|| 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 {
|
||||
#[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>> {
|
||||
let data: Value = reqwest::get(&url).await?.json().await?;
|
||||
let Some(streams) = data["streams"].as_array() else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Broadcast, LiveFeed};
|
||||
use crate::{Broadcast, Feed};
|
||||
|
||||
pub(super) trait ToRss {
|
||||
fn title(&self) -> &str;
|
||||
@@ -39,7 +39,7 @@ pub(super) trait ToRss {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRss for LiveFeed {
|
||||
impl ToRss for Feed {
|
||||
fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
44
src/web.rs
44
src/web.rs
@@ -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 reqwest::header;
|
||||
use std::sync::Arc;
|
||||
use tokio::{net::TcpListener, sync::RwLock};
|
||||
|
||||
async fn stream_handler<T: Feed + ToRss + Send>(
|
||||
State(state): State<Arc<RwLock<T>>>,
|
||||
) -> impl IntoResponse {
|
||||
async fn stream_handler(State(state): State<Arc<RwLock<Feed>>>) -> impl IntoResponse {
|
||||
state.write().await.fetch().await.unwrap();
|
||||
|
||||
let content = state.read().await.to_rss();
|
||||
@@ -17,7 +15,7 @@ async fn stream_handler<T: Feed + ToRss + Send>(
|
||||
}
|
||||
|
||||
pub(super) async fn serve(
|
||||
state: Arc<RwLock<LiveFeed>>,
|
||||
state: Arc<RwLock<Feed>>,
|
||||
listener: TcpListener,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app = Router::new()
|
||||
@@ -28,39 +26,3 @@ pub(super) async fn serve(
|
||||
|
||||
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);
|
||||
// }
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user