split into multiple files
This commit is contained in:
130
src/lib.rs
130
src/lib.rs
@@ -1,14 +1,48 @@
|
|||||||
|
mod rss;
|
||||||
|
mod web;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use serde_json::Value;
|
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<String>,
|
||||||
|
listener: TcpListener,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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<Broadcast>,
|
episodes: Vec<Broadcast>,
|
||||||
|
title: String,
|
||||||
|
link: String,
|
||||||
|
desc: String,
|
||||||
|
filter_titles: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Feed {
|
impl Feed {
|
||||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
async fn new(
|
||||||
|
title: String,
|
||||||
|
link: String,
|
||||||
|
desc: String,
|
||||||
|
filter_titles: Vec<String>,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let mut ret = Self {
|
let mut ret = Self {
|
||||||
episodes: Vec::new(),
|
episodes: Vec::new(),
|
||||||
|
title,
|
||||||
|
link,
|
||||||
|
desc,
|
||||||
|
filter_titles,
|
||||||
};
|
};
|
||||||
|
|
||||||
ret.fetch().await?;
|
ret.fetch().await?;
|
||||||
@@ -16,12 +50,12 @@ impl Feed {
|
|||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
async fn fetch(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let broadcasts = get_all_broadcasts().await?;
|
let broadcasts = self.get_all_broadcasts().await?;
|
||||||
|
|
||||||
for broadcast in broadcasts {
|
for broadcast in broadcasts {
|
||||||
if !self.has_broadcast_url(&broadcast) {
|
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);
|
self.episodes.push(broadcast);
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -43,77 +77,47 @@ impl Feed {
|
|||||||
self.episodes.iter().any(|e| e.url == url)
|
self.episodes.iter().any(|e| e.url == url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_rss(&self) -> String {
|
async fn get_all_broadcasts(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||||
let mut ret = String::new();
|
// List of broadcasts: https://audioapi.orf.at/oe1/api/json/current/broadcasts
|
||||||
ret.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
|
//
|
||||||
ret.push_str(r#"<rss version="2.0">"#);
|
// ^ contains link, e.g. https://audioapi.orf.at/oe1/api/json/4.0/broadcast/797577/20250611
|
||||||
ret.push_str("<channel>");
|
let url = "https://audioapi.orf.at/oe1/api/json/current/broadcasts";
|
||||||
ret.push_str("<title>Ö1 Live Journal Feed</title>");
|
let data: Value = reqwest::get(url).await?.json().await?;
|
||||||
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 {
|
let mut ret: Vec<String> = Vec::new();
|
||||||
ret.push_str("<item>");
|
|
||||||
ret.push_str(&format!(
|
|
||||||
"<title>{} ({})</title>",
|
|
||||||
episode.title,
|
|
||||||
episode.timestamp.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(&format!(
|
|
||||||
"<pubDate>{}</pubDate>",
|
|
||||||
episode.timestamp.to_rfc2822()
|
|
||||||
));
|
|
||||||
ret.push_str("</item>");
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push_str(" </channel>");
|
if let Some(days) = data.as_array() {
|
||||||
ret.push_str("</rss>");
|
for day in days {
|
||||||
|
if let Some(broadcasts) = day["broadcasts"].as_array() {
|
||||||
ret
|
for broadcast in broadcasts {
|
||||||
}
|
if self.filter_titles.is_empty()
|
||||||
}
|
|| self
|
||||||
|
.filter_titles
|
||||||
async fn get_all_broadcasts() -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
.contains(&broadcast["title"].as_str().unwrap().into())
|
||||||
// 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
|
ret.push(broadcast["href"].as_str().unwrap().into());
|
||||||
let url = "https://audioapi.orf.at/oe1/api/json/current/broadcasts";
|
}
|
||||||
let data: Value = reqwest::get(url).await?.json().await?;
|
}
|
||||||
|
|
||||||
let mut ret: Vec<String> = 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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Broadcast {
|
struct Broadcast {
|
||||||
pub url: String,
|
url: String,
|
||||||
pub media_url: String,
|
media_url: String,
|
||||||
pub title: String,
|
title: String,
|
||||||
pub timestamp: DateTime<chrono::Utc>,
|
timestamp: DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Broadcast {
|
impl Broadcast {
|
||||||
async fn from(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 {
|
||||||
return Err(String::from("No 'streams' found").into());
|
return Err(String::from("No 'streams' found").into());
|
||||||
|
|||||||
38
src/main.rs
38
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<Arc<RwLock<Feed>>>) -> 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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
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?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/rss.rs
Normal file
58
src/rss.rs
Normal file
@@ -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<Broadcast>;
|
||||||
|
|
||||||
|
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(&format!("<title>{}</title>", self.title()));
|
||||||
|
ret.push_str(&format!("<link>{}</link>", self.link()));
|
||||||
|
ret.push_str(&format!("<description>{}</description>", self.desc()));
|
||||||
|
|
||||||
|
for episode in self.episodes() {
|
||||||
|
let desc = format!("{} ({})", episode.title, episode.timestamp.format("%d.%m."));
|
||||||
|
|
||||||
|
ret.push_str("<item>");
|
||||||
|
ret.push_str(&format!("<title>{desc}</title>",));
|
||||||
|
ret.push_str(&format!(
|
||||||
|
"<enclosure url=\"{}\" length=\"0\" type=\"audio/mpeg\"/>\n",
|
||||||
|
quick_xml::escape::escape(&episode.media_url)
|
||||||
|
));
|
||||||
|
ret.push_str(&format!("<description>{desc}</description>"));
|
||||||
|
ret.push_str(&format!(
|
||||||
|
"<pubDate>{}</pubDate>",
|
||||||
|
episode.timestamp.to_rfc2822()
|
||||||
|
));
|
||||||
|
ret.push_str("</item>");
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push_str(" </channel>");
|
||||||
|
ret.push_str("</rss>");
|
||||||
|
|
||||||
|
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<Broadcast> {
|
||||||
|
&self.episodes
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/web.rs
Normal file
28
src/web.rs
Normal file
@@ -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<Arc<RwLock<Feed>>>) -> 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<RwLock<Feed>>,
|
||||||
|
listener: TcpListener,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(stream_handler))
|
||||||
|
.with_state(state.clone());
|
||||||
|
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user