From 453a496377c7e188b05ae294e491cb9ad96cbe87 Mon Sep 17 00:00:00 2001 From: Lyssieth Date: Mon, 9 Oct 2023 15:49:33 +0300 Subject: [PATCH] feat: we can now do things The most crappy basic implementation, but: - It can list albums - It can list a single album's tracks - It can play a song Closes #1 --- src/authentication.rs | 27 +++-- src/main.rs | 2 + src/random_types.rs | 3 + src/random_types/sort_type.rs | 55 +++++++++ src/rest.rs | 23 +++- src/rest/get_album.rs | 122 +++++++++++++++++++ src/rest/get_album_list.rs | 99 ++++++++++++++++ src/rest/get_album_list2.rs | 53 +++++++++ src/rest/get_license.rs | 16 +++ src/rest/get_music_folders.rs | 34 ++++++ src/rest/ping.rs | 24 +--- src/rest/stream.rs | 47 ++++++++ src/subsonic.rs | 212 +++++++++++++++++++++++++++++++--- src/utils.rs | 36 ++++++ 14 files changed, 710 insertions(+), 43 deletions(-) create mode 100644 src/random_types.rs create mode 100644 src/random_types/sort_type.rs create mode 100644 src/rest/get_album.rs create mode 100644 src/rest/get_album_list.rs create mode 100644 src/rest/get_album_list2.rs create mode 100644 src/rest/get_license.rs create mode 100644 src/rest/get_music_folders.rs create mode 100644 src/rest/stream.rs create mode 100644 src/utils.rs diff --git a/src/authentication.rs b/src/authentication.rs index b1650d2..8f336e7 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt::Display, str::FromStr, string::ToString}; use color_eyre::Report; use poem::{Error, FromRequest, IntoResponse, Request, RequestBody, Result}; -use tracing::debug; +use tracing::trace; use crate::subsonic::{self, SubsonicResponse}; @@ -28,7 +28,7 @@ impl<'a> FromRequest<'a> for Authentication { .filter_map(|q| q.split_once('=')) .collect::>(); - debug!("Query: {query:?}"); + trace!("Query: {query:?}"); let user = { let user = query.get("u").map(ToString::to_string); @@ -43,7 +43,7 @@ impl<'a> FromRequest<'a> for Authentication { user.expect("Missing username") }; - debug!("User: {user}"); + trace!("User: {user}"); let password = query.get("p").map(ToString::to_string); if password.is_some() { @@ -55,7 +55,7 @@ impl<'a> FromRequest<'a> for Authentication { )); } - debug!("Password: {password:?}"); + trace!("Password: {password:?}"); let token = query.get("t").map(ToString::to_string); let salt = query.get("s").map(ToString::to_string); @@ -68,9 +68,9 @@ impl<'a> FromRequest<'a> for Authentication { )); } let token = token.expect("Missing token"); - debug!("Token: {token}"); + trace!("Token: {token}"); let salt = salt.expect("Missing salt"); - debug!("Salt: {salt}"); + trace!("Salt: {salt}"); let version = { let version = query.get("v").map(ToString::to_string); @@ -95,7 +95,7 @@ impl<'a> FromRequest<'a> for Authentication { ) }) }?; - debug!("Version: {version}"); + trace!("Version: {version}"); let client = { let client = query.get("c").map(ToString::to_string); @@ -111,13 +111,22 @@ impl<'a> FromRequest<'a> for Authentication { client.expect("Missing client") }; - debug!("Client: {client}"); + trace!("Client: {client}"); let format = query .get("f") .map_or_else(|| "xml".to_string(), ToString::to_string); - debug!("Format: {format}"); + 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, diff --git a/src/main.rs b/src/main.rs index 18f1d1d..885d2c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,11 @@ use tracing::info; use tracing_subscriber::{fmt, EnvFilter}; mod authentication; +mod random_types; mod rest; mod subsonic; mod user; +mod utils; const LISTEN: &str = "0.0.0.0:1234"; diff --git a/src/random_types.rs b/src/random_types.rs new file mode 100644 index 0000000..2d83320 --- /dev/null +++ b/src/random_types.rs @@ -0,0 +1,3 @@ +mod sort_type; + +pub use sort_type::SortType; diff --git a/src/random_types/sort_type.rs b/src/random_types/sort_type.rs new file mode 100644 index 0000000..f2f4754 --- /dev/null +++ b/src/random_types/sort_type.rs @@ -0,0 +1,55 @@ +use std::str::FromStr; + +use serde::Deserialize; +use tracing::warn; + +use crate::subsonic::Error; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortType { + Random, + Newest, + Highest, + Frequent, + Recent, + AlphabeticalByName, + AlphabeticalByArtist, + Starred, + ByYear, + ByGenre, +} + +impl<'de> Deserialize<'de> for SortType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Self::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom) + } +} + +impl FromStr for SortType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_ref() { + "random" => Ok(Self::Random), + "newest" => Ok(Self::Newest), + "highest" => Ok(Self::Highest), + "frequent" => Ok(Self::Frequent), + "recent" => Ok(Self::Recent), + "alphabeticalbyname" => Ok(Self::AlphabeticalByName), + "alphabeticalbyartist" => Ok(Self::AlphabeticalByArtist), + "starred" => Ok(Self::Starred), + "byyear" => Ok(Self::ByYear), + "bygenre" => Ok(Self::ByGenre), + + _ => { + warn!("got invalid type parameter {s}"); + Err(Error::Generic(Some( + "type parameter is invalid".to_string(), + ))) + } + } + } +} diff --git a/src/rest.rs b/src/rest.rs index 79f7248..88b933c 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1,7 +1,28 @@ use poem::{Endpoint, EndpointExt, Route}; +// rest/getLicense +mod get_license; +// rest/getMusicFolders +mod get_music_folders; +// rest/ping mod ping; +// rest/getAlbumList +mod get_album_list; +// rest/getAlbumList2 +mod get_album_list2; +// rest/getAlbum +mod get_album; +// rest/stream +mod stream; pub fn build() -> Box> { - Route::new().at("/ping", ping::ping).boxed() + Route::new() + .at("/ping", ping::ping) + .at("/getLicense", get_license::get_license) + .at("/getMusicFolders", get_music_folders::get_music_folders) + .at("/getAlbumList", get_album_list::get_album_list) + .at("/getAlbumList2", get_album_list2::get_album_list2) + .at("/getAlbum", get_album::get_album) + .at("/stream", stream::stream) + .boxed() } diff --git a/src/rest/get_album.rs b/src/rest/get_album.rs new file mode 100644 index 0000000..69a1e75 --- /dev/null +++ b/src/rest/get_album.rs @@ -0,0 +1,122 @@ +use poem::web::{Data, Query}; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::{ + authentication::Authentication, + subsonic::{AlbumId3, Child, Error, MediaType, SubsonicResponse}, + utils, +}; + +#[poem::handler] +pub async fn get_album( + Data(pool): Data<&SqlitePool>, + auth: Authentication, + Query(params): Query, +) -> SubsonicResponse { + let u = utils::verify_user(pool, auth).await; + + match u { + Ok(_) => {} + Err(e) => return e, + } + + let mut count = 0; + let album = match params.id { + 11 => AlbumId3 { + id: 11, + name: "Example".to_string(), + artist: Some("Example".to_string()), + song_count: 5, + duration: 100, + songs: vec![ + Child { + id: 111, + title: "Example - 1".to_string(), + album: Some("Example".to_string()), + duration: Some(20), + content_type: Some("audio/mpeg".to_string()), + r#type: Some(MediaType::Music), + track: Some({ + count += 1; + count + }), + ..Default::default() + }, + Child { + id: 112, + title: "Example - 2".to_string(), + album: Some("Example".to_string()), + duration: Some(20), + content_type: Some("audio/mpeg".to_string()), + r#type: Some(MediaType::Music), + track: Some({ + count += 1; + count + }), + ..Default::default() + }, + Child { + id: 113, + title: "Example - 3".to_string(), + album: Some("Example".to_string()), + duration: Some(20), + content_type: Some("audio/mpeg".to_string()), + r#type: Some(MediaType::Music), + track: Some({ + count += 1; + count + }), + ..Default::default() + }, + Child { + id: 114, + title: "Example - 4".to_string(), + album: Some("Example".to_string()), + duration: Some(20), + content_type: Some("audio/mpeg".to_string()), + r#type: Some(MediaType::Music), + track: Some({ + count += 1; + count + }), + ..Default::default() + }, + Child { + id: 115, + title: "Example - 5".to_string(), + album: Some("Example".to_string()), + duration: Some(20), + content_type: Some("audio/mpeg".to_string()), + r#type: Some(MediaType::Music), + track: Some({ + count += 1; + count + }), + ..Default::default() + }, + ], + ..Default::default() + }, + 12 => AlbumId3 { + id: 12, + name: "Example 2".to_string(), + artist: Some("Example 2".to_string()), + song_count: 7, + duration: 200, + ..Default::default() + }, + _ => { + return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(Some( + "Album does not exist".to_string(), + ))) + } + }; + + SubsonicResponse::new_album(album) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GetAlbumParams { + pub id: i32, +} diff --git a/src/rest/get_album_list.rs b/src/rest/get_album_list.rs new file mode 100644 index 0000000..c79ed9d --- /dev/null +++ b/src/rest/get_album_list.rs @@ -0,0 +1,99 @@ +use poem::web::{Data, Query}; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::{ + authentication::Authentication, + random_types::SortType, + subsonic::{Child, Error, SubsonicResponse}, + utils, +}; + +#[poem::handler] +pub async fn get_album_list( + Data(pool): Data<&SqlitePool>, + auth: Authentication, + Query(params): Query, +) -> SubsonicResponse { + let u = utils::verify_user(pool, auth).await; + + match u { + Ok(_) => {} + Err(e) => return e, + } + + let _params = match params.verify() { + Ok(p) => p, + Err(e) => return e, + }; + + let album_list = vec![ + Child { + id: 11, + parent: Some(1), + title: "Example".to_string(), + artist: Some("Example".to_string()), + is_dir: true, + ..Default::default() + }, + Child { + id: 12, + parent: Some(1), + title: "Example 2".to_string(), + artist: Some("Example 2".to_string()), + is_dir: true, + ..Default::default() + }, + ]; + + SubsonicResponse::new_album_list(album_list) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GetAlbumListParams { + #[serde(rename = "type")] + pub r#type: SortType, + #[serde(default = "default_size")] + pub size: i32, + #[serde(default)] + pub offset: i32, + #[serde(default)] + pub from_year: Option, + #[serde(default)] + pub to_year: Option, + #[serde(default)] + pub genre: Option, + #[serde(default)] + pub music_folder_id: Option, +} + +impl GetAlbumListParams { + #[allow(clippy::result_large_err)] + pub fn verify(self) -> Result { + if self.r#type == SortType::ByYear { + if self.from_year.is_none() || self.to_year.is_none() { + return Err(SubsonicResponse::new_error( + Error::RequiredParameterMissing(Some( + "Missing required parameter: fromYear or toYear".to_string(), + )), + )); + } + } else if self.r#type == SortType::ByGenre && self.genre.is_none() { + return Err(SubsonicResponse::new_error( + Error::RequiredParameterMissing(Some( + "Missing required parameter: genre".to_string(), + )), + )); + } else if self.size > 500 || self.size < 1 { + return Err(SubsonicResponse::new_error(Error::Generic(Some( + "size must be between 1 and 500".to_string(), + )))); + } + + Ok(self) + } +} + +const fn default_size() -> i32 { + 10 +} diff --git a/src/rest/get_album_list2.rs b/src/rest/get_album_list2.rs new file mode 100644 index 0000000..f2e7e92 --- /dev/null +++ b/src/rest/get_album_list2.rs @@ -0,0 +1,53 @@ +use poem::web::{Data, Query}; +use sqlx::SqlitePool; + +use crate::{ + authentication::Authentication, + rest::get_album_list::GetAlbumListParams, + subsonic::{AlbumId3, SubsonicResponse}, + utils, +}; + +#[poem::handler] +pub async fn get_album_list2( + Data(pool): Data<&SqlitePool>, + auth: Authentication, + Query(params): Query, +) -> SubsonicResponse { + let u = utils::verify_user(pool, auth).await; + + match u { + Ok(_) => {} + Err(e) => return e, + } + + let params = match params.verify() { + Ok(p) => p, + Err(e) => return e, + }; + + if params.offset > 0 { + return SubsonicResponse::new_album_list2(Vec::new()); + } + + let album_list = vec![ + AlbumId3 { + id: 11, + name: "Example".to_string(), + artist: Some("Example".to_string()), + song_count: 5, + duration: 100, + ..Default::default() + }, + AlbumId3 { + id: 12, + name: "Example 2".to_string(), + artist: Some("Example 2".to_string()), + song_count: 7, + duration: 200, + ..Default::default() + }, + ]; + + SubsonicResponse::new_album_list2(album_list) +} diff --git a/src/rest/get_license.rs b/src/rest/get_license.rs new file mode 100644 index 0000000..cc46eea --- /dev/null +++ b/src/rest/get_license.rs @@ -0,0 +1,16 @@ +use poem::web::Data; +use sqlx::SqlitePool; + +use crate::{authentication::Authentication, subsonic::SubsonicResponse, utils}; + +#[poem::handler] +pub async fn get_license(Data(pool): Data<&SqlitePool>, auth: Authentication) -> SubsonicResponse { + let u = utils::verify_user(pool, auth).await; + + match u { + Ok(_) => {} + Err(e) => return e, + } + + SubsonicResponse::new(crate::subsonic::SubResponseType::License { valid: true }) +} diff --git a/src/rest/get_music_folders.rs b/src/rest/get_music_folders.rs new file mode 100644 index 0000000..152a3b5 --- /dev/null +++ b/src/rest/get_music_folders.rs @@ -0,0 +1,34 @@ +use poem::web::Data; +use sqlx::SqlitePool; + +use crate::{ + authentication::Authentication, + subsonic::{MusicFolder, SubsonicResponse}, + utils, +}; + +#[poem::handler] +pub async fn get_music_folders( + Data(pool): Data<&SqlitePool>, + auth: Authentication, +) -> SubsonicResponse { + let u = utils::verify_user(pool, auth).await; + + match u { + Ok(_) => {} + Err(e) => return e, + } + + let folders = vec![ + MusicFolder { + id: 0, + name: "Music".to_string(), + }, + MusicFolder { + id: 1, + name: "Podcasts".to_string(), + }, + ]; + + SubsonicResponse::new_music_folders(folders) +} diff --git a/src/rest/ping.rs b/src/rest/ping.rs index 6b2aeda..cf816a6 100644 --- a/src/rest/ping.rs +++ b/src/rest/ping.rs @@ -1,28 +1,14 @@ use poem::web::Data; use sqlx::SqlitePool; -use crate::{ - authentication::Authentication, - subsonic::{self, SubsonicResponse}, - user, -}; +use crate::{authentication::Authentication, subsonic::SubsonicResponse, utils}; #[poem::handler] pub async fn ping(Data(pool): Data<&SqlitePool>, auth: Authentication) -> SubsonicResponse { - let user = user::get_user(pool, &auth.username).await; + let u = utils::verify_user(pool, auth).await; - match user { - Ok(Some(u)) => { - if u.verify(&auth.token, &auth.salt) { - SubsonicResponse::new_empty() - } else { - SubsonicResponse::new_error(subsonic::Error::WrongUsernameOrPassword(None)) - } - } - Ok(None) => SubsonicResponse::new_error(subsonic::Error::WrongUsernameOrPassword(None)), - Err(e) => { - tracing::error!("Error getting user: {}", e); - SubsonicResponse::new_error(subsonic::Error::WrongUsernameOrPassword(None)) - } + match u { + Ok(_) => SubsonicResponse::new_empty(), + Err(e) => e, } } diff --git a/src/rest/stream.rs b/src/rest/stream.rs new file mode 100644 index 0000000..edc765c --- /dev/null +++ b/src/rest/stream.rs @@ -0,0 +1,47 @@ +use poem::{ + http::StatusCode, + web::{Data, Query}, + IntoResponse, Response, +}; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::{authentication::Authentication, utils}; + +const SONG: &[u8] = include_bytes!("../../../data.mp3"); + +#[poem::handler] +pub async fn stream( + Data(pool): Data<&SqlitePool>, + auth: Authentication, + Query(_params): Query, +) -> Response { + let u = utils::verify_user(pool, auth).await; + + match u { + Ok(_) => {} + Err(e) => return e.into_response(), + } + + poem::Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "audio/mpeg") + .body(SONG) +} + +#[derive(Debug, Clone, Deserialize, Default)] +pub struct StreamParams { + pub id: i32, + #[serde(rename = "maxBitRate", default)] + pub max_bit_rate: Option, + #[serde(default)] + pub format: Option, + #[serde(rename = "timeOffset", default)] + pub time_offset: Option, + #[serde(rename = "size", default)] + pub size: Option, + #[serde(rename = "estimateContentLength", default)] + pub estimate_content_length: bool, + #[serde(default)] + pub converted: bool, +} diff --git a/src/subsonic.rs b/src/subsonic.rs index 3f3e426..72a83b0 100644 --- a/src/subsonic.rs +++ b/src/subsonic.rs @@ -1,18 +1,22 @@ #![allow(dead_code)] // TODO: Remove this +use std::fmt::Display; + use poem::{http::StatusCode, IntoResponse, Response}; use serde::{ser::SerializeStruct, Serialize}; +use time::OffsetDateTime; use crate::authentication::VersionTriple; impl IntoResponse for SubsonicResponse { fn into_response(self) -> poem::Response { - let body = quick_xml::se::to_string(&self).expect("Failed to serialize response body"); + let body = quick_xml::se::to_string(&self).expect("Failed to serialize response"); Response::builder().status(StatusCode::OK).body(body) } } #[derive(Debug, Clone, Serialize)] +#[serde(rename = "subsonic-response")] pub struct SubsonicResponse { #[serde(rename = "@xmlns")] pub xmlns: String, @@ -21,7 +25,7 @@ pub struct SubsonicResponse { #[serde(rename = "@version")] pub version: VersionTriple, #[serde(rename = "$value")] - pub inner: SubResponseType, + pub value: Box, } impl SubsonicResponse { @@ -30,17 +34,28 @@ impl SubsonicResponse { xmlns: "http://subsonic.org/restapi".to_string(), status: ResponseStatus::Ok, version: VersionTriple(1, 16, 1), - inner, + value: Box::new(inner), } } + pub fn new_music_folders(music_folders: Vec) -> Self { + Self::new(SubResponseType::MusicFolders { music_folders }) + } + + pub fn new_album_list(albums: Vec) -> Self { + Self::new(SubResponseType::AlbumList { albums }) + } + + pub fn new_album_list2(albums: Vec) -> Self { + Self::new(SubResponseType::AlbumList2 { albums }) + } + + pub fn new_album(album: AlbumId3) -> Self { + Self::new(SubResponseType::Album(album)) + } + pub fn new_empty() -> Self { - Self { - xmlns: "http://subsonic.org/restapi".to_string(), - status: ResponseStatus::Ok, - version: VersionTriple(1, 16, 1), - inner: SubResponseType::Empty, - } + Self::new(SubResponseType::Empty) } pub fn new_error(inner: Error) -> Self { @@ -48,7 +63,7 @@ impl SubsonicResponse { xmlns: "http://subsonic.org/restapi".to_string(), status: ResponseStatus::Failed, version: VersionTriple(1, 16, 1), - inner: SubResponseType::Error(inner), + value: Box::new(SubResponseType::Error(inner)), } } } @@ -56,14 +71,151 @@ impl SubsonicResponse { #[derive(Debug, Clone, Serialize)] pub enum SubResponseType { #[serde(rename = "musicFolders")] - MusicFolders(MusicFolders), + MusicFolders { + #[serde(rename = "musicFolder")] + music_folders: Vec, + }, #[serde(rename = "error")] Error(Error), + #[serde(rename = "license")] + License { + #[serde(rename = "valid")] + valid: bool, + }, + #[serde(rename = "albumList")] + AlbumList { + #[serde(rename = "album")] + albums: Vec, + }, + #[serde(rename = "albumList2")] + AlbumList2 { + #[serde(rename = "album")] + albums: Vec, + }, + #[serde(rename = "album")] + Album(AlbumId3), Empty, } +#[derive(Debug, Clone, Serialize, Default)] +pub struct AlbumId3 { + #[serde(rename = "@id")] + pub id: i32, + #[serde(rename = "@parent")] + pub name: String, + #[serde(rename = "@artist", skip_serializing_if = "Option::is_none")] + pub artist: Option, + #[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")] + pub artist_id: Option, + #[serde(rename = "@coverArt", skip_serializing_if = "Option::is_none")] + pub cover_art: Option, + #[serde(rename = "@songCount")] + pub song_count: i32, + #[serde(rename = "@duration")] + pub duration: i32, + #[serde(rename = "@playCount", skip_serializing_if = "Option::is_none")] + pub play_count: Option, + #[serde(rename = "@created", skip_serializing_if = "Option::is_none")] + pub created: Option, + #[serde(rename = "@starred", skip_serializing_if = "Option::is_none")] + pub starred: Option, + #[serde(rename = "@year", skip_serializing_if = "Option::is_none")] + pub year: Option, + #[serde(rename = "@genre", skip_serializing_if = "Option::is_none")] + pub genre: Option, + #[serde(rename = "song", skip_serializing_if = "Vec::is_empty")] + pub songs: Vec, +} + +#[derive(Debug, Clone, Serialize, Default)] +#[serde(default)] +pub struct Child { + #[serde(rename = "@id")] + pub id: i32, + #[serde(rename = "@parent", skip_serializing_if = "Option::is_none")] + pub parent: Option, + #[serde(rename = "@isDir")] + pub is_dir: bool, + #[serde(rename = "@title")] + pub title: String, + #[serde(rename = "@album", skip_serializing_if = "Option::is_none")] + pub album: Option, + #[serde(rename = "@artist", skip_serializing_if = "Option::is_none")] + pub artist: Option, + #[serde(rename = "@track", skip_serializing_if = "Option::is_none")] + pub track: Option, + #[serde(rename = "@year", skip_serializing_if = "Option::is_none")] + pub year: Option, + #[serde(rename = "@genre", skip_serializing_if = "Option::is_none")] + pub genre: Option, + #[serde(rename = "@coverArt", skip_serializing_if = "Option::is_none")] + pub cover_art: Option, + #[serde(rename = "@size", skip_serializing_if = "Option::is_none")] + pub size: Option, + #[serde(rename = "@contentType", skip_serializing_if = "Option::is_none")] + pub content_type: Option, + #[serde(rename = "@suffix", skip_serializing_if = "Option::is_none")] + pub suffix: Option, + #[serde( + rename = "@transcodedContentType", + skip_serializing_if = "Option::is_none" + )] + pub transcoded_content_type: Option, + #[serde(rename = "@transcodedSuffix", skip_serializing_if = "Option::is_none")] + pub transcoded_suffix: Option, + #[serde(rename = "@duration", skip_serializing_if = "Option::is_none")] + pub duration: Option, + #[serde(rename = "@bitRate", skip_serializing_if = "Option::is_none")] + pub bit_rate: Option, + #[serde(rename = "@path", skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(rename = "@isVideo", skip_serializing_if = "Option::is_none")] + pub is_video: Option, + #[serde(rename = "@userRating", skip_serializing_if = "Option::is_none")] + pub user_rating: Option, + #[serde(rename = "@averageRating", skip_serializing_if = "Option::is_none")] + pub average_rating: Option, + #[serde(rename = "@playCount", skip_serializing_if = "Option::is_none")] + pub play_count: Option, + #[serde(rename = "@discNumber", skip_serializing_if = "Option::is_none")] + pub disc_number: Option, + #[serde(rename = "@created", skip_serializing_if = "Option::is_none")] + pub created: Option, + #[serde(rename = "@starred", skip_serializing_if = "Option::is_none")] + pub starred: Option, + #[serde(rename = "@albumId", skip_serializing_if = "Option::is_none")] + pub album_id: Option, + #[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")] + pub artist_id: Option, + #[serde(rename = "@type", skip_serializing_if = "Option::is_none")] + pub r#type: Option, + #[serde(rename = "@bookmarkPosition", skip_serializing_if = "Option::is_none")] + pub bookmark_position: Option, + #[serde(rename = "@originalWidth", skip_serializing_if = "Option::is_none")] + pub original_width: Option, + #[serde(rename = "@originalHeight", skip_serializing_if = "Option::is_none")] + pub original_height: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub enum MediaType { + #[serde(rename = "music")] + Music, + #[serde(rename = "video")] + Video, + #[serde(rename = "audiobook")] + Audiobook, + #[serde(rename = "podcast")] + Podcast, +} + #[derive(Debug, Clone, Serialize)] -pub struct MusicFolders {} +pub struct MusicFolder { + #[serde(rename = "@id")] + pub id: i32, + #[serde(rename = "@name")] + pub name: String, +} #[derive(Debug, Clone, Copy)] pub enum ResponseStatus { @@ -96,14 +248,46 @@ pub enum Error { RequestedDataWasNotFound(Option), } +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = self.message(); + + match self { + Self::Generic(_) => write!(f, "Generic error: {message}"), + Self::RequiredParameterMissing(_) => { + write!(f, "Required parameter missing: {message}") + } + Self::IncompatibleClientVersion(_) => { + write!(f, "Incompatible client version: {message}") + } + Self::IncompatibleServerVersion(_) => { + write!(f, "Incompatible server version: {message}") + } + Self::WrongUsernameOrPassword(_) => { + write!(f, "Wrong username or password: {message}") + } + Self::TokenAuthenticationNotSupportedForLDAP(_) => { + write!(f, "Token authentication not supported for LDAP: {message}") + } + Self::UserIsNotAuthorizedForGivenOperation(_) => { + write!(f, "User is not authorized for given operation: {message}") + } + Self::TrialPeriodExpired(_) => write!(f, "Trial period expired: {message}"), + Self::RequestedDataWasNotFound(_) => { + write!(f, "Requested data was not found: {message}") + } + } + } +} + impl Serialize for Error { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut error = serializer.serialize_struct("error", 2)?; - error.serialize_field("@code", &self.code())?; - error.serialize_field("@message", &self.message())?; + error.serialize_field("code", &self.code())?; + error.serialize_field("message", &self.message())?; error.end() } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e0e07f5 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,36 @@ +use sqlx::SqlitePool; +use tracing::error; + +use crate::{ + authentication::Authentication, + subsonic::{Error, SubsonicResponse}, + user::{get_user, User}, +}; + +pub async fn verify_user( + pool: &SqlitePool, + auth: Authentication, +) -> Result { + let user = get_user(pool, &auth.username).await; + + match user { + Ok(Some(u)) => { + if u.verify(&auth.token, &auth.salt) { + Ok(u) + } else { + Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword( + None, + ))) + } + } + Ok(None) => Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword( + None, + ))), + Err(e) => { + error!("Error getting user: {e}"); + Err(SubsonicResponse::new_error(Error::WrongUsernameOrPassword( + None, + ))) + } + } +}