Remove database-json (#267)
All checks were successful
Cargo CI / Build and Test (push) Successful in 2m17s
Cargo CI / Lint (push) Successful in 1m16s

Part 3 of #248
Closes #248

Reviewed-on: #267
This commit is contained in:
Wojciech Kozlowski 2025-01-12 12:18:25 +01:00
parent c869489919
commit cdb5c1c713
16 changed files with 97 additions and 481 deletions

View File

@ -31,11 +31,10 @@ mockall = "0.13.1"
tempfile = "3.15.0" tempfile = "3.15.0"
[features] [features]
default = ["database-json", "library-beets"] default = ["database-sqlite", "library-beets"]
bin = ["structopt"] bin = ["structopt"]
database-sqlite = ["rusqlite", "serde", "serde_json"] database-sqlite = ["rusqlite", "serde", "serde_json"]
database-sqlite-bundled = ["rusqlite/bundled"] database-sqlite-bundled = ["rusqlite/bundled"]
database-json = ["serde", "serde_json"]
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"]
@ -43,7 +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"]
[[example]] [[example]]
name = "musicbrainz-api---browse" name = "musicbrainz-api---browse"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
#[cfg(feature = "database-json")]
pub mod json;
#[cfg(feature = "database-sqlite")] #[cfg(feature = "database-sqlite")]
pub mod sql; pub mod sql;
#[cfg(any(feature = "database-json", feature = "database-sqlite"))] #[cfg(feature = "database-sqlite")]
mod serde; mod serde;

View File

@ -35,8 +35,8 @@ impl From<DeserializeDatabase> for Collection {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct DeserializeArtist { pub struct DeserializeArtist {
pub name: String, pub name: String,
pub mb_ref: DeserializeMbRefOption,
pub sort: Option<String>, pub sort: Option<String>,
pub musicbrainz: DeserializeMbRefOption,
pub properties: HashMap<String, Vec<String>>, pub properties: HashMap<String, Vec<String>>,
pub albums: Vec<DeserializeAlbum>, pub albums: Vec<DeserializeAlbum>,
} }
@ -45,9 +45,9 @@ pub struct DeserializeArtist {
pub struct DeserializeAlbum { pub struct DeserializeAlbum {
pub title: String, pub title: String,
pub lib_id: SerdeAlbumLibId, pub lib_id: SerdeAlbumLibId,
pub mb_ref: DeserializeMbRefOption,
pub date: SerdeAlbumDate, pub date: SerdeAlbumDate,
pub seq: u8, pub seq: u8,
pub musicbrainz: DeserializeMbRefOption,
pub primary_type: Option<SerdeAlbumPrimaryType>, pub primary_type: Option<SerdeAlbumPrimaryType>,
pub secondary_types: Vec<SerdeAlbumSecondaryType>, pub secondary_types: Vec<SerdeAlbumSecondaryType>,
} }
@ -120,7 +120,7 @@ impl From<DeserializeArtist> for 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 {
@ -139,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),

View File

@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::HashMap;
use serde::Serialize; use serde::Serialize;
@ -25,9 +25,9 @@ impl<'a> From<&'a Collection> for SerializeDatabase<'a> {
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq, Eq)]
pub struct SerializeArtist<'a> { pub struct SerializeArtist<'a> {
pub name: &'a str, pub name: &'a str,
pub sort: Option<&'a str>, pub mb_ref: SerializeMbRefOption<'a>,
pub musicbrainz: SerializeMbRefOption<'a>, pub sort: &'a Option<String>,
pub properties: BTreeMap<&'a str, &'a Vec<String>>, pub properties: &'a HashMap<String, Vec<String>>,
pub albums: Vec<SerializeAlbum<'a>>, pub albums: Vec<SerializeAlbum<'a>>,
} }
@ -35,9 +35,9 @@ pub struct SerializeArtist<'a> {
pub struct SerializeAlbum<'a> { pub struct SerializeAlbum<'a> {
pub title: &'a str, pub title: &'a str,
pub lib_id: SerdeAlbumLibId, pub lib_id: SerdeAlbumLibId,
pub mb_ref: SerializeMbRefOption<'a>,
pub date: SerdeAlbumDate, pub date: SerdeAlbumDate,
pub seq: u8, pub seq: u8,
pub musicbrainz: SerializeMbRefOption<'a>,
pub primary_type: Option<SerdeAlbumPrimaryType>, pub primary_type: Option<SerdeAlbumPrimaryType>,
pub secondary_types: Vec<SerdeAlbumSecondaryType>, pub secondary_types: Vec<SerdeAlbumSecondaryType>,
} }
@ -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

View File

