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 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(
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| {
if json {
Error::from_response(
SubsonicResponse::new_error(subsonic::Error::Generic(Some(format!(
"invalid version parameter: {e}"
))))
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)]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
json_or_xml!(
auth,
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 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(), &params).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(), &params).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(), &params).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)));
}
};
json_or_xml!(
auth,
SubsonicResponse::new_search_result3(artists, albums, songs)
)
}
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 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() {
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)))
}
}
}

View file

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

View file

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

View file

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