From 5c7579ba23dee770072237638dff8c88cb279752 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 1 Apr 2023 01:59:59 +0200 Subject: [PATCH] Add artist as top level in data structure hierarchy (#13) Closes #12 Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/13 --- src/database/json.rs | 117 +++++++++++------------------ src/lib.rs | 101 ++++++++++++++++++++++++- src/library/beets.rs | 172 ++++++++++++++++++++----------------------- src/library/mod.rs | 4 +- 4 files changed, 227 insertions(+), 167 deletions(-) diff --git a/src/database/json.rs b/src/database/json.rs index 05477cb..e2f7278 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -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,98 +94,69 @@ mod tests { } } - fn test_data() -> Vec { - 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 = vec![]; + for album in artist.albums.iter() { + let album_year = album.id.year; + let album_title = &album.id.title; - let mut album_tracks: Vec = vec![]; - for track in album.tracks.iter() { - let track_number = track.number; - let track_title = &track.title; + let mut tracks: Vec = vec![]; + for track in album.tracks.iter() { + let track_number = track.number; + let track_title = &track.title; - let mut track_artist: Vec = vec![]; - for artist in track.artist.iter() { - track_artist.push(format!("\"{artist}\"")) + let mut track_artist: Vec = vec![]; + for artist in track.artist.iter() { + track_artist.push(format!("\"{artist}\"")) + } + let track_artist = track_artist.join(","); + + tracks.push(format!( + "{{\ + \"number\":{track_number},\ + \"title\":\"{track_title}\",\ + \"artist\":[{track_artist}]\ + }}" + )); } - let track_artist = track_artist.join(","); + let tracks = tracks.join(","); - album_tracks.push(format!( + albums.push(format!( "{{\ - \"number\":{track_number},\ - \"title\":\"{track_title}\",\ - \"artist\":[{track_artist}]\ + \"id\":{{\ + \"year\":{album_year},\ + \"title\":\"{album_title}\"\ + }},\"tracks\":[{tracks}]\ }}" )); } - let album_tracks = album_tracks.join(","); + 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) -> String { - let mut albums_strings: Vec = vec![]; - for album in albums.iter() { - albums_strings.push(album_to_json(album)); + fn artists_to_json(artists: &Vec) -> String { + let mut artists_strings: Vec = 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 = vec![]; + let mut read_data: Vec = 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 = vec![]; + let mut read_data: Vec = vec![]; database.write(&write_data).unwrap(); database.read(&mut read_data).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 1110f60..d471250 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, } + +/// 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, +} + +#[cfg(test)] +mod tests { + use super::*; + + pub fn test_data() -> Vec { + 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(), + ], + }, + ], + }], + }, + ] + } +} diff --git a/src/library/beets.rs b/src/library/beets.rs index 3ce2d94..1140210 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -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; - fn list_to_albums(list_output: Vec) -> Result, Error>; + fn list_to_artists(list_output: Vec) -> Result, Error>; } impl Beets { @@ -104,10 +108,10 @@ impl Beets { } impl Library for Beets { - fn list(&mut self, query: &Query) -> Result, Error> { + fn list(&mut self, query: &Query) -> Result, 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) -> Result, Error> { - let mut albums: Vec = vec![]; - let mut album_ids = HashSet::::new(); + fn list_to_artists(list_output: Vec) -> Result, Error> { + let mut artists: Vec = vec![]; + let mut album_ids = HashMap::>::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::::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>, output: Option, Error>>, @@ -232,79 +263,35 @@ mod tests { } } - fn test_data() -> Vec { - 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 { - let album_artist = &album.id.artist; - let album_year = &album.id.year; - let album_title = &album.id.title; - + fn artist_to_beets_string(artist: &Artist) -> Vec { let mut strings = vec![]; - for track in album.tracks.iter() { - let track_number = &track.number; - let track_title = &track.title; - let track_artist = &track.artist.join("; "); - strings.push(format!( - "{album_artist}{0}{album_year}{0}{album_title}{0}\ - {track_number}{0}{track_title}{0}{track_artist}", - Beets::LIST_FORMAT_SEPARATOR, - )); + let album_artist = &artist.id.name; + + for album in artist.albums.iter() { + let album_year = &album.id.year; + let album_title = &album.id.title; + + for track in album.tracks.iter() { + let track_number = &track.number; + let track_title = &track.title; + let track_artist = &track.artist.join("; "); + + strings.push(format!( + "{album_artist}{0}{album_year}{0}{album_title}{0}\ + {track_number}{0}{track_title}{0}{track_artist}", + Beets::LIST_FORMAT_SEPARATOR, + )); + } } strings } - fn albums_to_beets_string(albums: &Vec) -> Vec { + fn artists_to_beets_string(artists: &Vec) -> Vec { 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 = vec![]; + let expected: Vec = 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 = vec![]; + let expected: Vec = vec![]; assert_eq!(output, expected); } } diff --git a/src/library/mod.rs b/src/library/mod.rs index 4201d24..ed8eb25 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -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 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, Error>; + fn list(&mut self, query: &Query) -> Result, Error>; }