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}