Gestion des évènements d'écoute sur le cache.
This commit is contained in:
@@ -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>>;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user