From 8f043a4d808add2d636e5c46a8aa0dfa9098e184 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 29 Nov 2025 12:38:23 +0100 Subject: [PATCH] =?UTF-8?q?Gestion=20des=20=C3=A9v=C3=A8nements=20d'=C3=A9?= =?UTF-8?q?coute=20sur=20le=20cache.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pmoaudiocache/src/metadata_ext.rs | 5 ++ pmoaudiocache/src/openapi.rs | 7 ++ pmoaudiocache/src/streaming.rs | 11 ++- pmoaudiocache/src/track_metadata.rs | 13 +++ pmocache/src/cache.rs | 134 ++++++++++++++++++++++++++++ pmocache/src/download.rs | 64 +++++++++++-- pmocache/src/lib.rs | 2 +- pmocache/src/pmoserver_ext.rs | 26 +++++- pmocovers/src/cache.rs | 18 +++- pmocovers/src/config_ext.rs | 7 +- pmocovers/src/openapi.rs | 9 ++ pmocovers/src/webp.rs | 13 ++- 12 files changed, 289 insertions(+), 20 deletions(-) diff --git a/pmoaudiocache/src/metadata_ext.rs b/pmoaudiocache/src/metadata_ext.rs index 4c8b5a29..73e24cb2 100644 --- a/pmoaudiocache/src/metadata_ext.rs +++ b/pmoaudiocache/src/metadata_ext.rs @@ -11,6 +11,9 @@ use std::sync::Arc; use tokio::sync::RwLock; /// Fournit un accès direct à une implémentation `TrackMetadata` basée sur le cache. +/// +/// Utilise `AudioCacheTrackMetadata` comme backend, ce qui garantit que toutes +/// les lectures/écritures passent par la DB `pmocache` sans recharger le FLAC. pub trait AudioTrackMetadataExt { fn track_metadata(&self, pk: impl Into) -> Arc>; } @@ -27,6 +30,8 @@ impl AudioTrackMetadataExt for Arc> { /// Cette version remplace l'ancienne macro `define_metadata_properties!` qui /// accédait directement aux clés de la DB. Les méthodes restent asynchrones et /// retournent `Option` ; en cas d'erreur backend, elles lèvent `anyhow::Error`. +/// Utile dans les handlers HTTP ou les services qui n'ont besoin que de quelques +/// champs sans manipuler explicitement un `TrackMetadata`. pub trait AudioMetadataExt { async fn get_title(&self, pk: &str) -> anyhow::Result>; async fn get_artist(&self, pk: &str) -> anyhow::Result>; diff --git a/pmoaudiocache/src/openapi.rs b/pmoaudiocache/src/openapi.rs index ed8689e7..ef05a746 100644 --- a/pmoaudiocache/src/openapi.rs +++ b/pmoaudiocache/src/openapi.rs @@ -8,6 +8,13 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( paths( + pmocache::api::list_items::, + pmocache::api::get_item_info::, + pmocache::api::get_download_status::, + pmocache::api::add_item::, + pmocache::api::delete_item::, + pmocache::api::purge_cache::, + pmocache::api::consolidate_cache::, crate::api::get_cover_url, ), components( diff --git a/pmoaudiocache/src/streaming.rs b/pmoaudiocache/src/streaming.rs index e011c3bf..53d98779 100644 --- a/pmoaudiocache/src/streaming.rs +++ b/pmoaudiocache/src/streaming.rs @@ -12,7 +12,16 @@ use pmoflac::{transcode_to_flac_stream, AudioCodec, TranscodeOptions}; use serde_json::json; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -/// Creates the transformer consumed by the audio cache. +/// Crée le `StreamTransformer` utilisé par le cache audio. +/// +/// - Accepte n'importe quel codec pris en charge par `pmoflac` (FLAC, MP3, OGG/Vorbis, +/// Opus, WAV, AIFF). +/// - Convertit en FLAC en streaming (passthrough si entrée FLAC) et écrit dans le +/// fichier fourni par `pmocache`. +/// - Renseigne `TransformMetadata` (codec source, mode, SR, BPS, canaux, total_samples) +/// pour que `pmocache` puisse les stocker en DB. +/// +/// À utiliser comme factory dans `Cache::with_transformer`. pub fn create_streaming_flac_transformer() -> StreamTransformer { Box::new(|input, mut file, context| { Box::pin(async move { diff --git a/pmoaudiocache/src/track_metadata.rs b/pmoaudiocache/src/track_metadata.rs index e67f9705..5b55248e 100644 --- a/pmoaudiocache/src/track_metadata.rs +++ b/pmoaudiocache/src/track_metadata.rs @@ -8,12 +8,25 @@ fn map_db_err(err: rusqlite::Error) -> MetadataError { MetadataError::Backend(err.to_string()) } +/// Implémentation `TrackMetadata` adossée au cache audio. +/// +/// Cette couche lit/écrit directement dans la table `metadata` de `pmocache` +/// pour un `pk` donné. Elle se comporte comme une façade `TrackMetadata` +/// classique mais repose sur la DB du cache plutôt que sur un fichier taggé, +/// ce qui permet : +/// - d'exposer les métadonnées immédiatement après ingestion/transform ; +/// - de servir des lecteurs UPnP/DLNA sans relire le FLAC sur disque ; +/// - de persister les mises à jour d'un client (ex: renommer un titre). pub struct AudioCacheTrackMetadata { cache: Arc, pk: String, } impl AudioCacheTrackMetadata { + /// Construit un adaptateur `TrackMetadata` pour un `pk` du cache audio. + /// + /// Le type implémente ensuite toutes les méthodes du trait `pmometadata::TrackMetadata` + /// en stockant les données dans la base SQLite de `pmocache`. pub fn new(cache: Arc, pk: impl Into) -> Self { Self { cache, diff --git a/pmocache/src/cache.rs b/pmocache/src/cache.rs index 8db3bc15..61a3b140 100755 --- a/pmocache/src/cache.rs +++ b/pmocache/src/cache.rs @@ -12,11 +12,43 @@ use anyhow::{anyhow, bail, Result}; use serde_json::{Number, Value}; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::sync::RwLock; use tracing; +/// Informations transmises lors de la diffusion d'un élément du cache via HTTP. +/// +/// - Emis uniquement quand une réponse 2xx est renvoyée par les routes HTTP générées +/// (fichier complet, stream progressif ou variante générée). +/// - Inclut le qualifier utilisé pour la requête afin de distinguer `orig`, `stream`, etc. +/// - Peut être utilisé pour synchroniser des clients (ex: WebSocket) ou tracer les hits. +#[derive(Debug, Clone)] +pub struct CacheBroadcastEvent { + /// Identifiant unique du fichier servi. + pub pk: String, + /// Qualifier (paramètre de route) utilisé pour cette diffusion. + pub qualifier: String, + /// Nom logique du cache (`CacheConfig::cache_name`). + pub cache_name: &'static str, + /// Type du cache (`CacheConfig::cache_type`). + pub cache_type: &'static str, +} + +/// Handle retourné lors de l'abonnement à un évènement de diffusion. +/// +/// Conservez-le pour pouvoir vous désabonner explicitement via +/// [`Cache::unsubscribe_broadcast`]. Le couple `(pk, id)` identifie de manière +/// unique la callback enregistrée. +#[derive(Debug, Clone)] +pub struct CacheSubscription { + pub pk: String, + pub id: u64, +} + +type CacheServeCallback = Arc bool + Send + Sync>; + /// Taille minimale de prébuffering par défaut (512 KB = ~5 secondes de FLAC) pub const DEFAULT_PREBUFFER_SIZE: u64 = 512 * 1024; @@ -59,6 +91,10 @@ pub struct Cache { pub db: Arc, /// Map des downloads en cours (pk -> Download) downloads: Arc>>>, + /// Callback(s) à déclencher lorsqu'un élément est servi via HTTP (pk -> callbacks) + serve_subscribers: Arc>>>, + /// Générateur d'identifiants uniques pour les abonnements + subscriber_counter: AtomicU64, /// Factory pour créer des transformers (optionnel) transformer_factory: Option StreamTransformer + Send + Sync>>, /// Taille minimale de prébuffering en octets (0 = désactivé) @@ -314,6 +350,8 @@ impl Cache { limit, db: Arc::new(db), downloads: Arc::new(RwLock::new(HashMap::new())), + serve_subscribers: Arc::new(RwLock::new(HashMap::new())), + subscriber_counter: AtomicU64::new(1), transformer_factory, min_prebuffer_size: DEFAULT_PREBUFFER_SIZE, _phantom: std::marker::PhantomData, @@ -348,6 +386,102 @@ impl Cache { self.min_prebuffer_size } + /// S'abonne aux diffusions HTTP pour un `pk` donné. + /// + /// La callback est appelée à chaque fois qu'un élément est servi avec succès via les routes + /// HTTP du cache. Si la callback retourne `false`, elle est automatiquement désinscrite ; + /// retourner `true` permet de rester abonné aux diffusions suivantes. + /// + /// Retourne un [`CacheSubscription`] à conserver pour se désabonner explicitement via + /// [`Cache::unsubscribe_broadcast`]. + /// + /// # Exemple + /// + /// ```rust,no_run + /// use pmocache::{Cache, CacheConfig, CacheSubscription}; + /// use std::sync::Arc; + /// + /// struct MyConfig; + /// impl CacheConfig for MyConfig { + /// fn file_extension() -> &'static str { "dat" } + /// } + /// + /// # async fn demo() -> anyhow::Result<()> { + /// let cache = Arc::new(Cache::::new("/tmp/cache", 100)?); + /// let token: CacheSubscription = cache + /// .subscribe_broadcast("abc123", |event| { + /// println!("{} served with param {}", event.pk, event.qualifier); + /// // Retourner true pour rester abonné + /// true + /// }) + /// .await; + /// + /// // ... plus tard, pour se désabonner explicitement : + /// cache.unsubscribe_broadcast(&token).await; + /// # Ok(()) + /// # } + /// ``` + pub async fn subscribe_broadcast( + &self, + pk: impl Into, + callback: F, + ) -> CacheSubscription + where + F: Fn(&CacheBroadcastEvent) -> bool + Send + Sync + 'static, + { + let pk = pk.into(); + let id = self.subscriber_counter.fetch_add(1, Ordering::Relaxed); + let mut subscribers = self.serve_subscribers.write().await; + subscribers + .entry(pk.clone()) + .or_default() + .push((id, Arc::new(callback))); + + CacheSubscription { pk, id } + } + + /// Désabonne une callback précédemment enregistrée via [`Cache::subscribe_broadcast`]. + /// + /// N'a aucun effet si le token est inconnu ou déjà désinscrit. + pub async fn unsubscribe_broadcast(&self, token: &CacheSubscription) { + let mut subscribers = self.serve_subscribers.write().await; + if let Some(list) = subscribers.get_mut(&token.pk) { + list.retain(|(id, _)| *id != token.id); + if list.is_empty() { + subscribers.remove(&token.pk); + } + } + } + + /// Notifie les abonnés qu'un élément du cache a été diffusé via HTTP. + /// + /// Interne au crate : les routes Axum appellent cette méthode lorsqu'une réponse 2xx est + /// renvoyée. Les callbacks qui retournent `false` sont retirées. + pub(crate) async fn notify_broadcast(&self, pk: &str, qualifier: &str) { + let mut subscribers = self.serve_subscribers.write().await; + if let Some(callbacks) = subscribers.get_mut(pk) { + let event = CacheBroadcastEvent { + pk: pk.to_string(), + qualifier: qualifier.to_string(), + cache_name: C::cache_name(), + cache_type: C::cache_type(), + }; + + let mut to_keep = Vec::new(); + for (id, callback) in callbacks.drain(..) { + if callback(&event) { + to_keep.push((id, callback)); + } + } + + if to_keep.is_empty() { + subscribers.remove(pk); + } else { + callbacks.extend(to_keep); + } + } + } + /// Télécharge un fichier depuis une URL et l'ajoute au cache /// /// Cette méthode utilise un système d'identifiants basé sur le contenu plutôt que sur l'URL. diff --git a/pmocache/src/download.rs b/pmocache/src/download.rs index 9e58efa7..2e73c60d 100644 --- a/pmocache/src/download.rs +++ b/pmocache/src/download.rs @@ -32,6 +32,11 @@ pub type TransformContextHandle = Arc; type ByteStream = Pin> + Send>>; /// Source générique (HTTP ou lecteur) exposée aux transformers. +/// +/// `CacheInput` masque l'origine des données pour les transformers (HTTP ou flux +/// applicatif). Il permet de consulter la taille attendue (`content_length`), +/// de récupérer l'intégralité du buffer (`bytes`) ou d'itérer en streaming +/// (`into_byte_stream`). pub struct CacheInput { inner: CacheInputInner, } @@ -50,6 +55,10 @@ enum CacheInputInner { } impl CacheInput { + /// Crée un `CacheInput` à partir d'une réponse HTTP (`reqwest::Response`). + /// + /// Conserve la longueur du contenu si elle est fournie par le serveur et + /// permet un accès ultérieur en streaming ou en mémoire. pub fn from_response(response: reqwest::Response) -> Self { let length = response.content_length(); Self { @@ -61,6 +70,10 @@ impl CacheInput { } } + /// Crée un `CacheInput` à partir d'un `AsyncRead` typé. + /// + /// La longueur peut être fournie si elle est connue, ce qui améliore la + /// mise à jour des métadonnées de progression. pub fn from_reader(reader: R, length: Option) -> Self where R: AsyncRead + Send + Unpin + 'static, @@ -68,6 +81,7 @@ impl CacheInput { Self::from_reader_box(Box::new(reader), length) } + /// Crée un `CacheInput` à partir d'un trait object `AsyncRead`. pub fn from_reader_box(reader: Box, length: Option) -> Self { Self { inner: CacheInputInner::Reader { @@ -78,6 +92,7 @@ impl CacheInput { } } + /// Retourne la taille du contenu si elle est connue (Content-Length ou buffer déjà lu). pub fn content_length(&self) -> Option { match &self.inner { CacheInputInner::Http { length, buffer, .. } => { @@ -127,6 +142,9 @@ impl CacheInput { } } + /// Retourne un flux d'octets (stream) consommable par les transformers. + /// + /// Si le contenu a déjà été lu en mémoire, le stream renverra ce buffer. pub fn into_byte_stream(self) -> ByteStream { match self.inner { CacheInputInner::Http { @@ -185,7 +203,12 @@ struct DownloadState { transform_metadata: Option, } -/// Objet représentant un téléchargement en cours +/// Objet représentant un téléchargement en cours. +/// +/// Expose la progression, les tailles attendues/transformées, l'état d'erreur +/// et les métadonnées de transformation éventuelles. Les méthodes sont sûres +/// côté concurrence et peuvent être utilisées depuis les routes HTTP pour +/// suivre l'état du cache progressif. #[derive(Debug)] pub struct Download { filename: PathBuf, @@ -208,10 +231,14 @@ impl Download { }) } + /// Chemin du fichier cible sur disque. pub fn filename(&self) -> &Path { &self.filename } + /// Attend que `transformed_size` atteigne au moins `min_size` (ou fin / erreur). + /// + /// Utile pour le prébuffering audio ou vidéo avant de démarrer un stream HTTP. pub async fn wait_until_min_size(&self, min_size: u64) -> Result<(), String> { loop { let state = self.state.read().await; @@ -226,6 +253,7 @@ impl Download { } } + /// Attend la fin complète du téléchargement ou renvoie l'erreur rencontrée. pub async fn wait_until_finished(&self) -> Result<(), String> { loop { let state = self.state.read().await; @@ -240,62 +268,81 @@ impl Download { } } + /// Ouvre le fichier associé pour lecture (bloquant standard). pub fn open(&self) -> io::Result { File::open(&self.filename) } + /// Dernière position lue (tracking pour lecture progressive). pub async fn pos(&self) -> u64 { let state = self.state.read().await; state.read_position } + /// Met à jour la position lue (utile pour les streamers progressifs). pub async fn set_pos(&self, pos: u64) { let mut state = self.state.write().await; state.read_position = pos; } + /// Taille attendue du flux source (Content-Length ou renseignée par l'appelant). pub async fn expected_size(&self) -> Option { let state = self.state.read().await; state.expected_size } + /// Nombre d'octets effectivement téléchargés (source). pub async fn current_size(&self) -> u64 { let state = self.state.read().await; state.current_size } + /// Nombre d'octets écrits après transformation (peut différer de `current_size`). pub async fn transformed_size(&self) -> u64 { let state = self.state.read().await; state.transformed_size } + /// Indique si le téléchargement est terminé (succès ou erreur). pub async fn finished(&self) -> bool { let state = self.state.read().await; state.finished } + /// Renvoie l'erreur rencontrée, le cas échéant. pub async fn error(&self) -> Option { let state = self.state.read().await; state.error.clone() } + /// Métadonnées renseignées par le transformer (codec, sample rate, etc.). pub async fn transform_metadata(&self) -> Option { let state = self.state.read().await; state.transform_metadata.clone() } } +/// Métadonnées techniques optionnelles remontées par un transformer. #[derive(Debug, Clone, Default)] pub struct TransformMetadata { + /// Mode ou preset utilisé (ex: "flac", "webp-80"). pub mode: Option, + /// Codec ou format en entrée. pub input_codec: Option, + /// Détails libres (ex: paramètres d'encodage). pub details: Option, + /// Fréquence d'échantillonnage en Hz. pub sample_rate: Option, + /// Profondeur de bits par échantillon. pub bits_per_sample: Option, + /// Nombre de canaux audio. pub channels: Option, + /// Nombre total d'échantillons (si connu). pub total_samples: Option, } +/// Contexte passé aux transformers pour signaler la progression et renseigner +/// des métadonnées de transformation. pub struct TransformContext { state: Arc>, progress_cb: Arc, @@ -306,17 +353,17 @@ impl TransformContext { Self { state, progress_cb } } - /// Reports progress (in bytes) to the download state. + /// Signale une progression (en octets transformés) au download. pub fn report_progress(&self, bytes: u64) { (self.progress_cb)(bytes); } - /// Returns the underlying progress callback (useful for piping into other APIs). + /// Retourne le callback de progression sous-jacent (utile pour le passer à d'autres APIs). pub fn progress_callback(&self) -> Arc { Arc::clone(&self.progress_cb) } - /// Stores metadata describing the transformation that occurred. + /// Stocke des métadonnées décrivant la transformation appliquée. pub async fn set_metadata(&self, metadata: TransformMetadata) { let mut state = self.state.write().await; state.transform_metadata = Some(metadata); @@ -329,6 +376,9 @@ pub fn download>(filename: P, url: &str) -> Arc { } /// Lance le téléchargement d'une URL avec transformation du stream. +/// +/// Le transformer reçoit le flux source, un handle de fichier déjà ouvert et un +/// [`TransformContext`] pour reporter la progression et les métadonnées. pub fn download_with_transformer>( filename: P, url: &str, @@ -337,7 +387,11 @@ pub fn download_with_transformer>( spawn_download(filename, DownloadSource::Url(url.to_string()), transformer) } -/// Ingère un flux (AsyncRead) dans le cache avec transformation optionnelle. +/// Ingère un flux (`AsyncRead`) dans le cache avec transformation optionnelle. +/// +/// Permet d'alimenter le cache depuis une source non-HTTP (ex: pipe interne, +/// fichier local, décodage amont) tout en conservant la même mécanique de +/// suivi de progression qu'un téléchargement classique. pub fn ingest_with_transformer( filename: P, reader: R, diff --git a/pmocache/src/lib.rs b/pmocache/src/lib.rs index 0c0843c4..7b02399c 100644 --- a/pmocache/src/lib.rs +++ b/pmocache/src/lib.rs @@ -129,7 +129,7 @@ pub mod openapi; #[cfg(feature = "pmoconfig")] pub mod config_ext; -pub use cache::{Cache, CacheConfig}; +pub use cache::{Cache, CacheBroadcastEvent, CacheConfig, CacheSubscription}; pub use cache_trait::{pk_from_content_header, FileCache}; pub use db::{CacheEntry, DB}; pub use download::{ diff --git a/pmocache/src/pmoserver_ext.rs b/pmocache/src/pmoserver_ext.rs index e6dab594..9d9fd5a6 100644 --- a/pmocache/src/pmoserver_ext.rs +++ b/pmocache/src/pmoserver_ext.rs @@ -124,13 +124,21 @@ async fn serve_file_with_streaming( param_generator: Option>, ) -> Response { let file_path = cache.get_file_path_with_qualifier(pk, param); + let qualifier = param.to_string(); // Si le fichier n'existe pas et qu'on a un générateur, l'utiliser if !file_path.exists() { if let Some(generator) = param_generator { if let Some(data) = generator(cache.clone(), pk.to_string(), param.to_string()).await { // Le générateur a créé les données, les servir directement - return (StatusCode::OK, [("content-type", content_type)], data).into_response(); + let response = + (StatusCode::OK, [("content-type", content_type)], data).into_response(); + + if response.status().is_success() { + cache.notify_broadcast(pk, &qualifier).await; + } + + return response; } } } @@ -145,12 +153,24 @@ async fn serve_file_with_streaming( // Le fichier est en cours de téléchargement if !download.finished().await { // Streaming progressif - return stream_file_progressive(file_path, download, content_type).await; + let response = stream_file_progressive(file_path, download, content_type).await; + + if response.status().is_success() { + cache.notify_broadcast(pk, &qualifier).await; + } + + return response; } } // Fichier terminé ou pas de download en cours, servir normalement - serve_complete_file(file_path, content_type).await + let response = serve_complete_file(file_path, content_type).await; + + if response.status().is_success() { + cache.notify_broadcast(pk, &qualifier).await; + } + + response } /// Stream un fichier en cours de téléchargement de manière progressive diff --git a/pmocovers/src/cache.rs b/pmocovers/src/cache.rs index c0b4c898..c6f2521b 100644 --- a/pmocovers/src/cache.rs +++ b/pmocovers/src/cache.rs @@ -7,7 +7,10 @@ use anyhow::Result; use pmocache::{CacheConfig, StreamTransformer}; use std::sync::Arc; -/// Configuration pour le cache de couvertures +/// Configuration pour le cache de couvertures. +/// +/// Spécifie l'extension finale (`webp`), le type logique exposé (`image`) et +/// le nom de cache (`covers`) utilisés par les routes générées par `pmocache`. pub struct CoversConfig; impl CacheConfig for CoversConfig { @@ -24,12 +27,15 @@ impl CacheConfig for CoversConfig { } } -/// Type alias pour le cache de couvertures avec conversion WebP +/// Type alias pour le cache de couvertures avec conversion WebP. pub type Cache = pmocache::Cache; -/// Créateur de transformer WebP +/// Créateur de transformer WebP. /// /// Convertit automatiquement toute image téléchargée en format WebP +/// avant de l'écrire sur disque. Les octets d'entrée sont lus en mémoire, +/// décodés via `image`, ré-encodés en WebP puis persistés. La progression +/// est reportée pour que le cache puisse suivre la taille transformée. fn create_webp_transformer() -> StreamTransformer { Box::new(|mut input, mut file, context| { Box::pin(async move { @@ -55,7 +61,7 @@ fn create_webp_transformer() -> StreamTransformer { }) } -/// Crée un cache de couvertures avec conversion WebP automatique +/// Crée un cache de couvertures avec conversion WebP automatique. /// /// # Arguments /// @@ -79,6 +85,10 @@ pub fn new_cache(dir: &str, limit: usize) -> Result { } /// Crée un cache de couvertures et lance une consolidation en arrière-plan. +/// +/// Idéal pour un démarrage de service : la consolidation supprime les fichiers +/// incomplets et recalcule les markers `.complete` au besoin avant d'accepter +/// des requêtes. pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result> { let cache = Arc::new(new_cache(dir, limit)?); let cache_clone = cache.clone(); diff --git a/pmocovers/src/config_ext.rs b/pmocovers/src/config_ext.rs index 7899057e..f5a98178 100644 --- a/pmocovers/src/config_ext.rs +++ b/pmocovers/src/config_ext.rs @@ -11,10 +11,11 @@ use std::sync::Arc; const DEFAULT_COVER_CACHE_DIR: &str = "cache_covers"; const DEFAULT_COVER_CACHE_SIZE: usize = 2000; -/// Trait d'extension pour gérer le cache de couvertures dans pmoconfig +/// Trait d'extension pour gérer le cache de couvertures dans pmoconfig. /// -/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques -/// au cache de couvertures avec conversion WebP. +/// Fournit des helpers pour récupérer/définir le répertoire et la taille du +/// cache de couvertures, ainsi qu'une factory `create_cover_cache` prête à +/// l'emploi (conversion WebP activée). /// /// # Exemple /// diff --git a/pmocovers/src/openapi.rs b/pmocovers/src/openapi.rs index e4f1b675..88ef942b 100644 --- a/pmocovers/src/openapi.rs +++ b/pmocovers/src/openapi.rs @@ -10,6 +10,15 @@ use utoipa::OpenApi; /// L'API réutilise les handlers génériques de pmocache. #[derive(OpenApi)] #[openapi( + paths( + pmocache::api::list_items::, + pmocache::api::get_item_info::, + pmocache::api::get_download_status::, + pmocache::api::add_item::, + pmocache::api::delete_item::, + pmocache::api::purge_cache::, + pmocache::api::consolidate_cache::, + ), components( schemas( pmocache::CacheEntry, diff --git a/pmocovers/src/webp.rs b/pmocovers/src/webp.rs index c6a246b0..c6cc51c8 100644 --- a/pmocovers/src/webp.rs +++ b/pmocovers/src/webp.rs @@ -2,7 +2,10 @@ use anyhow::Result; use image::{imageops::FilterType, DynamicImage}; use webp::{Encoder, WebPMemory}; -/// Encode une image en format WebP avec un niveau de qualité de 85% +/// Encode une image en format WebP avec un niveau de qualité de 85%. +/// +/// Utilise l'encodeur `webp` et retourne les octets encodés prêts à être +/// écrits sur disque ou envoyés sur le réseau. /// /// # Arguments /// @@ -28,7 +31,7 @@ pub fn encode_webp(img: &DynamicImage) -> Result> { Ok(webp_data.to_vec()) } -/// Redimensionne une image pour l'inscrire dans un carré de taille donnée +/// Redimensionne une image pour l'inscrire dans un carré de taille donnée. /// /// Cette fonction préserve le ratio d'aspect de l'image originale en la redimensionnant /// pour qu'elle tienne dans un carré, puis la centre sur un fond transparent. @@ -82,7 +85,11 @@ pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage { square } -/// Génère une variante redimensionnée d'une image en cache +/// Génère une variante redimensionnée d'une image en cache. +/// +/// Repose sur le fichier original (`orig`) du cache, applique `ensure_square`, +/// encode en WebP et persiste la variante `{pk}.{size}.webp` pour éviter les +/// recalculs sur les requêtes suivantes. /// /// Cette fonction crée (ou récupère si déjà existante) une variante redimensionnée /// d'une image. La variante est mise en cache sur disque pour éviter les