feat: allow json usage
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
b6e7a36511
commit
a89ab1e690
15 changed files with 304 additions and 166 deletions
|
|
@ -4,19 +4,38 @@ use color_eyre::Report;
|
||||||
use poem::{Error, FromRequest, IntoResponse, Request, RequestBody, Result};
|
use poem::{Error, FromRequest, IntoResponse, Request, RequestBody, Result};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::subsonic::{self, SubsonicResponse};
|
use crate::subsonic::{self, SubsonicResponse, SubsonicResponseJson, SubsonicResponseXml};
|
||||||
|
|
||||||
mod de;
|
mod de;
|
||||||
|
|
||||||
|
macro_rules! return_json_or_xml {
|
||||||
|
($json:ident, $err:expr) => {
|
||||||
|
let e = SubsonicResponse::new_error($err);
|
||||||
|
|
||||||
|
return if $json {
|
||||||
|
Err(Error::from_response(
|
||||||
|
SubsonicResponseJson::from(e).into_response(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(Error::from_response(
|
||||||
|
SubsonicResponseXml::from(e).into_response(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[poem::async_trait]
|
#[poem::async_trait]
|
||||||
impl<'a> FromRequest<'a> for Authentication {
|
impl<'a> FromRequest<'a> for Authentication {
|
||||||
async fn from_request(req: &'a Request, _: &mut RequestBody) -> Result<Self> {
|
async fn from_request(req: &'a Request, _: &mut RequestBody) -> Result<Self> {
|
||||||
let query = req.uri().query().unwrap_or_default();
|
let query = req.uri().query().unwrap_or_default();
|
||||||
|
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
return Err(Error::from_response(
|
return Err(Error::from_response(
|
||||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
SubsonicResponseJson::from(SubsonicResponse::new_error(
|
||||||
|
subsonic::Error::RequiredParameterMissing(Some(
|
||||||
"please provide a `u` parameter".to_string(),
|
"please provide a `u` parameter".to_string(),
|
||||||
)))
|
)),
|
||||||
|
))
|
||||||
.into_response(),
|
.into_response(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -30,15 +49,22 @@ impl<'a> FromRequest<'a> for Authentication {
|
||||||
|
|
||||||
trace!("Query: {query:?}");
|
trace!("Query: {query:?}");
|
||||||
|
|
||||||
|
let format = query
|
||||||
|
.get("f")
|
||||||
|
.map_or_else(|| "xml".to_string(), ToString::to_string);
|
||||||
|
|
||||||
|
trace!("Format: {format}");
|
||||||
|
let json = format == "json";
|
||||||
|
|
||||||
let user = {
|
let user = {
|
||||||
let user = query.get("u").map(ToString::to_string);
|
let user = query.get("u").map(ToString::to_string);
|
||||||
if user.is_none() {
|
if user.is_none() {
|
||||||
return Err(Error::from_response(
|
return_json_or_xml!(
|
||||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
json,
|
||||||
|
subsonic::Error::RequiredParameterMissing(Some(
|
||||||
"please provide a `u` parameter".to_string(),
|
"please provide a `u` parameter".to_string(),
|
||||||
)))
|
))
|
||||||
.into_response(),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
user.expect("Missing username")
|
user.expect("Missing username")
|
||||||
};
|
};
|
||||||
|
|
@ -47,12 +73,12 @@ impl<'a> FromRequest<'a> for Authentication {
|
||||||
|
|
||||||
let password = query.get("p").map(ToString::to_string);
|
let password = query.get("p").map(ToString::to_string);
|
||||||
if password.is_some() {
|
if password.is_some() {
|
||||||
return Err(Error::from_response(
|
return_json_or_xml!(
|
||||||
SubsonicResponse::new_error(subsonic::Error::Generic(Some(
|
json,
|
||||||
|
subsonic::Error::Generic(Some(
|
||||||
"password authentication is not supported".to_string(),
|
"password authentication is not supported".to_string(),
|
||||||
)))
|
))
|
||||||
.into_response(),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Password: {password:?}");
|
trace!("Password: {password:?}");
|
||||||
|
|
@ -60,12 +86,12 @@ impl<'a> FromRequest<'a> for Authentication {
|
||||||
let token = query.get("t").map(ToString::to_string);
|
let token = query.get("t").map(ToString::to_string);
|
||||||
let salt = query.get("s").map(ToString::to_string);
|
let salt = query.get("s").map(ToString::to_string);
|
||||||
if token.is_none() || salt.is_none() {
|
if token.is_none() || salt.is_none() {
|
||||||
return Err(Error::from_response(
|
return_json_or_xml!(
|
||||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
json,
|
||||||
|
subsonic::Error::RequiredParameterMissing(Some(
|
||||||
"please provide both `t` and `s` parameters".to_string(),
|
"please provide both `t` and `s` parameters".to_string(),
|
||||||
)))
|
))
|
||||||
.into_response(),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
let token = token.expect("Missing token");
|
let token = token.expect("Missing token");
|
||||||
trace!("Token: {token}");
|
trace!("Token: {token}");
|
||||||
|
|
@ -76,23 +102,36 @@ impl<'a> FromRequest<'a> for Authentication {
|
||||||
let version = query.get("v").map(ToString::to_string);
|
let version = query.get("v").map(ToString::to_string);
|
||||||
|
|
||||||
if version.is_none() {
|
if version.is_none() {
|
||||||
return Err(Error::from_response(
|
return_json_or_xml!(
|
||||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
json,
|
||||||
"please provide a `v` parameter".to_string(),
|
subsonic::Error::RequiredParameterMissing(Some(
|
||||||
)))
|
"please provide a `v` parameter".to_string()
|
||||||
.into_response(),
|
),)
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
version
|
version
|
||||||
.expect("Missing version")
|
.expect("Missing version")
|
||||||
.parse::<VersionTriple>()
|
.parse::<VersionTriple>()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
if json {
|
||||||
Error::from_response(
|
Error::from_response(
|
||||||
SubsonicResponse::new_error(subsonic::Error::Generic(Some(format!(
|
SubsonicResponseJson::from(SubsonicResponse::new_error(
|
||||||
"invalid version parameter: {e}"
|
subsonic::Error::Generic(Some(format!(
|
||||||
))))
|
"failed to parse version: {e}",
|
||||||
|
))),
|
||||||
|
))
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Error::from_response(
|
||||||
|
SubsonicResponseXml::from(SubsonicResponse::new_error(
|
||||||
|
subsonic::Error::Generic(Some(format!(
|
||||||
|
"failed to parse version: {e}"
|
||||||
|
))),
|
||||||
|
))
|
||||||
|
.into_response(),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}?;
|
}?;
|
||||||
trace!("Version: {version}");
|
trace!("Version: {version}");
|
||||||
|
|
@ -101,33 +140,18 @@ impl<'a> FromRequest<'a> for Authentication {
|
||||||
let client = query.get("c").map(ToString::to_string);
|
let client = query.get("c").map(ToString::to_string);
|
||||||
|
|
||||||
if client.is_none() {
|
if client.is_none() {
|
||||||
return Err(Error::from_response(
|
return_json_or_xml!(
|
||||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
json,
|
||||||
|
subsonic::Error::RequiredParameterMissing(Some(
|
||||||
"please provide a `c` parameter".to_string(),
|
"please provide a `c` parameter".to_string(),
|
||||||
)))
|
))
|
||||||
.into_response(),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.expect("Missing client")
|
client.expect("Missing client")
|
||||||
};
|
};
|
||||||
trace!("Client: {client}");
|
trace!("Client: {client}");
|
||||||
|
|
||||||
let format = query
|
|
||||||
.get("f")
|
|
||||||
.map_or_else(|| "xml".to_string(), ToString::to_string);
|
|
||||||
|
|
||||||
if format != "xml" {
|
|
||||||
return Err(Error::from_response(
|
|
||||||
SubsonicResponse::new_error(subsonic::Error::Generic(Some(
|
|
||||||
"only xml format is supported".to_string(),
|
|
||||||
)))
|
|
||||||
.into_response(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("Format: {format}");
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
username: user,
|
username: user,
|
||||||
token,
|
token,
|
||||||
|
|
@ -135,6 +159,7 @@ impl<'a> FromRequest<'a> for Authentication {
|
||||||
version,
|
version,
|
||||||
client,
|
client,
|
||||||
format,
|
format,
|
||||||
|
json,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +172,7 @@ pub struct Authentication {
|
||||||
pub version: VersionTriple,
|
pub version: VersionTriple,
|
||||||
pub client: String,
|
pub client: String,
|
||||||
pub format: String,
|
pub format: String,
|
||||||
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::Authentication,
|
authentication::Authentication,
|
||||||
|
json_or_xml,
|
||||||
subsonic::{Album as AlbumId3, Artist as ArtistId3, Child, Error, SubsonicResponse},
|
subsonic::{Album as AlbumId3, Artist as ArtistId3, Child, Error, SubsonicResponse},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::utils::db::DbTxn;
|
use crate::utils::db::DbTxn;
|
||||||
use entities::prelude::{Album, Artist, Genre, Track};
|
use entities::prelude::{Album, Artist, Genre, Track};
|
||||||
use poem::web::{Data, Query};
|
use poem::{
|
||||||
|
web::{Data, Query},
|
||||||
|
Response,
|
||||||
|
};
|
||||||
use sea_orm::{EntityTrait, ModelTrait};
|
use sea_orm::{EntityTrait, ModelTrait};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{error, instrument, warn};
|
use tracing::{error, instrument, warn};
|
||||||
|
|
@ -17,19 +21,19 @@ pub async fn get_album(
|
||||||
Data(txn): Data<&DbTxn>,
|
Data(txn): Data<&DbTxn>,
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
Query(params): Query<GetAlbumParams>,
|
Query(params): Query<GetAlbumParams>,
|
||||||
) -> SubsonicResponse {
|
) -> Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let album_id = match params.id.strip_prefix("al-").ok_or_else(|| {
|
let album_id = match params.id.strip_prefix("al-").ok_or_else(|| {
|
||||||
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `al-{}`".to_string()))
|
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `al-{}`".to_string()))
|
||||||
}) {
|
}) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(e) => return SubsonicResponse::new_error(e),
|
Err(e) => return json_or_xml!(auth, SubsonicResponse::new_error(e)),
|
||||||
};
|
};
|
||||||
let album_id = match album_id.parse::<i64>() {
|
let album_id = match album_id.parse::<i64>() {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
|
|
@ -38,7 +42,7 @@ pub async fn get_album(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error parsing album ID: {e}"
|
"Error parsing album ID: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,13 +50,18 @@ pub async fn get_album(
|
||||||
let Ok(Some(album)) = album else {
|
let Ok(Some(album)) = album else {
|
||||||
match album {
|
match album {
|
||||||
Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"),
|
Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"),
|
||||||
Ok(None) => return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)),
|
Ok(None) => {
|
||||||
|
return json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
)
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error getting album: {e}"
|
"Error getting album: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +75,7 @@ pub async fn get_album(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error getting artist: {e}"
|
"Error getting artist: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -103,7 +112,7 @@ pub async fn get_album(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error getting tracks: {e}"
|
"Error getting tracks: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -134,7 +143,7 @@ pub async fn get_album(
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
tracks.sort_by_cached_key(|tr| tr.track.unwrap_or_default());
|
tracks.sort_by_cached_key(|tr| tr.track.unwrap_or_default());
|
||||||
|
|
||||||
SubsonicResponse::new_album(album, tracks)
|
json_or_xml!(auth, SubsonicResponse::new_album(album, tracks))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#![allow(clippy::unused_async)] // todo: remove
|
#![allow(clippy::unused_async)] // todo: remove
|
||||||
|
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use entities::{
|
use entities::{
|
||||||
album, artist, genre,
|
album, artist, genre,
|
||||||
prelude::{Album, Artist, Genre},
|
prelude::{Album, Artist, Genre},
|
||||||
};
|
};
|
||||||
use poem::{
|
use poem::{
|
||||||
web::{Data, Query},
|
web::{Data, Query},
|
||||||
Request,
|
Request, Response,
|
||||||
};
|
};
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect};
|
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -36,18 +36,18 @@ pub async fn get_album_list(
|
||||||
Data(txn): Data<&DbTxn>,
|
Data(txn): Data<&DbTxn>,
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
Query(params): Query<GetAlbumListParams>,
|
Query(params): Query<GetAlbumListParams>,
|
||||||
) -> SubsonicResponse {
|
) -> Response {
|
||||||
let txn = txn.clone();
|
let txn = txn.clone();
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = match params.verify() {
|
let params = match params.verify() {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
};
|
};
|
||||||
|
|
||||||
let album_list = match params.r#type {
|
let album_list = match params.r#type {
|
||||||
|
|
@ -69,18 +69,18 @@ pub async fn get_album_list(
|
||||||
|
|
||||||
let album_list = match album_list {
|
let album_list = match album_list {
|
||||||
Ok(a) => albums_to_album_id3(txn, &a).await,
|
Ok(a) => albums_to_album_id3(txn, &a).await,
|
||||||
Err(e) => return SubsonicResponse::new_error(e),
|
Err(e) => return json_or_xml!(auth, SubsonicResponse::new_error(e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
match album_list {
|
match album_list {
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
if req.uri().path().contains("getAlbumList2") {
|
if req.uri().path().contains("getAlbumList2") {
|
||||||
SubsonicResponse::new_album_list2(a)
|
json_or_xml!(auth, SubsonicResponse::new_album_list2(a))
|
||||||
} else {
|
} else {
|
||||||
SubsonicResponse::new_album_list(a)
|
json_or_xml!(auth, SubsonicResponse::new_album_list(a))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => SubsonicResponse::new_error(e),
|
Err(e) => json_or_xml!(auth, SubsonicResponse::new_error(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::Authentication,
|
authentication::Authentication,
|
||||||
|
json_or_xml,
|
||||||
subsonic::{Error, SubsonicResponse},
|
subsonic::{Error, SubsonicResponse},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::utils::db::DbTxn;
|
use crate::utils::db::DbTxn;
|
||||||
use entities::prelude::Artist;
|
use entities::prelude::Artist;
|
||||||
use poem::web::{Data, Query};
|
use poem::{
|
||||||
|
web::{Data, Query},
|
||||||
|
Response,
|
||||||
|
};
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
@ -17,24 +21,24 @@ pub async fn get_artist(
|
||||||
Data(txn): Data<&DbTxn>,
|
Data(txn): Data<&DbTxn>,
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
Query(params): Query<GetArtistParams>,
|
Query(params): Query<GetArtistParams>,
|
||||||
) -> SubsonicResponse {
|
) -> Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = params.id.split_once('-');
|
let id = params.id.split_once('-');
|
||||||
|
|
||||||
if id.is_none() {
|
if id.is_none() {
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = id.expect("none checked").1;
|
let id = id.expect("none checked").1;
|
||||||
|
|
||||||
let Ok(id) = id.parse::<i64>() else {
|
let Ok(id) = id.parse::<i64>() else {
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let artist = Artist::find_by_id(id).one(&**txn).await.map_err(|e| {
|
let artist = Artist::find_by_id(id).one(&**txn).await.map_err(|e| {
|
||||||
|
|
@ -47,13 +51,18 @@ pub async fn get_artist(
|
||||||
|
|
||||||
let artist = match artist {
|
let artist = match artist {
|
||||||
Ok(Some(v)) => v,
|
Ok(Some(v)) => v,
|
||||||
Ok(None) => return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)),
|
Ok(None) => {
|
||||||
Err(e) => return SubsonicResponse::new_error(e),
|
return json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => return json_or_xml!(auth, SubsonicResponse::new_error(e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let artist = artist.into();
|
let artist = artist.into();
|
||||||
|
|
||||||
SubsonicResponse::new_artist(artist)
|
json_or_xml!(auth, SubsonicResponse::new_artist(artist))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::Authentication,
|
authentication::Authentication,
|
||||||
|
json_or_xml,
|
||||||
subsonic::{Artist as ArtistId3, Error, SubsonicResponse},
|
subsonic::{Artist as ArtistId3, Error, SubsonicResponse},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::utils::db::DbTxn;
|
use crate::utils::db::DbTxn;
|
||||||
use entities::prelude::Artist;
|
use entities::prelude::Artist;
|
||||||
use poem::web::Data;
|
use poem::{web::Data, Response};
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn get_artists(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
pub async fn get_artists(Data(txn): Data<&DbTxn>, auth: Authentication) -> Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let artists = Artist::find().all(&**txn).await;
|
let artists = Artist::find().all(&**txn).await;
|
||||||
|
|
@ -26,11 +27,14 @@ pub async fn get_artists(Data(txn): Data<&DbTxn>, auth: Authentication) -> Subso
|
||||||
Ok(artists) => artists,
|
Ok(artists) => artists,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get artists: {}", e);
|
error!("Failed to get artists: {}", e);
|
||||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None));
|
return json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let artists = artists.into_iter().map(ArtistId3::from).collect();
|
let artists = artists.into_iter().map(ArtistId3::from).collect();
|
||||||
|
|
||||||
SubsonicResponse::new_artists(artists)
|
json_or_xml!(auth, SubsonicResponse::new_artists(artists))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{io::Cursor, path::PathBuf};
|
use std::{io::Cursor, path::PathBuf};
|
||||||
|
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use entities::prelude::CoverArt;
|
use entities::prelude::CoverArt;
|
||||||
use poem::{
|
use poem::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
web::{Data, Query},
|
web::{Data, Query},
|
||||||
IntoResponse, Response,
|
Response,
|
||||||
};
|
};
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -24,18 +24,18 @@ pub async fn get_cover_art(
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
Query(params): Query<GetCoverArtParams>,
|
Query(params): Query<GetCoverArtParams>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e.into_response(),
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let cover_art_id = match params.id.strip_prefix("ca-").ok_or_else(|| {
|
let cover_art_id = match params.id.strip_prefix("ca-").ok_or_else(|| {
|
||||||
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `ca-{}`".to_string()))
|
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `ca-{}`".to_string()))
|
||||||
}) {
|
}) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(e) => return SubsonicResponse::new_error(e).into_response(),
|
Err(e) => return json_or_xml!(auth, SubsonicResponse::new_error(e)),
|
||||||
};
|
};
|
||||||
let cover_art_id = match cover_art_id.parse::<i64>() {
|
let cover_art_id = match cover_art_id.parse::<i64>() {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
|
|
@ -44,7 +44,7 @@ pub async fn get_cover_art(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error parsing cover art ID: {e}"
|
"Error parsing cover art ID: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,15 +54,17 @@ pub async fn get_cover_art(
|
||||||
match cover_art {
|
match cover_art {
|
||||||
Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"),
|
Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
return json_or_xml!(
|
||||||
.into_response()
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error getting album: {e}"
|
"Error getting album: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -71,7 +73,10 @@ pub async fn get_cover_art(
|
||||||
let path: PathBuf = path.into();
|
let path: PathBuf = path.into();
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)).into_response();
|
return json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = match tokio::fs::read(&path).await {
|
let data = match tokio::fs::read(&path).await {
|
||||||
|
|
@ -81,7 +86,7 @@ pub async fn get_cover_art(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error reading cover art file: {e}"
|
"Error reading cover art file: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -102,7 +107,7 @@ pub async fn get_cover_art(
|
||||||
match size {
|
match size {
|
||||||
0.. => {}
|
0.. => {}
|
||||||
..=-1 => {
|
..=-1 => {
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +118,7 @@ pub async fn get_cover_art(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error loading image from memory: {e}"
|
"Error loading image from memory: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -133,7 +138,7 @@ pub async fn get_cover_art(
|
||||||
"png" => image::ImageOutputFormat::Png,
|
"png" => image::ImageOutputFormat::Png,
|
||||||
"gif" => image::ImageOutputFormat::Gif,
|
"gif" => image::ImageOutputFormat::Gif,
|
||||||
_ => {
|
_ => {
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -145,7 +150,7 @@ pub async fn get_cover_art(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error writing resized image to buffer: {e}"
|
"Error writing resized image to buffer: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use poem::web::Data;
|
use poem::{web::Data, Response};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -10,13 +10,16 @@ use crate::{
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn get_license(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
pub async fn get_license(Data(txn): Data<&DbTxn>, auth: Authentication) -> Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_or_xml!(
|
||||||
|
auth,
|
||||||
SubsonicResponse::new(crate::subsonic::SubResponseType::License { valid: true })
|
SubsonicResponse::new(crate::subsonic::SubResponseType::License { valid: true })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use entities::prelude::MusicFolder;
|
use entities::prelude::MusicFolder;
|
||||||
use poem::web::Data;
|
use poem::web::Data;
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
|
|
@ -12,19 +12,25 @@ use crate::{
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn get_music_folders(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
pub async fn get_music_folders(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let folders = MusicFolder::find().all(&**txn).await;
|
let folders = MusicFolder::find().all(&**txn).await;
|
||||||
|
|
||||||
let Ok(folders) = folders else {
|
let Ok(folders) = folders else {
|
||||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None));
|
return json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
json_or_xml!(
|
||||||
|
auth,
|
||||||
SubsonicResponse::new_music_folders(folders.into_iter().map(Into::into).collect())
|
SubsonicResponse::new_music_folders(folders.into_iter().map(Into::into).collect())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use poem::web::Data;
|
use poem::web::Data;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
|
@ -11,21 +11,24 @@ use crate::{
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn get_scan_status(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
pub async fn get_scan_status(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = scan::get_scan_status().await;
|
let status = scan::get_scan_status().await;
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
Ok(status) => SubsonicResponse::new_scan_status(status.scanning, status.count),
|
Ok(status) => json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_scan_status(status.scanning, status.count)
|
||||||
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = e.root_cause(), "Error getting scan status: {e}");
|
error!(error = e.root_cause(), "Error getting scan status: {e}");
|
||||||
SubsonicResponse::new_error(Error::Generic(None))
|
json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use poem::web::Data;
|
use poem::web::Data;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
|
|
@ -10,11 +10,14 @@ use crate::{
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn ping(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
pub async fn ping(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
|
json_or_xml!(
|
||||||
|
auth,
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => SubsonicResponse::new_empty(),
|
Ok(_) => SubsonicResponse::new_empty(),
|
||||||
Err(e) => e,
|
Err(e) => e,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use entities::{
|
use entities::{
|
||||||
album, artist,
|
album, artist,
|
||||||
|
|
@ -24,37 +24,40 @@ pub async fn search3(
|
||||||
Data(txn): Data<&DbTxn>,
|
Data(txn): Data<&DbTxn>,
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
Query(params): Query<Search3Params>,
|
Query(params): Query<Search3Params>,
|
||||||
) -> SubsonicResponse {
|
) -> poem::Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
};
|
};
|
||||||
|
|
||||||
let artists = match search_artists(txn.clone(), ¶ms).await {
|
let artists = match search_artists(txn.clone(), ¶ms).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = &e.root_cause(), "failed to search artists: {e}");
|
error!(error = &e.root_cause(), "failed to search artists: {e}");
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let albums = match search_albums(txn.clone(), ¶ms).await {
|
let albums = match search_albums(txn.clone(), ¶ms).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = &e.root_cause(), "failed to search albums: {e}");
|
error!(error = &e.root_cause(), "failed to search albums: {e}");
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let songs = match search_songs(txn.clone(), ¶ms).await {
|
let songs = match search_songs(txn.clone(), ¶ms).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = &e.root_cause(), "failed to search songs: {e}");
|
error!(error = &e.root_cause(), "failed to search songs: {e}");
|
||||||
return SubsonicResponse::new_error(Error::Generic(None));
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
json_or_xml!(
|
||||||
|
auth,
|
||||||
SubsonicResponse::new_search_result3(artists, albums, songs)
|
SubsonicResponse::new_search_result3(artists, albums, songs)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_artists(txn: DbTxn, params: &Search3Params) -> Result<Vec<ArtistId3>, Report> {
|
async fn search_artists(txn: DbTxn, params: &Search3Params) -> Result<Vec<ArtistId3>, Report> {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use poem::web::Data;
|
use poem::web::Data;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
|
@ -10,18 +10,19 @@ use crate::{
|
||||||
|
|
||||||
#[poem::handler]
|
#[poem::handler]
|
||||||
#[instrument(skip(txn, auth))]
|
#[instrument(skip(txn, auth))]
|
||||||
pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||||
let u = utils::verify_user(txn.clone(), auth).await;
|
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(u) => {
|
Ok(u) => {
|
||||||
if !u.is_admin {
|
if !u.is_admin {
|
||||||
return SubsonicResponse::new_error(Error::UserIsNotAuthorizedForGivenOperation(
|
return json_or_xml!(
|
||||||
None,
|
auth,
|
||||||
));
|
SubsonicResponse::new_error(Error::UserIsNotAuthorizedForGivenOperation(None,))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return e,
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
crate::scan::start_scan().await;
|
crate::scan::start_scan().await;
|
||||||
|
|
||||||
|
|
@ -30,18 +31,21 @@ pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> Subson
|
||||||
match res {
|
match res {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
if status.errors.is_empty() {
|
if status.errors.is_empty() {
|
||||||
|
json_or_xml!(
|
||||||
|
auth,
|
||||||
SubsonicResponse::new_scan_status(status.scanning, status.count)
|
SubsonicResponse::new_scan_status(status.scanning, status.count)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to start scan:");
|
error!("Failed to start scan:");
|
||||||
for e in status.errors {
|
for e in status.errors {
|
||||||
error!(error = e.report.root_cause(), "{e:?}");
|
error!(error = e.report.root_cause(), "{e:?}");
|
||||||
}
|
}
|
||||||
SubsonicResponse::new_error(Error::Generic(None))
|
json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = e.root_cause(), "Failed to start scan: {e}");
|
error!(error = e.root_cause(), "Failed to start scan: {e}");
|
||||||
SubsonicResponse::new_error(Error::Generic(None))
|
json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::utils::db::DbTxn;
|
use crate::{json_or_xml, utils::db::DbTxn};
|
||||||
use entities::prelude::Track;
|
use entities::prelude::Track;
|
||||||
use poem::{
|
use poem::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
web::{Data, Query},
|
web::{Data, Query},
|
||||||
IntoResponse, Response,
|
Response,
|
||||||
};
|
};
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -22,17 +22,19 @@ pub async fn stream(
|
||||||
auth: Authentication,
|
auth: Authentication,
|
||||||
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;
|
||||||
|
|
||||||
match u {
|
match u {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return e.into_response(),
|
Err(e) => return json_or_xml!(auth, e),
|
||||||
}
|
}
|
||||||
let Some(id) = params.id.strip_prefix("tr-") else {
|
let Some(id) = params.id.strip_prefix("tr-") else {
|
||||||
return SubsonicResponse::new_error(Error::RequiredParameterMissing(Some(
|
return json_or_xml!(
|
||||||
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequiredParameterMissing(Some(
|
||||||
"Track IDs must be formatted as `tr-{}`".to_string(),
|
"Track IDs must be formatted as `tr-{}`".to_string(),
|
||||||
)))
|
)))
|
||||||
.into_response();
|
);
|
||||||
};
|
};
|
||||||
let id = match id.parse::<i64>() {
|
let id = match id.parse::<i64>() {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
|
|
@ -41,7 +43,7 @@ pub async fn stream(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error parsing track ID: {e}"
|
"Error parsing track ID: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -50,15 +52,17 @@ pub async fn stream(
|
||||||
let track = match track {
|
let track = match track {
|
||||||
Ok(Some(track)) => track,
|
Ok(Some(track)) => track,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
return json_or_xml!(
|
||||||
.into_response()
|
auth,
|
||||||
|
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error fetching track: {e}"
|
"Error fetching track: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -73,7 +77,7 @@ pub async fn stream(
|
||||||
error = &e as &dyn std::error::Error,
|
error = &e as &dyn std::error::Error,
|
||||||
"Error reading track: {e}"
|
"Error reading track: {e}"
|
||||||
);
|
);
|
||||||
return SubsonicResponse::new_error(Error::Generic(None)).into_response();
|
return json_or_xml!(auth, SubsonicResponse::new_error(Error::Generic(None)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,55 @@ pub use types::artist::Artist;
|
||||||
pub use types::child::Child;
|
pub use types::child::Child;
|
||||||
pub use types::music_folder::MusicFolder;
|
pub use types::music_folder::MusicFolder;
|
||||||
|
|
||||||
impl IntoResponse for SubsonicResponse {
|
#[derive(Debug, Clone)]
|
||||||
fn into_response(self) -> poem::Response {
|
pub struct SubsonicResponse {
|
||||||
let body = quick_xml::se::to_string(&self).expect("Failed to serialize response");
|
pub status: ResponseStatus,
|
||||||
Response::builder().status(StatusCode::OK).body(body)
|
pub version: VersionTriple,
|
||||||
|
pub value: Box<SubResponseType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubsonicResponse> for SubsonicResponseJson {
|
||||||
|
fn from(value: SubsonicResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
status: value.status,
|
||||||
|
version: value.version,
|
||||||
|
value: value.value,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename = "subsonic-response")]
|
#[serde(rename = "subsonic-response")]
|
||||||
pub struct SubsonicResponse {
|
pub struct SubsonicResponseJson {
|
||||||
|
pub status: ResponseStatus,
|
||||||
|
pub version: VersionTriple,
|
||||||
|
pub value: Box<SubResponseType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for SubsonicResponseJson {
|
||||||
|
fn into_response(self) -> poem::Response {
|
||||||
|
let body = serde_json::to_string(&self).expect("Failed to serialize response");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubsonicResponse> for SubsonicResponseXml {
|
||||||
|
fn from(value: SubsonicResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
xmlns: "http://subsonic.org/restapi".to_string(),
|
||||||
|
status: value.status,
|
||||||
|
version: value.version,
|
||||||
|
value: value.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename = "subsonic-response")]
|
||||||
|
pub struct SubsonicResponseXml {
|
||||||
#[serde(rename = "@xmlns")]
|
#[serde(rename = "@xmlns")]
|
||||||
pub xmlns: String,
|
pub xmlns: String,
|
||||||
#[serde(rename = "@status")]
|
#[serde(rename = "@status")]
|
||||||
|
|
@ -32,10 +71,19 @@ pub struct SubsonicResponse {
|
||||||
pub value: Box<SubResponseType>,
|
pub value: Box<SubResponseType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for SubsonicResponseXml {
|
||||||
|
fn into_response(self) -> poem::Response {
|
||||||
|
let body = quick_xml::se::to_string(&self).expect("Failed to serialize response");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("Content-Type", "application/xml")
|
||||||
|
.body(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SubsonicResponse {
|
impl SubsonicResponse {
|
||||||
pub fn new(inner: SubResponseType) -> Self {
|
pub fn new(inner: SubResponseType) -> Self {
|
||||||
Self {
|
Self {
|
||||||
xmlns: "http://subsonic.org/restapi".to_string(),
|
|
||||||
status: ResponseStatus::Ok,
|
status: ResponseStatus::Ok,
|
||||||
version: VersionTriple(1, 16, 1),
|
version: VersionTriple(1, 16, 1),
|
||||||
value: Box::new(inner),
|
value: Box::new(inner),
|
||||||
|
|
@ -79,7 +127,6 @@ impl SubsonicResponse {
|
||||||
|
|
||||||
pub fn new_error(inner: Error) -> Self {
|
pub fn new_error(inner: Error) -> Self {
|
||||||
Self {
|
Self {
|
||||||
xmlns: "http://subsonic.org/restapi".to_string(),
|
|
||||||
status: ResponseStatus::Failed,
|
status: ResponseStatus::Failed,
|
||||||
version: VersionTriple(1, 16, 1),
|
version: VersionTriple(1, 16, 1),
|
||||||
value: Box::new(SubResponseType::Error(inner)),
|
value: Box::new(SubResponseType::Error(inner)),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ pub mod db;
|
||||||
|
|
||||||
pub async fn verify_user(
|
pub async fn verify_user(
|
||||||
conn: DbTxn,
|
conn: DbTxn,
|
||||||
auth: Authentication,
|
auth: &Authentication,
|
||||||
) -> Result<user::Model, SubsonicResponse> {
|
) -> Result<user::Model, SubsonicResponse> {
|
||||||
let user = User::find()
|
let user = User::find()
|
||||||
.filter(user::Column::Name.eq(&auth.username))
|
.filter(user::Column::Name.eq(&auth.username))
|
||||||
|
|
@ -196,3 +196,15 @@ fn format_artist(
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEPARATORS: &[char] = &[';', '/', '\\', ',', '\0', '&'];
|
const SEPARATORS: &[char] = &[';', '/', '\\', ',', '\0', '&'];
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! json_or_xml {
|
||||||
|
($auth:ident, $resp:expr) => {{
|
||||||
|
use poem::IntoResponse;
|
||||||
|
if $auth.json {
|
||||||
|
$crate::subsonic::SubsonicResponseJson::from($resp).into_response()
|
||||||
|
} else {
|
||||||
|
$crate::subsonic::SubsonicResponseXml::from($resp).into_response()
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue