selene-core 0.6.0

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

use blake3::Hash;
use image::{DynamicImage, ImageError, imageops::FilterType};
use lunar_lib::trace;
use serde::{Deserialize, Serialize};

use crate::cache_dir;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct ImageArt {
    hash: Hash,
    source: PathBuf,
}

pub trait CacheableArt {
    fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf>;
}

impl ImageArt {
    pub fn from_file(source: impl Into<PathBuf>) -> Result<Self, ImageError> {
        let source = source.into();
        let image = image::open(&source)?;
        let hash = blake3::hash(image.as_bytes());
        Ok(Self { hash, source })
    }

    #[must_use]
    pub fn hash(&self) -> Hash {
        self.hash
    }

    #[must_use]
    pub fn source(&self) -> &Path {
        &self.source
    }
}

impl CacheableArt for ImageArt {
    /// Extracts the image from the file and caches it with the given identifier
    ///
    /// The 'Identifier' for track cover art should ALWAYS be the hash of the image
    /// Art is cached in the `CACHE_DIR` with the name `{hash}_{x_width}_{y_width}.webp`
    /// The extracted file is LOSSY, not LOSSLESS
    fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf> {
        let cover_dir = cache_dir().join("covers");
        let cache_file = cover_dir.join(format!("{id}_{x}_{x}.jpeg", id = self.hash()));
        if cache_file.exists() {
            return Ok(cache_file);
        }

        let image = image::open(&self.source).expect("Image art should be pre-validated");

        cache_image(image, x, y, &cache_file)?;
        Ok(cache_file)
    }
}

pub fn hash_image(source: impl AsRef<Path>) -> Result<Hash, ImageError> {
    let image = image::open(source)?;
    Ok(blake3::hash(image.as_bytes()))
}

pub fn cache_image(image: DynamicImage, x: u32, y: u32, path: impl AsRef<Path>) -> io::Result<()> {
    let image = image.resize_to_fill(x, y, FilterType::Lanczos3);

    let path = path.as_ref();
    fs::create_dir_all(path.parent().unwrap())?;

    trace!("Starting image encoding for {}", path.display());
    let writer = BufWriter::new(
        fs::OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(path)?,
    );

    // let mut writer = Vec::new();
    let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(writer, 80);
    encoder.encode_image(&image).map_err(io::Error::other)?;
    // fs::write(path, writer)?;
    trace!("Finished image encoding for {}", path.display());

    Ok(())
}