Skip to main content

baken_export/
build.rs

1//! From the planned tracks and playlists to the `export.pdb` model.
2
3use 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/// A track as it will exist on the stick.
10#[derive(Debug, Clone)]
11pub struct DeviceTrack {
12    pub track: Track,
13    /// `/Contents/...`
14    pub usb_path: String,
15    /// `/PIONEER/USBANLZ/Pxxx/xxxxxxxx`
16    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
70/// Playlists to include, as indices into `library.playlists` (leaf playlists only).
71pub 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(); // name -> (id, single artist or None once mixed)
82    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    // Playlist tree: selected leaves plus every ancestor folder, ids in document order.
159    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
210/// Today's date as `YYYY-MM-DD` (UTC), without pulling in a date crate.
211pub 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    // Howard Hinnant's civil-from-days
218    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}