Gestion des évènements d'écoute sur le cache.

This commit is contained in:
2025-11-29 12:38:23 +01:00
parent 6ae06b38e1
commit 8f043a4d80
12 changed files with 289 additions and 20 deletions

View File

@@ -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<String>) -> Arc<RwLock<dyn TrackMetadata>>;
}
@@ -27,6 +30,8 @@ impl AudioTrackMetadataExt for Arc<pmocache::Cache<AudioConfig>> {
/// 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<Option<String>>;
async fn get_artist(&self, pk: &str) -> anyhow::Result<Option<String>>;

View File

@@ -8,6 +8,13 @@ use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
pmocache::api::list_items::<crate::cache::AudioConfig>,
pmocache::api::get_item_info::<crate::cache::AudioConfig>,
pmocache::api::get_download_status::<crate::cache::AudioConfig>,
pmocache::api::add_item::<crate::cache::AudioConfig>,
pmocache::api::delete_item::<crate::cache::AudioConfig>,
pmocache::api::purge_cache::<crate::cache::AudioConfig>,
pmocache::api::consolidate_cache::<crate::cache::AudioConfig>,
crate::api::get_cover_url,
),
components(

View File

@@ -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 {

View File

@@ -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<crate::Cache>,
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<crate::Cache>, pk: impl Into<String>) -> Self {
Self {
cache,

View File

@@ -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<dyn Fn(&CacheBroadcastEvent) -> 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<C: CacheConfig> {
pub db: Arc<DB>,
/// Map des downloads en cours (pk -> Download)
downloads: Arc<RwLock<HashMap<String, Arc<Download>>>>,
/// Callback(s) à déclencher lorsqu'un élément est servi via HTTP (pk -> callbacks)
serve_subscribers: Arc<RwLock<HashMap<String, Vec<(u64, CacheServeCallback)>>>>,
/// Générateur d'identifiants uniques pour les abonnements
subscriber_counter: AtomicU64,
/// Factory pour créer des transformers (optionnel)
transformer_factory: Option<Arc<dyn Fn() -> StreamTransformer + Send + Sync>>,
/// Taille minimale de prébuffering en octets (0 = désactivé)
@@ -314,6 +350,8 @@ impl<C: CacheConfig> Cache<C> {
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<C: CacheConfig> Cache<C> {
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::<MyConfig>::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<F>(
&self,
pk: impl Into<String>,
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.

View File

@@ -32,6 +32,11 @@ pub type TransformContextHandle = Arc<TransformContext>;
type ByteStream = Pin<Box<dyn Stream<Item = Result<Bytes, String>> + 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<R>(reader: R, length: Option<u64>) -> 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<dyn AsyncRead + Send + Unpin>, length: Option<u64>) -> 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<u64> {
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<TransformMetadata>,
}
/// 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> {
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<u64> {
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<String> {
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<TransformMetadata> {
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<String>,
/// Codec ou format en entrée.
pub input_codec: Option<String>,
/// Détails libres (ex: paramètres d'encodage).
pub details: Option<String>,
/// Fréquence d'échantillonnage en Hz.
pub sample_rate: Option<u32>,
/// Profondeur de bits par échantillon.
pub bits_per_sample: Option<u8>,
/// Nombre de canaux audio.
pub channels: Option<u8>,
/// Nombre total d'échantillons (si connu).
pub total_samples: Option<u64>,
}
/// Contexte passé aux transformers pour signaler la progression et renseigner
/// des métadonnées de transformation.
pub struct TransformContext {
state: Arc<RwLock<DownloadState>>,
progress_cb: Arc<dyn Fn(u64) + Send + Sync>,
@@ -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<dyn Fn(u64) + Send + Sync> {
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<P: AsRef<Path>>(filename: P, url: &str) -> Arc<Download> {
}
/// 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<P: AsRef<Path>>(
filename: P,
url: &str,
@@ -337,7 +387,11 @@ pub fn download_with_transformer<P: AsRef<Path>>(
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<P, R>(
filename: P,
reader: R,

View File

@@ -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::{

View File

@@ -124,13 +124,21 @@ async fn serve_file_with_streaming<C: CacheConfig>(
param_generator: Option<ParamGenerator<C>>,
) -> 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<C: CacheConfig>(
// 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

View File

@@ -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<CoversConfig>;
/// 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<Cache> {
}
/// 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<Arc<Cache>> {
let cache = Arc::new(new_cache(dir, limit)?);
let cache_clone = cache.clone();

View File

@@ -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
///

View File

@@ -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::<crate::cache::CoversConfig>,
pmocache::api::get_item_info::<crate::cache::CoversConfig>,
pmocache::api::get_download_status::<crate::cache::CoversConfig>,
pmocache::api::add_item::<crate::cache::CoversConfig>,
pmocache::api::delete_item::<crate::cache::CoversConfig>,
pmocache::api::purge_cache::<crate::cache::CoversConfig>,
pmocache::api::consolidate_cache::<crate::cache::CoversConfig>,
),
components(
schemas(
pmocache::CacheEntry,

View File

@@ -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<Vec<u8>> {
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