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 tracing::trace;
|
||||
|
||||
use crate::subsonic::{self, SubsonicResponse};
|
||||
use crate::subsonic::{self, SubsonicResponse, SubsonicResponseJson, SubsonicResponseXml};
|
||||
|
||||
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]
|
||||
impl<'a> FromRequest<'a> for Authentication {
|
||||
async fn from_request(req: &'a Request, _: &mut RequestBody) -> Result<Self> {
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
|
||||
if query.is_empty() {
|
||||
return Err(Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide a `u` parameter".to_string(),
|
||||
)))
|
||||
SubsonicResponseJson::from(SubsonicResponse::new_error(
|
||||
subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide a `u` parameter".to_string(),
|
||||
)),
|
||||
))
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
|
|
@ -30,15 +49,22 @@ impl<'a> FromRequest<'a> for Authentication {
|
|||
|
||||
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 = query.get("u").map(ToString::to_string);
|
||||
if user.is_none() {
|
||||
return Err(Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
||||
return_json_or_xml!(
|
||||
json,
|
||||
subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide a `u` parameter".to_string(),
|
||||
)))
|
||||
.into_response(),
|
||||
));
|
||||
))
|
||||
);
|
||||
}
|
||||
user.expect("Missing username")
|
||||
};
|
||||
|
|
@ -47,12 +73,12 @@ impl<'a> FromRequest<'a> for Authentication {
|
|||
|
||||
let password = query.get("p").map(ToString::to_string);
|
||||
if password.is_some() {
|
||||
return Err(Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::Generic(Some(
|
||||
return_json_or_xml!(
|
||||
json,
|
||||
subsonic::Error::Generic(Some(
|
||||
"password authentication is not supported".to_string(),
|
||||
)))
|
||||
.into_response(),
|
||||
));
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
trace!("Password: {password:?}");
|
||||
|
|
@ -60,12 +86,12 @@ impl<'a> FromRequest<'a> for Authentication {
|
|||
let token = query.get("t").map(ToString::to_string);
|
||||
let salt = query.get("s").map(ToString::to_string);
|
||||
if token.is_none() || salt.is_none() {
|
||||
return Err(Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
||||
return_json_or_xml!(
|
||||
json,
|
||||
subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide both `t` and `s` parameters".to_string(),
|
||||
)))
|
||||
.into_response(),
|
||||
));
|
||||
))
|
||||
);
|
||||
}
|
||||
let token = token.expect("Missing token");
|
||||
trace!("Token: {token}");
|
||||
|
|
@ -76,23 +102,36 @@ impl<'a> FromRequest<'a> for Authentication {
|
|||
let version = query.get("v").map(ToString::to_string);
|
||||
|
||||
if version.is_none() {
|
||||
return Err(Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide a `v` parameter".to_string(),
|
||||
)))
|
||||
.into_response(),
|
||||
));
|
||||
return_json_or_xml!(
|
||||
json,
|
||||
subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide a `v` parameter".to_string()
|
||||
),)
|
||||
);
|
||||
}
|
||||
version
|
||||
.expect("Missing version")
|
||||
.parse::<VersionTriple>()
|
||||
.map_err(|e| {
|
||||
Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::Generic(Some(format!(
|
||||
"invalid version parameter: {e}"
|
||||
))))
|
||||
.into_response(),
|
||||
)
|
||||
if json {
|
||||
Error::from_response(
|
||||
SubsonicResponseJson::from(SubsonicResponse::new_error(
|
||||
subsonic::Error::Generic(Some(format!(
|
||||
"failed to parse version: {e}",
|
||||
))),
|
||||
))
|
||||
.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}");
|
||||
|
|
@ -101,33 +140,18 @@ impl<'a> FromRequest<'a> for Authentication {
|
|||
let client = query.get("c").map(ToString::to_string);
|
||||
|
||||
if client.is_none() {
|
||||
return Err(Error::from_response(
|
||||
SubsonicResponse::new_error(subsonic::Error::RequiredParameterMissing(Some(
|
||||
return_json_or_xml!(
|
||||
json,
|
||||
subsonic::Error::RequiredParameterMissing(Some(
|
||||
"please provide a `c` parameter".to_string(),
|
||||
)))
|
||||
.into_response(),
|
||||
));
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
client.expect("Missing 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 {
|
||||
username: user,
|
||||
token,
|
||||
|
|
@ -135,6 +159,7 @@ impl<'a> FromRequest<'a> for Authentication {
|
|||
version,
|
||||
client,
|
||||
format,
|
||||
json,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -147,6 +172,7 @@ pub struct Authentication {
|
|||
pub version: VersionTriple,
|
||||
pub client: String,
|
||||
pub format: String,
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
use crate::{
|
||||
authentication::Authentication,
|
||||
json_or_xml,
|
||||
subsonic::{Album as AlbumId3, Artist as ArtistId3, Child, Error, SubsonicResponse},
|
||||
utils,
|
||||
};
|
||||
|
||||
use crate::utils::db::DbTxn;
|
||||
use entities::prelude::{Album, Artist, Genre, Track};
|
||||
use poem::web::{Data, Query};
|
||||
use poem::{
|
||||
web::{Data, Query},
|
||||
Response,
|
||||
};
|
||||
use sea_orm::{EntityTrait, ModelTrait};
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, instrument, warn};
|
||||
|
|
@ -17,19 +21,19 @@ pub async fn get_album(
|
|||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumParams>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
) -> Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
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(|| {
|
||||
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `al-{}`".to_string()))
|
||||
}) {
|
||||
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>() {
|
||||
Ok(id) => id,
|
||||
|
|
@ -38,7 +42,7 @@ pub async fn get_album(
|
|||
error = &e as &dyn std::error::Error,
|
||||
"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 {
|
||||
match album {
|
||||
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) => {
|
||||
error!(
|
||||
error = &e as &dyn std::error::Error,
|
||||
"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 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 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<_>>();
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#![allow(clippy::unused_async)] // todo: remove
|
||||
|
||||
use crate::utils::db::DbTxn;
|
||||
use crate::{json_or_xml, utils::db::DbTxn};
|
||||
use entities::{
|
||||
album, artist, genre,
|
||||
prelude::{Album, Artist, Genre},
|
||||
};
|
||||
use poem::{
|
||||
web::{Data, Query},
|
||||
Request,
|
||||
Request, Response,
|
||||
};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect};
|
||||
use serde::Deserialize;
|
||||
|
|
@ -36,18 +36,18 @@ pub async fn get_album_list(
|
|||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetAlbumListParams>,
|
||||
) -> SubsonicResponse {
|
||||
) -> Response {
|
||||
let txn = txn.clone();
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
|
||||
let params = match params.verify() {
|
||||
Ok(p) => p,
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
};
|
||||
|
||||
let album_list = match params.r#type {
|
||||
|
|
@ -69,18 +69,18 @@ pub async fn get_album_list(
|
|||
|
||||
let album_list = match album_list {
|
||||
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 {
|
||||
Ok(a) => {
|
||||
if req.uri().path().contains("getAlbumList2") {
|
||||
SubsonicResponse::new_album_list2(a)
|
||||
json_or_xml!(auth, SubsonicResponse::new_album_list2(a))
|
||||
} 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::{
|
||||
authentication::Authentication,
|
||||
json_or_xml,
|
||||
subsonic::{Error, SubsonicResponse},
|
||||
utils,
|
||||
};
|
||||
|
||||
use crate::utils::db::DbTxn;
|
||||
use entities::prelude::Artist;
|
||||
use poem::web::{Data, Query};
|
||||
use poem::{
|
||||
web::{Data, Query},
|
||||
Response,
|
||||
};
|
||||
use sea_orm::EntityTrait;
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, instrument};
|
||||
|
|
@ -17,24 +21,24 @@ pub async fn get_artist(
|
|||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<GetArtistParams>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
) -> Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
|
||||
let id = params.id.split_once('-');
|
||||
|
||||
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 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| {
|
||||
|
|
@ -47,13 +51,18 @@ pub async fn get_artist(
|
|||
|
||||
let artist = match artist {
|
||||
Ok(Some(v)) => v,
|
||||
Ok(None) => return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)),
|
||||
Err(e) => return SubsonicResponse::new_error(e),
|
||||
Ok(None) => {
|
||||
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();
|
||||
|
||||
SubsonicResponse::new_artist(artist)
|
||||
json_or_xml!(auth, SubsonicResponse::new_artist(artist))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
use crate::{
|
||||
authentication::Authentication,
|
||||
json_or_xml,
|
||||
subsonic::{Artist as ArtistId3, Error, SubsonicResponse},
|
||||
utils,
|
||||
};
|
||||
|
||||
use crate::utils::db::DbTxn;
|
||||
use entities::prelude::Artist;
|
||||
use poem::web::Data;
|
||||
use poem::{web::Data, Response};
|
||||
use sea_orm::EntityTrait;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
#[poem::handler]
|
||||
#[instrument(skip(txn, auth))]
|
||||
pub async fn get_artists(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
pub async fn get_artists(Data(txn): Data<&DbTxn>, auth: Authentication) -> Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
|
||||
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,
|
||||
Err(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();
|
||||
|
||||
SubsonicResponse::new_artists(artists)
|
||||
json_or_xml!(auth, SubsonicResponse::new_artists(artists))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::{io::Cursor, path::PathBuf};
|
||||
|
||||
use crate::utils::db::DbTxn;
|
||||
use crate::{json_or_xml, utils::db::DbTxn};
|
||||
use entities::prelude::CoverArt;
|
||||
use poem::{
|
||||
http::StatusCode,
|
||||
web::{Data, Query},
|
||||
IntoResponse, Response,
|
||||
Response,
|
||||
};
|
||||
use sea_orm::EntityTrait;
|
||||
use serde::Deserialize;
|
||||
|
|
@ -24,18 +24,18 @@ pub async fn get_cover_art(
|
|||
auth: Authentication,
|
||||
Query(params): Query<GetCoverArtParams>,
|
||||
) -> Response {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
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(|| {
|
||||
Error::RequiredParameterMissing(Some("Album IDs must be formatted as `ca-{}`".to_string()))
|
||||
}) {
|
||||
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>() {
|
||||
Ok(id) => id,
|
||||
|
|
@ -44,7 +44,7 @@ pub async fn get_cover_art(
|
|||
error = &e as &dyn std::error::Error,
|
||||
"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 {
|
||||
Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"),
|
||||
Ok(None) => {
|
||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||
.into_response()
|
||||
return json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
error = &e as &dyn std::error::Error,
|
||||
"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();
|
||||
|
||||
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 {
|
||||
|
|
@ -81,7 +86,7 @@ pub async fn get_cover_art(
|
|||
error = &e as &dyn std::error::Error,
|
||||
"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 {
|
||||
0.. => {}
|
||||
..=-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 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,
|
||||
"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 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 poem::web::Data;
|
||||
use crate::{json_or_xml, utils::db::DbTxn};
|
||||
use poem::{web::Data, Response};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -10,13 +10,16 @@ use crate::{
|
|||
|
||||
#[poem::handler]
|
||||
#[instrument(skip(txn, auth))]
|
||||
pub async fn get_license(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
pub async fn get_license(Data(txn): Data<&DbTxn>, auth: Authentication) -> Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
|
||||
SubsonicResponse::new(crate::subsonic::SubResponseType::License { valid: true })
|
||||
json_or_xml!(
|
||||
auth,
|
||||
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 poem::web::Data;
|
||||
use sea_orm::EntityTrait;
|
||||
|
|
@ -12,19 +12,25 @@ use crate::{
|
|||
|
||||
#[poem::handler]
|
||||
#[instrument(skip(txn, auth))]
|
||||
pub async fn get_music_folders(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
pub async fn get_music_folders(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
|
||||
let folders = MusicFolder::find().all(&**txn).await;
|
||||
|
||||
let Ok(folders) = folders else {
|
||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None));
|
||||
return json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||
);
|
||||
};
|
||||
|
||||
SubsonicResponse::new_music_folders(folders.into_iter().map(Into::into).collect())
|
||||
json_or_xml!(
|
||||
auth,
|
||||
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 tracing::{error, instrument};
|
||||
|
||||
|
|
@ -11,21 +11,24 @@ use crate::{
|
|||
|
||||
#[poem::handler]
|
||||
#[instrument(skip(txn, auth))]
|
||||
pub async fn get_scan_status(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
pub async fn get_scan_status(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
};
|
||||
|
||||
let status = scan::get_scan_status().await;
|
||||
|
||||
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) => {
|
||||
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 tracing::instrument;
|
||||
|
||||
|
|
@ -10,11 +10,14 @@ use crate::{
|
|||
|
||||
#[poem::handler]
|
||||
#[instrument(skip(txn, auth))]
|
||||
pub async fn ping(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
pub async fn ping(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => SubsonicResponse::new_empty(),
|
||||
Err(e) => e,
|
||||
}
|
||||
json_or_xml!(
|
||||
auth,
|
||||
match u {
|
||||
Ok(_) => SubsonicResponse::new_empty(),
|
||||
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 entities::{
|
||||
album, artist,
|
||||
|
|
@ -24,37 +24,40 @@ pub async fn search3(
|
|||
Data(txn): Data<&DbTxn>,
|
||||
auth: Authentication,
|
||||
Query(params): Query<Search3Params>,
|
||||
) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
) -> poem::Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
};
|
||||
|
||||
let artists = match search_artists(txn.clone(), ¶ms).await {
|
||||
Ok(v) => v,
|
||||
Err(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 {
|
||||
Ok(v) => v,
|
||||
Err(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 {
|
||||
Ok(v) => v,
|
||||
Err(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)));
|
||||
}
|
||||
};
|
||||
|
||||
SubsonicResponse::new_search_result3(artists, albums, songs)
|
||||
json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_search_result3(artists, albums, songs)
|
||||
)
|
||||
}
|
||||
|
||||
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 tracing::{error, instrument};
|
||||
|
||||
|
|
@ -10,18 +10,19 @@ use crate::{
|
|||
|
||||
#[poem::handler]
|
||||
#[instrument(skip(txn, auth))]
|
||||
pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> poem::Response {
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(u) => {
|
||||
if !u.is_admin {
|
||||
return SubsonicResponse::new_error(Error::UserIsNotAuthorizedForGivenOperation(
|
||||
None,
|
||||
));
|
||||
return json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_error(Error::UserIsNotAuthorizedForGivenOperation(None,))
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => return e,
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
crate::scan::start_scan().await;
|
||||
|
||||
|
|
@ -30,18 +31,21 @@ pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> Subson
|
|||
match res {
|
||||
Ok(status) => {
|
||||
if status.errors.is_empty() {
|
||||
SubsonicResponse::new_scan_status(status.scanning, status.count)
|
||||
json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_scan_status(status.scanning, status.count)
|
||||
)
|
||||
} else {
|
||||
error!("Failed to start scan:");
|
||||
for e in status.errors {
|
||||
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) => {
|
||||
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 poem::{
|
||||
http::StatusCode,
|
||||
web::{Data, Query},
|
||||
IntoResponse, Response,
|
||||
Response,
|
||||
};
|
||||
use sea_orm::EntityTrait;
|
||||
use serde::Deserialize;
|
||||
|
|
@ -22,17 +22,19 @@ pub async fn stream(
|
|||
auth: Authentication,
|
||||
Query(params): Query<StreamParams>,
|
||||
) -> Response {
|
||||
let u = utils::verify_user(txn.clone(), auth).await;
|
||||
let u = utils::verify_user(txn.clone(), &auth).await;
|
||||
|
||||
match u {
|
||||
Ok(_) => {}
|
||||
Err(e) => return e.into_response(),
|
||||
Err(e) => return json_or_xml!(auth, e),
|
||||
}
|
||||
let Some(id) = params.id.strip_prefix("tr-") else {
|
||||
return SubsonicResponse::new_error(Error::RequiredParameterMissing(Some(
|
||||
"Track IDs must be formatted as `tr-{}`".to_string(),
|
||||
)))
|
||||
.into_response();
|
||||
return json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_error(Error::RequiredParameterMissing(Some(
|
||||
"Track IDs must be formatted as `tr-{}`".to_string(),
|
||||
)))
|
||||
);
|
||||
};
|
||||
let id = match id.parse::<i64>() {
|
||||
Ok(id) => id,
|
||||
|
|
@ -41,7 +43,7 @@ pub async fn stream(
|
|||
error = &e as &dyn std::error::Error,
|
||||
"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 {
|
||||
Ok(Some(track)) => track,
|
||||
Ok(None) => {
|
||||
return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||
.into_response()
|
||||
return json_or_xml!(
|
||||
auth,
|
||||
SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None))
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
error = &e as &dyn std::error::Error,
|
||||
"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 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::music_folder::MusicFolder;
|
||||
|
||||
impl IntoResponse for SubsonicResponse {
|
||||
fn into_response(self) -> poem::Response {
|
||||
let body = quick_xml::se::to_string(&self).expect("Failed to serialize response");
|
||||
Response::builder().status(StatusCode::OK).body(body)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubsonicResponse {
|
||||
pub status: ResponseStatus,
|
||||
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)]
|
||||
#[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")]
|
||||
pub xmlns: String,
|
||||
#[serde(rename = "@status")]
|
||||
|
|
@ -32,10 +71,19 @@ pub struct SubsonicResponse {
|
|||
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 {
|
||||
pub fn new(inner: SubResponseType) -> Self {
|
||||
Self {
|
||||
xmlns: "http://subsonic.org/restapi".to_string(),
|
||||
status: ResponseStatus::Ok,
|
||||
version: VersionTriple(1, 16, 1),
|
||||
value: Box::new(inner),
|
||||
|
|
@ -79,7 +127,6 @@ impl SubsonicResponse {
|
|||
|
||||
pub fn new_error(inner: Error) -> Self {
|
||||
Self {
|
||||
xmlns: "http://subsonic.org/restapi".to_string(),
|
||||
status: ResponseStatus::Failed,
|
||||
version: VersionTriple(1, 16, 1),
|
||||
value: Box::new(SubResponseType::Error(inner)),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub mod db;
|
|||
|
||||
pub async fn verify_user(
|
||||
conn: DbTxn,
|
||||
auth: Authentication,
|
||||
auth: &Authentication,
|
||||
) -> Result<user::Model, SubsonicResponse> {
|
||||
let user = User::find()
|
||||
.filter(user::Column::Name.eq(&auth.username))
|
||||
|
|
@ -196,3 +196,15 @@ fn format_artist(
|
|||
}
|
||||
|
||||
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