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:
parent
83893f1e7e
commit
5c7579ba23
@ -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<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![];
|
||||
for track in album.tracks.iter() {
|
||||
let track_number = track.number;
|
||||
let track_title = &track.title;
|
||||
let mut tracks: Vec<String> = vec![];
|
||||
for track in album.tracks.iter() {
|
||||
let track_number = track.number;
|
||||
let track_title = &track.title;
|
||||
|
||||
let mut track_artist: Vec<String> = vec![];
|
||||
for artist in track.artist.iter() {
|
||||
track_artist.push(format!("\"{artist}\""))
|
||||
let mut track_artist: Vec<String> = 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<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();
|
||||
|
||||
|
101
src/lib.rs
101
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<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(),
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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,79 +263,35 @@ 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 album_to_beets_string(album: &Album) -> Vec<String> {
|
||||
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<String> {
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user