selene-core 0.8.0

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{
    fs,
    io::{self, Cursor},
    path::{Path, PathBuf},
};

use blake3::Hash;
use image::{DynamicImage, ImageError, imageops::FilterType};
use lofty::{
    error::LoftyError,
    file::TaggedFileExt,
    picture::{MimeType, Picture, PictureType},
};
use serde::{Deserialize, Serialize};

use crate::{
    cache_dir,
    library::image_art::{CacheableArt, ImageArt, cache_image},
};

#[derive(Debug, Deserialize, Serialize, Clone)]
pub enum CoverArt {
    Embedded { hash: Hash, source: PathBuf },
    File(ImageArt),
}

impl CoverArt {
    pub fn from_file(file: impl Into<PathBuf>) -> Result<Self, ImageError> {
        let image = Self::File(ImageArt::from_file(file)?);
        Ok(image)
    }

    pub fn to_picture(&self) -> Result<Picture, LoftyError> {
        match self {
            CoverArt::File(image_art) => image_art.to_picture(),
            CoverArt::Embedded { source, .. } => {
                let mut file = fs::File::open(source)?;
                let mut tagged_file = lofty::read_from(&mut file)
                    .expect("Embedded art already has its metadata read");

                let tags = tagged_file.primary_tag_mut().unwrap();

                let picture_pos = tags
                    .pictures()
                    .iter()
                    .position(|p| p.pic_type() == PictureType::CoverFront)
                    .unwrap_or(0);

                let mut picture = tags.remove_picture(picture_pos);
                picture.set_pic_type(PictureType::CoverFront);
                Ok(picture)
            }
        }
    }

    pub fn export_to_image_art(&self, path: &Path) -> Result<ImageArt, ImageError> {
        match self {
            CoverArt::File(image_art) => Ok(image_art.clone()),
            CoverArt::Embedded { source, hash } => {
                let mut file = fs::File::open(source)?;
                let mut tagged_file = lofty::read_from(&mut file)
                    .expect("Embedded art already has its metadata read");

                let tags = tagged_file.primary_tag_mut().unwrap();

                let picture_pos = tags
                    .pictures()
                    .iter()
                    .position(|p| p.pic_type() == PictureType::CoverFront)
                    .unwrap_or(0);

                let picture = tags.remove_picture(picture_pos);
                let (format, ext) = match picture.mime_type().unwrap() {
                    MimeType::Png => (image::ImageFormat::Png, ".png"),
                    MimeType::Jpeg => (image::ImageFormat::Jpeg, ".jpg"),
                    MimeType::Tiff => (image::ImageFormat::Tiff, ".tiff"),
                    MimeType::Bmp => (image::ImageFormat::Bmp, ".bmp"),
                    MimeType::Gif => (image::ImageFormat::Gif, ".gif"),
                    _ => unreachable!("Embedded cover art is pre-validated on extract"),
                };

                let reader = Cursor::new(picture.into_data());
                let image = image::ImageReader::with_format(reader, format).decode()?;

                let mut path = path.to_owned().into_os_string();
                path.push(ext);
                let path = PathBuf::from(path);

                fs::create_dir_all(path.parent().unwrap())?;
                image.save_with_format(&path, format)?;

                Ok(ImageArt::from_raw(path, *hash))
            }
        }
    }
}

impl CacheableArt for CoverArt {
    fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf> {
        match self {
            CoverArt::File(image_art) => image_art.cache_file(x, y),

            CoverArt::Embedded { hash, .. } => {
                let cover_dir = cache_dir().join("covers");
                let cache_file = cover_dir.join(format!("{hash}_{x}_{x}.jpeg"));
                if cache_file.exists() {
                    return Ok(cache_file);
                }

                self.cache_image(x, y).map(|(_, p)| p)
            }
        }
    }

    fn cache_image(&self, x: u32, y: u32) -> io::Result<(DynamicImage, PathBuf)> {
        match self {
            CoverArt::File(image_art) => image_art.cache_image(x, y),

            // TODO: Optimize speed of embedded cover art caching. Most likely with a dedicated loading thread
            CoverArt::Embedded { hash, source } => {
                let cover_dir = cache_dir().join("covers");
                let cache_file = cover_dir.join(format!("{hash}_{x}_{x}.jpeg"));
                if cache_file.exists() {
                    let image =
                        image::open(&cache_file).expect("Cache art was modified or corrupted");
                    return Ok((image, cache_file));
                }

                let mut file = fs::File::open(source)?;
                let mut tagged_file = lofty::read_from(&mut file)
                    .expect("Embedded art already has its metadata read");

                let tags = tagged_file.primary_tag_mut().unwrap();

                let picture_pos = tags
                    .pictures()
                    .iter()
                    .position(|p| p.pic_type() == PictureType::CoverFront)
                    .unwrap_or(0);

                let picture = tags.remove_picture(picture_pos);
                let reader = Cursor::new(picture.into_data());
                let image = image::ImageReader::new(reader)
                    .with_guessed_format()
                    .unwrap()
                    .decode()
                    .unwrap()
                    .resize_to_fill(x, y, FilterType::Lanczos3);

                cache_image(&image, &cache_file)?;

                Ok((image, cache_file))
            }
        }
    }

    fn hash(&self) -> Hash {
        match self {
            CoverArt::Embedded { hash, .. } => *hash,
            CoverArt::File(image_art) => image_art.hash(),
        }
    }
}