restructure + first tests
Some checks failed
CI/CD Pipeline / test (push) Failing after 3m23s
CI/CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
Philipp Hofer
2025-10-29 11:30:16 +01:00
parent e1e21f8837
commit 1cab4d0bf5
9 changed files with 212 additions and 90 deletions

73
src/fetch/broadcast.rs Normal file
View File

@@ -0,0 +1,73 @@
use crate::Backend;
use chrono::DateTime;
use serde_json::Value;
impl Backend {
pub(crate) async fn get_broadcast(
&self,
url: String,
) -> Result<Option<Broadcast>, Box<dyn std::error::Error>> {
let data: Value = match self {
Backend::Prod => reqwest::get(&url).await?.json().await?,
#[cfg(test)]
Backend::Test => todo!(),
};
Broadcast::from_data(url, data).await
}
}
#[derive(Clone)]
pub(crate) struct Broadcast {
pub(crate) url: String,
pub(crate) media_url: String,
pub(crate) title: String,
pub(crate) timestamp: DateTime<chrono::Utc>,
}
impl Broadcast {
async fn from_data(
url: String,
data: Value,
) -> Result<Option<Self>, Box<dyn std::error::Error>> {
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
}
}

2
src/fetch/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub(super) mod broadcast;
pub(super) mod overview;

95
src/fetch/overview.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::Backend;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct InferenceResponse {
pub title: String,
pub href: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct InferenceDay {
pub broadcasts: Vec<InferenceResponse>,
}
type InferenceData = Vec<InferenceDay>;
impl Backend {
/// Gets a list of all broadcasts
pub(crate) async fn get_broadcasts(
&self,
filter_titles: &[String],
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let data: InferenceData = match self {
Backend::Prod => {
let url = "https://audioapi.orf.at/oe1/api/json/current/broadcasts";
reqwest::get(url).await?.json::<InferenceData>().await?
}
#[cfg(test)]
Backend::Test => {
vec![InferenceDay {
broadcasts: vec![InferenceResponse {
title: "test-title".into(),
href: "test-href".into(),
}],
}]
}
};
get_broadcasts(data, filter_titles).await
}
}
/// Returns a list of urls of all filtered broadcasts.
async fn get_broadcasts(
days: InferenceData,
filter_titles: &[String],
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut ret: Vec<String> = Vec::new();
for day in days {
for broadcast in day.broadcasts {
if filter_titles.is_empty() || filter_titles.contains(&broadcast.title) {
{
ret.push(broadcast.href);
}
}
}
}
Ok(ret)
}
#[cfg(test)]
mod tests {
use crate::Backend;
#[tokio::test]
async fn happy() {
let backend = Backend::Test;
let actual = backend.get_broadcasts(&[]).await.unwrap();
assert_eq!(actual, vec!["test-href"]);
}
#[ignore]
#[tokio::test]
async fn happy_prod() {
let backend = Backend::Prod;
let actual = backend.get_broadcasts(&[]).await.unwrap();
assert!(actual.len() > 200);
assert!(actual[0].starts_with("https://audioapi.orf.at/oe1/api/json/4.0/broadcast/"))
}
#[tokio::test]
async fn filter_no_result() {
let backend = Backend::Test;
let actual = backend.get_broadcasts(&["no-match".into()]).await.unwrap();
assert!(actual.is_empty());
}
#[tokio::test]
async fn filter_result() {
let backend = Backend::Test;
let actual = backend.get_broadcasts(&["test-href".into()]).await.unwrap();
assert_eq!(actual, vec!["test-href"]);
}
}