Remove database-json #267

Merged
wojtek merged 7 commits from 248---replace-json-file-as-a-database-with-sqlite into main 2025-01-12 12:18:25 +01:00
9 changed files with 4 additions and 383 deletions
Showing only changes of commit a5ea60cc98 - Show all commits

View File

@ -31,11 +31,10 @@ mockall = "0.13.1"
tempfile = "3.15.0"
[features]
default = ["database-json", "library-beets"]
default = ["database-sqlite", "library-beets"]
bin = ["structopt"]
database-sqlite = ["rusqlite", "serde", "serde_json"]
database-sqlite-bundled = ["rusqlite/bundled"]
database-json = ["serde", "serde_json"]
library-beets = []
library-beets-ssh = ["openssh", "tokio"]
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
@ -43,7 +42,7 @@ tui = ["crossterm", "ratatui", "tui-input"]
[[bin]]
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]]
name = "musicbrainz-api---browse"

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")]
pub mod sql;
#[cfg(any(feature = "database-json", feature = "database-sqlite"))]
#[cfg(any(feature = "database-sqlite"))]
mod serde;

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")]
pub mod sql;

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")]
mod database;