Skip to content

Commit

Permalink
#22 Add relative plots for albums and songs
Browse files Browse the repository at this point in the history
  • Loading branch information
fsktom committed Mar 30, 2023
1 parent 14d2e5d commit d5353dc
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 13 deletions.
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
4 changes: 3 additions & 1 deletion src/plot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 70 additions & 7 deletions src/plot/relative.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,93 @@
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<Asp: Music>(entries: &SongEntries, aspect: &Asp) {
let mut times = Vec::<i64>::new();
// percentages relative to the sum of all plays
let mut plays = Vec::<f64>::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, art, false);
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, 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<Asp: HasArtist>(entries: &SongEntries, aspect: &Asp) {
let mut times = Vec::<i64>::new();
// percentages relative to the sum of all plays
let mut plays = Vec::<f64>::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::<i64>::new();
// percentages relative to the sum of all plays
let mut plays = Vec::<f64>::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());
}
32 changes: 29 additions & 3 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Asp: Music>(&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<Asp: HasArtist>(&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
Expand Down
98 changes: 96 additions & 2 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand All @@ -142,6 +169,7 @@ fn handle_error(err: &Box<dyn Error>) {
// also thx ChatGPT
match err.as_ref() {
not_found if not_found.is::<NotFoundError>() => eprintln!("{not_found}"),
invalid_arg if invalid_arg.is::<InvalidArgumentError>() => eprintln!("{invalid_arg}"),
date if date.is::<chrono::format::ParseError>() => {
eprintln!("Invalid date! Make sure you input the date in YYYY-MM-DD format.");
}
Expand Down Expand Up @@ -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
"" => (),
_ => {
Expand Down Expand Up @@ -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<ShellHelper, FileHistory>,
) -> Result<(), Box<dyn Error>> {
// 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<ShellHelper, FileHistory>,
) -> Result<(), Box<dyn Error>> {
// 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(())
}

Expand Down
14 changes: 14 additions & 0 deletions src/ui/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
]
}

Expand Down

0 comments on commit d5353dc

Please sign in to comment.