From 7a659ced4d71446255b7d9af358a75c57d089270 Mon Sep 17 00:00:00 2001 From: Lyssieth Date: Thu, 12 Oct 2023 19:18:36 +0300 Subject: [PATCH] feat: everything works again. now to break it again --- Cargo.lock | 85 +++++++++-- Justfile | 14 +- entities/src/album.rs | 2 +- entities/src/artist.rs | 2 +- entities/src/track.rs | 4 + migration/src/m000004_create_artist.rs | 5 +- migration/src/m000006_create_album.rs | 2 +- migration/src/m000007_create_track.rs | 22 +++ rave/Cargo.toml | 4 +- rave/src/main.rs | 6 +- rave/src/rest/get_album.rs | 76 +++++++++- rave/src/rest/get_album_list.rs | 119 +++++++++++---- rave/src/rest/stream.rs | 61 +++++++- rave/src/scan.rs | 164 +++----------------- rave/src/scan/flac.rs | 190 ++++++++++++++++++++++++ rave/src/scan/mp3.rs | 190 ++++++++++++++++++++++++ rave/src/scan/walk.rs | 24 ++- rave/src/subsonic/mod.rs | 15 +- rave/src/subsonic/types.rs | 2 +- rave/src/subsonic/types/album.rs | 76 +++++++--- rave/src/subsonic/types/child.rs | 86 +++++++++++ rave/src/subsonic/types/music_folder.rs | 24 +-- rave/src/subsonic/types/track.rs | 4 - 23 files changed, 910 insertions(+), 267 deletions(-) create mode 100644 rave/src/scan/flac.rs create mode 100644 rave/src/scan/mp3.rs create mode 100644 rave/src/subsonic/types/child.rs delete mode 100644 rave/src/subsonic/types/track.rs diff --git a/Cargo.lock b/Cargo.lock index 588e54a..e20fdb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,26 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "audiotags" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f47bdf1acc4fa4113f8aa72bb3bfe62a61443c61d0d997b723ba61ce102c79b" +dependencies = [ + "audiotags-dev-macro", + "id3", + "metaflac", + "mp4ameta", + "readme-rustdocifier", + "thiserror", +] + +[[package]] +name = "audiotags-dev-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79298591161f312f06327df7963063ee07466be303dcc3084a44ec293cb36e" + [[package]] name = "autocfg" version = "1.1.0" @@ -966,6 +986,7 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1371,7 +1392,6 @@ dependencies = [ "bitflags 2.4.0", "byteorder", "flate2", - "tokio", ] [[package]] @@ -1595,6 +1615,17 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "metaflac" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1470d3cc1bb0d692af5eb3afb594330b8ba09fd91c32c4e1c6322172a5ba750" +dependencies = [ + "byteorder", + "hex", + "log", +] + [[package]] name = "migration" version = "0.1.0" @@ -1645,6 +1676,22 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mp4ameta" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb23d62e8eb5299a3f79657c70ea9269eac8f6239a76952689bcd06a74057e81" +dependencies = [ + "lazy_static", + "mp4ameta_proc", +] + +[[package]] +name = "mp4ameta_proc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806" + [[package]] name = "multer" version = "2.1.0" @@ -1839,9 +1886,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "3.9.1" +version = "3.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" dependencies = [ "num-traits", ] @@ -2268,11 +2315,11 @@ dependencies = [ name = "rave" version = "0.1.0" dependencies = [ + "audiotags", "cfg-if", "color-eyre", "entities", - "futures-lite", - "id3", + "futures", "md5", "migration", "once_cell", @@ -2293,6 +2340,12 @@ dependencies = [ "url-escape", ] +[[package]] +name = "readme-rustdocifier" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac" + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2304,14 +2357,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.6" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.9", - "regex-syntax 0.7.5", + "regex-automata 0.4.1", + "regex-syntax 0.8.1", ] [[package]] @@ -2325,13 +2378,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.9" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.1", ] [[package]] @@ -2342,9 +2395,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" [[package]] name = "reqwest" @@ -2708,9 +2761,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "sentry" diff --git a/Justfile b/Justfile index eb5fdae..178c17d 100644 --- a/Justfile +++ b/Justfile @@ -14,4 +14,16 @@ run: mount refresh: sea migrate fresh - sea generate entity -o ./entities/src --with-serde both --date-time-crate time --lib \ No newline at end of file + sea generate entity -o ./entities/src --with-serde both --date-time-crate time --lib + +scan: + curl -vv "http://localhost:1234/rest/startScan?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0" | xq + +scanStatus: + curl -vv "http://localhost:1234/rest/getScanStatus?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0" | xq + +getAlbumList2: + curl -vv "http://localhost:1234/rest/getAlbumList2?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0&type=newest" | xq + +getAlbum ALBUM: + curl -vv "http://localhost:1234/rest/getAlbum?c=supersonic&f=xml&s=RTA3MKflcW&t=1058b65692a81c4aac17f5aff879891f&u=admin&v=1.8.0&id={{ ALBUM }}" | xq \ No newline at end of file diff --git a/entities/src/album.rs b/entities/src/album.rs index 5866c63..868c785 100644 --- a/entities/src/album.rs +++ b/entities/src/album.rs @@ -11,7 +11,7 @@ pub struct Model { pub name: String, pub artist_id: Option, pub cover_art_id: Option, - pub song_count: i32, + pub song_count: i64, pub duration: i64, pub play_count: i64, pub created: TimeDateTimeWithTimeZone, diff --git a/entities/src/artist.rs b/entities/src/artist.rs index 8c28ec5..0c78dc3 100644 --- a/entities/src/artist.rs +++ b/entities/src/artist.rs @@ -12,7 +12,7 @@ pub struct Model { pub cover_art_id: Option, pub artist_image_url: Option, pub album_count: i32, - pub starred: bool, + pub starred: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entities/src/track.rs b/entities/src/track.rs index f76c9e1..96e9c6f 100644 --- a/entities/src/track.rs +++ b/entities/src/track.rs @@ -14,9 +14,13 @@ pub struct Model { pub is_dir: bool, pub cover_art_id: Option, pub created: TimeDateTimeWithTimeZone, + pub starred: Option, pub duration: i64, pub bit_rate: Option, + pub track_number: Option, pub size: i64, + pub play_count: i64, + pub disc_number: i32, pub suffix: String, pub content_type: String, pub is_video: bool, diff --git a/migration/src/m000004_create_artist.rs b/migration/src/m000004_create_artist.rs index 415f43b..9e3b028 100644 --- a/migration/src/m000004_create_artist.rs +++ b/migration/src/m000004_create_artist.rs @@ -32,9 +32,8 @@ impl MigrationTrait for Migration { ) .col( ColumnDef::new(Artist::Starred) - .boolean() - .not_null() - .default(false), + .timestamp_with_time_zone() + .null(), ) .to_owned(), ) diff --git a/migration/src/m000006_create_album.rs b/migration/src/m000006_create_album.rs index 2010440..464062e 100644 --- a/migration/src/m000006_create_album.rs +++ b/migration/src/m000006_create_album.rs @@ -26,7 +26,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Album::Name).string().not_null()) .col(ColumnDef::new(Album::ArtistId).big_integer().null()) .col(ColumnDef::new(Album::CoverArtId).big_integer().null()) - .col(ColumnDef::new(Album::SongCount).integer().not_null()) + .col(ColumnDef::new(Album::SongCount).big_integer().not_null()) .col(ColumnDef::new(Album::Duration).big_integer().not_null()) .col( ColumnDef::new(Album::PlayCount) diff --git a/migration/src/m000007_create_track.rs b/migration/src/m000007_create_track.rs index e68ee57..f574c39 100644 --- a/migration/src/m000007_create_track.rs +++ b/migration/src/m000007_create_track.rs @@ -38,9 +38,27 @@ impl MigrationTrait for Migration { .timestamp_with_time_zone() .not_null(), ) + .col( + ColumnDef::new(Track::Starred) + .timestamp_with_time_zone() + .null(), + ) .col(ColumnDef::new(Track::Duration).big_integer().not_null()) .col(ColumnDef::new(Track::BitRate).big_integer().null()) + .col(ColumnDef::new(Track::TrackNumber).integer().null()) .col(ColumnDef::new(Track::Size).big_integer().not_null()) + .col( + ColumnDef::new(Track::PlayCount) + .big_integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Track::DiscNumber) + .integer() + .not_null() + .default(1), + ) .col(ColumnDef::new(Track::Suffix).string().not_null()) .col(ColumnDef::new(Track::ContentType).string().not_null()) .col( @@ -111,8 +129,12 @@ pub enum Track { AlbumId, ArtistId, IsDir, + TrackNumber, CoverArtId, Created, + Starred, + PlayCount, + DiscNumber, Duration, BitRate, Size, diff --git a/rave/Cargo.toml b/rave/Cargo.toml index 5606eea..8ce0c3a 100644 --- a/rave/Cargo.toml +++ b/rave/Cargo.toml @@ -39,8 +39,8 @@ sea-orm = { workspace = true } entities = { workspace = true } migration = { workspace = true } once_cell = { version = "1.18.0", features = ["parking_lot"] } -futures-lite = "1.13.0" -id3 = { version = "1.8.0", features = ["tokio"] } +futures = "0.3" +audiotags = "0.4.1" tracing-appender = "0.2.2" sentry = { version = "0.31.7", default-features = false, features = [ "backtrace", diff --git a/rave/src/main.rs b/rave/src/main.rs index 001953d..df16b68 100644 --- a/rave/src/main.rs +++ b/rave/src/main.rs @@ -1,6 +1,10 @@ #![warn(clippy::pedantic, clippy::nursery)] #![deny(clippy::unwrap_used)] -#![allow(clippy::module_name_repetitions, clippy::too_many_lines)] +#![allow( + clippy::module_name_repetitions, + clippy::too_many_lines, + clippy::cast_possible_truncation +)] use std::time::Duration; diff --git a/rave/src/rest/get_album.rs b/rave/src/rest/get_album.rs index d2a0526..e964a71 100644 --- a/rave/src/rest/get_album.rs +++ b/rave/src/rest/get_album.rs @@ -1,15 +1,15 @@ use crate::{ authentication::Authentication, - subsonic::{Error, SubsonicResponse}, + subsonic::{Album as AlbumId3, Child, Error, SubsonicResponse}, utils, }; -use entities::prelude::{Album, Track}; +use entities::prelude::{Album, Artist, Genre, Track}; use poem::web::{Data, Query}; use poem_ext::db::DbTxn; use sea_orm::{EntityTrait, ModelTrait}; use serde::Deserialize; -use tracing::{error, instrument, warn}; +use tracing::{error, instrument}; #[poem::handler] #[instrument(skip(txn, auth))] @@ -25,10 +25,27 @@ pub async fn get_album( Err(e) => return e, } - let album = Album::find_by_id(params.id).one(&**txn).await; + let album_id = match params.id.strip_prefix("al-").ok_or_else(|| { + Error::RequiredParameterMissing(Some("Album IDs must be formatted as `al-{}`".to_string())) + }) { + Ok(id) => id, + Err(e) => return SubsonicResponse::new_error(e), + }; + let album_id = match album_id.parse::() { + Ok(id) => id, + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error parsing album ID: {e}" + ); + return SubsonicResponse::new_error(Error::Generic(None)); + } + }; + + let album = Album::find_by_id(album_id).one(&**txn).await; let Ok(Some(album)) = album else { match album { - Ok(Some(_)) => unreachable!("Some(album) covered by `let .. else`"), + Ok(Some(_)) => unreachable!("Ok(Some(_)) covered by `let .. else`"), Ok(None) => return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)), Err(e) => { error!( @@ -40,6 +57,43 @@ pub async fn get_album( } }; + let artist = album.find_related(Artist).one(&**txn).await; + let Ok(artist) = artist else { + match artist { + Ok(_) => unreachable!("Ok(_) covered by `let .. else`"), + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error getting artist: {e}" + ); + return SubsonicResponse::new_error(Error::Generic(None)); + } + } + }; + + let genre = if let Some(genre_ids) = &album.genre_ids { + if genre_ids.is_empty() { + None + } else { + let genre = Genre::find_by_id(genre_ids[0]).one(&**txn).await; + + match genre { + Ok(Some(genre)) => Some(genre), + Ok(None) => None, + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error getting genre: {e}" + ); + + None + } + } + } + } else { + None + }; + let tracks = album.find_related(Track).all(&**txn).await; let tracks = match tracks { @@ -53,11 +107,17 @@ pub async fn get_album( } }; - todo!("finish implementing get_album") - // SubsonicResponse::new_album(album, tracks) + let album = AlbumId3::new(album, artist, genre); + let mut tracks = tracks + .into_iter() + .map(|t| Child::new(&t, &album)) + .collect::>(); + tracks.sort_by_cached_key(|tr| tr.track.unwrap_or_default()); + + SubsonicResponse::new_album(album, tracks) } #[derive(Debug, Clone, Deserialize)] pub struct GetAlbumParams { - pub id: i32, + pub id: String, } diff --git a/rave/src/rest/get_album_list.rs b/rave/src/rest/get_album_list.rs index 7240bf1..5b4f72e 100644 --- a/rave/src/rest/get_album_list.rs +++ b/rave/src/rest/get_album_list.rs @@ -9,14 +9,14 @@ use poem::{ Request, }; use poem_ext::db::DbTxn; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; +use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect}; use serde::Deserialize; -use tracing::instrument; +use tracing::{error, instrument}; use crate::{ authentication::Authentication, random_types::SortType, - subsonic::{Error, SubsonicResponse}, + subsonic::{Album as AlbumId3, Error, SubsonicResponse}, utils::{self}, }; @@ -51,30 +51,96 @@ pub async fn get_album_list( }; let album_list = match params.r#type { - SortType::Random => get_album_list_random(txn, params).await, - SortType::Newest => get_album_list_newest(txn, params).await, - SortType::Highest => get_album_list_highest(txn, params).await, - SortType::Frequent => get_album_list_frequent(txn, params).await, - SortType::Recent => get_album_list_recent(txn, params).await, - SortType::AlphabeticalByName => get_album_list_alphabetical_by_name(txn, params).await, - SortType::AlphabeticalByArtist => get_album_list_alphabetical_by_artist(txn, params).await, - SortType::Starred => get_album_list_starred(txn, params).await, - SortType::ByYear => get_album_list_by_year(txn, params).await, - SortType::ByGenre => get_album_list_by_genre(txn, params).await, + SortType::Random => get_album_list_random(txn.clone(), params).await, + SortType::Newest => get_album_list_newest(txn.clone(), params).await, + SortType::Highest => get_album_list_highest(txn.clone(), params).await, + SortType::Frequent => get_album_list_frequent(txn.clone(), params).await, + SortType::Recent => get_album_list_recent(txn.clone(), params).await, + SortType::AlphabeticalByName => { + get_album_list_alphabetical_by_name(txn.clone(), params).await + } + SortType::AlphabeticalByArtist => { + get_album_list_alphabetical_by_artist(txn.clone(), params).await + } + SortType::Starred => get_album_list_starred(txn.clone(), params).await, + SortType::ByYear => get_album_list_by_year(txn.clone(), params).await, + SortType::ByGenre => get_album_list_by_genre(txn.clone(), params).await, }; - todo!() + let album_list = match album_list { + Ok(a) => albums_to_album_id3(txn, &a).await, + Err(e) => return SubsonicResponse::new_error(e), + }; - // match album_list { - // Ok(a) => { - // if req.uri().path().contains("getAlbumList2") { - // SubsonicResponse::new_album_list2(a) - // } else { - // SubsonicResponse::new_album_list(a) - // } - // } - // Err(e) => SubsonicResponse::new_error(e), - // } + match album_list { + Ok(a) => { + if req.uri().path().contains("getAlbumList2") { + SubsonicResponse::new_album_list2(a) + } else { + SubsonicResponse::new_album_list(a) + } + } + Err(e) => SubsonicResponse::new_error(e), + } +} + +async fn albums_to_album_id3(conn: DbTxn, albums: &[album::Model]) -> Result, Error> { + let mut album_id3_futures = Vec::with_capacity(albums.len()); + + for album in albums { + album_id3_futures.push(album_to_album_id3(conn.clone(), album)); + } + + let result = futures::future::join_all(album_id3_futures).await; + + let mut album_id3s = Vec::with_capacity(result.len()); + + for album in result { + album_id3s.push(album?); + } + + Ok(album_id3s) +} + +async fn album_to_album_id3(conn: DbTxn, album: &album::Model) -> Result { + let artist = album.find_related(Artist).one(&*conn).await; + let Ok(artist) = artist else { + match artist { + Ok(_) => unreachable!("Ok(_) covered by `let .. else`"), + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error getting artist: {e}" + ); + return Err(Error::Generic(None)); + } + } + }; + + let genre = if let Some(genre_ids) = &album.genre_ids { + if genre_ids.is_empty() { + None + } else { + let genre = Genre::find_by_id(genre_ids[0]).one(&*conn).await; + + match genre { + Ok(Some(genre)) => Some(genre), + Ok(None) => None, + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error getting genre: {e}" + ); + + None + } + } + } + } else { + None + }; + + Ok(AlbumId3::new(album.clone(), artist, genre)) } #[instrument(skip(_conn, _params))] @@ -138,6 +204,7 @@ async fn get_album_list_recent( params: GetAlbumListParams, ) -> Result, Error> { let albums = Album::find() + .filter(album::Column::Played.is_not_null()) .order_by_desc(album::Column::Played) .limit(params.size) .offset(params.offset) @@ -153,7 +220,7 @@ async fn get_album_list_alphabetical_by_name( params: GetAlbumListParams, ) -> Result, Error> { let albums = Album::find() - .order_by_desc(album::Column::Name) + .order_by_asc(album::Column::Name) .limit(params.size) .offset(params.offset) .all(&*conn) @@ -170,7 +237,7 @@ async fn get_album_list_alphabetical_by_artist( let albums = Album::find() .filter(album::Column::ArtistId.is_not_null()) .find_also_related(Artist) - .order_by_desc(artist::Column::Name) + .order_by_asc(artist::Column::Name) .limit(params.size) .offset(params.offset) .all(&*conn) diff --git a/rave/src/rest/stream.rs b/rave/src/rest/stream.rs index cb9d413..d49c1a5 100644 --- a/rave/src/rest/stream.rs +++ b/rave/src/rest/stream.rs @@ -1,25 +1,26 @@ +use entities::prelude::Track; use poem::{ http::StatusCode, web::{Data, Query}, IntoResponse, Response, }; use poem_ext::db::DbTxn; +use sea_orm::EntityTrait; use serde::Deserialize; -use tracing::instrument; +use tracing::{error, instrument}; use crate::{ authentication::Authentication, + subsonic::{Error, SubsonicResponse}, utils::{self}, }; -const SONG: &[u8] = include_bytes!("../../../../data.mp3"); - #[poem::handler] #[instrument(skip(txn, auth))] pub async fn stream( Data(txn): Data<&DbTxn>, auth: Authentication, - Query(_params): Query, + Query(params): Query, ) -> Response { let u = utils::verify_user(txn.clone(), auth).await; @@ -27,16 +28,64 @@ pub async fn stream( Ok(_) => {} Err(e) => return e.into_response(), } + let Some(id) = params.id.strip_prefix("tr-") else { + return SubsonicResponse::new_error(Error::RequiredParameterMissing(Some( + "Track IDs must be formatted as `tr-{}`".to_string(), + ))) + .into_response(); + }; + let id = match id.parse::() { + Ok(id) => id, + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error parsing track ID: {e}" + ); + return SubsonicResponse::new_error(Error::Generic(None)).into_response(); + } + }; + + let track = Track::find_by_id(id).one(&**txn).await; + + let track = match track { + Ok(Some(track)) => track, + Ok(None) => { + return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)) + .into_response() + } + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error fetching track: {e}" + ); + return SubsonicResponse::new_error(Error::Generic(None)).into_response(); + } + }; + + let path = track.path; + + let song = tokio::fs::read(path).await; + + let song = match song { + Ok(song) => song, + Err(e) => { + error!( + error = &e as &dyn std::error::Error, + "Error reading track: {e}" + ); + return SubsonicResponse::new_error(Error::Generic(None)).into_response(); + } + }; poem::Response::builder() .status(StatusCode::OK) .header("Content-Type", "audio/mpeg") - .body(SONG) + .body(song) } #[derive(Debug, Clone, Deserialize, Default)] pub struct StreamParams { - pub id: i32, + pub id: String, #[serde(rename = "maxBitRate", default)] pub max_bit_rate: Option, #[serde(default)] diff --git a/rave/src/scan.rs b/rave/src/scan.rs index be42f05..4df0a86 100644 --- a/rave/src/scan.rs +++ b/rave/src/scan.rs @@ -1,24 +1,20 @@ use color_eyre::{Report, Result}; use entities::{ - album, artist, genre, music_folder, - prelude::{Album, Artist, Genre, MusicFolder, Track}, - track, + genre, music_folder, + prelude::{Genre, MusicFolder}, }; -use futures_lite::StreamExt; -use id3::{Tag, TagLike}; +use futures::StreamExt; use once_cell::sync::Lazy; use sea_orm::{ - ActiveModelBehavior, ColumnTrait, ConnectOptions, Database, DatabaseTransaction, EntityTrait, - QueryFilter, Set, TransactionTrait, + ColumnTrait, ConnectOptions, Database, DatabaseTransaction, EntityTrait, QueryFilter, Set, + TransactionTrait, }; use std::{ - convert::Into, ops::ControlFlow, path::{Path, PathBuf}, sync::Arc, }; -use time::OffsetDateTime; -use tokio::{fs::File, sync::RwLock}; +use tokio::sync::RwLock; use tracing::{debug, error, info, instrument}; mod walk; @@ -88,6 +84,10 @@ async fn scan() { do_entry(de, txn, state.clone()).await; // test without multithreading } + // TODO: Go over everything and consolidate duplicates? + // This is a bit of a pain, but it's probably worth it + // Since if we want to use multithreading, we need to do this + { let mut stat = STATUS.write().await; @@ -95,7 +95,7 @@ async fn scan() { } } -const VALID_EXTENSIONS: &[&str] = &["mp3"]; +const VALID_EXTENSIONS: &[&str] = &["mp3", "flac"]; async fn do_entry(de: walk::DirEntry, txn: DatabaseTransaction, state: Arc>) { if let Err(e) = handle_entry(&txn, de.clone(), state).await { @@ -175,99 +175,22 @@ async fn handle_entry( return Ok(()); }; - let stem = stem.to_string_lossy(); + let track = match ext.as_ref() { + "mp3" => mp3::handle(tx, path.clone(), stem.to_string_lossy(), state.clone()).await?, + "flac" => flac::handle(tx, path.clone(), stem.to_string_lossy(), state.clone()).await?, - let meta = { File::open(&path).await?.metadata().await? }; + _ => unreachable!("We already checked the extension"), + }; - let tag = Tag::async_read_from_path(&path).await?; - - let artist = find_artist(tx, &tag).await?; - - let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?; - - let mut am = track::ActiveModel::new(); - - am.title = Set(tag.title().unwrap_or(&stem).to_string()); - am.album_id = Set(Some(album.id)); - am.artist_id = Set(artist.as_ref().map(|c| c.id)); - am.content_type = Set("audio/mpeg".to_string()); - am.suffix = Set("mp3".to_string()); - am.path = Set(path.to_string_lossy().to_string()); - am.is_dir = Set(false); - am.is_video = Set(false); - am.duration = Set(tag.duration().map(Into::into).unwrap_or_default()); - am.created = Set(OffsetDateTime::now_utc()); - am.size = Set(meta - .len() - .try_into() - .expect("Failed to convert meta len to i64")); - - let model = Track::insert(am).exec_with_returning(tx).await?; - - debug!("Inserted track {:?}", model.id); + debug!("Inserted track {:?}", track.id); // TODO: figure out how to scan. steal from Gonic if we have to :3 Ok(()) } -#[instrument(skip(tx, tag, state))] -async fn find_album( - tx: &DatabaseTransaction, - artist_id: Option, - tag: &Tag, - state: Arc>, -) -> Result { - let Some(album_name) = tag.album() else { - return Err(Report::msg("Couldn't get album name from tag")); - }; - - // if not, search by name - let search = Album::find() - .filter(album::Column::Name.like(album_name)) - .one(tx) - .await?; - - // if we found one, return it - if let Some(search) = search { - return Ok(search); - } - - // otherwise, create it - let mut am = album::ActiveModel::new(); - - am.name = Set(album_name.to_string()); - am.music_folder_id = Set(state.read().await.music_folder_id); - am.artist_id = Set(artist_id); - am.year = Set(tag.year()); - - let genre = tag.genre_parsed(); - if let Some(genre) = genre { - let genre_id = find_or_create_genre(tx, genre.as_ref()).await?; - am.genre_ids = Set(Some(vec![genre_id])); - } - am.song_count = Set(tag - .total_tracks() - .map(|v| i32::try_from(v).expect("Failed to convert total tracks to i32")) - .unwrap_or_default()); - am.duration = Set(tag.duration().map(Into::into).unwrap_or_default()); - am.created = Set(OffsetDateTime::now_utc()); - - let model = Album::insert(am).exec_with_returning(tx).await; - - // if we failed to insert, return the error - let Ok(model) = model else { - let err = model.expect_err("somehow not err"); - error!( - error = &err as &dyn std::error::Error, - "Failed to insert album {err}" - ); - - return Err(Report::new(err)); - }; - - Ok(model) -} +mod flac; +mod mp3; #[instrument(skip(tx))] async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result { @@ -305,55 +228,8 @@ async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result Result, Report> { - let artist_to_search = match (tag.album_artist(), tag.artists()) { - (Some(tag_artist), None) => Some(tag_artist.to_string()), - (None, Some(tag_artists)) => Some(tag_artists.join(", ")), - (Some(tag_artist), Some(tag_artists)) => { - let mut artists = tag_artists.clone(); - artists.push(tag_artist); - Some(artists.join(", ")) - } - _ => None, - }; - - match &artist_to_search { - Some(artist_to_search) => { - let artist_to_search = artist_to_search.trim(); - let attempt = Artist::find() - .filter(artist::Column::Name.like(artist_to_search)) - .one(tx) - .await?; - - if let Some(attempt) = attempt { - Ok(Some(attempt)) - } else { - let am = artist::ActiveModel { - name: Set(artist_to_search.to_string()), - ..Default::default() - }; - let model = Artist::insert(am).exec_with_returning(tx).await; - - let Ok(model) = model else { - let err = model.expect_err("somehow not err"); - error!( - error = &err as &dyn std::error::Error, - "Failed to insert artist {err}" - ); - - return Err(Report::new(err)); - }; - - Ok(Some(model)) - } - } - None => Ok(None), - } -} - #[derive(Debug, Default)] -struct ScanState { +pub struct ScanState { pub music_folder_id: i64, } diff --git a/rave/src/scan/flac.rs b/rave/src/scan/flac.rs new file mode 100644 index 0000000..d2c3b0b --- /dev/null +++ b/rave/src/scan/flac.rs @@ -0,0 +1,190 @@ +use std::borrow::Cow; +use std::path::PathBuf; +use std::sync::Arc; + +use audiotags::{AudioTagEdit, FlacTag, Tag}; +use color_eyre::Report; +use entities::{ + album, artist, + prelude::{Album, Artist, Track}, + track, +}; +use sea_orm::{ + ActiveModelBehavior, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, Set, +}; +use time::OffsetDateTime; +use tokio::{fs::File, sync::RwLock}; +use tracing::{debug, error, instrument}; + +use super::ScanState; + +#[instrument(skip(tx, state))] +pub async fn handle( + tx: &DatabaseTransaction, + path: PathBuf, + stem: Cow<'_, str>, + state: Arc>, +) -> Result { + let meta = { File::open(&path).await?.metadata().await? }; + + let tag = { + let tag = Tag::default().read_from_path(&path)?; + + FlacTag::from(tag) + }; + + let artist = find_artist(tx, &tag).await?; + + let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?; + + let mut am = track::ActiveModel::new(); + + am.title = Set(tag.title().unwrap_or(&stem).to_string()); + am.album_id = Set(Some(album.id)); + am.artist_id = Set(artist.as_ref().map(|c| c.id)); + am.content_type = Set("audio/mpeg".to_string()); + am.suffix = Set("mp3".to_string()); + am.path = Set(path.to_string_lossy().to_string()); + am.is_dir = Set(false); + am.track_number = Set(tag + .track_number() + .map(|v| i32::try_from(v).expect("failed to convert track to i32"))); + if let Some(disc_number) = tag + .disc_number() + .map(|v| i32::try_from(v).expect("failed to convert disc to i32")) + { + am.disc_number = Set(disc_number); + } + am.is_video = Set(false); + am.duration = { + let duration = tag.duration(); + + let duration = duration.unwrap_or_default(); + + debug!("Duration: {duration}"); + let duration = duration.round().rem_euclid(2f64.powi(32)) as i64; + debug!("math'd duration: {duration}"); + + Set(duration) + }; + am.created = Set(OffsetDateTime::now_utc()); + am.size = Set(meta + .len() + .try_into() + .expect("Failed to convert meta len to i64")); + + let model = Track::insert(am).exec_with_returning(tx).await?; + + Ok(model) +} + +#[instrument(skip(tx, tag, state))] +async fn find_album( + tx: &DatabaseTransaction, + artist_id: Option, + tag: &FlacTag, + state: Arc>, +) -> Result { + let Some(album) = tag.album() else { + return Err(Report::msg("Couldn't get album name from tag")); + }; + + // if not, search by name + let search = Album::find() + .filter(album::Column::Name.like(album.title)) + .one(tx) + .await?; + + // if we found one, return it + if let Some(search) = search { + return Ok(search); + } + + // otherwise, create it + let mut am = album::ActiveModel::new(); + + am.name = Set(album.title.to_string()); + am.music_folder_id = Set(state.read().await.music_folder_id); + am.artist_id = Set(artist_id); + am.year = Set(tag.year()); + + let genre = tag.genre(); + if let Some(genre) = genre { + let genre_id = super::find_or_create_genre(tx, genre).await?; + am.genre_ids = Set(Some(vec![genre_id])); + } + am.song_count = Set(tag + .total_tracks() + .map(|v| i64::try_from(v).expect("Failed to convert total tracks to i32")) + .unwrap_or_default()); + am.duration = Set(tag + .duration() + .map(|c| c.round().rem_euclid(2f64.powi(32)) as i64) // TODO: figure out how to do this properly + .unwrap_or_default()); + am.created = Set(OffsetDateTime::now_utc()); + + let model = Album::insert(am).exec_with_returning(tx).await; + + // if we failed to insert, return the error + let Ok(model) = model else { + let err = model.expect_err("somehow not err"); + error!( + error = &err as &dyn std::error::Error, + "Failed to insert album {err}" + ); + + return Err(Report::new(err)); + }; + + Ok(model) +} + +#[instrument(skip(tx, tag))] +async fn find_artist( + tx: &DatabaseTransaction, + tag: &FlacTag, +) -> Result, Report> { + let artist_to_search = match (tag.album_artist(), tag.artists()) { + (Some(tag_artist), None) => Some(tag_artist.to_string()), + (None, Some(tag_artists)) => Some(tag_artists.join(", ")), + (Some(tag_artist), Some(tag_artists)) => { + let mut artists = tag_artists.clone(); + artists.push(tag_artist); + Some(artists.join(", ")) + } + _ => None, + }; + + match &artist_to_search { + Some(artist_to_search) => { + let artist_to_search = artist_to_search.trim(); + let attempt = Artist::find() + .filter(artist::Column::Name.like(artist_to_search)) + .one(tx) + .await?; + + if let Some(attempt) = attempt { + Ok(Some(attempt)) + } else { + let am = artist::ActiveModel { + name: Set(artist_to_search.to_string()), + ..Default::default() + }; + let model = Artist::insert(am).exec_with_returning(tx).await; + + let Ok(model) = model else { + let err = model.expect_err("somehow not err"); + error!( + error = &err as &dyn std::error::Error, + "Failed to insert artist {err}" + ); + + return Err(Report::new(err)); + }; + + Ok(Some(model)) + } + } + None => Ok(None), + } +} diff --git a/rave/src/scan/mp3.rs b/rave/src/scan/mp3.rs new file mode 100644 index 0000000..353ed57 --- /dev/null +++ b/rave/src/scan/mp3.rs @@ -0,0 +1,190 @@ +use std::borrow::Cow; +use std::path::PathBuf; +use std::sync::Arc; + +use audiotags::{AudioTagEdit, Id3v2Tag, Tag}; +use color_eyre::Report; +use entities::{ + album, artist, + prelude::{Album, Artist, Track}, + track, +}; +use sea_orm::{ + ActiveModelBehavior, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, Set, +}; +use time::OffsetDateTime; +use tokio::{fs::File, sync::RwLock}; +use tracing::{debug, error, instrument}; + +use super::ScanState; + +#[instrument(skip(tx, state))] +pub async fn handle( + tx: &DatabaseTransaction, + path: PathBuf, + stem: Cow<'_, str>, + state: Arc>, +) -> Result { + let meta = { File::open(&path).await?.metadata().await? }; + + let tag = { + let tag = Tag::default().read_from_path(&path)?; + + audiotags::Id3v2Tag::from(tag) + }; + + let artist = find_artist(tx, &tag).await?; + + let album = find_album(tx, artist.as_ref().map(|c| c.id), &tag, state.clone()).await?; + + let mut am = track::ActiveModel::new(); + + am.title = Set(tag.title().unwrap_or(&stem).to_string()); + am.album_id = Set(Some(album.id)); + am.artist_id = Set(artist.as_ref().map(|c| c.id)); + am.content_type = Set("audio/mpeg".to_string()); + am.suffix = Set("mp3".to_string()); + am.path = Set(path.to_string_lossy().to_string()); + am.is_dir = Set(false); + am.track_number = Set(tag + .track_number() + .map(|v| i32::try_from(v).expect("failed to convert track to i32"))); + if let Some(disc_number) = tag + .disc_number() + .map(|v| i32::try_from(v).expect("failed to convert disc to i32")) + { + am.disc_number = Set(disc_number); + } + am.is_video = Set(false); + am.duration = { + let duration = tag.duration(); + + let duration = duration.unwrap_or_default(); + + debug!("Duration: {duration}"); + let duration = duration.round().rem_euclid(2f64.powi(32)) as i64; + debug!("math'd duration: {duration}"); + + Set(duration) + }; + am.created = Set(OffsetDateTime::now_utc()); + am.size = Set(meta + .len() + .try_into() + .expect("Failed to convert meta len to i64")); + + let model = Track::insert(am).exec_with_returning(tx).await?; + + Ok(model) +} + +#[instrument(skip(tx, tag, state))] +async fn find_album( + tx: &DatabaseTransaction, + artist_id: Option, + tag: &Id3v2Tag, + state: Arc>, +) -> Result { + let Some(album) = tag.album() else { + return Err(Report::msg("Couldn't get album name from tag")); + }; + + // if not, search by name + let search = Album::find() + .filter(album::Column::Name.like(album.title)) + .one(tx) + .await?; + + // if we found one, return it + if let Some(search) = search { + return Ok(search); + } + + // otherwise, create it + let mut am = album::ActiveModel::new(); + + am.name = Set(album.title.to_string()); + am.music_folder_id = Set(state.read().await.music_folder_id); + am.artist_id = Set(artist_id); + am.year = Set(tag.year()); + + let genre = tag.genre(); + if let Some(genre) = genre { + let genre_id = super::find_or_create_genre(tx, genre).await?; + am.genre_ids = Set(Some(vec![genre_id])); + } + am.song_count = Set(tag + .total_tracks() + .map(|v| i64::try_from(v).expect("Failed to convert total tracks to i32")) + .unwrap_or_default()); + am.duration = Set(tag + .duration() + .map(|c| c.round().rem_euclid(2f64.powi(32)) as i64) // TODO: figure out how to do this properly + .unwrap_or_default()); + am.created = Set(OffsetDateTime::now_utc()); + + let model = Album::insert(am).exec_with_returning(tx).await; + + // if we failed to insert, return the error + let Ok(model) = model else { + let err = model.expect_err("somehow not err"); + error!( + error = &err as &dyn std::error::Error, + "Failed to insert album {err}" + ); + + return Err(Report::new(err)); + }; + + Ok(model) +} + +#[instrument(skip(tx, tag))] +async fn find_artist( + tx: &DatabaseTransaction, + tag: &Id3v2Tag, +) -> Result, Report> { + let artist_to_search = match (tag.album_artist(), tag.artists()) { + (Some(tag_artist), None) => Some(tag_artist.to_string()), + (None, Some(tag_artists)) => Some(tag_artists.join(", ")), + (Some(tag_artist), Some(tag_artists)) => { + let mut artists = tag_artists.clone(); + artists.push(tag_artist); + Some(artists.join(", ")) + } + _ => None, + }; + + match &artist_to_search { + Some(artist_to_search) => { + let artist_to_search = artist_to_search.trim(); + let attempt = Artist::find() + .filter(artist::Column::Name.like(artist_to_search)) + .one(tx) + .await?; + + if let Some(attempt) = attempt { + Ok(Some(attempt)) + } else { + let am = artist::ActiveModel { + name: Set(artist_to_search.to_string()), + ..Default::default() + }; + let model = Artist::insert(am).exec_with_returning(tx).await; + + let Ok(model) = model else { + let err = model.expect_err("somehow not err"); + error!( + error = &err as &dyn std::error::Error, + "Failed to insert artist {err}" + ); + + return Err(Report::new(err)); + }; + + Ok(Some(model)) + } + } + None => Ok(None), + } +} diff --git a/rave/src/scan/walk.rs b/rave/src/scan/walk.rs index 307e2f0..4673b26 100644 --- a/rave/src/scan/walk.rs +++ b/rave/src/scan/walk.rs @@ -7,18 +7,20 @@ use std::{ task::{Context, Poll}, }; -use futures_lite::{future::Boxed as BoxedFut, stream, Future, FutureExt, Stream, StreamExt}; +use futures::{ + future::BoxFuture as BoxedFut, + stream::{self, BoxStream}, + Future, FutureExt, Stream, StreamExt, +}; use tokio::fs::{read_dir, ReadDir}; pub use tokio::io::Result; pub type DirEntry = Arc; -type BoxStream = futures_lite::stream::Boxed>; - pub struct WalkDir { root: PathBuf, - entries: BoxStream, + entries: BoxStream<'static, Result>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -34,7 +36,7 @@ impl WalkDir { root: root.as_ref().to_path_buf(), entries: walk_dir( root, - None:: BoxedFut + Send>>, + None:: BoxedFut<'static, Filtering> + Send>>, ), } } @@ -61,7 +63,10 @@ impl Stream for WalkDir { } } -fn walk_dir(root: impl AsRef, filter: Option) -> BoxStream +fn walk_dir( + root: impl AsRef, + filter: Option, +) -> BoxStream<'static, Result> where F: FnMut(DirEntry) -> Fut + Send + 'static, Fut: Future + Send, @@ -90,7 +95,10 @@ enum State { type UnfoldState = (Result, State); -fn walk(mut dirs: Vec, filter: Option) -> BoxedFut>> +fn walk( + mut dirs: Vec, + filter: Option, +) -> BoxedFut<'static, Option>> where F: FnMut(DirEntry) -> Fut + Send + 'static, Fut: Future + Send, @@ -116,7 +124,7 @@ fn walk_entry( entry: DirEntry, mut dirs: Vec, mut filter: Option, -) -> BoxedFut>> +) -> BoxedFut<'static, Option>> where F: FnMut(DirEntry) -> Fut + Send + 'static, Fut: Future + Send, diff --git a/rave/src/subsonic/mod.rs b/rave/src/subsonic/mod.rs index 0ea3002..6ba6155 100644 --- a/rave/src/subsonic/mod.rs +++ b/rave/src/subsonic/mod.rs @@ -8,8 +8,8 @@ mod error; mod types; pub use error::Error; pub use types::album::Album; +pub use types::child::Child; pub use types::music_folder::MusicFolder; -pub use types::track::Track; impl IntoResponse for SubsonicResponse { fn into_response(self) -> poem::Response { @@ -53,8 +53,11 @@ impl SubsonicResponse { Self::new(SubResponseType::AlbumList2 { albums }) } - pub fn new_album(album: Album, songs: Vec) -> Self { - Self::new(SubResponseType::Album { album, songs }) + pub fn new_album(album: Album, songs: Vec) -> Self { + Self::new(SubResponseType::Album { + album: Box::new(album), + songs, + }) } pub fn new_empty() -> Self { @@ -107,9 +110,9 @@ pub enum SubResponseType { #[serde(rename = "album")] Album { #[serde(flatten)] - album: Album, - #[serde(flatten)] - songs: Vec, + album: Box, + #[serde(rename = "song")] + songs: Vec, }, #[serde(rename = "scanStatus")] ScanStatus { diff --git a/rave/src/subsonic/types.rs b/rave/src/subsonic/types.rs index 696ae53..9c32382 100644 --- a/rave/src/subsonic/types.rs +++ b/rave/src/subsonic/types.rs @@ -1,3 +1,3 @@ pub mod album; +pub mod child; pub mod music_folder; -pub mod track; diff --git a/rave/src/subsonic/types/album.rs b/rave/src/subsonic/types/album.rs index a0e1401..fd537ca 100644 --- a/rave/src/subsonic/types/album.rs +++ b/rave/src/subsonic/types/album.rs @@ -1,27 +1,61 @@ -// use entities::*; +use entities::{album, artist, genre}; use serde::Serialize; +use time::format_description::well_known::Iso8601; #[derive(Debug, Clone, Serialize)] pub struct Album { - id: String, - name: String, - artist: Option, - #[serde(rename = "artistId")] - artist_id: Option, + #[serde(rename = "@id")] + pub id: String, + #[serde(rename = "@name")] + pub name: String, + #[serde(rename = "@artist", skip_serializing_if = "Option::is_none")] + pub artist: Option, + #[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")] + pub artist_id: Option, + #[serde(rename = "@coverArt", skip_serializing_if = "Option::is_none")] + pub cover_art_id: Option, + #[serde(rename = "@songCount")] + pub song_count: u64, + #[serde(rename = "@duration")] + pub duration: u64, + #[serde(rename = "@playCount")] + pub play_count: u64, + #[serde(rename = "@created")] + pub created: String, + #[serde(rename = "@starred", skip_serializing_if = "Option::is_none")] + pub starred: Option, + #[serde(rename = "@year", skip_serializing_if = "Option::is_none")] + pub year: Option, + #[serde(rename = "@genre", skip_serializing_if = "Option::is_none")] + pub genre: Option, } -// impl Album { -// pub fn new( -// album: album::Model, -// artists: Option, -// cover_art: Option, -// genres: Vec, -// ) -> Self { -// Self { -// id: album.id.to_string(), -// name: album.name, -// artist: artist.map(|a| a.name), -// artist_id: artist.map(|a| format!("ar-{}", a.id)), -// } -// } -// } +impl Album { + #[allow(clippy::cast_sign_loss)] + #[must_use] + pub fn new( + album: album::Model, + artist: Option, + genre: Option, + ) -> Self { + Self { + id: format!("al-{}", album.id), + name: album.name, + artist: artist.clone().map(|c| c.name), + artist_id: artist.map(|c| format!("ar-{}", c.id)), + cover_art_id: album.cover_art_id.map(|c| format!("ca-{c}")), + song_count: album.song_count as u64, + duration: album.duration as u64, + play_count: album.play_count as u64, + created: album + .created + .format(&Iso8601::DEFAULT) + .expect("Failed to format time"), + starred: album + .starred + .map(|c| c.format(&Iso8601::DEFAULT).expect("Failed to format time")), + year: album.year.map(|c| c as u32), + genre: genre.map(|c| c.name), + } + } +} diff --git a/rave/src/subsonic/types/child.rs b/rave/src/subsonic/types/child.rs new file mode 100644 index 0000000..7ec482c --- /dev/null +++ b/rave/src/subsonic/types/child.rs @@ -0,0 +1,86 @@ +use entities::track; +use serde::Serialize; +use time::format_description::well_known::Iso8601; + +use crate::subsonic::Album; + +#[derive(Debug, Clone, Serialize)] +pub struct Child { + #[serde(rename = "@id")] + pub id: String, + #[serde(rename = "@isDir")] + pub is_dir: bool, + #[serde(rename = "@title")] + pub title: String, + #[serde(rename = "@album", skip_serializing_if = "Option::is_none")] + pub album: Option, + #[serde(rename = "@track", skip_serializing_if = "Option::is_none")] + pub track: Option, + #[serde(rename = "@year", skip_serializing_if = "Option::is_none")] + pub year: Option, + #[serde(rename = "@coverArtId", skip_serializing_if = "Option::is_none")] + pub cover_art_id: Option, + #[serde(rename = "@size")] + pub size: u64, + #[serde(rename = "@contentType")] + pub content_type: String, + #[serde(rename = "@suffix")] + pub suffix: String, + #[serde(rename = "@starred", skip_serializing_if = "Option::is_none")] + pub starred: Option, + #[serde(rename = "@duration")] + pub duration: u64, + #[serde(rename = "@bitRate", skip_serializing_if = "Option::is_none")] + pub bit_rate: Option, + #[serde(rename = "@path")] + pub path: String, + #[serde(rename = "@playCount")] + pub play_count: u64, + #[serde(rename = "@discNumber")] + pub disc_number: u32, + #[serde(rename = "@created")] + pub created: String, + #[serde(rename = "@albumId", skip_serializing_if = "Option::is_none")] + pub album_id: Option, + #[serde(rename = "@artistId", skip_serializing_if = "Option::is_none")] + pub artist_id: Option, + #[serde(rename = "@type")] + pub child_type: String, + #[serde(rename = "@isVideo")] + pub is_video: bool, +} + +impl Child { + #[allow(clippy::cast_sign_loss)] + #[must_use] + pub fn new(track: &track::Model, album: &Album) -> Self { + Self { + id: format!("tr-{}", track.id), + is_dir: false, + title: track.title.clone(), + album: Some(album.name.clone()), + track: track.track_number.map(|c| c as u32), + year: album.year, + cover_art_id: track.cover_art_id.map(|c| format!("ca-{c}")), + size: track.size as u64, + content_type: track.content_type.clone(), + suffix: track.suffix.clone(), + starred: track + .starred + .map(|c| c.format(&Iso8601::DEFAULT).expect("Failed to format date")), + duration: track.duration as u64, + bit_rate: track.bit_rate.map(|c| c as u64), + path: track.path.clone(), + play_count: track.play_count as u64, + disc_number: track.disc_number as u32, + created: track + .created + .format(&Iso8601::DEFAULT) + .expect("Failed to format date"), + album_id: Some(format!("al-{}", album.id)), + artist_id: album.artist_id.clone(), + child_type: "music".to_string(), + is_video: false, + } + } +} diff --git a/rave/src/subsonic/types/music_folder.rs b/rave/src/subsonic/types/music_folder.rs index c37d02f..d74c171 100644 --- a/rave/src/subsonic/types/music_folder.rs +++ b/rave/src/subsonic/types/music_folder.rs @@ -1,29 +1,19 @@ use entities::music_folder::Model; -use serde::{ser::SerializeStruct, Serialize}; +use serde::Serialize; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct MusicFolder { - pub(crate) id: i64, - pub(crate) name: String, + #[serde(rename = "@id")] + pub id: String, + #[serde(rename = "@name")] + pub name: String, } impl From for MusicFolder { fn from(value: Model) -> Self { Self { - id: value.id, + id: format!("mf-{}", value.id), name: value.name, } } } - -impl Serialize for MusicFolder { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut s = serializer.serialize_struct("MusicFolder", 2)?; - s.serialize_field("id", &format!("mf-{}", self.id))?; - s.serialize_field("name", &self.name)?; - s.end() - } -} diff --git a/rave/src/subsonic/types/track.rs b/rave/src/subsonic/types/track.rs deleted file mode 100644 index 5b8ca7d..0000000 --- a/rave/src/subsonic/types/track.rs +++ /dev/null @@ -1,4 +0,0 @@ -use serde::Serialize; - -#[derive(Debug, Clone, Serialize)] -pub struct Track {}