podbringer/src/lib.rs

229 lines
7.2 KiB
Rust
Raw Normal View History

#![doc = include_str!("../README.md")]
2022-05-17 11:15:22 +02:00
#![warn(
clippy::all,
missing_copy_implementations,
2022-05-17 11:15:22 +02:00
missing_debug_implementations,
rust_2018_idioms,
rustdoc::broken_intra_doc_links,
trivial_numeric_casts
2022-05-17 11:15:22 +02:00
)]
#![deny(missing_docs)]
2022-05-17 11:15:22 +02:00
use std::path::PathBuf;
2022-05-17 11:15:22 +02:00
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_dyn_templates::{context, Template};
use rss::extension::itunes::{
ITunesCategoryBuilder, ITunesChannelExtensionBuilder, ITunesItemExtensionBuilder,
};
2022-05-17 11:15:22 +02:00
use rss::{
CategoryBuilder, ChannelBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, ItemBuilder,
};
use crate::backends::{mixcloud, Backend};
pub(crate) mod backends;
2022-05-17 11:15:22 +02:00
/// The possible errors that can occur.
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
/// A standard I/O error occurred.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// No redirect URL found in item metadata.
#[error("No redirect URL found")]
NoRedirectUrlFound,
/// A (reqwest) HTTP error occurred.
#[error("HTTP error: {0}")]
Request(#[from] reqwest::Error),
/// Unsupported back-end encountered.
#[error("Unsupported back-end: {0}")]
UnsupportedBackend(String),
/// A URL parse error occurred.
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),
/// An error occurred in youtube-dl.
#[error("Youtube-dl failed: {0}")]
YoutubeDl(#[from] youtube_dl::Error),
}
impl<'r, 'o: 'r> rocket::response::Responder<'r, 'o> for Error {
fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'o> {
eprintln!("💥 Encountered error: {}", self);
match self {
Error::NoRedirectUrlFound => Err(Status::NotFound),
_ => Err(Status::InternalServerError),
}
}
}
/// Result type that defaults to [`Error`] as the default error type.
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
/// The extra application specific configuration.
#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub(crate) struct Config {
/// The URL at which the application is hosted or proxied from.
#[serde(default)]
url: String,
}
2022-05-17 11:15:22 +02:00
/// A Rocket responder wrapper type for RSS feeds.
#[derive(Responder)]
#[response(content_type = "application/xml")]
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> {
match backend {
"mixcloud" => mixcloud::backend()
.redirect_url(&file)
.await
.map(Redirect::to),
_ => Err(Error::UnsupportedBackend(backend.to_string())),
}
}
/// Handler for retrieving the RSS feed of a channel on a certain back-end.
2022-05-27 22:47:36 +02:00
///
/// The limit parameter determines the maximum of items that can be in the feed.
#[get("/feed/<backend>/<channel_id>?<limit>")]
async fn feed(
backend: &str,
channel_id: &str,
limit: Option<usize>,
config: &State<Config>,
) -> Result<RssFeed> {
2022-05-17 11:15:22 +02:00
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 category = CategoryBuilder::default()
.name(
channel
.categories
.first()
.map(Clone::clone)
.unwrap_or_default(),
)
.build();
2022-05-17 11:15:22 +02:00
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
2022-05-17 11:15:22 +02:00
.into_iter()
.map(|item| {
let categories = item
.categories
2022-05-17 11:15:22 +02:00
.into_iter()
.map(|(cat_name, cat_url)| {
2022-05-17 11:15:22 +02:00
CategoryBuilder::default()
.name(cat_name)
.domain(Some(cat_url.to_string()))
2022-05-17 11:15:22 +02:00
.build()
})
.collect::<Vec<_>>();
let url = uri!(
Absolute::parse(&config.url).expect("valid URL"),
download(backend = backend, file = item.enclosure.file)
);
2022-05-17 11:15:22 +02:00
let enclosure = EnclosureBuilder::default()
.url(url.to_string())
.length(item.enclosure.length.to_string())
.mime_type(item.enclosure.mime_type)
2022-05-17 11:15:22 +02:00
.build();
let guid = GuidBuilder::default()
.value(item.guid)
.permalink(false)
.build();
let keywords = item.keywords.join(", ");
2022-05-17 11:15:22 +02:00
let itunes_ext = ITunesItemExtensionBuilder::default()
.image(Some(item.image.to_string()))
.duration(item.duration.map(|dur| format!("{dur}")))
.subtitle(item.description.clone())
2022-05-17 11:15:22 +02:00
.keywords(Some(keywords))
.build();
if item.updated_at > last_build {
last_build = item.updated_at;
2022-05-17 11:15:22 +02:00
}
ItemBuilder::default()
.title(Some(item.title))
.link(Some(item.link.to_string()))
.description(item.description)
2022-05-17 11:15:22 +02:00
.categories(categories)
.enclosure(Some(enclosure))
.guid(Some(guid))
.pub_date(Some(item.updated_at.to_rfc2822()))
2022-05-17 11:15:22 +02:00
.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()))
2022-05-17 11:15:22 +02:00
.build();
let channel = ChannelBuilder::default()
.title(channel.title)
.link(channel.link)
.description(channel.description)
.category(category)
2022-05-17 11:15:22 +02:00
.last_build_date(Some(last_build.to_rfc2822()))
.generator(Some(generator))
.image(Some(image))
.items(items)
.itunes_ext(Some(itunes_ext))
2022-05-17 11:15:22 +02:00
.build();
let feed = RssFeed(channel.to_string());
Ok(feed)
2022-05-17 11:15:22 +02:00
}
/// Returns a simple index page that explains the usage.
#[get("/")]
pub(crate) async fn index(config: &State<Config>) -> Template {
Template::render("index", context! { url: &config.url })
}
2022-05-17 11:15:22 +02:00
/// Sets up Rocket.
pub fn setup() -> Rocket<Build> {
rocket::build()
.mount("/", routes![download, feed, index])
.attach(AdHoc::config::<Config>())
.attach(Template::fairing())
2022-05-17 11:15:22 +02:00
}