1use std::{
2 collections::HashSet,
3 fs::{File, create_dir_all},
4 hash::{DefaultHasher, Hash, Hasher},
5 io::{Cursor, Read},
6 path::PathBuf,
7 sync::{Arc, LazyLock, RwLock},
8};
9
10pub use image::ImageFormat;
11use image::{ImageBuffer, ImageReader, Rgb, imageops::resize};
12use lofty::{
13 picture::{Picture, PictureType},
14 tag::Tag,
15};
16use log::{error, warn};
17use serde::{Deserialize, Serialize};
18
19use crate::music_lib::{
20 CACHE_DIR,
21 indexer::{self, CacheMode},
22 state::Track,
23};
24
25pub use image::imageops::FilterType as CoverFilter;
26
27#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum Quality {
29 Low,
30 #[default]
31 Default,
32 High,
33 Lossless,
34 Custom(u32),
35}
36
37impl Quality {
38 pub(crate) fn resolution(self) -> Option<u32> {
39 match self {
40 Quality::Low => Some(256),
41 Quality::Default => Some(512),
42 Quality::High => Some(1024),
43 Quality::Lossless => None,
44 Quality::Custom(pixels) => Some(pixels),
45 }
46 }
47}
48
49#[cfg_attr(test, derive(Default))]
50#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
51pub struct Cover {
52 hash: u64,
53 cover_available: bool,
54 thumbnail_available: bool,
55 is_playlist: bool,
56}
57
58impl Cover {
59 pub(crate) fn none() -> Self {
60 Self {
61 hash: 0,
62 cover_available: false,
63 thumbnail_available: false,
64 is_playlist: false,
65 }
66 }
67
68 pub fn cover(&self) -> Result<PathBuf, CoverError> {
76 let mut path = match self.is_playlist {
77 true => PLAYLIST_COVER_CACHE_DIR.clone()?,
78 false => TRACK_COVER_CACHE_DIR.clone()?,
79 };
80 let guard = crate::CONFIG.read().unwrap();
81 let extension = guard.as_ref().unwrap().library.indexer.covers.extension();
82
83 match self.cover_available {
84 true => {
85 path.push(format!("{}.{extension}", self.hash));
86 Ok(path)
87 }
88 false => Err(CoverError::ImageUnavailable),
89 }
90 }
91
92 pub fn thumbnail(&self) -> Result<PathBuf, CoverError> {
100 let mut path = match self.is_playlist {
101 true => PLAYLIST_THUMBNAIL_CACHE_DIR.clone()?,
102 false => TRACK_THUMBNAIL_CACHE_DIR.clone()?,
103 };
104 let guard = crate::CONFIG.read().unwrap();
105 let extension = guard.as_ref().unwrap().library.indexer.covers.extension();
106
107 match self.thumbnail_available {
108 true => {
109 path.push(format!("{}.{extension}", self.hash));
110 Ok(path)
111 }
112 false => Err(CoverError::ImageUnavailable),
113 }
114 }
115}
116
117#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub struct Config {
119 pub format: ImageFormat,
120 pub filter: CoverFilter,
121 pub thumbnail_resolution: u32,
122 pub quality: Quality,
123}
124
125impl Default for Config {
126 fn default() -> Self {
127 Self {
128 format: ImageFormat::Png,
129 filter: CoverFilter::Triangle,
130 thumbnail_resolution: 40,
131 quality: Quality::default(),
132 }
133 }
134}
135
136impl Config {
137 pub(crate) fn extension(&self) -> String {
138 match self.format {
139 ImageFormat::Png => "png",
140 ImageFormat::Jpeg => "jpg",
141 ImageFormat::Gif => "gif",
142 ImageFormat::WebP => "webp",
143 ImageFormat::Pnm => "pnm",
144 ImageFormat::Tiff => "tiff",
145 ImageFormat::Tga => "tga",
146 ImageFormat::Dds => "dds",
147 ImageFormat::Bmp => "bmp",
148 ImageFormat::Ico => "ico",
149 ImageFormat::Hdr => "hdr",
150 ImageFormat::OpenExr => "exr",
151 ImageFormat::Farbfeld => "ff",
152 ImageFormat::Avif => "avif",
153 ImageFormat::Qoi => "qoi",
154 _ => "img",
155 }
156 .to_string()
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
161pub enum CoverError {
162 NoPictures,
163 NoSuitablePictures,
164 CoverWriteError { path: PathBuf, error: String },
165 CacheDirError { path: PathBuf, error: String },
166 UnknownFileFormat,
167 DecodingError,
168 Playlist,
169 ImageUnavailable,
170}
171
172impl std::fmt::Display for CoverError {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 Self::NoPictures => write!(f, "Could not find any pictures in the tag"),
176 Self::NoSuitablePictures => write!(
177 f,
178 "The tag contains pictures, but none of them can be used as cover art replacement"
179 ),
180 Self::CoverWriteError { path, error } => write!(
181 f,
182 "Could not write the cover image to cache at {path:?}: {error}"
183 ),
184 Self::CacheDirError { path, error } => {
185 write!(
186 f,
187 "Could not create the cache directory at {path:?}: {error}"
188 )
189 }
190 Self::UnknownFileFormat => write!(f, "Could not guess the image format"),
191 Self::DecodingError => write!(f, "Could not decode the image data"),
192 Self::Playlist => write!(f, "Could not create a playlist cover image"),
193 Self::ImageUnavailable => write!(
194 f,
195 "The cover image does not exist in the cache or is unavailable"
196 ),
197 }
198 }
199}
200
201const FRONT_COVER_PRIORITY: [PictureType; 21] = [
202 PictureType::CoverFront,
203 PictureType::CoverBack,
204 PictureType::Illustration,
205 PictureType::Leaflet,
206 PictureType::Media,
207 PictureType::BandLogo,
208 PictureType::Other,
209 PictureType::Band,
210 PictureType::ScreenCapture,
211 PictureType::DuringPerformance,
212 PictureType::DuringRecording,
213 PictureType::RecordingLocation,
214 PictureType::BrightFish,
215 PictureType::LeadArtist,
216 PictureType::Artist,
217 PictureType::Composer,
218 PictureType::Lyricist,
219 PictureType::Conductor,
220 PictureType::PublisherLogo,
221 PictureType::Icon,
222 PictureType::OtherIcon,
223];
224
225pub(crate) static TRACK_COVER_CACHE_DIR: LazyLock<Result<PathBuf, CoverError>> =
226 LazyLock::new(|| {
227 let mut cache = CACHE_DIR.read().unwrap().clone().unwrap();
228 cache.push("covers");
229 cache.push("tracks");
230 cache.push("hires");
231 if !cache.is_dir()
232 && let Err(e) = create_dir_all(&cache)
233 {
234 error!("Could not create directory at {cache:?}!");
235 Err(CoverError::CoverWriteError {
236 path: cache,
237 error: e.to_string(),
238 })
239 } else {
240 Ok(cache)
241 }
242 });
243
244pub(crate) static TRACK_THUMBNAIL_CACHE_DIR: LazyLock<Result<PathBuf, CoverError>> =
245 LazyLock::new(|| {
246 let mut cache = CACHE_DIR.read().unwrap().clone().unwrap();
247 cache.push("covers");
248 cache.push("tracks");
249 cache.push("thumbnails");
250 if !cache.is_dir()
251 && let Err(e) = create_dir_all(&cache)
252 {
253 error!("Could not create directory at {cache:?}!");
254 Err(CoverError::CoverWriteError {
255 path: cache,
256 error: e.to_string(),
257 })
258 } else {
259 Ok(cache)
260 }
261 });
262
263pub(crate) static PLAYLIST_COVER_CACHE_DIR: LazyLock<Result<PathBuf, CoverError>> =
264 LazyLock::new(|| {
265 let mut cache = CACHE_DIR.read().unwrap().clone().unwrap();
266 cache.push("covers");
267 cache.push("playlists");
268 cache.push("hires");
269 if !cache.is_dir()
270 && let Err(e) = create_dir_all(&cache)
271 {
272 error!("Could not create directory at {cache:?}!");
273 Err(CoverError::CoverWriteError {
274 path: cache,
275 error: e.to_string(),
276 })
277 } else {
278 Ok(cache)
279 }
280 });
281
282pub(crate) static PLAYLIST_THUMBNAIL_CACHE_DIR: LazyLock<Result<PathBuf, CoverError>> =
283 LazyLock::new(|| {
284 let mut cache = CACHE_DIR.read().unwrap().clone().unwrap();
285 cache.push("covers");
286 cache.push("playlists");
287 cache.push("thumbnails");
288 if !cache.is_dir()
289 && let Err(e) = create_dir_all(&cache)
290 {
291 error!("Could not create directory at {cache:?}!");
292 Err(CoverError::CoverWriteError {
293 path: cache,
294 error: e.to_string(),
295 })
296 } else {
297 Ok(cache)
298 }
299 });
300
301fn pick_front_cover_or_replacement(pictures: &[Picture]) -> Result<&Picture, CoverError> {
302 if pictures.is_empty() {
303 return Err(CoverError::NoPictures);
304 }
305
306 for pic_type in FRONT_COVER_PRIORITY {
307 for pic in pictures {
308 if pic.pic_type() == pic_type {
309 return Ok(pic);
310 }
311 }
312 }
313
314 Err(CoverError::NoSuitablePictures)
315}
316
317pub(crate) fn get_track_cover(
318 tag: &Tag,
319 config: indexer::Config,
320 covers_lookup_set: &RwLock<HashSet<PathBuf>>,
321) -> Result<Cover, CoverError> {
322 let cover_cache = TRACK_COVER_CACHE_DIR.clone()?;
323 let thumbnail_cache = TRACK_THUMBNAIL_CACHE_DIR.clone()?;
324
325 let image_buf = pick_front_cover_or_replacement(tag.pictures())?.data();
326 let mut hasher = DefaultHasher::new();
327 image_buf.hash(&mut hasher);
328 let hash = hasher.finish();
329
330 let extension = config.covers.extension();
331 let mut cover_path = cover_cache.clone();
332 cover_path.push(format!("{hash}.{extension}"));
333 let mut thumbnail_path = thumbnail_cache.clone();
334 thumbnail_path.push(format!("{hash}.{extension}"));
335
336 let image =
337 LazyLock::new(
338 || match ImageReader::new(Cursor::new(image_buf)).with_guessed_format() {
339 Ok(image) => match image.decode() {
340 Ok(i) => Ok(i),
341 Err(e) => {
342 warn!("Couldn't decode image data: {e}");
343 Err(CoverError::DecodingError)
344 }
345 },
346 Err(e) => {
347 warn!("{e}");
348 Err(CoverError::UnknownFileFormat)
349 }
350 },
351 );
352
353 let cover_available = if config.cache_mode == CacheMode::UseCache
354 && covers_lookup_set.read().unwrap().contains(&cover_path)
355 {
356 true
357 } else if config.cache_mode == CacheMode::UseCache && cover_path.is_file() {
358 covers_lookup_set
359 .write()
360 .unwrap()
361 .insert(cover_path.clone());
362 true
363 } else {
364 let image = match image.as_ref() {
365 Ok(image) => image,
366 Err(e) => {
367 return Err(e.clone());
368 }
369 };
370
371 let cover = match config.covers.quality.resolution() {
372 Some(res) => {
373 if image.height() > res || image.height() > res {
374 resize(image, res, res, config.covers.filter)
375 } else {
376 image.clone().into()
377 }
378 }
379 None => image.clone().into(),
380 };
381
382 match File::create(&cover_path) {
383 Ok(mut file) => match cover.write_to(&mut file, config.covers.format) {
384 Ok(_) => {
385 covers_lookup_set
386 .write()
387 .unwrap()
388 .insert(cover_path.clone());
389 true
390 }
391 Err(e) => {
392 warn!("Couldn't write thumbnail to {cover_path:?}: {e}");
393 false
394 }
395 },
396 Err(e) => {
397 warn!("Couldn't write thumbnail to {cover_path:?}: {e}");
398 false
399 }
400 }
401 };
402
403 let thumbnail_available = if config.cache_mode == CacheMode::UseCache
404 && covers_lookup_set.read().unwrap().contains(&thumbnail_path)
405 {
406 true
407 } else if config.cache_mode == CacheMode::UseCache && thumbnail_path.is_file() {
408 covers_lookup_set
409 .write()
410 .unwrap()
411 .insert(thumbnail_path.clone());
412 true
413 } else {
414 let image = match &*image {
415 Ok(image) => image,
416 Err(e) => {
417 return Err(e.clone());
418 }
419 };
420
421 let thumbnail = resize(
422 image,
423 config.covers.thumbnail_resolution,
424 config.covers.thumbnail_resolution,
425 config.covers.filter,
426 );
427
428 match File::create(&thumbnail_path) {
429 Ok(mut file) => match thumbnail.write_to(&mut file, config.covers.format) {
430 Ok(_) => {
431 covers_lookup_set
432 .write()
433 .unwrap()
434 .insert(thumbnail_path.clone());
435 true
436 }
437 Err(e) => {
438 warn!("Couldn't write thumbnail to {thumbnail_path:?}: {e}");
439 false
440 }
441 },
442 Err(e) => {
443 warn!("Couldn't write thumbnail to {thumbnail_path:?}: {e}");
444 false
445 }
446 }
447 };
448
449 Ok(Cover {
450 hash,
451 cover_available,
452 thumbnail_available,
453 is_playlist: false,
454 })
455}
456
457fn get_unique_covers(tracks: &[Arc<Track>]) -> Vec<&Cover> {
458 let mut covers: HashSet<&Cover> = HashSet::new();
459 for track in tracks {
460 covers.insert(&track.cover);
461 }
462 covers.into_iter().collect()
463}
464
465fn tile_2x2(images: [&ImageBuffer<Rgb<u8>, Vec<u8>>; 4]) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
466 let w = images.iter().map(|img| img.width()).max().unwrap();
467 let h = images.iter().map(|img| img.height()).max().unwrap();
468
469 let mut canvas = ImageBuffer::new(w * 2, h * 2);
470
471 for (i, img) in images.iter().enumerate() {
472 let x0 = if i % 2 == 0 { 0 } else { w };
473 let y0 = if i / 2 == 0 { 0 } else { h };
474
475 for y in 0..img.height() {
476 for x in 0..img.width() {
477 canvas.put_pixel(x0 + x, y0 + y, *img.get_pixel(x, y));
478 }
479 }
480 }
481
482 canvas
483}
484
485pub(crate) fn get_playlist_cover(
486 name: &str,
487 config: indexer::Config,
488 tracks: &[Arc<Track>],
489) -> Result<Cover, CoverError> {
490 if tracks.is_empty() {
491 Ok(Cover::none()) } else if tracks.len() >= 4 {
493 let unique_covers = get_unique_covers(tracks);
494 if unique_covers.len() >= 4 {
495 let mut hasher = DefaultHasher::new();
498 name.hash(&mut hasher);
499 let hash = hasher.finish();
500
501 let extension = config.covers.extension();
502 let mut cover_path = PLAYLIST_COVER_CACHE_DIR.clone()?;
503 cover_path.push(format!("{hash}.{extension}"));
504 let mut thumbnail_path = PLAYLIST_THUMBNAIL_CACHE_DIR.clone()?;
505 thumbnail_path.push(format!("{hash}.{extension}"));
506
507 let image = LazyLock::new(|| {
508 let mut image_buffers = Vec::new();
509 for cover in &unique_covers {
510 let image_buf = match cover.cover() {
511 Ok(path) => match File::open(&path) {
512 Ok(mut handle) => {
513 let mut buf = Vec::new();
514 if let Err(e) = handle.read_to_end(&mut buf) {
515 warn!("Couldn't read from file {path:?}: {e}")
516 }
517 buf
518 }
519 Err(e) => {
520 warn!("Couldn't open cover {path:?}: {e}");
521 continue;
522 }
523 },
524 Err(_) => continue,
525 };
526
527 match ImageReader::new(Cursor::new(image_buf)).with_guessed_format() {
528 Ok(image) => match image.decode() {
529 Ok(i) => image_buffers.push(i),
530 Err(e) => {
531 warn!("Couldn't decode image data: {e}");
532 continue;
533 }
534 },
535 Err(e) => {
536 warn!("{e}");
537 continue;
538 }
539 }
540
541 if image_buffers.len() == 4 {
542 break;
543 }
544 }
545
546 let buffers = if let (Some(a), Some(b), Some(c), Some(d)) = (
547 image_buffers.first(),
548 image_buffers.get(1),
549 image_buffers.get(2),
550 image_buffers.get(3),
551 ) {
552 Some([&a.to_rgb8(), &b.to_rgb8(), &c.to_rgb8(), &d.to_rgb8()])
553 } else {
554 None
555 };
556
557 buffers.map(tile_2x2)
558 });
559
560 let cover_available =
561 if config.cache_mode == CacheMode::UseCache && cover_path.is_file() {
562 true
563 } else {
564 let image = match image.as_ref() {
565 Some(image) => image,
566 None => {
567 return Err(CoverError::Playlist);
568 }
569 };
570
571 let cover = match config.covers.quality.resolution() {
572 Some(res) => {
573 if image.height() > res && image.height() > res {
574 resize(image, res, res, config.covers.filter)
575 } else {
576 image.clone()
577 }
578 }
579 None => image.clone(),
580 };
581
582 match File::create(&cover_path) {
583 Ok(mut file) => match cover.write_to(&mut file, config.covers.format) {
584 Ok(_) => true,
585 Err(e) => {
586 warn!("Couldn't write thumbnail to {cover_path:?}: {e}");
587 false
588 }
589 },
590 Err(e) => {
591 warn!("Couldn't write thumbnail to {cover_path:?}: {e}");
592 false
593 }
594 }
595 };
596
597 let thumbnail_available =
598 if config.cache_mode == CacheMode::UseCache && thumbnail_path.is_file() {
599 true
600 } else {
601 let image = match image.as_ref() {
602 Some(image) => image,
603 None => {
604 return Err(CoverError::Playlist);
605 }
606 };
607
608 let thumbnail = resize(
609 image,
610 config.covers.thumbnail_resolution,
611 config.covers.thumbnail_resolution,
612 config.covers.filter,
613 );
614
615 match File::create(&thumbnail_path) {
616 Ok(mut file) => match thumbnail.write_to(&mut file, config.covers.format) {
617 Ok(_) => true,
618 Err(e) => {
619 warn!("Couldn't write thumbnail to {thumbnail_path:?}: {e}");
620 false
621 }
622 },
623 Err(e) => {
624 warn!("Couldn't write thumbnail to {thumbnail_path:?}: {e}");
625 false
626 }
627 }
628 };
629
630 Ok(Cover {
631 hash,
632 cover_available,
633 thumbnail_available,
634 is_playlist: true,
635 })
636 } else {
637 Ok(tracks[0].cover.clone())
638 }
639 } else {
640 Ok(tracks[0].cover.clone())
641 }
642}