adaptation de la crate pmocovers

This commit is contained in:
2025-10-17 22:15:00 +02:00
parent fdd6cb6401
commit 2e4b69e758
9 changed files with 450 additions and 875 deletions

View File

@@ -3,62 +3,24 @@
//! Cette crate fournit un système de cache d'images optimisé pour les couvertures d'albums,
//! avec conversion automatique en WebP et génération de variantes de tailles.
//!
//! ## Vue d'ensemble
//! ## Fonctionnalités
//!
//! `pmocovers` gère le téléchargement, la conversion, le stockage et la distribution
//! d'images de couvertures d'albums, avec :
//! - Conversion automatique en WebP pour réduire la taille
//! - Génération de variantes de tailles à la demande
//! - Cache persistant avec base de données SQLite
//! - API HTTP pour récupérer les images
//!
//! ## Fonctionnalités
//!
//! ### 📦 Gestion du cache
//! - Téléchargement automatique depuis des URLs
//! - Conversion des images en WebP (format optimisé)
//! - Stockage persistant sur disque
//! - Base de données SQLite pour le tracking
//!
//! ### 🎨 Génération de variantes
//! - Redimensionnement automatique à la demande
//! - Création d'images carrées avec centrage
//! - Cache des variantes générées
//! - Support de multiples tailles
//!
//! ### 📊 Statistiques d'utilisation
//! - Comptage des accès (hits)
//! - Suivi de la dernière utilisation
//! - API de statistiques complètes
//! - API HTTP complète (fournie par `pmocache`)
//!
//! ## Architecture
//!
//! `pmocovers` suit le pattern d'extension des autres crates PMO :
//! `pmocovers` est une spécialisation minimale de `pmocache` qui ajoute :
//! 1. La conversion WebP automatique lors du téléchargement (via transformer)
//! 2. La génération de variantes redimensionnées à la demande (via param generator)
//!
//! - `pmoserver` définit un serveur HTTP générique
//! - `pmocovers` étend ce serveur avec des méthodes de cache via un trait
//! - Le serveur n'a pas besoin de connaître `pmocovers`
//!
//! ## Structure des fichiers
//!
//! ```text
//! pmocovers/
//! ├── Cargo.toml
//! ├── src/
//! │ ├── lib.rs # Module principal (ce fichier)
//! │ ├── cache.rs # Gestion du cache
//! │ ├── db.rs # Base de données SQLite
//! │ ├── webp.rs # Conversion et redimensionnement WebP
//! │ └── pmoserver_impl.rs # Extension de pmoserver::Server
//! └── cache/ # Répertoire de cache (généré)
//! ├── cache.db # Base SQLite
//! ├── *.orig.webp # Images originales
//! └── *.{size}.webp # Variantes de tailles
//! ```
//! Tout le reste (API REST, serveur de fichiers, DB) est fourni par `pmocache`.
//!
//! ## Utilisation
//!
//! ### Exemple basique avec configuration automatique
//! ### Exemple avec configuration automatique
//!
//! ```rust,no_run
//! use pmocovers::CoverCacheExt;
@@ -67,161 +29,56 @@
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! let mut server = ServerBuilder::new_configured().build();
//!
//! // Utilise automatiquement la config (pmoconfig)
//! server.init_cover_cache_configured().await?;
//!
//! server.start().await;
//! server.wait().await;
//! Ok(())
//! }
//! ```
//!
//! ### Exemple avec paramètres personnalisés
//!
//! ```rust,no_run
//! use pmocovers::CoverCacheExt;
//! use pmoserver::ServerBuilder;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! let mut server = ServerBuilder::new("MyApp", "http://localhost:3000", 3000).build();
//!
//! // Paramètres personnalisés
//! server.init_cover_cache("./cache", 1000).await?;
//!
//! server.start().await;
//! server.wait().await;
//! Ok(())
//! }
//! ```
//!
//! ### Utilisation du cache directement
//!
//! ```rust,no_run
//! use pmocovers::Cache;
//! use pmocache::FileCache;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! let cache = Cache::new("./cache", 1000, "http://localhost:8080")?;
//!
//! // Ajouter une image depuis une URL (avec conversion WebP automatique)
//! let pk = cache.add_from_url("http://example.com/cover.jpg", None).await?;
//! println!("Image ajoutée avec clé: {}", pk);
//!
//! // Récupérer l'image originale
//! let path = cache.get(&pk).await?;
//! println!("Image stockée à: {:?}", path);
//!
//! Ok(())
//! }
//! ```
//!
//! ## API HTTP
//!
//! Une fois enregistré sur un serveur via `CoverCacheExt`, les endpoints suivants sont disponibles :
//!
//! ### GET /covers/images/{pk}
//! Récupère l'image originale en WebP
//!
//! ### GET /covers/images/{pk}/{size}
//! Récupère une variante de taille spécifique (ex: `/covers/images/abc123/256`)
//!
//! ### GET /covers/stats
//! Récupère les statistiques du cache (JSON)
//!
//! ## Format des clés (pk)
//!
//! Les images sont identifiées par une clé (pk) dérivée de l'URL source :
//! - Hash SHA1 de l'URL
//! - Encodé en hexadécimal (8 premiers octets)
//! - Exemple: `"1a2b3c4d5e6f7a8b"`
//!
//! ## Stockage
//!
//! Les fichiers sont organisés comme suit :
//!
//! ```text
//! cache/
//! ├── cache.db # Base SQLite
//! ├── 1a2b3c4d.orig.webp # Image originale
//! ├── 1a2b3c4d.256.webp # Variante 256x256
//! └── 1a2b3c4d.512.webp # Variante 512x512
//! ```
//!
//! ## Opérations de maintenance
//!
//! ### Purge du cache
//!
//! ```rust,no_run
//! # use pmocovers::Cache;
//! # async fn example(cache: &Cache) -> anyhow::Result<()> {
//! // Supprimer tous les fichiers et entrées DB
//! cache.purge().await?;
//! # Ok(())
//! # }
//! ```
//!
//! ### Consolidation du cache
//!
//! ```rust,no_run
//! # use pmocovers::Cache;
//! # async fn example(cache: &Cache) -> anyhow::Result<()> {
//! // Re-télécharger les images manquantes et supprimer les orphelins
//! cache.consolidate().await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Dépendances principales
//!
//! - `image` : Chargement et manipulation d'images
//! - `webp` : Encodage WebP
//! - `rusqlite` : Base de données SQLite
//! - `reqwest` : Téléchargement HTTP
//! - `sha1` : Génération de clés
//!
//! ## Voir aussi
//!
//! - [`pmoserver`] : Serveur HTTP Axum
//! - [`pmoapp`] : Application web frontend
//! - [`pmoupnp`] : Bibliothèque UPnP MediaRenderer
pub mod cache;
pub mod db;
pub mod webp;
#[cfg(feature = "pmoserver")]
pub mod api;
#[cfg(feature = "pmoserver")]
pub mod openapi;
pub use cache::{Cache, CoversConfig};
pub use db::{CacheEntry, DB};
pub use cache::{Cache, CoversConfig, new_cache};
#[cfg(feature = "pmoserver")]
pub use openapi::ApiDoc;
use anyhow::Result;
#[cfg(feature = "pmoserver")]
use utoipa::OpenApi;
#[cfg(feature = "pmoserver")]
use std::sync::Arc;
/// Trait pour étendre un serveur HTTP avec des fonctionnalités de cache d'images.
/// Générateur de variantes d'images
///
/// Ce trait permet à `pmocovers` d'ajouter des méthodes d'extension sur des types
/// de serveurs externes (comme `pmoserver::Server`) sans que ces crates dépendent de `pmocovers`.
///
/// # Architecture
///
/// Similaire au pattern utilisé par `pmoapp` pour `WebAppExt`, ce trait permet
/// une extension propre et découplée :
///
/// - `pmoserver` définit un serveur HTTP générique
/// - `pmocovers` étend ce serveur avec des méthodes de cache via ce trait
/// - Le serveur n'a pas besoin de connaître `pmocovers`
/// Si param est numérique, génère une variante redimensionnée
#[cfg(feature = "pmoserver")]
fn create_variant_generator() -> pmocache::pmoserver_ext::ParamGenerator<CoversConfig> {
Arc::new(|cache, pk, param| {
Box::pin(async move {
// Si le param est numérique, c'est une taille de variante
if let Ok(size) = param.parse::<usize>() {
match webp::generate_variant(&cache, &pk, size).await {
Ok(data) => return Some(data),
Err(e) => {
tracing::warn!("Cannot generate variant {}x{} for {}: {}", size, size, pk, e);
return None;
}
}
}
// Param non reconnu
None
})
})
}
/// Trait d'extension pour ajouter le cache de couvertures à pmoserver
#[cfg(feature = "pmoserver")]
pub trait CoverCacheExt {
/// Initialise le cache d'images et enregistre les routes HTTP.
/// Initialise le cache d'images et enregistre les routes HTTP
///
/// # Arguments
///
@@ -230,49 +87,59 @@ pub trait CoverCacheExt {
///
/// # Returns
///
/// * `Arc<Cache>` - Instance partagée du cache
/// Instance partagée du cache
///
/// # Routes enregistrées
///
/// - `GET /covers/images/{pk}` - Image originale
/// - `GET /covers/images/{pk}/{size}` - Variante de taille
/// - `GET /covers/stats` - Statistiques
/// - `GET /covers/image/{pk}` - Image originale
/// - `GET /covers/image/{pk}/{size}` - Variante de taille (ex: 256, 512)
/// - `GET /api/covers` - Liste des images (API REST)
/// - `POST /api/covers` - Ajouter une image (API REST)
/// - `DELETE /api/covers/{pk}` - Supprimer une image (API REST)
/// - `GET /swagger-ui` - Documentation interactive
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize) -> Result<Arc<Cache>>;
/// - `GET /api/covers/{pk}/status` - Statut du téléchargement
/// - `GET /swagger-ui/covers` - Documentation interactive
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize)
-> anyhow::Result<Arc<Cache>>;
/// Initialise le cache d'images avec la configuration par défaut.
/// Initialise le cache d'images avec la configuration par défaut
///
/// Utilise automatiquement les paramètres de `pmoconfig::Config` :
/// - `host.cover_cache.directory` pour le répertoire
/// - `host.cover_cache.size` pour la limite de taille
///
/// # Returns
///
/// * `Arc<Cache>` - Instance partagée du cache
///
/// # Exemple
///
/// ```rust,no_run
/// use pmocovers::CoverCacheExt;
/// use pmoserver::ServerBuilder;
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
/// let mut server = ServerBuilder::new_configured().build();
///
/// // Utilise automatiquement la config
/// server.init_cover_cache_configured().await?;
///
/// server.start().await;
/// Ok(())
/// }
/// ```
async fn init_cover_cache_configured(&mut self) -> Result<Arc<Cache>>;
/// Utilise automatiquement les paramètres de `pmoconfig::Config`
async fn init_cover_cache_configured(&mut self)
-> anyhow::Result<Arc<Cache>>;
}
// Implémentation du trait pour pmoserver::Server (feature-gated)
#[cfg(feature = "pmoserver")]
mod pmoserver_impl;
impl CoverCacheExt for pmoserver::Server {
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize)
-> anyhow::Result<Arc<Cache>> {
use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router};
let base_url = self.info().base_url;
let cache = Arc::new(cache::new_cache(cache_dir, limit, &base_url)?);
// Router de fichiers avec génération de variantes
// Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size}
let file_router = create_file_router_with_generator(
cache.clone(),
"image/webp",
Some(create_variant_generator())
);
self.add_router("/", file_router).await;
// API REST générique (pmocache)
// Routes: GET/POST/DELETE /api/covers, etc.
let api_router = create_api_router(cache.clone());
let openapi = crate::ApiDoc::openapi();
self.add_openapi(api_router, openapi, "covers").await;
Ok(cache)
}
async fn init_cover_cache_configured(&mut self)
-> anyhow::Result<Arc<Cache>> {
let config = pmoconfig::get_config();
let cache_dir = config.get_cover_cache_dir()?;
let limit = config.get_cover_cache_size()?;
self.init_cover_cache(&cache_dir, limit).await
}
}