# Évaluation du plan : centraliser_base_url_axum_middleware

## Résumé de l'audit

Le plan est **bien pensé et cohérent**. Il identifie correctement le problème et la solution. Cependant, j'ai identifié plusieurs points nécessitant des amendements.

---

## Points validés (conformes au code actuel)

1. **Problème bien identifié** : URLs hardcodées avec IP locale (`PMO_SERVER_URL`) retournées au frontend via reverse proxy.

2. **`get_request_base_url` existe déjà** à `pmoserver/src/lib.rs:199` — pas besoin de la recréer.

3. **`covers_route_for` existe déjà** dans `pmocache/src/lib.rs:149`.

4. **`covers_absolute_url_for` utilisée dans les contextes UPnP** :
   - `pmoupnp/src/cache_registry.rs:57`
   - `pmoradiofrance/src/metadata_cache.rs:263`
   - `pmoparadise/src/source.rs:216`
   - `pmoaudio-ext/src/sinks/streaming_icyflac_sink.rs:91`

5. **Route audio correcte** : `/audio/tracks/{pk}` (pas `/audio/flac/{pk}`).

6. **Architecture du Server** : Les routes sont construites dynamiquement via `Arc<RwLock<Router>>`. Le layer devra être ajouté dans la construction du router, pas après.

---

## Points à amender

### 1. Ajout du layer dans le Server

Le plan suggère d'ajouter le layer "dans `server.rs`" mais la structure du router est complexe :
- Les routes sont dynamiques (`RwLock<Router>`)
- Le router final est un fallback qui délègue

**Correction** : Ajouter le layer directement lors de la création du `registry_route` initial (ligne 120-122) :

```rust
let registry_route = Router::new()
    .route("/api/registry", get(get_api_registry))
    .with_state(api_registry.clone())
    .layer(base_url_layer());  // ← ici
```

### 2. Comportement requis pour LAN vs WAN

Le middleware doit supporter les deux cas d'usage :

- **LAN (sans reverse proxy)** : Pas de headers `X-Forwarded-*` → utiliser l'adresse IP locale du serveur (`PMO_SERVER_URL`)
- **WAN (via reverse proxy)** : Headers `X-Forwarded-*` présents → utiliser l'URL publique du reverse proxy

**Important** : `get_request_base_url` dans `pmoserver/src/lib.rs:199` lit déjà ces headers. Le fallback doit être `PMO_SERVER_URL` qui est configuré au démarrage avec l'IP locale.

### 3. Chemin du middleware dans la pile

Le plan dit d'appliquer le layer "avant" les autres. En réalité, Tower/Acorn applique les couches dans l'ordre où elles sont ajoutées — le premier layer ajouté est le plus extérieur (exécuté en premier). Le `base_url_layer` doit donc être ajouté en **premier** (le plus intérieur) pour voir les headers nettoyés.

### 3. Les handlers n'ont PAS besoin de BaseUrl

Après analyse, **aucun handler** dans le codebase actuel n'appelle `covers_absolute_url_for()` directement pour le frontend. Les `album_art_uri` sont :
- Soit **propagés** depuis les réponses UPnP des media servers (pas des URLs pmomusic)
- Soit **construits en tâche de fond** dans les caches (RadioFrance, RadioParadise)

**Correction** : Le plan surestime le nombre de handlers à modifier. La vraie question est : d'où viennent les URLs incorrectes ?

### 4. Source du problème à clarifier

Les URLs incorrectes ne viennent pas des handlers REST classiques. Elles viennent probablement de :

**a) Tâches de fond** (background tasks) qui stockent des URLs complètes :
- `pmoradiofrance/src/metadata_cache.rs:263` — construit `covers_absolute_url_for()` dans le cache
- `pmoparadise/src/source.rs:216` — même problème

**b) API Qobuz** (`pmoqobuz/src/api_rest.rs:302`) — utilise `covers_route_for` (route relative, OK)

**c) Playlist** (`pmoplaylist/src/handle/read.rs:204,271,355`) — utilise `covers_route_for` (OK)

### 5. Correction du fallback

Le plan suggère `localhost:8080` ou `0.0.0.0:8080` comme fallback. Le port doit provenir de la configuration du serveur (`get_server_base_url()` existe déjà dans `pmoserver/src/lib.rs`).

**Correction** : Le fallback utilise `get_server_base_url()` (disponible via `GLOBAL_SERVER`) :
- En LAN : pas de `X-Forwarded-*` → `get_server_base_url()` → URLs en IP locale
- En WAN : `X-Forwarded-*` présents → URLs en URL publique du reverse proxy

