Skip to content

Commit d5353dc

Browse files
committed
#22 Add relative plots for albums and songs
1 parent 14d2e5d commit d5353dc

File tree

6 files changed

+216
-13
lines changed

6 files changed

+216
-13
lines changed

src/main.rs

+4
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,8 @@ fn test(entries: &SongEntries) {
120120
#[allow(dead_code)]
121121
fn test_plot(entries: &SongEntries) {
122122
plot::absolute::artist(entries, &types::Artist::from_str("Sabaton"));
123+
124+
let coat = types::Album::from_str("Coat of Arms", "Sabaton");
125+
plot::relative::to_all(entries, &coat);
126+
plot::relative::to_artist(entries, &coat);
123127
}

src/plot.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use plotly::{Layout, Plot, Scatter};
99
/// Responsible for plotting absolute plots
1010
pub mod absolute;
1111

12-
/// Responsible for plotting plots relative to sum of plays
12+
/// Responsible for plotting relative plots
13+
///
14+
/// Either to all plays, the artist or the album
1315
pub mod relative;
1416

1517
/// Creates a plot in a `plots/` folder

src/plot/relative.rs

+70-7
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,93 @@
11
use super::{create_plot, find_dates};
22
use crate::display::date;
3-
use crate::types::{Artist, SongEntries};
3+
use crate::types::{HasArtist, Music, Song, SongEntries};
44

5-
/// Creates a plot of the amount of plays of an [`Artist`] relative to all plays
5+
/// Creates a plot of the amount of plays of a [`Music`] relative to all plays
66
///
77
/// Opens the plot in the browser
8-
pub fn artist(entries: &SongEntries, art: &Artist) {
8+
pub fn to_all<Asp: Music>(entries: &SongEntries, aspect: &Asp) {
99
let mut times = Vec::<i64>::new();
1010
// percentages relative to the sum of all plays
1111
let mut plays = Vec::<f64>::new();
1212

1313
// TODO!
1414
// each data point lies at the occurrence -> looks weird when you haven't listened in a long time
1515
// maybe make it so there's at least a data point once a week?
16-
let dates = find_dates(entries, art, false);
16+
let dates = find_dates(entries, aspect, false);
1717

1818
let start = dates.first().unwrap();
1919
let sum_start = &entries.first_date();
2020

2121
#[allow(clippy::cast_precision_loss)]
2222
for date in &dates {
2323
times.push(date.timestamp());
24-
let sum_of_plays = date::gather_plays(entries, art, start, date) as f64;
24+
let sum_of_plays = date::gather_plays(entries, aspect, start, date) as f64;
2525
let sum_of_all_plays = date::sum_plays(entries, sum_start, date) as f64;
26-
plays.push(sum_of_plays / sum_of_all_plays);
26+
// *100 so that the percentage is easier to read...
27+
plays.push(100.0 * (sum_of_plays / sum_of_all_plays));
2728
}
2829

29-
create_plot(times, plays, format!("{art} - relative").as_str());
30+
let title = format!("{aspect} | relative to all plays");
31+
create_plot(times, plays, title.as_str());
32+
}
33+
34+
/// Creates a plot of the amount of plays of an [`Album`] or [`Song`]
35+
/// relative to total plays of the affiated [`Artist`]
36+
///
37+
/// Opens the plot in the browser
38+
pub fn to_artist<Asp: HasArtist>(entries: &SongEntries, aspect: &Asp) {
39+
let mut times = Vec::<i64>::new();
40+
// percentages relative to the sum of all plays
41+
let mut plays = Vec::<f64>::new();
42+
43+
// TODO!
44+
// each data point lies at the occurrence -> looks weird when you haven't listened in a long time
45+
// maybe make it so there's at least a data point once a week?
46+
let dates = find_dates(entries, aspect, false);
47+
48+
let start = dates.first().unwrap();
49+
let sum_start = &entries.first_date();
50+
51+
#[allow(clippy::cast_precision_loss)]
52+
for date in &dates {
53+
times.push(date.timestamp());
54+
let sum_of_plays = date::gather_plays(entries, aspect, start, date) as f64;
55+
let sum_of_artist_plays =
56+
date::gather_plays(entries, aspect.artist(), sum_start, date) as f64;
57+
// *100 so that the percentage is easier to read...
58+
plays.push(100.0 * (sum_of_plays / sum_of_artist_plays));
59+
}
60+
61+
let title = format!("{aspect} | relative to the artist");
62+
create_plot(times, plays, title.as_str());
63+
}
64+
65+
/// Creates a plot of the amount of plays of a [`Song`]
66+
/// relative to total plays of the affiated [`Album`]
67+
///
68+
/// Opens the plot in the browser
69+
pub fn to_album(entries: &SongEntries, aspect: &Song) {
70+
let mut times = Vec::<i64>::new();
71+
// percentages relative to the sum of all plays
72+
let mut plays = Vec::<f64>::new();
73+
74+
// TODO!
75+
// each data point lies at the occurrence -> looks weird when you haven't listened in a long time
76+
// maybe make it so there's at least a data point once a week?
77+
let dates = find_dates(entries, aspect, false);
78+
79+
let start = dates.first().unwrap();
80+
let sum_start = &entries.first_date();
81+
82+
#[allow(clippy::cast_precision_loss)]
83+
for date in &dates {
84+
times.push(date.timestamp());
85+
let sum_of_plays = date::gather_plays(entries, aspect, start, date) as f64;
86+
let sum_of_album_plays = date::gather_plays(entries, &aspect.album, sum_start, date) as f64;
87+
// *100 so that the percentage is easier to read...
88+
plays.push(100.0 * (sum_of_plays / sum_of_album_plays));
89+
}
90+
91+
let title = format!("{aspect} | relative to the album");
92+
create_plot(times, plays, title.as_str());
3093
}

src/types.rs

+29-3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ pub trait Music: Display {
5858
fn is_entry(&self, entry: &SongEntry) -> bool;
5959
}
6060

61+
/// Trait used to accept both [`Album`] and [`Song`]
62+
pub trait HasArtist: Music {
63+
/// Returns a reference to the corresponding [`Artist`]
64+
fn artist(&self) -> &Artist;
65+
}
66+
6167
/// Struct for representing an artist
6268
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
6369
pub struct Artist {
@@ -125,6 +131,11 @@ impl Music for Album {
125131
entry.artist.eq(&self.artist.name) && entry.album.eq(&self.name)
126132
}
127133
}
134+
impl HasArtist for Album {
135+
fn artist(&self) -> &Artist {
136+
&self.artist
137+
}
138+
}
128139

129140
/// Struct for representing a song
130141
// to allow for custom HashMap key
@@ -170,6 +181,11 @@ impl Music for Song {
170181
&& entry.track.eq(&self.name)
171182
}
172183
}
184+
impl HasArtist for Song {
185+
fn artist(&self) -> &Artist {
186+
&self.album.artist
187+
}
188+
}
173189

174190
/// A more specific version of [`parse::Entry`]
175191
/// utilized by many functions here.
@@ -276,9 +292,19 @@ impl SongEntries {
276292
plot::absolute::artist(self, art);
277293
}
278294

279-
/// Creates a plot of the artist relative to the total amount of plays
280-
pub fn plot_artist_relative(&self, art: &Artist) {
281-
plot::relative::artist(self, art);
295+
/// Creates a plot of the `aspect` relative to the total amount of plays
296+
pub fn plot_relative<Asp: Music>(&self, aspect: &Asp) {
297+
plot::relative::to_all(self, aspect);
298+
}
299+
300+
/// Creates a plot of the `aspect` relative to the plays of the artist
301+
pub fn plot_relative_to_artist<Asp: HasArtist>(&self, aspect: &Asp) {
302+
plot::relative::to_artist(self, aspect);
303+
}
304+
305+
/// Creates a plot of the [`Song`] relative to the plays of the album
306+
pub fn plot_relative_to_album(&self, song: &Song) {
307+
plot::relative::to_album(self, song);
282308
}
283309

284310
/// Adds search capability

src/ui.rs

+96-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ const PROMPT_MAIN: &str = " >> ";
3131
/// red ` >` with [`ShellHelper`]
3232
const PROMPT_SECONDARY: &str = " > ";
3333

34+
/// Errors raised by [`match_plot_album_relative()`] and
35+
/// [`match_plot_song_relative`]
36+
///
37+
/// when user argument for relative to what is invalid
38+
#[derive(Debug)]
39+
enum InvalidArgumentError {
40+
/// Error message: Invalid argument! Try using 'all' or 'artist' next time
41+
Artist,
42+
/// Error message: Invalid argument! Try using 'all', 'artist' or 'album' next time
43+
Album,
44+
}
45+
impl std::fmt::Display for InvalidArgumentError {
46+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47+
match self {
48+
InvalidArgumentError::Artist => {
49+
write!(f, "Invalid argument! Try using 'all' or 'artist' next time")
50+
}
51+
InvalidArgumentError::Album => write!(
52+
f,
53+
"Invalid argument! Try using 'all', 'artist' or 'album' next time"
54+
),
55+
}
56+
}
57+
}
58+
impl Error for InvalidArgumentError {}
59+
3460
/// Helper for [`Editor`]
3561
#[derive(Completer, Helper, Hinter, Validator)]
3662
struct ShellHelper;
@@ -133,7 +159,8 @@ pub fn start(entries: &SongEntries) {
133159

134160
/// Handles errors thrown by [`match_input()`] in [`start()`]
135161
///
136-
/// Prints error messages for [`NotFoundError`],
162+
/// Prints error messages for
163+
/// [`NotFoundError`], [`InvalidArgumentError`]
137164
/// [`ParseError`][`chrono::format::ParseError`],
138165
/// and [`ParseIntError`][`std::num::ParseIntError`]
139166
#[allow(clippy::borrowed_box)]
@@ -142,6 +169,7 @@ fn handle_error(err: &Box<dyn Error>) {
142169
// also thx ChatGPT
143170
match err.as_ref() {
144171
not_found if not_found.is::<NotFoundError>() => eprintln!("{not_found}"),
172+
invalid_arg if invalid_arg.is::<InvalidArgumentError>() => eprintln!("{invalid_arg}"),
145173
date if date.is::<chrono::format::ParseError>() => {
146174
eprintln!("Invalid date! Make sure you input the date in YYYY-MM-DD format.");
147175
}
@@ -175,6 +203,8 @@ fn match_input(
175203
"print top songs" | "ptsons" => match_print_top(entries, rl, &Aspect::Songs)?,
176204
"plot artist" | "gart" => match_plot_artist(entries, rl)?,
177205
"plot artist relative" | "gartr" => match_plot_artist_relative(entries, rl)?,
206+
"plot album relative" | "galbr" => match_plot_album_relative(entries, rl)?,
207+
"plot song relative" | "gsonr" => match_plot_song_relative(entries, rl)?,
178208
// when you press ENTER -> nothing happens, new prompt
179209
"" => (),
180210
_ => {
@@ -451,7 +481,71 @@ fn match_plot_artist_relative(
451481
let usr_input_art = rl.readline(PROMPT_MAIN)?;
452482
let art = entries.find().artist(&usr_input_art)?;
453483

454-
entries.plot_artist_relative(&art);
484+
entries.plot_relative(&art);
485+
Ok(())
486+
}
487+
488+
/// Used by [`match_input()`] for `plot album relative` command
489+
fn match_plot_album_relative(
490+
entries: &SongEntries,
491+
rl: &mut Editor<ShellHelper, FileHistory>,
492+
) -> Result<(), Box<dyn Error>> {
493+
// 1st prompt: artist name
494+
println!("Artist name?");
495+
let usr_input_art = rl.readline(PROMPT_MAIN)?;
496+
let art = entries.find().artist(&usr_input_art)?;
497+
498+
// 2nd prompt: album name
499+
println!("Album name?");
500+
let usr_input_alb = rl.readline(PROMPT_MAIN)?;
501+
let alb = entries.find().album(&usr_input_alb, &art.name)?;
502+
503+
// 3rd prompt: relative to what
504+
println!("Relative to all or artist?");
505+
let usr_input_rel = rl.readline(PROMPT_SECONDARY)?;
506+
507+
match usr_input_rel.as_str() {
508+
"all" => entries.plot_relative(&alb),
509+
"artist" => entries.plot_relative_to_artist(&alb),
510+
_ => return Err(Box::new(InvalidArgumentError::Artist)),
511+
}
512+
513+
Ok(())
514+
}
515+
516+
/// Used by [`match_input()`] for `plot song relative` command
517+
fn match_plot_song_relative(
518+
entries: &SongEntries,
519+
rl: &mut Editor<ShellHelper, FileHistory>,
520+
) -> Result<(), Box<dyn Error>> {
521+
// 1st prompt: artist name
522+
println!("Artist name?");
523+
let usr_input_art = rl.readline(PROMPT_MAIN)?;
524+
let art = entries.find().artist(&usr_input_art)?;
525+
526+
// 2nd prompt: album name
527+
println!("Album name?");
528+
let usr_input_alb = rl.readline(PROMPT_MAIN)?;
529+
let alb = entries.find().album(&usr_input_alb, &art.name)?;
530+
531+
// 3rd prompt: song name
532+
println!("Song name?");
533+
let usr_input_son = rl.readline(PROMPT_MAIN)?;
534+
let son = entries
535+
.find()
536+
.song_from_album(&usr_input_son, &alb.name, &alb.artist.name)?;
537+
538+
// 4th prompt: relative to what
539+
println!("Relative to all, artist or album?");
540+
let usr_input_rel = rl.readline(PROMPT_SECONDARY)?;
541+
542+
match usr_input_rel.as_str() {
543+
"all" => entries.plot_relative(&son),
544+
"artist" => entries.plot_relative_to_artist(&son),
545+
"album" => entries.plot_relative_to_album(&son),
546+
_ => return Err(Box::new(InvalidArgumentError::Album)),
547+
}
548+
455549
Ok(())
456550
}
457551

src/ui/help.rs

+14
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ fn plot_commands<'a>() -> Vec<[&'a str; 3]> {
208208
"creates a plot of the amount of plays of the given artist
209209
relative to all plays and opens it in the web browser",
210210
],
211+
[
212+
"plot album relative",
213+
"gartr",
214+
"creates a plot of the amount of plays of the given album
215+
relative to all plays or the artist
216+
and opens it in the web browser",
217+
],
218+
[
219+
"plot song relative",
220+
"gartr",
221+
"creates a plot of the amount of plays of the given album
222+
relative to all plays, the artist or the album
223+
and opens it in the web browser",
224+
],
211225
]
212226
}
213227

0 commit comments

Comments
 (0)