la crate des playlists
This commit is contained in:
@@ -130,6 +130,20 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
||||
|
||||
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les fichiers manquants
|
||||
async fn consolidate(&self) -> Result<()>;
|
||||
|
||||
/// Vérifie si une clé primaire est valide (existe en DB et fichier présent)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire à vérifier
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` si l'entrée existe en base de données et que le fichier est présent
|
||||
fn is_valid_pk(&self, pk: &str) -> bool {
|
||||
self.get_database().get(pk, false).is_ok()
|
||||
&& self.file_path(pk).exists()
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère une clé primaire à partir des premiers octets d'un document
|
||||
@@ -163,20 +177,3 @@ pub fn pk_from_content_header(header: &[u8]) -> String {
|
||||
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());
|
||||
let result = hasher.finalize();
|
||||
hex::encode(&result[..8])
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ pub mod cache;
|
||||
pub mod cache_trait;
|
||||
pub mod db;
|
||||
pub mod download;
|
||||
pub mod metadata_macros;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod pmoserver_ext;
|
||||
@@ -129,7 +130,7 @@ pub mod openapi;
|
||||
pub mod config_ext;
|
||||
|
||||
pub use cache::{Cache, CacheConfig};
|
||||
pub use cache_trait::{pk_from_content_header, pk_from_url, FileCache};
|
||||
pub use cache_trait::{pk_from_content_header, FileCache};
|
||||
pub use db::{CacheEntry, DB};
|
||||
pub use download::{
|
||||
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
||||
|
||||
234
pmocache/src/metadata_macros.rs
Normal file
234
pmocache/src/metadata_macros.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
//! Macros pour générer des extension traits typés sur les métadonnées
|
||||
//!
|
||||
//! La macro `define_metadata_properties!` génère automatiquement :
|
||||
//! - Un trait avec des méthodes `get_XXX()` et `set_XXX()` pour chaque métadonnée
|
||||
//! - L'implémentation complète pour `Cache<Config>`
|
||||
//! - Les conversions JSON ↔ Rust selon le type
|
||||
//!
|
||||
//! # Types supportés
|
||||
//!
|
||||
//! - `String` : Métadonnée texte (JSON String)
|
||||
//! - `i64` : Métadonnée numérique entière signée (JSON Number)
|
||||
//! - `f64` : Métadonnée numérique décimale (JSON Number)
|
||||
//! - `bool` : Métadonnée booléenne (JSON Boolean)
|
||||
//! - `Value` : Métadonnée JSON brute (Array, Object, ou tout type JSON)
|
||||
//!
|
||||
//! # Mécanisme de stockage
|
||||
//!
|
||||
//! - Types simples (String, Number, Boolean) : stockés directement
|
||||
//! - Types complexes (Array, Object) : sérialisés en string JSON, parsés automatiquement à la lecture
|
||||
//! - La conversion est transparente grâce à `decode_metadata_value`
|
||||
//!
|
||||
//! # Exemple
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use pmocache::define_metadata_properties;
|
||||
//!
|
||||
//! struct AudioConfig;
|
||||
//! impl CacheConfig for AudioConfig { ... }
|
||||
//!
|
||||
//! define_metadata_properties! {
|
||||
//! AudioMetadataExt for pmocache::Cache<AudioConfig> {
|
||||
//! title: String as string,
|
||||
//! duration_secs: i64 as i64,
|
||||
//! custom_tags: serde_json::Value as value, // Pour JSON complexe
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Utilisation
|
||||
//! use AudioMetadataExt;
|
||||
//! let title = cache.get_title("pk123").await?;
|
||||
//! cache.set_title("pk123", "New Title".into()).await?;
|
||||
//!
|
||||
//! // JSON complexe
|
||||
//! let tags = json!({"mood": "happy", "bpm": 120});
|
||||
//! cache.set_custom_tags("pk123", tags).await?;
|
||||
//! ```
|
||||
|
||||
/// Génère un extension trait pour accéder aux métadonnées de manière typée
|
||||
///
|
||||
/// Cette macro génère un trait complet avec toutes les méthodes get/set
|
||||
/// et son implémentation pour le type de cache spécifié.
|
||||
///
|
||||
/// # Syntaxe
|
||||
///
|
||||
/// ```ignore
|
||||
/// define_metadata_properties! {
|
||||
/// TraitName for CacheType {
|
||||
/// field_name: RustType as type_kind,
|
||||
/// field_name2: RustType as type_kind,
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Type kinds available: `string`, `i64`, `f64`, `bool`, `value`
|
||||
///
|
||||
/// Pour chaque champ, génère :
|
||||
/// - `async fn get_FIELD(&self, pk: &str) -> Result<Option<TYPE>>`
|
||||
/// - `async fn set_FIELD(&self, pk: &str, value: TYPE) -> Result<()>`
|
||||
///
|
||||
/// La clé JSON utilisée est le nom du champ (ex: `title` → clé `"title"`).
|
||||
#[macro_export]
|
||||
macro_rules! define_metadata_properties {
|
||||
(
|
||||
$trait_name:ident for $cache_type:ty {
|
||||
$(
|
||||
$field:ident: $rust_type:ty as $type_kind:ident
|
||||
),* $(,)?
|
||||
}
|
||||
) => {
|
||||
// Définition du trait
|
||||
pub trait $trait_name {
|
||||
$(
|
||||
// Génère get_FIELD
|
||||
paste::paste! {
|
||||
async fn [<get_ $field>](&self, pk: &str) -> anyhow::Result<Option<$rust_type>>;
|
||||
}
|
||||
|
||||
// Génère set_FIELD
|
||||
paste::paste! {
|
||||
async fn [<set_ $field>](&self, pk: &str, value: $rust_type) -> anyhow::Result<()>;
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
// Implémentation du trait
|
||||
impl $trait_name for $cache_type {
|
||||
$(
|
||||
// Implémentation de get_FIELD selon le type
|
||||
$crate::__impl_getter!($field, $rust_type, $type_kind);
|
||||
|
||||
// Implémentation de set_FIELD selon le type
|
||||
$crate::__impl_setter!($field, $rust_type, $type_kind);
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Macros internes pour générer les getters selon le type
|
||||
// ============================================================================
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __impl_getter {
|
||||
// String - utilise get_a_metadata_as_string
|
||||
($field:ident, $rust_type:ty, string) => {
|
||||
paste::paste! {
|
||||
async fn [<get_ $field>](&self, pk: &str) -> anyhow::Result<Option<$rust_type>> {
|
||||
self.get_a_metadata_as_string(pk, stringify!($field)).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// i64 - utilise get_a_metadata_as_number puis as_i64()
|
||||
($field:ident, $rust_type:ty, i64) => {
|
||||
paste::paste! {
|
||||
async fn [<get_ $field>](&self, pk: &str) -> anyhow::Result<Option<$rust_type>> {
|
||||
match self.get_a_metadata_as_number(pk, stringify!($field)).await? {
|
||||
Some(n) => Ok(n.as_i64()),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// f64 - utilise get_a_metadata_as_number puis as_f64()
|
||||
($field:ident, $rust_type:ty, f64) => {
|
||||
paste::paste! {
|
||||
async fn [<get_ $field>](&self, pk: &str) -> anyhow::Result<Option<$rust_type>> {
|
||||
match self.get_a_metadata_as_number(pk, stringify!($field)).await? {
|
||||
Some(n) => Ok(n.as_f64()),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// bool - utilise get_a_metadata_as_bool
|
||||
($field:ident, $rust_type:ty, bool) => {
|
||||
paste::paste! {
|
||||
async fn [<get_ $field>](&self, pk: &str) -> anyhow::Result<Option<$rust_type>> {
|
||||
self.get_a_metadata_as_bool(pk, stringify!($field)).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Value - utilise get_a_metadata directement (retourne JSON brut)
|
||||
($field:ident, $rust_type:ty, value) => {
|
||||
paste::paste! {
|
||||
async fn [<get_ $field>](&self, pk: &str) -> anyhow::Result<Option<$rust_type>> {
|
||||
self.get_a_metadata(pk, stringify!($field)).await
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Macros internes pour générer les setters selon le type
|
||||
// ============================================================================
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __impl_setter {
|
||||
// String - stocke comme Value::String
|
||||
($field:ident, $rust_type:ty, string) => {
|
||||
paste::paste! {
|
||||
async fn [<set_ $field>](&self, pk: &str, value: $rust_type) -> anyhow::Result<()> {
|
||||
use serde_json::Value;
|
||||
self.db.set_a_metadata(pk, stringify!($field), Value::String(value))
|
||||
.map_err(|e| anyhow::anyhow!("DB error: {}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// i64 - stocke comme Value::Number
|
||||
($field:ident, $rust_type:ty, i64) => {
|
||||
paste::paste! {
|
||||
async fn [<set_ $field>](&self, pk: &str, value: $rust_type) -> anyhow::Result<()> {
|
||||
use serde_json::{Value, Number};
|
||||
self.db.set_a_metadata(
|
||||
pk,
|
||||
stringify!($field),
|
||||
Value::Number(Number::from(value))
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("DB error: {}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// f64 - stocke comme Value::Number (avec validation)
|
||||
($field:ident, $rust_type:ty, f64) => {
|
||||
paste::paste! {
|
||||
async fn [<set_ $field>](&self, pk: &str, value: $rust_type) -> anyhow::Result<()> {
|
||||
use serde_json::{Value, Number};
|
||||
let number = Number::from_f64(value)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid f64 value: {}", value))?;
|
||||
self.db.set_a_metadata(pk, stringify!($field), Value::Number(number))
|
||||
.map_err(|e| anyhow::anyhow!("DB error: {}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// bool - stocke comme Value::Bool
|
||||
($field:ident, $rust_type:ty, bool) => {
|
||||
paste::paste! {
|
||||
async fn [<set_ $field>](&self, pk: &str, value: $rust_type) -> anyhow::Result<()> {
|
||||
use serde_json::Value;
|
||||
self.db.set_a_metadata(pk, stringify!($field), Value::Bool(value))
|
||||
.map_err(|e| anyhow::anyhow!("DB error: {}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Value - stocke directement (Array/Object sont sérialisés automatiquement)
|
||||
($field:ident, $rust_type:ty, value) => {
|
||||
paste::paste! {
|
||||
async fn [<set_ $field>](&self, pk: &str, value: $rust_type) -> anyhow::Result<()> {
|
||||
self.db.set_a_metadata(pk, stringify!($field), value)
|
||||
.map_err(|e| anyhow::anyhow!("DB error: {}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user