feat: implement a basic warp server for serving static audio files

This commit is contained in:
kalmenn 2024-05-23 23:58:06 +02:00
commit dcf4aeb913
Signed by: kalmenn
GPG key ID: F500055C44BC3834
6 changed files with 1500 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

1363
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

3
Cargo.toml Normal file
View file

@ -0,0 +1,3 @@
[workspace]
members = ["server"]
resolver = "2"

13
server/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
warp = "0.3.7"
tokio = { version = "1.37.0", features = ["full"] }
futures-util = "0.3.30"
clap = { version = "4.5.4", features = ["derive"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
bytes = "1.6.0"

39
server/src/error.rs Normal file
View file

@ -0,0 +1,39 @@
use std::borrow::Cow;
use hyper::StatusCode;
use tracing::warn;
use warp::{
hyper,
reject::{Reject, Rejection},
reply::Response,
};
#[derive(Debug)]
pub struct InternalServerError(pub Cow<'static, str>);
impl Reject for InternalServerError {}
#[derive(Debug)]
pub struct NotFoundError(pub Cow<'static, str>);
impl Reject for NotFoundError {}
pub async fn handle_rejection(err: Rejection) -> Result<Response, std::convert::Infallible> {
let (status_code, body): (StatusCode, Cow<'static, str>) =
if err.is_not_found() || err.find::<NotFoundError>().is_some() {
(
StatusCode::NOT_FOUND,
err.find::<NotFoundError>()
.map_or("".into(), |reason| reason.0.clone()),
)
} else if let Some(InternalServerError(reason)) = err.find::<InternalServerError>() {
(StatusCode::INTERNAL_SERVER_ERROR, reason.clone())
} else {
warn!("unhandled rejection: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, "".into())
};
Ok(hyper::Response::builder()
.header("Content-Type", "text/plain")
.status(status_code)
.body(body.into())
.expect("building the response should never fail"))
}

81
server/src/main.rs Normal file
View file

@ -0,0 +1,81 @@
pub mod error;
use error::{handle_rejection, InternalServerError, NotFoundError};
use std::path::PathBuf;
use bytes::Bytes;
use clap::Parser as _;
use tokio::io::AsyncReadExt;
use tracing::Level;
use warp::{
filters::{any::any, path::param},
http::HeaderMap,
hyper,
reject::Rejection,
Filter,
};
#[derive(clap::Parser)]
pub struct Args {
music_dir: PathBuf,
}
#[tokio::main]
async fn main() {
let config: &'static Args = Box::leak(Box::new(Args::parse()));
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.init();
let route = warp::get()
.and(warp::path("track"))
.and(any().map(move || config))
.and(warp::header::headers_cloned())
.and(param())
.and_then(serve_local_tracks);
warp::serve(route.recover(handle_rejection))
.run(([127, 0, 0, 1], 8080))
.await;
}
pub async fn serve_local_tracks(
config: &Args,
_headers: HeaderMap,
// TODO: request files based off of their MusicBrainz Identifier instead
track_name: String,
) -> Result<warp::reply::Response, Rejection> {
let mut location = config.music_dir.clone();
// FIXME: for now, file paths need to be URL safe to be able to be requested
location.push(PathBuf::from(track_name));
let (Ok(mut file), Some(filename)) = (
tokio::fs::File::options().read(true).open(&location).await,
location.file_name().map(|f| f.to_string_lossy()),
) else {
return Err(NotFoundError(
format!("The requested song could not be found on disk. Tried loading {location:?}")
.into(),
)
.into());
};
// TODO: handle range requests
// TODO: stream the file instead of fully reading it
let mut bytes = Vec::new();
let _ = file.read_to_end(&mut bytes).await.map_err(|_| {
InternalServerError(format!("Failed to read music file \"{filename}\" to the end").into())
});
if let Ok(response) = hyper::Response::builder()
.header("Content-Type", "audio/mpeg")
// .header("Accept-Ranges", "bytes") // TODO: handle range requests
.body(Bytes::from(bytes).into())
{
Ok(response)
} else {
Err(InternalServerError("Failed to build Response".into()).into())
}
}