diff --git a/.gitignore b/.gitignore index ef9c187..8a2ac5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target users.db* .en* + +.rave-dev-db diff --git a/Cargo.lock b/Cargo.lock index 7d50b94..491d351 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,17 +52,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.3" @@ -174,12 +163,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "async-attributes" version = "1.1.2" @@ -388,17 +371,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bigdecimal" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -414,18 +386,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -451,51 +411,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "borsh" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" -dependencies = [ - "borsh-derive", - "hashbrown 0.12.3", -] - -[[package]] -name = "borsh-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "brotli" version = "3.4.0" @@ -523,28 +438,6 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" -[[package]] -name = "bytecheck" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "byteorder" version = "1.5.0" @@ -581,7 +474,6 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "serde", "windows-targets", ] @@ -746,6 +638,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -1009,12 +911,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.28" @@ -1208,9 +1104,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] [[package]] name = "hashbrown" @@ -1218,7 +1111,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ - "ahash 0.8.3", + "ahash", "allocator-api2", ] @@ -1384,6 +1277,18 @@ dependencies = [ "cc", ] +[[package]] +name = "id3" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85caa20f41eef2391fd1dd42b015d0eba17171cdb1a162246a7cc03cd73ab1e" +dependencies = [ + "bitflags 2.4.0", + "byteorder", + "flate2", + "tokio", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1682,17 +1587,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-bigint" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1751,6 +1645,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.1" @@ -1969,7 +1872,7 @@ version = "1.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2550a0bce7273b278894ef3ccc5a6869e7031b6870042f3cc6826ed9faa980a6" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.38", @@ -2026,7 +1929,7 @@ dependencies = [ "http", "indexmap 2.0.2", "mime", - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", "regex", @@ -2078,15 +1981,6 @@ dependencies = [ "indexmap 1.9.3", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2130,26 +2024,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quick-xml" version = "0.30.0" @@ -2169,12 +2043,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -2213,6 +2081,7 @@ dependencies = [ "color-eyre", "entities", "futures-lite", + "id3", "md5", "migration", "once_cell", @@ -2225,6 +2094,7 @@ dependencies = [ "time", "tokio", "tracing", + "tracing-appender", "tracing-subscriber", "url", "url-escape", @@ -2283,15 +2153,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" -[[package]] -name = "rend" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" -dependencies = [ - "bytecheck", -] - [[package]] name = "rfc7239" version = "0.1.0" @@ -2316,34 +2177,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rkyv" -version = "0.7.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" -dependencies = [ - "bitvec", - "bytecheck", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rsa" version = "0.9.2" @@ -2366,22 +2199,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rust_decimal" -version = "1.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2497,24 +2314,19 @@ checksum = "da5b2d70c255bc5cbe1d49f69c3c8eadae0fbbaeb18ee978edbf2f75775cb94d" dependencies = [ "async-stream", "async-trait", - "bigdecimal", - "chrono", "futures", "log", "ouroboros", - "rust_decimal", "sea-orm-macros", "sea-query", "sea-query-binder", "serde", - "serde_json", "sqlx", "strum", "thiserror", "time", "tracing", "url", - "uuid", ] [[package]] @@ -2571,16 +2383,11 @@ version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb3e6bba153bb198646c8762c48414942a38db27d142e44735a133cabddcc820" dependencies = [ - "bigdecimal", - "chrono", "derivative", "inherent", "ordered-float", - "rust_decimal", "sea-query-derive", - "serde_json", "time", - "uuid", ] [[package]] @@ -2589,14 +2396,9 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", "sea-query", - "serde_json", "sqlx", "time", - "uuid", ] [[package]] @@ -2635,12 +2437,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "semver" version = "1.0.19" @@ -2753,12 +2549,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - [[package]] name = "slab" version = "0.4.9" @@ -2849,12 +2639,10 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash 0.8.3", + "ahash", "atoi", - "bigdecimal", "byteorder", "bytes", - "chrono", "crc", "crossbeam-queue", "dotenvy", @@ -2873,7 +2661,6 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rust_decimal", "rustls", "rustls-pemfile", "serde", @@ -2887,7 +2674,6 @@ dependencies = [ "tokio-stream", "tracing", "url", - "uuid", "webpki-roots", ] @@ -2938,11 +2724,9 @@ checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", "base64", - "bigdecimal", "bitflags 2.4.0", "byteorder", "bytes", - "chrono", "crc", "digest", "dotenvy", @@ -2963,7 +2747,6 @@ dependencies = [ "percent-encoding", "rand", "rsa", - "rust_decimal", "serde", "sha1", "sha2", @@ -2973,7 +2756,6 @@ dependencies = [ "thiserror", "time", "tracing", - "uuid", "whoami", ] @@ -2985,10 +2767,8 @@ checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", "base64", - "bigdecimal", "bitflags 2.4.0", "byteorder", - "chrono", "crc", "dotenvy", "etcetera", @@ -3004,10 +2784,8 @@ dependencies = [ "log", "md-5", "memchr", - "num-bigint", "once_cell", "rand", - "rust_decimal", "serde", "serde_json", "sha1", @@ -3018,7 +2796,6 @@ dependencies = [ "thiserror", "time", "tracing", - "uuid", "whoami", ] @@ -3029,7 +2806,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" dependencies = [ "atoi", - "chrono", "flume", "futures-channel", "futures-core", @@ -3044,7 +2820,6 @@ dependencies = [ "time", "tracing", "url", - "uuid", ] [[package]] @@ -3104,12 +2879,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tempfile" version = "3.8.0" @@ -3161,6 +2930,8 @@ checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", + "libc", + "num_threads", "serde", "time-core", "time-macros", @@ -3261,15 +3032,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.6.3" @@ -3306,6 +3068,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.26" @@ -3348,6 +3121,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.17" @@ -3359,6 +3142,8 @@ dependencies = [ "once_cell", "parking_lot", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", @@ -3366,6 +3151,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -3480,15 +3266,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" -[[package]] -name = "uuid" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" -dependencies = [ - "serde", -] - [[package]] name = "valuable" version = "0.1.0" @@ -3731,15 +3508,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 862a8ed..4e9ac9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.dependencies] entities = { path = "entities" } migration = { path = "migration" } -sea-orm = { version = "0.12", features = [ +sea-orm = { version = "0.12", default-features = false, features = [ "sqlx-postgres", "runtime-tokio-rustls", "with-time", diff --git a/Dockerfile b/Dockerfile index 9dc3863..0e6ad61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,8 +24,6 @@ FROM gcr.io/distroless/static AS runtime COPY --from=build /tmp/rave /rave -VOLUME [ "/storage", "/config" ] - ENV RUST_LOG=info ENTRYPOINT ["/rave"] diff --git a/docker-compose.yml b/docker-compose.yml index 601fd5c..9f9388c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,4 @@ services: ports: - 12345:5432 volumes: - - /tmp/rave-dev-db:/var/lib/postgresql/data + - ./.rave-dev-db:/var/lib/postgresql/data diff --git a/entities/src/album.rs b/entities/src/album.rs index 54fbcab..053ca1a 100644 --- a/entities/src/album.rs +++ b/entities/src/album.rs @@ -14,11 +14,15 @@ pub struct Model { pub song_count: i32, pub duration: i64, pub play_count: i64, - pub created: DateTimeWithTimeZone, - pub starred: Option, + pub created: TimeDateTimeWithTimeZone, + pub starred: Option, pub year: Option, - pub genre: Option, - pub music_folder_id: Option, + pub genre_ids: Option>, + pub played: TimeDateTimeWithTimeZone, + pub user_rating: Option, + pub artist_ids: Option>, + pub original_release_date: Option, + pub music_folder_id: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entities/src/artist.rs b/entities/src/artist.rs index 09513b1..2c086a7 100644 --- a/entities/src/artist.rs +++ b/entities/src/artist.rs @@ -8,12 +8,14 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key)] pub id: i64, - #[sea_orm(unique)] pub name: String, pub cover_art_id: Option, pub artist_image_url: Option, pub album_count: i32, pub starred: bool, + pub music_brainz_id: String, + pub sort_name: String, + pub roles: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entities/src/genre.rs b/entities/src/genre.rs new file mode 100644 index 0000000..0fa53fb --- /dev/null +++ b/entities/src/genre.rs @@ -0,0 +1,20 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "genre")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + #[sea_orm(unique)] + pub name: String, + pub song_count: i64, + pub album_count: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entities/src/lib.rs b/entities/src/lib.rs index 4212510..e5a61ae 100644 --- a/entities/src/lib.rs +++ b/entities/src/lib.rs @@ -5,6 +5,7 @@ pub mod prelude; pub mod album; pub mod artist; pub mod cover_art; +pub mod genre; pub mod music_folder; pub mod track; pub mod user; diff --git a/entities/src/prelude.rs b/entities/src/prelude.rs index 4030456..976d011 100644 --- a/entities/src/prelude.rs +++ b/entities/src/prelude.rs @@ -3,6 +3,7 @@ pub use super::album::Entity as Album; pub use super::artist::Entity as Artist; pub use super::cover_art::Entity as CoverArt; +pub use super::genre::Entity as Genre; pub use super::music_folder::Entity as MusicFolder; pub use super::track::Entity as Track; pub use super::user::Entity as User; diff --git a/entities/src/track.rs b/entities/src/track.rs index 3d7c2dd..f76c9e1 100644 --- a/entities/src/track.rs +++ b/entities/src/track.rs @@ -13,9 +13,9 @@ pub struct Model { pub artist_id: Option, pub is_dir: bool, pub cover_art_id: Option, - pub created: DateTimeWithTimeZone, + pub created: TimeDateTimeWithTimeZone, pub duration: i64, - pub bit_rate: i64, + pub bit_rate: Option, pub size: i64, pub suffix: String, pub content_type: String, diff --git a/entities/src/user.rs b/entities/src/user.rs index 0f88856..ab0c550 100644 --- a/entities/src/user.rs +++ b/entities/src/user.rs @@ -12,6 +12,8 @@ pub struct Model { pub name: String, pub password: String, pub is_admin: bool, + pub can_download: bool, + pub scrobbling_enabled: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 8ac29f6..34b3be6 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -1,11 +1,12 @@ pub use sea_orm_migration::prelude::*; -mod m20220101_000001_create_user; -mod m20231009_181004_create_music_folder; -mod m20231009_181104_create_cover_art; -mod m20231009_181204_create_artist; -mod m20231009_181346_create_album; -mod m20231009_185712_create_track; +mod m000001_create_user; +mod m000002_create_music_folder; +mod m000003_create_cover_art; +mod m000004_create_artist; +mod m000005_create_genre; +mod m000006_create_album; +mod m000007_create_track; pub struct Migrator; @@ -13,12 +14,13 @@ pub struct Migrator; impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ - Box::new(m20220101_000001_create_user::Migration), - Box::new(m20231009_181004_create_music_folder::Migration), - Box::new(m20231009_181104_create_cover_art::Migration), - Box::new(m20231009_181204_create_artist::Migration), - Box::new(m20231009_181346_create_album::Migration), - Box::new(m20231009_185712_create_track::Migration), + Box::new(m000001_create_user::Migration), + Box::new(m000002_create_music_folder::Migration), + Box::new(m000003_create_cover_art::Migration), + Box::new(m000004_create_artist::Migration), + Box::new(m000005_create_genre::Migration), + Box::new(m000006_create_album::Migration), + Box::new(m000007_create_track::Migration), ] } } diff --git a/migration/src/m20220101_000001_create_user.rs b/migration/src/m000001_create_user.rs similarity index 79% rename from migration/src/m20220101_000001_create_user.rs rename to migration/src/m000001_create_user.rs index a1f1804..1ff10a5 100644 --- a/migration/src/m20220101_000001_create_user.rs +++ b/migration/src/m000001_create_user.rs @@ -27,6 +27,18 @@ impl MigrationTrait for Migration { .not_null() .default(false), ) + .col( + ColumnDef::new(User::CanDownload) + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new(User::ScrobblingEnabled) + .boolean() + .not_null() + .default(false), + ) .to_owned(), ) .await?; @@ -65,4 +77,6 @@ pub enum User { Name, Password, IsAdmin, + CanDownload, + ScrobblingEnabled, } diff --git a/migration/src/m20231009_181004_create_music_folder.rs b/migration/src/m000002_create_music_folder.rs similarity index 100% rename from migration/src/m20231009_181004_create_music_folder.rs rename to migration/src/m000002_create_music_folder.rs diff --git a/migration/src/m20231009_181104_create_cover_art.rs b/migration/src/m000003_create_cover_art.rs similarity index 100% rename from migration/src/m20231009_181104_create_cover_art.rs rename to migration/src/m000003_create_cover_art.rs diff --git a/migration/src/m20231009_181204_create_artist.rs b/migration/src/m000004_create_artist.rs similarity index 84% rename from migration/src/m20231009_181204_create_artist.rs rename to migration/src/m000004_create_artist.rs index c143091..afa9d16 100644 --- a/migration/src/m20231009_181204_create_artist.rs +++ b/migration/src/m000004_create_artist.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::m20231009_181104_create_cover_art::CoverArt; +use crate::m000003_create_cover_art::CoverArt; #[derive(DeriveMigrationName)] pub struct Migration; @@ -21,12 +21,7 @@ impl MigrationTrait for Migration { .auto_increment() .unique_key(), ) - .col( - ColumnDef::new(Artist::Name) - .string() - .not_null() - .unique_key(), - ) + .col(ColumnDef::new(Artist::Name).string().not_null()) .col(ColumnDef::new(Artist::CoverArtId).big_integer().null()) .col(ColumnDef::new(Artist::ArtistImageUrl).string().null()) .col( @@ -41,6 +36,14 @@ impl MigrationTrait for Migration { .not_null() .default(false), ) + .col( + ColumnDef::new(Artist::MusicBrainzId) + .string() + .not_null() + .default(""), + ) + .col(ColumnDef::new(Artist::SortName).string().not_null()) + .col(ColumnDef::new(Artist::Roles).string().not_null()) .to_owned(), ) .await?; @@ -76,4 +79,7 @@ pub enum Artist { ArtistImageUrl, AlbumCount, Starred, + MusicBrainzId, + SortName, + Roles, } diff --git a/migration/src/m000005_create_genre.rs b/migration/src/m000005_create_genre.rs new file mode 100644 index 0000000..72109f1 --- /dev/null +++ b/migration/src/m000005_create_genre.rs @@ -0,0 +1,54 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Genre::Table) + .if_not_exists() + .col( + ColumnDef::new(Genre::Id) + .big_integer() + .not_null() + .primary_key() + .auto_increment() + .unique_key(), + ) + .col(ColumnDef::new(Genre::Name).string().not_null().unique_key()) + .col( + ColumnDef::new(Genre::SongCount) + .big_integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Genre::AlbumCount) + .big_integer() + .not_null() + .default(0), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Genre::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +pub enum Genre { + Table, + Id, + Name, + SongCount, + AlbumCount, +} diff --git a/migration/src/m20231009_181346_create_album.rs b/migration/src/m000006_create_album.rs similarity index 72% rename from migration/src/m20231009_181346_create_album.rs rename to migration/src/m000006_create_album.rs index de89305..93fef11 100644 --- a/migration/src/m20231009_181346_create_album.rs +++ b/migration/src/m000006_create_album.rs @@ -1,8 +1,8 @@ use sea_orm_migration::prelude::*; use crate::{ - m20231009_181004_create_music_folder::MusicFolder, m20231009_181104_create_cover_art::CoverArt, - m20231009_181204_create_artist::Artist, + m000002_create_music_folder::MusicFolder, m000003_create_cover_art::CoverArt, + m000004_create_artist::Artist, }; #[derive(DeriveMigrationName)] @@ -45,8 +45,32 @@ impl MigrationTrait for Migration { .null(), ) .col(ColumnDef::new(Album::Year).integer().null()) - .col(ColumnDef::new(Album::Genre).string().null()) - .col(ColumnDef::new(Album::MusicFolderId).big_integer().null()) + .col( + ColumnDef::new(Album::GenreIds) + .array(ColumnType::BigInteger) + .null(), + ) + .col( + ColumnDef::new(Album::Played) + .timestamp_with_time_zone() + .not_null(), + ) + .col(ColumnDef::new(Album::UserRating).tiny_integer().null()) + .col( + ColumnDef::new(Album::ArtistIds) + .array(ColumnType::BigInteger) + .null(), + ) + .col( + ColumnDef::new(Album::OriginalReleaseDate) + .timestamp_with_time_zone() + .null(), + ) + .col( + ColumnDef::new(Album::MusicFolderId) + .big_integer() + .not_null(), + ) .to_owned(), ) .await?; @@ -110,6 +134,10 @@ pub enum Album { Created, Starred, Year, - Genre, + GenreIds, + Played, + UserRating, + ArtistIds, + OriginalReleaseDate, MusicFolderId, } diff --git a/migration/src/m20231009_185712_create_track.rs b/migration/src/m000007_create_track.rs similarity index 96% rename from migration/src/m20231009_185712_create_track.rs rename to migration/src/m000007_create_track.rs index 7e8dd9a..e68ee57 100644 --- a/migration/src/m20231009_185712_create_track.rs +++ b/migration/src/m000007_create_track.rs @@ -1,8 +1,7 @@ use sea_orm_migration::prelude::*; use crate::{ - m20231009_181104_create_cover_art::CoverArt, m20231009_181204_create_artist::Artist, - m20231009_181346_create_album::Album, + m000003_create_cover_art::CoverArt, m000004_create_artist::Artist, m000006_create_album::Album, }; #[derive(DeriveMigrationName)] @@ -40,7 +39,7 @@ impl MigrationTrait for Migration { .not_null(), ) .col(ColumnDef::new(Track::Duration).big_integer().not_null()) - .col(ColumnDef::new(Track::BitRate).big_integer().not_null()) + .col(ColumnDef::new(Track::BitRate).big_integer().null()) .col(ColumnDef::new(Track::Size).big_integer().not_null()) .col(ColumnDef::new(Track::Suffix).string().not_null()) .col(ColumnDef::new(Track::ContentType).string().not_null()) diff --git a/rave/Cargo.toml b/rave/Cargo.toml index 6409d06..a8e95da 100644 --- a/rave/Cargo.toml +++ b/rave/Cargo.toml @@ -22,7 +22,7 @@ poem-ext = "0.9.4" quick-xml = { version = "0.30.0", features = ["serialize"] } serde = { workspace = true } serde_json = "1.0.107" -time = { workspace = true } +time = { workspace = true, features = ["local-offset"] } tokio = { version = "1.32.0", features = ["full"] } tracing = { workspace = true } tracing-subscriber = { version = "0.3.17", features = [ @@ -30,6 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = [ "tracing", "parking_lot", "time", + "json", ] } url = { version = "2.4.1", features = ["serde"] } url-escape = "0.1.1" @@ -38,3 +39,5 @@ 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"] } +tracing-appender = "0.2.2" diff --git a/rave/src/main.rs b/rave/src/main.rs index b40df53..639818a 100644 --- a/rave/src/main.rs +++ b/rave/src/main.rs @@ -15,7 +15,11 @@ use poem::{ use poem_ext::db::DbTransactionMiddleware; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use tracing::info; -use tracing_subscriber::{fmt, EnvFilter}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{ + fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, + Registry, +}; mod authentication; mod random_types; @@ -30,7 +34,7 @@ const LISTEN: &str = "0.0.0.0:1234"; #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; - install_tracing()?; + let _guards = install_tracing().await?; let route = create_route(); @@ -90,60 +94,30 @@ fn create_server() -> poem::Server, std::convert::Infa poem::Server::new(listener).name("rave") } -fn install_tracing() -> Result<()> { +#[allow(clippy::unused_async)] +async fn install_tracing() -> Result<[WorkerGuard; 1]> { let filter = { cfg_if::cfg_if! { if #[cfg(debug_assertions)] { - std::env::var("RUST_LOG").unwrap_or_else(|_| "poem=trace,rave=debug".to_string()) + std::env::var("RUST_LOG").unwrap_or_else(|_| "info,rave=debug".to_string()) } else { - std::env::var("RUST_LOG").unwrap_or_else(|_| "poem=warn,rave=debug".to_string()) + std::env::var("RUST_LOG").unwrap_or_else(|_| "warn,rave=debug".to_string()) } } }; let filter = EnvFilter::from(filter); - fmt() - .pretty() - .with_env_filter(filter) - .try_init() - .map_err(|v| color_eyre::eyre::eyre!("failed to install tracing: {v}"))?; + let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout()); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - use poem::{ - http::{Method, StatusCode}, - Request, - }; - - #[tokio::test] - async fn test_hello_world() { - let app = create_route(); - - let resp = app - .call(Request::builder().method(Method::GET).uri_str("/").finish()) - .await; - assert!( - resp.is_ok(), - "Failed to get response: {}", - resp.expect_err("Failed to get response") - ); - - let resp = resp.expect("Failed to get response"); - - assert_eq!(resp.status(), StatusCode::OK); - let body = resp.into_body().into_string().await; - assert!( - body.is_ok(), - "Failed to get body: {}", - body.expect_err("Failed to get body") - ); - let body = body.expect("Failed to get body"); - assert_eq!(body, "Hello, world!"); - } + Registry::default() + .with( + fmt::layer() + .pretty() + .with_writer(non_blocking) + .with_filter(filter), + ) + .try_init()?; + + Ok([guard]) } diff --git a/rave/src/rest/get_album.rs b/rave/src/rest/get_album.rs index 2c48d4a..5c28570 100644 --- a/rave/src/rest/get_album.rs +++ b/rave/src/rest/get_album.rs @@ -46,7 +46,8 @@ pub async fn get_album( } }; - SubsonicResponse::new_album(album, tracks) + todo!() + // SubsonicResponse::new_album(album, tracks) } #[derive(Debug, Clone, Deserialize)] diff --git a/rave/src/rest/get_album_list.rs b/rave/src/rest/get_album_list.rs index cffe607..e2ac06c 100644 --- a/rave/src/rest/get_album_list.rs +++ b/rave/src/rest/get_album_list.rs @@ -1,8 +1,8 @@ #![allow(clippy::unused_async)] // todo: remove use entities::{ - album, artist, - prelude::{Album, Artist}, + album, artist, genre, + prelude::{Album, Artist, Genre}, }; use poem::{ web::{Data, Query}, @@ -62,17 +62,19 @@ pub async fn get_album_list( SortType::ByGenre => get_album_list_by_genre(txn, params).await, }; - match album_list { - Ok(a) => { - debug!("uri path: {}", req.uri().path()); - if req.uri().path().contains("getAlbumList2") { - SubsonicResponse::new_album_list2(a) - } else { - SubsonicResponse::new_album_list(a) - } - } - Err(e) => SubsonicResponse::new_error(e), - } + todo!() + + // match album_list { + // Ok(a) => { + // debug!("uri path: {}", req.uri().path()); + // if req.uri().path().contains("getAlbumList2") { + // SubsonicResponse::new_album_list2(a) + // } else { + // SubsonicResponse::new_album_list(a) + // } + // } + // Err(e) => SubsonicResponse::new_error(e), + // } } #[allow(unused_variables)] @@ -221,14 +223,39 @@ async fn get_album_list_by_genre( ))); }; + let genre_id = Genre::find() + .filter(genre::Column::Name.eq(genre)) + .one(&*conn) + .await; + + let genre = match genre_id { + Ok(Some(g)) => g, + Ok(None) | Err(_) => return Err(Error::Generic(Some("Genre not found".to_string()))), + }; + let albums = Album::find() - .filter(album::Column::Genre.is_not_null()) - .filter(album::Column::Genre.eq(genre)) + .filter(album::Column::GenreIds.is_not_null()) .limit(params.size) .offset(params.offset) .all(&*conn) .await; + let albums = albums.map(|c| { + c.into_iter() + .filter_map(|c| { + if c.genre_ids + .clone() + .expect("we have a genre") + .contains(&genre.id) + { + Some(c) + } else { + None + } + }) + .collect() + }); + error_or!(albums) } diff --git a/rave/src/rest/get_music_folders.rs b/rave/src/rest/get_music_folders.rs index 75a77df..8b40535 100644 --- a/rave/src/rest/get_music_folders.rs +++ b/rave/src/rest/get_music_folders.rs @@ -24,5 +24,5 @@ pub async fn get_music_folders(Data(txn): Data<&DbTxn>, auth: Authentication) -> return SubsonicResponse::new_error(Error::RequestedDataWasNotFound(None)); }; - SubsonicResponse::new_music_folders(folders) + SubsonicResponse::new_music_folders(folders.into_iter().map(Into::into).collect()) } diff --git a/rave/src/rest/get_scan_status.rs b/rave/src/rest/get_scan_status.rs new file mode 100644 index 0000000..dcfaf4b --- /dev/null +++ b/rave/src/rest/get_scan_status.rs @@ -0,0 +1,30 @@ +use poem::web::Data; +use poem_ext::db::DbTxn; +use tracing::warn; + +use crate::{ + authentication::Authentication, + scan, + subsonic::{Error, SubsonicResponse}, + utils::{self}, +}; + +#[poem::handler] +pub async fn get_scan_status(Data(txn): Data<&DbTxn>, auth: Authentication) -> SubsonicResponse { + let u = utils::verify_user(txn.clone(), auth).await; + + match u { + Ok(_) => {} + Err(e) => return e, + }; + + let status = scan::get_scan_status().await; + + match status { + Ok(status) => SubsonicResponse::new_scan_status(status.scanning, status.count), + Err(e) => { + warn!("Error getting scan status: {}", e); + SubsonicResponse::new_error(Error::Generic(None)) + } + } +} diff --git a/rave/src/rest/mod.rs b/rave/src/rest/mod.rs index 138c38f..39cc111 100644 --- a/rave/src/rest/mod.rs +++ b/rave/src/rest/mod.rs @@ -14,6 +14,8 @@ mod get_album; mod stream; // rest/startScan mod start_scan; +// rest/getScanStatus +mod get_scan_status; pub fn build() -> Box> { Route::new() @@ -25,5 +27,6 @@ pub fn build() -> Box> { .at("/getAlbum", get_album::get_album) .at("/stream", stream::stream) .at("/startScan", start_scan::start_scan) + .at("/getScanStatus", get_scan_status::get_scan_status) .boxed() } diff --git a/rave/src/rest/start_scan.rs b/rave/src/rest/start_scan.rs index fbf28b4..8e8e16b 100644 --- a/rave/src/rest/start_scan.rs +++ b/rave/src/rest/start_scan.rs @@ -22,7 +22,7 @@ pub async fn start_scan(Data(txn): Data<&DbTxn>, auth: Authentication) -> Subson } Err(e) => return e, } - crate::scan::start_scan(); + crate::scan::start_scan().await; let res = crate::scan::get_scan_status().await; diff --git a/rave/src/scan.rs b/rave/src/scan.rs index 7d2fb06..d3fcbe2 100644 --- a/rave/src/scan.rs +++ b/rave/src/scan.rs @@ -1,14 +1,31 @@ use color_eyre::{Report, Result}; +use entities::{ + album, artist, genre, music_folder, + prelude::{Album, Artist, Genre, MusicFolder, Track}, + track, +}; use futures_lite::StreamExt; +use id3::{Tag, TagLike}; use once_cell::sync::Lazy; -use sea_orm::{ConnectOptions, Database, DatabaseTransaction, TransactionTrait}; -use std::{path::PathBuf, sync::Arc}; -use tokio::sync::RwLock; -use tracing::warn; +use sea_orm::{ + ActiveModelBehavior, 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 tracing::{debug, info, warn}; mod walk; -pub fn start_scan() { +pub async fn start_scan() { + STATUS.write().await.scanning = true; + tokio::spawn(scan()); } @@ -34,14 +51,11 @@ pub struct ScanStatus { } async fn scan() { - { - let mut stat = STATUS.write().await; - - stat.scanning = true; - } - let url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set"); - let conn = ConnectOptions::new(url); + let mut conn = ConnectOptions::new(url); + conn.max_connections(500) + .min_connections(1) + .sqlx_logging(true); let dbc = get_dbc(conn).await; if dbc.is_none() { @@ -50,60 +64,335 @@ async fn scan() { let dbc = dbc.expect("Failed to connect to database"); let root_dir = get_root_dir(); + let state = Arc::new(RwLock::new(ScanState::default())); + + if create_root_music_folder(&dbc, &root_dir, state.clone()).await == ControlFlow::Break(()) { + return; + } + + info!("Scanning {root_dir:?}"); + let mut walk = walk::WalkDir::new(root_dir); - let mut count = 0; - while let Some(res) = walk.next().await { - let Some(de) = check_dir_entry(res, &mut count).await else { + let Some(de) = check_dir_entry(res).await else { continue; }; - let Some(txn) = create_txn(&dbc, &mut count).await else { + let Some(txn) = create_txn(&dbc).await else { continue; }; - if let Err(e) = handle_entry(&txn, de).await { - warn!("Failed to handle directory entry: {e}"); - - { - let mut write = STATUS.write().await; - write.errors.push(Arc::new(e)); - } - let _ = txn.rollback().await; - - count += 1; - continue; - } - - let _ = txn.commit().await; + tokio::spawn(do_entry(de, txn, state.clone())); } { let mut stat = STATUS.write().await; stat.scanning = false; - stat.count = count; } } -async fn handle_entry(tx: &DatabaseTransaction, entry: walk::DirEntry) -> Result<()> { +const VALID_EXTENSIONS: &[&str] = &["mp3"]; + +async fn do_entry(de: walk::DirEntry, txn: DatabaseTransaction, state: Arc>) { + if let Err(e) = handle_entry(&txn, de, state).await { + let _ = txn.rollback().await; + warn!("Failed to handle directory entry: {e}"); + + { + let mut write = STATUS.write().await; + write.errors.push(Arc::new(e)); + } + + return; + } + + let _ = txn.commit().await; + { + STATUS.write().await.count += 1; + } +} + +async fn handle_entry( + tx: &DatabaseTransaction, + entry: walk::DirEntry, + state: Arc>, +) -> Result<()> { let path = entry.path(); - let path = path - .to_str() - .ok_or_else(|| Report::msg("Failed to convert path to string"))?; let file_type = entry.file_type().await?; - let meta = entry.metadata().await?; + + if !file_type.is_file() { + return Ok(()); + } + + let file_ext = path.extension(); + + let Some(ext) = file_ext else { + warn!("Couldn't get file extension for {path:?}"); + + { + STATUS + .write() + .await + .errors + .push(Arc::new(Report::msg(format!( + "Couldn't get file extension for {path:?}" + )))); + } + return Ok(()); + }; + + let ext = ext.to_string_lossy(); + + let file_stem = path.file_stem(); + + let Some(stem) = file_stem else { + warn!("Couldn't get file stem for {path:?}"); + + { + STATUS + .write() + .await + .errors + .push(Arc::new(Report::msg(format!( + "Couldn't get file stem for {path:?}" + )))); + } + return Ok(()); + }; + + let stem = stem.to_string_lossy(); + + if !VALID_EXTENSIONS.contains(&ext.as_ref()) { + debug!("Skipping file with invalid extension: {path:?}"); + return Ok(()); + } + + let meta = { File::open(&path).await?.metadata().await? }; + + let current_album = { state.read().await.album_id }; + + let tag = Tag::async_read_from_path(&path).await?; + + let artist = find_artist(tx, &tag).await?; + + let album = find_album( + tx, + current_album, + 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); // TODO: figure out how to scan. steal from Gonic if we have to :3 Ok(()) } -async fn create_txn( +async fn find_album( + tx: &DatabaseTransaction, + current_album: Option, + artist_id: Option, + tag: &Tag, + state: Arc>, +) -> Result { + if let Some(current_album) = current_album { + let album = Album::find_by_id(current_album).one(tx).await?; + + let Some(album) = album else { + warn!("Couldn't find album with id {current_album}"); + + return Err(Report::msg(format!( + "Couldn't find album with id {current_album}" + ))); + }; + + Ok(album) + } else { + let mut am = album::ActiveModel::new(); + + if let Some(tag_album) = tag.album() { + am.name = Set(tag_album.to_string()); + } else { + am.name = Set("Unknown Album".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; + + let Ok(model) = model else { + let err = model.expect_err("somehow not err"); + warn!("Failed to insert album {err}"); + + return Err(Report::new(err)); + }; + + Ok(model) + } +} + +async fn find_or_create_genre(tx: &DatabaseTransaction, name: &str) -> Result { + let res = Genre::find() + .filter(genre::Column::Name.eq(name)) + .one(tx) + .await?; + + if let Some(genre) = res { + Ok(genre.id) + } else { + let am = genre::ActiveModel { + name: Set(name.to_string()), + ..Default::default() + }; + + let model = Genre::insert(am).exec_with_returning(tx).await; + + let Ok(model) = model else { + let err = model.expect_err("somehow not err"); + warn!("Failed to insert genre {err}"); + + return Err(Report::new(err)); + }; + + Ok(model.id) + } +} + +async fn find_artist(tx: &DatabaseTransaction, tag: &Tag) -> 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 attempt = Artist::find() + .filter(artist::Column::Name.contains(artist_to_search)) + .one(tx) + .await?; + + if let Some(attempt) = attempt { + Ok(Some(attempt)) + } else { + let am = artist::ActiveModel { + name: Set(artist_to_search.clone()), + ..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"); + warn!("Failed to insert artist {err}"); + + return Err(Report::new(err)); + }; + + Ok(Some(model)) + } + } + None => Ok(None), + } +} + +#[derive(Debug, Default)] +struct ScanState { + pub music_folder_id: i64, + pub album_id: Option, +} + +async fn create_root_music_folder( dbc: &sea_orm::DatabaseConnection, - count: &mut u64, -) -> Option { + root_dir: &Path, + state: Arc>, +) -> ControlFlow<()> { + let txn = dbc.begin().await; + let Ok(txn) = txn else { + let err = txn.expect_err("somehow not err"); + warn!("Failed to start database transaction to add the root music folder {err}"); + + { + let mut stat = STATUS.write().await; + stat.scanning = false; + stat.errors.push(Arc::new(Report::new(err))); + } + return ControlFlow::Break(()); + }; + debug!("created transaction"); + + let new_music_folder = music_folder::ActiveModel { + name: Set(root_dir.to_string_lossy().to_string()), + ..Default::default() + }; + debug!("created new music folder model"); + + let mf = MusicFolder::insert(new_music_folder).exec(&txn).await; + + debug!("inserted new music folder model"); + + let Ok(mf) = mf else { + let err = mf.expect_err("somehow not err"); + warn!("Failed to add the root music folder {err}"); + + { + let mut stat = STATUS.write().await; + stat.scanning = false; + stat.errors.push(Arc::new(Report::new(err))); + } + + let _ = txn.rollback().await; + return ControlFlow::Break(()); + }; + + let _ = txn.commit().await; + + state.write().await.music_folder_id = mf.last_insert_id; + ControlFlow::Continue(()) +} + +async fn create_txn(dbc: &sea_orm::DatabaseConnection) -> Option { let txn = match dbc.begin().await { Ok(txn) => txn, Err(e) => { @@ -114,7 +403,6 @@ async fn create_txn( write.errors.push(Arc::new(Report::new(e))); } - *count += 1; return None; } }; @@ -123,7 +411,6 @@ async fn create_txn( async fn check_dir_entry( res: std::result::Result, std::io::Error>, - count: &mut u64, ) -> Option> { let de = match res { Ok(de) => de, @@ -135,7 +422,6 @@ async fn check_dir_entry( write.errors.push(Arc::new(Report::new(e))); } - *count += 1; return None; } }; @@ -164,3 +450,8 @@ fn get_root_dir() -> PathBuf { let root_dir = std::env::var("RAVE_STORAGE_DIR").expect("RAVE_STORAGE_DIR not set"); PathBuf::from(root_dir) } + +// fn get_cache_dir() -> PathBuf { +// let cache_dir = std::env::var("RAVE_CACHE_DIR").expect("RAVE_CACHE_DIR not set"); +// PathBuf::from(cache_dir) +// } diff --git a/rave/src/subsonic.rs b/rave/src/subsonic/error.rs similarity index 54% rename from rave/src/subsonic.rs rename to rave/src/subsonic/error.rs index 103352e..22e4ce3 100644 --- a/rave/src/subsonic.rs +++ b/rave/src/subsonic/error.rs @@ -1,139 +1,7 @@ use std::fmt::Display; -use entities::{album, music_folder, track}; -use poem::{http::StatusCode, IntoResponse, Response}; use serde::{ser::SerializeStruct, Serialize}; -use crate::authentication::VersionTriple; - -impl IntoResponse for SubsonicResponse { - fn into_response(self) -> poem::Response { - let body = quick_xml::se::to_string(&self).expect("Failed to serialize response"); - Response::builder().status(StatusCode::OK).body(body) - } -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename = "subsonic-response")] -pub struct SubsonicResponse { - #[serde(rename = "@xmlns")] - pub xmlns: String, - #[serde(rename = "@status")] - pub status: ResponseStatus, - #[serde(rename = "@version")] - pub version: VersionTriple, - #[serde(rename = "$value")] - pub value: Box, -} - -impl SubsonicResponse { - pub fn new(inner: SubResponseType) -> Self { - Self { - xmlns: "http://subsonic.org/restapi".to_string(), - status: ResponseStatus::Ok, - version: VersionTriple(1, 16, 1), - value: Box::new(inner), - } - } - - pub fn new_music_folders(music_folders: Vec) -> Self { - Self::new(SubResponseType::MusicFolders { music_folders }) - } - - pub fn new_album_list(albums: Vec) -> Self { - Self::new(SubResponseType::AlbumList { albums }) - } - - pub fn new_album_list2(albums: Vec) -> Self { - Self::new(SubResponseType::AlbumList2 { albums }) - } - - pub fn new_album(album: album::Model, songs: Vec) -> Self { - Self::new(SubResponseType::Album { album, songs }) - } - - pub fn new_empty() -> Self { - Self::new(SubResponseType::Empty) - } - - pub fn new_error(inner: Error) -> Self { - Self { - xmlns: "http://subsonic.org/restapi".to_string(), - status: ResponseStatus::Failed, - version: VersionTriple(1, 16, 1), - value: Box::new(SubResponseType::Error(inner)), - } - } - - pub fn new_scan_status(scanning: bool, count: u64) -> Self { - Self { - xmlns: "http://subsonic.org/restapi".to_string(), - status: ResponseStatus::Ok, - version: VersionTriple(1, 16, 1), - value: Box::new(SubResponseType::ScanStatus { scanning, count }), - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub enum SubResponseType { - #[serde(rename = "musicFolders")] - MusicFolders { - #[serde(rename = "musicFolder")] - music_folders: Vec, - }, - #[serde(rename = "error")] - Error(Error), - #[serde(rename = "license")] - License { - #[serde(rename = "valid")] - valid: bool, - }, - #[serde(rename = "albumList")] - AlbumList { - #[serde(rename = "album")] - albums: Vec, - }, - #[serde(rename = "albumList2")] - AlbumList2 { - #[serde(rename = "album")] - albums: Vec, - }, - #[serde(rename = "album")] - Album { - #[serde(flatten)] - album: album::Model, - #[serde(flatten)] - songs: Vec, - }, - #[serde(rename = "scanStatus")] - ScanStatus { - #[serde(rename = "scanning")] - scanning: bool, - #[serde(rename = "count")] - count: u64, - }, - Empty, -} - -#[derive(Debug, Clone, Copy)] -pub enum ResponseStatus { - Ok, - Failed, -} - -impl Serialize for ResponseStatus { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(match self { - Self::Ok => "ok", - Self::Failed => "failed", - }) - } -} - #[derive(Debug, Clone)] #[allow(unused)] pub enum Error { diff --git a/rave/src/subsonic/mod.rs b/rave/src/subsonic/mod.rs new file mode 100644 index 0000000..0ea3002 --- /dev/null +++ b/rave/src/subsonic/mod.rs @@ -0,0 +1,140 @@ +use poem::{http::StatusCode, IntoResponse, Response}; +use serde::Serialize; + +use crate::authentication::VersionTriple; + +mod error; + +mod types; +pub use error::Error; +pub use types::album::Album; +pub use types::music_folder::MusicFolder; +pub use types::track::Track; + +impl IntoResponse for SubsonicResponse { + fn into_response(self) -> poem::Response { + let body = quick_xml::se::to_string(&self).expect("Failed to serialize response"); + Response::builder().status(StatusCode::OK).body(body) + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename = "subsonic-response")] +pub struct SubsonicResponse { + #[serde(rename = "@xmlns")] + pub xmlns: String, + #[serde(rename = "@status")] + pub status: ResponseStatus, + #[serde(rename = "@version")] + pub version: VersionTriple, + #[serde(rename = "$value")] + pub value: Box, +} + +impl SubsonicResponse { + pub fn new(inner: SubResponseType) -> Self { + Self { + xmlns: "http://subsonic.org/restapi".to_string(), + status: ResponseStatus::Ok, + version: VersionTriple(1, 16, 1), + value: Box::new(inner), + } + } + + pub fn new_music_folders(music_folders: Vec) -> Self { + Self::new(SubResponseType::MusicFolders { music_folders }) + } + + pub fn new_album_list(albums: Vec) -> Self { + Self::new(SubResponseType::AlbumList { albums }) + } + + pub fn new_album_list2(albums: Vec) -> Self { + Self::new(SubResponseType::AlbumList2 { albums }) + } + + pub fn new_album(album: Album, songs: Vec) -> Self { + Self::new(SubResponseType::Album { album, songs }) + } + + pub fn new_empty() -> Self { + Self::new(SubResponseType::Empty) + } + + pub fn new_error(inner: Error) -> Self { + Self { + xmlns: "http://subsonic.org/restapi".to_string(), + status: ResponseStatus::Failed, + version: VersionTriple(1, 16, 1), + value: Box::new(SubResponseType::Error(inner)), + } + } + + pub fn new_scan_status(scanning: bool, count: u64) -> Self { + Self { + xmlns: "http://subsonic.org/restapi".to_string(), + status: ResponseStatus::Ok, + version: VersionTriple(1, 16, 1), + value: Box::new(SubResponseType::ScanStatus { scanning, count }), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub enum SubResponseType { + #[serde(rename = "musicFolders")] + MusicFolders { + #[serde(rename = "musicFolder")] + music_folders: Vec, + }, + #[serde(rename = "error")] + Error(error::Error), + #[serde(rename = "license")] + License { + #[serde(rename = "valid")] + valid: bool, + }, + #[serde(rename = "albumList")] + AlbumList { + #[serde(rename = "album")] + albums: Vec, + }, + #[serde(rename = "albumList2")] + AlbumList2 { + #[serde(rename = "album")] + albums: Vec, + }, + #[serde(rename = "album")] + Album { + #[serde(flatten)] + album: Album, + #[serde(flatten)] + songs: Vec, + }, + #[serde(rename = "scanStatus")] + ScanStatus { + #[serde(rename = "scanning")] + scanning: bool, + #[serde(rename = "count")] + count: u64, + }, + Empty, +} + +#[derive(Debug, Clone, Copy)] +pub enum ResponseStatus { + Ok, + Failed, +} + +impl Serialize for ResponseStatus { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(match self { + Self::Ok => "ok", + Self::Failed => "failed", + }) + } +} diff --git a/rave/src/subsonic/types.rs b/rave/src/subsonic/types.rs new file mode 100644 index 0000000..696ae53 --- /dev/null +++ b/rave/src/subsonic/types.rs @@ -0,0 +1,3 @@ +pub mod album; +pub mod music_folder; +pub mod track; diff --git a/rave/src/subsonic/types/album.rs b/rave/src/subsonic/types/album.rs new file mode 100644 index 0000000..a0e1401 --- /dev/null +++ b/rave/src/subsonic/types/album.rs @@ -0,0 +1,27 @@ +// use entities::*; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct Album { + id: String, + name: String, + artist: Option, + #[serde(rename = "artistId")] + artist_id: 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)), +// } +// } +// } diff --git a/rave/src/subsonic/types/music_folder.rs b/rave/src/subsonic/types/music_folder.rs new file mode 100644 index 0000000..c37d02f --- /dev/null +++ b/rave/src/subsonic/types/music_folder.rs @@ -0,0 +1,29 @@ +use entities::music_folder::Model; +use serde::{ser::SerializeStruct, Serialize}; + +#[derive(Debug, Clone)] +pub struct MusicFolder { + pub(crate) id: i64, + pub(crate) name: String, +} + +impl From for MusicFolder { + fn from(value: Model) -> Self { + Self { + id: 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 new file mode 100644 index 0000000..5b8ca7d --- /dev/null +++ b/rave/src/subsonic/types/track.rs @@ -0,0 +1,4 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct Track {}