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(
110 library: &Library,
111 tracks: &[DeviceTrack],
112 selected: &[usize],
113 device_name: &str,
114 export_date: &str,
115) -> Export {
116 let mut artists = Ids::default();
117 let mut genres = Ids::default();
118 let mut labels = Ids::default();
119 let mut albums: HashMap<String, (u32, Option<u32>)> = HashMap::new(); let mut album_rows: Vec<(u32, String)> = Vec::new();
121 let mut track_ids: HashMap<u64, u32> = HashMap::new();
122 let mut rows_out = Vec::with_capacity(tracks.len());
123 let keys = key_rows(tracks);
124
125 for (i, dt) in tracks.iter().enumerate() {
126 let t = &dt.track;
127 let id = i as u32 + 1;
128 track_ids.insert(t.id, id);
129 let artist_id = artists.get(&t.artist);
130 let album_id = if t.album.trim().is_empty() {
131 0
132 } else {
133 let entry = albums.entry(t.album.trim().to_string()).or_insert_with(|| {
134 album_rows.push((album_rows.len() as u32 + 1, t.album.trim().to_string()));
135 (album_rows.len() as u32, Some(artist_id))
136 });
137 if entry.1 != Some(artist_id) {
138 entry.1 = None;
139 }
140 entry.0
141 };
142 let key_id = keys
143 .iter()
144 .find(|(_, n)| n == t.tonality.trim())
145 .map_or(0, |(id, _)| *id);
146 let color_id = t
147 .colour
148 .and_then(|rgb| COLORS.iter().find(|(_, _, c)| *c == rgb))
149 .map(|(id, _, _)| *id)
150 .unwrap_or(0);
151 let file_name = dt.usb_path.rsplit('/').next().unwrap_or("").to_string();
152 rows_out.push(TrackRow {
153 id,
154 sample_rate: dt.sample_rate,
155 composer_id: artists.get(&t.composer),
156 file_size: dt.file_size.min(u32::MAX as u64) as u32,
157 artwork_id: 0,
158 key_id,
159 original_artist_id: 0,
160 label_id: labels.get(&t.label),
161 remixer_id: artists.get(&t.remixer),
162 bitrate: dt.bitrate,
163 track_number: t.track_number,
164 tempo: (t.average_bpm * 100.0).round() as u32,
165 genre_id: genres.get(&t.genre),
166 album_id,
167 artist_id,
168 disc_number: t.disc_number as u16,
169 play_count: t.play_count as u16,
170 year: t.year as u16,
171 sample_depth: dt.sample_depth,
172 duration_seconds: t.total_time as u16,
173 color_id,
174 rating: t.stars(),
175 file_type: dt.file_type,
176 date_added: t.date_added.clone(),
177 release_date: String::new(),
178 mix_name: t.mix.clone(),
179 analyze_path: dt.anlz_path("DAT"),
180 analyze_date: export_date.to_string(),
181 comment: t.comments.clone(),
182 title: t.name.clone(),
183 filename: file_name,
184 file_path: dt.usb_path.clone(),
185 });
186 }
187
188 let albums_out: Vec<(u32, u32, String)> = album_rows
189 .into_iter()
190 .map(|(id, name)| {
191 let artist = albums.get(&name).and_then(|(_, a)| *a).unwrap_or(0);
192 (id, artist, name)
193 })
194 .collect();
195
196 let mut include = vec![false; library.playlists.len()];
198 for &i in selected {
199 let mut cur = Some(i);
200 while let Some(c) = cur {
201 include[c] = true;
202 cur = library.playlists[c].parent;
203 }
204 }
205 let mut node_id = vec![0u32; library.playlists.len()];
206 let mut next = 1u32;
207 let mut sibling_count: HashMap<Option<usize>, u32> = HashMap::new();
208 let mut playlists = Vec::new();
209 for (i, p) in library.playlists.iter().enumerate() {
210 if !include[i] {
211 continue;
212 }
213 node_id[i] = next;
214 next += 1;
215 let sort = sibling_count.entry(p.parent).or_insert(0);
216 let sort_order = *sort;
217 *sort += 1;
218 let ids = if p.is_folder {
219 Vec::new()
220 } else {
221 p.track_ids
222 .iter()
223 .filter_map(|tid| track_ids.get(tid).copied())
224 .collect()
225 };
226 playlists.push(ExportPlaylist {
227 id: node_id[i],
228 parent: p.parent.map(|pi| node_id[pi]).unwrap_or(0),
229 sort_order,
230 is_folder: p.is_folder,
231 name: p.name.clone(),
232 track_ids: ids,
233 });
234 }
235
236 Export {
237 tracks: rows_out,
238 artists: artists.rows,
239 albums: albums_out,
240 genres: genres.rows,
241 labels: labels.rows,
242 keys,
243 playlists,
244 device_name: device_name.to_string(),
245 export_date: export_date.to_string(),
246 }
247}
248
249pub fn today() -> String {
251 let secs = std::time::SystemTime::now()
252 .duration_since(std::time::UNIX_EPOCH)
253 .map(|d| d.as_secs())
254 .unwrap_or(0);
255 let days = (secs / 86400) as i64;
256 let z = days + 719468;
258 let era = z.div_euclid(146097);
259 let doe = z.rem_euclid(146097);
260 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
261 let y = yoe + era * 400;
262 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
263 let mp = (5 * doy + 2) / 153;
264 let d = doy - (153 * mp + 2) / 5 + 1;
265 let m = if mp < 10 { mp + 3 } else { mp - 9 };
266 let y = if m <= 2 { y + 1 } else { y };
267 format!("{y:04}-{m:02}-{d:02}")
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn extension_wins_over_a_wrong_kind() {
276 assert_eq!(
277 file_type_for("MP3 File", "Transfer.flac"),
278 rows::FILE_TYPE_FLAC
279 );
280 assert_eq!(file_type_for("ALAC File", "a.m4a"), rows::FILE_TYPE_ALAC);
281 assert_eq!(file_type_for("M4A File", "a.m4a"), rows::FILE_TYPE_M4A);
282 assert_eq!(
283 file_type_for("WAV File", "no-extension"),
284 rows::FILE_TYPE_WAV
285 );
286 }
287
288 fn keyed(tonality: &[&str]) -> Vec<DeviceTrack> {
289 tonality
290 .iter()
291 .map(|k| DeviceTrack {
292 track: Track {
293 tonality: k.to_string(),
294 ..Default::default()
295 },
296 usb_path: String::new(),
297 anlz_dir: String::new(),
298 anlz_index: 0,
299 file_size: 0,
300 sample_depth: 0,
301 file_type: 0,
302 bitrate: 0,
303 sample_rate: 0,
304 })
305 .collect()
306 }
307
308 fn names(rows: &[(u32, String)]) -> Vec<(u32, &str)> {
309 rows.iter().map(|(id, n)| (*id, n.as_str())).collect()
310 }
311
312 #[test]
313 fn keys_keep_the_xml_spelling_in_wheel_order() {
314 let rows = key_rows(&keyed(&["Cm", "Ab", "Cm", "", "A minor", " Abm "]));
316 assert_eq!(names(&rows), [(1, "Abm"), (2, "Ab"), (3, "Cm")]);
317 let rows = key_rows(&keyed(&["8A", "Am", "1A"]));
319 assert_eq!(names(&rows), [(1, "1A"), (2, "8A"), (3, "Am")]);
320 assert!(key_rows(&keyed(&["", "x"])).is_empty());
321 }
322
323 #[test]
324 fn today_is_a_date() {
325 let t = today();
326 assert_eq!(t.len(), 10);
327 assert!(t.starts_with("20"));
328 }
329}