refactoring des caches
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ xxx
|
|||||||
**/.pmomusic.yml
|
**/.pmomusic.yml
|
||||||
**/.pmomusic_covers/**
|
**/.pmomusic_covers/**
|
||||||
**/.pmomusic_audio/**
|
**/.pmomusic_audio/**
|
||||||
|
/.pmomusic
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/target/
|
/target/
|
||||||
/.pmomusic_covers
|
/.pmomusic_covers
|
||||||
|
|||||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2382,10 +2382,13 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
|
"pmoconfig",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
"sha1",
|
"sha1",
|
||||||
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ import {
|
|||||||
purgeCache,
|
purgeCache,
|
||||||
consolidateCache,
|
consolidateCache,
|
||||||
getImageUrl,
|
getImageUrl,
|
||||||
|
waitForDownload,
|
||||||
} from "../services/coverCache";
|
} from "../services/coverCache";
|
||||||
|
|
||||||
// --- États ---
|
// --- États ---
|
||||||
@@ -190,11 +191,16 @@ async function handleAddImage() {
|
|||||||
isAdding.value = true; addError.value=""; addSuccess.value="";
|
isAdding.value = true; addError.value=""; addSuccess.value="";
|
||||||
try {
|
try {
|
||||||
const result = await addImage(newImageUrl.value);
|
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}`;
|
addSuccess.value = `Image added! PK: ${result.pk}`;
|
||||||
newImageUrl.value = "";
|
newImageUrl.value = "";
|
||||||
await refreshImages();
|
await refreshImages();
|
||||||
} catch(e:any) { addError.value = e.message ?? "Failed to add image"; }
|
} 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){
|
async function handleDeleteImage(pk:string){
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ export interface ApiError {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DownloadStatus {
|
||||||
|
pk: string;
|
||||||
|
finished: boolean;
|
||||||
|
current_size?: number;
|
||||||
|
expected_size?: number;
|
||||||
|
transformed_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liste toutes les images en cache
|
* 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
|
* Génère l'URL pour afficher une image
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -48,4 +48,5 @@ tracing-subscriber = "0.3"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["pmoserver"]
|
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")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub mod openapi;
|
pub mod openapi;
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoconfig")]
|
||||||
|
pub mod config_ext;
|
||||||
|
|
||||||
// Re-exports principaux
|
// Re-exports principaux
|
||||||
pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache};
|
pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache};
|
||||||
pub use metadata::AudioMetadata;
|
pub use metadata::AudioMetadata;
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoconfig")]
|
||||||
|
pub use config_ext::AudioCacheConfigExt;
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub use openapi::ApiDoc;
|
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>> {
|
async fn init_audio_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
|
||||||
|
use crate::AudioCacheConfigExt;
|
||||||
let config = pmoconfig::get_config();
|
let config = pmoconfig::get_config();
|
||||||
let cache_dir = config.get_audio_cache_dir()?;
|
let cache_dir = config.get_audiocache_dir()?;
|
||||||
let limit = config.get_audio_cache_size()?;
|
let limit = config.get_audiocache_size()?;
|
||||||
self.init_audio_cache(&cache_dir, limit).await
|
self.init_audio_cache(&cache_dir, limit).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,46 @@ pub struct AudioMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
/// Extrait les métadonnées d'un fichier audio
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -88,41 +128,7 @@ impl AudioMetadata {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn from_file(path: &Path) -> Result<Self> {
|
pub fn from_file(path: &Path) -> Result<Self> {
|
||||||
let tagged_file = Probe::open(path)?.options(ParseOptions::new()).read()?;
|
let tagged_file = Probe::open(path)?.options(ParseOptions::new()).read()?;
|
||||||
|
Ok(Self::from_tagged_file(tagged_file))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crée des métadonnées depuis des données brutes audio
|
/// Crée des métadonnées depuis des données brutes audio
|
||||||
@@ -136,41 +142,7 @@ impl AudioMetadata {
|
|||||||
.guess_file_type()?
|
.guess_file_type()?
|
||||||
.options(ParseOptions::new())
|
.options(ParseOptions::new())
|
||||||
.read()?;
|
.read()?;
|
||||||
|
Ok(Self::from_tagged_file(tagged_file))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Génère une clé de collection basée sur l'artiste et l'album
|
/// Génère une clé de collection basée sur l'artiste et l'album
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ futures-util = "0.3"
|
|||||||
|
|
||||||
# Cryptographie
|
# Cryptographie
|
||||||
sha1 = "0.10"
|
sha1 = "0.10"
|
||||||
|
sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
||||||
# Utilitaires
|
# Utilitaires
|
||||||
@@ -34,7 +35,12 @@ utoipa = { version = "5.3", optional = true }
|
|||||||
# Feature pour pmoserver (extension HTTP)
|
# Feature pour pmoserver (extension HTTP)
|
||||||
axum = { version = "0.8", optional = true }
|
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]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
openapi = ["dep:utoipa"]
|
openapi = ["dep:utoipa"]
|
||||||
pmoserver = ["dep:axum"]
|
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
|
//! 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.
|
//! 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::db::DB;
|
||||||
use crate::download::{
|
use crate::download::{
|
||||||
download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
|
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
|
/// 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.
|
/// Cette méthode utilise un système d'identifiants basé sur le contenu plutôt que sur l'URL.
|
||||||
/// Le download est tracké dans la map jusqu'à sa fin.
|
/// 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
|
/// # Arguments
|
||||||
///
|
///
|
||||||
@@ -142,21 +152,46 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # 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> {
|
pub async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||||
let pk = pk_from_url(url);
|
// 1. Télécharger les 512 premiers octets pour calculer le pk
|
||||||
let file_path = self.file_path(&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
|
// 2. Calculer le pk basé sur le contenu
|
||||||
{
|
let pk = crate::cache_trait::pk_from_content_header(&header);
|
||||||
let downloads = self.downloads.read().await;
|
tracing::debug!("Computed pk {} for URL {}", pk, url);
|
||||||
if downloads.contains_key(&pk) {
|
|
||||||
// Download déjà en cours, retourner la clé
|
// 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);
|
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 transformer = self.transformer_factory.as_ref().map(|f| f());
|
||||||
let download = download_with_transformer(&file_path, url, transformer);
|
let download = download_with_transformer(&file_path, url, transformer);
|
||||||
|
|
||||||
@@ -170,7 +205,6 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
self.db.add(&pk, url, collection)?;
|
self.db.add(&pk, url, collection)?;
|
||||||
|
|
||||||
// Appliquer la politique d'éviction LRU si nécessaire
|
// 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 {
|
if let Err(e) = self.enforce_limit().await {
|
||||||
tracing::warn!("Error enforcing cache limit: {}", e);
|
tracing::warn!("Error enforcing cache limit: {}", e);
|
||||||
}
|
}
|
||||||
@@ -179,9 +213,7 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
let downloads_clone = self.downloads.clone();
|
let downloads_clone = self.downloads.clone();
|
||||||
let pk_clone = pk.clone();
|
let pk_clone = pk.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Attendre la fin du téléchargement
|
|
||||||
let _ = download.wait_until_finished().await;
|
let _ = download.wait_until_finished().await;
|
||||||
// Retirer de la map
|
|
||||||
downloads_clone.write().await.remove(&pk_clone);
|
downloads_clone.write().await.remove(&pk_clone);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,38 +222,79 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
|
|
||||||
/// Ajoute un fichier à partir d'un flux asynchrone.
|
/// Ajoute un fichier à partir d'un flux asynchrone.
|
||||||
///
|
///
|
||||||
/// Le flux peut provenir de n'importe quelle source (stream HTTP custom, décodeur,
|
/// Cette méthode utilise le même système d'identifiants basé sur le contenu que `add_from_url`.
|
||||||
/// extraction en mémoire, etc.). Les mêmes transformers que `add_from_url` sont
|
/// Elle lit les 512 premiers octets du flux pour calculer l'identifiant, puis reconstitue
|
||||||
/// appliqués.
|
/// 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
|
/// # 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
|
/// * `reader` - Flux asynchrone fournissant les données
|
||||||
/// * `length` - Taille attendue (si connue)
|
/// * `length` - Taille attendue (si connue)
|
||||||
/// * `collection` - Collection optionnelle à laquelle appartient l'élément
|
/// * `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>(
|
pub async fn add_from_reader<R>(
|
||||||
&self,
|
&self,
|
||||||
source_uri: &str,
|
source_uri: &str,
|
||||||
reader: R,
|
mut reader: R,
|
||||||
length: Option<u64>,
|
length: Option<u64>,
|
||||||
collection: Option<&str>,
|
collection: Option<&str>,
|
||||||
) -> Result<String>
|
) -> Result<String>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Send + Unpin + 'static,
|
R: AsyncRead + Send + Unpin + 'static,
|
||||||
{
|
{
|
||||||
let pk = pk_from_url(source_uri);
|
// 1. Lire les 512 premiers octets pour calculer le pk
|
||||||
let file_path = self.file_path(&pk);
|
let header = crate::download::peek_reader_header(&mut reader, 512)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Failed to peek reader header: {}", e))?;
|
||||||
|
|
||||||
{
|
// 2. Calculer le pk basé sur le contenu
|
||||||
let downloads = self.downloads.read().await;
|
let pk = crate::cache_trait::pk_from_content_header(&header);
|
||||||
if downloads.contains_key(&pk) {
|
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);
|
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 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;
|
let mut downloads = self.downloads.write().await;
|
||||||
@@ -246,7 +319,9 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
|
|
||||||
/// Ajoute un fichier local au cache
|
/// 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
|
/// # Arguments
|
||||||
///
|
///
|
||||||
@@ -255,7 +330,21 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # 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> {
|
pub async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String> {
|
||||||
let canonical_path = std::fs::canonicalize(path)?;
|
let canonical_path = std::fs::canonicalize(path)?;
|
||||||
let file_url = format!("file://{}", canonical_path.display());
|
let file_url = format!("file://{}", canonical_path.display());
|
||||||
@@ -264,31 +353,12 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
.ok()
|
.ok()
|
||||||
.map(|m| m.len());
|
.map(|m| m.len());
|
||||||
let reader = tokio::fs::File::open(&canonical_path).await?;
|
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)
|
self.add_from_reader(&file_url, reader, length, collection)
|
||||||
.await
|
.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
|
/// Récupère le chemin d'un fichier dans le cache
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -620,10 +690,6 @@ impl<C: CacheConfig> FileCache<C> for Cache<C> {
|
|||||||
self.add_from_file(path, collection).await
|
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> {
|
async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||||
self.get(pk).await
|
self.get(pk).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,16 +116,6 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
|||||||
/// La clé primaire (pk) du fichier dans le cache
|
/// La clé primaire (pk) du fichier dans le cache
|
||||||
async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String>;
|
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
|
/// Récupère le chemin d'un fichier dans le cache
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -147,9 +137,48 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
|||||||
async fn consolidate(&self) -> Result<()>;
|
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.
|
/// 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 {
|
pub fn pk_from_url(url: &str) -> String {
|
||||||
let mut hasher = Sha1::new();
|
let mut hasher = Sha1::new();
|
||||||
hasher.update(url.as_bytes());
|
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;
|
s.finished = true;
|
||||||
Ok(())
|
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")]
|
#[cfg(feature = "openapi")]
|
||||||
pub mod openapi;
|
pub mod openapi;
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoconfig")]
|
||||||
|
pub mod config_ext;
|
||||||
|
|
||||||
pub use cache::{Cache, CacheConfig};
|
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 db::{CacheEntry, DB};
|
||||||
pub use download::{
|
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")]
|
#[cfg(feature = "pmoserver")]
|
||||||
@@ -148,3 +152,6 @@ pub use pmoserver_ext::{create_api_router, create_file_router, GenericCacheExt};
|
|||||||
|
|
||||||
#[cfg(all(feature = "pmoserver", feature = "openapi"))]
|
#[cfg(all(feature = "pmoserver", feature = "openapi"))]
|
||||||
pub use api::{AddItemRequest, AddItemResponse, DeleteItemResponse, DownloadStatus, ErrorResponse};
|
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
|
// Default values for configuration
|
||||||
const DEFAULT_HTTP_PORT: u16 = 8080;
|
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_BUFFER_CAPACITY: usize = 1000;
|
||||||
const DEFAULT_LOG_MIN_LEVEL: &str = "TRACE";
|
const DEFAULT_LOG_MIN_LEVEL: &str = "TRACE";
|
||||||
const DEFAULT_LOG_ENABLE_CONSOLE: bool = true;
|
const DEFAULT_LOG_ENABLE_CONSOLE: bool = true;
|
||||||
@@ -437,33 +433,57 @@ impl Config {
|
|||||||
Ok(absolute_path.to_string_lossy().to_string())
|
Ok(absolute_path.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic function to get cache directory
|
/// Récupère un répertoire géré par la configuration
|
||||||
fn get_cache_dir(&self, cache_type: &str, default_dir: &str) -> Result<String> {
|
///
|
||||||
let dir_path = match self.get_value(&["host", cache_type, "directory"]) {
|
/// 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,
|
Ok(Value::String(s)) => s,
|
||||||
_ => default_dir.to_string(),
|
_ => default.to_string(),
|
||||||
};
|
};
|
||||||
self.resolve_and_create_dir(&dir_path)
|
self.resolve_and_create_dir(&dir_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic function to set cache directory
|
/// Définit un répertoire géré par la configuration
|
||||||
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()> {
|
///
|
||||||
self.set_value(&["host", cache_type, "directory"], Value::String(directory))
|
/// # Arguments
|
||||||
}
|
///
|
||||||
|
/// * `path` - Chemin dans l'arbre de configuration (ex: `&["host", "cache", "directory"]`)
|
||||||
/// Generic function to get cache size
|
/// * `directory` - Chemin du répertoire (absolu ou relatif au config_dir)
|
||||||
fn get_cache_size(&self, cache_type: &str, default_size: usize) -> Result<usize> {
|
///
|
||||||
match self.get_value(&["host", cache_type, "size"])? {
|
/// # Exemple
|
||||||
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),
|
/// ```no_run
|
||||||
_ => Ok(default_size),
|
/// use pmoconfig::get_config;
|
||||||
}
|
///
|
||||||
}
|
/// let config = get_config();
|
||||||
|
/// config.set_managed_dir(&["host", "audio_cache", "directory"], "/var/cache/audio".to_string())?;
|
||||||
/// Generic function to set cache size
|
/// # Ok::<(), anyhow::Error>(())
|
||||||
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()> {
|
/// ```
|
||||||
let n = Number::from(size);
|
pub fn set_managed_dir(&self, path: &[&str], directory: String) -> Result<()> {
|
||||||
self.set_value(&["host", cache_type, "size"], Value::Number(n))
|
self.set_value(path, Value::String(directory))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the base URL for the HTTP server
|
/// Gets the base URL for the HTTP server
|
||||||
@@ -566,77 +586,6 @@ impl Config {
|
|||||||
self.set_value(&["devices", devtype, name, "udn"], Value::String(udn))
|
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!(
|
impl_string_config!(
|
||||||
/// Gets the Qobuz username from configuration
|
/// Gets the Qobuz username from configuration
|
||||||
|
|||||||
@@ -31,4 +31,5 @@ tracing = "0.1.41"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["pmoserver"]
|
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")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub mod openapi;
|
pub mod openapi;
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoconfig")]
|
||||||
|
pub mod config_ext;
|
||||||
|
|
||||||
pub use cache::{new_cache, Cache, CoversConfig};
|
pub use cache::{new_cache, Cache, CoversConfig};
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub use openapi::ApiDoc;
|
pub use openapi::ApiDoc;
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoconfig")]
|
||||||
|
pub use config_ext::CoverCacheConfigExt;
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
@@ -146,9 +152,10 @@ impl CoverCacheExt for pmoserver::Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn init_cover_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
|
async fn init_cover_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
|
||||||
|
use crate::CoverCacheConfigExt;
|
||||||
let config = pmoconfig::get_config();
|
let config = pmoconfig::get_config();
|
||||||
let cache_dir = config.get_cover_cache_dir()?;
|
let cache_dir = config.get_covers_dir()?;
|
||||||
let limit = config.get_cover_cache_size()?;
|
let limit = config.get_covers_size()?;
|
||||||
self.init_cover_cache(&cache_dir, limit).await
|
self.init_cover_cache(&cache_dir, limit).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,25 @@ use anyhow::Result;
|
|||||||
use image::{imageops::FilterType, DynamicImage};
|
use image::{imageops::FilterType, DynamicImage};
|
||||||
use webp::{Encoder, WebPMemory};
|
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>> {
|
pub fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>> {
|
||||||
let rgb_img = img.to_rgba8();
|
let rgb_img = img.to_rgba8();
|
||||||
let encoder = Encoder::from_rgba(&rgb_img, rgb_img.width(), rgb_img.height());
|
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())
|
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 {
|
pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage {
|
||||||
let (width, height) = (img.width(), img.height());
|
let (width, height) = (img.width(), img.height());
|
||||||
|
|
||||||
@@ -38,6 +82,36 @@ pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage {
|
|||||||
square
|
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(
|
pub async fn generate_variant(
|
||||||
cache: &super::cache::Cache,
|
cache: &super::cache::Cache,
|
||||||
pk: &str,
|
pk: &str,
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ async fn get_cache_stats(
|
|||||||
#[cfg(all(feature = "pmoserver", feature = "covers"))]
|
#[cfg(all(feature = "pmoserver", feature = "covers"))]
|
||||||
async fn cache_album_image(mut album: Album, cover_cache: &Arc<pmocovers::Cache>) -> Album {
|
async fn cache_album_image(mut album: Album, cover_cache: &Arc<pmocovers::Cache>) -> Album {
|
||||||
if let Some(ref image_url) = album.image {
|
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) => {
|
Ok(pk) => {
|
||||||
album.image_cached = Some(format!("/covers/images/{}", 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> {
|
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 config = pmoconfig::get_config();
|
||||||
|
|
||||||
let cover_cache = self
|
let cover_cache = self
|
||||||
.init_cover_cache(
|
.init_cover_cache(
|
||||||
&config.get_cover_cache_dir()?,
|
&config.get_covers_dir()?,
|
||||||
config.get_cover_cache_size()?,
|
config.get_covers_size()?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let audio_cache = self
|
let audio_cache = self
|
||||||
.init_audio_cache(
|
.init_audio_cache(
|
||||||
&config.get_audio_cache_dir()?,
|
&config.get_audiocache_dir()?,
|
||||||
config.get_audio_cache_size()?,
|
config.get_audiocache_size()?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user