Split off feed generation to feed module
Also rename the handler function names so they don't conflict with (current and future) modules.
This commit is contained in:
parent
bc9a9e307d
commit
cb40f6b192
|
@ -0,0 +1,129 @@
|
|||
//! Helper functions for constructing RSS feeds.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use rocket::http::uri::Absolute;
|
||||
use rocket::uri;
|
||||
use rss::extension::itunes::{
|
||||
ITunesCategoryBuilder, ITunesChannelExtensionBuilder, ITunesItemExtensionBuilder,
|
||||
};
|
||||
use rss::{
|
||||
CategoryBuilder, ChannelBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, ItemBuilder,
|
||||
};
|
||||
|
||||
use crate::backends::{Channel, Item};
|
||||
use crate::Config;
|
||||
|
||||
/// Constructs a feed as string from a back-end channel using the `rss` crate.
|
||||
///
|
||||
/// It requires the backend and configuration to be able to construct download URLs.
|
||||
pub(crate) fn construct(backend: &str, config: &Config, channel: Channel) -> rss::Channel {
|
||||
let category = CategoryBuilder::default()
|
||||
.name(
|
||||
channel
|
||||
.categories
|
||||
.first()
|
||||
.map(Clone::clone)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build();
|
||||
let mut last_build = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc);
|
||||
let generator = String::from(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
let image = ImageBuilder::default()
|
||||
.link(channel.image.clone())
|
||||
.url(channel.image.clone())
|
||||
.build();
|
||||
let items = channel
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| construct_item(backend, config, item, &mut last_build))
|
||||
.collect::<Vec<_>>();
|
||||
let itunes_ext = ITunesChannelExtensionBuilder::default()
|
||||
.author(channel.author)
|
||||
.categories(
|
||||
channel
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|cat| ITunesCategoryBuilder::default().text(cat).build())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.image(Some(channel.image.to_string()))
|
||||
.explicit(Some(String::from("no")))
|
||||
.summary(Some(channel.description.clone()))
|
||||
.build();
|
||||
|
||||
ChannelBuilder::default()
|
||||
.title(channel.title)
|
||||
.link(channel.link)
|
||||
.description(channel.description)
|
||||
.category(category)
|
||||
.last_build_date(Some(last_build.to_rfc2822()))
|
||||
.generator(Some(generator))
|
||||
.image(Some(image))
|
||||
.items(items)
|
||||
.itunes_ext(Some(itunes_ext))
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Constructs an RSS feed item from a back-end item using the `rss` crate.
|
||||
///
|
||||
/// It requires the backend and configuration to be able to construct download URLs.
|
||||
/// It also bumps the last build timestamp if the last updated timestamp is later than the current
|
||||
/// value.
|
||||
fn construct_item(
|
||||
backend: &str,
|
||||
config: &Config,
|
||||
item: Item,
|
||||
last_build: &mut DateTime<Utc>,
|
||||
) -> rss::Item {
|
||||
let categories = item
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|(cat_name, cat_url)| {
|
||||
CategoryBuilder::default()
|
||||
.name(cat_name)
|
||||
.domain(Some(cat_url.to_string()))
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let url = uri!(
|
||||
Absolute::parse(&config.url).expect("valid URL"),
|
||||
crate::get_download(backend = backend, file = item.enclosure.file)
|
||||
);
|
||||
let enclosure = EnclosureBuilder::default()
|
||||
.url(url.to_string())
|
||||
.length(item.enclosure.length.to_string())
|
||||
.mime_type(item.enclosure.mime_type)
|
||||
.build();
|
||||
let guid = GuidBuilder::default()
|
||||
.value(item.guid)
|
||||
.permalink(false)
|
||||
.build();
|
||||
let keywords = item.keywords.join(", ");
|
||||
let itunes_ext = ITunesItemExtensionBuilder::default()
|
||||
.image(Some(item.image.to_string()))
|
||||
.duration(item.duration.map(|dur| format!("{dur}")))
|
||||
.subtitle(item.description.clone())
|
||||
.keywords(Some(keywords))
|
||||
.build();
|
||||
|
||||
if item.updated_at > *last_build {
|
||||
*last_build = item.updated_at;
|
||||
}
|
||||
|
||||
ItemBuilder::default()
|
||||
.title(Some(item.title))
|
||||
.link(Some(item.link.to_string()))
|
||||
.description(item.description)
|
||||
.categories(categories)
|
||||
.enclosure(Some(enclosure))
|
||||
.guid(Some(guid))
|
||||
.pub_date(Some(item.updated_at.to_rfc2822()))
|
||||
.itunes_ext(Some(itunes_ext))
|
||||
.build()
|
||||
}
|
119
src/lib.rs
119
src/lib.rs
|
@ -11,24 +11,17 @@
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::http::uri::Absolute;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::{get, routes, uri, Build, Request, Responder, Rocket, State};
|
||||
use rocket::{get, routes, Build, Request, Responder, Rocket, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rss::extension::itunes::{
|
||||
ITunesCategoryBuilder, ITunesChannelExtensionBuilder, ITunesItemExtensionBuilder,
|
||||
};
|
||||
use rss::{
|
||||
CategoryBuilder, ChannelBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, ItemBuilder,
|
||||
};
|
||||
|
||||
use crate::backends::{mixcloud, Backend};
|
||||
|
||||
pub(crate) mod backends;
|
||||
pub(crate) mod feed;
|
||||
|
||||
/// The possible errors that can occur.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -88,7 +81,7 @@ struct RssFeed(String);
|
|||
|
||||
/// Retrieves a download by redirecting to the URL resolved by the selected back-end.
|
||||
#[get("/download/<backend>/<file..>")]
|
||||
pub(crate) async fn download(file: PathBuf, backend: &str) -> Result<Redirect> {
|
||||
pub(crate) async fn get_download(file: PathBuf, backend: &str) -> Result<Redirect> {
|
||||
match backend {
|
||||
"mixcloud" => mixcloud::backend()
|
||||
.redirect_url(&file)
|
||||
|
@ -102,127 +95,31 @@ pub(crate) async fn download(file: PathBuf, backend: &str) -> Result<Redirect> {
|
|||
///
|
||||
/// The limit parameter determines the maximum of items that can be in the feed.
|
||||
#[get("/feed/<backend>/<channel_id>?<limit>")]
|
||||
async fn feed(
|
||||
async fn get_feed(
|
||||
backend: &str,
|
||||
channel_id: &str,
|
||||
limit: Option<usize>,
|
||||
config: &State<Config>,
|
||||
) -> Result<RssFeed> {
|
||||
let mut last_build = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc);
|
||||
let channel = match backend {
|
||||
"mixcloud" => mixcloud::backend().channel(channel_id, limit).await?,
|
||||
_ => return Err(Error::UnsupportedBackend(backend.to_string())),
|
||||
};
|
||||
let feed = feed::construct(backend, config, channel);
|
||||
|
||||
let category = CategoryBuilder::default()
|
||||
.name(
|
||||
channel
|
||||
.categories
|
||||
.first()
|
||||
.map(Clone::clone)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build();
|
||||
let generator = String::from(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
let image = ImageBuilder::default()
|
||||
.link(channel.image.clone())
|
||||
.url(channel.image.clone())
|
||||
.build();
|
||||
let items = channel
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let categories = item
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|(cat_name, cat_url)| {
|
||||
CategoryBuilder::default()
|
||||
.name(cat_name)
|
||||
.domain(Some(cat_url.to_string()))
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let url = uri!(
|
||||
Absolute::parse(&config.url).expect("valid URL"),
|
||||
download(backend = backend, file = item.enclosure.file)
|
||||
);
|
||||
let enclosure = EnclosureBuilder::default()
|
||||
.url(url.to_string())
|
||||
.length(item.enclosure.length.to_string())
|
||||
.mime_type(item.enclosure.mime_type)
|
||||
.build();
|
||||
let guid = GuidBuilder::default()
|
||||
.value(item.guid)
|
||||
.permalink(false)
|
||||
.build();
|
||||
let keywords = item.keywords.join(", ");
|
||||
let itunes_ext = ITunesItemExtensionBuilder::default()
|
||||
.image(Some(item.image.to_string()))
|
||||
.duration(item.duration.map(|dur| format!("{dur}")))
|
||||
.subtitle(item.description.clone())
|
||||
.keywords(Some(keywords))
|
||||
.build();
|
||||
|
||||
if item.updated_at > last_build {
|
||||
last_build = item.updated_at;
|
||||
}
|
||||
|
||||
ItemBuilder::default()
|
||||
.title(Some(item.title))
|
||||
.link(Some(item.link.to_string()))
|
||||
.description(item.description)
|
||||
.categories(categories)
|
||||
.enclosure(Some(enclosure))
|
||||
.guid(Some(guid))
|
||||
.pub_date(Some(item.updated_at.to_rfc2822()))
|
||||
.itunes_ext(Some(itunes_ext))
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let itunes_ext = ITunesChannelExtensionBuilder::default()
|
||||
.author(channel.author)
|
||||
.categories(
|
||||
channel
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|cat| ITunesCategoryBuilder::default().text(cat).build())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.image(Some(channel.image.to_string()))
|
||||
.explicit(Some(String::from("no")))
|
||||
.summary(Some(channel.description.clone()))
|
||||
.build();
|
||||
|
||||
let channel = ChannelBuilder::default()
|
||||
.title(channel.title)
|
||||
.link(channel.link)
|
||||
.description(channel.description)
|
||||
.category(category)
|
||||
.last_build_date(Some(last_build.to_rfc2822()))
|
||||
.generator(Some(generator))
|
||||
.image(Some(image))
|
||||
.items(items)
|
||||
.itunes_ext(Some(itunes_ext))
|
||||
.build();
|
||||
let feed = RssFeed(channel.to_string());
|
||||
|
||||
Ok(feed)
|
||||
Ok(RssFeed(feed.to_string()))
|
||||
}
|
||||
|
||||
/// Returns a simple index page that explains the usage.
|
||||
#[get("/")]
|
||||
pub(crate) async fn index(config: &State<Config>) -> Template {
|
||||
pub(crate) async fn get_index(config: &State<Config>) -> Template {
|
||||
Template::render("index", context! { url: &config.url })
|
||||
}
|
||||
|
||||
/// Sets up Rocket.
|
||||
pub fn setup() -> Rocket<Build> {
|
||||
rocket::build()
|
||||
.mount("/", routes![download, feed, index])
|
||||
.mount("/", routes![get_download, get_feed, get_index])
|
||||
.attach(AdHoc::config::<Config>())
|
||||
.attach(Template::fairing())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue