feat: add 'Range' header support
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Should allow for seeking.
This commit is contained in:
Lys 2023-12-28 07:39:04 +02:00
parent 79e2f730cc
commit 9d5373797c
Signed by: lyssieth
GPG key ID: C9CF3D614FAA3940
4 changed files with 281 additions and 242 deletions

436
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@ quick-xml = { version = "0.31", features = ["serialize"] }
serde = { workspace = true } serde = { workspace = true }
serde_json = "1.0" serde_json = "1.0"
time = { workspace = true, features = ["local-offset"] } time = { workspace = true, features = ["local-offset"] }
tokio = { version = "1.34", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = [ tracing-subscriber = { version = "0.3", features = [
"env-filter", "env-filter",
@ -37,11 +37,12 @@ url-escape = "0.1"
sea-orm = { workspace = true } sea-orm = { workspace = true }
entities = { workspace = true } entities = { workspace = true }
migration = { workspace = true } migration = { workspace = true }
once_cell = { version = "1.18", features = ["parking_lot"] } once_cell = { version = "1", features = ["parking_lot"] }
futures = "0.3" futures = "0.3"
audiotags = "0.4" audiotags = "0.4"
tracing-appender = "0.2" tracing-appender = "0.2"
blake3 = "1.5" blake3 = "1.5"
image = "0.24" image = "0.24"
nate = "0.4" nate = "0.4"
rand = "0.8.5" rand = "0.8"
http-range = "0.1"

View file

@ -1,4 +1,7 @@
use poem::{http::StatusCode, Endpoint, EndpointExt, Response, Route}; use poem::{
http::{header, StatusCode},
Endpoint, EndpointExt, Response, Route,
};
// rest/getLicense // rest/getLicense
mod get_license; mod get_license;
@ -43,8 +46,16 @@ pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
.at("/getAlbumList2.view", get_album_list::get_album_list) .at("/getAlbumList2.view", get_album_list::get_album_list)
.at("/getAlbum", get_album::get_album) .at("/getAlbum", get_album::get_album)
.at("/getAlbum.view", get_album::get_album) .at("/getAlbum.view", get_album::get_album)
.at("/stream", stream::stream) .at(
.at("/stream.view", stream::stream) "/stream",
stream::stream
.with(poem::middleware::SetHeader::new().appending(header::ACCEPT_RANGES, "bytes")),
)
.at(
"/stream.view",
stream::stream
.with(poem::middleware::SetHeader::new().appending(header::ACCEPT_RANGES, "bytes")),
)
.at("/startScan", start_scan::start_scan) .at("/startScan", start_scan::start_scan)
.at("/startScan.view", start_scan::start_scan) .at("/startScan.view", start_scan::start_scan)
.at("/getScanStatus", get_scan_status::get_scan_status) .at("/getScanStatus", get_scan_status::get_scan_status)

View file

@ -1,7 +1,8 @@
use crate::{json_or_xml, utils::db::DbTxn}; use crate::{json_or_xml, utils::db::DbTxn};
use entities::prelude::Track; use entities::prelude::Track;
use http_range::HttpRange;
use poem::{ use poem::{
http::StatusCode, http::{header, HeaderMap, StatusCode},
web::{Data, Query}, web::{Data, Query},
Response, Response,
}; };
@ -20,6 +21,7 @@ use crate::{
pub async fn stream( pub async fn stream(
Data(txn): Data<&DbTxn>, Data(txn): Data<&DbTxn>,
auth: Authentication, auth: Authentication,
headers: &HeaderMap,
Query(params): Query<StreamParams>, Query(params): Query<StreamParams>,
) -> Response { ) -> Response {
let u = utils::verify_user(txn.clone(), &auth).await; let u = utils::verify_user(txn.clone(), &auth).await;
@ -81,11 +83,62 @@ pub async fn stream(
} }
}; };
if let Some(range) = headers.get(header::RANGE) {
let range = match range.to_str() {
Ok(range) => range,
Err(e) => {
error!(
error = &e as &dyn std::error::Error,
"Error parsing range header: {e}"
);
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
}
};
let range = match HttpRange::parse(range, song.len() as u64) {
Ok(range) => range,
Err(e) => {
error!("Error parsing range header: {e:?}");
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
}
};
if range.len() > 1 {
return json_or_xml!(
auth,
SubsonicResponse::new_error(Error::Generic(Some(
"Multiple ranges are not supported".to_string()
)))
);
}
if let Some(range) = range.first() {
let body = song[range.start as usize..(range.start + range.length) as usize].to_vec();
poem::Response::builder()
.status(StatusCode::PARTIAL_CONTENT)
.header(header::CONTENT_TYPE, track.content_type)
.header(
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", range.start, range.length, song.len()),
)
.header(header::CONTENT_LENGTH, body.len())
.body(body)
} else {
poem::Response::builder() poem::Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", "audio/mpeg") .header(header::CONTENT_TYPE, track.content_type)
.header(header::CONTENT_LENGTH, song.len())
.body(song) .body(song)
} }
} else {
poem::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, track.content_type)
.header(header::CONTENT_LENGTH, song.len())
.body(song)
}
}
#[derive(Debug, Clone, Deserialize, Default)] #[derive(Debug, Clone, Deserialize, Default)]
pub struct StreamParams { pub struct StreamParams {