Guide
+d’implémentation d’une nouvelle MusicSource
+
Ce document décrit comment implémenter une nouvelle source musicale
+dans l’écosystème PMOMusic en suivant le trait MusicSource
+défini dans le crate pmosource.
Une MusicSource est une abstraction qui représente une
+source de contenu musical dans PMOMusic. Elle peut être :
+
+
Dynamique (FIFO) : Radio Paradise, streaming radio,
+playlists live
+
Statique : Albums Qobuz, bibliothèque locale,
+playlists fixes
+
+
Le trait MusicSource définit une interface unifiée pour
+: - La navigation UPnP ContentDirectory (browse) - La résolution d’URI
+audio (avec cache) - La gestion de playlists FIFO (pour les sources
+dynamiques) - Le suivi des changements (update_id, last_change)
<source-id> # Racine
+<source-id>:albums # Container albums
+<source-id>:album:<album_id> # Album spécifique
+<source-id>:track:<track_id> # Track spécifique
+<source-id>:playlist:<playlist_id> # Playlist spécifique
+
Types de BrowseResult : -
+Containers(Vec<Container>) : Liste de containers
+(navigation) - Items(Vec<Item>) : Liste de tracks
+(lecture) - Mixed { containers, items } : Les deux (album
+avec tracks)
Points clés : - Utiliser
+pmoplaylist::PlaylistManager singleton - Incrémenter
+update_id à chaque modification - Mettre à jour
+last_change à chaque modification - Adapter les IDs des
+items au schéma de la source
+
4. Support statique
+(albums, bibliothèques)
+
Si votre source est statique (catalogue, albums) :
+
#[async_trait]
+impl MusicSource for CatalogSource {
+fn supports_fifo(&self) ->bool{
+false// Pas de FIFO
+}
+
+asyncfn append_track(&self, _track: Item) ->Result<()>{
+Err(MusicSourceError::NotSupported(
+"This source is read-only".to_string()
+ ))
+}
+
+asyncfn remove_oldest(&self) ->Result<Option<Item>>{
+Ok(None) // Pas de suppression
+}
+
+asyncfn update_id(&self) ->u32{
+0// Jamais de changement
+}
+
+asyncfn last_change(&self) ->Option<SystemTime>{
+None// Pas de suivi des changements
+}
+
+asyncfn get_items(&self, offset:usize, count:usize) ->Result<Vec<Item>>{
+// Retourner une liste paginée depuis le catalogue
+self.get_catalog_items(offset, count).await
+}
+}
+
Patterns d’implémentation
+
Pattern 1 :
+Source dynamique avec FIFO (Radio Paradise)
+
Caractéristiques : - Flux continu de tracks -
+Capacité limitée (50-100 tracks) - Suppression automatique des plus
+anciens - supports_fifo() = true
Points clés : - Callbacks sur
+pmoplaylist pour détecter les changements - Notification du
+ContentDirectory via un notifier injecté - update_counter
+partagé via Arc<RwLock<u32>>
+
Pattern 2
+: Source catalogue avec playlists lazy (Qobuz)
+
Caractéristiques : - Catalogue vaste (millions de
+tracks) - Playlists créées à la demande - Cache lazy (cover eager, audio
+lazy) - supports_fifo() = false
Points clés : - Cache lazy pour l’audio (téléchargé
+à la demande) - Cache eager pour les covers (petit, UI en a besoin) -
+Playlists avec TTL (7 jours) - LazyProvider pour
+télécharger l’audio lors de la lecture
+
Pattern 3
+: Adaptation des IDs entre playlist et source
+
Lorsqu’une source utilise pmoplaylist, les items
+retournés ont des IDs génériques. Il faut les adapter au schéma de la
+source :
Points clés : - Stocker source_track_id
+dans les metadata du cache audio - Reconstituer l’ID correct lors de la
+récupération depuis playlist - Normaliser URLs (relatives → absolues) -
+Ajouter champs requis par certains clients UPnP
Points d’intérêt : - Structure avec
+Arc<RwLock<>> pour l’état partagé - Callbacks
+sur playlists pour détecter les changements - Notifier injecté pour
+ContentDirectory - Adaptation des IDs playlist → Radio Paradise -
+Support de 4 canaux avec sous-containers
+
Schema d’Object ID :
+
radio-paradise # Racine
+radio-paradise:channel:{slug} # Canal (main, mellow, rock, eclectic)
+radio-paradise:channel:{slug}:live # Stream live
+radio-paradise:channel:{slug}:liveplaylist # Playlist live (queue)
+radio-paradise:channel:{slug}:liveplaylist:track:{pk} # Track dans queue
+radio-paradise:channel:{slug}:history # Historique
+radio-paradise:channel:{slug}:history:track:{pk} # Track dans historique
Ce document décrit le pattern architectural utilisé dans PMOMusic
+pour étendre la configuration centralisée
+(pmoconfig::Config) avec des fonctionnalités spécifiques à
+chaque crate.
+
Objectif
+
Permettre à chaque crate du projet d’ajouter ses propres méthodes de
+configuration sans modifier directement pmoconfig, tout en
+maintenant une interface cohérente et type-safe.
+
Principe
+
Chaque crate qui nécessite un accès à la configuration implémente un
+trait d’extension pour pmoconfig::Config.
+Ce trait définit des méthodes helpers spécifiques au domaine du crate
+(cache, authentification, UPnP, etc.).
+
Architecture du pattern
+
Structure de base
+
pmoconfig/ # Crate de configuration centralisée
+ ├── Config # Struct principale avec get_value/set_value génériques
+ └── encryption # Module de chiffrement des mots de passe
+
+pmocrate/ # Crate spécialisé (audio, qobuz, upnp, etc.)
+ └── config_ext.rs # Trait d'extension pour Config
+ ├── DEFAULT_* # Constantes pour valeurs par défaut
+ ├── XxxConfigExt # Trait d'extension
+ └── impl # Implémentation du trait pour Config
//! Extension pour intégrer [fonctionnalité] dans pmoconfig
+//!
+//! Ce module fournit le trait `XxxConfigExt` qui permet d'ajouter facilement
+//! des méthodes de gestion de [fonctionnalité] à pmoconfig::Config.
+
+useanyhow::Result;
+usepmoconfig::Config;
+useserde_yaml::Value;
+
+// Constantes pour valeurs par défaut
+const DEFAULT_XXX_DIR:&str="cache_xxx";
+const DEFAULT_XXX_SIZE:usize=1000;
+
+/// Trait d'extension pour gérer [fonctionnalité] dans pmoconfig
+///
+/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
+/// à la gestion de [fonctionnalité].
+///
+/// # Exemple
+///
+/// ```rust,ignore
+/// use pmoconfig::get_config;
+/// use pmoxxx::XxxConfigExt;
+///
+/// let config = get_config();
+/// let value = config.get_xxx_value()?;
+/// ```
+pubtrait XxxConfigExt {
+/// Documentation de la méthode getter
+fn get_xxx_value(&self) ->Result<Type>;
+
+/// Documentation de la méthode setter
+fn set_xxx_value(&self, value: Type) ->Result<()>;
+}
+
+impl XxxConfigExt for Config {
+fn get_xxx_value(&self) ->Result<Type>{
+// Implémentation
+}
+
+fn set_xxx_value(&self, value: Type) ->Result<()>{
+// Implémentation
+}
+}
+
2. Patterns de chemins YAML
+
Les chemins dans la configuration suivent une hiérarchie logique
+:
+
Configuration hôte/système
+
// Chemins sous "host"
+&["host","cache_type","directory"] // Répertoires de cache
+&["host","cache_type","size"] // Tailles de cache
+&["host","upnp","manufacturer"] // Configuration UPnP
+
Configuration
+comptes/services
+
// Chemins sous "accounts"
+&["accounts","service","username"]
+&["accounts","service","password"]
+&["accounts","service","auth_token"]
+
Configuration sources
+
// Chemins sous "sources"
+&["sources","source_name","enabled"]
+&["sources","source_name","default_channel"]
fn clear_xxx_info(&self) ->Result<()>{
+// Ne pas propager les erreurs (valeurs peuvent ne pas exister)
+let _ =self.set_value(&["path","to","field1"],Value::String(String::new()));
+let _ =self.set_value(&["path","to","field2"],Value::Number(Number::from(0)));
+Ok(())
+}
Usage : Différencier plusieurs instances du serveur
+(dev, prod, test).
+
Bonnes pratiques
+
1. Nommage des méthodes
+
// ✅ BON : Préfixer avec le nom du service/composant
+fn get_qobuz_username(&self) ->Result<String>
+fn get_cache_dir(&self, cache_type:&str,default:&str) ->Result<String>
+fn get_paradise_enabled(&self) ->Result<bool>
+
+// ❌ MAUVAIS : Nom trop générique
+fn get_username(&self) ->Result<String>
+fn get_directory(&self) ->Result<String>
+fn is_enabled(&self) ->bool
+
2. Gestion des erreurs
+
// ✅ BON : Retourner Result pour les valeurs obligatoires
+fn get_xxx_username(&self) ->Result<String>{
+matchself.get_value(&["accounts","xxx","username"])?{
+Value::String(s) =>Ok(s),
+ _ =>Err(anyhow!("XXX username not configured")),
+}
+}
+
+// ✅ BON : Retourner Option pour les valeurs optionnelles
+fn get_xxx_token(&self) ->Result<Option<String>>
+
+// ✅ BON : Retourner bool pour les checks (sans erreur)
+fn is_xxx_valid(&self) ->bool
+
+// ❌ MAUVAIS : Panic ou unwrap
+fn get_xxx_value(&self) ->String{
+self.get_value(&["path"]).unwrap().as_str().unwrap()
+}
+
3. Valeurs par défaut
+
// ✅ BON : Constantes en haut du fichier
+const DEFAULT_CACHE_SIZE:usize=500;
+const DEFAULT_CACHE_DIR:&str="cache_audio";
+
+// ✅ BON : Valeurs par défaut documentées
+/// Récupère la taille du cache
+///
+/// # Returns
+///
+/// Le nombre maximal d'éléments (default: 500)
+fn get_cache_size(&self) ->Result<usize>
+
+// ❌ MAUVAIS : Magic numbers
+fn get_cache_size(&self) ->Result<usize>{
+matchself.get_value(&["cache","size"]) {
+Ok(Value::Number(n)) =>Ok(n.as_u64().unwrap() asusize),
+ _ =>Ok(500),// Où vient ce 500 ?
+}
+}
+
4. Documentation
+
Chaque méthode doit avoir :
+
/// Description courte de ce que fait la méthode
+///
+/// # Arguments (si applicable)
+///
+/// * `param` - Description du paramètre
+///
+/// # Returns
+///
+/// Description de ce qui est retourné (avec valeur par défaut si applicable)
+///
+/// # Errors (si Result)
+///
+/// Description des cas d'erreur
+///
+/// # Exemple
+///
+/// ```rust,ignore
+/// use pmoconfig::get_config;
+/// use pmoxxx::XxxConfigExt;
+///
+/// let config = get_config();
+/// let value = config.get_xxx_value()?;
+/// ```
+fn get_xxx_value(&self) ->Result<Type>;
+
5. Organisation du code
+
Structure du fichier
+config_ext.rs
+
//! Documentation du module
+
+// Imports
+useanyhow::Result;
+usepmoconfig::Config;
+useserde_yaml::Value;
+
+// Constantes
+const DEFAULT_XXX: Type = value;
+
+// Trait
+pubtrait XxxConfigExt {
+// Méthodes groupées logiquement
+}
+
+// Implémentation
+impl XxxConfigExt for Config {
+// Méthodes dans le même ordre que le trait
+}
+
+// Tests (optionnel)
+#[cfg(test)]
+mod tests {
+usesuper::*;
+}
Le pattern pmoserver_ext permet d’étendre les
+fonctionnalités du serveur HTTP pmoserver de manière
+modulaire et découplée. Chaque crate spécialisée peut ajouter ses
+propres routes HTTP sans que pmoserver ne dépende de ces
+crates.
+
Principe : Définir un trait d’extension que
+pmoserver::Server implémente via une feature Cargo.
pmoserver::Server expose ces méthodes pour enregistrer
+des routes :
+
+
+
+
+
+
+
+
Méthode
+
Usage
+
+
+
+
+
add_handler(path, handler)
+
Ajoute un handler simple sans état
+
+
+
add_handler_with_state(path, handler, state)
+
Ajoute un handler avec état partagé
+
+
+
add_router(path, router)
+
Monte un sous-router Axum
+
+
+
add_openapi(router, doc, tag)
+
Enregistre une API avec documentation OpenAPI
+
+
+
add_spa::<W>(path)
+
Sert une Single Page Application (RustEmbed)
+
+
+
base_url()
+
Récupère l’URL de base du serveur
+
+
+
+
Documentation OpenAPI avec
+utoipa
+
La documentation OpenAPI est essentielle pour une extension
+pmoserver. Elle génère automatiquement une interface
+Swagger UI et documente les endpoints de l’API.
+
Configuration de base
+
Ajouter utoipa dans Cargo.toml :
+
[dependencies]
+utoipa={ version ="5", features =["axum_extras"] }
+serde={ version ="1", features =["derive"] }
+
1. Définir les schémas de
+données
+
Annoter les structures de réponse/requête avec
+#[derive(ToSchema)] :
+
useserde::{Serialize, Deserialize};
+useutoipa::ToSchema;
+
+/// Information sur un item
+#[derive(Debug,Clone, Serialize, Deserialize, ToSchema)]
+pubstruct ItemInfo {
+/// ID unique de l'item
+#[schema(example ="item-123")]
+pub id:String,
+
+/// Nom de l'item
+#[schema(example ="Mon Item")]
+pub name:String,
+
+/// Description optionnelle
+#[schema(example ="Une description détaillée")]
+pub description:Option<String>,
+
+/// Timestamp de création (millisecondes)
+#[schema(example =1234567890)]
+pub created_at:u64,
+}
+
+/// Liste d'items
+#[derive(Debug,Clone, Serialize, ToSchema)]
+pubstruct ItemList {
+/// Nombre total d'items
+pub total:usize,
+
+/// Items de la page courante
+pub items:Vec<ItemInfo>,
+}
+
+/// Requête de création d'item
+#[derive(Debug,Clone, Deserialize, ToSchema)]
+pubstruct CreateItemRequest {
+/// Nom de l'item à créer
+#[schema(example ="Nouvel Item")]
+pub name:String,
+
+/// Description optionnelle
+pub description:Option<String>,
+}
+
+/// Réponse d'erreur standard
+#[derive(Debug,Clone, Serialize, ToSchema)]
+pubstruct ErrorResponse {
+/// Message d'erreur
+#[schema(example ="Item not found")]
+pub error:String,
+}
+
Points clés : -
+#[schema(example = "...")] : Fournit des exemples pour la
+doc Swagger - Documenter chaque champ avec /// pour
+apparaître dans l’API - Utiliser Option<T> pour les
+champs optionnels
+
2. Annoter les handlers
+
Utiliser #[utoipa::path(...)] pour documenter chaque
+endpoint :
+
/// GET /items - Liste tous les items
+#[utoipa::path(
+ get,
+ path ="/items",
+ params(
+("limit"=Option<u32>, Query, description ="Nombre max d'items à retourner"),
+("offset"=Option<u32>, Query, description ="Offset pour la pagination")
+),
+ responses(
+(status =200, description ="Liste des items", body = ItemList),
+(status =500, description ="Erreur serveur", body = ErrorResponse)
+),
+ tag ="items"
+)]
+asyncfn list_items(
+ State(state): State<XxxState>,
+ Query(params): Query<ListParams>,
+) ->Result<Json<ItemList>, (StatusCode, Json<ErrorResponse>)>{
+let items = state.resource.list_items(params.limit, params.offset)
+.map_err(|e| (
+StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse { error: e.to_string() })
+ ))?;
+
+Ok(Json(ItemList {
+ total: items.len(),
+ items,
+}))
+}
+
+/// GET /items/{id} - Récupère un item spécifique
+#[utoipa::path(
+ get,
+ path ="/items/{id}",
+ params(
+("id"=String,Path, description ="ID unique de l'item")
+),
+ responses(
+(status =200, description ="Item trouvé", body = ItemInfo),
+(status =404, description ="Item non trouvé", body = ErrorResponse),
+(status =500, description ="Erreur serveur", body = ErrorResponse)
+),
+ tag ="items"
+)]
+asyncfn get_item(
+ State(state): State<XxxState>,
+Path(id):Path<String>,
+) ->Result<Json<ItemInfo>, (StatusCode, Json<ErrorResponse>)>{
+ state.resource.get_item(&id)
+.ok_or_else(|| (
+StatusCode::NOT_FOUND,
+ Json(ErrorResponse {
+ error:format!("Item {} not found", id)
+})
+ ))
+.map(Json)
+}
+
+/// POST /items - Crée un nouvel item
+#[utoipa::path(
+ post,
+ path ="/items",
+ request_body = CreateItemRequest,
+ responses(
+(status =201, description ="Item créé", body = ItemInfo),
+(status =400, description ="Requête invalide", body = ErrorResponse),
+(status =500, description ="Erreur serveur", body = ErrorResponse)
+),
+ tag ="items"
+)]
+asyncfn create_item(
+ State(state): State<XxxState>,
+ Json(req): Json<CreateItemRequest>,
+) ->Result<(StatusCode, Json<ItemInfo>), (StatusCode, Json<ErrorResponse>)>{
+let item = state.resource.create_item(req.name, req.description)
+.map_err(|e| (
+StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse { error: e.to_string() })
+ ))?;
+
+Ok((StatusCode::CREATED, Json(item)))
+}
+
+/// DELETE /items/{id} - Supprime un item
+#[utoipa::path(
+ delete,
+ path ="/items/{id}",
+ params(
+("id"=String,Path, description ="ID unique de l'item")
+),
+ responses(
+(status =204, description ="Item supprimé"),
+(status =404, description ="Item non trouvé", body = ErrorResponse),
+(status =500, description ="Erreur serveur", body = ErrorResponse)
+),
+ tag ="items"
+)]
+asyncfn delete_item(
+ State(state): State<XxxState>,
+Path(id):Path<String>,
+) ->Result<StatusCode, (StatusCode, Json<ErrorResponse>)>{
+ state.resource.delete_item(&id)
+.map_err(|e| (
+StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ErrorResponse { error: e.to_string() })
+ ))?;
+
+Ok(StatusCode::NO_CONTENT)
+}
+
Structure de #[utoipa::path] : -
+Méthode HTTP : get, post,
+put, delete, patch -
+path : Chemin de l’endpoint (doit
+correspondre au router) - params :
+Paramètres Path ou Query avec description -
+request_body : Type du body pour POST/PUT
+- responses : Liste des réponses possibles
+avec codes HTTP - tag : Groupe d’endpoints
+dans Swagger UI
+
3. Créer la structure OpenAPI
+
Définir une structure avec #[derive(OpenApi)] :
+
useutoipa::OpenApi;
+
+/// Documentation OpenAPI pour l'API XXX
+#[derive(OpenApi)]
+#[openapi(
+ info(
+ title ="XXX API",
+ version ="1.0.0",
+ description =r#"
+# API REST pour XXX
+
+Cette API permet de gérer les items XXX avec les fonctionnalités suivantes :
+
+## Fonctionnalités
+
+- **CRUD complet** : Création, lecture, mise à jour et suppression d'items
+- **Pagination** : Support de limit/offset pour les listes
+- **Filtrage** : Recherche par critères multiples
+- **Validation** : Vérification automatique des données
+
+## Exemples d'utilisation
+
+### Lister les items
+
GET /api/xxx/items?limit=10&offset=0
+
+### Créer un item
+
POST /api/xxx/items Content-Type: application/json
Sections importantes : -
+info : Titre, version et description
+Markdown de l’API - paths : Liste des
+fonctions handler annotées -
+components(schemas(...)) : Liste des
+structures ToSchema - tags :
+Organisation des endpoints en groupes
+
4. Enregistrer l’API avec
+OpenAPI
+
Dans l’implémentation du trait d’extension :
+
#[async_trait]
+impl XxxExt forpmoserver::Server {
+asyncfn init_xxx(&mutself) ->anyhow::Result<Arc<Resource>>{
+let resource =Arc::new(Resource::new()?);
+let state = XxxState { resource: resource.clone() };
+
+// Créer le router avec les routes
+let router =Router::new()
+.route("/items", get(list_items).post(create_item))
+.route("/items/{id}", get(get_item).delete(delete_item))
+.with_state(state);
+
+// Enregistrer avec OpenAPI (génère aussi /swagger-ui/xxx)
+let openapi =ApiDoc::openapi();
+self.add_openapi(router, openapi,"xxx").await;
+
+Ok(resource)
+}
+}
+
Ce que fait add_openapi : - Monte le
+router sur /api/{tag}/ - Génère la spec OpenAPI JSON sur
+/api/{tag}/openapi.json - Crée une UI Swagger sur
+/swagger-ui/{tag}/
+
5. Exemple complet : Radio
+Paradise
+
Extrait de
+pmoparadise/src/pmoserver_ext.rs:93-315
+
/// Information sur un morceau
+#[derive(Debug,Clone, Serialize, ToSchema)]
+pubstruct SongInfo {
+/// Index dans le block
+pub index:usize,
+/// Artiste
+pub artist:String,
+/// Titre
+pub title:String,
+/// Album
+pub album:String,
+/// Année
+pub year:Option<u32>,
+/// Temps écoulé depuis le début du block (ms)
+pub elapsed_ms:u64,
+/// Durée du morceau (ms)
+pub duration_ms:u64,
+/// URL de la pochette
+pub cover_url:Option<String>,
+}
+
+/// Réponse pour l'URL de streaming
+#[derive(Debug,Clone, Serialize, ToSchema)]
+pubstruct StreamUrlResponse {
+/// Event ID du block
+#[schema(example =1234567)]
+pub event:u64,
+/// URL de streaming FLAC
+#[schema(example ="https://apps.radioparadise.com/blocks/chan/0/4/1234567-1234580.flac")]
+pub stream_url:String,
+/// Durée totale (ms)
+#[schema(example =900000)]
+pub length_ms:u64,
+}
+
+/// GET /stream-url/{event_id} - Récupère l'URL de streaming
+#[utoipa::path(
+ get,
+ path ="/stream-url/{event_id}",
+ params(
+("event_id"=u64,Path, description ="Event ID du block"),
+("channel"=Option<u8>, Query, description ="Channel ID (0-3)")
+),
+ responses(
+(status =200, description ="URL de streaming", body = StreamUrlResponse),
+(status =500, description ="Erreur serveur")
+),
+ tag ="Radio Paradise"
+)]
+asyncfn get_stream_url(
+ State(state): State<RadioParadiseState>,
+Path(event_id):Path<u64>,
+ Query(params): Query<ParadiseQuery>,
+) ->Result<Json<StreamUrlResponse>, StatusCode>{
+let client = state.client_for_params(¶ms).await?;
+let block = client.get_block(Some(event_id)).await.map_err(|e|{
+tracing::error!("Failed to fetch block {}: {}", event_id, e);
+StatusCode::INTERNAL_SERVER_ERROR
+})?;
+
+Ok(Json(StreamUrlResponse {
+ event: block.event,
+ stream_url: block.url,
+ length_ms: block.length,
+}))
+}
+
+#[derive(OpenApi)]
+#[openapi(
+ info(
+ title ="Radio Paradise API",
+ version ="1.0.0",
+ description ="API REST pour accéder aux métadonnées Radio Paradise"
+),
+ paths(
+ get_now_playing,
+ get_current_block,
+ get_stream_url,
+),
+ components(schemas(
+ SongInfo,
+ StreamUrlResponse,
+)),
+ tags(
+(name ="Radio Paradise", description ="Endpoints Radio Paradise")
+)
+)]
+pubstruct RadioParadiseApiDoc;
+
Résultat : Interface Swagger
+
Après avoir appelé init_xxx(), l’API est accessible
+:
L’interface Swagger permet : - Parcourir tous les endpoints avec leur
+documentation - Tester les requêtes directement depuis le navigateur -
+Voir les schémas de données avec exemples - Consulter les codes de
+réponse HTTP possibles
+
Patterns courants
+
Pattern 1 : Extension
+simple avec router
+
Exemple : pmoparadise
+(pmoparadise/src/pmoserver_ext.rs:367-392)
+
#[async_trait]
+impl RadioParadiseExt forpmoserver::Server {
+asyncfn init_radioparadise(&mutself) ->anyhow::Result<State>{
+let state =RadioParadiseState::new().await?;
+
+// Créer le router API
+let api_router = create_api_router(state.clone());
+
+// Enregistrer avec OpenAPI
+self.add_openapi(api_router,ApiDoc::openapi(),"radioparadise")
+.await;
+
+Ok(state)
+}
+}
+
Pattern 2 :
+Extension avec cache et fichiers
+
Exemple : pmoaudiocache
+(pmoaudiocache/src/lib.rs:225-260)
Rapport
+Final : Items Épinglables et TTL dans PMOcache
+
Objectif de la tâche
+
Étendre le système de cache PMOcache pour permettre un contrôle plus
+fin des règles de suppression des items. L’objectif était double :
+
+
Phase 1 : Implémenter un système d’items
+épinglables (pinned) protégés de l’éviction LRU, avec support du TTL
+(Time To Live) pour l’expiration automatique
+
Phase 2 : Exposer ces fonctionnalités via une API
+REST complète avec documentation OpenAPI
+
+
Contexte
+
La crate PMOcache implémente un système de cache avec : - Capacité
+maximale configurable - Politique d’éviction LRU (Least Recently Used) -
+TTL optionnel pour les items
+
La nouvelle fonctionnalité permet de : - Épingler
+des items critiques pour les rendre permanents -
+Exclure les items épinglés du comptage de la limite du
+cache - Définir un TTL pour supprimer automatiquement
+les items temporaires - Garantir l’incompatibilité
+entre pinning et TTL (règle métier)
Un item ne peut jamais être à la fois épinglé ET
+avoir un TTL :
+
+
+
+
État actuel
+
Action
+
Résultat
+
+
+
+
+
Aucun TTL
+
pin()
+
✅ Succès
+
+
+
TTL défini
+
pin()
+
❌ Erreur 409
+
+
+
Non épinglé
+
set_ttl()
+
✅ Succès
+
+
+
Épinglé
+
set_ttl()
+
❌ Erreur 409
+
+
+
+
Rationale : - Épinglé = permanent,
+ne doit jamais être supprimé automatiquement - TTL =
+temporaire, sera supprimé à expiration - Ces deux concepts sont
+sémantiquement contradictoires
+
2. Exclusion du comptage
+
Les items épinglés ne comptent pas dans la limite du
+cache :
+
// Cache avec limite de 100 items
+let unpinned_count = cache.db.count_unpinned()?;// 100
+let total_count = cache.db.count()?;// 150
+
+// Le cache peut contenir :
+// - 100 items non épinglés (limite respectée)
+// - 50 items épinglés (hors limite)
+
3. Protection absolue
+contre l’éviction
+
Les items épinglés sont jamais retournés par
+get_oldest() :
+
-- Requête LRU exclut automatiquement les épinglés
+SELECT... FROM asset
+WHERE pinned =0-- ← Filtre explicite
+ORDERBY last_used ASC
+
Tests et validation
+
Suite de tests dédiée
+(test_pinnable.rs)
+
9 tests couvrant tous les cas d’usage :
+
+
test_pin_unpin :
+Épinglage/désépinglage basique
+
test_pinned_excluded_from_lru : Items
+épinglés protégés de l’éviction
+
test_pinned_count_separately :
+Comptage séparé des items
+
test_cannot_pin_with_ttl : Règle
+métier TTL → pas de pin
+
test_cannot_set_ttl_when_pinned :
+Règle métier pin → pas de TTL
+
test_ttl_expiration : Suppression
+automatique des items expirés
+
test_clear_ttl : Suppression du
+TTL
+
test_get_expired : Récupération des
+items expirés
+
test_cache_entry_fields : Vérification
+des champs dans les entrées
+
+
Résultat : ✅ 9/9 tests passent
+
Tests de non-régression
+
Tous les tests existants de test_cache.rs passent sans
+modification : - Test de création de cache - Test d’ajout de fichiers -
+Test de déduplication - Test de collections - Test de suppression - Test
+d’éviction LRU - Test de purge - Test de consolidation
+
Résultat : ✅ Aucune régression détectée
+
Compilation
+
cargo build -p pmocache
+
Résultat : ✅ Compilation sans erreur ni warning
+
Compatibilité et migration
+
Rétrocompatibilité de
+la base de données
+
Aucune migration manuelle requise. Les colonnes ont
+des valeurs par défaut :
+
pinned INTEGERDEFAULT0-- Non épinglé par défaut
+ttl_expires_at TEXT -- NULL par défaut
+
Les bases existantes sont automatiquement compatibles : - Tous les
+items existants sont non épinglés - Aucun TTL défini par défaut - Le
+comportement LRU standard reste identique
+
Rétrocompatibilité du code
+
Toutes les méthodes existantes continuent de fonctionner : -
+add_from_url(), add_from_file(),
+get(), etc. - Pas de changement de signature - Comportement
+LRU identique pour les items non épinglés
+
Documentation API REST
+
Tableau récapitulatif des
+endpoints
+
+
+
+
Méthode
+
Route
+
Description
+
Codes retour
+
+
+
+
+
GET
+
/{pk}/pin
+
Récupère le statut de pinning
+
200, 404
+
+
+
POST
+
/{pk}/pin
+
Épingle un item
+
200, 404, 409
+
+
+
DELETE
+
/{pk}/pin
+
Désépingle un item
+
200, 404
+
+
+
POST
+
/{pk}/ttl
+
Définit le TTL
+
200, 400, 404, 409
+
+
+
DELETE
+
/{pk}/ttl
+
Supprime le TTL
+
200, 404
+
+
+
+
Codes de statut HTTP
+
+
+
+
Code
+
Signification
+
Quand ?
+
+
+
+
+
200
+
Succès
+
Opération réussie
+
+
+
400
+
Requête invalide
+
Format de date TTL incorrect
+
+
+
404
+
Non trouvé
+
PK inexistant dans le cache
+
+
+
409
+
Conflit
+
Violation de règle métier (pin+TTL)
+
+
+
500
+
Erreur serveur
+
Erreur de base de données
+
+
+
+
Structure des erreurs
+
Format cohérent pour toutes les erreurs :
+
{
+"error":"CODE_ERREUR",
+"message":"Description lisible pour l'utilisateur"
+}
+
Exemples : - "CONFLICT" : Violation de règle métier -
+"NOT_FOUND" : Item inexistant - "INVALID_DATE"
+: Format de date RFC3339 invalide - "PIN_ERROR" /
+"TTL_ERROR" : Erreur technique
+
Exemples d’utilisation
+
Utilisation programmatique
+(Rust)
+
usepmocache::{Cache, CacheConfig};
+usechrono::{Duration, Utc};
+
+// Créer un cache
+let cache =Cache::<MyConfig>::new("./cache",100)?;
+
+// Ajouter un fichier
+let pk = cache.add_from_url("https://example.com/file.dat",None).await?;
+
+// ═══════════════════════════════════════
+// Scénario 1 : Item permanent (épinglé)
+// ═══════════════════════════════════════
+cache.pin(&pk).await?;
+
+// Vérifier le statut
+assert!(cache.is_pinned(&pk).await?);
+
+// L'item ne sera JAMAIS supprimé automatiquement
+// même si le cache est plein
+
+// ═══════════════════════════════════════
+// Scénario 2 : Item temporaire (TTL)
+// ═══════════════════════════════════════
+let pk2 = cache.add_from_url("https://example.com/temp.dat",None).await?;
+
+// Définir une expiration dans 24h
+let expires_at = (Utc::now() +Duration::hours(24)).to_rfc3339();
+cache.set_ttl(&pk2,&expires_at).await?;
+
+// L'item sera automatiquement supprimé après 24h
+// lors du prochain appel à enforce_limit()
+
+// ═══════════════════════════════════════
+// Scénario 3 : Conversion épinglé → TTL
+// ═══════════════════════════════════════
+cache.unpin(&pk).await?;// Désépingler d'abord
+cache.set_ttl(&pk,&expires_at).await?;// OK maintenant
Total : 7 fichiers modifiés, ~1055 lignes de code
+ajoutées
+
Avantages de la solution
+
1. Architecture propre et
+extensible
+
+
Séparation des responsabilités :
+
+
db.rs : logique de base de données
+
cache.rs : logique métier
+
api.rs : interface HTTP
+
+
Réutilisabilité :
+
+
Traits existants conservés
+
Pas de duplication de code
+
Pattern cohérent avec l’architecture PMOcache
+
+
+
2. Sécurité et fiabilité
+
+
Règles métier strictes :
+
+
Incompatibilité TTL ↔︎ Pinned appliquée à tous les niveaux
+
Validation au niveau DB, cache ET API
+
+
Gestion d’erreurs robuste :
+
+
Codes HTTP sémantiques
+
Messages explicites
+
Pas d’état incohérent possible
+
+
+
3. Performance
+
+
Requêtes SQL optimisées :
+
+
Index sur pinned pour requêtes rapides
+
WHERE pinned = 0 évite le scan complet
+
+
Comptage efficace :
+
+
count_unpinned() utilise un index
+
Pas de post-filtrage en mémoire
+
+
+
4. Expérience développeur
+
+
API intuitive :
+
+
Méthodes async cohérentes avec l’existant
+
Nommage clair (pin(), unpin(),
+set_ttl())
+
+
Documentation complète :
+
+
OpenAPI générée automatiquement
+
Swagger UI interactive
+
Exemples d’utilisation
+
+
+
5. Compatibilité
+
+
Migration transparente :
+
+
Aucune intervention manuelle
+
Valeurs par défaut appropriées
+
+
Pas de breaking change :
+
+
API existante inchangée
+
Nouveaux champs optionnels dans CacheEntry
+
+
+
Cas d’usage concrets
+
1. Cache de couvertures
+d’albums
+
// Épingler les couvertures des albums favoris
+for album in user.favorite_albums {
+let cover_pk = covers_cache.get_cover_pk(&album.id).await?;
+ covers_cache.pin(&cover_pk).await?;
+}
+
+// → Les couvertures favorites restent toujours en cache
+// → Même si le cache se remplit de nouvelles couvertures
+
2. Cache audio avec
+previews temporaires
+
// Pistes complètes : épinglées si dans la playlist courante
+for track in current_playlist.tracks {
+ audio_cache.pin(&track.pk).await?;
+}
+
+// Previews de 30 secondes : TTL de 1 heure
+let preview_pk = audio_cache.add_preview(&track_url).await?;
+let expires_at = (Utc::now() +Duration::hours(1)).to_rfc3339();
+audio_cache.set_ttl(&preview_pk,&expires_at).await?;
+
+// → Pistes courantes toujours disponibles
+// → Previews nettoyées automatiquement
+
3. Cache de
+métadonnées avec rafraîchissement
+
// Métadonnées d'album : TTL de 24h pour forcer le rafraîchissement
+let metadata_pk = metadata_cache.add_metadata(&album).await?;
+let tomorrow = (Utc::now() +Duration::days(1)).to_rfc3339();
+metadata_cache.set_ttl(&metadata_pk,&tomorrow).await?;
+
+// → Métadonnées rafraîchies quotidiennement
+// → Pas de données obsolètes
+
Limitations et
+considérations
+
1. Pas de limite sur les
+items épinglés
+
Les items épinglés peuvent s’accumuler indéfiniment. Recommandations
+:
+
// Surveiller le nombre d'items épinglés
+let pinned_count = cache.db.count()?- cache.db.count_unpinned()?;
+if pinned_count > MAX_PINNED_ITEMS {
+warn!("Too many pinned items: {}", pinned_count);
+}
+
2. TTL vérifié
+uniquement lors de enforce_limit()
+
Les items expirés ne sont pas supprimés immédiatement. Solutions
+possibles :
2025-01-15T10:30:00Z ✅ UTC
+2025-01-15T10:30:00+01:00 ✅ Avec timezone
+2025-01-15T10:30:00.123Z ✅ Avec millisecondes
+2025-01-15 10:30:00 ❌ Format invalide
L’implémentation des items épinglables et du TTL dans PMOcache est
+complète et production-ready. La solution répond à tous
+les objectifs initiaux :
+
✅ Objectifs atteints
+
+
Items épinglables fonctionnels :
+
+
Protection absolue contre l’éviction LRU
+
Exclusion du comptage de la limite du cache
+
+
Système de TTL robuste :
+
+
Expiration automatique des items temporaires
+
Suppression prioritaire lors de l’éviction
+
+
Règle métier stricte :
+
+
Incompatibilité TTL ↔︎ Pinned garantie à tous les niveaux
+
Validation DB, cache et API
+
+
API REST complète :
+
+
5 nouveaux endpoints documentés
+
Gestion d’erreurs cohérente
+
Documentation OpenAPI automatique
+
+
Compatibilité préservée :
+
+
Migration transparente des bases existantes
+
Aucun breaking change dans l’API
+
Tous les tests existants passent
+
+
+
Points forts
+
+
Architecture propre : Séparation claire des
+responsabilités
+
Code maintenable : Bien documenté, testé
+exhaustivement
+
Extensible : Facile d’ajouter de nouvelles
+fonctionnalités
+
Performant : Requêtes SQL optimisées avec
+index
+
Sécurisé : Règles métier appliquées
+strictement
+
+
Prêt pour la production
+
La fonctionnalité peut être déployée immédiatement : - Tous les tests
+passent - Documentation complète - API stable et documentée - Pas de
+régression sur l’existant
+
Cette implémentation renforce significativement PMOcache en le
+rendant adapté à une gamme plus large de cas d’usage, tout en maintenant
+sa simplicité et sa robustesse.
Rapport :
+Suppression de la logique de débouncing SSE
+
Date: 2026-01-12 Tâche:
+WeabApp_debouncingSSE.md
+
Objectif
+
Supprimer la logique de débouncing inutile sur le canal SSE de
+l’application web PMOControl, puisque le serveur contrôle déjà le flux
+des événements.
+
Analyse préalable
+
J’ai identifié trois endroits avec des mécanismes de temporisation
+dans l’application web :
1. Suppression
+des variables de débouncing (ligne ~27)
+
Avant:
+
// Flags pour gérer le rechargement automatique avec debounce et cooldown
+const isRefreshing =ref(false);
+const refreshTimeoutId =ref<number|null>(null);
+const lastRefreshTime =ref<number>(0);
+const REFRESH_COOLDOWN_MS =2000;// Ne pas recharger plus d'une fois toutes les 2 secondes
+
Après:
+
// Flag pour gérer le rechargement automatique
+const isRefreshing =ref(false);
+
2. Simplification
+du watcher de cache (ligne ~53)
+
Avant:
+
// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)
+// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,
+// ce qui déclenche un événement ContainersUpdated qui invalide le cache
+// Utilise un debounce de 3 secondes pour regrouper les multiples invalidations
+// et un cooldown de 5 secondes pour éviter les rechargements successifs
+watch(
+ () => browseData.value,
+ (data) => {
+if (!data && props.containerId&&!loading.value) {
+// Vérifier le cooldown: ignorer si on a rechargé il y a moins de 5 secondes
+const timeSinceLastRefresh =Date.now() - lastRefreshTime.value;
+if (timeSinceLastRefresh < REFRESH_COOLDOWN_MS) {
+console.log(
+`[MediaBrowser] Cache invalidé mais cooldown actif (${Math.round((REFRESH_COOLDOWN_MS - timeSinceLastRefresh) /1000)}s restantes), rechargement ignoré`,
+ );
+return;
+ }
+
+// Annuler tout timeout en cours
+if (refreshTimeoutId.value!==null) {
+clearTimeout(refreshTimeoutId.value);
+ }
+
+// Planifier le rechargement après 200ms
+ refreshTimeoutId.value=window.setTimeout(async () => {
+if (!isRefreshing.value) {
+console.log(
+`[MediaBrowser] Cache invalidé pour ${props.serverId}/${props.containerId}, rechargement après debounce...`,
+ );
+ isRefreshing.value=true;
+awaitbrowseContainer(
+ props.serverId,
+ props.containerId,
+false,
+ );
+ lastRefreshTime.value=Date.now();
+ isRefreshing.value=false;
+ refreshTimeoutId.value=null;
+ }
+ },200);
+ }
+ },
+);
+
Après:
+
// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)
+// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,
+// ce qui déclenche un événement ContainersUpdated qui invalide le cache
+// Le serveur contrôle déjà le flux SSE, pas besoin de debouncing côté client
+watch(
+ () => browseData.value,
+async (data) => {
+// Si browseData devient undefined alors que containerId est présent,
+// et qu'on n'est pas déjà en train de charger, recharger immédiatement
+if (!data && props.containerId&&!loading.value&&!isRefreshing.value) {
+console.log(
+`[MediaBrowser] Cache invalidé pour ${props.serverId}/${props.containerId}, rechargement...`,
+ );
+ isRefreshing.value=true;
+awaitbrowseContainer(props.serverId, props.containerId,false);
+ isRefreshing.value=false;
+ }
+ },
+);
+
Résultats
+
Changements de comportement
+
+
Avant: Délai de 200ms + cooldown de 2s entre les
+rechargements de cache
+
Après: Rechargement immédiat dès l’invalidation du
+cache
+
Impact: Réactivité améliorée de l’interface, les
+mises à jour apparaissent immédiatement
Logique simplifiée: De ~40 lignes à ~10 lignes dans
+le watcher
+
Code plus lisible: Intention claire sans mécanismes
+de temporisation complexes
+
+
Tests
+
+
✓ Le projet compile sans erreurs TypeScript
+
✓ Le flag isRefreshing empêche toujours les
+rechargements concurrents
+
✓ Les autres composants (useRenderers.ts, VolumeControl.vue)
+conservent leurs optimisations légitimes
+
+
Conclusion
+
La suppression du débouncing et du cooldown dans MediaBrowser.vue
+simplifie le code tout en améliorant la réactivité de l’interface.
+Puisque le serveur contrôle déjà le flux SSE, ces mécanismes côté client
+étaient redondants et ajoutaient une latence artificielle.
+
Le code est maintenant plus simple, plus réactif, et fait confiance
+au serveur pour contrôler la fréquence des événements SSE.
Rapport
+: Implémentation des items épinglables dans PMOcache
+
Résumé
+
Implémentation réussie de la fonctionnalité d’items épinglables dans
+la crate PMOcache, permettant de protéger certains items de l’éviction
+automatique par la politique LRU. Cette fonctionnalité inclut également
+un système de TTL (Time To Live) avec une règle métier empêchant qu’un
+item soit à la fois épinglé et avec un TTL.
+
Modifications apportées
+
1. Structure
+de la base de données (pmocache/src/db.rs)
Suppression prioritaire des items expirés : Les
+items dont le TTL est dépassé sont supprimés en premier
+
Comptage des items non épinglés : Utilise
+count_unpinned() au lieu de count()
+
Protection des items épinglés : Ils ne peuvent pas
+être évincés par LRU
+
Logging amélioré : Messages distincts pour les
+items expirés et l’éviction LRU
+
+
3. Tests
+(pmocache/tests/test_pinnable.rs)
+
Création d’une suite complète de tests (9 tests, tous passants) :
+
+
test_pin_unpin : Vérifie l’épinglage
+et le désépinglage basiques
+
test_pinned_excluded_from_lru :
+Vérifie que les items épinglés ne sont pas évincés
+
test_pinned_count_separately : Vérifie
+le comptage séparé des items épinglés
+
test_cannot_pin_with_ttl : Vérifie la
+règle métier TTL → pas de pinning
+
test_cannot_set_ttl_when_pinned :
+Vérifie la règle métier pinned → pas de TTL
+
test_ttl_expiration : Vérifie la
+suppression automatique des items expirés
+
test_clear_ttl : Vérifie la
+suppression du TTL
+
test_get_expired : Vérifie la
+récupération des items expirés
+
test_cache_entry_fields : Vérifie les
+valeurs des champs dans CacheEntry
+
+
Règles métier implémentées
+
Incompatibilité TTL ↔︎ Pinned
+
Un item ne peut pas être à la fois épinglé ET avoir un TTL :
+
+
Si TTL défini : pin() retourne une
+erreur
+
Si épinglé : set_ttl() retourne une
+erreur
+
+
Cette règle garantit une sémantique claire : -
+Épinglé = permanent, protégé de l’éviction -
+TTL = temporaire, sera supprimé à expiration
+
Comptage des items
+
Les items épinglés sont exclus du comptage de la
+limite du cache :
+
+
Un cache de limite 100 peut contenir 100 items non épinglés + N
+items épinglés
+
Seuls les items non épinglés sont pris en compte pour l’éviction
+LRU
+
+
Ordre de suppression
+lors de enforce_limit()
+
+
Items expirés (TTL dépassé) : supprimés en
+priorité
+
Items LRU : si la limite est toujours dépassée,
+suppression des plus vieux items non épinglés
+
+
Compatibilité
+
Migration de base de données
+
Aucune migration nécessaire : Les colonnes
+pinned et ttl_expires_at ont des valeurs par
+défaut : - pinned = 0 (non épinglé) -
+ttl_expires_at = NULL (pas de TTL)
+
Les bases existantes seront automatiquement mises à jour au prochain
+démarrage via le CREATE TABLE IF NOT EXISTS avec les
+nouvelles colonnes.
+
Rétrocompatibilité du code
+
Toutes les méthodes existantes continuent de fonctionner sans
+modification : - Les items existants ne sont pas épinglés par défaut -
+Le comportement LRU standard reste identique pour les items non
+épinglés
+
Exemples d’utilisation
+
Utilisation programmatique
+(Rust)
+
usepmocache::{Cache, CacheConfig};
+usechrono::{Duration, Utc};
+
+// Créer un cache
+let cache =Cache::<MyConfig>::new("./cache",100).unwrap();
+
+// Ajouter un fichier
+let pk = cache.add_from_url("https://example.com/file.dat",None).await?;
+
+// Épingler pour protéger de l'éviction
+cache.pin(&pk).await?;
+
+// Ou définir un TTL de 24 heures
+let expires_at = (Utc::now() +Duration::hours(24)).to_rfc3339();
+cache.set_ttl(&pk2,&expires_at).await?;
+
+// Vérifier le statut
+if cache.is_pinned(&pk).await?{
+println!("Fichier protégé");
+}
Modification de get_oldest(), get(),
+get_from_id(), get_all(),
+get_by_collection()
+
+
pmocache/src/cache.rs :
+
+
Ajout de 5 méthodes publiques
+
Modification de enforce_limit()
+
+
pmocache/tests/test_pinnable.rs :
+
+
Nouveau fichier de tests (9 tests)
+
+
+
Phase 2 : Enrichissement de
+l’API REST
+
+
pmocache/src/api.rs :
+
+
Ajout de 3 nouvelles structures de données :
+SetTtlRequest, PinResponse,
+PinStatus
+
Ajout de 5 nouveaux handlers d’API :
+
+
get_pin_status() : Récupération du statut de
+pinning
+
pin_item() : Épinglage d’un item
+
unpin_item() : Désépinglage d’un item
+
set_item_ttl() : Définition du TTL
+
clear_item_ttl() : Suppression du TTL
+
+
+
pmocache/src/pmoserver_ext.rs :
+
+
Ajout de 4 nouvelles routes dans create_api_router() :
+
+
GET /{pk}/pin : Statut de pinning
+
POST /{pk}/pin : Épingler
+
DELETE /{pk}/pin : Désépingler
+
POST /{pk}/ttl : Définir TTL
+
DELETE /{pk}/ttl : Supprimer TTL
+
+
+
pmocache/src/openapi.rs :
+
+
Mise à jour de la macro create_cache_openapi! pour
+inclure :
+
+
Les 5 nouveaux endpoints dans la documentation
+
Les 3 nouvelles structures dans les schémas OpenAPI
+
+
+
pmocache/src/lib.rs :
+
+
Export des nouvelles structures publiques pour l’API
+
+
+
API REST et Documentation
+OpenAPI
+
Routes disponibles
+
Toutes les routes sont préfixées par /api/{cache_name}/
+(ex: /api/covers/, /api/audio/).
+
+
+
+
+
+
+
+
+
Méthode
+
Route
+
Description
+
+
+
+
+
GET
+
/{pk}/pin
+
Récupère le statut de pinning d’un item
+
+
+
POST
+
/{pk}/pin
+
Épingle un item (le protège de l’éviction LRU)
+
+
+
DELETE
+
/{pk}/pin
+
Désépingle un item
+
+
+
POST
+
/{pk}/ttl
+
Définit le TTL d’un item (expiration automatique)
+
+
+
DELETE
+
/{pk}/ttl
+
Supprime le TTL d’un item
+
+
+
+
Codes de statut HTTP
+
+
+
+
+
+
+
+
+
Code
+
Signification
+
Cas d’usage
+
+
+
+
+
200 OK
+
Opération réussie
+
Tous les cas de succès
+
+
+
400 BAD REQUEST
+
Requête invalide
+
Format de date TTL invalide
+
+
+
404 NOT FOUND
+
Item non trouvé
+
PK inexistant dans le cache
+
+
+
409 CONFLICT
+
Conflit de règle métier
+
Tentative de pin avec TTL ou vice-versa
+
+
+
500 INTERNAL SERVER ERROR
+
Erreur serveur
+
Erreur de base de données
+
+
+
+
Documentation OpenAPI/Swagger
+
La documentation OpenAPI est automatiquement générée et inclut :
+
+
Schémas de données :
+
+
PinStatus : Statut de pinning (pinned,
+ttl_expires_at)
+
PinResponse : Réponse d’opération de pinning
+
SetTtlRequest : Requête de définition de TTL
+
CacheEntry : Mis à jour avec les champs
+pinned et ttl_expires_at
+
+
Endpoints documentés :
+
+
Description détaillée de chaque route
+
Exemples de requêtes et réponses
+
Codes d’erreur possibles
+
+
Interface Swagger UI :
+
+
Accessible à /swagger-ui/{cache_name}
+
Permet de tester l’API directement depuis le navigateur
+
+
+
Gestion des erreurs
+
L’API suit une structure d’erreur cohérente :
+
{
+"error":"CODE_ERREUR",
+"message":"Description lisible de l'erreur"
+}
+
Les règles métier sont appliquées strictement : - 409
+CONFLICT si tentative de pin avec TTL défini - 409
+CONFLICT si tentative de set TTL sur item épinglé - Messages
+d’erreur explicites guidant l’utilisateur
+
Tests
+
+
Suite de tests dédiée : 9 tests, tous passants
+
Tests existants : Tous les tests de
+test_cache.rs passent toujours
+
Couverture : Toutes les nouvelles fonctionnalités
+sont testées
+
Compilation : Aucune erreur, tous les modules
+compilent correctement
+
+
Résultat
+
✅ Implémentation complète et fonctionnelle des
+items épinglables avec TTL
+✅ Règle métier TTL ↔︎ Pinned correctement
+implémentée
+✅ Tests exhaustifs validant tous les cas d’usage
+✅ Compatibilité avec les bases de données
+existantes
+✅ Pas de régression sur les tests existants
+✅ API REST complète avec 5 nouveaux endpoints
+✅ Documentation OpenAPI automatiquement générée
+✅ Gestion d’erreurs cohérente avec codes HTTP
+appropriés
Rapport :
+Suppression de la logique de débouncing SSE
+
Date: 2026-01-12 Tâche:
+WeabApp_debouncingSSE.md
+
Objectif
+
Supprimer la logique de débouncing inutile sur le canal SSE de
+l’application web PMOControl, puisque le serveur contrôle déjà le flux
+des événements.
+
Analyse préalable
+
J’ai identifié trois endroits avec des mécanismes de temporisation
+dans l’application web :
1. Suppression
+des variables de débouncing (ligne ~27)
+
Avant:
+
// Flags pour gérer le rechargement automatique avec debounce et cooldown
+const isRefreshing =ref(false);
+const refreshTimeoutId =ref<number|null>(null);
+const lastRefreshTime =ref<number>(0);
+const REFRESH_COOLDOWN_MS =2000;// Ne pas recharger plus d'une fois toutes les 2 secondes
+
Après:
+
// Flag pour gérer le rechargement automatique
+const isRefreshing =ref(false);
+
2. Simplification
+du watcher de cache (ligne ~53)
+
Avant:
+
// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)
+// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,
+// ce qui déclenche un événement ContainersUpdated qui invalide le cache
+// Utilise un debounce de 3 secondes pour regrouper les multiples invalidations
+// et un cooldown de 5 secondes pour éviter les rechargements successifs
+watch(
+ () => browseData.value,
+ (data) => {
+if (!data && props.containerId&&!loading.value) {
+// Vérifier le cooldown: ignorer si on a rechargé il y a moins de 5 secondes
+const timeSinceLastRefresh =Date.now() - lastRefreshTime.value;
+if (timeSinceLastRefresh < REFRESH_COOLDOWN_MS) {
+console.log(
+`[MediaBrowser] Cache invalidé mais cooldown actif (${Math.round((REFRESH_COOLDOWN_MS - timeSinceLastRefresh) /1000)}s restantes), rechargement ignoré`,
+ );
+return;
+ }
+
+// Annuler tout timeout en cours
+if (refreshTimeoutId.value!==null) {
+clearTimeout(refreshTimeoutId.value);
+ }
+
+// Planifier le rechargement après 200ms
+ refreshTimeoutId.value=window.setTimeout(async () => {
+if (!isRefreshing.value) {
+console.log(
+`[MediaBrowser] Cache invalidé pour ${props.serverId}/${props.containerId}, rechargement après debounce...`,
+ );
+ isRefreshing.value=true;
+awaitbrowseContainer(
+ props.serverId,
+ props.containerId,
+false,
+ );
+ lastRefreshTime.value=Date.now();
+ isRefreshing.value=false;
+ refreshTimeoutId.value=null;
+ }
+ },200);
+ }
+ },
+);
+
Après:
+
// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)
+// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,
+// ce qui déclenche un événement ContainersUpdated qui invalide le cache
+// Le serveur contrôle déjà le flux SSE, pas besoin de debouncing côté client
+watch(
+ () => browseData.value,
+async (data) => {
+// Si browseData devient undefined alors que containerId est présent,
+// et qu'on n'est pas déjà en train de charger, recharger immédiatement
+if (!data && props.containerId&&!loading.value&&!isRefreshing.value) {
+console.log(
+`[MediaBrowser] Cache invalidé pour ${props.serverId}/${props.containerId}, rechargement...`,
+ );
+ isRefreshing.value=true;
+awaitbrowseContainer(props.serverId, props.containerId,false);
+ isRefreshing.value=false;
+ }
+ },
+);
+
Résultats
+
Changements de comportement
+
+
Avant: Délai de 200ms + cooldown de 2s entre les
+rechargements de cache
+
Après: Rechargement immédiat dès l’invalidation du
+cache
+
Impact: Réactivité améliorée de l’interface, les
+mises à jour apparaissent immédiatement
Logique simplifiée: De ~40 lignes à ~10 lignes dans
+le watcher
+
Code plus lisible: Intention claire sans mécanismes
+de temporisation complexes
+
+
Tests
+
+
✓ Le projet compile sans erreurs TypeScript
+
✓ Le flag isRefreshing empêche toujours les
+rechargements concurrents
+
✓ Les autres composants (useRenderers.ts, VolumeControl.vue)
+conservent leurs optimisations légitimes
+
+
Conclusion
+
La suppression du débouncing et du cooldown dans MediaBrowser.vue
+simplifie le code tout en améliorant la réactivité de l’interface.
+Puisque le serveur contrôle déjà le flux SSE, ces mécanismes côté client
+étaient redondants et ajoutaient une latence artificielle.
+
Le code est maintenant plus simple, plus réactif, et fait confiance
+au serveur pour contrôler la fréquence des événements SSE.
Rapport :
+Documentation du pattern d’extension pmoconfig
+
Objectif de la tâche
+
Créer une fiche descriptive documentant le pattern d’implémentation
+des traits d’extension de pmoconfig::Config en analysant
+les implémentations existantes dans les différents crates du projet.
+
Travail réalisé
+
1. Analyse des fichiers source
+
Les fichiers suivants ont été analysés :
+
+
pmocovers/src/config_ext.rs - Pattern cache avec
+conversion WebP
+
pmoaudiocache/src/config_ext.rs - Pattern cache avec
+conversion FLAC
Tous les traits d’extension suivent la même structure : - Trait
+public avec méthodes getter/setter - Implémentation pour
+pmoconfig::Config - Utilisation de
+get_value/set_value génériques - Constantes
+pour valeurs par défaut
+
Patterns spécialisés
+
+
Cache : Utilisation de CacheConfigExt
+et factory methods
+
Authentification : Getters combinés, helpers de
+validation, déchiffrement automatique
+
Rate limiting : Configuration des limites avec
+valeurs par défaut
+
Configuration minimale : Auto-persistence des
+valeurs par défaut
+
UPnP : Configuration des identifiants devices
+
+
3. Structure de la
+documentation
+
La documentation créée couvre :
+
+
Vue d’ensemble : Objectif et principe du
+pattern
+
Architecture : Structure et flux de données
+
Implémentation : Guide détaillé avec patterns de
+code
+
Patterns spécialisés : Exemples pour chaque cas
+d’usage
Checklist : Liste de vérification pour nouveaux
+traits
+
Philosophie : Principes directeurs et
+avantages
+
+
4. Contenu clé
+
Patterns de getters
+
+
Getter simple avec valeur par défaut
+
Getter avec auto-persistence
+
Getter optionnel
+
Getter avec déchiffrement
+
Getter avec parsing et fallback
+
+
Patterns de setters
+
+
Setter simple
+
Setter avec transformation
+
Setter multiple (transaction)
+
Setter de nettoyage
+
+
Helpers
+
+
Factory methods
+
Getters combinés
+
Helpers de validation
+
+
5. Hiérarchie de configuration
+YAML
+
Documentation des chemins standards : - host.* :
+Configuration hôte/système - accounts.* : Comptes et
+services - sources.* : Sources de médias
+
Résultat
+
Le document Blackboard/Architecture/pmoconfig_ext.md a
+été créé avec : - 800+ lignes de documentation complète - 3 exemples
+d’implémentation complète - Patterns pour tous les cas d’usage
+identifiés - Bonnes pratiques et anti-patterns - Checklist
+d’implémentation
+
Fichiers créés ou modifiés
+
+
Créé :
+Blackboard/Architecture/pmoconfig_ext.md - Documentation
+complète du pattern
+
Créé : Blackboard/Report/config_ext.md
+- Ce rapport
+
+
Conformité avec Rules.md
+
+
Documentation placée dans Blackboard/Architecture/
+comme demandé
+
Rapport créé dans Blackboard/Report/ avec le même nom
+de fichier
+
Analyse focalisée sur l’objectif principal
+
Documentation prête pour classification (Done/ToDiscuss) par
+l’humain
Caractéristiques : - Flux continu de tracks avec
+capacité limitée - Suppression automatique des plus anciens - Callbacks
+sur playlists pour détecter les changements - Notification du
+ContentDirectory via notifier injecté - Adaptation des IDs playlist →
+schema source
Caractéristiques : - Catalogue vaste avec navigation
+hiérarchique - Cache lazy pour audio, eager pour covers - Playlists
+créées à la demande avec TTL - LazyProvider pour télécharger l’audio à
+la lecture - Métadonnées riches stockées dans le cache
+
Éléments clés :
+
SourceCacheManager centralisé
+QobuzLazyProvider implémentant LazyProvider
+Playlists avec rôle Album et TTL de 7 jours
+Adaptation IDs avec metadata source_track_id
+
3. Structure du document créé
+
Le document Blackboard/Architecture/music_source.md
+contient :
+
Table des matières
+
+
Vue d’ensemble
+
Structure d’une MusicSource
+
Implémentation du trait MusicSource
+
Patterns d’implémentation
+
Intégration avec l’écosystème PMOMusic
+
Checklist de mise en œuvre
+
Exemples de référence
+
+
Sections détaillées
+
Section 1 : Vue d’ensemble - Définition d’une
+MusicSource - Types de sources (dynamique vs statique) - Capacités du
+trait
+
Section 2 : Structure - Organisation du code -
+Dépendances recommandées - Features Cargo
+
Section 3 : Implémentation du trait - Informations
+de base (name, id, default_image) - Navigation ContentDirectory
+(root_container, browse, resolve_uri) - Support FIFO (append_track,
+remove_oldest, update_id) - Support statique (get_items, search)
+
Section 4 : Patterns - Pattern 1 : Source dynamique
+avec FIFO (code complet) - Pattern 2 : Source catalogue avec playlists
+lazy (code complet) - Pattern 3 : Adaptation des IDs entre playlist et
+source
+
Section 5 : Intégration écosystème - pmoplaylist :
+création et gestion de playlists - pmoaudiocache/pmocovers via
+SourceCacheManager - pmodidl : conversion vers DIDL-Lite - LazyProvider
+personnalisé
Exemples concrets de Radio Paradise et Qobuz fournis.
+
Adaptation des IDs
+
Code complet pour adapter les items de playlist au schema de la
+source : - Extraction du cache_pk depuis l’URL - Récupération du
+source_track_id depuis metadata - Reconstruction de l’ID correct -
+Normalisation des URLs (relatives → absolues) - Ajout de champs requis
+(genre)
+
Cache lazy vs eager
+
Stratégie claire : - Covers : Cache eager (petit, UI
+en a besoin immédiatement) - Audio : Cache lazy (grand,
+téléchargé à la demande)
+
Thread Safety
+
Règles explicites : - Arc<RwLock<>> pour
+état mutable partagé - tokio::sync::RwLock pour async -
+Éviter Rc<>, RefCell (non thread-safe) -
+Implémenter Clone via Arc<>
+
Compatibilité UPnP
+
Points de vigilance : - Genre obligatoire pour certains clients
+(gupnp-av-cp) - URLs absolues uniquement - Protocol Info correct pour
+FLAC - Duration au format H:MM:SS - childCount optionnel
+mais recommandé
+
5. Code d’exemple complet
+
Le document contient des exemples de code complets et fonctionnels
+pour :
+
+
Structure de base : définition de la struct et
+implémentation basique
+
Navigation : root_container et browse avec pattern
+matching
+
Résolution URI : avec fallback cache →
+original
+
FIFO : append_track, remove_oldest, callbacks
+
Adaptation IDs : fonction complète
+d’adaptation
+
LazyProvider : implémentation personnalisée
+
Conversion DIDL : traits ToDIDLContainer et
+ToDIDLItem
+
+
Couverture des besoins
+
Sources couvertes
+
+
✅ Radio Paradise : source dynamique FIFO
+
✅ Qobuz : source catalogue lazy
+
✅ Patterns génériques applicables à d’autres sources
+
+
Cas d’usage couverts
+
+
✅ Source radio/streaming live
+
✅ Source catalogue de streaming (Spotify, Deezer, etc.)
+
✅ Source bibliothèque locale
+
✅ Source playlists fixes
+
✅ Source avec authentification (via client)
+
+
Intégrations couvertes
+
+
✅ pmoplaylist (FIFO et persistant)
+
✅ pmoaudiocache (cache audio)
+
✅ pmocovers (cache covers)
+
✅ SourceCacheManager (centralisé)
+
✅ LazyProvider (téléchargement lazy)
+
✅ pmodidl (DIDL-Lite)
+
+
Limitations et
+améliorations futures
+
Limitations actuelles
+
+
Search : Pas d’exemple détaillé de search
+(optionnel dans le trait)
+
Authentification : Mentionné mais pas d’exemple
+complet
+
Multi-format : Pas d’exemple de source supportant
+plusieurs formats
+
Offline : Pas de pattern pour source
+offline/synchronisation
+
+
Améliorations possibles
+
+
Ajouter un exemple complet de search avec filtres
+
Documenter l’intégration avec un système d’auth OAuth
+
Ajouter un pattern pour sources multi-formats (FLAC/MP3/AAC)
+
Documenter la gestion offline avec synchronisation
Un développeur peut suivre ce guide étape par étape pour créer une
+nouvelle source musicale compatible avec l’écosystème PMOMusic, en
+s’inspirant des patterns éprouvés de Radio Paradise et Qobuz.
Documentation du pattern d’extension du PMOServer à travers plusieurs
+itérations basées sur les retours utilisateur.
+
Travail réalisé
+
Analyse des fichiers sources
+
Les fichiers suivants ont été analysés pour extraire le pattern :
+
+
pmoapp/src/lib.rs : Pattern SPA avec RustEmbed
+
pmocontrol/src/pmoserver_ext.rs : API REST avec Control
+Point (1506+ lignes)
+
pmoparadise/src/pmoserver_ext.rs : API REST simple avec
+client externe
+
pmoaudiocache/src/lib.rs : Extension avec cache et
+fichiers
+
pmomediaserver/src/paradise_streaming.rs : Extension
+complexe avec streaming
+
+
Round 1 : Document initial
+
Premier jet documentant exhaustivement tous les aspects des
+extensions (~850 lignes).
+
Round 2 : Recentrage sur le
+pattern
+
Annotation : “se recentrer sur le sujet
+principal”
+
Actions : - Réduction de ~850 à ~400 lignes -
+Suppression des digressions (OpenAPI détaillé, handlers spécifiques) -
+Focus sur l’anatomie du pattern en 5 étapes - Ajout d’une checklist et
+d’un exemple minimal
+
Résultat : Document focalisé sur l’implémentation du
+pattern uniquement.
+
Round 3 : Réintégration
+OpenAPI
+
Annotation : “Je trouve que le fait de devoir
+déclarer et documenter les URL dans OpenAPI / utopia était quelque chose
+d’important. Remets le.”
+
Actions : - Ajout d’une section complète
+“Documentation OpenAPI avec utoipa” (~260 lignes) - 5 sous-sections
+détaillées : 1. Configuration de base (dépendances Cargo) 2. Définition
+des schémas avec #[derive(ToSchema)] 3. Annotation des
+handlers avec #[utoipa::path] 4. Création de la structure
+#[derive(OpenApi)] 5. Exemple complet extrait de Radio
+Paradise - Mise à jour de la checklist avec section “Documentation
+OpenAPI” - Ajout des dépendances utoipa et
+serde dans la section références
+
Positionnement : Section insérée après “Méthodes
+disponibles du serveur” et avant “Patterns courants”, car elle fait
+partie intégrante de l’implémentation.
+
Structure finale du document
+
+
Vue d’ensemble : Principe du pattern
+
Anatomie d’une extension : 5 étapes détaillées
+
Méthodes disponibles du serveur : API de
+pmoserver::Server
+
Documentation OpenAPI avec utoipa : Guide complet
+en 5 étapes ⭐ Ajouté au Round 3
+
Patterns courants : 3 exemples concrets
+
Gestion des opérations longues : spawn_blocking,
+timeouts, background tasks
+
Checklist d’implémentation : Organisée par
+catégories
+
Exemple complet minimal : Code fonctionnel
+
Références : Fichiers sources et dépendances
+
+
Résultat final
+
Le document est maintenant :
+
+
Complet : Couvre tous les aspects essentiels
+incluant OpenAPI
+
Structuré : Progression logique de la configuration
+à l’implémentation
+
Pratique : Exemples de code concrets extraits du
+codebase
+
Actionnable : Checklist détaillée en 4
+catégories
+
+
Taille finale : ~660 lignes (avec section OpenAPI complète)
+
Fichiers modifiés
+
+
Blackboard/Architecture/pmoserver_ext.md : Document
+complet avec OpenAPI (660 lignes)
Il faut suivre les instructions générales placées dans le
+fichier : Blackboard/Rules.md
+
La crâte PMOcache, implémente un system de cache qui pourrait être
+étendu pour permettre une utilisation plus large. L’idée est de modifier
+les règles de déletion des items. Actuellement le cache a une capacité
+maximale. Et les items ont des TTL, qui peuvent être non définies.
+Lorsque le cash est plein, les plus vieux items en termes d’utilisation
+ou ceux qui ont dépassé leur TTL peuvent être détruits. Je propose de
+rajouter une fonctionnalité qui permet d’épingler certains items pour
+les rendre non destructibles. Ils pourraient aussi sortir du comptage
+général des items pour savoir si le cache est plein.
+
Il faudra modifier la structure de la base de données. Ajouter une
+colonne indiquant cette propriété. Mettre une règle métier en disant
+qu’on ne peut pas être à la fois épinglés et avec un TTL.
+
On se moque de maintenir la compatibilité avec la base de données
+actuelle, il n’y a pas à prévoir de phase de transition. Nous sommes en
+période de développement.
Il faut suivre les instructions générales placées dans le
+fichier : Blackboard/Rules.md
+
Partir des fichiers suivants:
+
+
pmoapp/src/lib.rs
+
pmocontrol/src/pmoserver_ext.rs
+
pmoparadise/src/pmoserver_ext.rs
+
pmoaudiocache/src/lib.rs
+
pmomediaserver/src/paradise_streaming.rs
+
+
réalise une fiche descriptive sur le pattern à réaliser pour
+implémenter un trait d’extension du PMO serveur.
+
Le résultat sera une documentation d’implémentation qui sera placé
+dans le fichier:
+Blackboard/Architecture/pmoserver_ext.md
+
Round 2
+
J’ai regardé ton document généré et je trouve que tu t’élargis du
+sujet central documenter lecture d’une extension PMOserver. Peux-tu te
+recentrer sur le sujet principal.
+
Round 3
+
Je trouve que le fait de devoir déclarer et documenter les URL dans
+OpenAPI / utopia était quelque chose d’important. Remets le.
Il faut suivre les instructions générales placées dans le
+fichier : Blackboard/Rules.md
+
MusicBoxSource
+: Bibliothèque musicale universelle
+
Créer une “boîte à musique” personnelle : un
+catalogue unifié de morceaux provenant de n’importe quelle source
+(Qobuz, URLs, fichiers locaux, Radio Paradise, etc.), avec taxonomie de
+tags et playlists intelligentes.
+
+
🎯 Vision
+
Concept
+
MusicBoxSource est une bibliothèque musicale
+curatoriale qui permet de : - Collecter : Ajouter des
+morceaux depuis n’importe quelle source PMOMusic ou URL -
+Organiser : Classifier avec une taxonomie de tags
+extensible - Requêter : Créer des playlists statiques
+et smart playlists (requêtes dynamiques) - Exposer :
+Servir via UPnP/DIDL-Lite avec navigation multi-axes
+
Différence avec
+pmoplaylist
+
+
pmoplaylist : Playlists FIFO
+éphémères pour sources live (Radio Paradise)
+
pmomusicbox : Bibliothèque
+persistante cross-sources avec métadonnées
+enrichies
erDiagram
+ TAG_CATEGORIES ||--o{ TAGS : contient
+ TAG_CATEGORIES ||--o{ TAG_CATEGORIES : parent
+ TAGS ||--o{ ITEM_TAGS : associe
+ MUSIC_ITEMS ||--o{ ITEM_TAGS : a
+ MUSIC_ITEMS ||--o{ PLAYLIST_ITEMS : dans
+ PLAYLISTS ||--o{ PLAYLIST_ITEMS : contient
+
+ TAG_CATEGORIES {
+ text id PK "Ex: mood, genre"
+ text name "Nom affiché"
+ text parent_id FK "Hiérarchie"
+ text color "Hex color"
+ text icon "Emoji/icon"
+ int display_order
+ }
+
+ TAGS {
+ text id PK "Ex: mood:energetic"
+ text category_id FK
+ text name "energetic, chill"
+ text description
+ text color "Override"
+ }
+
+ MUSIC_ITEMS {
+ text id PK "UUID"
+ text source_type "qobuz, url, local"
+ text source_id "ID source"
+ text original_uri "URI source"
+ text cache_audio_pk FK "pmoaudiocache"
+ text cache_cover_pk FK "pmocovers"
+ text title
+ text artist
+ text album
+ int year
+ int rating "1-5 étoiles"
+ int play_count
+ }
+
+ ITEM_TAGS {
+ text item_id PK,FK
+ text tag_id PK,FK
+ int added_at
+ text source "user, auto"
+ }
+
+ PLAYLISTS {
+ text id PK
+ text name
+ bool is_smart
+ text smart_query "JSON"
+ }
+
+ PLAYLIST_ITEMS {
+ text playlist_id PK,FK
+ text item_id FK
+ int position PK
+ }
+
Tables d’association
+
+
item_tags : Liens items ↔︎ tags
+(N:M)
+
playlist_items : Items dans playlists
+statiques (position, ordre)
+
tag_synonyms : Synonymes pour
+recherche (ex: “jazz” → “swing”)
+
+
Index & Recherche
+
+
Indexes B-tree : artist, album, genre, year,
+rating, play_count
+
FTS5 (Full-Text Search) : title, artist, album,
+comment
+
Triggers : Maintien des tables FTS en sync avec
+music_items
+
+
+
🎨 Taxonomie par défaut
+
Catégories préchargées à l’initialisation :
+
+
+
+
+
+
+
+
+
Catégorie
+
Description
+
Exemples de tags
+
+
+
+
+
Mood
+
État d’esprit, émotion
+
energetic, chill, melancholic, happy
+
+
+
Genre
+
Style musical
+
rock, jazz, classical, electronic, metal
+
+
+
Era
+
Période, décennie
+
60s, 70s, 80s, 90s, contemporary
+
+
+
Occasion
+
Contexte d’écoute
+
workout, focus, party, driving, sleep
+
+
+
Tempo
+
Vitesse
+
slow, medium, fast
+
+
+
Instrument
+
Instrument dominant
+
piano, guitar, vocal, synthesizer
+
+
+
Quality
+
Qualité audio
+
lossless, high-res, remastered, live
+
+
+
Origin
+
Origine géographique
+
usa, uk, france, japan, latin, africa
+
+
+
+
Extensibilité : L’utilisateur peut créer ses propres
+catégories et tags.
+
+
📦 Crates architecture
+
1.
+pmojspf - Parser de playlists
+(utilitaire)
+
But : Parser/écrire différents formats de playlists
+vers/depuis un format pivot JSPF (JSON).
Il faut suivre les instructions générales placées dans le
+fichier : Blackboard/Rules.md
+
PlaylistSource :
+MusicSource pour playlists
+
Implémenter une source PMOMusic capable de servir un catalogue de
+playlists hiérarchisé via UPnP.
+
+
📋 Décisions de conception
+
Format pivot : JSPF (JSON)
+
Choix : JSPF comme format interne central -
+Métadonnées riches (title, creator, album, annotation, image, duration,
+etc.) - JSON natif avec serde (Rust-friendly) - Standard ouvert
+(Xiph.Org) - Extensible via champ meta