### 6. Fonction `audio_route_for` pas nécessaire maintenant

Le plan propose d'ajouter `audio_route_for` dans `pmoaudiocache`. Mais :
- Les fichiers audio sont servis par `pmoaudiocache` lui-même (routes internes)
-Aucune URL audio n'est retournée au frontend via JSON

**Supprimer** cette étape du plan.

---

## Plan amendé

### Étape 0 — Audit spécifique (à faire avant implémentation)

```bash
# Trouver les constructions d'URLs dans les tâches de fond (caches, sources)
grep -rn "covers_absolute_url_for\|PMO_SERVER_URL" --include="*.rs" | grep -v "pmocontrol\|pmoplaylist\|pmoqobuz"

# Vérifier les URLs dans les réponses JSON des handlers
grep -rn "album_art_uri" --include="*.rs" | grep -E "fn |->"
```

Identifier spécifiquement quels endpoints REST retournent des URLs au frontend.

### Étape 1 — `pmoserver/src/lib.rs` : Ajouter `BaseUrl` + middleware

```rust
use axum::{extract::Request, middleware::Next, response::Response};

#[derive(Debug, Clone)]
pub struct BaseUrl(pub String);

impl BaseUrl {
    pub fn url_for(&self, route: &str) -> String {
        debug_assert!(route.starts_with('/'), "route must start with '/'");
        format!("{}{}", self.0.trim_end_matches('/'), route)
    }
}

pub async fn base_url_middleware(mut request: Request, next: Next) -> Response {
    // Priorité : 1) X-Forwarded-* (reverse proxy), 2) get_server_base_url() (adresse configurée)
    let base = get_request_base_url(request.headers())
        .or_else(|| get_server_base_url())
        .unwrap_or_else(|| {
            panic!(
                "BaseUrl: impossible de déterminer l'URL de base.\n\
                Configurer PMO_SERVER_URL ou démarrer le serveur avant les handlers HTTP."
            );
        });
    tracing::debug!("BaseUrl calculée : {}", base);
    request.extensions_mut().insert(BaseUrl(base));
    next.run(request).await
}

pub fn base_url_layer() -> axum::middleware::FromFnLayer {
    axum::middleware::from_fn(base_url_middleware)
}
```

**Comportement** :
- Accès LAN (pas de proxy) : `get_server_base_url()` → URLs en IP locale configurée
- Accès WAN (reverse proxy) : `X-Forwarded-*` → URLs en URL publique

**Note** : Si ni les headers ni le serveur ne sont disponibles, le middleware panic (fail-fast) car c'est une erreur de configuration.

### Étape 2 — `pmoserver/src/server.rs` : Appliquer le layer

Dans `Server::new()`, ligne ~120-122 :

```rust
let registry_route = Router::new()
    .route("/api/registry", get(get_api_registry))
    .with_state(api_registry.clone())
    .layer(base_url_layer());  // ← Ajouter ici (couche la plus intérieure)
```

### Étape 3 — `pmocache/src/lib.rs` : Renommer sans déprecation

```rust
// Rename direct - pas de déprecation (soft en cours de dev, pas une library)
pub fn covers_absolute_url_for_upnp(pk: &str, param: Option<&str>) -> String {
    // PMO_SERVER_URL contient l'IP locale (LAN) - utilisé uniquement pour UPnP
    // Fallback sur get_server_base_url() si dispo, sinon erreur
    let base = std::env::var("PMO_SERVER_URL")
        .or_else(|_| pmoserver::get_server_base_url().ok_or("PMO_SERVER_URL not set"))
        .unwrap_or_else(|e| {
            tracing::error!("covers_absolute_url_for_upnp: {}", e);
            panic!("BaseUrl non disponible pour UPnP");
        });
    format!("{}{}", base.trim_end_matches('/'), covers_route_for(pk, param))
}
```

### Étape 4 — Mettre à jour les appels UPnP

```bash
grep -rn "covers_absolute_url_for" --include="*.rs"
```

Modifier `pmoupnp/src/cache_registry.rs:57` → `covers_absolute_url_for_upnp`

### Étape 5 — Tâches de fond : stocker la route, pas l'URL

**pmoradiofrance/src/metadata_cache.rs:263** :
```rust
// Avant :
let public_url = pmocache::covers_absolute_url_for(&pk, None);

// Après : stocker la route relative
let album_art_route = pmocache::covers_route_for(&pk, None);
```

