1use std::fs;
2use std::path::{Path, PathBuf};
3
4fn detect_image_extension(data: &[u8]) -> &'static str {
5 if data.len() >= 12 && &data[..8] == b"\x89PNG\r\n\x1a\n" {
6 "png"
7 } else if data.len() >= 3 && data[..3] == [0xFF, 0xD8, 0xFF] {
8 "jpg"
9 } else if data.len() >= 12 && &data[..4] == b"RIFF" && &data[8..12] == b"WEBP" {
10 "webp"
11 } else if data.len() >= 6 && (data[..6] == *b"GIF87a" || data[..6] == *b"GIF89a") {
12 "gif"
13 } else if data.len() >= 2 && data[..2] == [0x42, 0x4D] {
14 "bmp"
15 } else {
16 "jpg"
17 }
18}
19
20fn remove_stale_cover_variants(album_id: &str, cache_dir: &Path, keep_path: &Path) {
21 for extension in ["jpg", "png", "webp", "gif", "bmp", "tif"] {
22 let candidate = cache_dir.join(format!("{album_id}.{extension}"));
23 if candidate != keep_path {
24 let _ = fs::remove_file(candidate);
25 }
26 }
27}
28
29pub fn find_folder_cover(dir: &Path) -> Option<PathBuf> {
30 let candidates = [
31 "cover",
32 "folder",
33 "album",
34 "thumbnail",
35 "default",
36 "hqdefault",
37 "maxresdefault",
38 "preview",
39 ];
40 let extensions = ["jpg", "jpeg", "png", "webp"];
41
42 let entries = std::fs::read_dir(dir).ok()?;
43 let mut fallback_image = None;
44 let mut has_multiple_fallbacks = false;
45 let mut best_idx: Option<usize> = None;
46 let mut best_path = None;
47
48 for entry in entries.flatten() {
49 let path = entry.path();
50 if !path.is_file() {
51 continue;
52 }
53 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
54 continue;
55 };
56 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
57 continue;
58 };
59
60 if extensions.iter().any(|e| e.eq_ignore_ascii_case(ext)) {
61 if let Some(pos) = candidates.iter().position(|c| c.eq_ignore_ascii_case(stem)) {
62 if best_idx.is_none() || pos < best_idx.unwrap() {
63 best_idx = Some(pos);
64 best_path = Some(path);
65 }
66 } else if is_safe_fallback_cover(stem) {
67 if fallback_image.is_some() {
68 has_multiple_fallbacks = true;
69 fallback_image = None;
70 } else if !has_multiple_fallbacks {
71 fallback_image = Some(path);
72 }
73 }
74 }
75 }
76 best_path.or(fallback_image)
77}
78
79fn is_safe_fallback_cover(stem: &str) -> bool {
80 !matches!(
81 stem.to_ascii_lowercase().as_str(),
82 "artist"
83 | "background"
84 | "back"
85 | "banner"
86 | "booklet"
87 | "cd"
88 | "disc"
89 | "label"
90 | "logo"
91 | "spine"
92 )
93}
94
95pub fn is_artist_image_file(path: &Path) -> bool {
96 path.file_name()
97 .and_then(|name| name.to_str())
98 .map(|name| {
99 matches!(
100 name.to_ascii_lowercase().as_str(),
101 "artist.jpg" | "artist.jpeg" | "artist.png" | "artist.webp"
102 )
103 })
104 .unwrap_or(false)
105}
106
107pub fn save_cover(
108 album_id: &str,
109 data: &[u8],
110 extension: Option<&str>,
111 cache_dir: &Path,
112) -> std::io::Result<PathBuf> {
113 fs::create_dir_all(cache_dir)?;
114 let extension = extension.unwrap_or_else(|| detect_image_extension(data));
115 let path = cache_dir.join(format!("{album_id}.{extension}"));
116
117 remove_stale_cover_variants(album_id, cache_dir, &path);
118 fs::write(&path, data)?;
119 Ok(path)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::fs::File;
126
127 #[test]
128 fn test_find_folder_cover() {
129 let nanos = std::time::SystemTime::now()
130 .duration_since(std::time::UNIX_EPOCH)
131 .unwrap()
132 .as_nanos();
133 let dir_path = std::env::temp_dir().join(format!("kopuz_test_dir_{nanos}"));
134 std::fs::create_dir_all(&dir_path).unwrap();
135
136 assert!(find_folder_cover(&dir_path).is_none());
138
139 File::create(dir_path.join("song.mp3")).unwrap();
141 File::create(dir_path.join("readme.txt")).unwrap();
142 assert!(find_folder_cover(&dir_path).is_none());
143
144 let random_image = dir_path.join("random_picture.jpg");
146 File::create(&random_image).unwrap();
147 assert_eq!(find_folder_cover(&dir_path), Some(random_image.clone()));
148
149 let cover_image = dir_path.join("cover.png");
151 File::create(&cover_image).unwrap();
152 assert_eq!(find_folder_cover(&dir_path), Some(cover_image.clone()));
154
155 std::fs::remove_file(cover_image).unwrap();
158 std::fs::remove_file(random_image).unwrap();
159 let hq_image = dir_path.join("hqdefault.webp");
160 File::create(&hq_image).unwrap();
161 assert_eq!(find_folder_cover(&dir_path), Some(hq_image));
162
163 std::fs::remove_file(dir_path.join("hqdefault.webp")).unwrap();
165 File::create(dir_path.join("random_picture.jpg")).unwrap();
166 File::create(dir_path.join("other_picture.png")).unwrap();
167 assert!(find_folder_cover(&dir_path).is_none());
168
169 let _ = std::fs::remove_dir_all(&dir_path);
171 }
172}