diff --git a/rave/src/authentication.rs b/rave/src/authentication.rs index 8f336e7..fcc15b6 100644 --- a/rave/src/authentication.rs +++ b/rave/src/authentication.rs @@ -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 { 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::() .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)] diff --git a/rave/src/rest/get_album.rs b/rave/src/rest/get_album.rs index 8364f6c..fe36a4b 100644 --- a/rave/src/rest/get_album.rs +++ b/rave/src/rest/get_album.rs @@ -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, -) -> 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::() { 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::>(); 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)] diff --git a/rave/src/rest/get_album_list.rs b/rave/src/rest/get_album_list.rs index fe5257d..33a8e33 100644 --- a/rave/src/rest/get_album_list.rs +++ b/rave/src/rest/get_album_list.rs @@ -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, -) -> 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)), } } diff --git a/rave/src/rest/get_artist.rs b/rave/src/rest/get_artist.rs index c748500..dcc3a4f 100644 --- a/rave/src/rest/get_artist.rs +++ b/rave/src/rest/get_artist.rs @@ -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, -) -> 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::() 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)] diff --git a/rave/src/rest/get_artists.rs b/rave/src/rest/get_artists.rs index b6c7d6d..b8b90b1 100644 --- a/rave/src/rest/get_artists.rs +++ b/rave/src/rest/get_artists.rs @@ -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)) } diff --git a/rave/src/rest/get_cover_art.rs b/rave/src/rest/get_cover_art.rs index 2ceb18b..b7ff692 100644 --- a/rave/src/rest/get_cover_art.rs +++ b/rave/src/rest/get_cover_art.rs @@ -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, ) -> 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::() { 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))); } }; diff --git a/rave/src/rest/get_license.rs b/rave/src/rest/get_license.rs index 3c0b4b8..eebf1ce 100644 --- a/rave/src/rest/get_license.rs +++ b/rave/src/rest/get_license.rs @@ -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 }) + ) } diff --git a/rave/src/rest/get_music_folders.rs b/rave/src/rest/get_music_folders.rs index dd1d226..4de1f49 100644 --- a/rave/src/rest/get_music_folders.rs +++ b/rave/src/rest/get_music_folders.rs @@ -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()) + ) } diff --git a/rave/src/rest/get_scan_status.rs b/rave/src/rest/get_scan_status.rs index 347ad1b..7333f6e 100644 --- a/rave/src/rest/get_scan_status.rs +++ b/rave/src/rest/get_scan_status.rs @@ -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))) } } } diff --git a/rave/src/rest/ping.rs b/rave/src/rest/ping.rs index 9ef84b9..e355726 100644 --- a/rave/src/rest/ping.rs +++ b/rave/src/rest/ping.rs @@ -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, + } + ) } diff --git a/rave/src/rest/search3.rs b/rave/src/rest/search3.rs index bc64ef2..a389fa1 100644 --- a/rave/src/rest/search3.rs +++ b/rave/src/rest/search3.rs @@ -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, -) -> 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, Report> { diff --git a/rave/src/rest/start_scan.rs b/rave/src/rest/start_scan.rs index 00e7343..b0db73b 100644 --- a/rave/src/rest/start_scan.rs +++ b/rave/src/rest/start_scan.rs @@ -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))) } } } diff --git a/rave/src/rest/stream.rs b/rave/src/rest/stream.rs index 517d642..ba4c5d0 100644 --- a/rave/src/rest/stream.rs +++ b/rave/src/rest/stream.rs @@ -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, ) -> 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::() { 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))); } }; diff --git a/rave/src/subsonic/mod.rs b/rave/src/subsonic/mod.rs index 29b978a..41b278c 100644 --- a/rave/src/subsonic/mod.rs +++ b/rave/src/subsonic/mod.rs @@ -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, +} + +impl From 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, +} + +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 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, } +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)), diff --git a/rave/src/utils.rs b/rave/src/utils.rs index 4ef94b7..9f19b06 100644 --- a/rave/src/utils.rs +++ b/rave/src/utils.rs @@ -18,7 +18,7 @@ pub mod db; pub async fn verify_user( conn: DbTxn, - auth: Authentication, + auth: &Authentication, ) -> Result { 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() + } + }}; +}