API versioning

This commit is contained in:
Antoine Gersant 2024-07-27 18:06:19 -07:00
parent 6871f41a99
commit caa8907297
11 changed files with 907 additions and 384 deletions

View file

@ -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": []
}
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ use crate::app::{self, App};
mod api;
mod auth;
mod error;
mod version;
#[cfg(test)]
pub mod test;

View file

@ -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(

View file

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

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

View file

@ -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
View 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
View 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

View file

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