refactoring des caches
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ xxx
|
||||
**/.pmomusic.yml
|
||||
**/.pmomusic_covers/**
|
||||
**/.pmomusic_audio/**
|
||||
/.pmomusic
|
||||
.DS_Store
|
||||
/target/
|
||||
/.pmomusic_covers
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2382,10 +2382,13 @@ dependencies = [
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"pmoconfig",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
|
||||
@@ -141,6 +141,7 @@ import {
|
||||
purgeCache,
|
||||
consolidateCache,
|
||||
getImageUrl,
|
||||
waitForDownload,
|
||||
} from "../services/coverCache";
|
||||
|
||||
// --- États ---
|
||||
@@ -190,11 +191,16 @@ async function handleAddImage() {
|
||||
isAdding.value = true; addError.value=""; addSuccess.value="";
|
||||
try {
|
||||
const result = await addImage(newImageUrl.value);
|
||||
addSuccess.value = `Image downloading... PK: ${result.pk}`;
|
||||
|
||||
// Attendre que le téléchargement et la transformation soient terminés
|
||||
await waitForDownload(result.pk);
|
||||
|
||||
addSuccess.value = `Image added! PK: ${result.pk}`;
|
||||
newImageUrl.value = "";
|
||||
await refreshImages();
|
||||
} catch(e:any) { addError.value = e.message ?? "Failed to add image"; }
|
||||
finally { isAdding.value=false; setTimeout(()=>addSuccess.value="",1500); }
|
||||
finally { isAdding.value=false; setTimeout(()=>addSuccess.value="",3000); }
|
||||
}
|
||||
|
||||
async function handleDeleteImage(pk:string){
|
||||
|
||||
@@ -24,6 +24,14 @@ export interface ApiError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface DownloadStatus {
|
||||
pk: string;
|
||||
finished: boolean;
|
||||
current_size?: number;
|
||||
expected_size?: number;
|
||||
transformed_size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les images en cache
|
||||
*/
|
||||
@@ -109,6 +117,54 @@ export async function consolidateCache(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut du téléchargement d'une image
|
||||
*/
|
||||
export async function getDownloadStatus(pk: string): Promise<DownloadStatus> {
|
||||
const response = await fetch(`/api/covers/${pk}/status`);
|
||||
if (!response.ok) {
|
||||
const error: ApiError = await response.json();
|
||||
throw new Error(error.message || "Failed to get download status");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attend que le téléchargement d'une image soit terminé
|
||||
*
|
||||
* @param pk - Clé primaire de l'image
|
||||
* @param maxWaitMs - Temps maximum d'attente en millisecondes (défaut: 30000)
|
||||
* @param pollIntervalMs - Intervalle entre les vérifications en millisecondes (défaut: 500)
|
||||
*/
|
||||
export async function waitForDownload(
|
||||
pk: string,
|
||||
maxWaitMs: number = 30000,
|
||||
pollIntervalMs: number = 500
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
try {
|
||||
const status = await getDownloadStatus(pk);
|
||||
if (status.finished) {
|
||||
return; // Téléchargement terminé
|
||||
}
|
||||
} catch (error) {
|
||||
// Si l'API retourne une erreur, on continue d'attendre
|
||||
console.warn(`Error checking download status for ${pk}:`, error);
|
||||
}
|
||||
|
||||
// Attendre avant la prochaine vérification
|
||||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
|
||||
// Timeout atteint, on lance une dernière vérification
|
||||
const finalStatus = await getDownloadStatus(pk);
|
||||
if (!finalStatus.finished) {
|
||||
console.warn(`Download timeout for ${pk}, but continuing anyway`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère l'URL pour afficher une image
|
||||
*/
|
||||
|
||||
@@ -48,4 +48,5 @@ tracing-subscriber = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["pmoserver"]
|
||||
pmoserver = ["dep:pmoserver", "dep:pmoconfig", "dep:axum", "dep:utoipa", "pmocache/pmoserver", "pmocache/openapi"]
|
||||
pmoconfig = ["dep:pmoconfig", "pmocache/pmoconfig"]
|
||||
pmoserver = ["pmoconfig", "dep:pmoserver", "dep:axum", "dep:utoipa", "pmocache/pmoserver", "pmocache/openapi"]
|
||||
|
||||
106
pmoaudiocache/src/config_ext.rs
Normal file
106
pmoaudiocache/src/config_ext.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
//! Extension pour intégrer le cache audio dans pmoconfig
|
||||
//!
|
||||
//! Ce module fournit le trait `AudioCacheConfigExt` qui permet d'ajouter facilement
|
||||
//! des méthodes de gestion du cache audio à pmoconfig::Config.
|
||||
|
||||
use anyhow::Result;
|
||||
use pmoconfig::Config;
|
||||
use pmocache::CacheConfigExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
const DEFAULT_AUDIO_CACHE_DIR: &str = "cache_audio";
|
||||
const DEFAULT_AUDIO_CACHE_SIZE: usize = 500;
|
||||
|
||||
/// Trait d'extension pour gérer le cache audio dans pmoconfig
|
||||
///
|
||||
/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
|
||||
/// au cache audio avec conversion FLAC.
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmoconfig::get_config;
|
||||
/// use pmoaudiocache::AudioCacheConfigExt;
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache = config.create_audio_cache()?;
|
||||
///
|
||||
/// // Utiliser le cache
|
||||
/// let pk = cache.add_from_url("http://example.com/track.mp3", Some("album:123")).await?;
|
||||
/// ```
|
||||
pub trait AudioCacheConfigExt {
|
||||
/// Récupère le répertoire du cache audio
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le chemin absolu du répertoire du cache audio (default: "cache_audio")
|
||||
fn get_audiocache_dir(&self) -> Result<String>;
|
||||
|
||||
/// Définit le répertoire du cache audio
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - Chemin du répertoire (absolu ou relatif au config_dir)
|
||||
fn set_audiocache_dir(&self, directory: String) -> Result<()>;
|
||||
|
||||
/// Récupère la taille maximale du cache audio
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le nombre maximal de pistes audio dans le cache (default: 500)
|
||||
fn get_audiocache_size(&self) -> Result<usize>;
|
||||
|
||||
/// Définit la taille maximale du cache audio
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - Nombre maximal de pistes audio
|
||||
fn set_audiocache_size(&self, size: usize) -> Result<()>;
|
||||
|
||||
/// Crée une instance du cache audio configurée avec conversion FLAC
|
||||
///
|
||||
/// Cette méthode factory crée un cache audio en utilisant les paramètres
|
||||
/// de configuration (répertoire et taille) et active la conversion FLAC
|
||||
/// automatique pour tous les fichiers audio téléchargés.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Une instance Arc du cache audio configuré
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmoconfig::get_config;
|
||||
/// use pmoaudiocache::AudioCacheConfigExt;
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache = config.create_audio_cache()?;
|
||||
///
|
||||
/// // Le cache est prêt à être utilisé avec conversion FLAC automatique
|
||||
/// ```
|
||||
fn create_audio_cache(&self) -> Result<Arc<crate::Cache>>;
|
||||
}
|
||||
|
||||
impl AudioCacheConfigExt for Config {
|
||||
fn get_audiocache_dir(&self) -> Result<String> {
|
||||
self.get_cache_dir("audio_cache", DEFAULT_AUDIO_CACHE_DIR)
|
||||
}
|
||||
|
||||
fn set_audiocache_dir(&self, directory: String) -> Result<()> {
|
||||
self.set_cache_dir("audio_cache", directory)
|
||||
}
|
||||
|
||||
fn get_audiocache_size(&self) -> Result<usize> {
|
||||
self.get_cache_size("audio_cache", DEFAULT_AUDIO_CACHE_SIZE)
|
||||
}
|
||||
|
||||
fn set_audiocache_size(&self, size: usize) -> Result<()> {
|
||||
self.set_cache_size("audio_cache", size)
|
||||
}
|
||||
|
||||
fn create_audio_cache(&self) -> Result<Arc<crate::Cache>> {
|
||||
let dir = self.get_audiocache_dir()?;
|
||||
let size = self.get_audiocache_size()?;
|
||||
Ok(Arc::new(crate::cache::new_cache(&dir, size)?))
|
||||
}
|
||||
}
|
||||
@@ -137,10 +137,16 @@ pub mod metadata;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod openapi;
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub mod config_ext;
|
||||
|
||||
// Re-exports principaux
|
||||
pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache};
|
||||
pub use metadata::AudioMetadata;
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub use config_ext::AudioCacheConfigExt;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub use openapi::ApiDoc;
|
||||
|
||||
@@ -207,9 +213,10 @@ impl AudioCacheExt for pmoserver::Server {
|
||||
}
|
||||
|
||||
async fn init_audio_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
|
||||
use crate::AudioCacheConfigExt;
|
||||
let config = pmoconfig::get_config();
|
||||
let cache_dir = config.get_audio_cache_dir()?;
|
||||
let limit = config.get_audio_cache_size()?;
|
||||
let cache_dir = config.get_audiocache_dir()?;
|
||||
let limit = config.get_audiocache_size()?;
|
||||
self.init_audio_cache(&cache_dir, limit).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,46 @@ pub struct AudioMetadata {
|
||||
}
|
||||
|
||||
impl AudioMetadata {
|
||||
/// Extrait les métadonnées depuis un fichier audio taggé
|
||||
///
|
||||
/// Fonction interne commune pour extraire les métadonnées depuis un TaggedFile
|
||||
fn from_tagged_file(tagged_file: lofty::file::TaggedFile) -> Self {
|
||||
let properties = tagged_file.properties();
|
||||
let tag = tagged_file
|
||||
.primary_tag()
|
||||
.or_else(|| tagged_file.first_tag());
|
||||
|
||||
let mut metadata = Self {
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
year: None,
|
||||
track_number: None,
|
||||
track_total: None,
|
||||
disc_number: None,
|
||||
disc_total: None,
|
||||
genre: None,
|
||||
duration_secs: Some(properties.duration().as_secs()),
|
||||
sample_rate: properties.sample_rate(),
|
||||
channels: properties.channels(),
|
||||
bitrate: properties.audio_bitrate(),
|
||||
};
|
||||
|
||||
if let Some(tag) = tag {
|
||||
metadata.title = tag.title().map(|s| s.to_string());
|
||||
metadata.artist = tag.artist().map(|s| s.to_string());
|
||||
metadata.album = tag.album().map(|s| s.to_string());
|
||||
metadata.year = tag.year();
|
||||
metadata.track_number = tag.track();
|
||||
metadata.track_total = tag.track_total();
|
||||
metadata.disc_number = tag.disk();
|
||||
metadata.disc_total = tag.disk_total();
|
||||
metadata.genre = tag.genre().map(|s| s.to_string());
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
/// Extrait les métadonnées d'un fichier audio
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -88,41 +128,7 @@ impl AudioMetadata {
|
||||
/// ```
|
||||
pub fn from_file(path: &Path) -> Result<Self> {
|
||||
let tagged_file = Probe::open(path)?.options(ParseOptions::new()).read()?;
|
||||
|
||||
let properties = tagged_file.properties();
|
||||
let tag = tagged_file
|
||||
.primary_tag()
|
||||
.or_else(|| tagged_file.first_tag());
|
||||
|
||||
let mut metadata = Self {
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
year: None,
|
||||
track_number: None,
|
||||
track_total: None,
|
||||
disc_number: None,
|
||||
disc_total: None,
|
||||
genre: None,
|
||||
duration_secs: Some(properties.duration().as_secs()),
|
||||
sample_rate: properties.sample_rate(),
|
||||
channels: properties.channels(),
|
||||
bitrate: properties.audio_bitrate(),
|
||||
};
|
||||
|
||||
if let Some(tag) = tag {
|
||||
metadata.title = tag.title().map(|s| s.to_string());
|
||||
metadata.artist = tag.artist().map(|s| s.to_string());
|
||||
metadata.album = tag.album().map(|s| s.to_string());
|
||||
metadata.year = tag.year();
|
||||
metadata.track_number = tag.track();
|
||||
metadata.track_total = tag.track_total();
|
||||
metadata.disc_number = tag.disk();
|
||||
metadata.disc_total = tag.disk_total();
|
||||
metadata.genre = tag.genre().map(|s| s.to_string());
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
Ok(Self::from_tagged_file(tagged_file))
|
||||
}
|
||||
|
||||
/// Crée des métadonnées depuis des données brutes audio
|
||||
@@ -136,41 +142,7 @@ impl AudioMetadata {
|
||||
.guess_file_type()?
|
||||
.options(ParseOptions::new())
|
||||
.read()?;
|
||||
|
||||
let properties = tagged_file.properties();
|
||||
let tag = tagged_file
|
||||
.primary_tag()
|
||||
.or_else(|| tagged_file.first_tag());
|
||||
|
||||
let mut metadata = Self {
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
year: None,
|
||||
track_number: None,
|
||||
track_total: None,
|
||||
disc_number: None,
|
||||
disc_total: None,
|
||||
genre: None,
|
||||
duration_secs: Some(properties.duration().as_secs()),
|
||||
sample_rate: properties.sample_rate(),
|
||||
channels: properties.channels(),
|
||||
bitrate: properties.audio_bitrate(),
|
||||
};
|
||||
|
||||
if let Some(tag) = tag {
|
||||
metadata.title = tag.title().map(|s| s.to_string());
|
||||
metadata.artist = tag.artist().map(|s| s.to_string());
|
||||
metadata.album = tag.album().map(|s| s.to_string());
|
||||
metadata.year = tag.year();
|
||||
metadata.track_number = tag.track();
|
||||
metadata.track_total = tag.track_total();
|
||||
metadata.disc_number = tag.disk();
|
||||
metadata.disc_total = tag.disk_total();
|
||||
metadata.genre = tag.genre().map(|s| s.to_string());
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
Ok(Self::from_tagged_file(tagged_file))
|
||||
}
|
||||
|
||||
/// Génère une clé de collection basée sur l'artiste et l'album
|
||||
|
||||
@@ -13,6 +13,7 @@ futures-util = "0.3"
|
||||
|
||||
# Cryptographie
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
# Utilitaires
|
||||
@@ -34,7 +35,12 @@ utoipa = { version = "5.3", optional = true }
|
||||
# Feature pour pmoserver (extension HTTP)
|
||||
axum = { version = "0.8", optional = true }
|
||||
|
||||
# Feature pour pmoconfig (extension de configuration)
|
||||
pmoconfig = { path = "../pmoconfig", optional = true }
|
||||
serde_yaml = { version = "0.9", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
openapi = ["dep:utoipa"]
|
||||
pmoserver = ["dep:axum"]
|
||||
pmoconfig = ["dep:pmoconfig", "dep:serde_yaml"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Ce module fournit une interface générique pour gérer un cache de fichiers
|
||||
//! avec métadonnées dans une base de données SQLite.
|
||||
|
||||
use crate::cache_trait::{pk_from_url, FileCache};
|
||||
use crate::cache_trait::FileCache;
|
||||
use crate::db::DB;
|
||||
use crate::download::{
|
||||
download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
|
||||
@@ -132,8 +132,18 @@ impl<C: CacheConfig> Cache<C> {
|
||||
|
||||
/// Télécharge un fichier depuis une URL et l'ajoute au cache
|
||||
///
|
||||
/// Utilise le module download pour gérer le téléchargement asynchrone.
|
||||
/// Le download est tracké dans la map jusqu'à sa fin.
|
||||
/// Cette méthode utilise un système d'identifiants basé sur le contenu plutôt que sur l'URL.
|
||||
/// Elle télécharge les 512 premiers octets du fichier pour calculer un identifiant unique (pk),
|
||||
/// puis vérifie si le fichier est déjà en cache. Si c'est le cas, elle met à jour le timestamp
|
||||
/// et retourne rapidement. Sinon, elle lance le téléchargement complet en arrière-plan.
|
||||
///
|
||||
/// # Workflow
|
||||
///
|
||||
/// 1. Télécharge les 512 premiers octets via une requête HTTP partielle
|
||||
/// 2. Calcule le pk en hashant (SHA256) ces premiers octets
|
||||
/// 3. Vérifie si le fichier existe déjà dans le cache
|
||||
/// 4. Si oui : update timestamp et retour rapide
|
||||
/// 5. Si non : lance le téléchargement complet en background
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -142,21 +152,46 @@ impl<C: CacheConfig> Cache<C> {
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// La clé primaire (pk) du fichier dans le cache
|
||||
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Deux URLs différentes pointant vers le même contenu auront le même pk,
|
||||
/// permettant une déduplication automatique.
|
||||
pub async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let pk = pk_from_url(url);
|
||||
let file_path = self.file_path(&pk);
|
||||
// 1. Télécharger les 512 premiers octets pour calculer le pk
|
||||
let header = crate::download::peek_header(url, 512)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to peek header: {}", e))?;
|
||||
|
||||
// Vérifier si déjà en cours de téléchargement
|
||||
{
|
||||
let downloads = self.downloads.read().await;
|
||||
if downloads.contains_key(&pk) {
|
||||
// Download déjà en cours, retourner la clé
|
||||
// 2. Calculer le pk basé sur le contenu
|
||||
let pk = crate::cache_trait::pk_from_content_header(&header);
|
||||
tracing::debug!("Computed pk {} for URL {}", pk, url);
|
||||
|
||||
// 3. Vérifier si le fichier est déjà en cache
|
||||
if self.db.get(&pk).is_ok() {
|
||||
let file_path = self.file_path(&pk);
|
||||
if file_path.exists() {
|
||||
// Déjà en cache, update timestamp et retour rapide
|
||||
tracing::debug!("File with pk {} already in cache, updating timestamp", pk);
|
||||
self.db.update_hit(&pk)?;
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
// Lancer le téléchargement avec transformer
|
||||
// 4. Vérifier si un download est déjà en cours pour ce pk
|
||||
{
|
||||
let downloads = self.downloads.read().await;
|
||||
if downloads.contains_key(&pk) {
|
||||
// Download déjà en cours pour ce contenu, retourner la clé
|
||||
tracing::debug!("Download already in progress for pk {}", pk);
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Lancer le téléchargement complet avec transformer
|
||||
tracing::debug!("Starting full download for pk {} from URL {}", pk, url);
|
||||
let file_path = self.file_path(&pk);
|
||||
let transformer = self.transformer_factory.as_ref().map(|f| f());
|
||||
let download = download_with_transformer(&file_path, url, transformer);
|
||||
|
||||
@@ -170,7 +205,6 @@ impl<C: CacheConfig> Cache<C> {
|
||||
self.db.add(&pk, url, collection)?;
|
||||
|
||||
// Appliquer la politique d'éviction LRU si nécessaire
|
||||
// Cela garantit que le cache respecte toujours la limite configurée
|
||||
if let Err(e) = self.enforce_limit().await {
|
||||
tracing::warn!("Error enforcing cache limit: {}", e);
|
||||
}
|
||||
@@ -179,9 +213,7 @@ impl<C: CacheConfig> Cache<C> {
|
||||
let downloads_clone = self.downloads.clone();
|
||||
let pk_clone = pk.clone();
|
||||
tokio::spawn(async move {
|
||||
// Attendre la fin du téléchargement
|
||||
let _ = download.wait_until_finished().await;
|
||||
// Retirer de la map
|
||||
downloads_clone.write().await.remove(&pk_clone);
|
||||
});
|
||||
|
||||
@@ -190,38 +222,79 @@ impl<C: CacheConfig> Cache<C> {
|
||||
|
||||
/// Ajoute un fichier à partir d'un flux asynchrone.
|
||||
///
|
||||
/// Le flux peut provenir de n'importe quelle source (stream HTTP custom, décodeur,
|
||||
/// extraction en mémoire, etc.). Les mêmes transformers que `add_from_url` sont
|
||||
/// appliqués.
|
||||
/// Cette méthode utilise le même système d'identifiants basé sur le contenu que `add_from_url`.
|
||||
/// Elle lit les 512 premiers octets du flux pour calculer l'identifiant, puis reconstitue
|
||||
/// le flux complet pour l'ingestion.
|
||||
///
|
||||
/// # Workflow
|
||||
///
|
||||
/// 1. Lit les 512 premiers octets du reader
|
||||
/// 2. Calcule le pk en hashant (SHA256) ces premiers octets
|
||||
/// 3. Vérifie si le fichier existe déjà dans le cache
|
||||
/// 4. Si oui : update timestamp et retour rapide
|
||||
/// 5. Si non : reconstitue le reader (header + reste) et lance l'ingestion
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `source_uri` - Identifiant logique du flux (utilisé pour générer le pk)
|
||||
/// * `source_uri` - Identifiant logique du flux (pour traçabilité dans la DB)
|
||||
/// * `reader` - Flux asynchrone fournissant les données
|
||||
/// * `length` - Taille attendue (si connue)
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient l'élément
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
|
||||
pub async fn add_from_reader<R>(
|
||||
&self,
|
||||
source_uri: &str,
|
||||
reader: R,
|
||||
mut reader: R,
|
||||
length: Option<u64>,
|
||||
collection: Option<&str>,
|
||||
) -> Result<String>
|
||||
where
|
||||
R: AsyncRead + Send + Unpin + 'static,
|
||||
{
|
||||
let pk = pk_from_url(source_uri);
|
||||
let file_path = self.file_path(&pk);
|
||||
// 1. Lire les 512 premiers octets pour calculer le pk
|
||||
let header = crate::download::peek_reader_header(&mut reader, 512)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to peek reader header: {}", e))?;
|
||||
|
||||
{
|
||||
let downloads = self.downloads.read().await;
|
||||
if downloads.contains_key(&pk) {
|
||||
// 2. Calculer le pk basé sur le contenu
|
||||
let pk = crate::cache_trait::pk_from_content_header(&header);
|
||||
tracing::debug!("Computed pk {} for source_uri {}", pk, source_uri);
|
||||
|
||||
// 3. Vérifier si le fichier est déjà en cache
|
||||
if self.db.get(&pk).is_ok() {
|
||||
let file_path = self.file_path(&pk);
|
||||
if file_path.exists() {
|
||||
// Déjà en cache, update timestamp et retour rapide
|
||||
tracing::debug!("File with pk {} already in cache, updating timestamp", pk);
|
||||
self.db.update_hit(&pk)?;
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Vérifier si un download est déjà en cours pour ce pk
|
||||
{
|
||||
let downloads = self.downloads.read().await;
|
||||
if downloads.contains_key(&pk) {
|
||||
tracing::debug!("Download already in progress for pk {}", pk);
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Reconstituer le reader complet (header + reste)
|
||||
// Utiliser tokio::io::chain pour créer un reader composé
|
||||
use std::io::Cursor;
|
||||
use tokio::io::AsyncReadExt;
|
||||
let header_reader = Cursor::new(header);
|
||||
let full_reader = header_reader.chain(reader);
|
||||
|
||||
// 6. Lancer l'ingestion avec transformer
|
||||
tracing::debug!("Starting ingestion for pk {} from reader", pk);
|
||||
let file_path = self.file_path(&pk);
|
||||
let transformer = self.transformer_factory.as_ref().map(|factory| factory());
|
||||
let download = ingest_with_transformer(&file_path, reader, length, transformer);
|
||||
let download = ingest_with_transformer(&file_path, full_reader, length, transformer);
|
||||
|
||||
{
|
||||
let mut downloads = self.downloads.write().await;
|
||||
@@ -246,7 +319,9 @@ impl<C: CacheConfig> Cache<C> {
|
||||
|
||||
/// Ajoute un fichier local au cache
|
||||
///
|
||||
/// Le fichier est copié dans le cache via une URL file://
|
||||
/// Cette méthode lit les 512 premiers octets du fichier local pour calculer
|
||||
/// l'identifiant basé sur le contenu, puis utilise `add_from_reader()` pour
|
||||
/// l'ingestion complète.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -255,7 +330,21 @@ impl<C: CacheConfig> Cache<C> {
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// La clé primaire (pk) du fichier dans le cache
|
||||
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocache::{Cache, CacheConfig};
|
||||
///
|
||||
/// struct MyConfig;
|
||||
/// impl CacheConfig for MyConfig {
|
||||
/// fn file_extension() -> &'static str { "dat" }
|
||||
/// }
|
||||
///
|
||||
/// let cache = Cache::<MyConfig>::new("./cache", 1000)?;
|
||||
/// let pk = cache.add_from_file("/path/to/file.dat", None).await?;
|
||||
/// ```
|
||||
pub async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String> {
|
||||
let canonical_path = std::fs::canonicalize(path)?;
|
||||
let file_url = format!("file://{}", canonical_path.display());
|
||||
@@ -264,31 +353,12 @@ impl<C: CacheConfig> Cache<C> {
|
||||
.ok()
|
||||
.map(|m| m.len());
|
||||
let reader = tokio::fs::File::open(&canonical_path).await?;
|
||||
|
||||
// add_from_reader() s'occupe de lire les 512 premiers octets et de calculer le pk
|
||||
self.add_from_reader(&file_url, reader, length, collection)
|
||||
.await
|
||||
}
|
||||
|
||||
/// S'assure qu'un fichier est présent dans le cache
|
||||
///
|
||||
/// Si le fichier existe déjà, retourne sa clé. Sinon, le télécharge.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL du fichier
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
pub async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let pk = pk_from_url(url);
|
||||
|
||||
if self.db.get(&pk).is_ok() {
|
||||
let file_path = self.file_path(&pk);
|
||||
if file_path.exists() {
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
self.add_from_url(url, collection).await
|
||||
}
|
||||
|
||||
/// Récupère le chemin d'un fichier dans le cache
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -620,10 +690,6 @@ impl<C: CacheConfig> FileCache<C> for Cache<C> {
|
||||
self.add_from_file(path, collection).await
|
||||
}
|
||||
|
||||
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
self.ensure_from_url(url, collection).await
|
||||
}
|
||||
|
||||
async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||
self.get(pk).await
|
||||
}
|
||||
|
||||
@@ -116,16 +116,6 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
||||
/// La clé primaire (pk) du fichier dans le cache
|
||||
async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String>;
|
||||
|
||||
/// S'assure qu'un fichier est présent dans le cache
|
||||
///
|
||||
/// Si le fichier existe déjà, retourne sa clé. Sinon, le télécharge.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL du fichier
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String>;
|
||||
|
||||
/// Récupère le chemin d'un fichier dans le cache
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -147,9 +137,48 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
||||
async fn consolidate(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Génère une clé primaire à partir d'une URL
|
||||
/// Génère une clé primaire à partir des premiers octets d'un document
|
||||
///
|
||||
/// Utilise SHA256 pour hasher les premiers octets du contenu et retourne les 16 premiers octets
|
||||
/// en hexadécimal (32 caractères). L'utilisation de 16 octets au lieu de 8 réduit considérablement
|
||||
/// les risques de collision.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `header` - Les premiers octets du document (typiquement 512 octets)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Une chaîne hexadécimale de 32 caractères servant de clé primaire unique
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```
|
||||
/// use pmocache::pk_from_content_header;
|
||||
///
|
||||
/// let data = b"Some file content...";
|
||||
/// let pk = pk_from_content_header(data);
|
||||
/// assert_eq!(pk.len(), 32); // 16 bytes = 32 hex chars
|
||||
/// ```
|
||||
pub fn pk_from_content_header(header: &[u8]) -> String {
|
||||
use sha2::{Sha256, Digest};
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(header);
|
||||
let result = hasher.finalize();
|
||||
hex::encode(&result[..16]) // 16 octets = 32 caractères hex
|
||||
}
|
||||
|
||||
/// Génère une clé primaire à partir d'une URL (legacy)
|
||||
///
|
||||
/// **DEPRECATED**: Cette fonction est obsolète et ne devrait plus être utilisée.
|
||||
/// Utilisez `pk_from_content_header()` à la place pour générer des identifiants
|
||||
/// basés sur le contenu plutôt que sur l'URL.
|
||||
///
|
||||
/// Utilise SHA1 pour hasher l'URL et retourne les 8 premiers octets en hexadécimal.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Utilisez pk_from_content_header() pour des identifiants basés sur le contenu"
|
||||
)]
|
||||
pub fn pk_from_url(url: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(url.as_bytes());
|
||||
|
||||
225
pmocache/src/config_ext.rs
Normal file
225
pmocache/src/config_ext.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! Extension pour intégrer la gestion des caches dans pmoconfig
|
||||
//!
|
||||
//! Ce module fournit le trait `CacheConfigExt` qui permet d'ajouter facilement
|
||||
//! des méthodes de gestion de cache générique à pmoconfig::Config.
|
||||
//!
|
||||
//! Il propose également un macro `impl_cache_config_ext!` pour simplifier
|
||||
//! l'implémentation de traits d'extension spécialisés (audio, covers, etc.).
|
||||
|
||||
use anyhow::Result;
|
||||
use pmoconfig::Config;
|
||||
use serde_yaml::{Number, Value};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Trait d'extension pour ajouter la gestion des caches à pmoconfig
|
||||
///
|
||||
/// Ce trait étend `pmoconfig::Config` avec des méthodes génériques pour gérer
|
||||
/// n'importe quel type de cache (audio, images, etc.).
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmoconfig::get_config;
|
||||
/// use pmocache::{CacheConfigExt, AudioConfig};
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache_dir = config.get_cache_dir("audio_cache", "cache_audio")?;
|
||||
/// let cache_size = config.get_cache_size("audio_cache", 500)?;
|
||||
/// ```
|
||||
pub trait CacheConfigExt {
|
||||
/// Récupère le répertoire d'un cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
|
||||
/// * `default` - Nom de répertoire par défaut si non configuré
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le chemin absolu du répertoire du cache
|
||||
fn get_cache_dir(&self, cache_type: &str, default: &str) -> Result<String>;
|
||||
|
||||
/// Définit le répertoire d'un cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
|
||||
/// * `directory` - Chemin du répertoire (absolu ou relatif au config_dir)
|
||||
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()>;
|
||||
|
||||
/// Récupère la taille maximale d'un cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
|
||||
/// * `default` - Taille par défaut si non configurée
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le nombre maximal d'éléments dans le cache
|
||||
fn get_cache_size(&self, cache_type: &str, default: usize) -> Result<usize>;
|
||||
|
||||
/// Définit la taille maximale d'un cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
|
||||
/// * `size` - Nombre maximal d'éléments
|
||||
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()>;
|
||||
|
||||
/// Crée une instance de cache générique configurée
|
||||
///
|
||||
/// Cette méthode factory crée un cache en utilisant les paramètres
|
||||
/// de configuration (répertoire et taille).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
|
||||
/// * `default_dir` - Répertoire par défaut
|
||||
/// * `default_size` - Taille par défaut
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Une instance Arc du cache configuré
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmoconfig::get_config;
|
||||
/// use pmocache::{CacheConfigExt, AudioConfig};
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache = config.create_cache::<AudioConfig>("audio_cache", "cache_audio", 500)?;
|
||||
/// ```
|
||||
fn create_cache<C: crate::CacheConfig>(
|
||||
&self,
|
||||
cache_type: &str,
|
||||
default_dir: &str,
|
||||
default_size: usize,
|
||||
) -> Result<Arc<crate::Cache<C>>>;
|
||||
}
|
||||
|
||||
impl CacheConfigExt for Config {
|
||||
fn get_cache_dir(&self, cache_type: &str, default: &str) -> Result<String> {
|
||||
self.get_managed_dir(&["host", cache_type, "directory"], default)
|
||||
}
|
||||
|
||||
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()> {
|
||||
self.set_managed_dir(&["host", cache_type, "directory"], directory)
|
||||
}
|
||||
|
||||
fn get_cache_size(&self, cache_type: &str, default: usize) -> Result<usize> {
|
||||
match self.get_value(&["host", cache_type, "size"])? {
|
||||
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
||||
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()> {
|
||||
let n = Number::from(size);
|
||||
self.set_value(&["host", cache_type, "size"], Value::Number(n))
|
||||
}
|
||||
|
||||
fn create_cache<C: crate::CacheConfig>(
|
||||
&self,
|
||||
cache_type: &str,
|
||||
default_dir: &str,
|
||||
default_size: usize,
|
||||
) -> Result<Arc<crate::Cache<C>>> {
|
||||
let dir = self.get_cache_dir(cache_type, default_dir)?;
|
||||
let size = self.get_cache_size(cache_type, default_size)?;
|
||||
Ok(Arc::new(crate::Cache::<C>::new(&dir, size)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro pour simplifier l'implémentation de traits d'extension de cache spécialisés
|
||||
///
|
||||
/// Ce macro génère automatiquement un trait d'extension pour `pmoconfig::Config`
|
||||
/// avec des méthodes spécifiques à un type de cache (audio, covers, etc.).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `trait_name` - Nom du trait à générer (ex: `AudioCacheConfigExt`)
|
||||
/// * `cache_type` - Type de cache dans la config (ex: `"audio_cache"`)
|
||||
/// * `default_dir` - Répertoire par défaut (ex: `"cache_audio"`)
|
||||
/// * `default_size` - Taille par défaut (ex: `500`)
|
||||
/// * `cache_struct` - Type du cache (ex: `crate::Cache`)
|
||||
/// * `constructor` - Expression pour construire le cache (ex: `crate::cache::new_cache(&dir, size)`)
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocache::impl_cache_config_ext;
|
||||
///
|
||||
/// impl_cache_config_ext! {
|
||||
/// AudioCacheConfigExt,
|
||||
/// "audio_cache",
|
||||
/// "cache_audio",
|
||||
/// 500,
|
||||
/// crate::Cache,
|
||||
/// |dir, size| crate::cache::new_cache(dir, size)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Cela génère un trait avec les méthodes :
|
||||
/// - `get_audiocache_dir()` / `set_audiocache_dir()`
|
||||
/// - `get_audiocache_size()` / `set_audiocache_size()`
|
||||
/// - `create_audio_cache()`
|
||||
#[macro_export]
|
||||
macro_rules! impl_cache_config_ext {
|
||||
(
|
||||
$trait_name:ident,
|
||||
$cache_type:expr,
|
||||
$default_dir:expr,
|
||||
$default_size:expr,
|
||||
$cache_type_struct:ty,
|
||||
$constructor:expr
|
||||
) => {
|
||||
pub trait $trait_name {
|
||||
/// Récupère le répertoire du cache
|
||||
fn get_cache_dir_ext(&self) -> anyhow::Result<String>;
|
||||
|
||||
/// Définit le répertoire du cache
|
||||
fn set_cache_dir_ext(&self, directory: String) -> anyhow::Result<()>;
|
||||
|
||||
/// Récupère la taille maximale du cache
|
||||
fn get_cache_size_ext(&self) -> anyhow::Result<usize>;
|
||||
|
||||
/// Définit la taille maximale du cache
|
||||
fn set_cache_size_ext(&self, size: usize) -> anyhow::Result<()>;
|
||||
|
||||
/// Crée une instance du cache configurée
|
||||
fn create_cache_ext(&self) -> anyhow::Result<std::sync::Arc<$cache_type_struct>>;
|
||||
}
|
||||
|
||||
impl $trait_name for pmoconfig::Config {
|
||||
fn get_cache_dir_ext(&self) -> anyhow::Result<String> {
|
||||
use $crate::CacheConfigExt;
|
||||
self.get_cache_dir($cache_type, $default_dir)
|
||||
}
|
||||
|
||||
fn set_cache_dir_ext(&self, directory: String) -> anyhow::Result<()> {
|
||||
use $crate::CacheConfigExt;
|
||||
self.set_cache_dir($cache_type, directory)
|
||||
}
|
||||
|
||||
fn get_cache_size_ext(&self) -> anyhow::Result<usize> {
|
||||
use $crate::CacheConfigExt;
|
||||
self.get_cache_size($cache_type, $default_size)
|
||||
}
|
||||
|
||||
fn set_cache_size_ext(&self, size: usize) -> anyhow::Result<()> {
|
||||
use $crate::CacheConfigExt;
|
||||
self.set_cache_size($cache_type, size)
|
||||
}
|
||||
|
||||
fn create_cache_ext(&self) -> anyhow::Result<std::sync::Arc<$cache_type_struct>> {
|
||||
let dir = self.get_cache_dir_ext()?;
|
||||
let size = self.get_cache_size_ext()?;
|
||||
let constructor = $constructor;
|
||||
Ok(std::sync::Arc::new(constructor(&dir, size)?))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -460,3 +460,97 @@ async fn default_copy(
|
||||
s.finished = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lit les premiers octets d'une URL sans télécharger le fichier complet
|
||||
///
|
||||
/// Cette fonction effectue une requête HTTP partielle (Range header) pour télécharger
|
||||
/// uniquement les premiers octets d'un fichier. C'est utilisé pour calculer l'identifiant
|
||||
/// basé sur le contenu sans avoir à télécharger tout le fichier.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL du fichier à télécharger
|
||||
/// * `max_bytes` - Nombre maximum d'octets à lire (par défaut 512)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Un `Vec<u8>` contenant les premiers octets du fichier
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocache::download::peek_header;
|
||||
///
|
||||
/// let header = peek_header("http://example.com/file.dat", 512).await?;
|
||||
/// let pk = pk_from_content_header(&header);
|
||||
/// ```
|
||||
pub async fn peek_header(url: &str, max_bytes: usize) -> Result<Vec<u8>, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Essayer d'abord avec une requête Range
|
||||
let range_header = format!("bytes=0-{}", max_bytes - 1);
|
||||
let mut response = client
|
||||
.get(url)
|
||||
.header("Range", range_header)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch URL '{}': {}", url, e))?;
|
||||
|
||||
// Si le serveur ne supporte pas Range (status 200 au lieu de 206),
|
||||
// on lit quand même mais on limite la lecture
|
||||
if !response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
|
||||
return Err(format!("HTTP error: {}", response.status()));
|
||||
}
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? {
|
||||
buffer.extend_from_slice(&chunk);
|
||||
if buffer.len() >= max_bytes {
|
||||
buffer.truncate(max_bytes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Lit les premiers octets d'un reader asynchrone
|
||||
///
|
||||
/// Cette fonction lit jusqu'à `max_bytes` octets depuis un reader asynchrone.
|
||||
/// C'est utilisé pour calculer l'identifiant basé sur le contenu des fichiers locaux
|
||||
/// ou des streams.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - Le reader asynchrone à lire
|
||||
/// * `max_bytes` - Nombre maximum d'octets à lire (par défaut 512)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Un `Vec<u8>` contenant les premiers octets lus
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocache::download::peek_reader_header;
|
||||
/// use tokio::fs::File;
|
||||
///
|
||||
/// let mut file = File::open("file.dat").await?;
|
||||
/// let header = peek_reader_header(&mut file, 512).await?;
|
||||
/// let pk = pk_from_content_header(&header);
|
||||
/// ```
|
||||
pub async fn peek_reader_header<R>(reader: &mut R, max_bytes: usize) -> Result<Vec<u8>, String>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let mut buffer = vec![0u8; max_bytes];
|
||||
let n = reader
|
||||
.read(&mut buffer)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read from stream: {}", e))?;
|
||||
buffer.truncate(n);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
@@ -136,11 +136,15 @@ pub mod api;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub mod openapi;
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub mod config_ext;
|
||||
|
||||
pub use cache::{Cache, CacheConfig};
|
||||
pub use cache_trait::{pk_from_url, FileCache};
|
||||
pub use cache_trait::{pk_from_content_header, pk_from_url, FileCache};
|
||||
pub use db::{CacheEntry, DB};
|
||||
pub use download::{
|
||||
download, download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
|
||||
download, download_with_transformer, ingest_with_transformer, peek_header,
|
||||
peek_reader_header, Download, StreamTransformer,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
@@ -148,3 +152,6 @@ pub use pmoserver_ext::{create_api_router, create_file_router, GenericCacheExt};
|
||||
|
||||
#[cfg(all(feature = "pmoserver", feature = "openapi"))]
|
||||
pub use api::{AddItemRequest, AddItemResponse, DeleteItemResponse, DownloadStatus, ErrorResponse};
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub use config_ext::CacheConfigExt;
|
||||
|
||||
@@ -59,10 +59,6 @@ const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__";
|
||||
|
||||
// Default values for configuration
|
||||
const DEFAULT_HTTP_PORT: u16 = 8080;
|
||||
const DEFAULT_COVER_CACHE_DIR: &str = "cache_covers";
|
||||
const DEFAULT_COVER_CACHE_SIZE: usize = 2000;
|
||||
const DEFAULT_AUDIO_CACHE_DIR: &str = "cache_audio";
|
||||
const DEFAULT_AUDIO_CACHE_SIZE: usize = 500;
|
||||
const DEFAULT_LOG_BUFFER_CAPACITY: usize = 1000;
|
||||
const DEFAULT_LOG_MIN_LEVEL: &str = "TRACE";
|
||||
const DEFAULT_LOG_ENABLE_CONSOLE: bool = true;
|
||||
@@ -437,33 +433,57 @@ impl Config {
|
||||
Ok(absolute_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Generic function to get cache directory
|
||||
fn get_cache_dir(&self, cache_type: &str, default_dir: &str) -> Result<String> {
|
||||
let dir_path = match self.get_value(&["host", cache_type, "directory"]) {
|
||||
/// Récupère un répertoire géré par la configuration
|
||||
///
|
||||
/// Cette méthode générique permet de récupérer n'importe quel répertoire
|
||||
/// configuré dans le YAML. Le répertoire peut être absolu ou relatif au
|
||||
/// répertoire de configuration. Il sera créé s'il n'existe pas.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Chemin dans l'arbre de configuration (ex: `&["host", "cache", "directory"]`)
|
||||
/// * `default` - Nom de répertoire par défaut si non configuré
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le chemin absolu du répertoire, créé s'il n'existait pas
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoconfig::get_config;
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache_dir = config.get_managed_dir(&["host", "audio_cache", "directory"], "cache_audio")?;
|
||||
/// println!("Audio cache directory: {}", cache_dir);
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
pub fn get_managed_dir(&self, path: &[&str], default: &str) -> Result<String> {
|
||||
let dir_path = match self.get_value(path) {
|
||||
Ok(Value::String(s)) => s,
|
||||
_ => default_dir.to_string(),
|
||||
_ => default.to_string(),
|
||||
};
|
||||
self.resolve_and_create_dir(&dir_path)
|
||||
}
|
||||
|
||||
/// Generic function to set cache directory
|
||||
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()> {
|
||||
self.set_value(&["host", cache_type, "directory"], Value::String(directory))
|
||||
}
|
||||
|
||||
/// Generic function to get cache size
|
||||
fn get_cache_size(&self, cache_type: &str, default_size: usize) -> Result<usize> {
|
||||
match self.get_value(&["host", cache_type, "size"])? {
|
||||
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
||||
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
||||
_ => Ok(default_size),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic function to set cache size
|
||||
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()> {
|
||||
let n = Number::from(size);
|
||||
self.set_value(&["host", cache_type, "size"], Value::Number(n))
|
||||
/// Définit un répertoire géré par la configuration
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Chemin dans l'arbre de configuration (ex: `&["host", "cache", "directory"]`)
|
||||
/// * `directory` - Chemin du répertoire (absolu ou relatif au config_dir)
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoconfig::get_config;
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// config.set_managed_dir(&["host", "audio_cache", "directory"], "/var/cache/audio".to_string())?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
pub fn set_managed_dir(&self, path: &[&str], directory: String) -> Result<()> {
|
||||
self.set_value(path, Value::String(directory))
|
||||
}
|
||||
|
||||
/// Gets the base URL for the HTTP server
|
||||
@@ -566,77 +586,6 @@ impl Config {
|
||||
self.set_value(&["devices", devtype, name, "udn"], Value::String(udn))
|
||||
}
|
||||
|
||||
/// Gets the cover cache directory
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a `Result` containing the absolute path to the cover cache directory (default: "cache_covers")
|
||||
pub fn get_cover_cache_dir(&self) -> Result<String> {
|
||||
self.get_cache_dir("cover_cache", DEFAULT_COVER_CACHE_DIR)
|
||||
}
|
||||
|
||||
/// Sets the cover cache directory
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - The directory path (absolute or relative to config dir)
|
||||
pub fn set_cover_cache_dir(&self, directory: String) -> Result<()> {
|
||||
self.set_cache_dir("cover_cache", directory)
|
||||
}
|
||||
|
||||
/// Gets the maximum number of items in the cover cache
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a `Result` containing the cache size (default: 2000)
|
||||
pub fn get_cover_cache_size(&self) -> Result<usize> {
|
||||
self.get_cache_size("cover_cache", DEFAULT_COVER_CACHE_SIZE)
|
||||
}
|
||||
|
||||
/// Sets the maximum number of items in the cover cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The maximum cache size
|
||||
pub fn set_cover_cache_size(&self, size: usize) -> Result<()> {
|
||||
self.set_cache_size("cover_cache", size)
|
||||
}
|
||||
|
||||
/// Gets the audio cache directory
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a `Result` containing the absolute path to the audio cache directory (default: "cache_audio")
|
||||
pub fn get_audio_cache_dir(&self) -> Result<String> {
|
||||
self.get_cache_dir("audio_cache", DEFAULT_AUDIO_CACHE_DIR)
|
||||
}
|
||||
|
||||
/// Sets the audio cache directory
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - The directory path (absolute or relative to config dir)
|
||||
pub fn set_audio_cache_dir(&self, directory: String) -> Result<()> {
|
||||
self.set_cache_dir("audio_cache", directory)
|
||||
}
|
||||
|
||||
/// Gets the maximum number of items in the audio cache
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a `Result` containing the cache size (default: 500)
|
||||
pub fn get_audio_cache_size(&self) -> Result<usize> {
|
||||
self.get_cache_size("audio_cache", DEFAULT_AUDIO_CACHE_SIZE)
|
||||
}
|
||||
|
||||
/// Sets the maximum number of items in the audio cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The maximum cache size
|
||||
pub fn set_audio_cache_size(&self, size: usize) -> Result<()> {
|
||||
self.set_cache_size("audio_cache", size)
|
||||
}
|
||||
|
||||
impl_string_config!(
|
||||
/// Gets the Qobuz username from configuration
|
||||
|
||||
@@ -31,4 +31,5 @@ tracing = "0.1.41"
|
||||
|
||||
[features]
|
||||
default = ["pmoserver"]
|
||||
pmoserver = ["dep:pmoserver", "dep:pmoconfig", "dep:axum", "dep:utoipa", "pmocache/openapi", "pmocache/pmoserver"]
|
||||
pmoconfig = ["dep:pmoconfig", "pmocache/pmoconfig"]
|
||||
pmoserver = ["pmoconfig", "dep:pmoserver", "dep:axum", "dep:utoipa", "pmocache/openapi", "pmocache/pmoserver"]
|
||||
|
||||
106
pmocovers/src/config_ext.rs
Normal file
106
pmocovers/src/config_ext.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
//! Extension pour intégrer le cache de couvertures dans pmoconfig
|
||||
//!
|
||||
//! Ce module fournit le trait `CoverCacheConfigExt` qui permet d'ajouter facilement
|
||||
//! des méthodes de gestion du cache de couvertures à pmoconfig::Config.
|
||||
|
||||
use anyhow::Result;
|
||||
use pmoconfig::Config;
|
||||
use pmocache::CacheConfigExt;
|
||||
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
|
||||
///
|
||||
/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
|
||||
/// au cache de couvertures avec conversion WebP.
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmoconfig::get_config;
|
||||
/// use pmocovers::CoverCacheConfigExt;
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache = config.create_cover_cache()?;
|
||||
///
|
||||
/// // Utiliser le cache
|
||||
/// let pk = cache.add_from_url("http://example.com/cover.jpg", Some("album:123")).await?;
|
||||
/// ```
|
||||
pub trait CoverCacheConfigExt {
|
||||
/// Récupère le répertoire du cache de couvertures
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le chemin absolu du répertoire du cache de couvertures (default: "cache_covers")
|
||||
fn get_covers_dir(&self) -> Result<String>;
|
||||
|
||||
/// Définit le répertoire du cache de couvertures
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - Chemin du répertoire (absolu ou relatif au config_dir)
|
||||
fn set_covers_dir(&self, directory: String) -> Result<()>;
|
||||
|
||||
/// Récupère la taille maximale du cache de couvertures
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le nombre maximal d'images dans le cache (default: 2000)
|
||||
fn get_covers_size(&self) -> Result<usize>;
|
||||
|
||||
/// Définit la taille maximale du cache de couvertures
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - Nombre maximal d'images
|
||||
fn set_covers_size(&self, size: usize) -> Result<()>;
|
||||
|
||||
/// Crée une instance du cache de couvertures configurée avec conversion WebP
|
||||
///
|
||||
/// Cette méthode factory crée un cache de couvertures en utilisant les paramètres
|
||||
/// de configuration (répertoire et taille) et active la conversion WebP
|
||||
/// automatique pour toutes les images téléchargées.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Une instance Arc du cache de couvertures configuré
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmoconfig::get_config;
|
||||
/// use pmocovers::CoverCacheConfigExt;
|
||||
///
|
||||
/// let config = get_config();
|
||||
/// let cache = config.create_cover_cache()?;
|
||||
///
|
||||
/// // Le cache est prêt à être utilisé avec conversion WebP automatique
|
||||
/// ```
|
||||
fn create_cover_cache(&self) -> Result<Arc<crate::Cache>>;
|
||||
}
|
||||
|
||||
impl CoverCacheConfigExt for Config {
|
||||
fn get_covers_dir(&self) -> Result<String> {
|
||||
self.get_cache_dir("cover_cache", DEFAULT_COVER_CACHE_DIR)
|
||||
}
|
||||
|
||||
fn set_covers_dir(&self, directory: String) -> Result<()> {
|
||||
self.set_cache_dir("cover_cache", directory)
|
||||
}
|
||||
|
||||
fn get_covers_size(&self) -> Result<usize> {
|
||||
self.get_cache_size("cover_cache", DEFAULT_COVER_CACHE_SIZE)
|
||||
}
|
||||
|
||||
fn set_covers_size(&self, size: usize) -> Result<()> {
|
||||
self.set_cache_size("cover_cache", size)
|
||||
}
|
||||
|
||||
fn create_cover_cache(&self) -> Result<Arc<crate::Cache>> {
|
||||
let dir = self.get_covers_dir()?;
|
||||
let size = self.get_covers_size()?;
|
||||
Ok(Arc::new(crate::cache::new_cache(&dir, size)?))
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,17 @@ pub mod webp;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod openapi;
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub mod config_ext;
|
||||
|
||||
pub use cache::{new_cache, Cache, CoversConfig};
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub use openapi::ApiDoc;
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub use config_ext::CoverCacheConfigExt;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
@@ -146,9 +152,10 @@ impl CoverCacheExt for pmoserver::Server {
|
||||
}
|
||||
|
||||
async fn init_cover_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
|
||||
use crate::CoverCacheConfigExt;
|
||||
let config = pmoconfig::get_config();
|
||||
let cache_dir = config.get_cover_cache_dir()?;
|
||||
let limit = config.get_cover_cache_size()?;
|
||||
let cache_dir = config.get_covers_dir()?;
|
||||
let limit = config.get_covers_size()?;
|
||||
self.init_cover_cache(&cache_dir, limit).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,25 @@ 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%
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `img` - Image à encoder
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Les données WebP encodées sous forme de vecteur d'octets
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocovers::webp::encode_webp;
|
||||
/// use image::DynamicImage;
|
||||
///
|
||||
/// let img = DynamicImage::new_rgba8(100, 100);
|
||||
/// let webp_data = encode_webp(&img)?;
|
||||
/// ```
|
||||
pub fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>> {
|
||||
let rgb_img = img.to_rgba8();
|
||||
let encoder = Encoder::from_rgba(&rgb_img, rgb_img.width(), rgb_img.height());
|
||||
@@ -9,6 +28,31 @@ 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
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `img` - Image à redimensionner
|
||||
/// * `size` - Taille du carré de sortie (en pixels)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Une nouvelle image carrée de taille `size × size` avec l'image originale centrée
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocovers::webp::ensure_square;
|
||||
/// use image::DynamicImage;
|
||||
///
|
||||
/// let img = DynamicImage::new_rgba8(800, 600);
|
||||
/// let square = ensure_square(&img, 256);
|
||||
/// assert_eq!(square.width(), 256);
|
||||
/// assert_eq!(square.height(), 256);
|
||||
/// ```
|
||||
pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage {
|
||||
let (width, height) = (img.width(), img.height());
|
||||
|
||||
@@ -38,6 +82,36 @@ pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage {
|
||||
square
|
||||
}
|
||||
|
||||
/// Génère une variante redimensionnée d'une image en cache
|
||||
///
|
||||
/// 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
|
||||
/// recalculs futurs.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache` - Instance du cache de couvertures
|
||||
/// * `pk` - Clé primaire de l'image
|
||||
/// * `size` - Taille de la variante (carré de `size × size`)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Les données WebP de la variante redimensionnée
|
||||
///
|
||||
/// # Comportement
|
||||
///
|
||||
/// 1. Si la variante existe déjà sur disque, elle est retournée directement
|
||||
/// 2. Sinon, l'image originale est chargée, redimensionnée et encodée en WebP
|
||||
/// 3. La variante est sauvegardée sur disque pour utilisation future
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use pmocovers::webp::generate_variant;
|
||||
///
|
||||
/// let cache = pmocovers::cache::new_cache("./cache", 1000)?;
|
||||
/// let variant_256 = generate_variant(&cache, "abc123", 256).await?;
|
||||
/// ```
|
||||
pub async fn generate_variant(
|
||||
cache: &super::cache::Cache,
|
||||
pk: &str,
|
||||
|
||||
@@ -303,7 +303,7 @@ async fn get_cache_stats(
|
||||
#[cfg(all(feature = "pmoserver", feature = "covers"))]
|
||||
async fn cache_album_image(mut album: Album, cover_cache: &Arc<pmocovers::Cache>) -> Album {
|
||||
if let Some(ref image_url) = album.image {
|
||||
match cover_cache.ensure_from_url(image_url).await {
|
||||
match cover_cache.add_from_url(image_url, None).await {
|
||||
Ok(pk) => {
|
||||
album.image_cached = Some(format!("/covers/images/{}", pk));
|
||||
}
|
||||
|
||||
@@ -367,19 +367,22 @@ impl UpnpServerExt for Server {
|
||||
}
|
||||
|
||||
async fn init_caches(&mut self) -> Result<(Arc<CoverCache>, Arc<AudioCache>), anyhow::Error> {
|
||||
use pmocovers::CoverCacheConfigExt;
|
||||
use pmoaudiocache::AudioCacheConfigExt;
|
||||
|
||||
let config = pmoconfig::get_config();
|
||||
|
||||
let cover_cache = self
|
||||
.init_cover_cache(
|
||||
&config.get_cover_cache_dir()?,
|
||||
config.get_cover_cache_size()?,
|
||||
&config.get_covers_dir()?,
|
||||
config.get_covers_size()?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let audio_cache = self
|
||||
.init_audio_cache(
|
||||
&config.get_audio_cache_dir()?,
|
||||
config.get_audio_cache_size()?,
|
||||
&config.get_audiocache_dir()?,
|
||||
config.get_audiocache_size()?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user