lastfm_client/file_handler.rs
1use chrono::Local;
2use csv::Writer;
3use serde::Serialize;
4use std::collections::HashMap;
5use std::fs::{self, File, OpenOptions};
6use std::io::{Result, prelude::*};
7
8use crate::lastfm_handler::TrackPlayInfo;
9
10/// File format options for saving track data
11#[allow(dead_code)]
12pub enum FileFormat {
13 /// Save as JSON format with pretty printing
14 Json,
15 /// Save as CSV format with headers
16 Csv,
17}
18
19pub struct FileHandler;
20
21impl FileHandler {
22 /// Save data to a file in the data directory.
23 ///
24 /// Files are saved to the `data/` directory (created if it doesn't exist) with a timestamp in the filename.
25 ///
26 /// # Arguments
27 /// * `data` - Data to save (must implement Serialize)
28 /// * `format` - File format to save as (`FileFormat::Json` for JSON or `FileFormat::Csv` for CSV)
29 /// * `filename_prefix` - Prefix for the filename. The final filename will be `{prefix}_{timestamp}.{extension}`
30 ///
31 /// # Errors
32 /// * `std::io::Error` - If the file cannot be opened or written to, or if the data directory cannot be created
33 /// * `serde_json::Error` - If the JSON cannot be serialized
34 ///
35 /// # Returns
36 /// * `Result<String>` - Full path to the saved file (e.g., `data/recent_tracks_20240101_120000.json`)
37 pub fn save<T: Serialize>(
38 data: &[T],
39 format: &FileFormat,
40 filename_prefix: &str,
41 ) -> Result<String> {
42 // Create data directory if it doesn't exist
43 fs::create_dir_all("data")?;
44
45 // Generate timestamp
46 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
47
48 // Create filename with timestamp
49 let filename = format!(
50 "data/{}_{}.{}",
51 filename_prefix,
52 timestamp,
53 match format {
54 FileFormat::Json => "json",
55 FileFormat::Csv => "csv",
56 }
57 );
58
59 match format {
60 FileFormat::Json => {
61 // Special case: if T is a HashMap with track info
62 if std::any::type_name::<T>()
63 == std::any::type_name::<HashMap<String, TrackPlayInfo>>()
64 && let Some(single_item) = data.first()
65 {
66 Self::save_single(single_item, &filename)?;
67 return Ok(filename);
68 }
69 Self::save_as_json(data, &filename)
70 }
71 FileFormat::Csv => Self::save_as_csv(data, &filename),
72 }?;
73
74 Ok(filename)
75 }
76
77 /// Save data to a JSON file.
78 ///
79 /// # Arguments
80 /// * `data` - Data to save
81 /// * `filename` - Filename to save as
82 #[allow(dead_code)]
83 fn save_as_json<T: Serialize>(data: &[T], filename: &str) -> Result<()> {
84 let json = serde_json::to_string_pretty(data)?;
85 let mut file = File::create(filename)?;
86
87 file.write_all(json.as_bytes())?;
88
89 Ok(())
90 }
91
92 /// Save data to a CSV file.
93 ///
94 /// # Arguments
95 /// * `data` - Data to save
96 /// * `filename` - Filename to save as
97 fn save_as_csv<T: Serialize>(data: &[T], filename: &str) -> Result<()> {
98 let mut writer = Writer::from_path(filename)?;
99
100 for item in data {
101 writer.serialize(item)?;
102 }
103
104 writer.flush()?;
105 Ok(())
106 }
107
108 /// Append data to an existing file.
109 ///
110 /// # Arguments
111 /// * `data` - Data to append
112 /// * `file_path` - Path to the file to append to
113 ///
114 /// # Returns
115 /// * `Result<String>` - Path of the updated file
116 ///
117 /// Append data to an existing file.
118 ///
119 /// # Arguments
120 /// * `data` - Data to append
121 /// * `file_path` - Path to the file to append to
122 ///
123 /// # Errors
124 /// * `std::io::Error` - If an I/O error occurs
125 ///
126 /// # Returns
127 /// * `Result<String>` - Path of the updated file
128 #[allow(dead_code)]
129 pub fn append<T: Serialize + for<'de> serde::Deserialize<'de> + Clone>(
130 data: &[T],
131 file_path: &str,
132 ) -> Result<String> {
133 // Determine file format from extension
134 let format = if std::path::Path::new(file_path)
135 .extension()
136 .is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
137 {
138 FileFormat::Json
139 } else if std::path::Path::new(file_path)
140 .extension()
141 .is_some_and(|ext| ext.eq_ignore_ascii_case("csv"))
142 {
143 FileFormat::Csv
144 } else {
145 return Err(std::io::Error::new(
146 std::io::ErrorKind::InvalidInput,
147 "Unsupported file format",
148 ));
149 };
150
151 match format {
152 FileFormat::Json => {
153 // For JSON, we need to read the existing data, combine it, and write it back
154 let file = File::open(file_path)?;
155 let mut existing_data: Vec<T> = serde_json::from_reader(file)?;
156
157 existing_data.extend(data.iter().cloned());
158
159 Self::save_as_json(&existing_data, file_path)?;
160 }
161 FileFormat::Csv => {
162 // For CSV, we can simply append to the file
163 let mut writer =
164 Writer::from_writer(OpenOptions::new().append(true).open(file_path)?);
165
166 for item in data {
167 writer.serialize(item)?;
168 }
169 writer.flush()?;
170 }
171 }
172
173 Ok(file_path.to_string())
174 }
175
176 /// Save a single item to a JSON file
177 ///
178 /// # Errors
179 /// * `std::io::Error` - If there was an error reading or writing the file
180 /// * `serde_json::Error` - If there was an error serializing the data
181 ///
182 /// # Arguments
183 /// * `data` - Data to save
184 /// * `filename` - Filename to save as
185 pub fn save_single<T: Serialize>(data: &T, filename: &str) -> Result<()> {
186 let json = serde_json::to_string_pretty(data)?;
187 let mut file = File::create(filename)?;
188 file.write_all(json.as_bytes())?;
189 Ok(())
190 }
191}