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