feat: allow json usage
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Lys 2023-11-28 03:44:24 +02:00
parent b6e7a36511
commit a89ab1e690
Signed by: lyssieth
GPG key ID: C9CF3D614FAA3940
15 changed files with 304 additions and 166 deletions

View file

@ -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(
"please provide a `u` parameter".to_string(), subsonic::Error::RequiredParameterMissing(Some(
))) "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| {
Error::from_response( if json {
SubsonicResponse::new_error(subsonic::Error::Generic(Some(format!( Error::from_response(
"invalid version parameter: {e}" SubsonicResponseJson::from(SubsonicResponse::new_error(
)))) subsonic::Error::Generic(Some(format!(
.into_response(), "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}"); 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)]

View file

@ -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)]

View file

@ -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)),
} }
} }

View file

@ -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)]

View file

@ -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))
} }

View file

@ -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)));
} }
}; };

View file

@ -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),
} }
SubsonicResponse::new(crate::subsonic::SubResponseType::License { valid: true }) json_or_xml!(
auth,
SubsonicResponse::new(crate::subsonic::SubResponseType::License { valid: true })
)
} }

View file

@ -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))
);
}; };
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())
)
} }

View file

@ -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)))
} }
} }
} }

View file

@ -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;
match u { json_or_xml!(
Ok(_) => SubsonicResponse::new_empty(), auth,
Err(e) => e, match u {
} Ok(_) => SubsonicResponse::new_empty(),
Err(e) => e,
}
)
} }

View file

@ -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(), &params).await { let artists = match search_artists(txn.clone(), &params).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(), &params).await { let albums = match search_albums(txn.clone(), &params).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(), &params).await { let songs = match search_songs(txn.clone(), &params).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)));
} }
}; };
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> { async fn search_artists(txn: DbTxn, params: &Search3Params) -> Result<Vec<ArtistId3>, Report> {

View file

@ -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() {
SubsonicResponse::new_scan_status(status.scanning, status.count) json_or_xml!(
auth,
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)))
} }
} }
} }

View file

@ -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!(
"Track IDs must be formatted as `tr-{}`".to_string(), auth,
))) SubsonicResponse::new_error(Error::RequiredParameterMissing(Some(
.into_response(); "Track IDs must be formatted as `tr-{}`".to_string(),
)))
);
}; };
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)));
} }
}; };

View file

@ -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)),

View file

@ -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()
}
}};
}