@ -105,9 +105,9 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
fn create_artists_table(&self) -> Result<(), Error> { fn create_artists_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare( let mut stmt = self.prepare(
"CREATE TABLE IF NOT EXISTS artists ( "CREATE TABLE IF NOT EXISTS artists (
name TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL,
sort TEXT NULL,
mbid JSON NOT NULL DEFAULT '\"None\"', mbid JSON NOT NULL DEFAULT '\"None\"',
sort TEXT NULL,
properties JSON NOT NULL DEFAULT '{}' properties JSON NOT NULL DEFAULT '{}'
)", )",
); );
@ -122,7 +122,7 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
fn create_albums_table(&self) -> Result<(), Error> { fn create_albums_table(&self) -> Result<(), Error> {
let mut stmt = self.prepare( let mut stmt = self.prepare(
"CREATE TABLE IF NOT EXISTS albums ( "CREATE TABLE IF NOT EXISTS albums (
title TEXT NOT NULL PRIMARY KEY, title TEXT NOT NULL,
lib_id JSON NOT NULL DEFAULT '\"None\"', lib_id JSON NOT NULL DEFAULT '\"None\"',
mbid JSON NOT NULL DEFAULT '\"None\"', mbid JSON NOT NULL DEFAULT '\"None\"',
artist_name TEXT NOT NULL, artist_name TEXT NOT NULL,
@ -131,8 +131,7 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
day INT NULL, day INT NULL,
seq INT NOT NULL, seq INT NOT NULL,
primary_type JSON NOT NULL DEFAULT 'null', primary_type JSON NOT NULL DEFAULT 'null',
secondary_types JSON NOT NULL DEFAULT '[]', secondary_types JSON NOT NULL DEFAULT '[]'
FOREIGN KEY (artist_name) REFERENCES artists(name) ON DELETE CASCADE ON UPDATE NO ACTION
)", )",
); );
Self::execute(&mut stmt, ()) Self::execute(&mut stmt, ())
@ -163,27 +162,33 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
fn insert_artist(&self, artist: &SerializeArtist<'_>) -> Result<(), Error> { fn insert_artist(&self, artist: &SerializeArtist<'_>) -> Result<(), Error> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"INSERT INTO artists (name, sort, mbid, properties) "INSERT INTO artists (name, mbid, sort, properties)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
); );
Self::execute( Self::execute(
&mut stmt, &mut stmt,
( (
artist.name, artist.name,
serde_json::to_string(&artist.mb_ref)?,
artist.sort, artist.sort,
serde_json::to_string(&artist.musicbrainz)?,
serde_json::to_string(&artist.properties)?, serde_json::to_string(&artist.properties)?,
), ),
) )
} }
fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error> { fn select_all_artists(&self) -> Result<Vec<DeserializeArtist>, Error> {
let mut stmt = self.prepare_cached("SELECT name, sort, mbid, properties FROM artists"); let mut stmt = self.prepare_cached("SELECT name, mbid, sort, properties FROM artists");
let mut rows = Self::query(&mut stmt, ())?; let mut rows = Self::query(&mut stmt, ())?;
let mut artists = vec![]; let mut artists = vec![];
while let Some(row) = Self::next_row(&mut rows)? { while let Some(row) = Self::next_row(&mut rows)? {
artists.push(row.try_into()?); 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) Ok(artists)
@ -200,7 +205,7 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
( (
album.title, album.title,
serde_json::to_string(&album.lib_id)?, serde_json::to_string(&album.lib_id)?,
serde_json::to_string(&album.musicbrainz)?, serde_json::to_string(&album.mb_ref)?,
artist_name, artist_name,
album.date.0.year, album.date.0.year,
album.date.0.month, album.date.0.month,
@ -214,52 +219,28 @@ impl ISqlTransactionBackend for SqlTransactionSqliteBackend<'_> {
fn select_artist_albums(&self, artist_name: &str) -> Result<Vec<DeserializeAlbum>, Error> { fn select_artist_albums(&self, artist_name: &str) -> Result<Vec<DeserializeAlbum>, Error> {
let mut stmt = self.prepare_cached( let mut stmt = self.prepare_cached(
"SELECT title, lib_id, year, month, day, seq, mbid, primary_type, secondary_types "SELECT title, lib_id, mbid, year, month, day, seq, primary_type, secondary_types
FROM albums WHERE artist_name = ?1", FROM albums WHERE artist_name = ?1",
); );
let mut rows = Self::query(&mut stmt, [artist_name])?; let mut rows = Self::query(&mut stmt, [artist_name])?;
let mut albums = vec![]; let mut albums = vec![];
while let Some(row) = Self::next_row(&mut rows)? { while let Some(row) = Self::next_row(&mut rows)? {
albums.push(row.try_into()?); 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) Ok(albums)
} }
} }
impl TryFrom<&Row<'_>> for DeserializeArtist {
type Error = Error;
fn try_from(row: &Row<'_>) -> Result<Self, Self::Error> {
type Backend<'a> = SqlTransactionSqliteBackend<'a>;
Ok(DeserializeArtist {
name: Backend::get_value(row, 0)?,
sort: Backend::get_value(row, 1)?,
musicbrainz: serde_json::from_str(&Backend::get_value::<String>(row, 2)?)?,
properties: serde_json::from_str(&Backend::get_value::<String>(row, 3)?)?,
albums: vec![],
})
}
}
impl TryFrom<&Row<'_>> for DeserializeAlbum {
type Error = Error;
fn try_from(row: &Row<'_>) -> Result<Self, Self::Error> {
type Backend<'a> = SqlTransactionSqliteBackend<'a>;
Ok(DeserializeAlbum {
title: Backend::get_value(row, 0)?,
lib_id: serde_json::from_str(&Backend::get_value::<String>(row, 1)?)?,
date: SerdeAlbumDate(AlbumDate::new(
Backend::get_value(row, 2)?,
Backend::get_value(row, 3)?,
Backend::get_value(row, 4)?,
)),
seq: Backend::get_value(row, 5)?,
musicbrainz: serde_json::from_str(&Backend::get_value::<String>(row, 6)?)?,
primary_type: serde_json::from_str(&Backend::get_value::<String>(row, 7)?)?,
secondary_types: serde_json::from_str(&Backend::get_value::<String>(row, 8)?)?,
})
}
}

View File

@ -21,10 +21,10 @@ pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
vec![ vec![
DeserializeArtist { DeserializeArtist {
name: String::from("Album_Artist A"), name: String::from("Album_Artist A"),
sort: None, mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"00000000-0000-0000-0000-000000000000".try_into().unwrap(), "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
))), ))),
sort: None,
properties: HashMap::from([ properties: HashMap::from([
( (
String::from("MusicButler"), String::from("MusicButler"),
@ -43,10 +43,10 @@ pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
}, },
DeserializeArtist { DeserializeArtist {
name: String::from("Album_Artist B"), name: String::from("Album_Artist B"),
sort: None, mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111111".try_into().unwrap(), "11111111-1111-1111-1111-111111111111".try_into().unwrap(),
))), ))),
sort: None,
properties: HashMap::from([ properties: HashMap::from([
(String::from("MusicButler"), vec![ (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/111111111"), String::from("https://www.musicbutler.io/artist-page/111111111"),
@ -65,15 +65,15 @@ pub static DATABASE_SQL_ARTISTS: Lazy<Vec<DeserializeArtist>> = Lazy::new(|| {
}, },
DeserializeArtist { DeserializeArtist {
name: String::from("The Album_Artist C"), name: String::from("The Album_Artist C"),
mb_ref: DeserializeMbRefOption(MbRefOption::CannotHaveMbid),
sort: Some(String::from("Album_Artist C, The")), sort: Some(String::from("Album_Artist C, The")),
musicbrainz: DeserializeMbRefOption(MbRefOption::CannotHaveMbid),
properties: HashMap::new(), properties: HashMap::new(),
albums: vec![], albums: vec![],
}, },
DeserializeArtist { DeserializeArtist {
name: String::from("Album_Artist D"), name: String::from("Album_Artist D"),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
sort: None, sort: None,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
properties: HashMap::new(), properties: HashMap::new(),
albums: vec![], albums: vec![],
}, },
@ -88,20 +88,20 @@ pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = L
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title a.a"), title: String::from("album_title a.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(1)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(1)),
date: SerdeAlbumDate(AlbumDate::new(Some(1998), None, None)), mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
seq: 1,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"00000000-0000-0000-0000-000000000000".try_into().unwrap(), "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
))), ))),
date: SerdeAlbumDate(AlbumDate::new(Some(1998), None, None)),
seq: 1,
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title a.b"), title: String::from("album_title a.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(2)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(2)),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
date: SerdeAlbumDate(AlbumDate::new(Some(2015), Some(4), None)), date: SerdeAlbumDate(AlbumDate::new(Some(2015), Some(4), None)),
seq: 1, seq: 1,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
@ -113,40 +113,40 @@ pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = L
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title b.a"), title: String::from("album_title b.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(3)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(3)),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
date: SerdeAlbumDate(AlbumDate::new(Some(2003), Some(6), Some(6))), date: SerdeAlbumDate(AlbumDate::new(Some(2003), Some(6), Some(6))),
seq: 1, seq: 1,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title b.b"), title: String::from("album_title b.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(4)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(4)),
date: SerdeAlbumDate(AlbumDate::new(Some(2008), None, None)), mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
seq: 3,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111111".try_into().unwrap(), "11111111-1111-1111-1111-111111111111".try_into().unwrap(),
))), ))),
date: SerdeAlbumDate(AlbumDate::new(Some(2008), None, None)),
seq: 3,
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title b.c"), title: String::from("album_title b.c"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(5)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(5)),
date: SerdeAlbumDate(AlbumDate::new(Some(2009), None, None)), mb_ref: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
seq: 2,
musicbrainz: DeserializeMbRefOption(MbRefOption::Some(DeserializeMbid(
"11111111-1111-1111-1111-111111111112".try_into().unwrap(), "11111111-1111-1111-1111-111111111112".try_into().unwrap(),
))), ))),
date: SerdeAlbumDate(AlbumDate::new(Some(2009), None, None)),
seq: 2,
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title b.d"), title: String::from("album_title b.d"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(6)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(6)),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
date: SerdeAlbumDate(AlbumDate::new(Some(2015), None, None)), date: SerdeAlbumDate(AlbumDate::new(Some(2015), None, None)),
seq: 4, seq: 4,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
@ -158,18 +158,18 @@ pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = L
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title c.a"), title: String::from("album_title c.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(7)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(7)),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
date: SerdeAlbumDate(AlbumDate::new(Some(1985), None, None)), date: SerdeAlbumDate(AlbumDate::new(Some(1985), None, None)),
seq: 0, seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title c.b"), title: String::from("album_title c.b"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(8)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(8)),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
date: SerdeAlbumDate(AlbumDate::new(Some(2018), None, None)), date: SerdeAlbumDate(AlbumDate::new(Some(2018), None, None)),
seq: 0, seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
@ -181,9 +181,9 @@ pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = L
DeserializeAlbum { DeserializeAlbum {
title: String::from("album_title d.a"), title: String::from("album_title d.a"),
lib_id: SerdeAlbumLibId(AlbumLibId::Value(9)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(9)),
mb_ref: DeserializeMbRefOption(MbRefOption::None),
date: SerdeAlbumDate(AlbumDate::new(Some(1995), None, None)), date: SerdeAlbumDate(AlbumDate::new(Some(1995), None, None)),
seq: 0, seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },
@ -192,7 +192,7 @@ pub static DATABASE_SQL_ALBUMS: Lazy<HashMap<String, Vec<DeserializeAlbum>>> = L
lib_id: SerdeAlbumLibId(AlbumLibId::Value(10)), lib_id: SerdeAlbumLibId(AlbumLibId::Value(10)),
date: SerdeAlbumDate(AlbumDate::new(Some(2028), None, None)), date: SerdeAlbumDate(AlbumDate::new(Some(2028), None, None)),
seq: 0, seq: 0,
musicbrainz: DeserializeMbRefOption(MbRefOption::None), mb_ref: DeserializeMbRefOption(MbRefOption::None),
primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: vec![], secondary_types: vec![],
}, },

