diff --git a/src/lib.rs b/src/lib.rs index d372e05..61f73d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,13 @@ pub mod database; pub mod library; -use std::fmt; +use std::{ + collections::{HashMap, HashSet}, + fmt, +}; use database::IDatabase; -use library::{ILibrary, Query}; +use library::{ILibrary, Item, Query}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -117,7 +120,8 @@ impl MusicHoard { } 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(()) } @@ -129,6 +133,72 @@ impl MusicHoard { pub fn get_collection(&self) -> &Collection { &self.collection } + + fn items_to_artists(items: Vec) -> Vec { + let mut artists: Vec = vec![]; + let mut album_ids = HashMap::>::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::::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)] diff --git a/src/library/beets/mod.rs b/src/library/beets/mod.rs index 761e974..cedbfd0 100644 --- a/src/library/beets/mod.rs +++ b/src/library/beets/mod.rs @@ -1,17 +1,12 @@ //! Module for interacting with the music library via //! [beets](https://beets.readthedocs.io/en/stable/). -use std::{ - collections::{HashMap, HashSet}, - str, -}; - #[cfg(test)] 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; @@ -92,7 +87,7 @@ pub struct BeetsLibrary { trait ILibraryPrivate { fn list_cmd_and_args(query: &Query) -> Vec; - fn list_to_artists>(list_output: &[S]) -> Result, Error>; + fn list_to_items>(list_output: &[S]) -> Result, Error>; } impl BeetsLibrary { @@ -104,10 +99,10 @@ impl BeetsLibrary { } impl ILibrary for BeetsLibrary { - 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_artists(&output) + Self::list_to_items(&output) } } @@ -119,9 +114,8 @@ impl ILibraryPrivate for BeetsLibrary { cmd } - fn list_to_artists>(list_output: &[S]) -> Result, Error> { - let mut artists: Vec = vec![]; - let mut album_ids = HashMap::>::new(); + fn list_to_items>(list_output: &[S]) -> Result, Error> { + let mut items: Vec = vec![]; for line in list_output.iter().map(|s| s.as_ref()) { if line.is_empty() { @@ -138,69 +132,31 @@ impl ILibraryPrivate for BeetsLibrary { let album_title = split[2].to_string(); let track_number = split[3].parse::()?; let track_title = split[4].to_string(); - let track_artist = split[5].to_string(); - let track_format = split[6].to_string(); + let track_artist = split[5] + .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::()?; - let artist_id = ArtistId { name: album_artist }; - - let album_id = AlbumId { - year: album_year, - title: album_title, - }; - - let track = Track { - number: track_number, - 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::::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], - }); - } + items.push(Item { + album_artist, + album_year, + album_title, + track_number, + track_title, + track_artist, + track_format, + track_bitrate, + }); } - Ok(artists) + Ok(items) } } diff --git a/src/library/mod.rs b/src/library/mod.rs index 506e54e..eba3b2d 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -5,7 +5,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error}; #[cfg(test)] use mockall::automock; -use crate::Artist; +use crate::Format; #[cfg(feature = "library-beets")] pub mod beets; @@ -14,7 +14,19 @@ pub mod beets; #[cfg_attr(test, automock)] pub trait ILibrary { /// 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>; +} + +/// 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, + pub track_format: Format, + pub track_bitrate: u32, } /// Individual fields that can be queried on.