Add artist as top level in data structure hierarchy (#13)

Closes #12

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/13
This commit is contained in:
Wojciech Kozlowski 2023-04-01 01:59:59 +02:00
parent 83893f1e7e
commit 5c7579ba23
4 changed files with 227 additions and 167 deletions

View File

@ -77,7 +77,7 @@ impl DatabaseJsonBackend for DatabaseJsonFile {
mod tests { mod tests {
use super::*; use super::*;
use crate::{Album, AlbumId, Track}; use crate::{tests::test_data, Artist};
struct DatabaseJsonTest { struct DatabaseJsonTest {
json: String, json: String,
@ -94,53 +94,15 @@ mod tests {
} }
} }
fn test_data() -> Vec<Album> { fn artist_to_json(artist: &Artist) -> String {
vec![ let album_artist = &artist.id.name;
Album {
id: AlbumId {
artist: String::from("Artist A"),
year: 1998,
title: String::from("Release group A"),
},
tracks: vec![
Track {
number: 1,
title: String::from("Track A.1"),
artist: vec![String::from("Artist A.A")],
},
Track {
number: 2,
title: String::from("Track A.2"),
artist: vec![String::from("Artist A.A")],
},
Track {
number: 3,
title: String::from("Track A.3"),
artist: vec![String::from("Artist A.A"), String::from("Artist A.B")],
},
],
},
Album {
id: AlbumId {
artist: String::from("Artist B"),
year: 2008,
title: String::from("Release group B"),
},
tracks: vec![Track {
number: 1,
title: String::from("Track B.1"),
artist: vec![String::from("Artist B.A")],
}],
},
]
}
fn album_to_json(album: &Album) -> String { let mut albums: Vec<String> = vec![];
let album_id_artist = &album.id.artist; for album in artist.albums.iter() {
let album_id_year = album.id.year; let album_year = album.id.year;
let album_id_title = &album.id.title; let album_title = &album.id.title;
let mut album_tracks: Vec<String> = vec![]; let mut tracks: Vec<String> = vec![];
for track in album.tracks.iter() { for track in album.tracks.iter() {
let track_number = track.number; let track_number = track.number;
let track_title = &track.title; let track_title = &track.title;
@ -151,7 +113,7 @@ mod tests {
} }
let track_artist = track_artist.join(","); let track_artist = track_artist.join(",");
album_tracks.push(format!( tracks.push(format!(
"{{\ "{{\
\"number\":{track_number},\ \"number\":{track_number},\
\"title\":\"{track_title}\",\ \"title\":\"{track_title}\",\
@ -159,33 +121,42 @@ mod tests {
}}" }}"
)); ));
} }
let album_tracks = album_tracks.join(","); let tracks = tracks.join(",");
albums.push(format!(
"{{\
\"id\":{{\
\"year\":{album_year},\
\"title\":\"{album_title}\"\
}},\"tracks\":[{tracks}]\
}}"
));
}
let albums = albums.join(",");
format!( format!(
"{{\ "{{\
\"id\":{{\ \"id\":{{\
\"artist\":\"{album_id_artist}\",\ \"name\":\"{album_artist}\"\
\"year\":{album_id_year},\ }},\"albums\":[{albums}]\
\"title\":\"{album_id_title}\"\
}},\"tracks\":[{album_tracks}]\
}}" }}"
) )
} }
fn albums_to_json(albums: &Vec<Album>) -> String { fn artists_to_json(artists: &Vec<Artist>) -> String {
let mut albums_strings: Vec<String> = vec![]; let mut artists_strings: Vec<String> = vec![];
for album in albums.iter() { for artist in artists.iter() {
albums_strings.push(album_to_json(album)); artists_strings.push(artist_to_json(artist));
} }
let albums_json = albums_strings.join(","); let artists_json = artists_strings.join(",");
format!("[{albums_json}]") format!("[{artists_json}]")
} }
#[test] #[test]
fn write() { fn write() {
let write_data = test_data(); let write_data = test_data();
let backend = DatabaseJsonTest { let backend = DatabaseJsonTest {
json: albums_to_json(&write_data), json: artists_to_json(&write_data),
}; };
DatabaseJson::new(Box::new(backend)) DatabaseJson::new(Box::new(backend))
@ -197,10 +168,10 @@ mod tests {
fn read() { fn read() {
let expected = test_data(); let expected = test_data();
let backend = DatabaseJsonTest { let backend = DatabaseJsonTest {
json: albums_to_json(&expected), json: artists_to_json(&expected),
}; };
let mut read_data: Vec<Album> = vec![]; let mut read_data: Vec<Artist> = vec![];
DatabaseJson::new(Box::new(backend)) DatabaseJson::new(Box::new(backend))
.read(&mut read_data) .read(&mut read_data)
.unwrap(); .unwrap();
@ -212,12 +183,12 @@ mod tests {
fn reverse() { fn reverse() {
let expected = test_data(); let expected = test_data();
let backend = DatabaseJsonTest { let backend = DatabaseJsonTest {
json: albums_to_json(&expected), json: artists_to_json(&expected),
}; };
let mut database = DatabaseJson::new(Box::new(backend)); let mut database = DatabaseJson::new(Box::new(backend));
let write_data = test_data(); let write_data = test_data();
let mut read_data: Vec<Album> = vec![]; let mut read_data: Vec<Artist> = vec![];
database.write(&write_data).unwrap(); database.write(&write_data).unwrap();
database.read(&mut read_data).unwrap(); database.read(&mut read_data).unwrap();

View File

@ -20,7 +20,6 @@ pub struct Track {
/// The album identifier. /// The album identifier.
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)] #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)]
pub struct AlbumId { pub struct AlbumId {
pub artist: String,
pub year: u32, pub year: u32,
pub title: String, pub title: String,
} }
@ -31,3 +30,103 @@ pub struct Album {
pub id: AlbumId, pub id: AlbumId,
pub tracks: Vec<Track>, pub tracks: Vec<Track>,
} }
/// The artist identifier.
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)]
pub struct ArtistId {
pub name: String,
}
/// An artist.
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Artist {
pub id: ArtistId,
pub albums: Vec<Album>,
}
#[cfg(test)]
mod tests {
use super::*;
pub fn test_data() -> Vec<Artist> {
vec![
Artist {
id: ArtistId {
name: "album_artist a".to_string(),
},
albums: vec![
Album {
id: AlbumId {
year: 1998,
title: "album_title a.a".to_string(),
},
tracks: vec![
Track {
number: 1,
title: "track a.a.1".to_string(),
artist: vec!["artist a.a.1".to_string()],
},
Track {
number: 2,
title: "track a.a.2".to_string(),
artist: vec![
"artist a.a.2.1".to_string(),
"artist a.a.2.2".to_string(),
],
},
Track {
number: 3,
title: "track a.a.3".to_string(),
artist: vec!["artist a.a.3".to_string()],
},
],
},
Album {
id: AlbumId {
year: 2015,
title: "album_title a.b".to_string(),
},
tracks: vec![
Track {
number: 1,
title: "track a.b.1".to_string(),
artist: vec!["artist a.b.1".to_string()],
},
Track {
number: 2,
title: "track a.b.2".to_string(),
artist: vec!["artist a.b.2".to_string()],
},
],
},
],
},
Artist {
id: ArtistId {
name: "album_artist b.a".to_string(),
},
albums: vec![Album {
id: AlbumId {
year: 2003,
title: "album_title b.a".to_string(),
},
tracks: vec![
Track {
number: 1,
title: "track b.a.1".to_string(),
artist: vec!["artist b.a.1".to_string()],
},
Track {
number: 2,
title: "track b.a.2".to_string(),
artist: vec![
"artist b.a.2.1".to_string(),
"artist b.a.2.2".to_string(),
],
},
],
}],
},
]
}
}

View File

@ -1,9 +1,13 @@
//! Module for interacting with the music library via //! Module for interacting with the music library via
//! [beets](https://beets.readthedocs.io/en/stable/). //! [beets](https://beets.readthedocs.io/en/stable/).
use std::{collections::HashSet, fmt::Display, process::Command}; use std::{
collections::{HashMap, HashSet},
fmt::Display,
process::Command,
};
use crate::{Album, AlbumId, Track}; use crate::{Album, AlbumId, Artist, ArtistId, Track};
use super::{Error, Library, Query, QueryOption}; use super::{Error, Library, Query, QueryOption};
@ -94,7 +98,7 @@ trait LibraryPrivate {
const LIST_FORMAT_ARG: &'static str; const LIST_FORMAT_ARG: &'static str;
fn list_cmd_and_args(query: &Query) -> Vec<String>; fn list_cmd_and_args(query: &Query) -> Vec<String>;
fn list_to_albums(list_output: Vec<String>) -> Result<Vec<Album>, Error>; fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>;
} }
impl Beets { impl Beets {
@ -104,10 +108,10 @@ impl Beets {
} }
impl Library for Beets { impl Library for Beets {
fn list(&mut self, query: &Query) -> Result<Vec<Album>, Error> { fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
let cmd = Self::list_cmd_and_args(query); let cmd = Self::list_cmd_and_args(query);
let output = self.executor.exec(&cmd)?; let output = self.executor.exec(&cmd)?;
Self::list_to_albums(output) Self::list_to_artists(output)
} }
} }
@ -142,9 +146,9 @@ impl LibraryPrivate for Beets {
cmd cmd
} }
fn list_to_albums(list_output: Vec<String>) -> Result<Vec<Album>, Error> { fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error> {
let mut albums: Vec<Album> = vec![]; let mut artists: Vec<Artist> = vec![];
let mut album_ids = HashSet::<AlbumId>::new(); let mut album_ids = HashMap::<ArtistId, HashSet<AlbumId>>::new();
for line in list_output.iter() { for line in list_output.iter() {
let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect(); let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect();
@ -160,8 +164,9 @@ impl LibraryPrivate for Beets {
let track_title = split[4].to_string(); let track_title = split[4].to_string();
let track_artist = split[5].to_string(); let track_artist = split[5].to_string();
let aid = AlbumId { let artist_id = ArtistId { name: album_artist };
artist: album_artist,
let album_id = AlbumId {
year: album_year, year: album_year,
title: album_title, title: album_title,
}; };
@ -172,20 +177,44 @@ impl LibraryPrivate for Beets {
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
}; };
if album_ids.contains(&aid) { let artist = if album_ids.contains_key(&artist_id) {
// Beets returns results in order so we look from the back. // Beets returns results in order so we look from the back.
let album = albums.iter_mut().rev().find(|a| a.id == aid).unwrap(); artists
.iter_mut()
.rev()
.find(|a| a.id == artist_id)
.unwrap()
} else {
album_ids.insert(artist_id.clone(), HashSet::<AlbumId>::new());
artists.push(Artist {
id: artist_id.clone(),
albums: vec![],
});
artists.last_mut().unwrap()
};
if album_ids[&artist_id].contains(&album_id) {
// Beets returns results in order so we look from the back.
let album = artist
.albums
.iter_mut()
.rev()
.find(|a| a.id == album_id)
.unwrap();
album.tracks.push(track); album.tracks.push(track);
} else { } else {
album_ids.insert(aid.clone()); album_ids
albums.push(Album { .get_mut(&artist_id)
id: aid, .unwrap()
.insert(album_id.clone());
artist.albums.push(Album {
id: album_id,
tracks: vec![track], tracks: vec![track],
}); });
} }
} }
Ok(albums) Ok(artists)
} }
} }
@ -220,6 +249,8 @@ impl BeetsExecutor for SystemExecutor {
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::test_data;
struct TestExecutor { struct TestExecutor {
arguments: Option<Vec<String>>, arguments: Option<Vec<String>>,
output: Option<Result<Vec<String>, Error>>, output: Option<Result<Vec<String>, Error>>,
@ -232,60 +263,15 @@ mod tests {
} }
} }
fn test_data() -> Vec<Album> { fn artist_to_beets_string(artist: &Artist) -> Vec<String> {
vec![ let mut strings = vec![];
Album {
id: AlbumId {
artist: "album_artist.a".to_string(),
year: 1998,
title: "album_title.a".to_string(),
},
tracks: vec![
Track {
number: 1,
title: "track.a.1".to_string(),
artist: vec!["artist.a.1".to_string()],
},
Track {
number: 2,
title: "track.a.2".to_string(),
artist: vec!["artist.a.2.1".to_string(), "artist.a.2.2".to_string()],
},
Track {
number: 3,
title: "track.a.3".to_string(),
artist: vec!["artist.a.3".to_string()],
},
],
},
Album {
id: AlbumId {
artist: "album_artist.b".to_string(),
year: 2003,
title: "album_title.b".to_string(),
},
tracks: vec![
Track {
number: 1,
title: "track.b.1".to_string(),
artist: vec!["artist.b.1".to_string()],
},
Track {
number: 2,
title: "track.b.2".to_string(),
artist: vec!["artist.b.2.1".to_string(), "artist.b.2.2".to_string()],
},
],
},
]
}
fn album_to_beets_string(album: &Album) -> Vec<String> { let album_artist = &artist.id.name;
let album_artist = &album.id.artist;
for album in artist.albums.iter() {
let album_year = &album.id.year; let album_year = &album.id.year;
let album_title = &album.id.title; let album_title = &album.id.title;
let mut strings = vec![];
for track in album.tracks.iter() { for track in album.tracks.iter() {
let track_number = &track.number; let track_number = &track.number;
let track_title = &track.title; let track_title = &track.title;
@ -297,14 +283,15 @@ mod tests {
Beets::LIST_FORMAT_SEPARATOR, Beets::LIST_FORMAT_SEPARATOR,
)); ));
} }
}
strings strings
} }
fn albums_to_beets_string(albums: &Vec<Album>) -> Vec<String> { fn artists_to_beets_string(artists: &Vec<Artist>) -> Vec<String> {
let mut strings = vec![]; let mut strings = vec![];
for album in albums.iter() { for artist in artists.iter() {
strings.append(&mut album_to_beets_string(album)); strings.append(&mut artist_to_beets_string(artist));
} }
strings strings
} }
@ -340,14 +327,14 @@ mod tests {
let mut beets = Beets::new(Box::new(executor)); let mut beets = Beets::new(Box::new(executor));
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
let expected: Vec<Album> = vec![]; let expected: Vec<Artist> = vec![];
assert_eq!(output, expected); assert_eq!(output, expected);
} }
#[test] #[test]
fn test_list_ordered() { fn test_list_ordered() {
let expected = test_data(); let expected = test_data();
let output = albums_to_beets_string(&expected); let output = artists_to_beets_string(&expected);
let executor = TestExecutor { let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
@ -362,18 +349,21 @@ mod tests {
#[test] #[test]
fn test_list_unordered() { fn test_list_unordered() {
let mut expected = test_data(); let mut expected = test_data();
let mut output = albums_to_beets_string(&expected); let mut output = artists_to_beets_string(&expected);
let last = output.len() - 1; let last = output.len() - 1;
output.swap(0, last); output.swap(0, last);
// Putting the last track first will make its entire album come first in the output. // Putting the last track first will make the entire artist come first in the output.
expected.rotate_right(1); expected.rotate_right(1);
// Same applies to that artists' albums, but here the artist has only one album.
assert_eq!(expected[0].albums.len(), 1);
// Same applies to that album's tracks. // Same applies to that album's tracks.
expected[0].tracks.rotate_right(1); expected[0].albums[0].tracks.rotate_right(1);
// And the (now) second album's tracks first track comes last. // And the (now) second album's tracks first track comes last.
expected[1].tracks.rotate_left(1); expected[1].albums[0].tracks.rotate_left(1);
let executor = TestExecutor { let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
@ -388,10 +378,10 @@ mod tests {
#[test] #[test]
fn test_list_album_title_year_clash() { fn test_list_album_title_year_clash() {
let mut expected = test_data(); let mut expected = test_data();
expected[1].id.year = expected[0].id.year; expected[0].albums[0].id.year = expected[1].albums[0].id.year;
expected[1].id.title = expected[0].id.title.clone(); expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone();
let output = albums_to_beets_string(&expected); let output = artists_to_beets_string(&expected);
let executor = TestExecutor { let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
@ -423,7 +413,7 @@ mod tests {
let mut beets = Beets::new(Box::new(executor)); let mut beets = Beets::new(Box::new(executor));
let output = beets.list(&query).unwrap(); let output = beets.list(&query).unwrap();
let expected: Vec<Album> = vec![]; let expected: Vec<Artist> = vec![];
assert_eq!(output, expected); assert_eq!(output, expected);
} }
} }

View File

@ -2,7 +2,7 @@
use std::{num::ParseIntError, str::Utf8Error}; use std::{num::ParseIntError, str::Utf8Error};
use crate::Album; use crate::Artist;
pub mod beets; pub mod beets;
@ -129,5 +129,5 @@ impl From<Utf8Error> for Error {
/// Trait for interacting with the music library. /// Trait for interacting with the music library.
pub trait Library { pub trait Library {
/// List lirbary items that match the a specific query. /// List lirbary items that match the a specific query.
fn list(&mut self, query: &Query) -> Result<Vec<Album>, Error>; fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error>;
} }