View File

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

View File

@ -1,68 +0,0 @@
use std::{fs, path::PathBuf};
use once_cell::sync::Lazy;
use tempfile::NamedTempFile;
use musichoard::{
collection::{artist::Artist, Collection},
external::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
interface::database::IDatabase,
};
use crate::testlib::COLLECTION;
pub static DATABASE_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/database.json").unwrap());
fn expected() -> Collection {
let mut expected = COLLECTION.to_owned();
for artist in expected.iter_mut() {
for album in artist.albums.iter_mut() {
album.tracks.clear();
}
}
expected
}
#[test]
fn save() {
let file = NamedTempFile::new().unwrap();
let backend = JsonDatabaseFileBackend::new(file.path());
let mut database = JsonDatabase::new(backend);
let write_data = COLLECTION.to_owned();
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]
fn load() {
let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE);
let mut database = JsonDatabase::new(backend);
let read_data: Vec<Artist> = database.load().unwrap();
let expected = expected();
assert_eq!(read_data, expected);
}
#[test]
fn reverse() {
let file = NamedTempFile::new().unwrap();
let backend = JsonDatabaseFileBackend::new(file.path());
let mut database = JsonDatabase::new(backend);
let write_data = COLLECTION.to_owned();
database.save(&write_data).unwrap();
let read_data: Vec<Artist> = database.load().unwrap();
// Album data is not saved into database.
let expected = expected();
assert_eq!(read_data, expected);
}

View File

@ -1,4 +1,2 @@
#[cfg(feature = "database-json")]
pub mod json;
#[cfg(feature = "database-sqlite")] #[cfg(feature = "database-sqlite")]
pub mod sql; pub mod sql;

Binary file not shown.

View File

@ -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 [rerecorded]","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":"Heavens Basement","sort":"Heavens 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"]}]}]}

View File

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