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

View File

@ -20,7 +20,6 @@ pub struct Track {
/// The album identifier.
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)]
pub struct AlbumId {
pub artist: String,
pub year: u32,
pub title: String,
}
@ -31,3 +30,103 @@ pub struct Album {
pub id: AlbumId,
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
//! [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};
@ -94,7 +98,7 @@ trait LibraryPrivate {
const LIST_FORMAT_ARG: &'static str;
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 {
@ -104,10 +108,10 @@ impl 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 output = self.executor.exec(&cmd)?;
Self::list_to_albums(output)
Self::list_to_artists(output)
}
}
@ -142,9 +146,9 @@ impl LibraryPrivate for Beets {
cmd
}
fn list_to_albums(list_output: Vec<String>) -> Result<Vec<Album>, Error> {
let mut albums: Vec<Album> = vec![];
let mut album_ids = HashSet::<AlbumId>::new();
fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error> {
let mut artists: Vec<Artist> = vec![];
let mut album_ids = HashMap::<ArtistId, HashSet<AlbumId>>::new();
for line in list_output.iter() {
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_artist = split[5].to_string();
let aid = AlbumId {
artist: album_artist,
let artist_id = ArtistId { name: album_artist };
let album_id = AlbumId {
year: album_year,
title: album_title,
};
@ -172,20 +177,44 @@ impl LibraryPrivate for Beets {
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.
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);
} else {
album_ids.insert(aid.clone());
albums.push(Album {
id: aid,
album_ids
.get_mut(&artist_id)
.unwrap()
.insert(album_id.clone());
artist.albums.push(Album {
id: album_id,
tracks: vec![track],
});
}
}
Ok(albums)
Ok(artists)
}
}
@ -220,6 +249,8 @@ impl BeetsExecutor for SystemExecutor {
mod tests {
use super::*;
use crate::tests::test_data;
struct TestExecutor {
arguments: Option<Vec<String>>,
output: Option<Result<Vec<String>, Error>>,
@ -232,60 +263,15 @@ mod tests {
}
}
fn test_data() -> Vec<Album> {
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 artist_to_beets_string(artist: &Artist) -> Vec<String> {
let mut strings = vec![];
fn album_to_beets_string(album: &Album) -> Vec<String> {
let album_artist = &album.id.artist;
let album_artist = &artist.id.name;
for album in artist.albums.iter() {
let album_year = &album.id.year;
let album_title = &album.id.title;
let mut strings = vec![];
for track in album.tracks.iter() {
let track_number = &track.number;
let track_title = &track.title;
@ -297,14 +283,15 @@ mod tests {
Beets::LIST_FORMAT_SEPARATOR,
));
}
}
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![];
for album in albums.iter() {
strings.append(&mut album_to_beets_string(album));
for artist in artists.iter() {
strings.append(&mut artist_to_beets_string(artist));
}
strings
}
@ -340,14 +327,14 @@ mod tests {
let mut beets = Beets::new(Box::new(executor));
let output = beets.list(&Query::default()).unwrap();
let expected: Vec<Album> = vec![];
let expected: Vec<Artist> = vec![];
assert_eq!(output, expected);
}
#[test]
fn test_list_ordered() {
let expected = test_data();
let output = albums_to_beets_string(&expected);
let output = artists_to_beets_string(&expected);
let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
@ -362,18 +349,21 @@ mod tests {
#[test]
fn test_list_unordered() {
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;
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);
// 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.
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.
expected[1].tracks.rotate_left(1);
expected[1].albums[0].tracks.rotate_left(1);
let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
@ -388,10 +378,10 @@ mod tests {
#[test]
fn test_list_album_title_year_clash() {
let mut expected = test_data();
expected[1].id.year = expected[0].id.year;
expected[1].id.title = expected[0].id.title.clone();
expected[0].albums[0].id.year = expected[1].albums[0].id.year;
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 {
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 output = beets.list(&query).unwrap();
let expected: Vec<Album> = vec![];
let expected: Vec<Artist> = vec![];
assert_eq!(output, expected);
}
}

View File

@ -2,7 +2,7 @@
use std::{num::ParseIntError, str::Utf8Error};
use crate::Album;
use crate::Artist;
pub mod beets;
@ -129,5 +129,5 @@ impl From<Utf8Error> for Error {
/// Trait for interacting with the music library.
pub trait Library {
/// 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>;
}