diff --git a/src/main.rs b/src/main.rs index e07b4f8..9f1b678 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,4 +120,8 @@ fn test(entries: &SongEntries) { #[allow(dead_code)] fn test_plot(entries: &SongEntries) { plot::absolute::artist(entries, &types::Artist::from_str("Sabaton")); + + let coat = types::Album::from_str("Coat of Arms", "Sabaton"); + plot::relative::to_all(entries, &coat); + plot::relative::to_artist(entries, &coat); } diff --git a/src/plot.rs b/src/plot.rs index 84e47ff..0015153 100644 --- a/src/plot.rs +++ b/src/plot.rs @@ -9,7 +9,9 @@ use plotly::{Layout, Plot, Scatter}; /// Responsible for plotting absolute plots pub mod absolute; -/// Responsible for plotting plots relative to sum of plays +/// Responsible for plotting relative plots +/// +/// Either to all plays, the artist or the album pub mod relative; /// Creates a plot in a `plots/` folder diff --git a/src/plot/relative.rs b/src/plot/relative.rs index 0dea4d9..ddfa4dd 100644 --- a/src/plot/relative.rs +++ b/src/plot/relative.rs @@ -1,11 +1,11 @@ use super::{create_plot, find_dates}; use crate::display::date; -use crate::types::{Artist, SongEntries}; +use crate::types::{HasArtist, Music, Song, SongEntries}; -/// Creates a plot of the amount of plays of an [`Artist`] relative to all plays +/// Creates a plot of the amount of plays of a [`Music`] relative to all plays /// /// Opens the plot in the browser -pub fn artist(entries: &SongEntries, art: &Artist) { +pub fn to_all(entries: &SongEntries, aspect: &Asp) { let mut times = Vec::::new(); // percentages relative to the sum of all plays let mut plays = Vec::::new(); @@ -13,7 +13,7 @@ pub fn artist(entries: &SongEntries, art: &Artist) { // TODO! // each data point lies at the occurrence -> looks weird when you haven't listened in a long time // maybe make it so there's at least a data point once a week? - let dates = find_dates(entries, art, false); + let dates = find_dates(entries, aspect, false); let start = dates.first().unwrap(); let sum_start = &entries.first_date(); @@ -21,10 +21,73 @@ pub fn artist(entries: &SongEntries, art: &Artist) { #[allow(clippy::cast_precision_loss)] for date in &dates { times.push(date.timestamp()); - let sum_of_plays = date::gather_plays(entries, art, start, date) as f64; + let sum_of_plays = date::gather_plays(entries, aspect, start, date) as f64; let sum_of_all_plays = date::sum_plays(entries, sum_start, date) as f64; - plays.push(sum_of_plays / sum_of_all_plays); + // *100 so that the percentage is easier to read... + plays.push(100.0 * (sum_of_plays / sum_of_all_plays)); } - create_plot(times, plays, format!("{art} - relative").as_str()); + let title = format!("{aspect} | relative to all plays"); + create_plot(times, plays, title.as_str()); +} + +/// Creates a plot of the amount of plays of an [`Album`] or [`Song`] +/// relative to total plays of the affiated [`Artist`] +/// +/// Opens the plot in the browser +pub fn to_artist(entries: &SongEntries, aspect: &Asp) { + let mut times = Vec::::new(); + // percentages relative to the sum of all plays + let mut plays = Vec::::new(); + + // TODO! + // each data point lies at the occurrence -> looks weird when you haven't listened in a long time + // maybe make it so there's at least a data point once a week? + let dates = find_dates(entries, aspect, false); + + let start = dates.first().unwrap(); + let sum_start = &entries.first_date(); + + #[allow(clippy::cast_precision_loss)] + for date in &dates { + times.push(date.timestamp()); + let sum_of_plays = date::gather_plays(entries, aspect, start, date) as f64; + let sum_of_artist_plays = + date::gather_plays(entries, aspect.artist(), sum_start, date) as f64; + // *100 so that the percentage is easier to read... + plays.push(100.0 * (sum_of_plays / sum_of_artist_plays)); + } + + let title = format!("{aspect} | relative to the artist"); + create_plot(times, plays, title.as_str()); +} + +/// Creates a plot of the amount of plays of a [`Song`] +/// relative to total plays of the affiated [`Album`] +/// +/// Opens the plot in the browser +pub fn to_album(entries: &SongEntries, aspect: &Song) { + let mut times = Vec::::new(); + // percentages relative to the sum of all plays + let mut plays = Vec::::new(); + + // TODO! + // each data point lies at the occurrence -> looks weird when you haven't listened in a long time + // maybe make it so there's at least a data point once a week? + let dates = find_dates(entries, aspect, false); + + let start = dates.first().unwrap(); + let sum_start = &entries.first_date(); + + #[allow(clippy::cast_precision_loss)] + for date in &dates { + times.push(date.timestamp()); + let sum_of_plays = date::gather_plays(entries, aspect, start, date) as f64; + let sum_of_album_plays = date::gather_plays(entries, &aspect.album, sum_start, date) as f64; + // *100 so that the percentage is easier to read... + plays.push(100.0 * (sum_of_plays / sum_of_album_plays)); + } + + let title = format!("{aspect} | relative to the album"); + create_plot(times, plays, title.as_str()); } diff --git a/src/types.rs b/src/types.rs index a83cc14..fe5746c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -58,6 +58,12 @@ pub trait Music: Display { fn is_entry(&self, entry: &SongEntry) -> bool; } +/// Trait used to accept both [`Album`] and [`Song`] +pub trait HasArtist: Music { + /// Returns a reference to the corresponding [`Artist`] + fn artist(&self) -> &Artist; +} + /// Struct for representing an artist #[derive(PartialEq, Eq, Hash, Debug, Clone)] pub struct Artist { @@ -125,6 +131,11 @@ impl Music for Album { entry.artist.eq(&self.artist.name) && entry.album.eq(&self.name) } } +impl HasArtist for Album { + fn artist(&self) -> &Artist { + &self.artist + } +} /// Struct for representing a song // to allow for custom HashMap key @@ -170,6 +181,11 @@ impl Music for Song { && entry.track.eq(&self.name) } } +impl HasArtist for Song { + fn artist(&self) -> &Artist { + &self.album.artist + } +} /// A more specific version of [`parse::Entry`] /// utilized by many functions here. @@ -276,9 +292,19 @@ impl SongEntries { plot::absolute::artist(self, art); } - /// Creates a plot of the artist relative to the total amount of plays - pub fn plot_artist_relative(&self, art: &Artist) { - plot::relative::artist(self, art); + /// Creates a plot of the `aspect` relative to the total amount of plays + pub fn plot_relative(&self, aspect: &Asp) { + plot::relative::to_all(self, aspect); + } + + /// Creates a plot of the `aspect` relative to the plays of the artist + pub fn plot_relative_to_artist(&self, aspect: &Asp) { + plot::relative::to_artist(self, aspect); + } + + /// Creates a plot of the [`Song`] relative to the plays of the album + pub fn plot_relative_to_album(&self, song: &Song) { + plot::relative::to_album(self, song); } /// Adds search capability diff --git a/src/ui.rs b/src/ui.rs index ae3a2a1..45248c2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -31,6 +31,32 @@ const PROMPT_MAIN: &str = " >> "; /// red ` >` with [`ShellHelper`] const PROMPT_SECONDARY: &str = " > "; +/// Errors raised by [`match_plot_album_relative()`] and +/// [`match_plot_song_relative`] +/// +/// when user argument for relative to what is invalid +#[derive(Debug)] +enum InvalidArgumentError { + /// Error message: Invalid argument! Try using 'all' or 'artist' next time + Artist, + /// Error message: Invalid argument! Try using 'all', 'artist' or 'album' next time + Album, +} +impl std::fmt::Display for InvalidArgumentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InvalidArgumentError::Artist => { + write!(f, "Invalid argument! Try using 'all' or 'artist' next time") + } + InvalidArgumentError::Album => write!( + f, + "Invalid argument! Try using 'all', 'artist' or 'album' next time" + ), + } + } +} +impl Error for InvalidArgumentError {} + /// Helper for [`Editor`] #[derive(Completer, Helper, Hinter, Validator)] struct ShellHelper; @@ -133,7 +159,8 @@ pub fn start(entries: &SongEntries) { /// Handles errors thrown by [`match_input()`] in [`start()`] /// -/// Prints error messages for [`NotFoundError`], +/// Prints error messages for +/// [`NotFoundError`], [`InvalidArgumentError`] /// [`ParseError`][`chrono::format::ParseError`], /// and [`ParseIntError`][`std::num::ParseIntError`] #[allow(clippy::borrowed_box)] @@ -142,6 +169,7 @@ fn handle_error(err: &Box) { // also thx ChatGPT match err.as_ref() { not_found if not_found.is::() => eprintln!("{not_found}"), + invalid_arg if invalid_arg.is::() => eprintln!("{invalid_arg}"), date if date.is::() => { eprintln!("Invalid date! Make sure you input the date in YYYY-MM-DD format."); } @@ -175,6 +203,8 @@ fn match_input( "print top songs" | "ptsons" => match_print_top(entries, rl, &Aspect::Songs)?, "plot artist" | "gart" => match_plot_artist(entries, rl)?, "plot artist relative" | "gartr" => match_plot_artist_relative(entries, rl)?, + "plot album relative" | "galbr" => match_plot_album_relative(entries, rl)?, + "plot song relative" | "gsonr" => match_plot_song_relative(entries, rl)?, // when you press ENTER -> nothing happens, new prompt "" => (), _ => { @@ -451,7 +481,71 @@ fn match_plot_artist_relative( let usr_input_art = rl.readline(PROMPT_MAIN)?; let art = entries.find().artist(&usr_input_art)?; - entries.plot_artist_relative(&art); + entries.plot_relative(&art); + Ok(()) +} + +/// Used by [`match_input()`] for `plot album relative` command +fn match_plot_album_relative( + entries: &SongEntries, + rl: &mut Editor, +) -> Result<(), Box> { + // 1st prompt: artist name + println!("Artist name?"); + let usr_input_art = rl.readline(PROMPT_MAIN)?; + let art = entries.find().artist(&usr_input_art)?; + + // 2nd prompt: album name + println!("Album name?"); + let usr_input_alb = rl.readline(PROMPT_MAIN)?; + let alb = entries.find().album(&usr_input_alb, &art.name)?; + + // 3rd prompt: relative to what + println!("Relative to all or artist?"); + let usr_input_rel = rl.readline(PROMPT_SECONDARY)?; + + match usr_input_rel.as_str() { + "all" => entries.plot_relative(&alb), + "artist" => entries.plot_relative_to_artist(&alb), + _ => return Err(Box::new(InvalidArgumentError::Artist)), + } + + Ok(()) +} + +/// Used by [`match_input()`] for `plot song relative` command +fn match_plot_song_relative( + entries: &SongEntries, + rl: &mut Editor, +) -> Result<(), Box> { + // 1st prompt: artist name + println!("Artist name?"); + let usr_input_art = rl.readline(PROMPT_MAIN)?; + let art = entries.find().artist(&usr_input_art)?; + + // 2nd prompt: album name + println!("Album name?"); + let usr_input_alb = rl.readline(PROMPT_MAIN)?; + let alb = entries.find().album(&usr_input_alb, &art.name)?; + + // 3rd prompt: song name + println!("Song name?"); + let usr_input_son = rl.readline(PROMPT_MAIN)?; + let son = entries + .find() + .song_from_album(&usr_input_son, &alb.name, &alb.artist.name)?; + + // 4th prompt: relative to what + println!("Relative to all, artist or album?"); + let usr_input_rel = rl.readline(PROMPT_SECONDARY)?; + + match usr_input_rel.as_str() { + "all" => entries.plot_relative(&son), + "artist" => entries.plot_relative_to_artist(&son), + "album" => entries.plot_relative_to_album(&son), + _ => return Err(Box::new(InvalidArgumentError::Album)), + } + Ok(()) } diff --git a/src/ui/help.rs b/src/ui/help.rs index a8d98c2..d2418a6 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -208,6 +208,20 @@ fn plot_commands<'a>() -> Vec<[&'a str; 3]> { "creates a plot of the amount of plays of the given artist relative to all plays and opens it in the web browser", ], + [ + "plot album relative", + "gartr", + "creates a plot of the amount of plays of the given album + relative to all plays or the artist + and opens it in the web browser", + ], + [ + "plot song relative", + "gartr", + "creates a plot of the amount of plays of the given album + relative to all plays, the artist or the album + and opens it in the web browser", + ], ] }