Implement proper error logging and handling (closes: #6)
* Use the `thiserror` crate to make our own error type * Implement Rocket's `Responder` type for the error type * Adjust all methods to use the error type * Small documentation tweaks
This commit is contained in:
parent
9d1bfd8351
commit
b53365a293
|
@ -1702,6 +1702,7 @@ dependencies = [
|
|||
"rocket_dyn_templates",
|
||||
"rss",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
|||
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] }
|
||||
rss = "2.0.1"
|
||||
tempfile = "3"
|
||||
thiserror = "1.0.31"
|
||||
tokio = { version = "1.6.1", features = ["process"] }
|
||||
|
||||
[package.metadata.deb]
|
||||
|
|
50
src/lib.rs
50
src/lib.rs
|
@ -8,13 +8,15 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
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, Responder, Rocket, State};
|
||||
use rocket::{get, routes, uri, Build, Request, Responder, Rocket, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rss::extension::itunes::{
|
||||
ITunesCategoryBuilder, ITunesChannelExtensionBuilder, ITunesItemExtensionBuilder,
|
||||
|
@ -22,9 +24,43 @@ use rss::extension::itunes::{
|
|||
use rss::{
|
||||
CategoryBuilder, ChannelBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, ItemBuilder,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
pub(crate) mod mixcloud;
|
||||
|
||||
/// The possible errors that can occur.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum Error {
|
||||
#[error("Command failed: {0:?} exited with {1}")]
|
||||
CommandFailed(Command, ExitStatus),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("No redirect URL found")]
|
||||
NoRedirectUrlFound,
|
||||
|
||||
#[error("HTTP error: {0}")]
|
||||
Request(#[from] reqwest::Error),
|
||||
|
||||
#[error("Unknown supported back-end: {0}")]
|
||||
UnsupportedBackend(String),
|
||||
}
|
||||
|
||||
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")]
|
||||
|
@ -39,22 +75,22 @@ pub(crate) struct Config {
|
|||
#[response(content_type = "application/xml")]
|
||||
struct RssFeed(String);
|
||||
|
||||
/// Retrieves a download using youtube-dl and redirection.
|
||||
/// 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) -> Option<Redirect> {
|
||||
pub(crate) async fn download(file: PathBuf, backend: &str) -> Result<Redirect> {
|
||||
match backend {
|
||||
"mixcloud" => {
|
||||
let key = format!("/{}/", file.with_extension("").to_string_lossy());
|
||||
|
||||
mixcloud::redirect_url(&key).await.map(Redirect::to)
|
||||
}
|
||||
_ => None,
|
||||
_ => Err(Error::UnsupportedBackend(backend.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for retrieving the RSS feed of an Mixcloud user.
|
||||
/// Handler for retrieving the RSS feed of user on a certain back-end.
|
||||
#[get("/feed/<backend>/<username>")]
|
||||
async fn feed(backend: &str, username: &str, config: &State<Config>) -> Option<RssFeed> {
|
||||
async fn feed(backend: &str, username: &str, config: &State<Config>) -> Result<RssFeed> {
|
||||
let user = mixcloud::get_user(username).await?;
|
||||
let cloudcasts = mixcloud::get_cloudcasts(username).await?;
|
||||
let mut last_build = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc);
|
||||
|
@ -156,7 +192,7 @@ async fn feed(backend: &str, username: &str, config: &State<Config>) -> Option<R
|
|||
.build();
|
||||
let feed = RssFeed(channel.to_string());
|
||||
|
||||
Some(feed)
|
||||
Ok(feed)
|
||||
}
|
||||
|
||||
/// Returns a simple index page that explains the usage.
|
||||
|
|
|
@ -10,6 +10,8 @@ use reqwest::Url;
|
|||
use rocket::serde::Deserialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::{Error, Result};
|
||||
|
||||
/// A Mixcloud user.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
|
@ -108,52 +110,50 @@ pub(crate) fn estimated_file_size(duration: u32) -> u32 {
|
|||
}
|
||||
|
||||
/// Retrieves the user data using the Mixcloud API.
|
||||
pub(crate) async fn get_user(username: &str) -> Option<User> {
|
||||
let mut url = Url::parse(API_BASE_URL).unwrap();
|
||||
pub(crate) async fn get_user(username: &str) -> Result<User> {
|
||||
let mut url = Url::parse(API_BASE_URL).expect("URL can always be parsed");
|
||||
url.set_path(username);
|
||||
|
||||
println!("⏬ Retrieving user {username} from {url}...");
|
||||
let response = reqwest::get(url).await.ok()?;
|
||||
let user = match response.error_for_status() {
|
||||
Ok(res) => res.json().await.ok()?,
|
||||
Err(_err) => return None,
|
||||
};
|
||||
let response = reqwest::get(url).await?.error_for_status()?;
|
||||
let user = response.json().await?;
|
||||
|
||||
Some(user)
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Retrieves the cloudcasts of the user using the Mixcloud API.
|
||||
pub(crate) async fn get_cloudcasts(username: &str) -> Option<Vec<Cloudcast>> {
|
||||
let mut url = Url::parse(API_BASE_URL).unwrap();
|
||||
pub(crate) async fn get_cloudcasts(username: &str) -> Result<Vec<Cloudcast>> {
|
||||
let mut url = Url::parse(API_BASE_URL).expect("URL can always be parsed");
|
||||
url.set_path(&format!("{username}/cloudcasts/"));
|
||||
|
||||
println!("⏬ Retrieving cloudcasts of user {username} from {url}...");
|
||||
let response = reqwest::get(url).await.ok()?;
|
||||
let cloudcasts: CloudcastData = match response.error_for_status() {
|
||||
Ok(res) => res.json().await.ok()?,
|
||||
Err(_err) => return None,
|
||||
};
|
||||
let response = reqwest::get(url).await?.error_for_status()?;
|
||||
let cloudcasts: CloudcastData = response.json().await?;
|
||||
|
||||
Some(cloudcasts.data)
|
||||
Ok(cloudcasts.data)
|
||||
}
|
||||
|
||||
/// Retrieves the redirect URL for the provided Mixcloud cloudcast key.
|
||||
pub(crate) async fn redirect_url(key: &str) -> Option<String> {
|
||||
pub(crate) async fn redirect_url(key: &str) -> Result<String> {
|
||||
let mut cmd = Command::new("youtube-dl");
|
||||
cmd.args(&["--format", "http"])
|
||||
.arg("--get-url")
|
||||
.arg(&format!("{FILES_BASE_URL}{key}"))
|
||||
.stdout(Stdio::piped());
|
||||
|
||||
let output = cmd.output().await.ok()?;
|
||||
println!("🌍 Determining direct URL for {key}...");
|
||||
let output = cmd.output().await?;
|
||||
if output.status.success() {
|
||||
let direct_url = String::from_utf8_lossy(&output.stdout)
|
||||
.trim_end()
|
||||
.to_owned();
|
||||
println!("🌍 Determined direct URL for {key}: {direct_url}...");
|
||||
|
||||
Some(direct_url)
|
||||
if direct_url.is_empty() {
|
||||
return Err(Error::NoRedirectUrlFound);
|
||||
} else {
|
||||
println!(" Found direct URL: {direct_url}");
|
||||
Ok(direct_url)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Err(Error::CommandFailed(cmd, output.status))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue