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