From cb40f6b192f4f09d0d4e2dd51b3ea1439af85154 Mon Sep 17 00:00:00 2001 From: Paul van Tilburg Date: Sun, 14 Aug 2022 10:15:59 +0200 Subject: [PATCH] Split off feed generation to feed module Also rename the handler function names so they don't conflict with (current and future) modules. --- src/feed.rs | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 119 ++++-------------------------------------------- 2 files changed, 137 insertions(+), 111 deletions(-) create mode 100644 src/feed.rs diff --git a/src/feed.rs b/src/feed.rs new file mode 100644 index 0000000..35a9019 --- /dev/null +++ b/src/feed.rs @@ -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::::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::>(); + let itunes_ext = ITunesChannelExtensionBuilder::default() + .author(channel.author) + .categories( + channel + .categories + .into_iter() + .map(|cat| ITunesCategoryBuilder::default().text(cat).build()) + .collect::>(), + ) + .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, +) -> 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::>(); + 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() +} diff --git a/src/lib.rs b/src/lib.rs index d1209d0..012ee7a 100644 --- a/src/lib.rs +++ b/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//")] -pub(crate) async fn download(file: PathBuf, backend: &str) -> Result { +pub(crate) async fn get_download(file: PathBuf, backend: &str) -> Result { match backend { "mixcloud" => mixcloud::backend() .redirect_url(&file) @@ -102,127 +95,31 @@ pub(crate) async fn download(file: PathBuf, backend: &str) -> Result { /// /// The limit parameter determines the maximum of items that can be in the feed. #[get("/feed//?")] -async fn feed( +async fn get_feed( backend: &str, channel_id: &str, limit: Option, config: &State, ) -> Result { - let mut last_build = DateTime::::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::>(); - 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::>(); - let itunes_ext = ITunesChannelExtensionBuilder::default() - .author(channel.author) - .categories( - channel - .categories - .into_iter() - .map(|cat| ITunesCategoryBuilder::default().text(cat).build()) - .collect::>(), - ) - .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) -> Template { +pub(crate) async fn get_index(config: &State) -> Template { Template::render("index", context! { url: &config.url }) } /// Sets up Rocket. pub fn setup() -> Rocket { rocket::build() - .mount("/", routes![download, feed, index]) + .mount("/", routes![get_download, get_feed, get_index]) .attach(AdHoc::config::()) .attach(Template::fairing()) }