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:
parent
67244e63b3
commit
453a496377
14 changed files with 710 additions and 43 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
3
src/random_types.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod sort_type;
|
||||
|
||||
pub use sort_type::SortType;
|
||||
55
src/random_types/sort_type.rs
Normal file
55
src/random_types/sort_type.rs
Normal 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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/rest.rs
23
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<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
122
src/rest/get_album.rs
Normal 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,
|
||||
}
|
||||
99
src/rest/get_album_list.rs
Normal file
99
src/rest/get_album_list.rs
Normal 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
|
||||
}
|
||||
53
src/rest/get_album_list2.rs
Normal file
53
src/rest/get_album_list2.rs
Normal 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
16
src/rest/get_license.rs
Normal 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 })
|
||||
}
|
||||
34
src/rest/get_music_folders.rs
Normal file
34
src/rest/get_music_folders.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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
47
src/rest/stream.rs
Normal 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,
|
||||
}
|
||||
212
src/subsonic.rs
212
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<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
36
src/utils.rs
Normal 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,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue