feat: fiddling with the bits

:3
This commit is contained in:
Lys 2023-10-10 21:25:02 +03:00
parent 4d826ed2a4
commit 7e1f504368
Signed by: lyssieth
GPG key ID: C9CF3D614FAA3940
35 changed files with 874 additions and 573 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target
users.db*
.en*
.rave-dev-db

358
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -11,4 +11,4 @@ services:
ports:
- 12345:5432
volumes:
- /tmp/rave-dev-db:/var/lib/postgresql/data
- ./.rave-dev-db:/var/lib/postgresql/data

View file

@ -14,11 +14,15 @@ pub struct Model {
pub song_count: i32,
pub duration: i64,
pub play_count: i64,
pub created: DateTimeWithTimeZone,
pub starred: Option<DateTimeWithTimeZone>,
pub created: TimeDateTimeWithTimeZone,
pub starred: Option<TimeDateTimeWithTimeZone>,
pub year: Option<i32>,
pub genre: Option<String>,
pub music_folder_id: Option<i64>,
pub genre_ids: Option<Vec<i64>>,
pub played: TimeDateTimeWithTimeZone,
pub user_rating: Option<i16>,
pub artist_ids: Option<Vec<i64>>,
pub original_release_date: Option<TimeDateTimeWithTimeZone>,
pub music_folder_id: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -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<i64>,
pub artist_image_url: Option<String>,
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)]

20
entities/src/genre.rs Normal file
View file

@ -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 {}

View file

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

View file

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

View file

@ -13,9 +13,9 @@ pub struct Model {
pub artist_id: Option<i64>,
pub is_dir: bool,
pub cover_art_id: Option<i64>,
pub created: DateTimeWithTimeZone,
pub created: TimeDateTimeWithTimeZone,
pub duration: i64,
pub bit_rate: i64,
pub bit_rate: Option<i64>,
pub size: i64,
pub suffix: String,
pub content_type: String,

View file

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

View file

@ -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<Box<dyn MigrationTrait>> {
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),
]
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<TcpListener<&'static str>, 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()
let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout());
Registry::default()
.with(
fmt::layer()
.pretty()
.with_env_filter(filter)
.try_init()
.map_err(|v| color_eyre::eyre::eyre!("failed to install tracing: {v}"))?;
.with_writer(non_blocking)
.with_filter(filter),
)
.try_init()?;
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!");
}
Ok([guard])
}

View file

@ -46,7 +46,8 @@ pub async fn get_album(
}
};
SubsonicResponse::new_album(album, tracks)
todo!()
// SubsonicResponse::new_album(album, tracks)
}
#[derive(Debug, Clone, Deserialize)]

View file

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

View file

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

View file

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

View file

@ -14,6 +14,8 @@ mod get_album;
mod stream;
// rest/startScan
mod start_scan;
// rest/getScanStatus
mod get_scan_status;
pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
Route::new()
@ -25,5 +27,6 @@ pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
.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()
}

View file

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

View file

@ -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<RwLock<ScanState>>) {
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<RwLock<ScanState>>,
) -> 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<i64>,
artist_id: Option<i64>,
tag: &Tag,
state: Arc<RwLock<ScanState>>,
) -> Result<album::Model, Report> {
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<i64, Report> {
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<Option<artist::Model>, 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<i64>,
}
async fn create_root_music_folder(
dbc: &sea_orm::DatabaseConnection,
count: &mut u64,
) -> Option<DatabaseTransaction> {
root_dir: &Path,
state: Arc<RwLock<ScanState>>,
) -> 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<DatabaseTransaction> {
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<Arc<tokio::fs::DirEntry>, std::io::Error>,
count: &mut u64,
) -> Option<Arc<tokio::fs::DirEntry>> {
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)
// }

View file

@ -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<SubResponseType>,
}
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<music_folder::Model>) -> Self {
Self::new(SubResponseType::MusicFolders { music_folders })
}
pub fn new_album_list(albums: Vec<album::Model>) -> Self {
Self::new(SubResponseType::AlbumList { albums })
}
pub fn new_album_list2(albums: Vec<album::Model>) -> Self {
Self::new(SubResponseType::AlbumList2 { albums })
}
pub fn new_album(album: album::Model, songs: Vec<track::Model>) -> 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<music_folder::Model>,
},
#[serde(rename = "error")]
Error(Error),
#[serde(rename = "license")]
License {
#[serde(rename = "valid")]
valid: bool,
},
#[serde(rename = "albumList")]
AlbumList {
#[serde(rename = "album")]
albums: Vec<album::Model>,
},
#[serde(rename = "albumList2")]
AlbumList2 {
#[serde(rename = "album")]
albums: Vec<album::Model>,
},
#[serde(rename = "album")]
Album {
#[serde(flatten)]
album: album::Model,
#[serde(flatten)]
songs: Vec<track::Model>,
},
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(match self {
Self::Ok => "ok",
Self::Failed => "failed",
})
}
}
#[derive(Debug, Clone)]
#[allow(unused)]
pub enum Error {

140
rave/src/subsonic/mod.rs Normal file
View file

@ -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<SubResponseType>,
}
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<MusicFolder>) -> Self {
Self::new(SubResponseType::MusicFolders { music_folders })
}
pub fn new_album_list(albums: Vec<Album>) -> Self {
Self::new(SubResponseType::AlbumList { albums })
}
pub fn new_album_list2(albums: Vec<Album>) -> Self {
Self::new(SubResponseType::AlbumList2 { albums })
}
pub fn new_album(album: Album, songs: Vec<Track>) -> 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<MusicFolder>,
},
#[serde(rename = "error")]
Error(error::Error),
#[serde(rename = "license")]
License {
#[serde(rename = "valid")]
valid: bool,
},
#[serde(rename = "albumList")]
AlbumList {
#[serde(rename = "album")]
albums: Vec<Album>,
},
#[serde(rename = "albumList2")]
AlbumList2 {
#[serde(rename = "album")]
albums: Vec<Album>,
},
#[serde(rename = "album")]
Album {
#[serde(flatten)]
album: Album,
#[serde(flatten)]
songs: Vec<Track>,
},
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(match self {
Self::Ok => "ok",
Self::Failed => "failed",
})
}
}

View file

@ -0,0 +1,3 @@
pub mod album;
pub mod music_folder;
pub mod track;

View file

@ -0,0 +1,27 @@
// use entities::*;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Album {
id: String,
name: String,
artist: Option<String>,
#[serde(rename = "artistId")]
artist_id: Option<String>,
}
// impl Album {
// pub fn new(
// album: album::Model,
// artists: Option<artist::Model>,
// cover_art: Option<cover_art::Model>,
// genres: Vec<genre::Model>,
// ) -> 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)),
// }
// }
// }

View file

@ -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<Model> for MusicFolder {
fn from(value: Model) -> Self {
Self {
id: value.id,
name: value.name,
}
}
}
impl Serialize for MusicFolder {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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()
}
}

View file

@ -0,0 +1,4 @@
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Track {}