1use crate::collection::{Library, Track};
4use crate::pdb::fixed::COLORS;
5use crate::pdb::rows::{self, TrackRow};
6use crate::pdb::{Export, ExportPlaylist};
7use baken_core::rbsort::parse_key;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct DeviceTrack {
13 pub track: Track,
14 pub usb_path: String,
16 pub anlz_dir: String,
18 pub anlz_index: u16,
21 pub file_size: u64,
22 pub sample_depth: u16,
23 pub file_type: u16,
24 pub bitrate: u32,
25 pub sample_rate: u32,
26}
27
28impl DeviceTrack {
29 pub fn anlz_path(&self, ext: &str) -> String {
31 format!("{}/ANLZ{:04}.{ext}", self.anlz_dir, self.anlz_index)
32 }
33}
34
35pub fn file_type_for(kind: &str, file_name: &str) -> u16 {
40 let ext = file_name
41 .rsplit('.')
42 .next()
43 .unwrap_or("")
44 .to_ascii_lowercase();
45 match ext.as_str() {
46 "mp3" => rows::FILE_TYPE_MP3,
47 "m4a" | "aac" | "mp4" if kind == "ALAC File" => rows::FILE_TYPE_ALAC,
48 "m4a" | "aac" | "mp4" => rows::FILE_TYPE_M4A,
49 "flac" => rows::FILE_TYPE_FLAC,
50 "wav" => rows::FILE_TYPE_WAV,
51 "aif" | "aiff" => rows::FILE_TYPE_AIFF,
52 _ => match kind {
53 "MP3 File" => rows::FILE_TYPE_MP3,
54 "M4A File" => rows::FILE_TYPE_M4A,
55 "FLAC File" => rows::FILE_TYPE_FLAC,
56 "ALAC File" => rows::FILE_TYPE_ALAC,
57 "WAV File" => rows::FILE_TYPE_WAV,
58 "AIFF File" => rows::FILE_TYPE_AIFF,
59 _ => 0,
60 },
61 }
62}
63
64#[derive(Default)]
65struct Ids {
66 map: HashMap<String, u32>,
67 rows: Vec<(u32, String)>,
68}
69
70impl Ids {
71 fn get(&mut self, name: &str) -> u32 {
72 let name = name.trim();
73 if name.is_empty() {
74 return 0;
75 }
76 if let Some(&id) = self.map.get(name) {
77 return id;
78 }
79 let id = self.rows.len() as u32 + 1;
80 self.map.insert(name.to_string(), id);
81 self.rows.push((id, name.to_string()));
82 id
83 }
84}
85
86fn key_rows(tracks: &[DeviceTrack]) -> Vec<(u32, String)> {
91 let mut names: Vec<(u8, String)> = Vec::new();
92 for dt in tracks {
93 let name = dt.track.tonality.trim();
94 if let Some(i) = parse_key(name) {
95 if !names.iter().any(|(_, n)| n == name) {
96 names.push((i, name.to_string()));
97 }
98 }
99 }
100 names.sort_by_key(|(i, _)| *i);
101 names
102 .into_iter()
103 .enumerate()
104 .map(|(id, (_, name))| (id as u32 + 1, name))
105 .collect()
106}
107
108pub fn build(
111 library: &Library,
112 tracks: &[DeviceTrack],
113 selected: &[usize],
114 device_name: &str,
115 export_date: &str,
116) -> Export {
117 let mut artists = Ids::default();
118 let mut genres = Ids::default();
119 let mut labels = Ids::default();
120 let mut albums: HashMap<String, (u32, Option<u32>)> = HashMap::new(); let mut album_rows: Vec<(u32, String)> = Vec::new();
122 let mut track_ids: HashMap<u64, u32> = HashMap::new();
123 let mut rows_out = Vec::with_capacity(tracks.len());
124 let keys = key_rows(tracks);
125
126 for (i, dt) in tracks.iter().enumerate() {
127 let t = &dt.track;
128 let id = i as u32 + 1;
129 track_ids.insert(t.id, id);
130 let artist_id = artists.get(&t.artist);
131 let album_id = if t.album.trim().is_empty() {
132 0
133 } else {
134 let entry = albums.entry(t.album.trim().to_string()).or_insert_with(|| {
135 album_rows.push((album_rows.len() as u32 + 1, t.album.trim().to_string()));
136 (album_rows.len() as u32, Some(artist_id))
137 });
138 if entry.1 != Some(artist_id) {
139 entry.1 = None;
140 }
141 entry.0
142 };
143 let key_id = keys
144 .iter()
145 .find(|(_, n)| n == t.tonality.trim())
146 .map_or(0, |(id, _)| *id);
147 let color_id = t
148 .colour
149 .and_then(|rgb| COLORS.iter().find(|(_, _, c)| *c == rgb))
150 .map(|(id, _, _)| *id)
151 .unwrap_or(0);
152 let file_name = dt.usb_path.rsplit('/').next().unwrap_or("").to_string();
153 rows_out.push(TrackRow {
154 id,
155 sample_rate: dt.sample_rate,
156 composer_id: artists.get(&t.composer),
157 file_size: dt.file_size.min(u32::MAX as u64) as u32,
158 artwork_id: 0,
159 key_id,
160 original_artist_id: 0,
161 label_id: labels.get(&t.label),
162 remixer_id: artists.get(&t.remixer),
163 bitrate: dt.bitrate,
164 track_number: t.track_number,
165 tempo: (t.average_bpm * 100.0).round() as u32,
166 genre_id: genres.get(&t.genre),
167 album_id,
168 artist_id,
169 disc_number: t.disc_number as u16,
170 play_count: t.play_count as u16,
171 year: t.year as u16,
172 sample_depth: dt.sample_depth,
173 duration_seconds: t.total_time as u16,
174 color_id,
175 rating: t.stars(),
176 file_type: dt.file_type,
177 date_added: t.date_added.clone(),
178 release_date: String::new(),
179 mix_name: t.mix.clone(),
180 analyze_path: dt.anlz_path("DAT"),
181 analyze_date: export_date.to_string(),
182 comment: t.comments.clone(),
183 title: t.name.clone(),
184 filename: file_name,
185 file_path: dt.usb_path.clone(),
186 });
187 }
188
189 let albums_out: Vec<(u32, u32, String)> = album_rows
190 .into_iter()
191 .map(|(id, name)| {
192 let artist = albums.get(&name).and_then(|(_, a)| *a).unwrap_or(0);
193 (id, artist, name)
194 })
195 .collect();
196
197 let mut include = vec![false; library.playlists.len()];
199 for &i in selected {
200 let mut cur = Some(i);
201 while let Some(c) = cur {
202 include[c] = true;
203 cur = library.playlists[c].parent;
204 }
205 }
206 let mut node_id = vec![0u32; library.playlists.len()];
207 let mut next = 1u32;
208 let mut sibling_count: HashMap<Option<usize>, u32> = HashMap::new();
209 let mut playlists = Vec::new();
210 for (i, p) in library.playlists.iter().enumerate() {
211 if !include[i] {
212 continue;
213 }
214 node_id[i] = next;
215 next += 1;
216 let sort = sibling_count.entry(p.parent).or_insert(0);
217 let sort_order = *sort;
218 *sort += 1;
219 let ids = if p.is_folder {
220 Vec::new()
221 } else {
222 p.track_ids
223 .iter()
224 .filter_map(|tid| track_ids.get(tid).copied())
225 .collect()
226 };
227 playlists.push(ExportPlaylist {
228 id: node_id[i],
229 parent: p.parent.map(|pi| node_id[pi]).unwrap_or(0),
230 sort_order,
231 is_folder: p.is_folder,
232 name: p.name.clone(),
233 track_ids: ids,
234 });
235 }
236
237 Export {
238 tracks: rows_out,
239 artists: artists.rows,
240 albums: albums_out,
241 genres: genres.rows,
242 labels: labels.rows,
243 keys,
244 playlists,
245 device_name: device_name.to_string(),
246 export_date: export_date.to_string(),
247 }
248}
249
250pub fn today() -> String {
252 let secs = std::time::SystemTime::now()
253 .duration_since(std::time::UNIX_EPOCH)
254 .map(|d| d.as_secs())
255 .unwrap_or(0);
256 let days = (secs / 86400) as i64;
257 let z = days + 719468;
259 let era = z.div_euclid(146097);
260 let doe = z.rem_euclid(146097);
261 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
262 let y = yoe + era * 400;
263 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
264 let mp = (5 * doy + 2) / 153;
265 let d = doy - (153 * mp + 2) / 5 + 1;
266 let m = if mp < 10 { mp + 3 } else { mp - 9 };
267 let y = if m <= 2 { y + 1 } else { y };
268 format!("{y:04}-{m:02}-{d:02}")
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn extension_wins_over_a_wrong_kind() {
277 assert_eq!(
278 file_type_for("MP3 File", "Transfer.flac"),
279 rows::FILE_TYPE_FLAC
280 );
281 assert_eq!(file_type_for("ALAC File", "a.m4a"), rows::FILE_TYPE_ALAC);
282 assert_eq!(file_type_for("M4A File", "a.m4a"), rows::FILE_TYPE_M4A);
283 assert_eq!(
284 file_type_for("WAV File", "no-extension"),
285 rows::FILE_TYPE_WAV
286 );
287 }
288
289 fn keyed(tonality: &[&str]) -> Vec<DeviceTrack> {
290 tonality
291 .iter()
292 .map(|k| DeviceTrack {
293 track: Track {
294 tonality: k.to_string(),
295 ..Default::default()
296 },
297 usb_path: String::new(),
298 anlz_dir: String::new(),
299 anlz_index: 0,
300 file_size: 0,
301 sample_depth: 0,
302 file_type: 0,
303 bitrate: 0,
304 sample_rate: 0,
305 })
306 .collect()
307 }
308
309 fn names(rows: &[(u32, String)]) -> Vec<(u32, &str)> {
310 rows.iter().map(|(id, n)| (*id, n.as_str())).collect()
311 }
312
313 #[test]
314 fn keys_keep_the_xml_spelling_in_wheel_order() {
315 let rows = key_rows(&keyed(&["Cm", "Ab", "Cm", "", "A minor", " Abm "]));
317 assert_eq!(names(&rows), [(1, "Abm"), (2, "Ab"), (3, "Cm")]);
318 let rows = key_rows(&keyed(&["8A", "Am", "1A"]));
320 assert_eq!(names(&rows), [(1, "1A"), (2, "8A"), (3, "Am")]);
321 assert!(key_rows(&keyed(&["", "x"])).is_empty());
322 }
323
324 #[test]
325 fn today_is_a_date() {
326 let t = today();
327 assert_eq!(t.len(), 10);
328 assert!(t.starts_with("20"));
329 }
330}