1use std::fs::File;
2use std::io::BufReader;
3use std::{collections::HashMap, path::Path};
4
5use serde::de::DeserializeOwned;
6
7use crate::types::{LovedTrack, RecentTrack};
8
9pub trait TrackAnalyzable {
11 fn get_artist_name(&self) -> String;
13
14 fn get_track_name(&self) -> String;
16
17 fn get_track_identifier(&self) -> String {
19 format!("{} - {}", self.get_artist_name(), self.get_track_name())
20 }
21}
22
23impl TrackAnalyzable for RecentTrack {
24 fn get_artist_name(&self) -> String {
25 self.artist.text.clone()
26 }
27
28 fn get_track_name(&self) -> String {
29 self.name.clone()
30 }
31}
32
33impl TrackAnalyzable for LovedTrack {
34 fn get_artist_name(&self) -> String {
35 self.artist.name.clone()
36 }
37
38 fn get_track_name(&self) -> String {
39 self.name.clone()
40 }
41}
42
43#[derive(Debug)]
45#[non_exhaustive]
46pub struct TrackStats {
47 pub total_tracks: usize,
49 pub artist_play_counts: HashMap<String, usize>,
51 pub track_play_counts: HashMap<String, usize>,
53 pub tracks_below_threshold: HashMap<String, usize>,
55 pub tracks_above_threshold: HashMap<String, usize>,
57 pub most_played_artist: Option<(String, usize)>,
59 pub most_played_track: Option<(String, usize)>,
61}
62
63#[derive(Debug)]
65#[non_exhaustive]
66pub struct AnalysisHandler;
67
68impl AnalysisHandler {
69 pub fn analyze_file<T: DeserializeOwned + TrackAnalyzable>(
83 file_path: &Path,
84 threshold: usize,
85 ) -> Result<TrackStats, Box<dyn std::error::Error>> {
86 let file = File::open(file_path)?;
87 let reader = BufReader::new(file);
88
89 let tracks: Vec<T> = serde_json::from_reader(reader)?;
90
91 Ok(Self::analyze_tracks(&tracks, threshold))
92 }
93
94 pub fn analyze_tracks<T: TrackAnalyzable>(tracks: &[T], threshold: usize) -> TrackStats {
104 let mut artist_play_counts: HashMap<String, usize> = HashMap::new();
105 let mut track_play_counts: HashMap<String, usize> = HashMap::new();
106
107 for track in tracks {
109 let artist_name = track.get_artist_name();
110 let track_identifier = track.get_track_identifier();
111
112 *artist_play_counts.entry(artist_name).or_insert(0) += 1;
113 *track_play_counts.entry(track_identifier).or_insert(0) += 1;
114 }
115
116 let most_played_artist = artist_play_counts
118 .iter()
119 .max_by_key(|(_, count)| *count)
120 .map(|(name, count)| (name.clone(), *count));
121
122 let most_played_track = track_play_counts
123 .iter()
124 .max_by_key(|(_, count)| *count)
125 .map(|(name, count)| (name.clone(), *count));
126
127 let tracks_below_threshold: HashMap<String, usize> = track_play_counts
129 .iter()
130 .filter(|(_, count)| **count < threshold)
131 .map(|(name, count)| (name.clone(), *count))
132 .collect();
133
134 let tracks_above_threshold: HashMap<String, usize> = track_play_counts
136 .iter()
137 .filter(|(_, count)| **count >= threshold)
138 .map(|(name, count)| (name.clone(), *count))
139 .collect();
140
141 TrackStats {
142 total_tracks: tracks.len(),
143 artist_play_counts,
144 track_play_counts,
145 tracks_below_threshold,
146 tracks_above_threshold,
147 most_played_artist,
148 most_played_track,
149 }
150 }
151
152 pub fn print_analysis(stats: &TrackStats) {
157 println!("=== Track Analysis ===");
158 println!("Total tracks: {}", stats.total_tracks);
159
160 if let Some((artist, count)) = &stats.most_played_artist {
161 println!("\nMost played artist: {artist} ({count} plays)");
162 }
163
164 if let Some((track, count)) = &stats.most_played_track {
165 println!("Most played track: {track} ({count} plays)");
166 }
167
168 println!("\nTop 10 Artists:");
169 let mut artists: Vec<_> = stats.artist_play_counts.iter().collect();
170 artists.sort_by(|a, b| b.1.cmp(a.1));
171 for (artist, count) in artists.iter().take(10) {
172 println!(" {artist} - {count} plays");
173 }
174
175 println!("\nTop 10 Tracks:");
176 let mut tracks: Vec<_> = stats.track_play_counts.iter().collect();
177 tracks.sort_by(|a, b| b.1.cmp(a.1));
178 for (track, count) in tracks.iter().take(10) {
179 println!(" {track} - {count} plays");
180 }
181
182 println!(
183 "\nTracks below threshold: {}",
184 stats.tracks_below_threshold.len()
185 );
186
187 println!(
188 "\nTracks above threshold: {}",
189 stats.tracks_above_threshold.len()
190 );
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::types::{BaseMbidText, BaseObject, Date, Streamable};
198
199 fn create_recent_track(artist: &str, name: &str) -> RecentTrack {
200 RecentTrack {
201 artist: BaseMbidText {
202 mbid: String::new(),
203 text: artist.to_string(),
204 },
205 streamable: false,
206 image: Vec::new(),
207 album: BaseMbidText {
208 mbid: String::new(),
209 text: String::new(),
210 },
211 attr: None,
212 date: None,
213 name: name.to_string(),
214 mbid: String::new(),
215 url: String::new(),
216 }
217 }
218
219 fn create_loved_track(artist: &str, name: &str) -> LovedTrack {
220 LovedTrack {
221 artist: BaseObject {
222 mbid: String::new(),
223 url: String::new(),
224 name: artist.to_string(),
225 },
226 date: Date {
227 uts: 0,
228 text: String::new(),
229 },
230 image: Vec::new(),
231 streamable: Streamable {
232 fulltrack: String::new(),
233 text: String::new(),
234 },
235 name: name.to_string(),
236 mbid: String::new(),
237 url: String::new(),
238 }
239 }
240
241 #[test]
242 fn test_analyze_recent_tracks() {
243 let tracks = vec![
244 create_recent_track("Artist1", "Song1"),
245 create_recent_track("Artist1", "Song1"),
246 create_recent_track("Artist1", "Song2"),
247 create_recent_track("Artist2", "Song3"),
248 ];
249
250 let stats = AnalysisHandler::analyze_tracks(&tracks, 2);
251
252 assert_eq!(stats.total_tracks, 4);
253 assert_eq!(stats.artist_play_counts["Artist1"], 3);
254 assert_eq!(stats.artist_play_counts["Artist2"], 1);
255 assert_eq!(stats.track_play_counts["Artist1 - Song1"], 2);
256 assert_eq!(stats.most_played_artist, Some(("Artist1".to_string(), 3)));
257 }
258
259 #[test]
260 fn test_analyze_loved_tracks() {
261 let tracks = vec![
262 create_loved_track("Artist1", "Song1"),
263 create_loved_track("Artist1", "Song1"),
264 create_loved_track("Artist1", "Song2"),
265 create_loved_track("Artist2", "Song3"),
266 ];
267
268 let stats = AnalysisHandler::analyze_tracks(&tracks, 2);
269
270 assert_eq!(stats.total_tracks, 4);
271 assert_eq!(stats.artist_play_counts["Artist1"], 3);
272 assert_eq!(stats.artist_play_counts["Artist2"], 1);
273 assert_eq!(stats.track_play_counts["Artist1 - Song1"], 2);
274 assert_eq!(stats.most_played_artist, Some(("Artist1".to_string(), 3)));
275 }
276}