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