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)]
201 pub fn get_most_recent_timestamp<T: DeserializeOwned + Timestamped>(
202 file_path: &Path,
203 ) -> crate::error::Result<Option<i64>> {
204 let file = File::open(file_path)?;
205 let reader = BufReader::new(file);
206 let tracks: Vec<T> = serde_json::from_reader(reader)?;
207
208 Ok(tracks
209 .iter()
210 .filter_map(Timestamped::get_timestamp)
211 .map(i64::from)
212 .max())
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::types::{BaseMbidText, BaseObject, Date, Streamable};
220
221 fn create_recent_track(artist: &str, name: &str) -> RecentTrack {
222 RecentTrack {
223 artist: BaseMbidText {
224 mbid: String::new(),
225 text: artist.to_string(),
226 },
227 streamable: false,
228 image: Vec::new(),
229 album: BaseMbidText {
230 mbid: String::new(),
231 text: String::new(),
232 },
233 attr: None,
234 date: None,
235 name: name.to_string(),
236 mbid: String::new(),
237 url: String::new(),
238 }
239 }
240
241 fn create_loved_track(artist: &str, name: &str) -> LovedTrack {
242 LovedTrack {
243 artist: BaseObject {
244 mbid: String::new(),
245 url: String::new(),
246 name: artist.to_string(),
247 },
248 date: Date {
249 uts: 0,
250 text: String::new(),
251 },
252 image: Vec::new(),
253 streamable: Streamable {
254 fulltrack: String::new(),
255 text: String::new(),
256 },
257 name: name.to_string(),
258 mbid: String::new(),
259 url: String::new(),
260 }
261 }
262
263 #[test]
264 fn test_analyze_recent_tracks() {
265 let tracks = vec![
266 create_recent_track("Artist1", "Song1"),
267 create_recent_track("Artist1", "Song1"),
268 create_recent_track("Artist1", "Song2"),
269 create_recent_track("Artist2", "Song3"),
270 ];
271
272 let stats = AnalysisHandler::analyze_tracks(&tracks, 2);
273
274 assert_eq!(stats.total_tracks, 4);
275 assert_eq!(stats.artist_play_counts["Artist1"], 3);
276 assert_eq!(stats.artist_play_counts["Artist2"], 1);
277 assert_eq!(stats.track_play_counts["Artist1 - Song1"], 2);
278 assert_eq!(stats.most_played_artist, Some(("Artist1".to_string(), 3)));
279 }
280
281 #[test]
282 fn test_analyze_loved_tracks() {
283 let tracks = vec![
284 create_loved_track("Artist1", "Song1"),
285 create_loved_track("Artist1", "Song1"),
286 create_loved_track("Artist1", "Song2"),
287 create_loved_track("Artist2", "Song3"),
288 ];
289
290 let stats = AnalysisHandler::analyze_tracks(&tracks, 2);
291
292 assert_eq!(stats.total_tracks, 4);
293 assert_eq!(stats.artist_play_counts["Artist1"], 3);
294 assert_eq!(stats.artist_play_counts["Artist2"], 1);
295 assert_eq!(stats.track_play_counts["Artist1 - Song1"], 2);
296 assert_eq!(stats.most_played_artist, Some(("Artist1".to_string(), 3)));
297 }
298}