API versioning
This commit is contained in:
parent
6871f41a99
commit
caa8907297
11 changed files with 907 additions and 384 deletions
docs/swagger
src
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"description": "",
|
||||
"version": "5.0",
|
||||
"version": "8.0",
|
||||
"title": "Polaris",
|
||||
"termsOfService": ""
|
||||
},
|
||||
|
@ -465,6 +465,11 @@
|
|||
],
|
||||
"summary": "Reads the content of the top-level directory in the music collection",
|
||||
"operationId": "getBrowse",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful operation",
|
||||
|
@ -503,6 +508,9 @@
|
|||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -535,6 +543,11 @@
|
|||
],
|
||||
"summary": "Recursively lists all the songs in the music collection",
|
||||
"operationId": "getFlatten",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful operation",
|
||||
|
@ -573,6 +586,9 @@
|
|||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -605,6 +621,11 @@
|
|||
],
|
||||
"summary": "Returns a list of random albums",
|
||||
"operationId": "getRandom",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful operation",
|
||||
|
@ -635,6 +656,11 @@
|
|||
],
|
||||
"summary": "Returns the albums most recently added to the collection",
|
||||
"operationId": "getRecent",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful operation",
|
||||
|
@ -673,6 +699,9 @@
|
|||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -757,7 +786,11 @@
|
|||
"description": "The maximum size of the thumbnail, either small (400x400), large (1200x1200) or native",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["small", "large", "native"],
|
||||
"enum": [
|
||||
"small",
|
||||
"large",
|
||||
"native"
|
||||
],
|
||||
"default": "small"
|
||||
}
|
||||
},
|
||||
|
@ -836,6 +869,9 @@
|
|||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/APIVersion"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -1346,13 +1382,19 @@
|
|||
"type": "string",
|
||||
"example": "Anthem of the World"
|
||||
},
|
||||
"artist": {
|
||||
"type": "string",
|
||||
"example": "Stratovarius"
|
||||
"artists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "Stratovarius"
|
||||
}
|
||||
},
|
||||
"album_artist": {
|
||||
"type": "string",
|
||||
"example": null
|
||||
"album_artists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": null
|
||||
}
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
|
@ -1370,21 +1412,33 @@
|
|||
"type": "integer",
|
||||
"example": 571
|
||||
},
|
||||
"lyricist": {
|
||||
"type": "string",
|
||||
"example": "Timo Tolkki"
|
||||
"lyricists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "Timo Tolkki"
|
||||
}
|
||||
},
|
||||
"composer": {
|
||||
"type": "string",
|
||||
"example": "Timo Tolkki"
|
||||
"composers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "Timo Tolkki"
|
||||
}
|
||||
},
|
||||
"genre": {
|
||||
"type": "string",
|
||||
"example": "Genre"
|
||||
"genres": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "Metal"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"example": "Noise Records"
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "Noise Records"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1410,6 +1464,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"APIVersion": {
|
||||
"name": "Accept-Version",
|
||||
"in": "header",
|
||||
"description": "Major version of Polaris API this client understands. If omitted, server assumes version 7.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"auth_http_bearer": {
|
||||
"type": "http",
|
||||
|
@ -1438,4 +1502,4 @@
|
|||
"callbacks": {}
|
||||
},
|
||||
"security": []
|
||||
}
|
||||
}
|
|
@ -60,11 +60,12 @@ impl Collector {
|
|||
}
|
||||
|
||||
if !tags.album_artists.is_empty() {
|
||||
inconsistent_directory_artist |=
|
||||
directory_artists.as_ref() != Some(&tags.album_artists);
|
||||
inconsistent_directory_artist |= directory_artists.is_some()
|
||||
&& directory_artists.as_ref() != Some(&tags.album_artists);
|
||||
directory_artists = Some(tags.album_artists.clone());
|
||||
} else if !tags.artists.is_empty() {
|
||||
inconsistent_directory_artist |= directory_artists.as_ref() != Some(&tags.artists);
|
||||
inconsistent_directory_artist |= directory_artists.is_some()
|
||||
&& directory_artists.as_ref() != Some(&tags.artists);
|
||||
directory_artists = Some(tags.artists.clone());
|
||||
}
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ pub struct Directory {
|
|||
pub id: i64,
|
||||
pub path: String,
|
||||
pub parent: Option<String>,
|
||||
// TODO remove all below when explorer and metadata browsing are separate
|
||||
pub artists: MultiString,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::app::{self, App};
|
|||
mod api;
|
||||
mod auth;
|
||||
mod error;
|
||||
mod version;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use axum::{
|
||||
extract::{DefaultBodyLimit, Path, Query, State},
|
||||
response::{Html, IntoResponse},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::{delete, get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ use crate::{
|
|||
};
|
||||
|
||||
use super::auth::{AdminRights, Auth};
|
||||
use super::version::Version;
|
||||
|
||||
pub fn router() -> Router<App> {
|
||||
Router::new()
|
||||
|
@ -56,6 +57,7 @@ pub fn router() -> Router<App> {
|
|||
.route("/lastfm/link", get(get_lastfm_link))
|
||||
.route("/lastfm/link", delete(delete_lastfm_link))
|
||||
// TODO figure out NormalizePathLayer and remove this
|
||||
// See https://github.com/tokio-rs/axum/discussions/2833
|
||||
.route("/browse/", get(get_browse_root))
|
||||
.route("/flatten/", get(get_flatten_root))
|
||||
.route("/random/", get(get_random))
|
||||
|
@ -252,73 +254,154 @@ async fn post_trigger_index(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn collection_files_to_response(files: Vec<index::CollectionFile>, version: Version) -> Response {
|
||||
match version {
|
||||
Version::V7 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::v7::CollectionFile>>(),
|
||||
)
|
||||
.into_response(),
|
||||
Version::V8 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::CollectionFile>>(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn songs_to_response(files: Vec<scanner::Song>, version: Version) -> Response {
|
||||
match version {
|
||||
Version::V7 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::v7::Song>>(),
|
||||
)
|
||||
.into_response(),
|
||||
Version::V8 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::Song>>(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn directories_to_response(files: Vec<scanner::Directory>, version: Version) -> Response {
|
||||
match version {
|
||||
Version::V7 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::v7::Directory>>(),
|
||||
)
|
||||
.into_response(),
|
||||
Version::V8 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::Directory>>(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_browse_root(
|
||||
_auth: Auth,
|
||||
version: Version,
|
||||
State(index): State<index::Index>,
|
||||
) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
|
||||
let result = index.browse(std::path::Path::new("")).await?;
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
) -> Response {
|
||||
let result = match index.browse(std::path::Path::new("")).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
collection_files_to_response(result, version)
|
||||
}
|
||||
|
||||
async fn get_browse(
|
||||
_auth: Auth,
|
||||
version: Version,
|
||||
State(index): State<index::Index>,
|
||||
Path(path): Path<String>,
|
||||
) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
|
||||
) -> Response {
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let result = index.browse(std::path::Path::new(path.as_ref())).await?;
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
let result = match index.browse(std::path::Path::new(path.as_ref())).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
collection_files_to_response(result, version)
|
||||
}
|
||||
|
||||
async fn get_flatten_root(
|
||||
_auth: Auth,
|
||||
version: Version,
|
||||
State(index): State<index::Index>,
|
||||
) -> Result<Json<Vec<dto::Song>>, APIError> {
|
||||
let songs = index.flatten(std::path::Path::new("")).await?;
|
||||
Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
|
||||
) -> Response {
|
||||
let songs = match index.flatten(std::path::Path::new("")).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
songs_to_response(songs, version)
|
||||
}
|
||||
|
||||
async fn get_flatten(
|
||||
_auth: Auth,
|
||||
version: Version,
|
||||
State(index): State<index::Index>,
|
||||
Path(path): Path<String>,
|
||||
) -> Result<Json<Vec<dto::Song>>, APIError> {
|
||||
) -> Response {
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let songs = index.flatten(std::path::Path::new(path.as_ref())).await?;
|
||||
Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
|
||||
let songs = match index.flatten(std::path::Path::new(path.as_ref())).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
songs_to_response(songs, version)
|
||||
}
|
||||
|
||||
async fn get_random(
|
||||
_auth: Auth,
|
||||
State(index): State<index::Index>,
|
||||
) -> Result<Json<Vec<dto::Directory>>, APIError> {
|
||||
let result = index.get_random_albums(20).await?;
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
async fn get_random(_auth: Auth, version: Version, State(index): State<index::Index>) -> Response {
|
||||
let directories = match index.get_random_albums(20).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
directories_to_response(directories, version)
|
||||
}
|
||||
|
||||
async fn get_recent(
|
||||
_auth: Auth,
|
||||
State(index): State<index::Index>,
|
||||
) -> Result<Json<Vec<dto::Directory>>, APIError> {
|
||||
let result = index.get_recent_albums(20).await?;
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
async fn get_recent(_auth: Auth, version: Version, State(index): State<index::Index>) -> Response {
|
||||
let directories = match index.get_recent_albums(20).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
directories_to_response(directories, version)
|
||||
}
|
||||
|
||||
async fn get_search_root(
|
||||
_auth: Auth,
|
||||
version: Version,
|
||||
State(index): State<index::Index>,
|
||||
) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
|
||||
let result = index.search("").await?;
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
) -> Response {
|
||||
let files = match index.search("").await {
|
||||
Ok(f) => f,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
collection_files_to_response(files, version)
|
||||
}
|
||||
|
||||
async fn get_search(
|
||||
_auth: Auth,
|
||||
version: Version,
|
||||
State(index): State<index::Index>,
|
||||
Path(query): Path<String>,
|
||||
) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
|
||||
let result = index.search(&query).await?;
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
) -> Response {
|
||||
let files = match index.search(&query).await {
|
||||
Ok(f) => f,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
collection_files_to_response(files, version)
|
||||
}
|
||||
|
||||
async fn get_playlists(
|
||||
|
@ -348,13 +431,18 @@ async fn put_playlist(
|
|||
|
||||
async fn get_playlist(
|
||||
auth: Auth,
|
||||
version: Version,
|
||||
State(playlist_manager): State<playlist::Manager>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<Json<Vec<dto::Song>>, APIError> {
|
||||
let songs = playlist_manager
|
||||
) -> Response {
|
||||
let songs = match playlist_manager
|
||||
.read_playlist(&name, auth.get_username())
|
||||
.await?;
|
||||
Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => return APIError::from(e).into_response(),
|
||||
};
|
||||
songs_to_response(songs, version)
|
||||
}
|
||||
|
||||
async fn delete_playlist(
|
||||
|
|
|
@ -7,6 +7,9 @@ impl IntoResponse for APIError {
|
|||
fn into_response(self) -> Response {
|
||||
let message = self.to_string();
|
||||
let status_code = match self {
|
||||
APIError::InvalidAPIVersionHeader => StatusCode::BAD_REQUEST,
|
||||
APIError::APIVersionHeaderParseError => StatusCode::BAD_REQUEST,
|
||||
APIError::UnsupportedAPIVersion => StatusCode::NOT_ACCEPTABLE,
|
||||
APIError::AuthorizationTokenEncoding => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
APIError::AdminPermissionRequired => StatusCode::FORBIDDEN,
|
||||
APIError::AudioFileIOError => StatusCode::NOT_FOUND,
|
||||
|
|
36
src/server/axum/version.rs
Normal file
36
src/server/axum/version.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use axum::{async_trait, extract::FromRequestParts};
|
||||
use http::request::Parts;
|
||||
|
||||
use crate::server::{dto, error::APIError};
|
||||
|
||||
pub enum Version {
|
||||
V7,
|
||||
V8,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Version
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = APIError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _app: &S) -> Result<Self, Self::Rejection> {
|
||||
let version_header = match parts.headers.get("Accept-Version").map(|h| h.to_str()) {
|
||||
Some(Ok(h)) => h,
|
||||
Some(Err(_)) => return Err(APIError::InvalidAPIVersionHeader),
|
||||
None => return Ok(Version::V7), // TODO Drop support for implicit version in future release
|
||||
};
|
||||
|
||||
let version: dto::Version = match serde_json::from_str(version_header) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(APIError::APIVersionHeaderParseError),
|
||||
};
|
||||
|
||||
Ok(match version.major {
|
||||
7 => Version::V7,
|
||||
8 => Version::V8,
|
||||
_ => return Err(APIError::UnsupportedAPIVersion),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,329 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::app::{config, ddns, index, scanner, settings, thumbnail, user, vfs};
|
||||
use std::convert::From;
|
||||
pub mod v7;
|
||||
pub mod v8;
|
||||
|
||||
pub const API_MAJOR_VERSION: i32 = 8;
|
||||
pub const API_MINOR_VERSION: i32 = 0;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
pub major: i32,
|
||||
pub minor: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct InitialSetup {
|
||||
pub has_any_users: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Credentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Authorization {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AuthQueryParameters {
|
||||
pub auth_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThumbnailOptions {
|
||||
pub size: Option<ThumbnailSize>,
|
||||
pub pad: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ThumbnailOptions> for thumbnail::Options {
|
||||
fn from(dto: ThumbnailOptions) -> Self {
|
||||
let mut options = thumbnail::Options::default();
|
||||
options.max_dimension = dto.size.map_or(options.max_dimension, Into::into);
|
||||
options.pad_to_square = dto.pad.unwrap_or(options.pad_to_square);
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ThumbnailSize {
|
||||
Small,
|
||||
Large,
|
||||
Native,
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<Option<u32>> for ThumbnailSize {
|
||||
fn into(self) -> Option<u32> {
|
||||
match self {
|
||||
Self::Small => Some(400),
|
||||
Self::Large => Some(1200),
|
||||
Self::Native => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ListPlaylistsEntry {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SavePlaylistInput {
|
||||
pub tracks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLink {
|
||||
pub auth_token: String, // user::AuthToken emitted by Polaris, valid for LastFMLink scope
|
||||
pub token: String, // LastFM token for use in scrobble calls
|
||||
pub content: String, // Payload to send back to client after successful link
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLinkToken {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl From<user::User> for User {
|
||||
fn from(u: user::User) -> Self {
|
||||
Self {
|
||||
name: u.name,
|
||||
is_admin: u.admin != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewUser {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl From<NewUser> for user::NewUser {
|
||||
fn from(u: NewUser) -> Self {
|
||||
Self {
|
||||
name: u.name,
|
||||
password: u.password,
|
||||
admin: u.admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserUpdate {
|
||||
pub new_password: Option<String>,
|
||||
pub new_is_admin: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct DDNSConfig {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl From<DDNSConfig> for ddns::Config {
|
||||
fn from(c: DDNSConfig) -> Self {
|
||||
Self {
|
||||
ddns_host: c.host,
|
||||
ddns_username: c.username,
|
||||
ddns_password: c.password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ddns::Config> for DDNSConfig {
|
||||
fn from(c: ddns::Config) -> Self {
|
||||
Self {
|
||||
host: c.ddns_host,
|
||||
username: c.ddns_username,
|
||||
password: c.ddns_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct MountDir {
|
||||
pub source: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<MountDir> for vfs::MountDir {
|
||||
fn from(m: MountDir) -> Self {
|
||||
Self {
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<vfs::MountDir> for MountDir {
|
||||
fn from(m: vfs::MountDir) -> Self {
|
||||
Self {
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub settings: Option<NewSettings>,
|
||||
pub users: Option<Vec<NewUser>>,
|
||||
pub mount_dirs: Option<Vec<MountDir>>,
|
||||
pub ydns: Option<DDNSConfig>,
|
||||
}
|
||||
|
||||
impl From<Config> for config::Config {
|
||||
fn from(s: Config) -> Self {
|
||||
Self {
|
||||
settings: s.settings.map(|s| s.into()),
|
||||
mount_dirs: s
|
||||
.mount_dirs
|
||||
.map(|v| v.into_iter().map(|m| m.into()).collect()),
|
||||
users: s.users.map(|v| v.into_iter().map(|u| u.into()).collect()),
|
||||
ydns: s.ydns.map(|c| c.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewSettings {
|
||||
pub album_art_pattern: Option<String>,
|
||||
pub reindex_every_n_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<NewSettings> for settings::NewSettings {
|
||||
fn from(s: NewSettings) -> Self {
|
||||
Self {
|
||||
album_art_pattern: s.album_art_pattern,
|
||||
reindex_every_n_seconds: s.reindex_every_n_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub album_art_pattern: String,
|
||||
pub reindex_every_n_seconds: i64,
|
||||
}
|
||||
|
||||
impl From<settings::Settings> for Settings {
|
||||
fn from(s: settings::Settings) -> Self {
|
||||
Self {
|
||||
album_art_pattern: s.index_album_art_pattern,
|
||||
reindex_every_n_seconds: s.index_sleep_duration_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CollectionFile {
|
||||
Directory(Directory),
|
||||
Song(Song),
|
||||
}
|
||||
|
||||
impl From<index::CollectionFile> for CollectionFile {
|
||||
fn from(f: index::CollectionFile) -> Self {
|
||||
match f {
|
||||
index::CollectionFile::Directory(d) => Self::Directory(d.into()),
|
||||
index::CollectionFile::Song(s) => Self::Song(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Song {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub track_number: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disc_number: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub artists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub album_artists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub year: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub album: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artwork: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub lyricists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub composers: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub genres: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub labels: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<scanner::Song> for Song {
|
||||
fn from(s: scanner::Song) -> Self {
|
||||
Self {
|
||||
path: s.path,
|
||||
track_number: s.track_number,
|
||||
disc_number: s.disc_number,
|
||||
title: s.title,
|
||||
artists: s.artists.0,
|
||||
album_artists: s.album_artists.0,
|
||||
year: s.year,
|
||||
album: s.album,
|
||||
artwork: s.artwork,
|
||||
duration: s.duration,
|
||||
lyricists: s.lyricists.0,
|
||||
composers: s.composers.0,
|
||||
genres: s.genres.0,
|
||||
labels: s.labels.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Directory {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub artists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub year: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub album: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artwork: Option<String>,
|
||||
pub date_added: i64,
|
||||
}
|
||||
|
||||
impl From<scanner::Directory> for Directory {
|
||||
fn from(d: scanner::Directory) -> Self {
|
||||
Self {
|
||||
path: d.path,
|
||||
artists: d.artists.0,
|
||||
year: d.year,
|
||||
album: d.album,
|
||||
artwork: d.artwork,
|
||||
date_added: d.date_added,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Preferences, CollectionFile should have dto types
|
||||
// TODO Song dto type should skip `None` values when serializing, to lower payload sizes by a lot
|
||||
pub use v8::*;
|
||||
|
|
320
src/server/dto/v7.rs
Normal file
320
src/server/dto/v7.rs
Normal file
|
@ -0,0 +1,320 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::app::{
|
||||
config, ddns, index,
|
||||
scanner::{self, MultiString},
|
||||
settings, thumbnail, user, vfs,
|
||||
};
|
||||
use std::convert::From;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
pub major: i32,
|
||||
pub minor: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct InitialSetup {
|
||||
pub has_any_users: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Credentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Authorization {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AuthQueryParameters {
|
||||
pub auth_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThumbnailOptions {
|
||||
pub size: Option<ThumbnailSize>,
|
||||
pub pad: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ThumbnailOptions> for thumbnail::Options {
|
||||
fn from(dto: ThumbnailOptions) -> Self {
|
||||
let mut options = thumbnail::Options::default();
|
||||
options.max_dimension = dto.size.map_or(options.max_dimension, Into::into);
|
||||
options.pad_to_square = dto.pad.unwrap_or(options.pad_to_square);
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ThumbnailSize {
|
||||
Small,
|
||||
Large,
|
||||
Native,
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<Option<u32>> for ThumbnailSize {
|
||||
fn into(self) -> Option<u32> {
|
||||
match self {
|
||||
Self::Small => Some(400),
|
||||
Self::Large => Some(1200),
|
||||
Self::Native => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ListPlaylistsEntry {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SavePlaylistInput {
|
||||
pub tracks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLink {
|
||||
pub auth_token: String, // user::AuthToken emitted by Polaris, valid for LastFMLink scope
|
||||
pub token: String, // LastFM token for use in scrobble calls
|
||||
pub content: String, // Payload to send back to client after successful link
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLinkToken {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl From<user::User> for User {
|
||||
fn from(u: user::User) -> Self {
|
||||
Self {
|
||||
name: u.name,
|
||||
is_admin: u.admin != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewUser {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl From<NewUser> for user::NewUser {
|
||||
fn from(u: NewUser) -> Self {
|
||||
Self {
|
||||
name: u.name,
|
||||
password: u.password,
|
||||
admin: u.admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserUpdate {
|
||||
pub new_password: Option<String>,
|
||||
pub new_is_admin: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct DDNSConfig {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl From<DDNSConfig> for ddns::Config {
|
||||
fn from(c: DDNSConfig) -> Self {
|
||||
Self {
|
||||
ddns_host: c.host,
|
||||
ddns_username: c.username,
|
||||
ddns_password: c.password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ddns::Config> for DDNSConfig {
|
||||
fn from(c: ddns::Config) -> Self {
|
||||
Self {
|
||||
host: c.ddns_host,
|
||||
username: c.ddns_username,
|
||||
password: c.ddns_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct MountDir {
|
||||
pub source: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<MountDir> for vfs::MountDir {
|
||||
fn from(m: MountDir) -> Self {
|
||||
Self {
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<vfs::MountDir> for MountDir {
|
||||
fn from(m: vfs::MountDir) -> Self {
|
||||
Self {
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub settings: Option<NewSettings>,
|
||||
pub users: Option<Vec<NewUser>>,
|
||||
pub mount_dirs: Option<Vec<MountDir>>,
|
||||
pub ydns: Option<DDNSConfig>,
|
||||
}
|
||||
|
||||
impl From<Config> for config::Config {
|
||||
fn from(s: Config) -> Self {
|
||||
Self {
|
||||
settings: s.settings.map(|s| s.into()),
|
||||
mount_dirs: s
|
||||
.mount_dirs
|
||||
.map(|v| v.into_iter().map(|m| m.into()).collect()),
|
||||
users: s.users.map(|v| v.into_iter().map(|u| u.into()).collect()),
|
||||
ydns: s.ydns.map(|c| c.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewSettings {
|
||||
pub album_art_pattern: Option<String>,
|
||||
pub reindex_every_n_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<NewSettings> for settings::NewSettings {
|
||||
fn from(s: NewSettings) -> Self {
|
||||
Self {
|
||||
album_art_pattern: s.album_art_pattern,
|
||||
reindex_every_n_seconds: s.reindex_every_n_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub album_art_pattern: String,
|
||||
pub reindex_every_n_seconds: i64,
|
||||
}
|
||||
|
||||
impl From<settings::Settings> for Settings {
|
||||
fn from(s: settings::Settings) -> Self {
|
||||
Self {
|
||||
album_art_pattern: s.index_album_art_pattern,
|
||||
reindex_every_n_seconds: s.index_sleep_duration_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CollectionFile {
|
||||
Directory(Directory),
|
||||
Song(Song),
|
||||
}
|
||||
|
||||
impl From<index::CollectionFile> for CollectionFile {
|
||||
fn from(f: index::CollectionFile) -> Self {
|
||||
match f {
|
||||
index::CollectionFile::Directory(d) => Self::Directory(d.into()),
|
||||
index::CollectionFile::Song(s) => Self::Song(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiString {
|
||||
fn to_v7_string(&self) -> Option<String> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.0.join(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Song {
|
||||
pub path: String,
|
||||
pub track_number: Option<i64>,
|
||||
pub disc_number: Option<i64>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
pub duration: Option<i64>,
|
||||
pub lyricist: Option<String>,
|
||||
pub composer: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
impl From<scanner::Song> for Song {
|
||||
fn from(s: scanner::Song) -> Self {
|
||||
Self {
|
||||
path: s.path,
|
||||
track_number: s.track_number,
|
||||
disc_number: s.disc_number,
|
||||
title: s.title,
|
||||
artist: s.artists.0.first().cloned(),
|
||||
album_artist: s.album_artists.to_v7_string(),
|
||||
year: s.year,
|
||||
album: s.album,
|
||||
artwork: s.artwork,
|
||||
duration: s.duration,
|
||||
lyricist: s.lyricists.to_v7_string(),
|
||||
composer: s.composers.to_v7_string(),
|
||||
genre: s.genres.to_v7_string(),
|
||||
label: s.labels.to_v7_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Directory {
|
||||
pub path: String,
|
||||
pub artist: Option<String>,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
pub date_added: i64,
|
||||
}
|
||||
|
||||
impl From<scanner::Directory> for Directory {
|
||||
fn from(d: scanner::Directory) -> Self {
|
||||
Self {
|
||||
path: d.path,
|
||||
artist: d.artists.to_v7_string(),
|
||||
year: d.year,
|
||||
album: d.album,
|
||||
artwork: d.artwork,
|
||||
date_added: d.date_added,
|
||||
}
|
||||
}
|
||||
}
|
325
src/server/dto/v8.rs
Normal file
325
src/server/dto/v8.rs
Normal file
|
@ -0,0 +1,325 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::app::{config, ddns, index, scanner, settings, thumbnail, user, vfs};
|
||||
use std::convert::From;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
pub major: i32,
|
||||
pub minor: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct InitialSetup {
|
||||
pub has_any_users: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Credentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Authorization {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AuthQueryParameters {
|
||||
pub auth_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThumbnailOptions {
|
||||
pub size: Option<ThumbnailSize>,
|
||||
pub pad: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ThumbnailOptions> for thumbnail::Options {
|
||||
fn from(dto: ThumbnailOptions) -> Self {
|
||||
let mut options = thumbnail::Options::default();
|
||||
options.max_dimension = dto.size.map_or(options.max_dimension, Into::into);
|
||||
options.pad_to_square = dto.pad.unwrap_or(options.pad_to_square);
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ThumbnailSize {
|
||||
Small,
|
||||
Large,
|
||||
Native,
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<Option<u32>> for ThumbnailSize {
|
||||
fn into(self) -> Option<u32> {
|
||||
match self {
|
||||
Self::Small => Some(400),
|
||||
Self::Large => Some(1200),
|
||||
Self::Native => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ListPlaylistsEntry {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SavePlaylistInput {
|
||||
pub tracks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLink {
|
||||
pub auth_token: String, // user::AuthToken emitted by Polaris, valid for LastFMLink scope
|
||||
pub token: String, // LastFM token for use in scrobble calls
|
||||
pub content: String, // Payload to send back to client after successful link
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLinkToken {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl From<user::User> for User {
|
||||
fn from(u: user::User) -> Self {
|
||||
Self {
|
||||
name: u.name,
|
||||
is_admin: u.admin != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewUser {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl From<NewUser> for user::NewUser {
|
||||
fn from(u: NewUser) -> Self {
|
||||
Self {
|
||||
name: u.name,
|
||||
password: u.password,
|
||||
admin: u.admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserUpdate {
|
||||
pub new_password: Option<String>,
|
||||
pub new_is_admin: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct DDNSConfig {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl From<DDNSConfig> for ddns::Config {
|
||||
fn from(c: DDNSConfig) -> Self {
|
||||
Self {
|
||||
ddns_host: c.host,
|
||||
ddns_username: c.username,
|
||||
ddns_password: c.password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ddns::Config> for DDNSConfig {
|
||||
fn from(c: ddns::Config) -> Self {
|
||||
Self {
|
||||
host: c.ddns_host,
|
||||
username: c.ddns_username,
|
||||
password: c.ddns_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct MountDir {
|
||||
pub source: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<MountDir> for vfs::MountDir {
|
||||
fn from(m: MountDir) -> Self {
|
||||
Self {
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<vfs::MountDir> for MountDir {
|
||||
fn from(m: vfs::MountDir) -> Self {
|
||||
Self {
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub settings: Option<NewSettings>,
|
||||
pub users: Option<Vec<NewUser>>,
|
||||
pub mount_dirs: Option<Vec<MountDir>>,
|
||||
pub ydns: Option<DDNSConfig>,
|
||||
}
|
||||
|
||||
impl From<Config> for config::Config {
|
||||
fn from(s: Config) -> Self {
|
||||
Self {
|
||||
settings: s.settings.map(|s| s.into()),
|
||||
mount_dirs: s
|
||||
.mount_dirs
|
||||
.map(|v| v.into_iter().map(|m| m.into()).collect()),
|
||||
users: s.users.map(|v| v.into_iter().map(|u| u.into()).collect()),
|
||||
ydns: s.ydns.map(|c| c.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewSettings {
|
||||
pub album_art_pattern: Option<String>,
|
||||
pub reindex_every_n_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<NewSettings> for settings::NewSettings {
|
||||
fn from(s: NewSettings) -> Self {
|
||||
Self {
|
||||
album_art_pattern: s.album_art_pattern,
|
||||
reindex_every_n_seconds: s.reindex_every_n_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub album_art_pattern: String,
|
||||
pub reindex_every_n_seconds: i64,
|
||||
}
|
||||
|
||||
impl From<settings::Settings> for Settings {
|
||||
fn from(s: settings::Settings) -> Self {
|
||||
Self {
|
||||
album_art_pattern: s.index_album_art_pattern,
|
||||
reindex_every_n_seconds: s.index_sleep_duration_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CollectionFile {
|
||||
Directory(Directory),
|
||||
Song(Song),
|
||||
}
|
||||
|
||||
impl From<index::CollectionFile> for CollectionFile {
|
||||
fn from(f: index::CollectionFile) -> Self {
|
||||
match f {
|
||||
index::CollectionFile::Directory(d) => Self::Directory(d.into()),
|
||||
index::CollectionFile::Song(s) => Self::Song(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Song {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub track_number: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disc_number: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub artists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub album_artists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub year: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub album: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artwork: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub lyricists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub composers: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub genres: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub labels: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<scanner::Song> for Song {
|
||||
fn from(s: scanner::Song) -> Self {
|
||||
Self {
|
||||
path: s.path,
|
||||
track_number: s.track_number,
|
||||
disc_number: s.disc_number,
|
||||
title: s.title,
|
||||
artists: s.artists.0,
|
||||
album_artists: s.album_artists.0,
|
||||
year: s.year,
|
||||
album: s.album,
|
||||
artwork: s.artwork,
|
||||
duration: s.duration,
|
||||
lyricists: s.lyricists.0,
|
||||
composers: s.composers.0,
|
||||
genres: s.genres.0,
|
||||
labels: s.labels.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Directory {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub artists: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub year: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub album: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artwork: Option<String>,
|
||||
pub date_added: i64,
|
||||
}
|
||||
|
||||
impl From<scanner::Directory> for Directory {
|
||||
fn from(d: scanner::Directory) -> Self {
|
||||
Self {
|
||||
path: d.path,
|
||||
artists: d.artists.0,
|
||||
year: d.year,
|
||||
album: d.album,
|
||||
artwork: d.artwork,
|
||||
date_added: d.date_added,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Preferencesshould have dto types
|
|
@ -6,6 +6,12 @@ use crate::db;
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum APIError {
|
||||
#[error("Could not read API version header")]
|
||||
InvalidAPIVersionHeader,
|
||||
#[error("Could not parse API version header")]
|
||||
APIVersionHeaderParseError,
|
||||
#[error("Unsupported API version")]
|
||||
UnsupportedAPIVersion,
|
||||
#[error("Could not encode authorization token")]
|
||||
AuthorizationTokenEncoding,
|
||||
#[error("Administrator permission is required")]
|
||||
|
|
Loading…
Add table
Reference in a new issue