Skip to main content

chilen_backend/music_lib/indexer/
covers.rs

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    /// Returns the path to the high resolution cover image of the element.
69    ///
70    /// This method might fail if the cache directory is not accessible, the image file is not
71    /// available.
72    ///
73    /// # Panics
74    /// Panics if the backend config isn't initialized.
75    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    /// Returns the path to the low resolution thumbnail image of the element.
93    ///
94    /// This method might fail if the cache directory is not accessible, the image file is not
95    /// available.
96    ///
97    /// # Panics
98    /// Panics if the backend config isn't initialized.
99    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()) // No covers for empty playlists
492    } else if tracks.len() >= 4 {
493        let unique_covers = get_unique_covers(tracks);
494        if unique_covers.len() >= 4 {
495            // And then for playlists with tracks with 4 or more unique covers, a custom cover is used
496
497            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}