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),
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(),
}
}
}