1use crate::collection::{Library, Track};
4use crate::pdb::fixed::{COLORS, KEYS};
5use crate::pdb::rows::{self, TrackRow};
6use crate::pdb::{Export, ExportPlaylist};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct DeviceTrack {
12 pub track: Track,
13 pub usb_path: String,
15 pub anlz_dir: String,
17 pub file_size: u64,
18 pub sample_depth: u16,
19 pub file_type: u16,
20 pub bitrate: u32,
21 pub sample_rate: u32,
22}
23
24pub fn file_type_for(kind: &str, file_name: &str) -> u16 {
25 let ext = file_name
26 .rsplit('.')
27 .next()
28 .unwrap_or("")
29 .to_ascii_lowercase();
30 match kind {
31 "MP3 File" => rows::FILE_TYPE_MP3,
32 "M4A File" => rows::FILE_TYPE_M4A,
33 "FLAC File" => rows::FILE_TYPE_FLAC,
34 "ALAC File" => rows::FILE_TYPE_ALAC,
35 "WAV File" => rows::FILE_TYPE_WAV,
36 "AIFF File" => rows::FILE_TYPE_AIFF,
37 _ => match ext.as_str() {
38 "mp3" => rows::FILE_TYPE_MP3,
39 "m4a" | "aac" | "mp4" => rows::FILE_TYPE_M4A,
40 "flac" => rows::FILE_TYPE_FLAC,
41 "wav" => rows::FILE_TYPE_WAV,
42 "aif" | "aiff" => rows::FILE_TYPE_AIFF,
43 _ => 0,
44 },
45 }
46}
47
48#[derive(Default)]
49struct Ids {
50 map: HashMap<String, u32>,
51 rows: Vec<(u32, String)>,
52}
53
54impl Ids {
55 fn get(&mut self, name: &str) -> u32 {
56 let name = name.trim();
57 if name.is_empty() {
58 return 0;
59 }
60 if let Some(&id) = self.map.get(name) {
61 return id;
62 }
63 let id = self.rows.len() as u32 + 1;
64 self.map.insert(name.to_string(), id);
65 self.rows.push((id, name.to_string()));
66 id
67 }
68}
69
70pub fn build(
72 library: &Library,
73 tracks: &[DeviceTrack],
74 selected: &[usize],
75 device_name: &str,
76 export_date: &str,
77) -> Export {
78 let mut artists = Ids::default();
79 let mut genres = Ids::default();
80 let mut labels = Ids::default();
81 let mut albums: HashMap<String, (u32, Option<u32>)> = HashMap::new(); let mut album_rows: Vec<(u32, String)> = Vec::new();
83 let mut track_ids: HashMap<u64, u32> = HashMap::new();
84 let mut rows_out = Vec::with_capacity(tracks.len());
85
86 for (i, dt) in tracks.iter().enumerate() {
87 let t = &dt.track;
88 let id = i as u32 + 1;
89 track_ids.insert(t.id, id);
90 let artist_id = artists.get(&t.artist);
91 let album_id = if t.album.trim().is_empty() {
92 0
93 } else {
94 let entry = albums.entry(t.album.trim().to_string()).or_insert_with(|| {
95 album_rows.push((album_rows.len() as u32 + 1, t.album.trim().to_string()));
96 (album_rows.len() as u32, Some(artist_id))
97 });
98 if entry.1 != Some(artist_id) {
99 entry.1 = None;
100 }
101 entry.0
102 };
103 let key_id = KEYS
104 .iter()
105 .position(|k| *k == t.tonality.trim())
106 .map(|p| p as u32 + 1)
107 .unwrap_or(0);
108 let color_id = t
109 .colour
110 .and_then(|rgb| COLORS.iter().find(|(_, _, c)| *c == rgb))
111 .map(|(id, _, _)| *id)
112 .unwrap_or(0);
113 let file_name = dt.usb_path.rsplit('/').next().unwrap_or("").to_string();
114 rows_out.push(TrackRow {
115 id,
116 sample_rate: dt.sample_rate,
117 composer_id: artists.get(&t.composer),
118 file_size: dt.file_size.min(u32::MAX as u64) as u32,
119 artwork_id: 0,
120 key_id,
121 original_artist_id: 0,
122 label_id: labels.get(&t.label),
123 remixer_id: artists.get(&t.remixer),
124 bitrate: dt.bitrate,
125 track_number: t.track_number,
126 tempo: (t.average_bpm * 100.0).round() as u32,
127 genre_id: genres.get(&t.genre),
128 album_id,
129 artist_id,
130 disc_number: t.disc_number as u16,
131 play_count: t.play_count as u16,
132 year: t.year as u16,
133 sample_depth: dt.sample_depth,
134 duration_seconds: t.total_time as u16,
135 color_id,
136 rating: t.stars(),
137 file_type: dt.file_type,
138 date_added: t.date_added.clone(),
139 release_date: String::new(),
140 mix_name: t.mix.clone(),
141 analyze_path: format!("{}/ANLZ0000.DAT", dt.anlz_dir),
142 analyze_date: export_date.to_string(),
143 comment: t.comments.clone(),
144 title: t.name.clone(),
145 filename: file_name,
146 file_path: dt.usb_path.clone(),
147 });
148 }
149
150 let albums_out: Vec<(u32, u32, String)> = album_rows
151 .into_iter()
152 .map(|(id, name)| {
153 let artist = albums.get(&name).and_then(|(_, a)| *a).unwrap_or(0);
154 (id, artist, name)
155 })
156 .collect();
157
158 let mut include = vec![false; library.playlists.len()];
160 for &i in selected {
161 let mut cur = Some(i);
162 while let Some(c) = cur {
163 include[c] = true;
164 cur = library.playlists[c].parent;
165 }
166 }
167 let mut node_id = vec![0u32; library.playlists.len()];
168 let mut next = 1u32;
169 let mut sibling_count: HashMap<Option<usize>, u32> = HashMap::new();
170 let mut playlists = Vec::new();
171 for (i, p) in library.playlists.iter().enumerate() {
172 if !include[i] {
173 continue;
174 }
175 node_id[i] = next;
176 next += 1;
177 let sort = sibling_count.entry(p.parent).or_insert(0);
178 let sort_order = *sort;
179 *sort += 1;
180 let ids = if p.is_folder {
181 Vec::new()
182 } else {
183 p.track_ids
184 .iter()
185 .filter_map(|tid| track_ids.get(tid).copied())
186 .collect()
187 };
188 playlists.push(ExportPlaylist {
189 id: node_id[i],
190 parent: p.parent.map(|pi| node_id[pi]).unwrap_or(0),
191 sort_order,
192 is_folder: p.is_folder,
193 name: p.name.clone(),
194 track_ids: ids,
195 });
196 }
197
198 Export {
199 tracks: rows_out,
200 artists: artists.rows,
201 albums: albums_out,
202 genres: genres.rows,
203 labels: labels.rows,
204 playlists,
205 device_name: device_name.to_string(),
206 export_date: export_date.to_string(),
207 }
208}
209
210pub fn today() -> String {
212 let secs = std::time::SystemTime::now()
213 .duration_since(std::time::UNIX_EPOCH)
214 .map(|d| d.as_secs())
215 .unwrap_or(0);
216 let days = (secs / 86400) as i64;
217 let z = days + 719468;
219 let era = z.div_euclid(146097);
220 let doe = z.rem_euclid(146097);
221 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
222 let y = yoe + era * 400;
223 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
224 let mp = (5 * doy + 2) / 153;
225 let d = doy - (153 * mp + 2) / 5 + 1;
226 let m = if mp < 10 { mp + 3 } else { mp - 9 };
227 let y = if m <= 2 { y + 1 } else { y };
228 format!("{y:04}-{m:02}-{d:02}")
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn today_is_a_date() {
237 let t = today();
238 assert_eq!(t.len(), 10);
239 assert!(t.starts_with("20"));
240 }
241}