Documentation du pattern d'extension pmoserver_ext
Ajout de la documentation complète du pattern d'extension pmoserver_ext pour permettre une implémentation modulaire et cohérente des fonctionnalités du serveur PMO. Cette documentation inclut : - Une vue d'ensemble de l'architecture du pattern - Les composants principaux : traits d'extension, état partagé, handlers HTTP, documentation OpenAPI - Des patterns avancés : async-trait, intégration Control Point et WebApp - Des registres globaux (singletons) pour le partage de ressources - Une checklist d'implémentation et des bonnes pratiques - Des exemples d'utilisation concrets Les fichiers générés sont : 1. Blackboard/Architecture/pmoserver_ext.md (745 lignes) 2. Blackboard/Report/pmoserver_ext.md (157 lignes) La documentation permet de suivre les conventions établies pour implémenter de nouvelles extensions au serveur PMOMusic de manière cohérente et modulaire.
This commit is contained in:
708
Blackboard/Architecture/pmoserver_ext.md
Normal file
708
Blackboard/Architecture/pmoserver_ext.md
Normal file
@@ -0,0 +1,708 @@
|
||||
# Pattern d'extension du serveur PMO (pmoserver_ext)
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
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 (audio, images, UPnP, Radio Paradise, etc.) peut ajouter ses propres routes HTTP, API REST et documentation OpenAPI sans que `pmoserver` ne dépende de ces crates.
|
||||
|
||||
## Architecture du pattern
|
||||
|
||||
### Principe de base
|
||||
|
||||
Le pattern utilise le système de traits Rust pour définir une interface d'extension que `pmoserver::Server` implémente. Chaque crate fonctionnelle définit son propre trait d'extension avec des méthodes préfixées par convention (ex: `init_*`, `add_*`).
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ pmoserver (core) │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ Server (struct) │ │
|
||||
│ │ - Router Axum │ │
|
||||
│ │ - add_handler(), add_router() │ │
|
||||
│ │ - add_openapi(), add_spa() │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ impl Trait
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
┌─────────┴──────────┐ ┌──────────┴─────────────┐
|
||||
│ pmoaudiocache │ │ pmoparadise │
|
||||
│ ┌──────────────┐ │ │ ┌──────────────────┐ │
|
||||
│ │AudioCacheExt │ │ │ │RadioParadiseExt │ │
|
||||
│ └──────────────┘ │ │ └──────────────────┘ │
|
||||
└────────────────────┘ └────────────────────────┘
|
||||
```
|
||||
|
||||
### Avantages
|
||||
|
||||
1. **Découplage** : `pmoserver` ne connaît pas les crates spécialisées
|
||||
2. **Modularité** : Chaque fonctionnalité est opt-in via features Cargo
|
||||
3. **Cohérence** : Interface uniforme pour toutes les extensions
|
||||
4. **Testabilité** : Chaque extension peut être testée indépendamment
|
||||
|
||||
## Composants du pattern
|
||||
|
||||
### 1. Trait d'extension
|
||||
|
||||
Définir un trait public avec des méthodes d'initialisation/configuration.
|
||||
|
||||
**Convention de nommage** :
|
||||
- Trait : `{Domaine}Ext` (ex: `AudioCacheExt`, `RadioParadiseExt`)
|
||||
- Méthodes : `init_{domaine}*`, `add_{domaine}*`
|
||||
|
||||
**Exemple** (pmoaudiocache/src/lib.rs:200-215):
|
||||
```rust
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub trait AudioCacheExt {
|
||||
/// Initialise le cache audio et enregistre les routes HTTP
|
||||
async fn init_audio_cache(
|
||||
&mut self,
|
||||
cache_dir: &str,
|
||||
limit: usize,
|
||||
) -> anyhow::Result<Arc<Cache>>;
|
||||
|
||||
/// Initialise avec configuration par défaut
|
||||
async fn init_audio_cache_configured(&mut self)
|
||||
-> anyhow::Result<Arc<Cache>>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Implémentation du trait
|
||||
|
||||
Implémenter le trait pour `pmoserver::Server` en utilisant les méthodes publiques du serveur.
|
||||
|
||||
**Méthodes disponibles du serveur** :
|
||||
- `add_handler()` : Ajoute un handler simple
|
||||
- `add_handler_with_state()` : Ajoute un handler avec état partagé
|
||||
- `add_router()` : Monte un sous-router Axum
|
||||
- `add_openapi()` : Enregistre une API avec documentation OpenAPI
|
||||
- `add_spa()` : Sert une Single Page Application (RustEmbed)
|
||||
- `base_url()` : Récupère l'URL de base du serveur
|
||||
|
||||
**Exemple** (pmoaudiocache/src/lib.rs:225-260):
|
||||
```rust
|
||||
#[cfg(feature = "pmoserver")]
|
||||
impl AudioCacheExt for pmoserver::Server {
|
||||
async fn init_audio_cache(
|
||||
&mut self,
|
||||
cache_dir: &str,
|
||||
limit: usize,
|
||||
) -> anyhow::Result<Arc<Cache>> {
|
||||
// 1. Créer le cache
|
||||
let cache = Arc::new(new_cache(cache_dir, limit)?);
|
||||
|
||||
// 2. Router pour servir les fichiers
|
||||
let file_router = create_file_router(
|
||||
cache.clone(),
|
||||
"audio/flac", // Content-Type
|
||||
);
|
||||
self.add_router("/", file_router).await;
|
||||
|
||||
// 3. API REST avec état
|
||||
let api_router = Router::new()
|
||||
.route("/", get(list).post(add).delete(purge))
|
||||
.route("/{pk}", get(get_info).delete(delete))
|
||||
.with_state(cache.clone());
|
||||
|
||||
// 4. Documentation OpenAPI
|
||||
let openapi = ApiDoc::openapi();
|
||||
self.add_openapi(api_router, openapi, "audio").await;
|
||||
|
||||
Ok(cache)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. État partagé (State)
|
||||
|
||||
Pour les handlers qui nécessitent un état, créer une structure dédiée cloneable.
|
||||
|
||||
**Convention** :
|
||||
- Nom : `{Domaine}State`
|
||||
- Doit implémenter `Clone`
|
||||
- Contient des `Arc<T>` pour les ressources partagées
|
||||
|
||||
**Exemple** (pmoparadise/src/pmoserver_ext.rs:18-22):
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct RadioParadiseState {
|
||||
client: Arc<RwLock<RadioParadiseClient>>,
|
||||
}
|
||||
|
||||
impl RadioParadiseState {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
let client = RadioParadiseClient::new().await?;
|
||||
Ok(Self {
|
||||
client: Arc::new(RwLock::new(client)),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Handlers HTTP
|
||||
|
||||
Définir les handlers comme des fonctions async avec les extracteurs Axum.
|
||||
|
||||
**Extracteurs courants** :
|
||||
- `State<T>` : Accès à l'état partagé
|
||||
- `Path<T>` : Paramètres d'URL
|
||||
- `Query<T>` : Paramètres de query string
|
||||
- `Json<T>` : Body JSON
|
||||
|
||||
**Exemple** (pmoparadise/src/pmoserver_ext.rs:168-180):
|
||||
```rust
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/now-playing",
|
||||
params(("channel" = Option<u8>, Query)),
|
||||
responses((status = 200, body = NowPlayingResponse)),
|
||||
tag = "Radio Paradise"
|
||||
)]
|
||||
async fn get_now_playing(
|
||||
State(state): State<RadioParadiseState>,
|
||||
Query(params): Query<ParadiseQuery>,
|
||||
) -> Result<Json<NowPlayingResponse>, StatusCode> {
|
||||
let client = state.client_for_params(¶ms).await?;
|
||||
let now_playing = client.now_playing().await?;
|
||||
Ok(Json(now_playing.into()))
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Documentation OpenAPI (optionnel)
|
||||
|
||||
Utiliser `utoipa` pour générer automatiquement la documentation Swagger.
|
||||
|
||||
**Étapes** :
|
||||
1. Annoter les handlers avec `#[utoipa::path(...)]`
|
||||
2. Définir les schémas avec `#[derive(ToSchema)]`
|
||||
3. Créer une structure `#[derive(OpenApi)]`
|
||||
|
||||
**Exemple** (pmoparadise/src/pmoserver_ext.rs:315-350):
|
||||
```rust
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct NowPlayingResponse {
|
||||
pub event: u64,
|
||||
pub stream_url: String,
|
||||
pub songs: Vec<SongInfo>,
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "Radio Paradise API",
|
||||
version = "1.0.0",
|
||||
description = "API REST pour Radio Paradise"
|
||||
),
|
||||
paths(
|
||||
get_now_playing,
|
||||
get_current_block,
|
||||
),
|
||||
components(schemas(
|
||||
NowPlayingResponse,
|
||||
SongInfo,
|
||||
)),
|
||||
tags((name = "Radio Paradise"))
|
||||
)]
|
||||
pub struct RadioParadiseApiDoc;
|
||||
```
|
||||
|
||||
### 6. Router Axum
|
||||
|
||||
Pour des endpoints complexes, créer un sous-router réutilisable.
|
||||
|
||||
**Exemple** (pmoparadise/src/pmoserver_ext.rs:354-365):
|
||||
```rust
|
||||
pub fn create_api_router(state: RadioParadiseState) -> Router {
|
||||
Router::new()
|
||||
.route("/now-playing", get(get_now_playing))
|
||||
.route("/block/current", get(get_current_block))
|
||||
.route("/block/{event_id}", get(get_block_by_id))
|
||||
.route("/channels", get(get_channels))
|
||||
.with_state(state)
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern avancé : Extension avec async-trait
|
||||
|
||||
Pour les extensions nécessitant des opérations asynchrones complexes, utiliser `async_trait`.
|
||||
|
||||
**Exemple** (pmomediaserver/src/paradise_streaming.rs:31-68):
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait ParadiseStreamingExt {
|
||||
async fn init_paradise_streaming(&mut self)
|
||||
-> Result<Arc<ParadiseChannelManager>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ParadiseStreamingExt for pmoserver::Server {
|
||||
async fn init_paradise_streaming(&mut self)
|
||||
-> Result<Arc<ParadiseChannelManager>>
|
||||
{
|
||||
// 1. Récupérer/initialiser des caches singletons
|
||||
let audio_cache = match get_audio_cache() {
|
||||
Some(cache) => cache,
|
||||
None => {
|
||||
let cache = self.init_audio_cache_configured().await?;
|
||||
register_audio_cache(cache.clone());
|
||||
cache
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Créer le manager de canaux
|
||||
let manager = Arc::new(
|
||||
ParadiseChannelManager::with_defaults(base_url).await?
|
||||
);
|
||||
register_global_manager(manager.clone());
|
||||
|
||||
// 3. Enregistrer les routes pour chaque canal
|
||||
for descriptor in ALL_CHANNELS.iter() {
|
||||
let path = format!("/stream/{}/flac", descriptor.slug);
|
||||
self.add_handler_with_state(&path, handler, state).await;
|
||||
}
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern d'intégration : Control Point
|
||||
|
||||
Le Control Point illustre une extension avec API REST complète incluant gestion d'état, timeouts et spawn de tâches.
|
||||
|
||||
### Structure de l'état
|
||||
|
||||
**Exemple** (pmocontrol/src/pmoserver_ext.rs:42-51):
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct ControlPointState {
|
||||
control_point: Arc<ControlPoint>,
|
||||
}
|
||||
|
||||
impl ControlPointState {
|
||||
pub fn new(control_point: Arc<ControlPoint>) -> Self {
|
||||
Self { control_point }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handlers avec spawn_blocking
|
||||
|
||||
Pour les opérations synchrones UPnP, utiliser `spawn_blocking` pour éviter de bloquer le runtime Tokio.
|
||||
|
||||
**Exemple** (pmocontrol/src/pmoserver_ext.rs:68-92):
|
||||
```rust
|
||||
async fn list_renderers(
|
||||
State(state): State<ControlPointState>
|
||||
) -> Json<Vec<RendererSummary>> {
|
||||
// Déporter le travail synchrone sur un thread dédié
|
||||
let control_point = state.control_point.clone();
|
||||
let summaries = tokio::task::spawn_blocking(move || {
|
||||
let renderers = control_point.list_music_renderers();
|
||||
renderers.into_iter()
|
||||
.map(|r| RendererSummary {
|
||||
id: r.id().0.clone(),
|
||||
friendly_name: r.friendly_name().to_string(),
|
||||
online: r.is_online(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
Json(summaries)
|
||||
}
|
||||
```
|
||||
|
||||
### Handlers avec timeouts
|
||||
|
||||
Pour les commandes réseau, toujours utiliser des timeouts pour éviter les blocages.
|
||||
|
||||
**Exemple** (pmocontrol/src/pmoserver_ext.rs:240-280):
|
||||
```rust
|
||||
const TRANSPORT_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
async fn play_renderer(
|
||||
State(state): State<ControlPointState>,
|
||||
Path(renderer_id): Path<String>,
|
||||
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let rid = DeviceId(renderer_id.clone());
|
||||
let renderer = state.control_point
|
||||
.music_renderer_by_id(&rid)
|
||||
.ok_or_else(|| (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse {
|
||||
error: format!("Renderer {} not found", renderer_id)
|
||||
})
|
||||
))?;
|
||||
|
||||
// Spawn blocking task avec timeout
|
||||
let play_task = tokio::task::spawn_blocking(move ||
|
||||
renderer.play()
|
||||
);
|
||||
|
||||
time::timeout(TRANSPORT_COMMAND_TIMEOUT, play_task)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
warn!("Play command timeout");
|
||||
(StatusCode::GATEWAY_TIMEOUT, Json(ErrorResponse {
|
||||
error: "Command timed out".to_string()
|
||||
}))
|
||||
})?
|
||||
.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
|
||||
error: format!("Task error: {}", e)
|
||||
}))
|
||||
})?
|
||||
.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
|
||||
error: format!("Failed to play: {}", e)
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(Json(SuccessResponse {
|
||||
message: "Playback started".to_string()
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Handlers avec spawn en arrière-plan
|
||||
|
||||
Pour les opérations longues qui ne nécessitent pas d'attente, utiliser `tokio::spawn` et retourner immédiatement.
|
||||
|
||||
**Exemple** (pmocontrol/src/pmoserver_ext.rs:635-675):
|
||||
```rust
|
||||
async fn seek_queue_index(
|
||||
State(state): State<ControlPointState>,
|
||||
Path(renderer_id): Path<String>,
|
||||
Json(payload): Json<SeekQueueRequest>,
|
||||
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let rid = DeviceId(renderer_id.clone());
|
||||
state.control_point.music_renderer_by_id(&rid)
|
||||
.ok_or_else(|| (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse {
|
||||
error: format!("Renderer {} not found", renderer_id)
|
||||
})
|
||||
))?;
|
||||
|
||||
// Lancer la commande en arrière-plan et retourner immédiatement
|
||||
// L'UI sera mise à jour via SSE
|
||||
let control_point = Arc::clone(&state.control_point);
|
||||
let rid_for_task = rid.clone();
|
||||
let index = payload.index;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
control_point.play_queue_index(&rid_for_task, index)
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
debug!("Successfully started playback at index {}", index);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("Failed to seek to index {}: {}", index, e);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Task join error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Retour immédiat
|
||||
Ok(Json(SuccessResponse {
|
||||
message: format!("Playing item at index {}", index)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern d'intégration : WebApp (SPA)
|
||||
|
||||
Le module `pmoapp` illustre l'intégration d'une application Vue.js via RustEmbed.
|
||||
|
||||
### Trait d'extension SPA
|
||||
|
||||
**Exemple** (pmoapp/src/lib.rs:145-165):
|
||||
```rust
|
||||
pub trait WebAppExt {
|
||||
/// Ajoute une Single Page Application
|
||||
async fn add_webapp<W>(&mut self, path: &str)
|
||||
where
|
||||
W: RustEmbed + Clone + Send + Sync + 'static;
|
||||
|
||||
/// Ajoute une webapp avec redirection automatique
|
||||
async fn add_webapp_with_redirect<W>(&mut self, path: &str)
|
||||
where
|
||||
W: RustEmbed + Clone + Send + Sync + 'static;
|
||||
}
|
||||
```
|
||||
|
||||
### Structure RustEmbed
|
||||
|
||||
**Exemple** (pmoapp/src/lib.rs:130-140):
|
||||
```rust
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed, Clone)]
|
||||
#[folder = "webapp/dist"]
|
||||
pub struct Webapp;
|
||||
```
|
||||
|
||||
### Implémentation (feature-gated)
|
||||
|
||||
**Fichier** : pmoapp/src/pmoserver_impl.rs
|
||||
```rust
|
||||
#[cfg(feature = "pmoserver")]
|
||||
impl WebAppExt for pmoserver::Server {
|
||||
async fn add_webapp<W>(&mut self, path: &str)
|
||||
where
|
||||
W: RustEmbed + Clone + Send + Sync + 'static
|
||||
{
|
||||
self.add_spa::<W>(path).await
|
||||
}
|
||||
|
||||
async fn add_webapp_with_redirect<W>(&mut self, path: &str)
|
||||
where
|
||||
W: RustEmbed + Clone + Send + Sync + 'static
|
||||
{
|
||||
self.add_spa::<W>(path).await;
|
||||
self.add_redirect("/", path).await;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registres globaux (Singletons)
|
||||
|
||||
Pour les ressources partagées entre extensions, utiliser des registres globaux.
|
||||
|
||||
### Pattern de singleton
|
||||
|
||||
**Exemple** (pmoaudiocache/src/lib.rs:268-295):
|
||||
```rust
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
static AUDIO_CACHE: OnceCell<Arc<Cache>> = OnceCell::new();
|
||||
|
||||
/// Enregistre le cache audio global
|
||||
pub fn register_audio_cache(cache: Arc<Cache>) {
|
||||
let _ = AUDIO_CACHE.set(cache);
|
||||
}
|
||||
|
||||
/// Accès global au cache audio
|
||||
pub fn get_audio_cache() -> Option<Arc<Cache>> {
|
||||
AUDIO_CACHE.get().cloned()
|
||||
}
|
||||
```
|
||||
|
||||
### Utilisation dans les extensions
|
||||
|
||||
**Exemple** (pmomediaserver/src/paradise_streaming.rs:77-95):
|
||||
```rust
|
||||
async fn init_paradise_streaming(&mut self) -> Result<Arc<Manager>> {
|
||||
// Récupérer ou initialiser le singleton
|
||||
let audio_cache = match get_audio_cache() {
|
||||
Some(cache) => {
|
||||
info!("Using existing audio cache singleton");
|
||||
cache
|
||||
}
|
||||
None => {
|
||||
info!("Initializing new audio cache singleton");
|
||||
let cache = self.init_audio_cache_configured().await?;
|
||||
register_audio_cache(cache.clone());
|
||||
cache
|
||||
}
|
||||
};
|
||||
|
||||
// Utiliser le cache dans le manager
|
||||
let manager = Manager::new(audio_cache).await?;
|
||||
Ok(Arc::new(manager))
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist d'implémentation
|
||||
|
||||
Pour implémenter une nouvelle extension `pmoserver`, suivre ces étapes :
|
||||
|
||||
### 1. Structure du module
|
||||
|
||||
- [ ] Créer un module `pmoserver_ext.rs` dans la crate
|
||||
- [ ] Ajouter la feature `pmoserver` dans `Cargo.toml`
|
||||
- [ ] Importer le module avec `#[cfg(feature = "pmoserver")]`
|
||||
|
||||
### 2. Définition du trait
|
||||
|
||||
- [ ] Créer un trait public `{Domaine}Ext`
|
||||
- [ ] Ajouter des méthodes préfixées `init_*` ou `add_*`
|
||||
- [ ] Documenter chaque méthode avec des exemples
|
||||
- [ ] Ajouter `#[async_trait]` si nécessaire
|
||||
|
||||
### 3. État partagé
|
||||
|
||||
- [ ] Créer une structure `{Domaine}State`
|
||||
- [ ] Implémenter `Clone`
|
||||
- [ ] Utiliser `Arc<T>` pour les ressources partagées
|
||||
- [ ] Ajouter des méthodes helpers si nécessaire
|
||||
|
||||
### 4. Handlers HTTP
|
||||
|
||||
- [ ] Définir les fonctions handler async
|
||||
- [ ] Utiliser les extracteurs Axum appropriés
|
||||
- [ ] Gérer les erreurs avec `Result<T, StatusCode>`
|
||||
- [ ] Ajouter des logs (info, warn, error)
|
||||
|
||||
### 5. Documentation OpenAPI (optionnel)
|
||||
|
||||
- [ ] Annoter les handlers avec `#[utoipa::path(...)]`
|
||||
- [ ] Définir les schémas avec `#[derive(ToSchema)]`
|
||||
- [ ] Créer une structure `#[derive(OpenApi)]`
|
||||
- [ ] Inclure des exemples dans la documentation
|
||||
|
||||
### 6. Implémentation du trait
|
||||
|
||||
- [ ] Implémenter le trait pour `pmoserver::Server`
|
||||
- [ ] Utiliser les méthodes du serveur pour enregistrer les routes
|
||||
- [ ] Gérer les erreurs avec `anyhow::Result`
|
||||
- [ ] Retourner les ressources créées si nécessaire
|
||||
|
||||
### 7. Tests
|
||||
|
||||
- [ ] Tester les handlers indépendamment
|
||||
- [ ] Tester l'intégration avec le serveur
|
||||
- [ ] Vérifier la documentation OpenAPI générée
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### 1. Gestion des erreurs
|
||||
|
||||
- Utiliser `anyhow::Result` pour les méthodes d'initialisation
|
||||
- Utiliser `Result<T, StatusCode>` pour les handlers HTTP
|
||||
- Toujours logger les erreurs avant de les retourner
|
||||
- Préférer les codes HTTP sémantiques (404, 500, 504, etc.)
|
||||
|
||||
### 2. Performance
|
||||
|
||||
- Utiliser `spawn_blocking` pour les opérations synchrones
|
||||
- Toujours ajouter des timeouts pour les opérations réseau
|
||||
- Cloner l'état minimal nécessaire dans les closures
|
||||
- Utiliser des `Arc<T>` plutôt que `Mutex<T>` quand possible
|
||||
|
||||
### 3. Concurrence
|
||||
|
||||
- Préférer `tokio::spawn` pour les tâches en arrière-plan
|
||||
- Retourner immédiatement pour les opérations longues
|
||||
- Utiliser des channels pour communiquer entre tâches
|
||||
- Éviter les `RwLock` dans les handlers (préférer `spawn_blocking`)
|
||||
|
||||
### 4. Documentation
|
||||
|
||||
- Documenter chaque fonction publique avec des exemples
|
||||
- Utiliser les annotations `utoipa` pour l'OpenAPI
|
||||
- Inclure des exemples d'utilisation dans la doc du trait
|
||||
- Documenter les routes HTTP créées
|
||||
|
||||
### 5. Features Cargo
|
||||
|
||||
- Toujours feature-gate les extensions avec `#[cfg(feature = "pmoserver")]`
|
||||
- Déclarer les dépendances comme optionnelles
|
||||
- Documenter les features dans le README de la crate
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### Initialisation simple
|
||||
|
||||
```rust
|
||||
use pmoserver::ServerBuilder;
|
||||
use pmoaudiocache::AudioCacheExt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let mut server = ServerBuilder::new("MyApp", "http://localhost", 8080)
|
||||
.build();
|
||||
|
||||
// Initialiser le cache audio
|
||||
server.init_audio_cache("./cache", 500).await?;
|
||||
|
||||
server.start().await;
|
||||
server.wait().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Initialisation avec configuration
|
||||
|
||||
```rust
|
||||
use pmoserver::ServerBuilder;
|
||||
use pmoaudiocache::AudioCacheExt;
|
||||
use pmocovers::CoverCacheExt;
|
||||
use pmoparadise::RadioParadiseExt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let mut server = ServerBuilder::new("MyApp", "http://localhost", 8080)
|
||||
.build();
|
||||
|
||||
// Initialiser plusieurs extensions
|
||||
server.init_audio_cache_configured().await?;
|
||||
server.init_cover_cache_configured().await?;
|
||||
server.init_radioparadise().await?;
|
||||
|
||||
server.start().await;
|
||||
server.wait().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Initialisation avec état partagé
|
||||
|
||||
```rust
|
||||
use pmoserver::ServerBuilder;
|
||||
use pmomediaserver::ParadiseStreamingExt;
|
||||
use pmocontrol::{ControlPoint, ControlPointState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let mut server = ServerBuilder::new("MyApp", "http://localhost", 8080)
|
||||
.build();
|
||||
|
||||
// Initialiser Radio Paradise avec caches
|
||||
let manager = server.init_paradise_streaming().await?;
|
||||
|
||||
// Créer et enregistrer le Control Point
|
||||
let control_point = Arc::new(ControlPoint::new());
|
||||
let state = ControlPointState::new(control_point);
|
||||
// ... enregistrer les routes du control point ...
|
||||
|
||||
server.start().await;
|
||||
server.wait().await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Références
|
||||
|
||||
### Fichiers sources analysés
|
||||
|
||||
- `pmoapp/src/lib.rs` : Pattern SPA avec RustEmbed
|
||||
- `pmocontrol/src/pmoserver_ext.rs` : API REST complète avec Control Point
|
||||
- `pmoparadise/src/pmoserver_ext.rs` : API REST avec client externe
|
||||
- `pmoaudiocache/src/lib.rs` : Cache avec routes de fichiers
|
||||
- `pmomediaserver/src/paradise_streaming.rs` : Extension complexe avec streaming
|
||||
|
||||
### Dépendances communes
|
||||
|
||||
- `axum` : Framework HTTP
|
||||
- `async-trait` : Traits async
|
||||
- `utoipa` : Documentation OpenAPI
|
||||
- `tokio` : Runtime async
|
||||
- `anyhow` : Gestion d'erreurs
|
||||
- `serde` : Sérialisation JSON
|
||||
- `tracing` : Logging
|
||||
|
||||
## Conclusion
|
||||
|
||||
Le pattern `pmoserver_ext` offre une architecture extensible et modulaire pour ajouter des fonctionnalités au serveur HTTP PMOMusic. En suivant les conventions établies et les bonnes pratiques, chaque nouvelle extension peut être développée indépendamment tout en s'intégrant de manière cohérente avec l'écosystème existant.
|
||||
157
Blackboard/Report/pmoserver_ext.md
Normal file
157
Blackboard/Report/pmoserver_ext.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Rapport : Documentation du pattern pmoserver_ext
|
||||
|
||||
## Date
|
||||
2026-01-12
|
||||
|
||||
## Tâche originale
|
||||
Réaliser une fiche descriptive sur le pattern à suivre pour implémenter un trait d'extension du PMO serveur, en analysant les fichiers :
|
||||
- pmoapp/src/lib.rs
|
||||
- pmocontrol/src/pmoserver_ext.rs
|
||||
- pmoparadise/src/pmoserver_ext.rs
|
||||
- pmoaudiocache/src/lib.rs
|
||||
- pmomediaserver/src/paradise_streaming.rs
|
||||
|
||||
## Travail réalisé
|
||||
|
||||
### 1. Analyse des fichiers sources
|
||||
J'ai analysé les cinq fichiers sources mentionnés pour identifier les patterns récurrents :
|
||||
|
||||
- **pmoapp/src/lib.rs** : Illustre le pattern d'intégration d'une Single Page Application (Vue.js) via RustEmbed
|
||||
- **pmocontrol/src/pmoserver_ext.rs** : Montre une API REST complète avec gestion d'état, timeouts, spawn_blocking pour opérations synchrones
|
||||
- **pmoparadise/src/pmoserver_ext.rs** : Démontre l'intégration d'un client externe avec documentation OpenAPI
|
||||
- **pmoaudiocache/src/lib.rs** : Présente le pattern de cache avec routes de fichiers et registre singleton
|
||||
- **pmomediaserver/src/paradise_streaming.rs** : Illustre une extension complexe avec streaming, gestion de caches multiples et async-trait
|
||||
|
||||
### 2. Identification des patterns communs
|
||||
|
||||
#### Architecture de base
|
||||
Tous les exemples suivent une architecture similaire :
|
||||
1. Définition d'un trait d'extension (ex: `AudioCacheExt`, `RadioParadiseExt`)
|
||||
2. Implémentation du trait pour `pmoserver::Server`
|
||||
3. Utilisation de feature gates `#[cfg(feature = "pmoserver")]`
|
||||
|
||||
#### Composants récurrents
|
||||
- **Trait d'extension** : Interface publique avec méthodes `init_*` ou `add_*`
|
||||
- **État partagé** : Structure `{Domaine}State` avec `Clone` et `Arc<T>`
|
||||
- **Handlers HTTP** : Fonctions async avec extracteurs Axum
|
||||
- **Documentation OpenAPI** : Annotations `utoipa` pour Swagger
|
||||
- **Router Axum** : Sous-routers réutilisables
|
||||
|
||||
#### Patterns avancés identifiés
|
||||
- Utilisation de `spawn_blocking` pour opérations synchrones UPnP
|
||||
- Timeouts systématiques pour opérations réseau
|
||||
- Spawn en arrière-plan pour opérations longues
|
||||
- Registres globaux (singletons) avec `OnceCell`
|
||||
- Intégration SPA avec RustEmbed
|
||||
|
||||
### 3. Rédaction de la documentation
|
||||
|
||||
Le document créé (`Blackboard/Architecture/pmoserver_ext.md`) contient :
|
||||
|
||||
#### Structure principale
|
||||
1. **Vue d'ensemble** : Principe et architecture du pattern
|
||||
2. **Composants du pattern** : 6 composants détaillés avec exemples
|
||||
3. **Pattern avancé** : Utilisation d'async-trait
|
||||
4. **Patterns d'intégration** : Control Point et WebApp
|
||||
5. **Registres globaux** : Pattern singleton avec OnceCell
|
||||
6. **Checklist d'implémentation** : Guide pas à pas
|
||||
7. **Bonnes pratiques** : 5 sections (erreurs, performance, concurrence, documentation, features)
|
||||
8. **Exemples d'utilisation** : 3 exemples concrets
|
||||
9. **Références** : Fichiers sources et dépendances
|
||||
|
||||
#### Points forts de la documentation
|
||||
|
||||
**Exemples de code concrets** : Chaque concept est illustré par des extraits de code réels avec références aux fichiers sources (ex: `pmoaudiocache/src/lib.rs:200-215`).
|
||||
|
||||
**Patterns avancés documentés** :
|
||||
- Gestion des timeouts pour éviter les blocages réseau
|
||||
- Utilisation de `spawn_blocking` pour les opérations synchrones
|
||||
- Spawn en arrière-plan pour retour immédiat à l'utilisateur
|
||||
- Registres singleton pour partage de ressources entre extensions
|
||||
|
||||
**Checklist pratique** : Liste de 7 sections avec cases à cocher pour guider l'implémentation d'une nouvelle extension.
|
||||
|
||||
**Bonnes pratiques** : Section dédiée couvrant la gestion d'erreurs, performance, concurrence, documentation et features Cargo.
|
||||
|
||||
**Exemples d'utilisation** : Trois scénarios d'utilisation progressive (simple, avec configuration, avec état partagé).
|
||||
|
||||
### 4. Organisation des livrables
|
||||
|
||||
#### Document d'architecture
|
||||
- **Emplacement** : `Blackboard/Architecture/pmoserver_ext.md`
|
||||
- **Taille** : 745 lignes
|
||||
- **Format** : Markdown structuré avec syntaxe code Rust
|
||||
|
||||
#### Rapport de travail
|
||||
- **Emplacement** : `Blackboard/Report/pmoserver_ext.md`
|
||||
- **Contenu** : Ce document
|
||||
|
||||
## Observations techniques
|
||||
|
||||
### Cohérence architecturale
|
||||
Tous les modules analysés suivent une architecture très cohérente :
|
||||
- Même convention de nommage (`{Domaine}Ext`, `{Domaine}State`)
|
||||
- Même structure d'implémentation (trait → implémentation → handlers)
|
||||
- Même gestion des erreurs (`anyhow::Result` pour init, `Result<T, StatusCode>` pour handlers)
|
||||
|
||||
### Patterns de concurrence
|
||||
La codebase fait un excellent usage des primitives Tokio :
|
||||
- `spawn_blocking` pour isoler les opérations synchrones UPnP
|
||||
- `spawn` pour les tâches en arrière-plan (ex: seek_queue_index)
|
||||
- `Arc` plutôt que `Mutex` pour le partage de ressources
|
||||
- Timeouts systématiques pour éviter les blocages
|
||||
|
||||
### Documentation OpenAPI
|
||||
L'utilisation de `utoipa` est systématique et bien structurée :
|
||||
- Annotations `#[utoipa::path(...)]` sur tous les handlers
|
||||
- Schémas `#[derive(ToSchema)]` pour tous les types exposés
|
||||
- Documentation complète avec exemples dans les structures `OpenApi`
|
||||
|
||||
## Qualité du résultat
|
||||
|
||||
### Points forts
|
||||
1. **Exhaustivité** : Tous les aspects du pattern sont couverts
|
||||
2. **Exemples concrets** : Extraits de code réels avec références aux fichiers
|
||||
3. **Praticité** : Checklist et bonnes pratiques directement applicables
|
||||
4. **Pédagogie** : Structure progressive du simple au complexe
|
||||
|
||||
### Ce qui pourrait être amélioré
|
||||
1. **Diagrammes** : Ajout de diagrammes de séquence pour les patterns complexes
|
||||
2. **Tests** : Exemples de tests unitaires pour les handlers
|
||||
3. **Cas d'erreur** : Documentation des cas d'erreur fréquents et leurs solutions
|
||||
4. **Performance** : Benchmarks ou métriques de performance
|
||||
|
||||
## Recommandations
|
||||
|
||||
### Pour l'utilisation de cette documentation
|
||||
1. Utiliser la checklist comme guide lors de l'implémentation d'une nouvelle extension
|
||||
2. Se référer aux exemples de code pour les patterns spécifiques (timeouts, spawn, etc.)
|
||||
3. Consulter les bonnes pratiques avant chaque implémentation
|
||||
|
||||
### Pour l'évolution de la documentation
|
||||
1. Ajouter des exemples de tests à mesure que le projet mûrit
|
||||
2. Documenter les problèmes rencontrés et leurs solutions
|
||||
3. Mettre à jour avec de nouveaux patterns si l'architecture évolue
|
||||
|
||||
### Pour le projet PMOMusic
|
||||
1. Considérer l'extraction de macros pour réduire le boilerplate
|
||||
2. Envisager un générateur de code pour les extensions simples
|
||||
3. Documenter les décisions d'architecture dans ce répertoire
|
||||
|
||||
## Conclusion
|
||||
|
||||
La documentation du pattern `pmoserver_ext` est maintenant disponible dans `Blackboard/Architecture/pmoserver_ext.md`. Elle fournit un guide complet et pratique pour implémenter de nouvelles extensions au serveur PMOMusic en suivant les conventions établies.
|
||||
|
||||
Le pattern identifié est solide, cohérent et bien adapté aux besoins du projet. La documentation créée devrait permettre à tout développeur de comprendre et d'appliquer ce pattern efficacement.
|
||||
|
||||
## Fichiers créés
|
||||
|
||||
1. `Blackboard/Architecture/pmoserver_ext.md` (745 lignes) - Documentation technique complète
|
||||
2. `Blackboard/Report/pmoserver_ext.md` (ce fichier) - Rapport de travail
|
||||
|
||||
## Prochaines étapes suggérées
|
||||
|
||||
1. Révision du document par l'humain
|
||||
2. Décision de classement : `Done` ou `ToDiscuss`
|
||||
3. Si `Done` : Synthèse finale pour archivage
|
||||
4. Si `ToDiscuss` : Annotations et travail supplémentaire
|
||||
96
Blackboard/Rules.md
Normal file
96
Blackboard/Rules.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Règles de développement du projet PMO Musique.
|
||||
|
||||
L'application PMO Music est développée en collaboration avec un assistant de type LLM. soit Claude, soit ChatGPT, soit des petits LLM qui tournent en local sur Ollama.
|
||||
|
||||
## L'application PMO Music.
|
||||
|
||||
L'application PMO Musique est un système de gestion de la musique basé autour d'une architecture UPNP DLNA. A terme, elle contiendra trois composants:
|
||||
|
||||
- Un serveur de médias
|
||||
- Un point de contrôle
|
||||
- Un render de médias.
|
||||
|
||||
Les seuls médias considérés sont des médias audio. Les médias vidéo ne sont absolument pas pris en charge. L'objectif est de fournir une architecture HiFi, donc LossLess et si possible Bit-Perfect.
|
||||
|
||||
L'application est entièrement développée en Rust. Sauf la couche application web qui est développée en JavaScript/Typescript en se basant sur le framework [Vue](https://vuejs.org/).
|
||||
|
||||
## Règles de fonctionnement du Blackboard.
|
||||
|
||||
Le Blackboard ou tableau noir est un répertoire nommé `Blackboard` situé à la racine du projet.
|
||||
|
||||
L'ensemble des fichiers de ce répertoire sont des textes suivant le format Markdown.
|
||||
|
||||
L'arborescence de ce répertoire est la suivante:
|
||||
|
||||
```
|
||||
Blackboard
|
||||
├── Done
|
||||
├── Report
|
||||
├── Rules.md
|
||||
├── ToDiscuss
|
||||
├── ToThinkAbout
|
||||
└── Todo
|
||||
```
|
||||
|
||||
### La phase de réflexion.
|
||||
|
||||
- `ToThinkAbout`: Contient les tâches à réfléchir pour le projet.
|
||||
|
||||
Les documents dans le répertoire `ToThinkAbout` sont des fichiers contenant des idées et des questions à réfléchir pour le projet. Ils sont utilisés à terme pour générer les documents de tâche qui seront placés dans le répertoire `Todo`.
|
||||
|
||||
### Les quatres répertoires de base pour le workflow de développement.
|
||||
- `Todo`: Contient les tâches à faire pour le projet.
|
||||
- `Report`: Contient les rapports de travail pour le projet.
|
||||
- `ToDiscuss`: Contient les tâches à discuter pour le projet.
|
||||
- `Done`: Contient les tâches terminées pour le projet.
|
||||
|
||||
Les tâches à faire sont décrites dans des fichiers présents dans le répertoire `Todo`. Leur réalisation conduit à la rédaction d'un rapport à placer dans le répertoire `Report`. Le rapport d'une tâche doit avoir le même nom de fichier que la tâche originale. À la suite du rapport, deux issues sont possibles. Soit la tâche est considérée comme achevée. Dans ce cas, elle est déplacée dans le répertoire `Done`. Soit la tâche est considérée comme incomplète. Dans ce cas, elle est déplacée dans le répertoire `ToDiscuss`.
|
||||
|
||||
C'est l'humain qui décide quand une tâche peut être considérée comme *done* ou *to discuss*. En aucun cas, l'assistant peut décider de classifier une tâche après la rédaction du rapport.
|
||||
|
||||
Lors du déplacement d'une tâche dans le répertoire `ToDiscuss`, Le document de tâche est complété par les remarques suite à la réalisation de la première étape du travail. La poursuite d'une tâche mise en mode *to discuss* est laissée à la decision de l'humain. Lorsqu'un travail sur cette tâche est réalisé, Le rapport d'analyse correspondant est complété pour expliquer comment les annotations ont été prises en compte.
|
||||
|
||||
Lors du déplacement d'une tâche dans le répertoire `Done` une synthèse du document de tâche originale et du rapport est réalisée. C'est ce rapport final qui est placé dans `Done`.
|
||||
|
||||
- `Rules.md`: Contient les règles de développement du projet.
|
||||
|
||||
## Versioning
|
||||
|
||||
### Gestion des versions
|
||||
|
||||
- La version du projet est définie dans le fichier `PMOMusic/Cargo.toml`.
|
||||
- La version du projet est synchronisée avec le fichier `version.txt`.
|
||||
- La version du projet est incrémentée à push vers le dépôt git à jour du projet.
|
||||
|
||||
### Système de gestion des versions
|
||||
|
||||
- Le logiciel de gestion de versionning utilisé est [jj](https://github.com/jj-vcs/jj).
|
||||
- L'URL du repository est: https://gargoton.petite-maison-orange.fr/eric/pmomusic.git
|
||||
|
||||
#### Workflow de gestion des versions
|
||||
|
||||
- Pour chaque nouveau travail sur le code, un nouveau commit `jj` est créé, par l'appel de la règle `jjnew` du makefile.:
|
||||
|
||||
```bash
|
||||
make jjnew
|
||||
```
|
||||
|
||||
L'appel de la règle `jjnew` documente le commit en cours à partir de tous les changements du commit, puis appelle la commande `jj new`.
|
||||
|
||||
- Pour pousser une série de commit sur le serveur Git on utilise la règle `jjpush` du makefile.:
|
||||
|
||||
```bash
|
||||
make jjpush
|
||||
```
|
||||
|
||||
- L'appel de la règle `jjpush` documente le commit en cours à partir de tous les changements du commit, puis appelle la commande `jj git push --change @`.
|
||||
|
||||
|
||||
|
||||
- Cela provoque la création d'une nouvelle branche sur le serveur Git et d'un pull request. Il faut ensuite valider le pull request pour pouvoir récupérer l'état courant dans la branche de développement sur la machine.
|
||||
|
||||
- Pour récupérer les dernières modifications du dépôt Git, après avoir validé le pull request, on utilise la règle `jjfetch` du makefile.:
|
||||
|
||||
```bash
|
||||
make jjfetch
|
||||
```
|
||||
0
Blackboard/ToDiscuss/pmoserver_ext.md
Normal file
0
Blackboard/ToDiscuss/pmoserver_ext.md
Normal file
0
Blackboard/Todo/WeabApp_debouncingSSE.md
Normal file
0
Blackboard/Todo/WeabApp_debouncingSSE.md
Normal file
Reference in New Issue
Block a user