Le handler REST qui retourne ces métadonnées devra extraire `Extension<BaseUrl>` et appliquer `base_url.url_for()`.

**pmoparadise/src/source.rs:216** : Même traitement.

### Étape 6 — Vérification et tests

```bash
# Plus d'appels à covers_absolute_url_for dans les contextes HTTP
grep -rn "covers_absolute_url_for" --include="*.rs" | grep -v "pmocache\|pmoupnp"

# Tests du middleware
cargo test base_url
```

---

## Questions en suspens

1. **Fallback avec panic** : Si ni les headers ni le serveur ne sont disponibles, le middleware panic au démarrage avec un message clair (ex: "BaseUrl: configurer PMO_SERVER_URL ou démarrer le serveur avant les handlers HTTP").
   → **Décision utilisateur** : OK, panic avec message clair.

2. **Reverse proxy avec Authelia** : NPM ajoutera les headers `X-Forwarded-*`. Authelia gère l'authentification separately. Pas de vérification de header supplémentaire nécessaire pour le middleware BaseUrl.
   → **Décision** : Pas de vérification supplémentaire.

---

## Problème complémentaire : URLs de covers des media servers externes

### Contexte

Quand le control point accède à un media server externe sur le LAN (autre que pmomusic), les URLs d'articles (`album_art_uri`) retournées par ce media server externe contiennent des IPs locales du LAN externe (ex: `http://192.168.1.100:8080/covers/...`).

Ces URLs ne passent pas par notre système de caching et ne peuvent pas être rewritées par le middleware `BaseUrl` car elles sont :
1. Recues depuis le réseau UPnP (pas via HTTP)
2. Propagées directement dans les réponses REST/SSE sans transformation

### Solution proposée : Proxy de covers avec cache

Créer un nouveau endpoint HTTP qui agit comme un proxy transparent :
1. **Détection** : Si l'URL demandée est une URL LAN externe (pas une URL locale de pmomusic)
2. **Caching** : Utiliser `cache.add_from_url()` qui gère déjà la déduplication (pas de double-cache)
3. **Rewriting** : Retourner l'URL locale du cache (`/covers/image/{pk}`)

