feat: add 'Range' header support
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Should allow for seeking.
This commit is contained in:
parent
79e2f730cc
commit
9d5373797c
4 changed files with 281 additions and 242 deletions
436
Cargo.lock
generated
436
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,10 +83,61 @@ 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)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue