selene-core 0.9.0-alpha.2

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

use blake3::Hash;
use image::ImageError;
use lofty::{error::LoftyError, picture::Picture};
use lunar_lib::{
    log::trace,
    paths::{cache_dir, data_dir},
};
use serde::{Deserialize, Serialize};

use crate::library::Id;

#[cfg(feature = "database-impls")]
lunar_lib::define_db!(Images {
    fn path() -> Option<PathBuf> {
        Some(data_dir().join("image_data"))
    }
});

#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
    Png,
    Jpeg,
    Tiff,
    Bmp,
    Gif,
}

impl From<ImageFormat> for lofty::picture::MimeType {
    fn from(value: ImageFormat) -> Self {
        match value {
            ImageFormat::Png => lofty::picture::MimeType::Png,
            ImageFormat::Jpeg => lofty::picture::MimeType::Jpeg,
            ImageFormat::Tiff => lofty::picture::MimeType::Tiff,
            ImageFormat::Bmp => lofty::picture::MimeType::Bmp,
            ImageFormat::Gif => lofty::picture::MimeType::Gif,
        }
    }
}

impl From<ImageFormat> for image::ImageFormat {
    fn from(value: ImageFormat) -> Self {
        match value {
            ImageFormat::Png => image::ImageFormat::Png,
            ImageFormat::Jpeg => image::ImageFormat::Jpeg,
            ImageFormat::Tiff => image::ImageFormat::Tiff,
            ImageFormat::Bmp => image::ImageFormat::Bmp,
            ImageFormat::Gif => image::ImageFormat::Gif,
        }
    }
}

impl ImageFormat {
    #[must_use]
    pub fn try_from_mime_type(value: lofty::picture::MimeType) -> Option<Self> {
        let format = match value {
            lofty::picture::MimeType::Png => ImageFormat::Png,
            lofty::picture::MimeType::Jpeg => ImageFormat::Jpeg,
            lofty::picture::MimeType::Tiff => ImageFormat::Tiff,
            lofty::picture::MimeType::Bmp => ImageFormat::Bmp,
            lofty::picture::MimeType::Gif => ImageFormat::Gif,
            _ => return None,
        };

        Some(format)
    }

    fn try_from_image_image_format(value: image::ImageFormat) -> Option<Self> {
        let format = match value {
            image::ImageFormat::Png => ImageFormat::Png,
            image::ImageFormat::Jpeg => ImageFormat::Jpeg,
            image::ImageFormat::Tiff => ImageFormat::Tiff,
            image::ImageFormat::Bmp => ImageFormat::Bmp,
            image::ImageFormat::Gif => ImageFormat::Gif,
            _ => return None,
        };
        Some(format)
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct ImageArt {
    hash: Hash,
    data: Vec<u8>,
    format: ImageFormat,
}

#[cfg(feature = "database-impls")]
impl lunar_lib::database::DatabaseEntry for ImageArt {
    type DbInner = Images;
    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "images";
}

impl ImageArt {
    #[must_use]
    pub fn try_from_picture(picture: Picture) -> Option<Self> {
        let Some(mime_type) = picture.mime_type() else {
            return None;
        };

        Some(Self {
            hash: blake3::hash(picture.data()),
            format: ImageFormat::try_from_mime_type(mime_type.clone())?,
            data: picture.into_data(),
        })
    }

    pub fn from_image_file(path: impl AsRef<Path>) -> Result<Self, ImageError> {
        let path = path.as_ref();
        let reader = io::BufReader::new(fs::File::open(path)?);

        let image = image::ImageReader::new(reader).with_guessed_format()?;
        let format = image.format().expect("Format was just guessed");
        let format = ImageFormat::try_from_image_image_format(format).ok_or(io::Error::new(
            io::ErrorKind::InvalidData,
            "invalid image format",
        ))?;

        let mut data = Vec::new();
        image.into_inner().read_to_end(&mut data)?;

        Ok(Self {
            hash: blake3::hash(&data),
            data,
            format,
        })
    }

    pub fn to_picture(&self) -> Result<Picture, LoftyError> {
        let mut cursor = io::Cursor::new(&self.data);
        let mut picture = Picture::from_reader(&mut cursor)?;
        picture.set_pic_type(lofty::picture::PictureType::CoverFront);
        Ok(picture)
    }

    pub fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf> {
        let path = cache_dir().join(format!("art/{}_{x}_{y}.jpg", self.hash));

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

        let image = image::load_from_memory_with_format(&self.data, self.format.into())
            .expect("Stored image data should always be valid");

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

        Ok(path)
    }

    #[must_use]
    pub fn id(&self) -> Id<ImageArt> {
        Id::from(*self.hash.as_bytes())
    }
}