**Note importante** : `pmocache::add_from_url()` gère déjà :
- La vérification si l'URL est déjà en cache (ligne 673-683)
- Le calcul du pk basé sur le contenu (pas sur l'URL)
- La déduplication automatique pour les mêmes contenus

### Implémentation

**Nouvel endpoint dans `pmocovers/src/lib.rs` ou nouveau fichier `pmocovers/src/proxy.rs`** :

```rust
#[derive(Debug, Deserialize)]
struct CoverProxyParams {
    url: String,
}

#[derive(Debug, Serialize)]
struct CoverProxyResponse {
    cached_url: String,
    pk: String,
}

/// GET /covers/proxy?url=<encoded_url>
/// Proxy transparent qui :
/// 1. Détecte si l'URL est une URL LAN externe (pas déjà locale)
/// 2. Ajoute à cache via add_from_url (déduplication automatique)
/// 3. Retourne l'URL locale du cache
pub async fn cover_proxy_handler(
    Query(params): Query<CoverProxyParams>,
    State(cache): State<CoverCache>,
    Extension(base_url): Extension<pmoserver::BaseUrl>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    let external_url = &params.url;
    
    // Ignorer si déjà une URL locale (ne pas se cacher soi-même)
    if is_local_cover_url(external_url, &base_url) {
        return Err((StatusCode::BAD_REQUEST, "URL is already a local cover"));
    }
    
    // Vérifier si c'est une URL LAN à proxyfier
    if !should_proxy_url(external_url) {
        return Err((StatusCode::BAD_REQUEST, "URL is not a LAN URL requiring proxy"));
    }
    
    // Ajouter au cache (add_from_url gère la déduplication)
    let pk = cache.add_from_url(external_url, Some("external-covers"))
        .await
        .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
    
    // Retourner l'URL locale
    let local_url = base_url.url_for(&pmocache::covers_route_for(&pk, None));
    Ok(Json(CoverProxyResponse { cached_url: local_url, pk }))
}

/// Vérifie si l'URL est déjà une cover locale de NOTRE instance pmomusic
/// Note: Les covers d'autres instances pmomusic sur le LAN DEVRAIENT être proxyfiées
///       et mises en cache localement - c'est le comportement desired!
fn is_local_cover_url(url: &str, base_url: &pmoserver::BaseUrl) -> bool {
    // Only skip if it's OUR instance's base URL
    // Covers from other pmomusic instances on LAN should be proxied and cached
    url.starts_with(&base_url.0)
}

/// Vérifie si l'URL doit être proxyfiée (URL LAN externe)
fn should_proxy_url(url: &str) -> bool {
    if let Ok(parsed) = url::Url::parse(url) {
        if let Some(host) = parsed.host_str() {
            // Proxy uniquement les URLs LAN (pas les URLs publiques)
            if let Ok(ip) = host.parse::<std::net::IpAddr>() {
                return ip.is_private() || ip.is_loopback();
            }
            // aussi les .local
            return host.ends_with(".local") || host == "localhost";
        }
    }
    false
}
```

**Points importants** :
- Utiliser `add_from_url()` pour bénéficier de la déduplication automatique
- Vérifier `is_local_cover_url()` avec uniquement la comparaison de base_url pour éviter que notre instance ne se cache elle-même
- Les covers d'autres instances pmomusic sur le LAN DEVRAIENT être proxyfiées (comportement souhaité!)
- Le TTL sera celui par défaut du cache (configurable)

**Mise à jour des handlers REST** :

Dans `pmocontrol/src/pmoserver_ext.rs` et `pmocontrol/src/sse.rs`, transformer les `album_art_uri` LAN :

```rust
fn transform_external_cover_url(url: &str) -> String {
    if is_lan_url(url) {
        // Remplacer par l'URL du proxy
        let encoded = urlencoding::encode(url);
        return format!("/covers/proxy?url={}", encoded);
    }
    url.to_string()
}
```

**Appels dans les handlers** :

- `pmocontrol/src/pmoserver_ext.rs:2173` : `browse_container` → transformer `album_art_uri`
- `pmocontrol/src/pmoserver_ext.rs:2424` : autre endpoint → même transformation
- `pmocontrol/src/sse.rs:213` : `MetadataChanged` events → même transformation

### TTL

- Le TTL sera celui par défaut du cache `pmocovers`
- C'est configurable via `pmoconfig` si besoin

### Sécurité

- Limiter aux URLs LAN uniquement (`192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`, `localhost`)
- Vérifier que l'URL n'est pas déjà une cover locale de pmomusic (éviter le cacheception)
- Ajouter un rate limiting pour éviter le flood de téléchargement
- Timeout de téléchargement : 10 secondes max

### Résumé des fichiers à modifier

1. **Nouveau** : `pmocovers/src/proxy.rs` - Endpoint de proxy
2. **Modifier** : `pmocontrol/src/pmoserver_ext.rs` - Transformer les album_art_uri
3. **Modifier** : `pmocontrol/src/sse.rs` - Transformer les album_art_uri dans les événements

---

## Plan: Passer le SSE en mode Async

### Contexte

Le SSE de PMO Control est **déjà async** (fonctions `pub async fn`), mais le traitement des événements utilise des fonctions **synchrones** (`fn renderer_event_to_payload` → `transform_cover_url_sync`). Cela nécessite des workarounds (threads avec runtime tokio séparés).

### Problèmes actuels

1. **Nested runtime**: `std::thread::spawn` avec `tokio::runtime::Runtime::new()` dans chaque appel
2. **Performance dégradée**: Création d'un thread par URL de cover
3. **Code complexe**: Workarounds pour exécuter de l'async dans du sync

### Solution

Rendre le traitement des événements **entièrement async** :

1. **Modifier** `renderer_event_to_payload` → `async fn renderer_event_to_payload`
2. **Modifier** `transform_cover_url_sync` → `transform_cover_url` (async) avec `.await` direct
3. **Supprimer** le workaround `proxy_cover_url_sync` dans `pmocovers` (quand les .await fonctionnent)

### Avantages attendus

1. **Fluidité accrue**: Pas de thread par cover,真正的 async/await
2. **Meilleure réactivité**: Pas de blocking sur les événements SSE
3. **Code plus propre**: Plus de workarounds, plus de runtime imbriqué
4. **Meilleure scalabilité**: Plus de création de thread

### Fichiers à modifier

1. `pmocontrol/src/sse.rs`:
   - `renderer_event_to_payload` → `async fn`
   - `media_server_event_to_payload` → `async fn`
   - `all_events_sse`: utiliser les versions async

2. `pmocontrol/src/pmoserver_ext.rs`:
   - Utiliser `transform_cover_url` (async) avec `.await` au lieu de `transform_cover_url_sync`

3. `pmocontrol/src/control_point.rs` (si nécessaire):
   - Adapter les appels aux fonctions async