Corrections mineurs des systèmes de caches
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3371,6 +3371,7 @@ dependencies = [
|
|||||||
"pmoserver",
|
"pmoserver",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -111,18 +111,7 @@ async fn persist_transform_streaminfo(cache: Arc<Cache>, pk: &str, tmeta: &Trans
|
|||||||
/// ```
|
/// ```
|
||||||
pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result<Arc<Cache>> {
|
pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result<Arc<Cache>> {
|
||||||
let cache = Arc::new(new_cache(dir, limit)?);
|
let cache = Arc::new(new_cache(dir, limit)?);
|
||||||
|
Ok(pmocache::Cache::with_consolidation(cache).await)
|
||||||
// Lancer la consolidation en arrière-plan pour nettoyer les fichiers incomplets
|
|
||||||
let cache_clone = cache.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = cache_clone.consolidate().await {
|
|
||||||
tracing::warn!("Failed to consolidate cache on startup: {}", e);
|
|
||||||
} else {
|
|
||||||
tracing::info!("Cache consolidated successfully on startup");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(cache)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ajoute un fichier audio local. Les FLAC sont référencés sans copie, les autres formats
|
/// Ajoute un fichier audio local. Les FLAC sont référencés sans copie, les autres formats
|
||||||
|
|||||||
@@ -83,9 +83,15 @@
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod metadata_ext;
|
pub mod metadata_ext;
|
||||||
pub mod streaming;
|
|
||||||
pub mod track_metadata;
|
pub mod track_metadata;
|
||||||
|
|
||||||
|
/// Module public pour la création de transformers FLAC streaming
|
||||||
|
///
|
||||||
|
/// Ce module expose les fonctionnalités de conversion FLAC progressive
|
||||||
|
/// pour permettre aux utilisateurs de créer des transformers custom ou
|
||||||
|
/// de réutiliser les implémentations par défaut.
|
||||||
|
pub mod streaming;
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use utoipa::OpenApi;
|
|||||||
/// L'API réutilise les handlers génériques de pmocache.
|
/// L'API réutilise les handlers génériques de pmocache.
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::api::get_cover_url,
|
||||||
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
pmocache::CacheEntry,
|
pmocache::CacheEntry,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ pub struct ErrorResponse {
|
|||||||
/// Liste tous les items en cache avec leurs statistiques
|
/// Liste tous les items en cache avec leurs statistiques
|
||||||
///
|
///
|
||||||
/// Retourne la liste complète des entrées du cache triées par nombre d'accès décroissant.
|
/// Retourne la liste complète des entrées du cache triées par nombre d'accès décroissant.
|
||||||
pub async fn list_items<C: CacheConfig>(State(cache): State<Arc<Cache<C>>>) -> impl IntoResponse {
|
pub async fn list_items<C: CacheConfig + 'static>(State(cache): State<Arc<Cache<C>>>) -> impl IntoResponse {
|
||||||
match cache.db.get_all(true) {
|
match cache.db.get_all(true) {
|
||||||
Ok(entries) => (StatusCode::OK, Json(entries)).into_response(),
|
Ok(entries) => (StatusCode::OK, Json(entries)).into_response(),
|
||||||
Err(e) => (
|
Err(e) => (
|
||||||
@@ -132,7 +132,7 @@ pub async fn list_items<C: CacheConfig>(State(cache): State<Arc<Cache<C>>>) -> i
|
|||||||
/// Récupère les informations d'un item spécifique
|
/// Récupère les informations d'un item spécifique
|
||||||
///
|
///
|
||||||
/// Retourne les métadonnées d'un item identifié par sa clé (pk).
|
/// Retourne les métadonnées d'un item identifié par sa clé (pk).
|
||||||
pub async fn get_item_info<C: CacheConfig>(
|
pub async fn get_item_info<C: CacheConfig + 'static>(
|
||||||
State(cache): State<Arc<Cache<C>>>,
|
State(cache): State<Arc<Cache<C>>>,
|
||||||
Path(pk): Path<String>,
|
Path(pk): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
@@ -153,7 +153,7 @@ pub async fn get_item_info<C: CacheConfig>(
|
|||||||
///
|
///
|
||||||
/// Retourne le statut actuel du téléchargement (progression, tailles, erreurs).
|
/// Retourne le statut actuel du téléchargement (progression, tailles, erreurs).
|
||||||
/// Si le téléchargement est terminé, retourne les informations du fichier.
|
/// Si le téléchargement est terminé, retourne les informations du fichier.
|
||||||
pub async fn get_download_status<C: CacheConfig>(
|
pub async fn get_download_status<C: CacheConfig + 'static>(
|
||||||
State(cache): State<Arc<Cache<C>>>,
|
State(cache): State<Arc<Cache<C>>>,
|
||||||
Path(pk): Path<String>,
|
Path(pk): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
@@ -265,7 +265,7 @@ enum AddSource<'a> {
|
|||||||
///
|
///
|
||||||
/// Télécharge l'item depuis l'URL fournie et l'ajoute au cache.
|
/// Télécharge l'item depuis l'URL fournie et l'ajoute au cache.
|
||||||
/// Si l'item existe déjà, il est mis à jour.
|
/// Si l'item existe déjà, il est mis à jour.
|
||||||
pub async fn add_item<C: CacheConfig>(
|
pub async fn add_item<C: CacheConfig + 'static>(
|
||||||
State(cache): State<Arc<Cache<C>>>,
|
State(cache): State<Arc<Cache<C>>>,
|
||||||
Json(req): Json<AddItemRequest>,
|
Json(req): Json<AddItemRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
@@ -337,7 +337,7 @@ pub async fn add_item<C: CacheConfig>(
|
|||||||
/// Supprime un item du cache
|
/// Supprime un item du cache
|
||||||
///
|
///
|
||||||
/// Supprime l'item et toutes ses variantes du disque et de la base de données.
|
/// Supprime l'item et toutes ses variantes du disque et de la base de données.
|
||||||
pub async fn delete_item<C: CacheConfig>(
|
pub async fn delete_item<C: CacheConfig + 'static>(
|
||||||
State(cache): State<Arc<Cache<C>>>,
|
State(cache): State<Arc<Cache<C>>>,
|
||||||
Path(pk): Path<String>,
|
Path(pk): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
@@ -389,7 +389,7 @@ pub async fn delete_item<C: CacheConfig>(
|
|||||||
/// Purge complètement le cache
|
/// Purge complètement le cache
|
||||||
///
|
///
|
||||||
/// Supprime tous les items et vide la base de données. Opération irréversible.
|
/// Supprime tous les items et vide la base de données. Opération irréversible.
|
||||||
pub async fn purge_cache<C: CacheConfig>(State(cache): State<Arc<Cache<C>>>) -> impl IntoResponse {
|
pub async fn purge_cache<C: CacheConfig + 'static>(State(cache): State<Arc<Cache<C>>>) -> impl IntoResponse {
|
||||||
match cache.purge().await {
|
match cache.purge().await {
|
||||||
Ok(_) => (
|
Ok(_) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
@@ -413,7 +413,7 @@ pub async fn purge_cache<C: CacheConfig>(State(cache): State<Arc<Cache<C>>>) ->
|
|||||||
///
|
///
|
||||||
/// Re-télécharge les items manquants et supprime les fichiers orphelins.
|
/// Re-télécharge les items manquants et supprime les fichiers orphelins.
|
||||||
/// Utile pour réparer un cache corrompu.
|
/// Utile pour réparer un cache corrompu.
|
||||||
pub async fn consolidate_cache<C: CacheConfig>(
|
pub async fn consolidate_cache<C: CacheConfig + 'static>(
|
||||||
State(cache): State<Arc<Cache<C>>>,
|
State(cache): State<Arc<Cache<C>>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match cache.consolidate().await {
|
match cache.consolidate().await {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ pub struct Cache<C: CacheConfig> {
|
|||||||
_phantom: std::marker::PhantomData<C>,
|
_phantom: std::marker::PhantomData<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: CacheConfig> Cache<C> {
|
impl<C: CacheConfig + 'static> Cache<C> {
|
||||||
/// Retourne le chemin du fichier marker de complétion
|
/// Retourne le chemin du fichier marker de complétion
|
||||||
fn get_completion_marker_path(&self, pk: &str) -> PathBuf {
|
fn get_completion_marker_path(&self, pk: &str) -> PathBuf {
|
||||||
self.get_file_path(pk)
|
self.get_file_path(pk)
|
||||||
@@ -424,6 +424,58 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lance une consolidation en arrière-plan pour un cache existant
|
||||||
|
///
|
||||||
|
/// Cette fonction utilitaire lance une tâche asynchrone qui consolide le cache
|
||||||
|
/// (supprime les fichiers incomplets sans marker de complétion) et retourne
|
||||||
|
/// immédiatement le cache fourni en paramètre.
|
||||||
|
///
|
||||||
|
/// Idéal pour les crates spécialisées qui veulent offrir une fonction
|
||||||
|
/// `new_cache_with_consolidation` sans dupliquer la logique de lancement.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cache` - Instance du cache à consolider
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Le même `Arc<Cache<C>>` fourni en paramètre
|
||||||
|
///
|
||||||
|
/// # Exemple
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// use pmocache::{Cache, CacheConfig};
|
||||||
|
/// use std::sync::Arc;
|
||||||
|
///
|
||||||
|
/// struct MyConfig;
|
||||||
|
/// impl CacheConfig for MyConfig {
|
||||||
|
/// fn file_extension() -> &'static str { "dat" }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn create_cache_with_cleanup() -> anyhow::Result<Arc<Cache<MyConfig>>> {
|
||||||
|
/// let cache = Arc::new(Cache::new("./cache", 1000)?);
|
||||||
|
/// Ok(Cache::with_consolidation(cache).await)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub async fn with_consolidation(cache: Arc<Cache<C>>) -> Arc<Cache<C>> {
|
||||||
|
let cache_clone = cache.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = cache_clone.consolidate().await {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to consolidate {} cache on startup: {}",
|
||||||
|
C::cache_name(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
"{} cache consolidated successfully on startup",
|
||||||
|
C::cache_name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cache
|
||||||
|
}
|
||||||
|
|
||||||
/// Configure la taille minimale de prébuffering
|
/// Configure la taille minimale de prébuffering
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -1559,7 +1611,7 @@ fn link_file(source: &Path, destination: &Path) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Implémentation du trait FileCache pour Cache
|
/// Implémentation du trait FileCache pour Cache
|
||||||
impl<C: CacheConfig> FileCache<C> for Cache<C> {
|
impl<C: CacheConfig + 'static> FileCache<C> for Cache<C> {
|
||||||
fn get_cache_dir(&self) -> &Path {
|
fn get_cache_dir(&self) -> &Path {
|
||||||
self.cache_dir()
|
self.cache_dir()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ pub trait CacheConfigExt {
|
|||||||
/// let config = get_config();
|
/// let config = get_config();
|
||||||
/// let cache = config.create_cache::<AudioConfig>("audio_cache", "cache_audio", 500)?;
|
/// let cache = config.create_cache::<AudioConfig>("audio_cache", "cache_audio", 500)?;
|
||||||
/// ```
|
/// ```
|
||||||
fn create_cache<C: crate::CacheConfig>(
|
fn create_cache<C: crate::CacheConfig + 'static>(
|
||||||
&self,
|
&self,
|
||||||
cache_type: &str,
|
cache_type: &str,
|
||||||
default_dir: &str,
|
default_dir: &str,
|
||||||
@@ -121,7 +121,7 @@ impl CacheConfigExt for Config {
|
|||||||
self.set_value(&["host", cache_type, "size"], Value::Number(n))
|
self.set_value(&["host", cache_type, "size"], Value::Number(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_cache<C: crate::CacheConfig>(
|
fn create_cache<C: crate::CacheConfig + 'static>(
|
||||||
&self,
|
&self,
|
||||||
cache_type: &str,
|
cache_type: &str,
|
||||||
default_dir: &str,
|
default_dir: &str,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ async fn get_file_with_param<C: CacheConfig + 'static>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
async fn serve_finalized_pk<C: CacheConfig>(
|
async fn serve_finalized_pk<C: CacheConfig + 'static>(
|
||||||
cache: &Arc<Cache<C>>,
|
cache: &Arc<Cache<C>>,
|
||||||
pk: &str,
|
pk: &str,
|
||||||
param: &str,
|
param: &str,
|
||||||
@@ -143,7 +143,7 @@ async fn serve_finalized_pk<C: CacheConfig>(
|
|||||||
/// 5. Broadcast l'event pour PK switching
|
/// 5. Broadcast l'event pour PK switching
|
||||||
/// 6. Sert directement le fichier téléchargé
|
/// 6. Sert directement le fichier téléchargé
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
async fn serve_lazy_audio_file<C: CacheConfig>(
|
async fn serve_lazy_audio_file<C: CacheConfig + 'static>(
|
||||||
cache: &Arc<Cache<C>>,
|
cache: &Arc<Cache<C>>,
|
||||||
lazy_pk: &str,
|
lazy_pk: &str,
|
||||||
param: &str,
|
param: &str,
|
||||||
@@ -187,7 +187,7 @@ async fn serve_lazy_audio_file<C: CacheConfig>(
|
|||||||
/// Sinon, le fichier complet est servi normalement.
|
/// Sinon, le fichier complet est servi normalement.
|
||||||
/// Si le fichier n'existe pas et qu'un param_generator est fourni, tente de générer le param.
|
/// Si le fichier n'existe pas et qu'un param_generator est fourni, tente de générer le param.
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
async fn serve_file_with_streaming<C: CacheConfig>(
|
async fn serve_file_with_streaming<C: CacheConfig + 'static>(
|
||||||
cache: &Arc<Cache<C>>,
|
cache: &Arc<Cache<C>>,
|
||||||
pk: &str,
|
pk: &str,
|
||||||
param: &str,
|
param: &str,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ reqwest = { version = "0.12", features = ["blocking"] }
|
|||||||
# Utilitaires
|
# Utilitaires
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
# Async
|
# Async
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
|||||||
91
pmocovers/src/api.rs
Normal file
91
pmocovers/src/api.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
//! API REST handlers spécifiques au cache de couvertures
|
||||||
|
|
||||||
|
use crate::cache;
|
||||||
|
use crate::Cache;
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use pmocache::api::{AddItemRequest, AddItemResponse, ErrorResponse};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum AddSource<'a> {
|
||||||
|
Url(&'a str),
|
||||||
|
Local(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler spécialisé pour l'ajout d'images dans le cache de couvertures.
|
||||||
|
///
|
||||||
|
/// Supporte l'ajout depuis une URL (avec conversion WebP) ou depuis un fichier local
|
||||||
|
/// (avec conversion ou passthrough selon le format).
|
||||||
|
pub async fn add_cover_item(
|
||||||
|
State(cache): State<Arc<Cache>>,
|
||||||
|
Json(req): Json<AddItemRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let mode = match (req.url.as_deref(), req.path.as_deref()) {
|
||||||
|
(Some(url), None) if !url.is_empty() => AddSource::Url(url),
|
||||||
|
(None, Some(path)) if !path.is_empty() => AddSource::Local(path),
|
||||||
|
(Some(_), Some(_)) => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: "INVALID_REQUEST".to_string(),
|
||||||
|
message: "Provide either 'url' or 'path', not both".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: "INVALID_REQUEST".to_string(),
|
||||||
|
message: "Either 'url' or 'path' must be provided".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let collection = req.collection.as_deref();
|
||||||
|
let add_result = match mode {
|
||||||
|
AddSource::Url(url) => cache.add_from_url(url, collection).await,
|
||||||
|
AddSource::Local(path) => cache::add_local_file(&cache, path, collection).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match add_result {
|
||||||
|
Ok(pk) => {
|
||||||
|
let origin =
|
||||||
|
cache
|
||||||
|
.db
|
||||||
|
.get_origin_url(&pk)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| match mode {
|
||||||
|
AddSource::Url(url) => url.to_string(),
|
||||||
|
AddSource::Local(path) => format!("file://{}", path),
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(AddItemResponse {
|
||||||
|
pk,
|
||||||
|
url: origin,
|
||||||
|
message: "Image added successfully".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: "PROCESSING_ERROR".to_string(),
|
||||||
|
message: format!("Cannot add image: {}", e),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,13 +91,117 @@ pub fn new_cache(dir: &str, limit: usize) -> Result<Cache> {
|
|||||||
/// des requêtes.
|
/// des requêtes.
|
||||||
pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result<Arc<Cache>> {
|
pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result<Arc<Cache>> {
|
||||||
let cache = Arc::new(new_cache(dir, limit)?);
|
let cache = Arc::new(new_cache(dir, limit)?);
|
||||||
let cache_clone = cache.clone();
|
Ok(pmocache::Cache::with_consolidation(cache).await)
|
||||||
tokio::spawn(async move {
|
}
|
||||||
if let Err(e) = cache_clone.consolidate().await {
|
|
||||||
tracing::warn!("Failed to consolidate cover cache on startup: {}", e);
|
/// Détecte si un buffer contient un fichier WebP
|
||||||
} else {
|
///
|
||||||
tracing::info!("Cover cache consolidated successfully on startup");
|
/// Le format WebP commence par "RIFF" (4 octets), suivi de la taille (4 octets),
|
||||||
}
|
/// puis "WEBP" (4 octets).
|
||||||
});
|
fn is_webp_header(buf: &[u8]) -> bool {
|
||||||
Ok(cache)
|
buf.len() >= 12 && &buf[0..4] == b"RIFF" && &buf[8..12] == b"WEBP"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajoute un fichier image local au cache
|
||||||
|
///
|
||||||
|
/// Les images WebP sont référencées sans copie (symlink/hardlink), les autres formats
|
||||||
|
/// sont convertis en WebP via le pipeline classique.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cache` - Instance du cache de couvertures
|
||||||
|
/// * `path` - Chemin vers le fichier image local
|
||||||
|
/// * `collection` - Collection optionnelle (ex: "album:xyz")
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Clé primaire (pk) de l'image ajoutée au cache
|
||||||
|
///
|
||||||
|
/// # Exemples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pmocovers::cache;
|
||||||
|
///
|
||||||
|
/// # async fn example() -> anyhow::Result<()> {
|
||||||
|
/// let cache = cache::new_cache("./covers", 1000)?;
|
||||||
|
/// let pk = cache::add_local_file(&cache, "/path/to/cover.webp", None).await?;
|
||||||
|
/// println!("Image ajoutée avec pk: {}", pk);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn add_local_file(cache: &Cache, path: &str, collection: Option<&str>) -> Result<String> {
|
||||||
|
use pmocache::download::read_exact_or_eof;
|
||||||
|
use pmocache::pk_from_content_header;
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio::io::AsyncSeekExt;
|
||||||
|
|
||||||
|
let canonical_path = std::fs::canonicalize(path)?;
|
||||||
|
let file_url = format!("file://{}", canonical_path.display());
|
||||||
|
let length = tokio::fs::metadata(&canonical_path)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|m| m.len());
|
||||||
|
let mut reader = tokio::fs::File::open(&canonical_path).await?;
|
||||||
|
|
||||||
|
let header = read_exact_or_eof(&mut reader, 1024)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to read header bytes: {}", e))?;
|
||||||
|
|
||||||
|
let pk_bytes = if header.len() >= 1024 {
|
||||||
|
&header[512..]
|
||||||
|
} else {
|
||||||
|
&header[..]
|
||||||
|
};
|
||||||
|
let pk = pk_from_content_header(pk_bytes);
|
||||||
|
|
||||||
|
// Si déjà en cache, incrémenter hit et retourner
|
||||||
|
if cache.db.get(&pk, false).is_ok() {
|
||||||
|
cache.db.update_hit(&pk)?;
|
||||||
|
return Ok(pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si téléchargement en cours, attendre et retourner
|
||||||
|
if let Some(download) = cache.get_download(&pk).await {
|
||||||
|
if download.finished().await {
|
||||||
|
cache.db.update_hit(&pk)?;
|
||||||
|
}
|
||||||
|
return Ok(pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_webp = is_webp_header(&header);
|
||||||
|
if !is_webp {
|
||||||
|
// Format non-WebP : passer par le pipeline de conversion
|
||||||
|
reader
|
||||||
|
.rewind()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to rewind local file: {}", e))?;
|
||||||
|
|
||||||
|
return cache
|
||||||
|
.add_from_reader_with_pk(Some(&file_url), reader, length, collection, Some(pk))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format WebP : créer un lien sans copie (passthrough)
|
||||||
|
let mut metadata = vec![
|
||||||
|
("local_passthrough".to_string(), json!(true)),
|
||||||
|
(
|
||||||
|
"local_source_path".to_string(),
|
||||||
|
json!(canonical_path.to_string_lossy().to_string()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if let Some(len) = length {
|
||||||
|
metadata.push(("source_size".to_string(), json!(len)));
|
||||||
|
}
|
||||||
|
|
||||||
|
cache
|
||||||
|
.register_local_file_reference(
|
||||||
|
&pk,
|
||||||
|
&canonical_path,
|
||||||
|
collection,
|
||||||
|
Some(&file_url),
|
||||||
|
Some(&metadata),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(pk)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,13 +54,16 @@
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod webp;
|
pub mod webp;
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoserver")]
|
||||||
|
pub mod api;
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub mod openapi;
|
pub mod openapi;
|
||||||
|
|
||||||
#[cfg(feature = "pmoconfig")]
|
#[cfg(feature = "pmoconfig")]
|
||||||
pub mod config_ext;
|
pub mod config_ext;
|
||||||
|
|
||||||
pub use cache::{new_cache, new_cache_with_consolidation, Cache, CoversConfig};
|
pub use cache::{add_local_file, new_cache, new_cache_with_consolidation, Cache, CoversConfig};
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub use openapi::ApiDoc;
|
pub use openapi::ApiDoc;
|
||||||
@@ -172,7 +175,34 @@ fn create_variant_generator() -> pmocache::pmoserver_ext::ParamGenerator<CoversC
|
|||||||
// Handlers JPEG (transcodage à la volée)
|
// Handlers JPEG (transcodage à la volée)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Sert une image de couverture au format JPEG (transcodage depuis WebP)
|
||||||
|
///
|
||||||
|
/// Cette route transcode à la volée l'image WebP stockée en cache vers le format JPEG.
|
||||||
|
/// Utile pour la compatibilité avec les clients qui ne supportent pas WebP (ex: UPnP).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `pk` - Clé primaire de l'image
|
||||||
|
///
|
||||||
|
/// # Responses
|
||||||
|
///
|
||||||
|
/// * `200 OK` - Image JPEG transcodée
|
||||||
|
/// * `404 NOT_FOUND` - Image non trouvée
|
||||||
|
/// * `500 INTERNAL_SERVER_ERROR` - Erreur de transcodage
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/covers/jpeg/{pk}",
|
||||||
|
tag = "covers",
|
||||||
|
params(
|
||||||
|
("pk" = String, Path, description = "Clé primaire de l'image")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Image JPEG", content_type = "image/jpeg"),
|
||||||
|
(status = 404, description = "Image non trouvée"),
|
||||||
|
(status = 500, description = "Erreur de transcodage"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn serve_cover_jpeg(
|
async fn serve_cover_jpeg(
|
||||||
axum::extract::State(cache): axum::extract::State<Arc<Cache>>,
|
axum::extract::State(cache): axum::extract::State<Arc<Cache>>,
|
||||||
axum::extract::Path(pk): axum::extract::Path<String>,
|
axum::extract::Path(pk): axum::extract::Path<String>,
|
||||||
@@ -180,7 +210,36 @@ async fn serve_cover_jpeg(
|
|||||||
serve_jpeg_internal(cache, pk, None).await
|
serve_jpeg_internal(cache, pk, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sert une image de couverture redimensionnée au format JPEG (transcodage depuis WebP)
|
||||||
|
///
|
||||||
|
/// Cette route transcode à la volée l'image WebP stockée en cache vers le format JPEG,
|
||||||
|
/// en la redimensionnant à la taille demandée (format carré).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `pk` - Clé primaire de l'image
|
||||||
|
/// * `size` - Taille souhaitée en pixels (ex: 256 pour 256x256)
|
||||||
|
///
|
||||||
|
/// # Responses
|
||||||
|
///
|
||||||
|
/// * `200 OK` - Image JPEG redimensionnée et transcodée
|
||||||
|
/// * `404 NOT_FOUND` - Image non trouvée
|
||||||
|
/// * `500 INTERNAL_SERVER_ERROR` - Erreur de transcodage ou redimensionnement
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/covers/jpeg/{pk}/{size}",
|
||||||
|
tag = "covers",
|
||||||
|
params(
|
||||||
|
("pk" = String, Path, description = "Clé primaire de l'image"),
|
||||||
|
("size" = String, Path, description = "Taille en pixels (ex: 256, 512)")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Image JPEG redimensionnée", content_type = "image/jpeg"),
|
||||||
|
(status = 404, description = "Image non trouvée"),
|
||||||
|
(status = 500, description = "Erreur de transcodage"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn serve_cover_jpeg_with_size(
|
async fn serve_cover_jpeg_with_size(
|
||||||
axum::extract::State(cache): axum::extract::State<Arc<Cache>>,
|
axum::extract::State(cache): axum::extract::State<Arc<Cache>>,
|
||||||
axum::extract::Path((pk, size)): axum::extract::Path<(String, String)>,
|
axum::extract::Path((pk, size)): axum::extract::Path<(String, String)>,
|
||||||
@@ -276,7 +335,7 @@ impl CoverCacheExt for pmoserver::Server {
|
|||||||
cache_dir: &str,
|
cache_dir: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> anyhow::Result<Arc<Cache>> {
|
) -> anyhow::Result<Arc<Cache>> {
|
||||||
use pmocache::pmoserver_ext::{create_api_router, create_file_router_with_generator};
|
use pmocache::pmoserver_ext::create_file_router_with_generator;
|
||||||
|
|
||||||
let cache = Arc::new(cache::new_cache(cache_dir, limit)?);
|
let cache = Arc::new(cache::new_cache(cache_dir, limit)?);
|
||||||
|
|
||||||
@@ -302,9 +361,29 @@ impl CoverCacheExt for pmoserver::Server {
|
|||||||
let combined_router = file_router.merge(jpeg_router);
|
let combined_router = file_router.merge(jpeg_router);
|
||||||
self.add_router("/", combined_router).await;
|
self.add_router("/", combined_router).await;
|
||||||
|
|
||||||
// API REST générique (pmocache)
|
// API REST (handlers génériques + POST spécialisé covers)
|
||||||
// Routes: GET/POST/DELETE /api/covers, etc.
|
let api_router = axum::Router::new()
|
||||||
let api_router = create_api_router(cache.clone());
|
.route(
|
||||||
|
"/",
|
||||||
|
axum::routing::get(pmocache::api::list_items::<CoversConfig>)
|
||||||
|
.post(crate::api::add_cover_item)
|
||||||
|
.delete(pmocache::api::purge_cache::<CoversConfig>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{pk}",
|
||||||
|
axum::routing::get(pmocache::api::get_item_info::<CoversConfig>)
|
||||||
|
.delete(pmocache::api::delete_item::<CoversConfig>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{pk}/status",
|
||||||
|
axum::routing::get(pmocache::api::get_download_status::<CoversConfig>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/consolidate",
|
||||||
|
axum::routing::post(pmocache::api::consolidate_cache::<CoversConfig>),
|
||||||
|
)
|
||||||
|
.with_state(cache.clone());
|
||||||
|
|
||||||
let openapi = crate::ApiDoc::openapi();
|
let openapi = crate::ApiDoc::openapi();
|
||||||
self.add_openapi(api_router, openapi, "covers").await;
|
self.add_openapi(api_router, openapi, "covers").await;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ use utoipa::OpenApi;
|
|||||||
/// L'API réutilise les handlers génériques de pmocache.
|
/// L'API réutilise les handlers génériques de pmocache.
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::serve_cover_jpeg,
|
||||||
|
crate::serve_cover_jpeg_with_size,
|
||||||
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
pmocache::CacheEntry,
|
pmocache::CacheEntry,
|
||||||
|
|||||||
Reference in New Issue
Block a user