Library now returns vector of Item

This commit is contained in:
Wojciech Kozlowski 2023-05-14 16:14:30 +02:00
parent d20a9a9dec
commit 1e1dbe8688
3 changed files with 115 additions and 77 deletions

View File

@ -3,10 +3,13 @@
pub mod database; pub mod database;
pub mod library; pub mod library;
use std::fmt; use std::{
collections::{HashMap, HashSet},
fmt,
};
use database::IDatabase; use database::IDatabase;
use library::{ILibrary, Query}; use library::{ILibrary, Item, Query};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -117,7 +120,8 @@ impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
} }
pub fn rescan_library(&mut self) -> Result<(), Error> { pub fn rescan_library(&mut self) -> Result<(), Error> {
self.collection = self.library.list(&Query::new())?; let items = self.library.list(&Query::new())?;
self.collection = Self::items_to_artists(items);
Ok(()) Ok(())
} }
@ -129,6 +133,72 @@ impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
pub fn get_collection(&self) -> &Collection { pub fn get_collection(&self) -> &Collection {
&self.collection &self.collection
} }
fn items_to_artists(items: Vec<Item>) -> Vec<Artist> {
let mut artists: Vec<Artist> = vec![];
let mut album_ids = HashMap::<ArtistId, HashSet<AlbumId>>::new();
for item in items.into_iter() {
let artist_id = ArtistId { name: item.album_artist };
let album_id = AlbumId {
year: item.album_year,
title: item.album_title,
};
let track = Track {
number: item.track_number,
title: item.track_title,
artist: item.track_artist,
quality: Quality {
format: item.track_format,
bitrate: item.track_bitrate,
},
};
let artist = if album_ids.contains_key(&artist_id) {
// Assume results are in some order which means they will likely be grouped by
// artist. Therefore, we look from the back since the last inserted artist is most
// likely the one we are looking for.
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) {
// Assume results are in some order which means they will likely be grouped by
// album. Therefore, we look from the back since the last inserted album is most
// likely the one we are looking for.
let album = artist
.albums
.iter_mut()
.rev()
.find(|a| a.id == album_id)
.unwrap();
album.tracks.push(track);
} else {
album_ids
.get_mut(&artist_id)
.unwrap()
.insert(album_id.clone());
artist.albums.push(Album {
id: album_id,
tracks: vec![track],
});
}
}
artists
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,17 +1,12 @@
//! 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::{HashMap, HashSet},
str,
};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use crate::{Album, AlbumId, Artist, ArtistId, Format, Quality, Track}; use crate::Format;
use super::{Error, Field, ILibrary, Query}; use super::{Error, Field, ILibrary, Item, Query};
pub mod executor; pub mod executor;
@ -92,7 +87,7 @@ pub struct BeetsLibrary<BLE> {
trait ILibraryPrivate { trait ILibraryPrivate {
fn list_cmd_and_args(query: &Query) -> Vec<String>; fn list_cmd_and_args(query: &Query) -> Vec<String>;
fn list_to_artists<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Artist>, Error>; fn list_to_items<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Item>, Error>;
} }
impl<BLE: IBeetsLibraryExecutor> BeetsLibrary<BLE> { impl<BLE: IBeetsLibraryExecutor> BeetsLibrary<BLE> {
@ -104,10 +99,10 @@ impl<BLE: IBeetsLibraryExecutor> BeetsLibrary<BLE> {
} }
impl<BLE: IBeetsLibraryExecutor> ILibrary for BeetsLibrary<BLE> { impl<BLE: IBeetsLibraryExecutor> ILibrary for BeetsLibrary<BLE> {
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> { fn list(&mut self, query: &Query) -> Result<Vec<Item>, 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_artists(&output) Self::list_to_items(&output)
} }
} }
@ -119,9 +114,8 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
cmd cmd
} }
fn list_to_artists<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Artist>, Error> { fn list_to_items<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Item>, Error> {
let mut artists: Vec<Artist> = vec![]; let mut items: Vec<Item> = vec![];
let mut album_ids = HashMap::<ArtistId, HashSet<AlbumId>>::new();
for line in list_output.iter().map(|s| s.as_ref()) { for line in list_output.iter().map(|s| s.as_ref()) {
if line.is_empty() { if line.is_empty() {
@ -138,69 +132,31 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
let album_title = split[2].to_string(); let album_title = split[2].to_string();
let track_number = split[3].parse::<u32>()?; let track_number = split[3].parse::<u32>()?;
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]
let track_format = split[6].to_string(); .to_string()
.split("; ")
.map(|s| s.to_owned())
.collect();
let track_format = match split[6].to_string().as_str() {
TRACK_FORMAT_FLAC => Format::Flac,
TRACK_FORMAT_MP3 => Format::Mp3,
_ => return Err(Error::Invalid(line.to_string())),
};
let track_bitrate = split[7].trim_end_matches("kbps").parse::<u32>()?; let track_bitrate = split[7].trim_end_matches("kbps").parse::<u32>()?;
let artist_id = ArtistId { name: album_artist }; items.push(Item {
album_artist,
let album_id = AlbumId { album_year,
year: album_year, album_title,
title: album_title, track_number,
}; track_title,
track_artist,
let track = Track { track_format,
number: track_number, track_bitrate,
title: track_title, });
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
quality: Quality {
format: match track_format.as_ref() {
TRACK_FORMAT_FLAC => Format::Flac,
TRACK_FORMAT_MP3 => Format::Mp3,
_ => return Err(Error::Invalid(line.to_string())),
},
bitrate: track_bitrate,
},
};
let artist = if album_ids.contains_key(&artist_id) {
// Beets returns results in order so we look from the back.
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
.get_mut(&artist_id)
.unwrap()
.insert(album_id.clone());
artist.albums.push(Album {
id: album_id,
tracks: vec![track],
});
}
} }
Ok(artists) Ok(items)
} }
} }

View File

@ -5,7 +5,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use crate::Artist; use crate::Format;
#[cfg(feature = "library-beets")] #[cfg(feature = "library-beets")]
pub mod beets; pub mod beets;
@ -14,7 +14,19 @@ pub mod beets;
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait ILibrary { pub trait ILibrary {
/// 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<Artist>, Error>; fn list(&mut self, query: &Query) -> Result<Vec<Item>, Error>;
}
/// An item from the library. An item corresponds to an individual file (usually a single track).
pub struct Item {
pub album_artist: String,
pub album_year: u32,
pub album_title: String,
pub track_number: u32,
pub track_title: String,
pub track_artist: Vec<String>,
pub track_format: Format,
pub track_bitrate: u32,
} }
/// Individual fields that can be queried on. /// Individual fields that can be queried on.