first draft of new version
This commit is contained in:
159
src/lib.rs
159
src/lib.rs
@@ -1,59 +1,142 @@
|
|||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct Feed {
|
||||||
pub struct Episode {
|
episodes: Vec<Broadcast>,
|
||||||
pub url: String,
|
|
||||||
pub date: NaiveDate,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn newest_morning_journal_streaming_url() -> Result<Episode, Box<dyn std::error::Error>> {
|
impl Feed {
|
||||||
let (date, url) = get_newest_morning_journal().await?;
|
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let url = get_streaming_url(url).await?;
|
let mut ret = Self {
|
||||||
|
episodes: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Episode { url, date })
|
ret.fetch().await?;
|
||||||
|
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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#"<?xml version="1.0" encoding="UTF-8"?>"#);
|
||||||
|
ret.push_str(r#"<rss version="2.0">"#);
|
||||||
|
ret.push_str("<channel>");
|
||||||
|
ret.push_str("<title>Ö1 Live Journal Feed</title>");
|
||||||
|
ret.push_str("<link>https://news.hofer.link</link>");
|
||||||
|
ret.push_str("<description>Feed für Ö1 Journale. Live.</description>");
|
||||||
|
|
||||||
|
for episode in &self.episodes {
|
||||||
|
ret.push_str("<item>");
|
||||||
|
ret.push_str(&format!(
|
||||||
|
"<title>{} ({})</title>",
|
||||||
|
episode.title,
|
||||||
|
episode.date.format("%d. %m.")
|
||||||
|
));
|
||||||
|
ret.push_str(&format!(
|
||||||
|
"<enclosure url=\"{}\" length=\"0\" type=\"audio/mpeg\"/>\n",
|
||||||
|
quick_xml::escape::escape(&episode.media_url)
|
||||||
|
));
|
||||||
|
ret.push_str("<description>Journal</description>");
|
||||||
|
ret.push_str("</item>");
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push_str(" </channel>");
|
||||||
|
ret.push_str("</rss>");
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of broadcasts: https://audioapi.orf.at/oe1/api/json/current/broadcasts
|
async fn get_all_broadcasts() -> 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
|
//
|
||||||
async fn get_newest_morning_journal() -> Result<(NaiveDate, String), Box<dyn std::error::Error>> {
|
// ^ 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).await?.json().await?;
|
let data: Value = reqwest::get(url).await?.json().await?;
|
||||||
|
|
||||||
|
let mut ret: Vec<String> = Vec::new();
|
||||||
|
|
||||||
if let Some(days) = data.as_array() {
|
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() {
|
if let Some(broadcasts) = day["broadcasts"].as_array() {
|
||||||
for broadcast in broadcasts.iter().rev() {
|
for broadcast in broadcasts {
|
||||||
if broadcast["title"] == "Einladen, ausladen, boykottieren"
|
// if broadcast["title"] in [];
|
||||||
&& let Some(href) = broadcast["href"].as_str()
|
ret.push(broadcast["href"].to_string());
|
||||||
{
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(String::from("No Ö1 Morgenjournal found").into())
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_streaming_url(url: String) -> Result<String, Box<dyn std::error::Error>> {
|
#[derive(Clone)]
|
||||||
let data: Value = reqwest::get(url).await?.json().await?;
|
pub struct Broadcast {
|
||||||
let Some(streams) = data["streams"].as_array() else {
|
pub url: String,
|
||||||
return Err(String::from("No 'streams' found").into());
|
pub media_url: String,
|
||||||
};
|
pub title: String,
|
||||||
assert_eq!(streams.len(), 1);
|
pub date: NaiveDate,
|
||||||
|
}
|
||||||
let Some(id) = streams[0]["loopStreamId"].as_str() else {
|
|
||||||
return Err(String::from("No 'loopStreamId' found").into());
|
impl Broadcast {
|
||||||
};
|
async fn from(url: String) -> Result<Option<Self>, Box<dyn std::error::Error>> {
|
||||||
|
let data: Value = reqwest::get(&url).await?.json().await?;
|
||||||
Ok(format!(
|
let Some(streams) = data["streams"].as_array() else {
|
||||||
"https://loopstream01.apa.at/?channel=oe1&shoutcast=0&id={id}"
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let state = Arc::new(AppState::new());
|
let state = Arc::new(AppState::new().await.unwrap());
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(streamer::stream_handler))
|
.route("/", get(streamer::stream_handler))
|
||||||
|
42
src/state.rs
42
src/state.rs
@@ -1,45 +1,19 @@
|
|||||||
use chrono::Local;
|
use player::Feed;
|
||||||
use player::Episode;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub episodes: RwLock<Vec<Episode>>,
|
pub feed: RwLock<Feed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new() -> Self {
|
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
Self {
|
Ok(Self {
|
||||||
episodes: RwLock::new(Vec::new()),
|
feed: RwLock::new(Feed::new().await?),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_update(self: Arc<Self>) {
|
pub async fn update(self: Arc<Self>) {
|
||||||
if self.already_downloaded_today().await {
|
self.feed.write().await.fetch().await.unwrap();
|
||||||
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<Self>) -> bool {
|
|
||||||
let today = Local::now().date_naive();
|
|
||||||
self.episodes.read().await.iter().any(|x| x.date == today)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn already_downloaded_url(self: &Arc<Self>, url: &str) -> bool {
|
|
||||||
self.episodes.read().await.iter().any(|x| x.url == url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,44 +1,14 @@
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use axum::{extract::State, http::HeaderMap, response::IntoResponse};
|
use axum::{extract::State, http::HeaderMap, response::IntoResponse};
|
||||||
use player::Episode;
|
|
||||||
use reqwest::header;
|
use reqwest::header;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn stream_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn stream_handler(State(state): State<Arc<AppState>>) -> 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();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(header::CONTENT_TYPE, "application/rss+xml".parse().unwrap());
|
headers.insert(header::CONTENT_TYPE, "application/rss+xml".parse().unwrap());
|
||||||
(headers, content)
|
(headers, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn feed(episodes: &Vec<Episode>) -> String {
|
|
||||||
let mut ret = String::new();
|
|
||||||
ret.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
|
|
||||||
ret.push_str(r#"<rss version="2.0">"#);
|
|
||||||
ret.push_str("<channel>");
|
|
||||||
ret.push_str("<title>Ö1 Morgenjournal Feed</title>");
|
|
||||||
ret.push_str("<link>https://news.hofer.link</link>");
|
|
||||||
ret.push_str("<description>Feed für Ö1 Morgenjournal. Live.</description>");
|
|
||||||
|
|
||||||
for episode in episodes {
|
|
||||||
ret.push_str("<item>");
|
|
||||||
ret.push_str(&format!(
|
|
||||||
"<title>Morgenjournal {}</title>",
|
|
||||||
&episode.date.format("%d. %m.")
|
|
||||||
));
|
|
||||||
ret.push_str(&format!(
|
|
||||||
"<enclosure url=\"{}\" length=\"0\" type=\"audio/mpeg\"/>\n",
|
|
||||||
quick_xml::escape::escape(&episode.url)
|
|
||||||
));
|
|
||||||
ret.push_str("<description>Morgenjournal</description>");
|
|
||||||
ret.push_str("</item>");
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push_str(" </channel>");
|
|
||||||
ret.push_str("</rss>");
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user