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 if head.starts_with(b"fLaC") {
40 let body = &head[8..];
42 let bps = (((body[12] & 1) as u16) << 4) | ((body[13] >> 4) as u16);
43 return Some(bps + 1);
44 }
45 if head.starts_with(b"FORM") && &head[8..12] == b"AIFF"
46 || head.starts_with(b"FORM") && &head[8..12] == b"AIFC"
47 {
48 let mut off = 12;
49 while off + 8 <= head.len() {
50 let len = u32::from_be_bytes(head[off + 4..off + 8].try_into().ok()?) as usize;
51 if &head[off..off + 4] == b"COMM" {
52 return Some(u16::from_be_bytes(
53 head[off + 14..off + 16].try_into().ok()?,
54 ));
55 }
56 off += 8 + len + (len & 1);
57 }
58 }
59 if head.starts_with(b"RIFF") && &head[8..12] == b"WAVE" {
60 let mut off = 12;
61 while off + 8 <= head.len() {
62 let len = u32::from_le_bytes(head[off + 4..off + 8].try_into().ok()?) as usize;
63 if &head[off..off + 4] == b"fmt " {
64 return Some(u16::from_le_bytes(
65 head[off + 22..off + 24].try_into().ok()?,
66 ));
67 }
68 off += 8 + len + (len & 1);
69 }
70 }
71 if let Some(i) = head
73 .windows(4)
74 .position(|w| w == b"alac")
75 .filter(|_| &head[4..8] == b"ftyp")
76 {
77 if let Some(j) = head[i + 4..].windows(4).position(|w| w == b"alac") {
79 let cfg = i + 4 + j + 4 + 4;
80 return head.get(cfg + 5).map(|&b| b as u16);
81 }
82 }
83 None
84 }
85 read(path).filter(|d| (8..=32).contains(d)).unwrap_or(16)
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 rekordbox_naming_rules() {
103 let mut l = Layout::default();
104 assert_eq!(
105 l.assign(&track(
106 "Lidvall",
107 "The Strange Guy EP",
108 "Lidvall - The Strange Guy EP - 05 Where have you been-.flac"
109 )),
110 "/Contents/Lidvall/The Strange Guy EP/Lidvall - The Strange Guy EP - 05 Where hav.flac"
111 );
112 assert_eq!(
113 l.assign(&track("6EJOU ", "", "6EJOU - Psycho Dreams.aif")),
114 "/Contents/6EJOU/UnknownAlbum/6EJOU - Psycho Dreams.aif"
115 );
116 assert_eq!(
117 l.assign(&track("Oktobr", "CRVA002 | ANNIVERSARY", "x.flac")),
118 "/Contents/Oktobr/CRVA002 _ ANNIVERSARY/x.flac"
119 );
120 let a = l.assign(&track(
122 "T.A.M",
123 "",
124 "T.A.M - Raw-Hypnotic 2023 - Year Compilation - 11 Speedy Hypno.flac",
125 ));
126 let b = l.assign(&track(
127 "T.A.M",
128 "",
129 "T.A.M - Raw-Hypnotic 2023 - Year Compilation - 12 Other.flac",
130 ));
131 assert_eq!(
132 a,
133 "/Contents/T.A.M/UnknownAlbum/T.A.M - Raw-Hypnotic 2023 - Year Compilatio.flac"
134 );
135 assert_eq!(
136 b,
137 "/Contents/T.A.M/UnknownAlbum/T.A.M - Raw-Hypnotic 2023 - Year Compilat-1.flac"
138 );
139 assert_eq!(
140 l.assign(&track("", "", "盾.mp3")),
141 "/Contents/UnknownArtist/UnknownAlbum/盾.mp3"
142 );
143 }
144}