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
This commit is contained in:
Lys 2023-10-09 15:49:33 +03:00
parent 67244e63b3
commit 453a496377
Signed by: lyssieth
GPG key ID: C9CF3D614FAA3940
14 changed files with 710 additions and 43 deletions

View file

@ -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::<HashMap<_, _>>();
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,

View file

@ -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";

3
src/random_types.rs Normal file
View file

@ -0,0 +1,3 @@
mod sort_type;
pub use sort_type::SortType;

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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<Self, Self::Err> {
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(),
)))
}
}
}
}

View file

@ -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<dyn Endpoint<Output = poem::Response>> {
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()
}

122
src/rest/get_album.rs Normal file
View file

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

View file

@ -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<GetAlbumListParams>,
) -> 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<i32>,
#[serde(default)]
pub to_year: Option<i32>,
#[serde(default)]
pub genre: Option<String>,
#[serde(default)]
pub music_folder_id: Option<i32>,
}
impl GetAlbumListParams {
#[allow(clippy::result_large_err)]
pub fn verify(self) -> Result<Self, SubsonicResponse> {
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
}

View file

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

16
src/rest/get_license.rs Normal file
View file

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

View file

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

View file

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

47
src/rest/stream.rs Normal file
View file

@ -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<StreamParams>,
) -> 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<i32>,
#[serde(default)]
pub format: Option<String>,
#[serde(rename = "timeOffset", default)]
pub time_offset: Option<i32>,
#[serde(rename = "size", default)]
pub size: Option<String>,
#[serde(rename = "estimateContentLength", default)]
pub estimate_content_length: bool,
#[serde(default)]
pub converted: bool,
}

View file

@ -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<SubResponseType>,
}
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_empty() -> Self {
Self {
xmlns: "http://subsonic.org/restapi".to_string(),
status: ResponseStatus::Ok,
version: VersionTriple(1, 16, 1),
inner: SubResponseType::Empty,
pub fn new_music_folders(music_folders: Vec<MusicFolder>) -> Self {
Self::new(SubResponseType::MusicFolders { music_folders })
}
pub fn new_album_list(albums: Vec<Child>) -> Self {
Self::new(SubResponseType::AlbumList { albums })
}
pub fn new_album_list2(albums: Vec<AlbumId3>) -> Self {
Self::new(SubResponseType::AlbumList2 { albums })
}
pub fn new_album(album: AlbumId3) -> Self {
Self::new(SubResponseType::Album(album))
}
pub fn new_empty() -> Self {
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<MusicFolder>,
},
#[serde(rename = "error")]
Error(Error),
#[serde(rename = "license")]
License {
#[serde(rename = "valid")]
valid: bool,
},
#[serde(rename = "albumList")]
AlbumList {
#[serde(rename = "album")]
albums: Vec<Child>,
},
#[serde(rename = "albumList2")]
AlbumList2 {
#[serde(rename = "album")]
albums: Vec<AlbumId3>,
},
#[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<String>,
#[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")]
pub artist_id: Option<i32>,
#[serde(rename = "@coverArt", skip_serializing_if = "Option::is_none")]
pub cover_art: Option<String>,
#[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<i64>,
#[serde(rename = "@created", skip_serializing_if = "Option::is_none")]
pub created: Option<OffsetDateTime>,
#[serde(rename = "@starred", skip_serializing_if = "Option::is_none")]
pub starred: Option<OffsetDateTime>,
#[serde(rename = "@year", skip_serializing_if = "Option::is_none")]
pub year: Option<i32>,
#[serde(rename = "@genre", skip_serializing_if = "Option::is_none")]
pub genre: Option<String>,
#[serde(rename = "song", skip_serializing_if = "Vec::is_empty")]
pub songs: Vec<Child>,
}
#[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<i32>,
#[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<String>,
#[serde(rename = "@artist", skip_serializing_if = "Option::is_none")]
pub artist: Option<String>,
#[serde(rename = "@track", skip_serializing_if = "Option::is_none")]
pub track: Option<i32>,
#[serde(rename = "@year", skip_serializing_if = "Option::is_none")]
pub year: Option<i32>,
#[serde(rename = "@genre", skip_serializing_if = "Option::is_none")]
pub genre: Option<String>,
#[serde(rename = "@coverArt", skip_serializing_if = "Option::is_none")]
pub cover_art: Option<String>,
#[serde(rename = "@size", skip_serializing_if = "Option::is_none")]
pub size: Option<i32>,
#[serde(rename = "@contentType", skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
#[serde(rename = "@suffix", skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(
rename = "@transcodedContentType",
skip_serializing_if = "Option::is_none"
)]
pub transcoded_content_type: Option<String>,
#[serde(rename = "@transcodedSuffix", skip_serializing_if = "Option::is_none")]
pub transcoded_suffix: Option<String>,
#[serde(rename = "@duration", skip_serializing_if = "Option::is_none")]
pub duration: Option<i32>,
#[serde(rename = "@bitRate", skip_serializing_if = "Option::is_none")]
pub bit_rate: Option<i32>,
#[serde(rename = "@path", skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(rename = "@isVideo", skip_serializing_if = "Option::is_none")]
pub is_video: Option<bool>,
#[serde(rename = "@userRating", skip_serializing_if = "Option::is_none")]
pub user_rating: Option<i32>,
#[serde(rename = "@averageRating", skip_serializing_if = "Option::is_none")]
pub average_rating: Option<f32>,
#[serde(rename = "@playCount", skip_serializing_if = "Option::is_none")]
pub play_count: Option<i32>,
#[serde(rename = "@discNumber", skip_serializing_if = "Option::is_none")]
pub disc_number: Option<i32>,
#[serde(rename = "@created", skip_serializing_if = "Option::is_none")]
pub created: Option<OffsetDateTime>,
#[serde(rename = "@starred", skip_serializing_if = "Option::is_none")]
pub starred: Option<OffsetDateTime>,
#[serde(rename = "@albumId", skip_serializing_if = "Option::is_none")]
pub album_id: Option<String>,
#[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")]
pub artist_id: Option<String>,
#[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
pub r#type: Option<MediaType>,
#[serde(rename = "@bookmarkPosition", skip_serializing_if = "Option::is_none")]
pub bookmark_position: Option<i32>,
#[serde(rename = "@originalWidth", skip_serializing_if = "Option::is_none")]
pub original_width: Option<i32>,
#[serde(rename = "@originalHeight", skip_serializing_if = "Option::is_none")]
pub original_height: Option<i32>,
}
#[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<String>),
}
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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()
}
}

36
src/utils.rs Normal file
View file

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