2025-10-12 21:39:20 +02:00
|
|
|
//! Module de gestion du cache d'images avec conversion WebP
|
|
|
|
|
//!
|
|
|
|
|
//! Ce module étend le cache générique de `pmocache` avec des fonctionnalités
|
|
|
|
|
//! spécifiques aux images : conversion WebP et génération de variantes.
|
|
|
|
|
|
2025-10-07 08:33:06 +02:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use anyhow::{anyhow, Result};
|
2025-10-12 21:39:20 +02:00
|
|
|
use pmocache::{Cache as GenericCache, CacheConfig};
|
2025-10-07 08:33:06 +02:00
|
|
|
use crate::webp;
|
2025-10-12 21:39:20 +02:00
|
|
|
use crate::db::DB;
|
2025-10-07 08:33:06 +02:00
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Cache d'images avec conversion WebP et génération de variantes
|
|
|
|
|
///
|
|
|
|
|
/// Gère le téléchargement, la conversion en WebP, le stockage et la génération
|
|
|
|
|
/// de variantes de tailles pour les images de couvertures.
|
2025-10-07 08:33:06 +02:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Cache {
|
2025-10-12 21:39:20 +02:00
|
|
|
cache: GenericCache,
|
2025-10-07 08:33:06 +02:00
|
|
|
pub(crate) dir: PathBuf,
|
|
|
|
|
pub(crate) limit: usize,
|
2025-10-12 21:39:20 +02:00
|
|
|
pub db: Arc<DB>,
|
2025-10-07 08:33:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Cache {
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Crée un nouveau cache d'images
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
///
|
|
|
|
|
/// * `dir` - Répertoire de stockage du cache
|
|
|
|
|
/// * `limit` - Limite de taille du cache (nombre d'images)
|
|
|
|
|
///
|
|
|
|
|
/// # Exemple
|
|
|
|
|
///
|
|
|
|
|
/// ```rust,no_run
|
|
|
|
|
/// use pmocovers::Cache;
|
|
|
|
|
///
|
|
|
|
|
/// let cache = Cache::new("./cache", 1000).unwrap();
|
|
|
|
|
/// ```
|
2025-10-07 08:33:06 +02:00
|
|
|
pub fn new(dir: &str, limit: usize) -> Result<Self> {
|
2025-10-12 21:39:20 +02:00
|
|
|
let config = CacheConfig::new(dir, limit, "covers", "orig.webp");
|
|
|
|
|
let cache = GenericCache::new(config)?;
|
2025-10-07 08:33:06 +02:00
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
dir: PathBuf::from(dir),
|
|
|
|
|
limit,
|
2025-10-12 21:39:20 +02:00
|
|
|
db: Arc::clone(&cache.db),
|
|
|
|
|
cache,
|
2025-10-07 08:33:06 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Télécharge une image depuis une URL et l'ajoute au cache
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
///
|
|
|
|
|
/// * `url` - URL de l'image à télécharger
|
|
|
|
|
///
|
|
|
|
|
/// # Returns
|
|
|
|
|
///
|
|
|
|
|
/// La clé primaire (pk) de l'image dans le cache
|
2025-10-07 08:33:06 +02:00
|
|
|
pub async fn add_from_url(&self, url: &str) -> Result<String> {
|
|
|
|
|
let response = reqwest::get(url).await?;
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
return Err(anyhow!("Bad status: {}", response.status()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data = response.bytes().await?;
|
|
|
|
|
self.add(url, &data).await
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// S'assure qu'une image est présente dans le cache
|
|
|
|
|
///
|
|
|
|
|
/// Si l'image existe déjà, retourne sa clé. Sinon, la télécharge.
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
///
|
|
|
|
|
/// * `url` - URL de l'image
|
2025-10-07 08:33:06 +02:00
|
|
|
pub async fn ensure_from_url(&self, url: &str) -> Result<String> {
|
2025-10-12 21:39:20 +02:00
|
|
|
self.cache.ensure_from_url(url, None).await
|
2025-10-07 08:33:06 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Ajoute une image au cache avec conversion en WebP
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
///
|
|
|
|
|
/// * `url` - URL source de l'image
|
|
|
|
|
/// * `data` - Données brutes de l'image
|
2025-10-07 08:33:06 +02:00
|
|
|
pub async fn add(&self, url: &str, data: &[u8]) -> Result<String> {
|
2025-10-12 21:39:20 +02:00
|
|
|
let pk = pmocache::pk_from_url(url);
|
2025-10-07 08:33:06 +02:00
|
|
|
let orig_path = self.dir.join(format!("{}.orig.webp", pk));
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
// Vérifier si le fichier existe déjà
|
2025-10-07 08:33:06 +02:00
|
|
|
if !orig_path.exists() {
|
2025-10-12 21:39:20 +02:00
|
|
|
// Convertir l'image en WebP
|
2025-10-07 08:33:06 +02:00
|
|
|
let img = image::load_from_memory(data)?;
|
|
|
|
|
let webp_data = webp::encode_webp(&img)?;
|
|
|
|
|
tokio::fs::write(&orig_path, webp_data).await?;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
// Ajouter à la DB (sans collection pour les covers)
|
|
|
|
|
self.db.add(&pk, url, None)?;
|
2025-10-07 08:33:06 +02:00
|
|
|
Ok(pk)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Récupère le chemin d'une image dans le cache
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
///
|
|
|
|
|
/// * `pk` - Clé primaire de l'image
|
2025-10-07 08:33:06 +02:00
|
|
|
pub async fn get(&self, pk: &str) -> Result<PathBuf> {
|
2025-10-12 21:39:20 +02:00
|
|
|
self.cache.get(pk).await
|
2025-10-07 08:33:06 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Supprime tous les fichiers et entrées du cache
|
2025-10-07 08:33:06 +02:00
|
|
|
pub async fn purge(&self) -> Result<()> {
|
2025-10-12 21:39:20 +02:00
|
|
|
self.cache.purge().await
|
2025-10-07 08:33:06 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les images manquantes
|
2025-10-07 08:33:06 +02:00
|
|
|
pub async fn consolidate(&self) -> Result<()> {
|
2025-10-12 21:39:20 +02:00
|
|
|
// Récupérer la liste des entrées à traiter
|
2025-10-07 08:33:06 +02:00
|
|
|
let entries = self.db.get_all()?;
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
// Supprimer les entrées sans fichiers correspondants
|
2025-10-07 08:33:06 +02:00
|
|
|
for entry in entries {
|
|
|
|
|
let orig_path = self.dir.join(format!("{}.orig.webp", entry.pk));
|
|
|
|
|
if !orig_path.exists() {
|
|
|
|
|
match reqwest::get(&entry.source_url).await {
|
|
|
|
|
Ok(response) if response.status().is_success() => {
|
|
|
|
|
let data = response.bytes().await?;
|
|
|
|
|
self.add(&entry.source_url, &data).await?;
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
self.db.delete(&entry.pk)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
// Supprimer les fichiers sans entrées DB correspondantes
|
2025-10-07 08:33:06 +02:00
|
|
|
let mut dir_entries = tokio::fs::read_dir(&self.dir).await?;
|
|
|
|
|
while let Some(entry) = dir_entries.next_entry().await? {
|
|
|
|
|
let path = entry.path();
|
2025-10-12 21:39:20 +02:00
|
|
|
if path.is_file() && path != self.dir.join("cache.db") {
|
2025-10-07 08:33:06 +02:00
|
|
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
|
|
|
|
if file_name.ends_with(".orig.webp") {
|
|
|
|
|
let pk = file_name.trim_end_matches(".orig.webp");
|
|
|
|
|
if self.db.get(pk).is_err() {
|
|
|
|
|
tokio::fs::remove_file(path).await?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
/// Retourne le répertoire du cache
|
2025-10-07 08:33:06 +02:00
|
|
|
pub fn cache_dir(&self) -> String {
|
|
|
|
|
self.dir.to_string_lossy().to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|