1use crate::collection::Track;
6use baken_core::cdjsafe::stick_path;
7use std::collections::HashSet;
8use std::path::Path;
9
10#[derive(Debug, Default)]
11pub struct Layout {
12 used: HashSet<String>,
13}
14
15impl Layout {
16 pub fn assign(&mut self, track: &Track) -> String {
18 let path =
19 |suffix: &str| stick_path(&track.artist, &track.album, track.file_name(), suffix);
20 let mut candidate = path("");
21 let mut n = 1;
22 while !self.used.insert(candidate.to_lowercase()) {
23 candidate = path(&format!("-{n}"));
24 n += 1;
25 }
26 candidate
27 }
28}
29
30pub fn sample_depth(path: &Path) -> u16 {
33 fn read(path: &Path) -> Option<u16> {
34 use std::io::Read;
35 let mut f = std::fs::File::open(path).ok()?;
36 let mut head = vec![0u8; 64 * 1024];
37 let n = f.read(&mut head).ok()?;
38 head.truncate(n);
39 let bytes = |at: usize| -> Option<[u8; 2]> { head.get(at..at + 2)?.try_into().ok() };
40 if head.starts_with(b"fLaC") {
41 let [a, b] = bytes(20)?;
43 return Some(((((a & 1) as u16) << 4) | (b >> 4) as u16) + 1);
44 }
45 if head.starts_with(b"FORM") && matches!(head.get(8..12), Some(b"AIFF" | b"AIFC")) {
46 let comm = chunk(&head, b"COMM", true)?;
47 return Some(u16::from_be_bytes(comm.get(6..8)?.try_into().ok()?));
48 }
49 if head.starts_with(b"RIFF") && head.get(8..12) == Some(b"WAVE") {
50 let fmt = chunk(&head, b"fmt ", false)?;
51 return Some(u16::from_le_bytes(fmt.get(14..16)?.try_into().ok()?));
52 }
53 if let Some(i) = head
55 .windows(4)
56 .position(|w| w == b"alac")
57 .filter(|_| head.get(4..8) == Some(b"ftyp"))
58 {
59 if let Some(j) = head[i + 4..].windows(4).position(|w| w == b"alac") {
61 let cfg = i + 4 + j + 4 + 4;
62 return head.get(cfg + 5).map(|&b| b as u16);
63 }
64 }
65 None
66 }
67 read(path).filter(|d| (8..=32).contains(d)).unwrap_or(16)
68}
69
70fn chunk<'a>(buf: &'a [u8], id: &[u8; 4], big_endian: bool) -> Option<&'a [u8]> {
72 let mut off = 12;
73 while off + 8 <= buf.len() {
74 let size: [u8; 4] = buf[off + 4..off + 8].try_into().ok()?;
75 let len = if big_endian {
76 u32::from_be_bytes(size)
77 } else {
78 u32::from_le_bytes(size)
79 } as usize;
80 if &buf[off..off + 4] == id {
81 return buf.get(off + 8..off + 8 + len);
82 }
83 off += 8 + len + (len & 1);
84 }
85 None
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 fn track(artist: &str, album: &str, file: &str) -> Track {
93 Track {
94 artist: artist.into(),
95 album: album.into(),
96 location: format!("/Volumes/X/{file}"),
97 ..Default::default()
98 }
99 }
100
101 #[test]
102 fn a_truncated_header_reports_16_instead_of_panicking() {
103 let path = std::env::temp_dir().join(format!("baken-depth-{}", std::process::id()));
104 for head in [
105 &b"fLaC\0\0"[..],
106 b"FORM\0\0\0\0AI",
107 b"RIFF\0\0\0\0WAVEfmt \x10\0\0\0",
108 ] {
109 std::fs::write(&path, head).unwrap();
110 assert_eq!(sample_depth(&path), 16);
111 }
112 let _ = std::fs::remove_file(&path);
113 }
114
115 #[test]
116 fn rekordbox_naming_rules() {
117 let mut l = Layout::default();
118 assert_eq!(
119 l.assign(&track(
120 "Lidvall",
121 "The Strange Guy EP",
122 "Lidvall - The Strange Guy EP - 05 Where have you been-.flac"
123 )),
124 "/Contents/Lidvall/The Strange Guy EP/Lidvall - The Strange Guy EP - 05 Where hav.flac"
125 );
126 assert_eq!(
127 l.assign(&track("6EJOU ", "", "6EJOU - Psycho Dreams.aif")),
128 "/Contents/6EJOU/UnknownAlbum/6EJOU - Psycho Dreams.aif"
129 );
130 assert_eq!(
131 l.assign(&track("Oktobr", "CRVA002 | ANNIVERSARY", "x.flac")),
132 "/Contents/Oktobr/CRVA002 _ ANNIVERSARY/x.flac"
133 );
134 let a = l.assign(&track(
136 "T.A.M",
137 "",
138 "T.A.M - Raw-Hypnotic 2023 - Year Compilation - 11 Speedy Hypno.flac",
139 ));
140 let b = l.assign(&track(
141 "T.A.M",
142 "",
143 "T.A.M - Raw-Hypnotic 2023 - Year Compilation - 12 Other.flac",
144 ));
145 assert_eq!(
146 a,
147 "/Contents/T.A.M/UnknownAlbum/T.A.M - Raw-Hypnotic 2023 - Year Compilatio.flac"
148 );
149 assert_eq!(
150 b,
151 "/Contents/T.A.M/UnknownAlbum/T.A.M - Raw-Hypnotic 2023 - Year Compilat-1.flac"
152 );
153 assert_eq!(
154 l.assign(&track("", "", "盾.mp3")),
155 "/Contents/UnknownArtist/UnknownAlbum/盾.mp3"
156 );
157 }
158}