selene-core 0.9.0-rc.1

Backend for selene-server
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::{
    database::{DatabaseError, Db, DbIdExt},
    log::trace,
    paths::cache_dir,
};
use selene_common::art::ImageSize;
use serde::{Deserialize, Serialize};

use crate::{database::Selene, library::Id};

#[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 mime_type(&self) -> &'static str {
        match self {
            ImageFormat::Png => "image/png",
            ImageFormat::Jpeg => "image/jpeg",
            ImageFormat::Tiff => "image/tiff",
            ImageFormat::Bmp => "image/bmp",
            ImageFormat::Gif => "image/gif",
        }
    }

    #[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,
}

impl lunar_lib::database::DatabaseEntry for ImageArt {
    type DbInner = Selene;
    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "art";
}

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 load_from_cache(
        id: Id<Self>,
        size: ImageSize,
        db: &Db<Selene>,
    ) -> Result<Option<PathBuf>, DatabaseError> {
        let (x, y) = size.size();
        let path = cache_dir().join(format!("art/{id}_{x}_{y}.jpg"));

        if path.try_exists()? {
            return Ok(Some(path));
        }

        let Some(art) = id.db_get(db)? else {
            return Ok(None);
        };

        let path = art.cache_file(size)?;
        Ok(Some(path))
    }

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

        if path.try_exists()? {
            return Ok(path);
        }

        trace!("Starting image encoding for {}", path.display());
        fs::create_dir_all(
            path.parent()
                .expect("Files must always live in a directory"),
        )?;
        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())
    }

    #[must_use]
    pub fn format(&self) -> ImageFormat {
        self.format
    }
}