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, Timestamped};
8
9#[allow(dead_code)]
11pub trait TrackAnalyzable {
12 fn get_artist_name(&self) -> String;
14
15 fn get_track_name(&self) -> String;
17
18 fn get_track_identifier(&self) -> String {
20 format!("{} - {}", self.get_artist_name(), self.get_track_name())
21 }
22}
23
24impl TrackAnalyzable for RecentTrack {
25 fn get_artist_name(&self) -> String {
26 self.artist.text.clone()
27 }
28
29 fn get_track_name(&self) -> String {
30 self.name.clone()
31 }
32}
33
34impl TrackAnalyzable for LovedTrack {
35 fn get_artist_name(&self) -> String {
36 self.artist.name.clone()
37 }
38
39 fn get_track_name(&self) -> String {
40 self.name.clone()
41 }
42}
43
44#[derive(Debug)]
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
63pub struct AnalysisHandler;
64
65impl AnalysisHandler {
66 pub fn analyze_file<T: DeserializeOwned + TrackAnalyzable>(
79 file_path: &Path,
80 threshold: usize,
81 ) -> Result<TrackStats, Box<dyn std::error::Error>> {
82 let file = File::open(file_path)?;
83 let reader = BufReader::new(file);
84
85 let tracks: Vec<T> = serde_json::from_reader(reader)?;
86
87 Ok(Self::analyze_tracks(&tracks, threshold))
88 }
89
90 pub fn analyze_tracks<T: TrackAnalyzable>(tracks: &[T], threshold: usize) -> TrackStats {
99 let mut artist_play_counts: HashMap<String, usize> = HashMap::new();
100 let mut track_play_counts: HashMap<String, usize> = HashMap::new();
101
102 for track in tracks {
104 let artist_name = track.get_artist_name();
105 let track_identifier = track.get_track_identifier();
106
107 *artist_play_counts.entry(artist_name).or_insert(0) += 1;
108 *track_play_counts.entry(track_identifier).or_insert(0) += 1;
109 }
110
111 let most_played_artist = artist_play_counts
113 .iter()
114 .max_by_key(|(_, count)| *count)
115 .map(|(name, count)| (name.clone(), *count));
116
117 let most_played_track = track_play_counts
118 .iter()
119 .max_by_key(|(_, count)| *count)
120 .map(|(name, count)| (name.clone(), *count));
121
122 let tracks_below_threshold: HashMap<String, usize> = track_play_counts
124 .iter()
125 .filter(|(_, count)| **count < threshold)
126 .map(|(name, count)| (name.clone(), *count))
127 .collect();
128
129 let tracks_above_threshold: HashMap<String, usize> = track_play_counts
131 .iter()
132 .filter(|(_, count)| **count >= threshold)
133 .map(|(name, count)| (name.clone(), *count))
134 .collect();
135
136 TrackStats {
137 total_tracks: tracks.len(),
138 artist_play_counts,
139 track_play_counts,
140 tracks_below_threshold,
141 tracks_above_threshold,
142 most_played_artist,
143 most_played_track,
144 }
145 }
146
147 pub fn print_analysis(stats: &TrackStats) {
152 println!("=== Track Analysis ===");
153 println!("Total tracks: {}", stats.total_tracks);
154
155 if let Some((artist, count)) = &stats.most_played_artist {
156 println!("\nMost played artist: {artist} ({count} plays)");
157 }
158
159 if let Some((track, count)) = &stats.most_played_track {
160 println!("Most played track: {track} ({count} plays)");
161 }
162
163 println!("\nTop 10 Artists:");
164 let mut artists: Vec<_> = stats.artist_play_counts.iter().collect();
165 artists.sort_by(|a, b| b.1.cmp(a.1));
166 for (artist, count) in artists.iter().take(10) {
167 println!(" {artist} - {count} plays");
168 }
169
170 println!("\nTop 10 Tracks:");
171 let mut tracks: Vec<_> = stats.track_play_counts.iter().collect();
172 tracks.sort_by(|a, b| b.1.cmp(a.1));
173 for (track, count) in tracks.iter().take(10) {
174 println!(" {track} - {count} plays");
175 }
176
177 println!(
178 "\nTracks below threshold: {}",
179 stats.tracks_below_threshold.len()
180 );
181
182 println!(
183 "\nTracks above threshold: {}",
184 stats.tracks_above_threshold.len()
185 );
186 }
187
188 #[allow(dead_code)]
200 pub fn get_most_recent_timestamp<T: DeserializeOwned + Timestamped>(
201 file_path: &Path,
202 ) -> Result<Option<i64>, Box<dyn std::error::Error>> {
203 let file = File::open(file_path)?;
204 let reader = BufReader::new(file);
205 let tracks: Vec<T> = serde_json::from_reader(reader)?;
206
207 Ok(tracks
208 .iter()
209 .filter_map(Timestamped::get_timestamp)
210 .map(i64::from)
211 .max())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::types::{BaseMbidText, BaseObject, Date, Streamable};
219
220 fn create_recent_track(artist: &str, name: &str) -> RecentTrack {
221 RecentTrack {
222 artist: BaseMbidText {
223 mbid: String::new(),
224 text: artist.to_string(),
225 },
226 streamable: false,
227 image: Vec::new(),
228 album: BaseMbidText {
229 mbid: String::new(),
230 text: String::new(),
231 },
232 attr: None,
233 date: None,
234 name: name.to_string(),
235 mbid: String::new(),
236 url: String::new(),
237 }
238 }
239
240 fn create_loved_track(artist: &str, name: &str) -> LovedTrack {
241 LovedTrack {
242 artist: BaseObject {
243 mbid: String::new(),
244 url: String::new(),
245 name: artist.to_string(),
246 },
247 date: Date {
248 uts: 0,
249 text: String::new(),
250 },
251 image: Vec::new(),
252 streamable: Streamable {
253 fulltrack: String::new(),
254 text: String::new(),
255 },
256 name: name.to_string(),
257 mbid: String::new(),
258 url: String::new(),
259 }
260 }
261
262 #[test]
263 fn test_analyze_recent_tracks() {
264 let tracks = vec![
265 create_recent_track("Artist1", "Song1"),
266 create_recent_track("Artist1", "Song1"),
267 create_recent_track("Artist1", "Song2"),
268 create_recent_track("Artist2", "Song3"),
269 ];
270
271 let stats = AnalysisHandler::analyze_tracks(&tracks, 2);
272
273 assert_eq!(stats.total_tracks, 4);
274 assert_eq!(stats.artist_play_counts["Artist1"], 3);
275 assert_eq!(stats.artist_play_counts["Artist2"], 1);
276 assert_eq!(stats.track_play_counts["Artist1 - Song1"], 2);
277 assert_eq!(stats.most_played_artist, Some(("Artist1".to_string(), 3)));
278 }
279
280 #[test]
281 fn test_analyze_loved_tracks() {
282 let tracks = vec![
283 create_loved_track("Artist1", "Song1"),
284 create_loved_track("Artist1", "Song1"),
285 create_loved_track("Artist1", "Song2"),
286 create_loved_track("Artist2", "Song3"),
287 ];
288
289 let stats = AnalysisHandler::analyze_tracks(&tracks, 2);
290
291 assert_eq!(stats.total_tracks, 4);
292 assert_eq!(stats.artist_play_counts["Artist1"], 3);
293 assert_eq!(stats.artist_play_counts["Artist2"], 1);
294 assert_eq!(stats.track_play_counts["Artist1 - Song1"], 2);
295 assert_eq!(stats.most_played_artist, Some(("Artist1".to_string(), 3)));
296 }
297}