2022-05-20 16:18:13 +02:00
|
|
|
#![doc = include_str!("../README.md")]
|
2022-05-17 11:15:22 +02:00
|
|
|
#![warn(
|
|
|
|
clippy::all,
|
2022-05-26 20:03:44 +02:00
|
|
|
missing_copy_implementations,
|
2022-05-17 11:15:22 +02:00
|
|
|
missing_debug_implementations,
|
|
|
|
rust_2018_idioms,
|
2022-05-26 20:03:44 +02:00
|
|
|
rustdoc::broken_intra_doc_links,
|
|
|
|
trivial_numeric_casts
|
2022-05-17 11:15:22 +02:00
|
|
|
)]
|
2022-05-20 16:18:13 +02:00
|
|
|
#![deny(missing_docs)]
|
2022-05-17 11:15:22 +02:00
|
|
|
|
2022-05-24 09:58:57 +02:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
2022-05-17 11:15:22 +02:00
|
|
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
2022-05-20 16:15:33 +02:00
|
|
|
use rocket::fairing::AdHoc;
|
2022-05-24 09:58:57 +02:00
|
|
|
use rocket::http::uri::Absolute;
|
2022-05-26 19:57:16 +02:00
|
|
|
use rocket::http::Status;
|
2022-05-23 22:21:38 +02:00
|
|
|
use rocket::response::Redirect;
|
2022-05-20 16:15:33 +02:00
|
|
|
use rocket::serde::{Deserialize, Serialize};
|
2022-05-26 19:57:16 +02:00
|
|
|
use rocket::{get, routes, uri, Build, Request, Responder, Rocket, State};
|
2022-05-24 11:04:27 +02:00
|
|
|
use rocket_dyn_templates::{context, Template};
|
2022-05-24 10:35:10 +02:00
|
|
|
use rss::extension::itunes::{
|
|
|
|
ITunesCategoryBuilder, ITunesChannelExtensionBuilder, ITunesItemExtensionBuilder,
|
|
|
|
};
|
2022-05-17 11:15:22 +02:00
|
|
|
use rss::{
|
|
|
|
CategoryBuilder, ChannelBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, ItemBuilder,
|
|
|
|
};
|
|
|
|
|
2022-08-13 15:14:15 +02:00
|
|
|
use crate::backends::{mixcloud, Backend};
|
|
|
|
|
|
|
|
pub(crate) mod backends;
|
2022-05-17 11:15:22 +02:00
|
|
|
|
2022-05-26 19:57:16 +02:00
|
|
|
/// The possible errors that can occur.
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
pub(crate) enum Error {
|
2022-06-05 21:58:02 +02:00
|
|
|
/// A standard I/O error occurred.
|
2022-05-26 19:57:16 +02:00
|
|
|
#[error("IO error: {0}")]
|
|
|
|
Io(#[from] std::io::Error),
|
|
|
|
|
2022-06-05 21:58:02 +02:00
|
|
|
/// No redirect URL found in item metadata.
|
2022-05-26 19:57:16 +02:00
|
|
|
#[error("No redirect URL found")]
|
|
|
|
NoRedirectUrlFound,
|
|
|
|
|
2022-06-05 21:58:02 +02:00
|
|
|
/// A (reqwest) HTTP error occurred.
|
2022-05-26 19:57:16 +02:00
|
|
|
#[error("HTTP error: {0}")]
|
|
|
|
Request(#[from] reqwest::Error),
|
|
|
|
|
2022-06-05 21:58:02 +02:00
|
|
|
/// Unsupported back-end encountered.
|
|
|
|
#[error("Unsupported back-end: {0}")]
|
2022-05-26 19:57:16 +02:00
|
|
|
UnsupportedBackend(String),
|
2022-05-26 20:37:27 +02:00
|
|
|
|
2022-06-05 21:58:02 +02:00
|
|
|
/// A URL parse error occurred.
|
2022-05-26 22:06:22 +02:00
|
|
|
#[error("URL parse error: {0}")]
|
|
|
|
UrlParse(#[from] url::ParseError),
|
|
|
|
|
2022-06-05 21:58:02 +02:00
|
|
|
/// An error occurred in youtube-dl.
|
|
|
|
#[error("Youtube-dl failed: {0}")]
|
2022-05-26 20:37:27 +02:00
|
|
|
YoutubeDl(#[from] youtube_dl::Error),
|
2022-05-26 19:57:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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>;
|
|
|
|
|
2022-05-20 16:15:33 +02:00
|
|
|
/// The extra application specific configuration.
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
#[serde(crate = "rocket::serde")]
|
|
|
|
pub(crate) struct Config {
|
2022-05-24 09:58:57 +02:00
|
|
|
/// The URL at which the application is hosted or proxied from.
|
2022-05-20 16:15:33 +02:00
|
|
|
#[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);
|
|
|
|
|
2022-05-26 19:57:16 +02:00
|
|
|
/// Retrieves a download by redirecting to the URL resolved by the selected back-end.
|
2022-05-24 09:58:57 +02:00
|
|
|
#[get("/download/<backend>/<file..>")]
|
2022-05-26 19:57:16 +02:00
|
|
|
pub(crate) async fn download(file: PathBuf, backend: &str) -> Result<Redirect> {
|
2022-05-24 09:58:57 +02:00
|
|
|
match backend {
|
2022-08-13 15:14:15 +02:00
|
|
|
"mixcloud" => mixcloud::backend()
|
|
|
|
.redirect_url(&file)
|
|
|
|
.await
|
|
|
|
.map(Redirect::to),
|
2022-05-26 19:57:16 +02:00
|
|
|
_ => Err(Error::UnsupportedBackend(backend.to_string())),
|
2022-05-24 09:58:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-13 15:14:15 +02:00
|
|
|
/// 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.
|
2022-08-13 15:14:15 +02:00
|
|
|
#[get("/feed/<backend>/<channel_id>?<limit>")]
|
2022-05-27 22:31:17 +02:00
|
|
|
async fn feed(
|
|
|
|
backend: &str,
|
2022-08-13 15:14:15 +02:00
|
|
|
channel_id: &str,
|
2022-05-27 22:31:17 +02:00
|
|
|
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);
|
2022-08-13 15:14:15 +02:00
|
|
|
let channel = match backend {
|
|
|
|
"mixcloud" => mixcloud::backend().channel(channel_id, limit).await?,
|
|
|
|
_ => return Err(Error::UnsupportedBackend(backend.to_string())),
|
|
|
|
};
|
2022-05-24 10:35:10 +02:00
|
|
|
|
|
|
|
let category = CategoryBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.name(
|
|
|
|
channel
|
|
|
|
.categories
|
|
|
|
.first()
|
|
|
|
.map(Clone::clone)
|
|
|
|
.unwrap_or_default(),
|
|
|
|
)
|
2022-05-24 10:35:10 +02:00
|
|
|
.build();
|
2022-05-17 11:15:22 +02:00
|
|
|
let generator = String::from(concat!(
|
|
|
|
env!("CARGO_PKG_NAME"),
|
|
|
|
" ",
|
|
|
|
env!("CARGO_PKG_VERSION")
|
|
|
|
));
|
2022-05-24 10:35:10 +02:00
|
|
|
let image = ImageBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.link(channel.image.clone())
|
|
|
|
.url(channel.image.clone())
|
2022-05-24 10:35:10 +02:00
|
|
|
.build();
|
2022-08-13 15:14:15 +02:00
|
|
|
let items = channel
|
|
|
|
.items
|
2022-05-17 11:15:22 +02:00
|
|
|
.into_iter()
|
2022-08-13 15:14:15 +02:00
|
|
|
.map(|item| {
|
|
|
|
let categories = item
|
|
|
|
.categories
|
2022-05-17 11:15:22 +02:00
|
|
|
.into_iter()
|
2022-08-13 15:14:15 +02:00
|
|
|
.map(|(cat_name, cat_url)| {
|
2022-05-17 11:15:22 +02:00
|
|
|
CategoryBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.name(cat_name)
|
|
|
|
.domain(Some(cat_url.to_string()))
|
2022-05-17 11:15:22 +02:00
|
|
|
.build()
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
2022-08-13 15:14:15 +02:00
|
|
|
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()
|
2022-05-24 09:58:57 +02:00
|
|
|
.url(url.to_string())
|
2022-08-13 15:14:15 +02:00
|
|
|
.length(item.enclosure.length.to_string())
|
|
|
|
.mime_type(item.enclosure.mime_type)
|
2022-05-17 11:15:22 +02:00
|
|
|
.build();
|
2022-05-24 10:35:10 +02:00
|
|
|
let guid = GuidBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.value(item.guid)
|
2022-05-24 10:35:10 +02:00
|
|
|
.permalink(false)
|
|
|
|
.build();
|
2022-08-13 15:14:15 +02:00
|
|
|
let keywords = item.keywords.join(", ");
|
2022-05-17 11:15:22 +02:00
|
|
|
let itunes_ext = ITunesItemExtensionBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.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();
|
|
|
|
|
2022-08-13 15:14:15 +02:00
|
|
|
if item.updated_at > last_build {
|
|
|
|
last_build = item.updated_at;
|
2022-05-17 11:15:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ItemBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.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))
|
2022-08-13 15:14:15 +02:00
|
|
|
.pub_date(Some(item.updated_at.to_rfc2822()))
|
2022-05-17 11:15:22 +02:00
|
|
|
.itunes_ext(Some(itunes_ext))
|
|
|
|
.build()
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
2022-05-24 10:35:10 +02:00
|
|
|
let itunes_ext = ITunesChannelExtensionBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.author(channel.author)
|
|
|
|
.categories(
|
|
|
|
channel
|
|
|
|
.categories
|
|
|
|
.into_iter()
|
|
|
|
.map(|cat| ITunesCategoryBuilder::default().text(cat).build())
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
)
|
|
|
|
.image(Some(channel.image.to_string()))
|
2022-05-24 10:35:10 +02:00
|
|
|
.explicit(Some(String::from("no")))
|
2022-08-13 15:14:15 +02:00
|
|
|
.summary(Some(channel.description.clone()))
|
2022-05-17 11:15:22 +02:00
|
|
|
.build();
|
|
|
|
|
|
|
|
let channel = ChannelBuilder::default()
|
2022-08-13 15:14:15 +02:00
|
|
|
.title(channel.title)
|
|
|
|
.link(channel.link)
|
|
|
|
.description(channel.description)
|
2022-05-24 10:35:10 +02:00
|
|
|
.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)
|
2022-05-24 10:35:10 +02:00
|
|
|
.itunes_ext(Some(itunes_ext))
|
2022-05-17 11:15:22 +02:00
|
|
|
.build();
|
|
|
|
let feed = RssFeed(channel.to_string());
|
|
|
|
|
2022-05-26 19:57:16 +02:00
|
|
|
Ok(feed)
|
2022-05-17 11:15:22 +02:00
|
|
|
}
|
|
|
|
|
2022-05-24 11:04:27 +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()
|
2022-05-24 11:04:27 +02:00
|
|
|
.mount("/", routes![download, feed, index])
|
2022-05-20 16:15:33 +02:00
|
|
|
.attach(AdHoc::config::<Config>())
|
2022-05-24 11:04:27 +02:00
|
|
|
.attach(Template::fairing())
|
2022-05-17 11:15:22 +02:00
|
|
|
}
|