Compare commits
4 Commits
v1.0.0-alp
...
main
Author | SHA1 | Date | |
---|---|---|---|
8dabbd098f | |||
cdb5c1c713 | |||
c869489919 | |||
fcbde5aecb |
@ -37,7 +37,6 @@ jobs:
|
|||||||
--ignore "examples/*"
|
--ignore "examples/*"
|
||||||
--ignore "tests/*"
|
--ignore "tests/*"
|
||||||
--ignore "src/main.rs"
|
--ignore "src/main.rs"
|
||||||
--ignore "src/bin/musichoard-edit.rs"
|
|
||||||
--excl-line "^#\[derive|unimplemented\!\(|unreachable\!\("
|
--excl-line "^#\[derive|unimplemented\!\(|unreachable\!\("
|
||||||
--excl-start "GRCOV_EXCL_START|mod tests \{"
|
--excl-start "GRCOV_EXCL_START|mod tests \{"
|
||||||
--excl-stop "GRCOV_EXCL_STOP"
|
--excl-stop "GRCOV_EXCL_STOP"
|
||||||
|
103
Cargo.lock
generated
103
Cargo.lock
generated
@ -17,6 +17,18 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -298,6 +310,18 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@ -432,6 +456,15 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
@ -443,6 +476,15 @@ dependencies = [
|
|||||||
"foldhash",
|
"foldhash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -731,7 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -796,6 +838,17 @@ version = "0.2.169"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@ -830,7 +883,7 @@ version = "0.12.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -868,14 +921,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mockall"
|
name = "mockall"
|
||||||
version = "0.12.1"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48"
|
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"downcast",
|
"downcast",
|
||||||
"fragile",
|
"fragile",
|
||||||
"lazy_static",
|
|
||||||
"mockall_derive",
|
"mockall_derive",
|
||||||
"predicates",
|
"predicates",
|
||||||
"predicates-tree",
|
"predicates-tree",
|
||||||
@ -883,9 +935,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mockall_derive"
|
name = "mockall_derive"
|
||||||
version = "0.12.1"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2"
|
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -895,7 +947,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "musichoard"
|
name = "musichoard"
|
||||||
version = "1.0.0-alpha"
|
version = "1.0.0-alpha.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@ -905,6 +957,7 @@ dependencies = [
|
|||||||
"paste",
|
"paste",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"structopt",
|
"structopt",
|
||||||
@ -1255,6 +1308,20 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@ -2209,6 +2276,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.95",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
21
Cargo.toml
21
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "musichoard"
|
name = "musichoard"
|
||||||
version = "1.0.0-alpha"
|
version = "1.0.0-alpha.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@ -13,6 +13,7 @@ openssh = { version = "0.11.4", features = ["native-mux"], default-features = fa
|
|||||||
paste = { version = "1.0.15", optional = true }
|
paste = { version = "1.0.15", optional = true }
|
||||||
ratatui = { version = "0.29.0", optional = true}
|
ratatui = { version = "0.29.0", optional = true}
|
||||||
reqwest = { version = "0.12.12", features = ["blocking", "json"], optional = true }
|
reqwest = { version = "0.12.12", features = ["blocking", "json"], optional = true }
|
||||||
|
rusqlite = { version = "0.32.1", optional = true }
|
||||||
serde = { version = "1.0.217", features = ["derive"], optional = true }
|
serde = { version = "1.0.217", features = ["derive"], optional = true }
|
||||||
serde_json = { version = "1.0.134", optional = true}
|
serde_json = { version = "1.0.134", optional = true}
|
||||||
structopt = { version = "0.3.26", optional = true}
|
structopt = { version = "0.3.26", optional = true}
|
||||||
@ -23,17 +24,17 @@ url = { version = "2.5.4" }
|
|||||||
uuid = { version = "1.11.0" }
|
uuid = { version = "1.11.0" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
version_check = "0.9.4"
|
version_check = "0.9.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = "0.12.1"
|
mockall = "0.13.1"
|
||||||
once_cell = "1.19.0"
|
tempfile = "3.15.0"
|
||||||
tempfile = "3.10.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["database-json", "library-beets"]
|
default = ["database-sqlite", "library-beets"]
|
||||||
bin = ["structopt"]
|
bin = ["structopt"]
|
||||||
database-json = ["serde", "serde_json"]
|
database-sqlite = ["rusqlite", "serde", "serde_json"]
|
||||||
|
database-sqlite-bundled = ["rusqlite/bundled"]
|
||||||
library-beets = []
|
library-beets = []
|
||||||
library-beets-ssh = ["openssh", "tokio"]
|
library-beets-ssh = ["openssh", "tokio"]
|
||||||
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
|
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
|
||||||
@ -41,11 +42,7 @@ tui = ["crossterm", "ratatui", "tui-input"]
|
|||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "musichoard"
|
name = "musichoard"
|
||||||
required-features = ["bin", "database-json", "library-beets", "library-beets-ssh", "musicbrainz", "tui"]
|
required-features = ["bin", "database-sqlite", "database-sqlite-bundled", "library-beets", "library-beets-ssh", "musicbrainz", "tui"]
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "musichoard-edit"
|
|
||||||
required-features = ["bin", "database-json"]
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "musicbrainz-api---browse"
|
name = "musicbrainz-api---browse"
|
||||||
|
14
README.md
14
README.md
@ -4,6 +4,19 @@
|
|||||||
|
|
||||||
### Pre-requisites
|
### Pre-requisites
|
||||||
|
|
||||||
|
#### database-sqlite
|
||||||
|
|
||||||
|
This feature requires the `sqlite` library.
|
||||||
|
|
||||||
|
Either install system libraries: with
|
||||||
|
|
||||||
|
On Fedora:
|
||||||
|
``` sh
|
||||||
|
sudo dnf install sqlite-devel
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a bundled version by enabling the `database-sqlite-bundled` feature.
|
||||||
|
|
||||||
#### musicbrainz-api
|
#### musicbrainz-api
|
||||||
|
|
||||||
This feature requires the `openssl` system library.
|
This feature requires the `openssl` system library.
|
||||||
@ -49,7 +62,6 @@ grcov codecov/debug/profraw \
|
|||||||
--ignore "examples/*" \
|
--ignore "examples/*" \
|
||||||
--ignore "tests/*" \
|
--ignore "tests/*" \
|
||||||
--ignore "src/main.rs" \
|
--ignore "src/main.rs" \
|
||||||
--ignore "src/bin/musichoard-edit.rs" \
|
|
||||||
--excl-line "^#\[derive|unimplemented\!\(|unreachable\!\(" \
|
--excl-line "^#\[derive|unimplemented\!\(|unreachable\!\(" \
|
||||||
--excl-start "GRCOV_EXCL_START|mod tests \{" \
|
--excl-start "GRCOV_EXCL_START|mod tests \{" \
|
||||||
--excl-stop "GRCOV_EXCL_STOP" \
|
--excl-stop "GRCOV_EXCL_STOP" \
|
||||||
|
@ -1,257 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use structopt::{clap::AppSettings, StructOpt};
|
|
||||||
|
|
||||||
use musichoard::{
|
|
||||||
collection::{album::AlbumId, artist::ArtistId},
|
|
||||||
external::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
|
||||||
IMusicHoardDatabase, MusicHoard, MusicHoardBuilder, NoLibrary,
|
|
||||||
};
|
|
||||||
|
|
||||||
type MH = MusicHoard<JsonDatabase<JsonDatabaseFileBackend>, NoLibrary>;
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
#[structopt(about = "musichoard-edit: edit the MusicHoard database",
|
|
||||||
global_settings=&[AppSettings::DeriveDisplayOrder])]
|
|
||||||
struct Opt {
|
|
||||||
#[structopt(
|
|
||||||
long = "database",
|
|
||||||
help = "Database file path",
|
|
||||||
default_value = "database.json"
|
|
||||||
)]
|
|
||||||
database_file_path: PathBuf,
|
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
|
||||||
command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
enum Command {
|
|
||||||
#[structopt(about = "Modify artist information")]
|
|
||||||
Artist(ArtistOpt),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct ArtistOpt {
|
|
||||||
// For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
|
|
||||||
// for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
|
|
||||||
// will insteady try to process it as a command.
|
|
||||||
#[structopt(long, help = "The name of the artist")]
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
|
||||||
command: ArtistCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
enum ArtistCommand {
|
|
||||||
#[structopt(about = "Add a new artist to the collection")]
|
|
||||||
Add,
|
|
||||||
#[structopt(about = "Remove an artist from the collection")]
|
|
||||||
Remove,
|
|
||||||
#[structopt(about = "Edit the artist's sort name")]
|
|
||||||
Sort(SortCommand),
|
|
||||||
#[structopt(about = "Edit a property of an artist")]
|
|
||||||
Property(PropertyCommand),
|
|
||||||
#[structopt(about = "Modify the artist's album information")]
|
|
||||||
Album(AlbumOpt),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
enum SortCommand {
|
|
||||||
#[structopt(about = "Set the provided name as the artist's sort name")]
|
|
||||||
Set(SortValue),
|
|
||||||
#[structopt(about = "Clear the artist's sort name")]
|
|
||||||
Clear,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct SortValue {
|
|
||||||
#[structopt(help = "The sort name")]
|
|
||||||
sort: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
enum PropertyCommand {
|
|
||||||
#[structopt(about = "Add values to the property without overwriting existing values")]
|
|
||||||
Add(PropertyValue),
|
|
||||||
#[structopt(about = "Remove values from the property")]
|
|
||||||
Remove(PropertyValue),
|
|
||||||
#[structopt(about = "Set the property's values overwriting any existing values")]
|
|
||||||
Set(PropertyValue),
|
|
||||||
#[structopt(about = "Clear all values of a property")]
|
|
||||||
Clear(PropertyName),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct PropertyValue {
|
|
||||||
#[structopt(help = "The name of the property")]
|
|
||||||
property: String,
|
|
||||||
#[structopt(help = "The list of values")]
|
|
||||||
values: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct PropertyName {
|
|
||||||
#[structopt(help = "The name of the property")]
|
|
||||||
property: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct AlbumOpt {
|
|
||||||
// Using `long` for consistency with `ArtistOpt`.
|
|
||||||
#[structopt(long, help = "The title of the album")]
|
|
||||||
title: String,
|
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
|
||||||
command: AlbumCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
enum AlbumCommand {
|
|
||||||
#[structopt(about = "Edit the album's sequence value")]
|
|
||||||
Seq(AlbumSeqCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
enum AlbumSeqCommand {
|
|
||||||
#[structopt(about = "Set the sequence value overwriting any existing value")]
|
|
||||||
Set(AlbumSeqValue),
|
|
||||||
#[structopt(about = "Clear the sequence value")]
|
|
||||||
Clear,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
|
||||||
struct AlbumSeqValue {
|
|
||||||
#[structopt(help = "The new sequence value")]
|
|
||||||
value: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
|
||||||
match self {
|
|
||||||
Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArtistOpt {
|
|
||||||
fn handle(self, music_hoard: &mut MH) {
|
|
||||||
self.command.handle(music_hoard, &self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArtistCommand {
|
|
||||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
|
||||||
match self {
|
|
||||||
ArtistCommand::Add => {
|
|
||||||
music_hoard
|
|
||||||
.add_artist(ArtistId::new(artist_name))
|
|
||||||
.expect("failed to add artist");
|
|
||||||
}
|
|
||||||
ArtistCommand::Remove => {
|
|
||||||
music_hoard
|
|
||||||
.remove_artist(ArtistId::new(artist_name))
|
|
||||||
.expect("failed to remove artist");
|
|
||||||
}
|
|
||||||
ArtistCommand::Sort(sort_command) => {
|
|
||||||
sort_command.handle(music_hoard, artist_name);
|
|
||||||
}
|
|
||||||
ArtistCommand::Property(property_command) => {
|
|
||||||
property_command.handle(music_hoard, artist_name);
|
|
||||||
}
|
|
||||||
ArtistCommand::Album(album_opt) => {
|
|
||||||
album_opt.handle(music_hoard, artist_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SortCommand {
|
|
||||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
|
||||||
match self {
|
|
||||||
SortCommand::Set(artist_sort_value) => music_hoard
|
|
||||||
.set_artist_sort(ArtistId::new(artist_name), artist_sort_value.sort)
|
|
||||||
.expect("faild to set artist sort name"),
|
|
||||||
SortCommand::Clear => music_hoard
|
|
||||||
.clear_artist_sort(ArtistId::new(artist_name))
|
|
||||||
.expect("failed to clear artist sort name"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PropertyCommand {
|
|
||||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
|
||||||
match self {
|
|
||||||
PropertyCommand::Add(property_value) => music_hoard
|
|
||||||
.add_to_artist_property(
|
|
||||||
ArtistId::new(artist_name),
|
|
||||||
property_value.property,
|
|
||||||
property_value.values,
|
|
||||||
)
|
|
||||||
.expect("failed to add values to property"),
|
|
||||||
PropertyCommand::Remove(property_value) => music_hoard
|
|
||||||
.remove_from_artist_property(
|
|
||||||
ArtistId::new(artist_name),
|
|
||||||
property_value.property,
|
|
||||||
property_value.values,
|
|
||||||
)
|
|
||||||
.expect("failed to remove values from property"),
|
|
||||||
PropertyCommand::Set(property_value) => music_hoard
|
|
||||||
.set_artist_property(
|
|
||||||
ArtistId::new(artist_name),
|
|
||||||
property_value.property,
|
|
||||||
property_value.values,
|
|
||||||
)
|
|
||||||
.expect("failed to set property"),
|
|
||||||
PropertyCommand::Clear(property_name) => music_hoard
|
|
||||||
.clear_artist_property(ArtistId::new(artist_name), property_name.property)
|
|
||||||
.expect("failed to clear property"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlbumOpt {
|
|
||||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
|
||||||
self.command.handle(music_hoard, artist_name, &self.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlbumCommand {
|
|
||||||
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
|
||||||
match self {
|
|
||||||
AlbumCommand::Seq(seq_command) => {
|
|
||||||
seq_command.handle(music_hoard, artist_name, album_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlbumSeqCommand {
|
|
||||||
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
|
||||||
match self {
|
|
||||||
AlbumSeqCommand::Set(seq_value) => music_hoard
|
|
||||||
.set_album_seq(
|
|
||||||
ArtistId::new(artist_name),
|
|
||||||
AlbumId::new(album_name),
|
|
||||||
seq_value.value,
|
|
||||||
)
|
|
||||||
.expect("failed to set sequence value"),
|
|
||||||
AlbumSeqCommand::Clear => music_hoard
|
|
||||||
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
|
|
||||||
.expect("failed to clear sequence value"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let opt = Opt::from_args();
|
|
||||||
|
|
||||||
let db = JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path));
|
|
||||||
|
|
||||||
let mut music_hoard = MusicHoardBuilder::default().set_database(db).build();
|
|
||||||
music_hoard
|
|
||||||
.reload_database()
|
|
||||||
.expect("failed to load MusicHoard database");
|
|
||||||
opt.command.handle(&mut music_hoard);
|
|
||||||
}
|
|
@ -11,7 +11,7 @@ use crate::core::collection::{self, Collection};
|
|||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait IDatabase {
|
pub trait IDatabase {
|
||||||
/// Load collection from the database.
|
/// Load collection from the database.
|
||||||
fn load(&self) -> Result<Collection, LoadError>;
|
fn load(&mut self) -> Result<Collection, LoadError>;
|
||||||
|
|
||||||
/// Save collection to the database.
|
/// Save collection to the database.
|
||||||
fn save(&mut self, collection: &Collection) -> Result<(), SaveError>;
|
fn save(&mut self, collection: &Collection) -> Result<(), SaveError>;
|
||||||
@ -21,7 +21,7 @@ pub trait IDatabase {
|
|||||||
pub struct NullDatabase;
|
pub struct NullDatabase;
|
||||||
|
|
||||||
impl IDatabase for NullDatabase {
|
impl IDatabase for NullDatabase {
|
||||||
fn load(&self) -> Result<Collection, LoadError> {
|
fn load(&mut self) -> Result<Collection, LoadError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn null_database_load() {
|
fn null_database_load() {
|
||||||
let database = NullDatabase;
|
let mut database = NullDatabase;
|
||||||
assert!(database.load().unwrap().is_empty());
|
assert!(database.load().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,5 +123,9 @@ mod tests {
|
|||||||
let io_err: SaveError = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
let io_err: SaveError = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
||||||
assert!(!io_err.to_string().is_empty());
|
assert!(!io_err.to_string().is_empty());
|
||||||
assert!(!format!("{:?}", io_err).is_empty());
|
assert!(!format!("{:?}", io_err).is_empty());
|
||||||
|
|
||||||
|
let sd_err: SaveError = SaveError::SerDeError(String::from("serde"));
|
||||||
|
assert!(!sd_err.to_string().is_empty());
|
||||||
|
assert!(!format!("{:?}", sd_err).is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
//! Module for storing MusicHoard data in a JSON file database.
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::external::database::json::IJsonDatabaseBackend;
|
|
||||||
|
|
||||||
/// JSON database backend that uses a local file for persistent storage.
|
|
||||||
pub struct JsonDatabaseFileBackend {
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsonDatabaseFileBackend {
|
|
||||||
/// Create a [`JsonDatabaseFileBackend`] that will read/write to the provided path.
|
|
||||||
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
|
||||||
JsonDatabaseFileBackend { path: path.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IJsonDatabaseBackend for JsonDatabaseFileBackend {
|
|
||||||
fn read(&self) -> Result<String, std::io::Error> {
|
|
||||||
// Read entire file to memory as for now this is faster than a buffered read from disk:
|
|
||||||
// https://github.com/serde-rs/json/issues/160
|
|
||||||
fs::read_to_string(&self.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
|
|
||||||
fs::write(&self.path, json)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,168 +0,0 @@
|
|||||||
//! Module for storing MusicHoard data in a JSON file database.
|
|
||||||
|
|
||||||
pub mod backend;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use mockall::automock;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
core::{
|
|
||||||
collection::Collection,
|
|
||||||
interface::database::{IDatabase, LoadError, SaveError},
|
|
||||||
},
|
|
||||||
external::database::serde::{deserialize::DeserializeDatabase, serialize::SerializeDatabase},
|
|
||||||
};
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for LoadError {
|
|
||||||
fn from(err: serde_json::Error) -> LoadError {
|
|
||||||
LoadError::SerDeError(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for SaveError {
|
|
||||||
fn from(err: serde_json::Error) -> SaveError {
|
|
||||||
SaveError::SerDeError(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for the JSON database backend.
|
|
||||||
#[cfg_attr(test, automock)]
|
|
||||||
pub trait IJsonDatabaseBackend {
|
|
||||||
/// Read the JSON string from the backend.
|
|
||||||
fn read(&self) -> Result<String, std::io::Error>;
|
|
||||||
|
|
||||||
/// Write the JSON string to the backend.
|
|
||||||
fn write(&mut self, json: &str) -> Result<(), std::io::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON database.
|
|
||||||
pub struct JsonDatabase<JDB> {
|
|
||||||
backend: JDB,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<JDB: IJsonDatabaseBackend> JsonDatabase<JDB> {
|
|
||||||
/// Create a new JSON database with the provided backend, e.g.
|
|
||||||
/// [`backend::JsonDatabaseFileBackend`].
|
|
||||||
pub fn new(backend: JDB) -> Self {
|
|
||||||
JsonDatabase { backend }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> {
|
|
||||||
fn load(&self) -> Result<Collection, LoadError> {
|
|
||||||
let serialized = self.backend.read()?;
|
|
||||||
let database: DeserializeDatabase = serde_json::from_str(&serialized)?;
|
|
||||||
Ok(database.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {
|
|
||||||
let database: SerializeDatabase = collection.into();
|
|
||||||
let serialized = serde_json::to_string(&database)?;
|
|
||||||
self.backend.write(&serialized)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod testmod;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use mockall::predicate;
|
|
||||||
|
|
||||||
use crate::core::{
|
|
||||||
collection::{artist::Artist, Collection},
|
|
||||||
testmod::FULL_COLLECTION,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use testmod::DATABASE_JSON;
|
|
||||||
|
|
||||||
fn expected() -> Collection {
|
|
||||||
let mut expected = FULL_COLLECTION.to_owned();
|
|
||||||
for artist in expected.iter_mut() {
|
|
||||||
for album in artist.albums.iter_mut() {
|
|
||||||
album.tracks.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expected
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn save() {
|
|
||||||
let write_data = FULL_COLLECTION.to_owned();
|
|
||||||
let input = DATABASE_JSON.to_owned();
|
|
||||||
|
|
||||||
let mut backend = MockIJsonDatabaseBackend::new();
|
|
||||||
backend
|
|
||||||
.expect_write()
|
|
||||||
.with(predicate::eq(input))
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_| Ok(()));
|
|
||||||
|
|
||||||
JsonDatabase::new(backend).save(&write_data).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load() {
|
|
||||||
let expected = expected();
|
|
||||||
let result = Ok(DATABASE_JSON.to_owned());
|
|
||||||
eprintln!("{DATABASE_JSON}");
|
|
||||||
|
|
||||||
let mut backend = MockIJsonDatabaseBackend::new();
|
|
||||||
backend.expect_read().times(1).return_once(|| result);
|
|
||||||
|
|
||||||
let read_data: Vec<Artist> = JsonDatabase::new(backend).load().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(read_data, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reverse() {
|
|
||||||
let input = DATABASE_JSON.to_owned();
|
|
||||||
let result = Ok(input.clone());
|
|
||||||
|
|
||||||
let mut backend = MockIJsonDatabaseBackend::new();
|
|
||||||
backend
|
|
||||||
.expect_write()
|
|
||||||
.with(predicate::eq(input))
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_| Ok(()));
|
|
||||||
backend.expect_read().times(1).return_once(|| result);
|
|
||||||
let mut database = JsonDatabase::new(backend);
|
|
||||||
|
|
||||||
let write_data = FULL_COLLECTION.to_owned();
|
|
||||||
database.save(&write_data).unwrap();
|
|
||||||
let read_data: Vec<Artist> = database.load().unwrap();
|
|
||||||
|
|
||||||
// Album information is not saved to disk.
|
|
||||||
let expected = expected();
|
|
||||||
assert_eq!(read_data, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_errors() {
|
|
||||||
let json = String::from("");
|
|
||||||
let serde_err = serde_json::from_str::<DeserializeDatabase>(&json);
|
|
||||||
assert!(serde_err.is_err());
|
|
||||||
|
|
||||||
let serde_err: LoadError = serde_err.unwrap_err().into();
|
|
||||||
assert!(!serde_err.to_string().is_empty());
|
|
||||||
assert!(!format!("{:?}", serde_err).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn save_errors() {
|
|
||||||
// serde_json will raise an error as it has certain requirements on keys.
|
|
||||||
let mut object = HashMap::<Result<(), ()>, String>::new();
|
|
||||||
object.insert(Ok(()), String::from("string"));
|
|
||||||
let serde_err = serde_json::to_string(&object);
|
|
||||||
assert!(serde_err.is_err());
|
|
||||||
|
|
||||||
let serde_err: SaveError = serde_err.unwrap_err().into();
|
|
||||||
assert!(!serde_err.to_string().is_empty());
|
|
||||||
assert!(!format!("{:?}", serde_err).is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
pub static DATABASE_JSON: &str = "{\
|
|
||||||
\"V20250103\":\
|
|
||||||
[\
|
|
||||||
{\
|
|
||||||
\"name\":\"Album_Artist ‘A’\",\
|
|
||||||
\"sort\":null,\
|
|
||||||
\"musicbrainz\":{\"Some\":\"00000000-0000-0000-0000-000000000000\"},\
|
|
||||||
\"properties\":{\
|
|
||||||
\"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\
|
|
||||||
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\
|
|
||||||
},\
|
|
||||||
\"albums\":[\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title a.a\",\"lib_id\":{\"Value\":1},\
|
|
||||||
\"date\":{\"year\":1998,\"month\":null,\"day\":null},\"seq\":1,\
|
|
||||||
\"musicbrainz\":{\"Some\":\"00000000-0000-0000-0000-000000000000\"},\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title a.b\",\"lib_id\":{\"Value\":2},\
|
|
||||||
\"date\":{\"year\":2015,\"month\":4,\"day\":null},\"seq\":1,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
}\
|
|
||||||
]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"name\":\"Album_Artist ‘B’\",\
|
|
||||||
\"sort\":null,\
|
|
||||||
\"musicbrainz\":{\"Some\":\"11111111-1111-1111-1111-111111111111\"},\
|
|
||||||
\"properties\":{\
|
|
||||||
\"Bandcamp\":[\"https://artist-b.bandcamp.com/\"],\
|
|
||||||
\"MusicButler\":[\
|
|
||||||
\"https://www.musicbutler.io/artist-page/111111111\",\
|
|
||||||
\"https://www.musicbutler.io/artist-page/111111112\"\
|
|
||||||
],\
|
|
||||||
\"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\
|
|
||||||
},\
|
|
||||||
\"albums\":[\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title b.a\",\"lib_id\":{\"Value\":3},\
|
|
||||||
\"date\":{\"year\":2003,\"month\":6,\"day\":6},\"seq\":1,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title b.b\",\"lib_id\":{\"Value\":4},\
|
|
||||||
\"date\":{\"year\":2008,\"month\":null,\"day\":null},\"seq\":3,\
|
|
||||||
\"musicbrainz\":{\"Some\":\"11111111-1111-1111-1111-111111111111\"},\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title b.c\",\"lib_id\":{\"Value\":5},\
|
|
||||||
\"date\":{\"year\":2009,\"month\":null,\"day\":null},\"seq\":2,\
|
|
||||||
\"musicbrainz\":{\"Some\":\"11111111-1111-1111-1111-111111111112\"},\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title b.d\",\"lib_id\":{\"Value\":6},\
|
|
||||||
\"date\":{\"year\":2015,\"month\":null,\"day\":null},\"seq\":4,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
}\
|
|
||||||
]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"name\":\"The Album_Artist ‘C’\",\
|
|
||||||
\"sort\":\"Album_Artist ‘C’, The\",\
|
|
||||||
\"musicbrainz\":\"CannotHaveMbid\",\
|
|
||||||
\"properties\":{},\
|
|
||||||
\"albums\":[\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title c.a\",\"lib_id\":{\"Value\":7},\
|
|
||||||
\"date\":{\"year\":1985,\"month\":null,\"day\":null},\"seq\":0,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title c.b\",\"lib_id\":{\"Value\":8},\
|
|
||||||
\"date\":{\"year\":2018,\"month\":null,\"day\":null},\"seq\":0,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
}\
|
|
||||||
]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"name\":\"Album_Artist ‘D’\",\
|
|
||||||
\"sort\":null,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"properties\":{},\
|
|
||||||
\"albums\":[\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title d.a\",\"lib_id\":{\"Value\":9},\
|
|
||||||
\"date\":{\"year\":1995,\"month\":null,\"day\":null},\"seq\":0,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
},\
|
|
||||||
{\
|
|
||||||
\"title\":\"album_title d.b\",\"lib_id\":{\"Value\":10},\
|
|
||||||
\"date\":{\"year\":2028,\"month\":null,\"day\":null},\"seq\":0,\
|
|
||||||
\"musicbrainz\":\"None\",\
|
|
||||||
\"primary_type\":\"Album\",\"secondary_types\":[]\
|
|
||||||
}\
|
|
||||||
]\
|
|
||||||
}\
|
|
||||||
]\
|
|
||||||
}";
|
|
@ -1,4 +1,5 @@
|
|||||||
#[cfg(feature = "database-json")]
|
#[cfg(feature = "database-sqlite")]
|
||||||
pub mod json;
|
pub mod sql;
|
||||||
#[cfg(feature = "database-json")]
|
|
||||||
|
#[cfg(feature = "database-sqlite")]
|
||||||
mod serde;
|
mod serde;
|
||||||
|
@ -13,8 +13,8 @@ pub enum AlbumLibIdDef {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerdeAlbumLibId(#[serde(with = "AlbumLibIdDef")] AlbumLibId);
|
pub struct SerdeAlbumLibId(#[serde(with = "AlbumLibIdDef")] pub AlbumLibId);
|
||||||
|
|
||||||
impl From<SerdeAlbumLibId> for AlbumLibId {
|
impl From<SerdeAlbumLibId> for AlbumLibId {
|
||||||
fn from(value: SerdeAlbumLibId) -> Self {
|
fn from(value: SerdeAlbumLibId) -> Self {
|
||||||
@ -36,8 +36,8 @@ pub struct AlbumDateDef {
|
|||||||
day: Option<u8>,
|
day: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerdeAlbumDate(#[serde(with = "AlbumDateDef")] AlbumDate);
|
pub struct SerdeAlbumDate(#[serde(with = "AlbumDateDef")] pub AlbumDate);
|
||||||
|
|
||||||
impl From<SerdeAlbumDate> for AlbumDate {
|
impl From<SerdeAlbumDate> for AlbumDate {
|
||||||
fn from(value: SerdeAlbumDate) -> Self {
|
fn from(value: SerdeAlbumDate) -> Self {
|
||||||
@ -69,8 +69,8 @@ pub enum AlbumPrimaryTypeDef {
|
|||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType);
|
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] pub AlbumPrimaryType);
|
||||||
|
|
||||||
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
|
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
|
||||||
fn from(value: SerdeAlbumPrimaryType) -> Self {
|
fn from(value: SerdeAlbumPrimaryType) -> Self {
|
||||||
@ -101,8 +101,8 @@ pub enum AlbumSecondaryTypeDef {
|
|||||||
FieldRecording,
|
FieldRecording,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType);
|
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] pub AlbumSecondaryType);
|
||||||
|
|
||||||
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
|
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
|
||||||
fn from(value: SerdeAlbumSecondaryType) -> Self {
|
fn from(value: SerdeAlbumSecondaryType) -> Self {
|
||||||
|
@ -23,38 +23,42 @@ pub enum DeserializeDatabase {
|
|||||||
impl From<DeserializeDatabase> for Collection {
|
impl From<DeserializeDatabase> for Collection {
|
||||||
fn from(database: DeserializeDatabase) -> Self {
|
fn from(database: DeserializeDatabase) -> Self {
|
||||||
match database {
|
match database {
|
||||||
DeserializeDatabase::V20250103(collection) => {
|
DeserializeDatabase::V20250103(db) => {
|
||||||
collection.into_iter().map(Into::into).collect()
|
let mut collection: Collection = db.into_iter().map(Into::into).collect();
|
||||||
|
collection.sort_unstable();
|
||||||
|
collection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct DeserializeArtist {
|
pub struct DeserializeArtist {
|
||||||
name: String,
|
pub name: String,
|
||||||
sort: Option<String>,
|
pub mb_ref: DeserializeMbRefOption,
|
||||||
musicbrainz: DeserializeMbRefOption,
|
pub sort: Option<String>,
|
||||||
properties: HashMap<String, Vec<String>>,
|
pub properties: HashMap<String, Vec<String>>,
|
||||||
albums: Vec<DeserializeAlbum>,
|
pub albums: Vec<DeserializeAlbum>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct DeserializeAlbum {
|
pub struct DeserializeAlbum {
|
||||||
title: String,
|
pub title: String,
|
||||||
lib_id: SerdeAlbumLibId,
|
pub lib_id: SerdeAlbumLibId,
|
||||||
date: SerdeAlbumDate,
|
pub mb_ref: DeserializeMbRefOption,
|
||||||
seq: u8,
|
pub date: SerdeAlbumDate,
|
||||||
musicbrainz: DeserializeMbRefOption,
|
pub seq: u8,
|
||||||
primary_type: Option<SerdeAlbumPrimaryType>,
|
pub primary_type: Option<SerdeAlbumPrimaryType>,
|
||||||
secondary_types: Vec<SerdeAlbumSecondaryType>,
|
pub secondary_types: Vec<SerdeAlbumSecondaryType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct DeserializeMbRefOption(#[serde(with = "MbRefOptionDef")] MbRefOption<DeserializeMbid>);
|
pub struct DeserializeMbRefOption(
|
||||||
|
#[serde(with = "MbRefOptionDef")] pub MbRefOption<DeserializeMbid>,
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DeserializeMbid(Mbid);
|
pub struct DeserializeMbid(pub Mbid);
|
||||||
|
|
||||||
macro_rules! impl_from_for_mb_ref_option {
|
macro_rules! impl_from_for_mb_ref_option {
|
||||||
($ref:ty) => {
|
($ref:ty) => {
|
||||||
@ -110,18 +114,20 @@ impl<'de> Deserialize<'de> for DeserializeMbid {
|
|||||||
|
|
||||||
impl From<DeserializeArtist> for Artist {
|
impl From<DeserializeArtist> for Artist {
|
||||||
fn from(artist: DeserializeArtist) -> Self {
|
fn from(artist: DeserializeArtist) -> Self {
|
||||||
|
let mut albums: Vec<Album> = artist.albums.into_iter().map(Into::into).collect();
|
||||||
|
albums.sort_unstable();
|
||||||
Artist {
|
Artist {
|
||||||
meta: ArtistMeta {
|
meta: ArtistMeta {
|
||||||
id: ArtistId {
|
id: ArtistId {
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
mb_ref: artist.musicbrainz.into(),
|
mb_ref: artist.mb_ref.into(),
|
||||||
},
|
},
|
||||||
sort: artist.sort,
|
sort: artist.sort,
|
||||||
info: ArtistInfo {
|
info: ArtistInfo {
|
||||||
properties: artist.properties,
|
properties: artist.properties,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
albums: artist.albums.into_iter().map(Into::into).collect(),
|
albums,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +139,7 @@ impl From<DeserializeAlbum> for Album {
|
|||||||
id: AlbumId {
|
id: AlbumId {
|
||||||
title: album.title,
|
title: album.title,
|
||||||
lib_id: album.lib_id.into(),
|
lib_id: album.lib_id.into(),
|
||||||
mb_ref: album.musicbrainz.into(),
|
mb_ref: album.mb_ref.into(),
|
||||||
},
|
},
|
||||||
date: album.date.into(),
|
date: album.date.into(),
|
||||||
seq: AlbumSeq(album.seq),
|
seq: AlbumSeq(album.seq),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//! Helper module for backends that can use serde for (de)serialisation.
|
//! Helper module for backends that can use serde for (de)serialisation.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
pub mod deserialize;
|
pub mod deserialize;
|
||||||
pub mod serialize;
|
pub mod serialize;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@ -22,32 +22,32 @@ impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerializeArtist<'a> {
|
pub struct SerializeArtist<'a> {
|
||||||
name: &'a str,
|
pub name: &'a str,
|
||||||
sort: Option<&'a str>,
|
pub mb_ref: SerializeMbRefOption<'a>,
|
||||||
musicbrainz: SerializeMbRefOption<'a>,
|
pub sort: &'a Option<String>,
|
||||||
properties: BTreeMap<&'a str, &'a Vec<String>>,
|
pub properties: &'a HashMap<String, Vec<String>>,
|
||||||
albums: Vec<SerializeAlbum<'a>>,
|
pub albums: Vec<SerializeAlbum<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerializeAlbum<'a> {
|
pub struct SerializeAlbum<'a> {
|
||||||
title: &'a str,
|
pub title: &'a str,
|
||||||
lib_id: SerdeAlbumLibId,
|
pub lib_id: SerdeAlbumLibId,
|
||||||
date: SerdeAlbumDate,
|
pub mb_ref: SerializeMbRefOption<'a>,
|
||||||
seq: u8,
|
pub date: SerdeAlbumDate,
|
||||||
musicbrainz: SerializeMbRefOption<'a>,
|
pub seq: u8,
|
||||||
primary_type: Option<SerdeAlbumPrimaryType>,
|
pub primary_type: Option<SerdeAlbumPrimaryType>,
|
||||||
secondary_types: Vec<SerdeAlbumSecondaryType>,
|
pub secondary_types: Vec<SerdeAlbumSecondaryType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
pub struct SerializeMbRefOption<'a>(
|
pub struct SerializeMbRefOption<'a>(
|
||||||
#[serde(with = "MbRefOptionDef")] MbRefOption<SerializeMbid<'a>>,
|
#[serde(with = "MbRefOptionDef")] MbRefOption<SerializeMbid<'a>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct SerializeMbid<'a>(&'a Mbid);
|
pub struct SerializeMbid<'a>(&'a Mbid);
|
||||||
|
|
||||||
impl<'a, T: IMusicBrainzRef> From<&'a MbRefOption<T>> for SerializeMbRefOption<'a> {
|
impl<'a, T: IMusicBrainzRef> From<&'a MbRefOption<T>> for SerializeMbRefOption<'a> {
|
||||||
@ -75,15 +75,9 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> {
|
|||||||
fn from(artist: &'a Artist) -> Self {
|
fn from(artist: &'a Artist) -> Self {
|
||||||
SerializeArtist {
|
SerializeArtist {
|
||||||
name: &artist.meta.id.name,
|
name: &artist.meta.id.name,
|
||||||
sort: artist.meta.sort.as_deref(),
|
mb_ref: (&artist.meta.id.mb_ref).into(),
|
||||||
musicbrainz: (&artist.meta.id.mb_ref).into(),
|
sort: &artist.meta.sort,
|
||||||
properties: artist
|
properties: &artist.meta.info.properties,
|
||||||
.meta
|
|
||||||
.info
|
|
||||||
.properties
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k.as_ref(), v))
|
|
||||||
.collect(),
|
|
||||||
albums: artist.albums.iter().map(Into::into).collect(),
|
albums: artist.albums.iter().map(Into::into).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,9 +88,9 @@ impl<'a> From<&'a Album> for SerializeAlbum<'a> {
|
|||||||
SerializeAlbum {
|
SerializeAlbum {
|
||||||
title: &album.meta.id.title,
|
title: &album.meta.id.title,
|
||||||
lib_id: album.meta.id.lib_id.into(),
|
lib_id: album.meta.id.lib_id.into(),
|
||||||
|
mb_ref: (&album.meta.id.mb_ref).into(),
|
||||||
date: album.meta.date.into(),
|
date: album.meta.date.into(),
|
||||||
seq: album.meta.seq.0,
|
seq: album.meta.seq.0,
|
||||||
musicbrainz: (&album.meta.id.mb_ref).into(),
|
|
||||||
primary_type: album.meta.info.primary_type.map(Into::into),
|
primary_type: album.meta.info.primary_type.map(Into::into),
|
||||||
secondary_types: album
|
secondary_types: album
|
||||||
.meta
|
.meta
|
||||||
|
246
src/external/database/sql/backend.rs
Normal file
246
src/external/database/sql/backend.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
//! Module for storing MusicHoard data in a SQLite database.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use rusqlite::{
|
||||||
|
self, types::FromSql, CachedStatement, Connection, Params, Row, Rows, Statement, Transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
collection::album::AlbumDate,
|
||||||
|
external::database::{
|
||||||
|
serde::{
|
||||||
|
common::SerdeAlbumDate,
|
||||||
|
deserialize::{DeserializeAlbum, DeserializeArtist},
|
||||||
|
serialize::{SerializeAlbum, SerializeArtist},
|
||||||
|
},
|
||||||
|
sql::{Error, ISqlDatabaseBackend, ISqlTransactionBackend},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// SQLite database backend that uses SQLite as the implementation.
|
||||||
|
pub struct SqlDatabaseSqliteBackend {
|
||||||
|
conn: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SqlTransactionSqliteBackend<'conn> {
|
||||||
|
tx: Transaction<'conn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqlDatabaseSqliteBackend {
|
||||||
|
/// Create a [`SqlDatabaseSqliteBackend`] that will read/write to the provided database.
|
||||||
|
pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
|
||||||
|
Ok(SqlDatabaseSqliteBackend {
|
||||||
|
conn: Connection::open(path.into()).map_err(|err| Error::OpenError(err.to_string()))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqlTransactionSqliteBackend<'_> {
|
||||||
|
// We only prepare strings known at compile time so errors in prep are bugs.
|
||||||
|
fn prepare(&self, sql: &'static str) -> Statement {
|
||||||
|
self.tx.prepare(sql).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only prepare strings known at compile time so errors in prep are bugs.
|
||||||
|
fn prepare_cached(&self, sql: &'static str) -> CachedStatement {
|
||||||
|
self.tx.prepare_cached(sql).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute<P: Params>(stmt: &mut Statement, params: P) -> Result<(), Error> {
|
||||||
|
stmt.execute(params)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|err| Error::ExecError(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query<'s, P: Params>(stmt: &'s mut Statement, params: P) -> Result<Rows<'s>, Error> {
|
||||||
|
stmt.query(params)
|
||||||
|
.map_err(|err| Error::ExecError(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_row<'r, 's>(rows: &'r mut Rows<'s>) -> Result<Option<&'r Row<'s>>, Error> {
|
||||||
|
rows.next()
|
||||||
|
.map_err(|err| Error::SerDeError(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_value<T: FromSql>(row: &Row<'_>, idx: usize) -> Result<T, Error> {
|
||||||
|
row.get(idx)
|
||||||
|
.map_err(|err| Error::SerDeError(err.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'conn> ISqlDatabaseBackend<'conn> for SqlDatabaseSqliteBackend {
|
||||||
|
type Tx = SqlTransactionSqliteBackend<'conn>;
|
||||||
|
|
||||||
|
fn transaction(&'conn mut self) -> Result<Self::Tx, Error> {
|
||||||
|
self.conn
|
||||||
|
.transaction()
|
||||||
|
.map(|tx| SqlTransactionSqliteBackend { tx })
|
||||||
|
.map_err(|err| Error::OpenError(err.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
|
||||||
|
fn commit(self) -> Result<(), Error> {
|
||||||
|
self.tx
|
||||||
|
.commit()
|
||||||
|
.map_err(|err| Error::ExecError(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_database_metadata_table(&self) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare(
|
||||||
|
"CREATE TABLE IF NOT EXISTS database_metadata (
|
||||||
|
name TEXT NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
);
|
||||||
|
Self::execute(&mut stmt, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_database_metadata_table(&self) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare_cached("DROP TABLE database_metadata");
|
||||||
|
Self::execute(&mut stmt, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_artists_table(&self) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare(
|
||||||
|
"CREATE TABLE IF NOT EXISTS artists (
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
mbid JSON NOT NULL DEFAULT '\"None\"',
|
||||||
|
sort TEXT NULL,
|
||||||
|
properties JSON NOT NULL DEFAULT '{}'
|
||||||
|
)",
|
||||||
|
);
|
||||||
|
Self::execute(&mut stmt, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_artists_table(&self) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare_cached("DROP TABLE artists");
|
||||||
|
Self::execute(&mut stmt, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_albums_table(&self) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare(
|
||||||
|
"CREATE TABLE IF NOT EXISTS albums (
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
lib_id JSON NOT NULL DEFAULT '\"None\"',
|
||||||
|
mbid JSON NOT NULL DEFAULT '\"None\"',
|
||||||
|
artist_name TEXT NOT NULL,
|
||||||
|
year INT NULL,
|
||||||
|
month INT NULL,
|
||||||
|
day INT NULL,
|
||||||
|
seq INT NOT NULL,
|
||||||
|
primary_type JSON NOT NULL DEFAULT 'null',
|
||||||
|
secondary_types JSON NOT NULL DEFAULT '[]'
|
||||||
|
)",
|
||||||
|
);
|
||||||
|
Self::execute(&mut stmt, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_albums_table(&self) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare_cached("DROP TABLE albums");
|
||||||
|
Self::execute(&mut stmt, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_database_version(&self, version: &str) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare_cached(
|
||||||
|
"INSERT INTO database_metadata (name, value)
|
||||||
|
VALUES (?1, ?2)",
|
||||||
|
);
|
||||||
|
Self::execute(&mut stmt, ("version", version))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_database_version<'a>(&self) -> Result<Option<String>, Error> {
|
||||||
|
let mut stmt =
|
||||||
|
self.prepare_cached("SELECT value FROM database_metadata WHERE name = 'version'");
|
||||||
|
let mut rows = Self::query(&mut stmt, ())?;
|
||||||
|
|
||||||
|
Self::next_row(&mut rows)?
|
||||||
|
.map(|row| Self::get_value(row, 0))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_artist(&self, artist: &SerializeArtist<'_>) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare_cached(
|
||||||
|
"INSERT INTO artists (name, mbid, sort, properties)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
);
|
||||||
|
Self::execute(
|
||||||
|
&mut stmt,
|
||||||
|
(
|
||||||
|
artist.name,
|
||||||
|
serde_json::to_string(&artist.mb_ref)?,
|
||||||
|
artist.sort,
|
||||||
|
serde_json::to_string(&artist.properties)?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error> {
|
||||||
|
let mut stmt = self.prepare_cached("SELECT name, mbid, sort, properties FROM artists");
|
||||||
|
let mut rows = Self::query(&mut stmt, ())?;
|
||||||
|
|
||||||
|
let mut artists = vec![];
|
||||||
|
while let Some(row) = Self::next_row(&mut rows)? {
|
||||||
|
artists.push(DeserializeArtist {
|
||||||
|
name: Self::get_value(row, 0)?,
|
||||||
|
mb_ref: serde_json::from_str(&Self::get_value::<String>(row, 1)?)?,
|
||||||
|
sort: Self::get_value(row, 2)?,
|
||||||
|
properties: serde_json::from_str(&Self::get_value::<String>(row, 3)?)?,
|
||||||
|
albums: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(artists)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_album(&self, artist_name: &str, album: &SerializeAlbum<'_>) -> Result<(), Error> {
|
||||||
|
let mut stmt = self.prepare_cached(
|
||||||
|
"INSERT INTO albums (title, lib_id, mbid, artist_name,
|
||||||
|
year, month, day, seq, primary_type, secondary_types)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||||
|
);
|
||||||
|
Self::execute(
|
||||||
|
&mut stmt,
|
||||||
|
(
|
||||||
|
album.title,
|
||||||
|
serde_json::to_string(&album.lib_id)?,
|
||||||
|
serde_json::to_string(&album.mb_ref)?,
|
||||||
|
artist_name,
|
||||||
|
album.date.0.year,
|
||||||
|
album.date.0.month,
|
||||||
|
album.date.0.day,
|
||||||
|
album.seq,
|
||||||
|
serde_json::to_string(&album.primary_type)?,
|
||||||
|
serde_json::to_string(&album.secondary_types)?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_artist_albums(&self, artist_name: &str) -> Result<Vec<DeserializeAlbum>, Error> {
|
||||||
|
let mut stmt = self.prepare_cached(
|
||||||
|
"SELECT title, lib_id, mbid, year, month, day, seq, primary_type, secondary_types
|
||||||
|
FROM albums WHERE artist_name = ?1",
|
||||||
|
);
|
||||||
|
let mut rows = Self::query(&mut stmt, [artist_name])?;
|
||||||
|
|
||||||
|
let mut albums = vec![];
|
||||||
|
while let Some(row) = Self::next_row(&mut rows)? {
|
||||||
|
albums.push(DeserializeAlbum {
|
||||||
|
title: Self::get_value(row, 0)?,
|
||||||
|
lib_id: serde_json::from_str(&Self::get_value::<String>(row, 1)?)?,
|
||||||
|
mb_ref: serde_json::from_str(&Self::get_value::<String>(row, 2)?)?,
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(
|
||||||
|
Self::get_value(row, 3)?,
|
||||||
|
Self::get_value(row, 4)?,
|
||||||
|
Self::get_value(row, 5)?,
|
||||||
|
)),
|
||||||
|
seq: Self::get_value(row, 6)?,
|
||||||
|
primary_type: serde_json::from_str(&Self::get_value::<String>(row, 7)?)?,
|
||||||
|
secondary_types: serde_json::from_str(&Self::get_value::<String>(row, 8)?)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(albums)
|
||||||
|
}
|
||||||
|
}
|
448
src/external/database/sql/mod.rs
Normal file
448
src/external/database/sql/mod.rs
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
//! Module for storing MusicHoard data in a SQLdatabase.
|
||||||
|
|
||||||
|
pub mod backend;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use mockall::automock;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::{
|
||||||
|
collection::Collection,
|
||||||
|
interface::database::{IDatabase, LoadError, SaveError},
|
||||||
|
},
|
||||||
|
external::database::serde::{
|
||||||
|
deserialize::{DeserializeAlbum, DeserializeArtist, DeserializeDatabase},
|
||||||
|
serialize::{SerializeAlbum, SerializeArtist, SerializeDatabase},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const V20250103: &str = "V20250103";
|
||||||
|
|
||||||
|
/// Trait for the SQL database backend.
|
||||||
|
pub trait ISqlDatabaseBackend<'conn> {
|
||||||
|
type Tx: ISqlTransactionBackend + 'conn;
|
||||||
|
|
||||||
|
/// Begin an SQL transaction.
|
||||||
|
fn transaction(&'conn mut self) -> Result<Self::Tx, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for the SQL database backend.
|
||||||
|
#[cfg_attr(test, automock)]
|
||||||
|
pub trait ISqlTransactionBackend {
|
||||||
|
/// Commit transaction.
|
||||||
|
fn commit(self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Create the database metadata table (if needed).
|
||||||
|
fn create_database_metadata_table(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Drop the database metadata table.
|
||||||
|
fn drop_database_metadata_table(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Create the artists table (if needed).
|
||||||
|
fn create_artists_table(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Drop the artists table.
|
||||||
|
fn drop_artists_table(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Create the albums table (if needed).
|
||||||
|
fn create_albums_table(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Drop the albums table.
|
||||||
|
fn drop_albums_table(&self) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Set the database version.
|
||||||
|
fn insert_database_version(&self, version: &str) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Get the database version.
|
||||||
|
fn select_database_version(&self) -> Result<Option<String>, Error>;
|
||||||
|
|
||||||
|
/// Insert an artist into the artist table.
|
||||||
|
#[allow(clippy::needless_lifetimes)] // Conflicts with automock.
|
||||||
|
fn insert_artist<'a>(&self, artist: &SerializeArtist<'a>) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Get all artists from the artist table.
|
||||||
|
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error>;
|
||||||
|
|
||||||
|
/// Insert an artist into the artist table.
|
||||||
|
#[allow(clippy::needless_lifetimes)] // Conflicts with automock.
|
||||||
|
fn insert_album<'a>(&self, artist_name: &str, album: &SerializeAlbum<'a>) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Get all albums from the album table.
|
||||||
|
fn select_artist_albums(&self, artist_name: &str) -> Result<Vec<DeserializeAlbum>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors for SQL database backend.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// An error occurred when connecting to the database.
|
||||||
|
OpenError(String),
|
||||||
|
/// An error occurred during serialisation.
|
||||||
|
SerDeError(String),
|
||||||
|
/// An error occurred during SQL execution.
|
||||||
|
ExecError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
Error::SerDeError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Self::OpenError(ref s) => {
|
||||||
|
write!(f, "an error occurred when connecting to the database: {s}")
|
||||||
|
}
|
||||||
|
Self::SerDeError(ref s) => write!(f, "an error occurred during serialisation : {s}"),
|
||||||
|
Self::ExecError(ref s) => write!(f, "an error occurred during SQL execution: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for SaveError {
|
||||||
|
fn from(value: Error) -> Self {
|
||||||
|
match value {
|
||||||
|
Error::SerDeError(s) => SaveError::SerDeError(s),
|
||||||
|
_ => SaveError::IoError(value.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for LoadError {
|
||||||
|
fn from(value: Error) -> Self {
|
||||||
|
match value {
|
||||||
|
Error::SerDeError(s) => LoadError::SerDeError(s),
|
||||||
|
_ => LoadError::IoError(value.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SQL database.
|
||||||
|
pub struct SqlDatabase<SDB> {
|
||||||
|
backend: SDB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> SqlDatabase<SDB> {
|
||||||
|
/// Create a new SQL database with the provided backend, e.g.
|
||||||
|
/// [`backend::SqlDatabaseSqliteBackend`].
|
||||||
|
pub fn new(backend: SDB) -> Result<Self, Error> {
|
||||||
|
let mut db = SqlDatabase { backend };
|
||||||
|
let tx = db.backend.transaction()?;
|
||||||
|
Self::create_tables(&tx)?;
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tables<Tx: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
|
||||||
|
tx.create_database_metadata_table()?;
|
||||||
|
tx.create_artists_table()?;
|
||||||
|
tx.create_albums_table()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_tables<Tx: ISqlTransactionBackend>(tx: &Tx) -> Result<(), Error> {
|
||||||
|
tx.drop_albums_table()?;
|
||||||
|
tx.drop_artists_table()?;
|
||||||
|
tx.drop_database_metadata_table()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SDB: for<'conn> ISqlDatabaseBackend<'conn>> IDatabase for SqlDatabase<SDB> {
|
||||||
|
fn load(&mut self) -> Result<Collection, LoadError> {
|
||||||
|
let tx = self.backend.transaction()?;
|
||||||
|
|
||||||
|
let version = tx
|
||||||
|
.select_database_version()?
|
||||||
|
.ok_or_else(|| Error::SerDeError(String::from("missing database version")))?;
|
||||||
|
|
||||||
|
let database = match version.as_str() {
|
||||||
|
V20250103 => {
|
||||||
|
let mut coll = tx.select_all_artists()?;
|
||||||
|
for artist in coll.iter_mut() {
|
||||||
|
artist.albums.extend(tx.select_artist_albums(&artist.name)?);
|
||||||
|
}
|
||||||
|
Ok(DeserializeDatabase::V20250103(coll))
|
||||||
|
}
|
||||||
|
s => Err(Error::SerDeError(format!("unknown database version: {s}"))),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(database.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {
|
||||||
|
let database: SerializeDatabase = collection.into();
|
||||||
|
let tx = self.backend.transaction()?;
|
||||||
|
|
||||||
|
Self::drop_tables(&tx)?;
|
||||||
|
Self::create_tables(&tx)?;
|
||||||
|
|
||||||
|
match database {
|
||||||
|
SerializeDatabase::V20250103(artists) => {
|
||||||
|
tx.insert_database_version(V20250103)?;
|
||||||
|
for artist in artists.iter() {
|
||||||
|
tx.insert_artist(artist)?;
|
||||||
|
for album in artist.albums.iter() {
|
||||||
|
tx.insert_album(artist.name, album)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod testmod;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use mockall::{predicate, Sequence};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::{collection::Collection, testmod::FULL_COLLECTION},
|
||||||
|
external::database::sql::testmod::{
|
||||||
|
DATABASE_SQL_ALBUMS, DATABASE_SQL_ARTISTS, DATABASE_SQL_VERSION,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn expected() -> Collection {
|
||||||
|
let mut expected = FULL_COLLECTION.to_owned();
|
||||||
|
for artist in expected.iter_mut() {
|
||||||
|
for album in artist.albums.iter_mut() {
|
||||||
|
album.tracks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SqlDatabaseTestBackend {
|
||||||
|
pub txs: VecDeque<MockISqlTransactionBackend>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqlDatabaseTestBackend {
|
||||||
|
fn new(txs: VecDeque<MockISqlTransactionBackend>) -> Self {
|
||||||
|
SqlDatabaseTestBackend { txs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ISqlDatabaseBackend<'_> for SqlDatabaseTestBackend {
|
||||||
|
type Tx = MockISqlTransactionBackend;
|
||||||
|
|
||||||
|
fn transaction(&mut self) -> Result<Self::Tx, Error> {
|
||||||
|
self.txs
|
||||||
|
.pop_front()
|
||||||
|
.ok_or_else(|| Error::OpenError(String::from("lol")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! then {
|
||||||
|
($tx:ident, $seq:ident, $expect:ident) => {
|
||||||
|
$tx.$expect().times(1).in_sequence(&mut $seq)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! then0 {
|
||||||
|
($tx:ident, $seq:ident, $expect:ident) => {
|
||||||
|
then!($tx, $seq, $expect).return_once(|| Ok(()))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! then1 {
|
||||||
|
($tx:ident, $seq:ident, $expect:ident) => {
|
||||||
|
then!($tx, $seq, $expect).return_once(|_| Ok(()))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! then2 {
|
||||||
|
($tx:ident, $seq:ident, $expect:ident) => {
|
||||||
|
then!($tx, $seq, $expect).return_once(|_, _| Ok(()))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! expect_create {
|
||||||
|
($tx:ident, $seq:ident) => {
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
then0!($tx, seq, expect_create_database_metadata_table);
|
||||||
|
then0!($tx, seq, expect_create_artists_table);
|
||||||
|
then0!($tx, seq, expect_create_albums_table);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! expect_drop {
|
||||||
|
($tx:ident, $seq:ident) => {
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
then0!($tx, seq, expect_drop_albums_table);
|
||||||
|
then0!($tx, seq, expect_drop_artists_table);
|
||||||
|
then0!($tx, seq, expect_drop_database_metadata_table);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn database(txs: VecDeque<MockISqlTransactionBackend>) -> SqlDatabase<SqlDatabaseTestBackend> {
|
||||||
|
let mut backend = SqlDatabaseTestBackend::new(txs);
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
expect_create!(tx, seq);
|
||||||
|
then0!(tx, seq, expect_commit);
|
||||||
|
|
||||||
|
backend.txs.push_front(tx);
|
||||||
|
SqlDatabase::new(backend).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save() {
|
||||||
|
let write_data = FULL_COLLECTION.to_owned();
|
||||||
|
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
expect_drop!(tx, seq);
|
||||||
|
expect_create!(tx, seq);
|
||||||
|
then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103));
|
||||||
|
for artist in write_data.iter() {
|
||||||
|
let ac = artist.clone();
|
||||||
|
then1!(tx, seq, expect_insert_artist)
|
||||||
|
.withf(move |a| a == &Into::<SerializeArtist>::into(&ac));
|
||||||
|
for album in artist.albums.iter() {
|
||||||
|
let (nc, ac) = (artist.meta.id.name.clone(), album.clone());
|
||||||
|
then2!(tx, seq, expect_insert_album)
|
||||||
|
.withf(move |n, a| n == nc && a == &Into::<SerializeAlbum>::into(&ac));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
then0!(tx, seq, expect_commit);
|
||||||
|
|
||||||
|
assert!(database(VecDeque::from([tx])).save(&write_data).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load() {
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
|
||||||
|
then!(tx, seq, expect_select_database_version)
|
||||||
|
.return_once(|| Ok(Some(DATABASE_SQL_VERSION.to_string())));
|
||||||
|
|
||||||
|
let de_artists = DATABASE_SQL_ARTISTS.to_owned();
|
||||||
|
let artists: Collection = de_artists.iter().cloned().map(Into::into).collect();
|
||||||
|
then!(tx, seq, expect_select_all_artists).return_once(|| Ok(de_artists));
|
||||||
|
|
||||||
|
for artist in artists.iter() {
|
||||||
|
let de_albums = DATABASE_SQL_ALBUMS.get(&artist.meta.id.name).unwrap();
|
||||||
|
then!(tx, seq, expect_select_artist_albums)
|
||||||
|
.with(predicate::eq(artist.meta.id.name.clone()))
|
||||||
|
.return_once(|_| Ok(de_albums.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
then0!(tx, seq, expect_commit);
|
||||||
|
|
||||||
|
let read_data = database(VecDeque::from([tx])).load().unwrap();
|
||||||
|
|
||||||
|
let expected = expected();
|
||||||
|
assert_eq!(read_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_missing_database_version() {
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
then!(tx, seq, expect_select_database_version).return_once(|| Ok(None));
|
||||||
|
let error = database(VecDeque::from([tx])).load().unwrap_err();
|
||||||
|
assert!(matches!(error, LoadError::SerDeError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_unknown_database_version() {
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
then!(tx, seq, expect_select_database_version)
|
||||||
|
.return_once(|| Ok(Some(String::from("no u"))));
|
||||||
|
let error = database(VecDeque::from([tx])).load().unwrap_err();
|
||||||
|
assert!(matches!(error, LoadError::SerDeError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_backend_open_error() {
|
||||||
|
let error = database(VecDeque::from([])).load().unwrap_err();
|
||||||
|
assert!(matches!(error, LoadError::IoError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_backend_open_error() {
|
||||||
|
let error = database(VecDeque::from([])).save(&vec![]).unwrap_err();
|
||||||
|
assert!(matches!(error, SaveError::IoError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_backend_exec_error() {
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
then!(tx, seq, expect_select_database_version)
|
||||||
|
.return_once(|| Err(Error::ExecError(String::from("serde"))));
|
||||||
|
let error = database(VecDeque::from([tx])).load().unwrap_err();
|
||||||
|
assert!(matches!(error, LoadError::IoError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_backend_exec_error() {
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
expect_drop!(tx, seq);
|
||||||
|
expect_create!(tx, seq);
|
||||||
|
then!(tx, seq, expect_insert_database_version)
|
||||||
|
.with(predicate::eq(V20250103))
|
||||||
|
.return_once(|_| Err(Error::ExecError(String::from("exec"))));
|
||||||
|
|
||||||
|
let error = database(VecDeque::from([tx])).save(&vec![]).unwrap_err();
|
||||||
|
assert!(matches!(error, SaveError::IoError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_backend_serde_error() {
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
|
||||||
|
then!(tx, seq, expect_select_database_version)
|
||||||
|
.return_once(|| Ok(Some(DATABASE_SQL_VERSION.to_string())));
|
||||||
|
then!(tx, seq, expect_select_all_artists)
|
||||||
|
.return_once(|| Err(Error::SerDeError(String::from("serde"))));
|
||||||
|
|
||||||
|
let error = database(VecDeque::from([tx])).load().unwrap_err();
|
||||||
|
assert!(matches!(error, LoadError::SerDeError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_backend_serde_error() {
|
||||||
|
let write_data = FULL_COLLECTION.to_owned();
|
||||||
|
|
||||||
|
let mut tx = MockISqlTransactionBackend::new();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
expect_drop!(tx, seq);
|
||||||
|
expect_create!(tx, seq);
|
||||||
|
then1!(tx, seq, expect_insert_database_version).with(predicate::eq(V20250103));
|
||||||
|
then!(tx, seq, expect_insert_artist)
|
||||||
|
.return_once(|_| Err(Error::SerDeError(String::from("serde"))));
|
||||||
|
|
||||||
|
let error = database(VecDeque::from([tx]))
|
||||||
|
.save(&write_data)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(error, SaveError::SerDeError(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serde_json_error() {
|
||||||
|
let error = serde_json::from_str::<DeserializeArtist>("").unwrap_err();
|
||||||
|
let error: Error = error.into();
|
||||||
|
assert!(matches!(error, Error::SerDeError(_)));
|
||||||
|
assert!(!error.to_string().is_empty());
|
||||||
|
}
|
||||||
|
}
|
202
src/external/database/sql/testmod.rs
Normal file
202
src/external/database/sql/testmod.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
collection::{
|
||||||
|
album::{AlbumDate, AlbumLibId, AlbumPrimaryType},
|
||||||
|
musicbrainz::MbRefOption,
|
||||||
|
},
|
||||||
|
external::database::serde::{
|
||||||
|
common::{SerdeAlbumDate, SerdeAlbumLibId, SerdeAlbumPrimaryType},
|
||||||
|
deserialize::{
|
||||||
|
DeserializeAlbum, DeserializeArtist, DeserializeMbRefOption, DeserializeMbid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static DATABASE_SQL_VERSION: &str = "V20250103";
|
||||||
|
|
||||||
|
pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
DeserializeArtist {
|
||||||
|
name: String::from("Album_Artist ‘A’"),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
|
||||||
|
"00000000-0000-0000-0000-000000000000".try_into().unwrap(),
|
||||||
|
))),
|
||||||
|
sort: None,
|
||||||
|
properties: HashMap::from([
|
||||||
|
(
|
||||||
|
String::from("MusicButler"),
|
||||||
|
vec![String::from(
|
||||||
|
"https://www.musicbutler.io/artist-page/000000000",
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
String::from("Qobuz"),
|
||||||
|
vec![String::from(
|
||||||
|
"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums",
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
albums: vec![],
|
||||||
|
},
|
||||||
|
DeserializeArtist {
|
||||||
|
name: String::from("Album_Artist ‘B’"),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
|
||||||
|
"11111111-1111-1111-1111-111111111111".try_into().unwrap(),
|
||||||
|
))),
|
||||||
|
sort: None,
|
||||||
|
properties: HashMap::from([
|
||||||
|
(String::from("MusicButler"), vec![
|
||||||
|
String::from("https://www.musicbutler.io/artist-page/111111111"),
|
||||||
|
String::from("https://www.musicbutler.io/artist-page/111111112"),
|
||||||
|
]),
|
||||||
|
(String::from("Bandcamp"), vec![
|
||||||
|
String::from("https://artist-b.bandcamp.com/")
|
||||||
|
]),
|
||||||
|
(String::from("Qobuz"), vec![
|
||||||
|
String::from(
|
||||||
|
"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums",
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
albums: vec![],
|
||||||
|
},
|
||||||
|
DeserializeArtist {
|
||||||
|
name: String::from("The Album_Artist ‘C’"),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::CannotHaveMbid),
|
||||||
|
sort: Some(String::from("Album_Artist ‘C’, The")),
|
||||||
|
properties: HashMap::new(),
|
||||||
|
albums: vec![],
|
||||||
|
},
|
||||||
|
DeserializeArtist {
|
||||||
|
name: String::from("Album_Artist ‘D’"),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
sort: None,
|
||||||
|
properties: HashMap::new(),
|
||||||
|
albums: vec![],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = Lazy::new(|| {
|
||||||
|
HashMap::from([
|
||||||
|
(
|
||||||
|
String::from("Album_Artist ‘A’"),
|
||||||
|
vec![
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title a.a"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(1)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
|
||||||
|
"00000000-0000-0000-0000-000000000000".try_into().unwrap(),
|
||||||
|
))),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(1998), None, None)),
|
||||||
|
seq: 1,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title a.b"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(2)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2015), Some(4), None)),
|
||||||
|
seq: 1,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
String::from("Album_Artist ‘B’"),
|
||||||
|
vec![
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title b.a"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(3)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2003), Some(6), Some(6))),
|
||||||
|
seq: 1,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title b.b"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(4)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
|
||||||
|
"11111111-1111-1111-1111-111111111111".try_into().unwrap(),
|
||||||
|
))),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2008), None, None)),
|
||||||
|
seq: 3,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title b.c"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(5)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
|
||||||
|
"11111111-1111-1111-1111-111111111112".try_into().unwrap(),
|
||||||
|
))),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2009), None, None)),
|
||||||
|
seq: 2,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title b.d"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(6)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2015), None, None)),
|
||||||
|
seq: 4,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
String::from("The Album_Artist ‘C’"),
|
||||||
|
vec![
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title c.a"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(7)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(1985), None, None)),
|
||||||
|
seq: 0,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title c.b"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(8)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2018), None, None)),
|
||||||
|
seq: 0,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
String::from("Album_Artist ‘D’"),
|
||||||
|
vec![
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title d.a"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(9)),
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(1995), None, None)),
|
||||||
|
seq: 0,
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
DeserializeAlbum {
|
||||||
|
title: String::from("album_title d.b"),
|
||||||
|
lib_id: SerdeAlbumLibId(AlbumLibId::Value(10)),
|
||||||
|
date: SerdeAlbumDate(AlbumDate::new(Some(2028), None, None)),
|
||||||
|
seq: 0,
|
||||||
|
mb_ref: DeserializeMbRefOption(MbRefOption::None),
|
||||||
|
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
|
||||||
|
secondary_types: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])
|
||||||
|
});
|
13
src/main.rs
13
src/main.rs
@ -11,7 +11,7 @@ use musichoard::{
|
|||||||
track::TrackFormat,
|
track::TrackFormat,
|
||||||
},
|
},
|
||||||
external::{
|
external::{
|
||||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
|
||||||
library::beets::{
|
library::beets::{
|
||||||
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
||||||
BeetsLibrary,
|
BeetsLibrary,
|
||||||
@ -141,7 +141,10 @@ fn with_database<Library: ILibrary + 'static>(
|
|||||||
{
|
{
|
||||||
Ok(f) => {
|
Ok(f) => {
|
||||||
drop(f);
|
drop(f);
|
||||||
JsonDatabase::new(JsonDatabaseFileBackend::new(&db_opt.database_file_path))
|
let db_exec = SqlDatabaseSqliteBackend::new(&db_opt.database_file_path)
|
||||||
|
.expect("failed to initialise SQLite database backend");
|
||||||
|
SqlDatabase::new(db_exec)
|
||||||
|
.expect("failed to open new database")
|
||||||
.save(&vec![])
|
.save(&vec![])
|
||||||
.expect("failed to create empty database");
|
.expect("failed to create empty database");
|
||||||
}
|
}
|
||||||
@ -151,8 +154,10 @@ fn with_database<Library: ILibrary + 'static>(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
let db_exec = JsonDatabaseFileBackend::new(&db_opt.database_file_path);
|
let db_exec = SqlDatabaseSqliteBackend::new(&db_opt.database_file_path)
|
||||||
with(builder.set_database(JsonDatabase::new(db_exec)));
|
.expect("failed to initialise SQLite database backend");
|
||||||
|
let db = SqlDatabase::new(db_exec).expect("failed to open database");
|
||||||
|
with(builder.set_database(db));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
#![cfg(feature = "database-json")]
|
#[cfg(feature = "database-sqlite")]
|
||||||
|
pub mod sql;
|
||||||
pub mod json;
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::{artist::Artist, Collection},
|
collection::{artist::Artist, Collection},
|
||||||
external::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
external::database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
|
||||||
interface::database::IDatabase,
|
interface::database::IDatabase,
|
||||||
};
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use crate::testlib::COLLECTION;
|
use crate::testlib::COLLECTION;
|
||||||
|
|
||||||
pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
|
pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
|
||||||
Lazy::new(|| fs::canonicalize("./tests/files/database/database.json").unwrap());
|
Lazy::new(|| fs::canonicalize("./tests/files/database/database.db").unwrap());
|
||||||
|
|
||||||
fn expected() -> Collection {
|
fn expected() -> Collection {
|
||||||
let mut expected = COLLECTION.to_owned();
|
let mut expected = COLLECTION.to_owned();
|
||||||
@ -28,22 +28,17 @@ fn expected() -> Collection {
|
|||||||
fn save() {
|
fn save() {
|
||||||
let file = NamedTempFile::new().unwrap();
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
|
||||||
let backend = JsonDatabaseFileBackend::new(file.path());
|
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
|
||||||
let mut database = JsonDatabase::new(backend);
|
let mut database = SqlDatabase::new(backend).unwrap();
|
||||||
|
|
||||||
let write_data = COLLECTION.to_owned();
|
let write_data = COLLECTION.to_owned();
|
||||||
database.save(&write_data).unwrap();
|
database.save(&write_data).unwrap();
|
||||||
|
|
||||||
let expected = fs::read_to_string(&*DATABASE_TEST_FILE).unwrap();
|
|
||||||
let actual = fs::read_to_string(file.path()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(actual, expected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load() {
|
fn load() {
|
||||||
let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE);
|
let backend = SqlDatabaseSqliteBackend::new(&*DATABASE_TEST_FILE).unwrap();
|
||||||
let database = JsonDatabase::new(backend);
|
let mut database = SqlDatabase::new(backend).unwrap();
|
||||||
|
|
||||||
let read_data: Vec<Artist> = database.load().unwrap();
|
let read_data: Vec<Artist> = database.load().unwrap();
|
||||||
|
|
||||||
@ -55,14 +50,14 @@ fn load() {
|
|||||||
fn reverse() {
|
fn reverse() {
|
||||||
let file = NamedTempFile::new().unwrap();
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
|
||||||
let backend = JsonDatabaseFileBackend::new(file.path());
|
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
|
||||||
let mut database = JsonDatabase::new(backend);
|
let mut database = SqlDatabase::new(backend).unwrap();
|
||||||
|
|
||||||
let write_data = COLLECTION.to_owned();
|
let write_data = COLLECTION.to_owned();
|
||||||
database.save(&write_data).unwrap();
|
database.save(&write_data).unwrap();
|
||||||
let read_data: Vec<Artist> = database.load().unwrap();
|
let read_data: Vec<Artist> = database.load().unwrap();
|
||||||
|
|
||||||
// Album data is not saved into database.
|
// Not all data is saved into database.
|
||||||
let expected = expected();
|
let expected = expected();
|
||||||
assert_eq!(read_data, expected);
|
assert_eq!(read_data, expected);
|
||||||
}
|
}
|
BIN
tests/files/database/database.db
Normal file
BIN
tests/files/database/database.db
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"V20250103":[{"name":"Аркона","sort":"Arkona","musicbrainz":{"Some":"baad262d-55ef-427a-83c7-f7530964f212"},"properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","lib_id":{"Value":7},"date":{"year":2011,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Album","secondary_types":[]}]},{"name":"Eluveitie","sort":null,"musicbrainz":{"Some":"8000598a-5edb-401c-8e6d-36b167feaf38"},"properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [re‐recorded]","lib_id":{"Value":1},"date":{"year":2004,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Ep","secondary_types":[]},{"title":"Slania","lib_id":{"Value":2},"date":{"year":2008,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Album","secondary_types":[]}]},{"name":"Frontside","sort":null,"musicbrainz":{"Some":"3a901353-fccd-4afd-ad01-9c03f451b490"},"properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","lib_id":{"Value":3},"date":{"year":2001,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Album","secondary_types":[]}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":{"Some":"c2c4d56a-d599-4a18-bd2f-ae644e2198cc"},"properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","lib_id":"Singleton","date":{"year":2011,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":null,"secondary_types":[]},{"title":"Unbreakable","lib_id":{"Value":4},"date":{"year":2011,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Album","secondary_types":[]}]},{"name":"Metallica","sort":null,"musicbrainz":{"Some":"65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"},"properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","lib_id":{"Value":5},"date":{"year":1984,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Album","secondary_types":[]},{"title":"S&M","lib_id":{"Value":6},"date":{"year":1999,"month":null,"day":null},"seq":0,"musicbrainz":"None","primary_type":"Album","secondary_types":["Live"]}]}]}
|
|
23
tests/lib.rs
23
tests/lib.rs
@ -1,4 +1,4 @@
|
|||||||
#![cfg(feature = "database-json")]
|
#![cfg(feature = "database-sqlite")]
|
||||||
#![cfg(feature = "library-beets")]
|
#![cfg(feature = "library-beets")]
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
@ -6,16 +6,25 @@ mod library;
|
|||||||
|
|
||||||
mod testlib;
|
mod testlib;
|
||||||
|
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
external::{
|
external::{
|
||||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
database::sql::{backend::SqlDatabaseSqliteBackend, SqlDatabase},
|
||||||
library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
||||||
},
|
},
|
||||||
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
|
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
|
||||||
};
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use crate::testlib::COLLECTION;
|
use crate::testlib::COLLECTION;
|
||||||
|
|
||||||
|
fn copy_file_into_temp<P: Into<PathBuf>>(path: P) -> NamedTempFile {
|
||||||
|
let temp = NamedTempFile::new().unwrap();
|
||||||
|
fs::copy(path.into(), temp.path()).unwrap();
|
||||||
|
temp
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_library_then_database() {
|
fn merge_library_then_database() {
|
||||||
// Acquired the lock on the beets config file. We need to own the underlying object so later we
|
// Acquired the lock on the beets config file. We need to own the underlying object so later we
|
||||||
@ -28,8 +37,9 @@ fn merge_library_then_database() {
|
|||||||
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
|
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
|
||||||
let library = BeetsLibrary::new(executor);
|
let library = BeetsLibrary::new(executor);
|
||||||
|
|
||||||
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
|
let file = copy_file_into_temp(&*database::sql::DATABASE_TEST_FILE);
|
||||||
let database = JsonDatabase::new(backend);
|
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
|
||||||
|
let database = SqlDatabase::new(backend).unwrap();
|
||||||
|
|
||||||
let mut music_hoard = MusicHoard::new(database, library);
|
let mut music_hoard = MusicHoard::new(database, library);
|
||||||
|
|
||||||
@ -51,8 +61,9 @@ fn merge_database_then_library() {
|
|||||||
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
|
.config(Some(&*library::beets::BEETS_TEST_CONFIG_PATH));
|
||||||
let library = BeetsLibrary::new(executor);
|
let library = BeetsLibrary::new(executor);
|
||||||
|
|
||||||
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
|
let file = copy_file_into_temp(&*database::sql::DATABASE_TEST_FILE);
|
||||||
let database = JsonDatabase::new(backend);
|
let backend = SqlDatabaseSqliteBackend::new(file.path()).unwrap();
|
||||||
|
let database = SqlDatabase::new(backend).unwrap();
|
||||||
|
|
||||||
let mut music_hoard = MusicHoard::new(database, library);
|
let mut music_hoard = MusicHoard::new(database, library);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user