=============== pmoqobuz/Cargo.toml ============
[package]
name = "pmoqobuz"
version = "0.1.0"
edition = "2021"

[dependencies]
# HTTP client pour les requêtes à l'API Qobuz
reqwest = { version = "0.12", features = ["json", "cookies"] }

# Gestion asynchrone
tokio = { version = "1", features = ["full"] }

# Sérialisation/Désérialisation JSON
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Gestion des erreurs
anyhow = "1.0"
thiserror = "1.0"

# Hashing pour les clés de cache
sha1 = "0.10"
hex = "0.4"

# Cache en mémoire avec TTL
moka = { version = "0.12", features = ["future"] }

# Logging
tracing = "0.1"

# Gestion du temps
chrono = { version = "0.4", features = ["serde"] }

# Configuration
pmoconfig = { path = "../pmoconfig" }

# Intégration avec pmocovers pour le cache d'images (OBLIGATOIRE)
pmocovers = { path = "../pmocovers" }

# Intégration avec pmoaudiocache pour le cache audio (OBLIGATOIRE)
pmoaudiocache = { path = "../pmoaudiocache" }

# Intégration avec pmodidl pour l'export DIDL
pmodidl = { path = "../pmodidl" }

# Intégration avec pmoserver pour l'API HTTP
pmoserver = { path = "../pmoserver", optional = true }
axum = { version = "0.8", optional = true }

# Documentation OpenAPI
utoipa = { version = "5.3", optional = true }

# Common music source traits
pmosource = { path = "../pmosource" }

[features]
default = []
# Feature pour activer les extensions pmoserver
pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"]
# Feature pour activer le support serveur (cache registry)
server = ["pmosource/server"]
# Feature cache (deprecated - toujours actif maintenant)
cache = []

[dev-dependencies]
# Tests
tokio-test = "0.4"
mockito = "1.0"
# Pour les exemples
tracing-subscriber = "0.3"

# Specify that the with_cache example requires the cache feature
[[example]]
name = "with_cache"
required-features = ["cache"]
========= End of pmoqobuz/Cargo.toml ===========

=============== pmoqobuz/README.md ============
# pmoqobuz - Client Qobuz pour PMOMusic

Client Rust pour l'API Qobuz avec cache en mémoire, inspiré de l'implémentation Python d'upmpdcli.

## Fonctionnalités

- ✅ **Authentification** : Login avec username/password depuis la configuration
- ✅ **Catalogue** : Accès complet au catalogue Qobuz (albums, tracks, artistes, playlists)
- ✅ **Recherche** : Recherche dans le catalogue avec filtres
- ✅ **Favoris** : Accès aux albums, artistes, tracks et playlists favoris
- ✅ **Cache en mémoire** : Minimisation des requêtes API avec TTL configurable
- ✅ **Export DIDL** : Conversion automatique en format DIDL-Lite (UPnP/DLNA)
- ✅ **Integration pmocovers** : Cache automatique des images (feature `covers`)
- ✅ **Integration pmoaudiocache** : Cache audio haute résolution avec métadonnées (feature `cache`)
- ✅ **API HTTP** : Endpoints REST via pmoserver (feature `pmoserver`)

## Installation

Ajoutez la dépendance dans votre `Cargo.toml` :

```toml
[dependencies]
pmoqobuz = { path = "../pmoqobuz" }
```

## Configuration

Les credentials Qobuz doivent être configurés dans `.pmomusic.yml` :

```yaml
accounts:
  qobuz:
    username: "votre@email.com"
    password: "votre_mot_de_passe"
```

## Utilisation

### Exemple basique

```rust
use pmoqobuz::QobuzClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Connexion depuis la configuration
    let client = QobuzClient::from_config().await?;

    // Rechercher des albums
    let albums = client.search_albums("Miles Davis").await?;

    for album in albums.iter().take(5) {
        println!("{} - {}", album.artist.name, album.title);
    }

    Ok(())
}
```

### Export DIDL

```rust
use pmoqobuz::{QobuzClient, ToDIDL};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = QobuzClient::from_config().await?;

    let album = client.get_album("album_id").await?;
    let didl_container = album.to_didl_container("parent_id")?;

    let tracks = client.get_album_tracks(&album.id).await?;
    for track in tracks {
        let didl_item = track.to_didl_item(&didl_container.id)?;
        println!("{}", didl_item.title);
    }

    Ok(())
}
```

### Favoris

```rust
use pmoqobuz::QobuzClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = QobuzClient::from_config().await?;

    // Albums favoris
    let albums = client.get_favorite_albums().await?;
    println!("{} albums favoris", albums.len());

    // Artistes favoris
    let artists = client.get_favorite_artists().await?;

    // Tracks favorites
    let tracks = client.get_favorite_tracks().await?;

    // Playlists de l'utilisateur
    let playlists = client.get_user_playlists().await?;

    Ok(())
}
```

## Formats audio

Qobuz propose plusieurs formats :

| Format | Description | Format ID |
|--------|-------------|-----------|
| `Mp3_320` | MP3 320 kbps | 5 |
| `Flac_Lossless` | FLAC 16 bit / 44.1 kHz | 6 (défaut) |
| `Flac_HiRes_96` | FLAC 24 bit / jusqu'à 96 kHz | 7 |
| `Flac_HiRes_192` | FLAC 24 bit / jusqu'à 192 kHz | 27 |

```rust
use pmoqobuz::{QobuzClient, AudioFormat};

let mut client = QobuzClient::from_config().await?;
client.set_format(AudioFormat::Flac_HiRes_96);
```

## Cache

Le cache en mémoire utilise `moka` avec TTL :

- **Albums** : 1 heure
- **Tracks** : 1 heure
- **Artistes** : 1 heure
- **Playlists** : 30 minutes
- **Recherches** : 15 minutes
- **URLs de streaming** : 5 minutes

```rust
// Statistiques du cache
let stats = client.cache().stats().await;
println!("Albums: {}", stats.albums_count);
println!("Total: {}", stats.total_count());

// Vider le cache
client.cache().clear_all().await;
```

## Cache avancé (feature `cache`)

La feature `cache` active le support complet de pmocovers et pmoaudiocache pour télécharger et cacher localement les images et l'audio haute résolution :

```rust
use pmoqobuz::{QobuzSource, QobuzClient};
use pmocovers::Cache as CoverCache;
use pmoaudiocache::AudioCache;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize caches
    let cover_cache = Arc::new(CoverCache::new("./cache/covers", 500)?);
    let audio_cache = Arc::new(AudioCache::new("./cache/audio", 100)?);

    // Create source with caching
    let client = QobuzClient::from_config().await?;
    let source = QobuzSource::new_with_cache(
        client,
        "http://localhost:8080",
        Some(cover_cache),
        Some(audio_cache),
    );

    // Add tracks with automatic caching
    let tracks = source.client().get_favorite_tracks().await?;
    for track in tracks.iter().take(5) {
        let track_id = source.add_track(track).await?;
        // Audio and cover are now cached locally
        let uri = source.resolve_uri(&track_id).await?;
        println!("Cached: {}", uri);
    }

    Ok(())
}
```

**Métadonnées enrichies préservées** :
- Titre, artiste, album
- Numéro de piste et de disque
- Année de sortie
- Genre(s) et label
- Qualité audio (sample rate, bit depth, channels)
- Durée

## Exemples

Exécutez les exemples :

```bash
# Exemple basique
cargo run --example basic_usage

# Exemple avec cache (nécessite la feature cache)
cargo run --example with_cache --features cache
```

## Architecture

```
pmoqobuz/
├── src/
│   ├── lib.rs           # Module principal
│   ├── client.rs        # Client haut-niveau
│   ├── models.rs        # Structures de données
│   ├── api/
│   │   ├── mod.rs       # API client bas-niveau
│   │   ├── auth.rs      # Authentification
│   │   ├── catalog.rs   # Accès catalogue
│   │   └── user.rs      # API utilisateur
│   ├── cache.rs         # Cache en mémoire
│   ├── didl.rs          # Export DIDL-Lite
│   └── error.rs         # Gestion des erreurs
└── examples/
    └── basic_usage.rs   # Exemple d'utilisation
```

## Tests

```bash
cargo test -p pmoqobuz
```

## Documentation

Générez la documentation :

```bash
cargo doc -p pmoqobuz --open
```

## Features

- `covers` : Active pmocovers pour le cache d'images
- `cache` : Active pmocovers + pmoaudiocache pour le cache complet (images + audio)
- `pmoserver` : Active les endpoints REST via pmoserver

## Dépendances principales

- `reqwest` : Client HTTP
- `tokio` : Runtime asynchrone
- `serde` / `serde_json` : Sérialisation JSON
- `moka` : Cache en mémoire avec TTL
- `pmodidl` : Export DIDL-Lite
- `pmoconfig` : Configuration
- `pmocovers` : Cache d'images (optionnel)
- `pmoaudiocache` : Cache audio (optionnel)

## Licence

Ce code fait partie du projet PMOMusic.

## Références

- [API Qobuz Documentation](https://github.com/Qobuz/api-documentation)
- [upmpdcli Qobuz Plugin](https://www.lesbonscomptes.com/upmpdcli/)
========= End of pmoqobuz/README.md ===========

=============== pmoqobuz/examples/server_with_covers.rs ============
//! Exemple d'utilisation de pmoqobuz avec pmoserver et pmocovers
//!
//! Cet exemple montre comment :
//! - Créer un serveur HTTP avec pmoserver
//! - Initialiser le cache d'images avec pmocovers
//! - Initialiser le client Qobuz avec intégration pmocovers
//! - Les images d'albums sont automatiquement mises en cache
//!
//! Pour tester :
//! ```bash
//! cargo run --example server_with_covers --features "pmoserver,covers"
//! ```
//!
//! Endpoints disponibles :
//! - GET /qobuz/search?q=query&type=albums - Recherche d'albums (images auto-cachées)
//! - GET /qobuz/albums/{id} - Détails d'un album (image auto-cachée)
//! - GET /qobuz/favorites/albums - Albums favoris (images auto-cachées)
//! - GET /covers/images/{pk} - Image originale mise en cache
//! - GET /covers/images/{pk}/{size} - Variante redimensionnée
//! - GET /api/covers - API REST du cache d'images
//! - GET /swagger-ui - Documentation interactive

#[cfg(all(feature = "pmoserver", feature = "covers"))]
use pmocovers::CoverCacheExt;

#[cfg(all(feature = "pmoserver", feature = "covers"))]
use pmoqobuz::QobuzServerExt;

#[cfg(all(feature = "pmoserver", feature = "covers"))]
use pmoserver::ServerBuilder;

#[cfg(all(feature = "pmoserver", feature = "covers"))]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialiser le logging
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    println!("=== PMOQobuz + PMOCovers - Serveur HTTP avec cache d'images ===\n");

    // Créer le serveur depuis la configuration
    let mut server = ServerBuilder::new_configured().build();

    println!("1. Initialisation du cache d'images (pmocovers)...");
    // Initialiser le cache d'images avec la configuration
    let cache = server.init_cover_cache_configured().await?;
    println!("   ✓ Cache d'images initialisé: {}", cache.cache_dir());

    println!("\n2. Initialisation du client Qobuz avec intégration pmocovers...");
    // Initialiser le client Qobuz avec intégration pmocovers
    // Les images d'albums seront automatiquement ajoutées au cache
    let client = server
        .init_qobuz_client_configured_with_covers(cache.clone())
        .await?;

    if let Some(auth_info) = client.auth_info() {
        println!("   ✓ Client Qobuz connecté !");
        println!("     User ID: {}", auth_info.user_id);
        if let Some(label) = &auth_info.subscription_label {
            println!("     Abonnement: {}", label);
        }
    }

    println!("\n3. Démarrage du serveur HTTP...");
    server.start().await;

    println!("\n✓ Serveur démarré avec succès !\n");
    println!("Endpoints disponibles :");
    println!("  • Qobuz API:");
    println!("    - GET /qobuz/search?q=query&type=albums");
    println!("    - GET /qobuz/albums/{{id}}");
    println!("    - GET /qobuz/albums/{{id}}/tracks");
    println!("    - GET /qobuz/favorites/albums");
    println!("    - GET /qobuz/favorites/artists");
    println!("    - GET /qobuz/cache/stats");
    println!("  • Images (auto-cachées depuis Qobuz):");
    println!("    - GET /covers/images/{{pk}}");
    println!("    - GET /covers/images/{{pk}}/{{size}}");
    println!("  • API REST du cache:");
    println!("    - GET /api/covers");
    println!("    - POST /api/covers");
    println!("    - DELETE /api/covers/{{pk}}");
    println!("  • Documentation:");
    println!("    - GET /swagger-ui");
    println!("\nExemple de requête :");
    println!("  curl 'http://localhost:3000/qobuz/search?q=Miles%20Davis&type=albums' | jq '.[0].image_cached'");
    println!("  # Retourne: \"/covers/images/{{pk}}\"");
    println!("\nAppuyez sur Ctrl+C pour arrêter le serveur...\n");

    // Attendre indéfiniment
    server.wait().await;

    Ok(())
}

#[cfg(not(all(feature = "pmoserver", feature = "covers")))]
fn main() {
    eprintln!("Cet exemple nécessite les features 'pmoserver' et 'covers'");
    eprintln!("Exécutez: cargo run --example server_with_covers --features \"pmoserver,covers\"");
    std::process::exit(1);
}
========= End of pmoqobuz/examples/server_with_covers.rs ===========

=============== pmoqobuz/examples/basic_usage.rs ============
//! Exemple d'utilisation basique de pmoqobuz
//!
//! Cet exemple montre comment :
//! - Se connecter à Qobuz avec les credentials de la configuration
//! - Rechercher des albums
//! - Récupérer les détails d'un album
//! - Exporter un album en format DIDL-Lite

use pmoqobuz::{QobuzClient, ToDIDL};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialiser le logging
    tracing_subscriber::fmt::init();

    println!("=== PMOQobuz - Exemple d'utilisation basique ===\n");

    // Créer un client depuis la configuration
    println!("Connexion à Qobuz...");
    let client = QobuzClient::from_config().await?;

    if let Some(auth_info) = client.auth_info() {
        println!("✓ Connecté avec succès !");
        println!("  User ID: {}", auth_info.user_id);
        if let Some(label) = &auth_info.subscription_label {
            println!("  Abonnement: {}", label);
        }
    }

    println!("\n--- Recherche d'albums ---");
    let query = "Miles Davis";
    println!("Recherche: '{}'...", query);

    let albums = client.search_albums(query).await?;
    println!("✓ {} album(s) trouvé(s)\n", albums.len());

    // Afficher les 5 premiers albums
    for (i, album) in albums.iter().take(5).enumerate() {
        println!("  {}. {} - {}", i + 1, album.artist.name, album.title);
        if let Some(date) = &album.release_date {
            println!("     Date: {}", date);
        }
        if let Some(count) = album.tracks_count {
            println!("     Pistes: {}", count);
        }
    }

    // Récupérer les détails du premier album
    if let Some(first_album) = albums.first() {
        println!("\n--- Détails de l'album ---");
        println!("Album: {} - {}", first_album.artist.name, first_album.title);

        // Récupérer les tracks
        let tracks = client.get_album_tracks(&first_album.id).await?;
        println!("Tracks ({}):", tracks.len());

        for track in tracks.iter().take(3) {
            println!(
                "  {}. {} - {} ({}:{})",
                track.track_number,
                track
                    .display_artist()
                    .map(|a| a.name.as_str())
                    .unwrap_or("Unknown"),
                track.title,
                track.duration / 60,
                track.duration % 60
            );
        }

        if tracks.len() > 3 {
            println!("  ... et {} autres pistes", tracks.len() - 3);
        }

        // Export DIDL
        println!("\n--- Export DIDL-Lite ---");
        let didl_container = first_album.to_didl_container("0")?;
        println!("Container ID: {}", didl_container.id);
        println!("Title: {}", didl_container.title);
        println!("Class: {}", didl_container.class);

        if let Some(first_track) = tracks.first() {
            let didl_item = first_track.to_didl_item(&didl_container.id)?;
            println!("\nPremière track en DIDL:");
            println!("  Item ID: {}", didl_item.id);
            println!("  Title: {}", didl_item.title);
            if let Some(artist) = &didl_item.artist {
                println!("  Artist: {}", artist);
            }
        }
    }

    // Afficher les statistiques du cache
    println!("\n--- Statistiques du cache ---");
    let stats = client.cache().stats().await;
    println!("Albums en cache: {}", stats.albums_count);
    println!("Tracks en cache: {}", stats.tracks_count);
    println!("Artistes en cache: {}", stats.artists_count);
    println!("Total: {} entrées", stats.total_count());

    // Favoris
    println!("\n--- Albums favoris ---");
    match client.get_favorite_albums().await {
        Ok(favorites) => {
            println!("✓ {} album(s) favori(s)", favorites.len());
            for (i, album) in favorites.iter().take(5).enumerate() {
                println!("  {}. {} - {}", i + 1, album.artist.name, album.title);
            }
            if favorites.len() > 5 {
                println!("  ... et {} autres", favorites.len() - 5);
            }
        }
        Err(e) => {
            println!("⚠ Impossible de récupérer les favoris: {}", e);
        }
    }

    println!("\n✓ Exemple terminé avec succès !");

    Ok(())
}
========= End of pmoqobuz/examples/basic_usage.rs ===========

=============== pmoqobuz/examples/with_cache.rs ============
//! Example demonstrating Qobuz with cache support
//!
//! This example shows how to use the QobuzSource with pmocovers
//! and pmoaudiocache to cache both cover images and audio tracks.
//!
//! Run with:
//! ```bash
//! cargo run --example with_cache --features cache
//! ```

use pmoaudiocache::AudioCache;
use pmocovers::Cache as CoverCache;
use pmoqobuz::{QobuzClient, QobuzSource};
use pmosource::MusicSource;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize tracing
    tracing_subscriber::fmt::init();

    println!("🎵 Qobuz with Cache Support");
    println!("============================\n");

    // Create the Qobuz client using configuration
    println!("📡 Connecting to Qobuz...");
    let client = QobuzClient::from_config().await?;
    println!("✅ Connected!\n");

    // Initialize caches
    println!("💾 Initializing caches...");
    let cover_cache = Arc::new(CoverCache::new("./cache/qobuz-covers", 500)?);
    let audio_cache = Arc::new(AudioCache::new("./cache/qobuz-audio", 100)?);
    println!("✅ Caches initialized!\n");

    // Create the source with caching enabled
    let source = QobuzSource::new_with_cache(
        client,
        "http://localhost:8080",
        Some(cover_cache.clone()),
        Some(audio_cache.clone()),
    );

    println!("📻 Source: {}", source.name());
    println!("🆔 ID: {}", source.id());
    println!("📝 Supports FIFO: {}\n", source.supports_fifo());

    // Get user's favorite tracks
    println!("🎧 Fetching your favorite tracks...");
    let favorite_tracks = source.client().get_favorite_tracks().await?;

    if favorite_tracks.is_empty() {
        println!("⚠️  No favorite tracks found. Add some favorites on Qobuz first!");
        println!("\n💡 Tip: You can also search for tracks:");

        // Example: Search for tracks
        println!("\n🔍 Searching for 'Miles Davis'...");
        let search_results = source.client().search("Miles Davis", None).await?;

        if !search_results.tracks.is_empty() {
            println!("\n📋 Found {} tracks:", search_results.tracks.len());
            for (i, track) in search_results.tracks.iter().enumerate().take(3) {
                println!(
                    "   {}. {} - {}",
                    i + 1,
                    track
                        .performer
                        .as_ref()
                        .map(|p| p.name.as_str())
                        .unwrap_or("Unknown"),
                    track.title
                );

                // Demonstrate adding a track with caching
                if i == 0 {
                    println!("\n➕ Adding first track to cache...");
                    let track_id = source.add_track(track).await?;
                    println!("✅ Track added with ID: {}", track_id);
                    println!("   - Cover image caching started");
                    println!("   - Audio caching started (high-quality FLAC)");

                    // Show resolved URI (will use cached version if available)
                    if let Ok(uri) = source.resolve_uri(&track_id).await {
                        println!("   - Stream URI: {}", uri);
                    }
                }
            }
        }
    } else {
        println!("✅ Found {} favorite tracks!\n", favorite_tracks.len());

        // Add first 3 favorite tracks with caching
        for (i, track) in favorite_tracks.iter().enumerate().take(3) {
            println!(
                "{}. {} - {}",
                i + 1,
                track
                    .performer
                    .as_ref()
                    .map(|p| p.name.as_str())
                    .unwrap_or("Unknown"),
                track.title
            );

            if let Some(album) = &track.album {
                println!("   Album: {}", album.title);
                if let Some(label) = &album.label {
                    println!("   Label: {}", label);
                }
                if let Some(sample_rate) = album.maximum_sampling_rate {
                    println!("   Max Sample Rate: {} kHz", sample_rate / 1000.0);
                }
                if let Some(bit_depth) = album.maximum_bit_depth {
                    println!("   Max Bit Depth: {} bit", bit_depth);
                }
            }

            println!("\n   ➕ Adding to cache...");
            match source.add_track(track).await {
                Ok(track_id) => {
                    println!("   ✅ Track cached successfully!");

                    // Show resolved URI
                    if let Ok(uri) = source.resolve_uri(&track_id).await {
                        println!("   📍 Stream URI: {}", uri);
                    }
                }
                Err(e) => {
                    println!("   ⚠️  Failed to cache track: {}", e);
                }
            }
            println!();
        }
    }

    // Browse favorite albums
    println!("\n📚 Browsing your favorite albums...");
    let favorite_albums = source.client().get_favorite_albums().await?;

    if !favorite_albums.is_empty() {
        println!("✅ Found {} favorite albums!\n", favorite_albums.len());

        for (i, album) in favorite_albums.iter().enumerate().take(3) {
            println!("{}. {} - {}", i + 1, album.artist.name, album.title);
            if let Some(release_date) = &album.release_date {
                println!("   Released: {}", release_date);
            }
            if let Some(tracks_count) = album.tracks_count {
                println!("   Tracks: {}", tracks_count);
            }
            if !album.genres.is_empty() {
                println!("   Genres: {}", album.genres.join(", "));
            }
        }
    } else {
        println!("⚠️  No favorite albums found.");
    }

    println!("\n✨ Example complete!");
    println!("\n💡 Tips:");
    println!("   - Run the example again to see faster loading from cache");
    println!("   - Check ./cache/qobuz-covers/ for cached cover images (WebP)");
    println!("   - Check ./cache/qobuz-audio/ for cached Hi-Res FLAC files");
    println!("   - Qobuz provides rich metadata (label, ISRC, sample rate, bit depth)");
    println!("   - Cached audio retains original quality (up to 24bit/192kHz)");

    Ok(())
}
========= End of pmoqobuz/examples/with_cache.rs ===========

=============== pmoqobuz/examples/show_source_image.rs ============
//! Example showing how to access and save the Qobuz source image
//!
//! This example demonstrates:
//! - Getting source information via the MusicSource trait
//! - Accessing the embedded WebP image
//! - Optionally saving it to a file

use pmoqobuz::{QobuzClient, QobuzSource};
use pmosource::MusicSource;
use std::fs;
use std::io::Write;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create the client and source
    let client = QobuzClient::from_config().await?;
    let source = QobuzSource::new(client, "http://localhost:8080");

    // Display source information
    println!("Music Source Information");
    println!("========================");
    println!("Name: {}", source.name());
    println!("ID: {}", source.id());
    println!("Image MIME type: {}", source.default_image_mime_type());

    // Get the embedded image
    let image_data = source.default_image();
    println!("Embedded image size: {} bytes", image_data.len());

    // Verify WebP format
    if image_data.len() >= 12 {
        let is_webp = &image_data[0..4] == b"RIFF" && &image_data[8..12] == b"WEBP";
        println!("Valid WebP format: {}", is_webp);
    }

    // Optional: save to file
    if std::env::args().any(|arg| arg == "--save") {
        let filename = format!("{}_default.webp", source.id());
        let mut file = fs::File::create(&filename)?;
        file.write_all(image_data)?;
        println!("\nImage saved to: {}", filename);
        println!("You can view it with: open {}", filename);
    } else {
        println!("\nTo save the image to disk, run with: --save");
    }

    Ok(())
}
========= End of pmoqobuz/examples/show_source_image.rs ===========

=============== pmoqobuz/src/cache.rs ============
//! Système de cache en mémoire pour les données Qobuz
//!
//! Ce module fournit un cache en mémoire avec TTL pour minimiser les requêtes à l'API Qobuz.

use crate::models::{Album, Artist, Playlist, SearchResult, StreamInfo, Track};
use moka::future::Cache as MokaCache;
use std::sync::Arc;
use std::time::Duration;

/// Cache principal pour les données Qobuz
#[derive(Clone)]
pub struct QobuzCache {
    /// Cache des albums (TTL: 1 heure)
    albums: Arc<MokaCache<String, Album>>,
    /// Cache des tracks (TTL: 1 heure)
    tracks: Arc<MokaCache<String, Track>>,
    /// Cache des artistes (TTL: 1 heure)
    artists: Arc<MokaCache<String, Artist>>,
    /// Cache des playlists (TTL: 30 minutes)
    playlists: Arc<MokaCache<String, Playlist>>,
    /// Cache des résultats de recherche (TTL: 15 minutes)
    searches: Arc<MokaCache<String, SearchResult>>,
    /// Cache des URLs de streaming (TTL: 5 minutes)
    stream_urls: Arc<MokaCache<String, StreamInfo>>,
}

impl QobuzCache {
    /// Crée un nouveau cache avec les paramètres par défaut
    pub fn new() -> Self {
        Self::with_capacity(1000)
    }

    /// Crée un nouveau cache avec une capacité spécifique
    pub fn with_capacity(max_capacity: u64) -> Self {
        Self {
            albums: Arc::new(
                MokaCache::builder()
                    .max_capacity(max_capacity)
                    .time_to_live(Duration::from_secs(3600)) // 1 heure
                    .build(),
            ),
            tracks: Arc::new(
                MokaCache::builder()
                    .max_capacity(max_capacity * 2)
                    .time_to_live(Duration::from_secs(3600)) // 1 heure
                    .build(),
            ),
            artists: Arc::new(
                MokaCache::builder()
                    .max_capacity(max_capacity / 2)
                    .time_to_live(Duration::from_secs(3600)) // 1 heure
                    .build(),
            ),
            playlists: Arc::new(
                MokaCache::builder()
                    .max_capacity(max_capacity / 4)
                    .time_to_live(Duration::from_secs(1800)) // 30 minutes
                    .build(),
            ),
            searches: Arc::new(
                MokaCache::builder()
                    .max_capacity(max_capacity / 2)
                    .time_to_live(Duration::from_secs(900)) // 15 minutes
                    .build(),
            ),
            stream_urls: Arc::new(
                MokaCache::builder()
                    .max_capacity(max_capacity / 4)
                    .time_to_live(Duration::from_secs(300)) // 5 minutes
                    .build(),
            ),
        }
    }

    // ============ Albums ============

    /// Récupère un album depuis le cache
    pub async fn get_album(&self, id: &str) -> Option<Album> {
        self.albums.get(id).await
    }

    /// Ajoute un album au cache
    pub async fn put_album(&self, id: String, album: Album) {
        self.albums.insert(id, album).await;
    }

    /// Invalide un album du cache
    pub async fn invalidate_album(&self, id: &str) {
        self.albums.invalidate(id).await;
    }

    // ============ Tracks ============

    /// Récupère une track depuis le cache
    pub async fn get_track(&self, id: &str) -> Option<Track> {
        self.tracks.get(id).await
    }

    /// Ajoute une track au cache
    pub async fn put_track(&self, id: String, track: Track) {
        self.tracks.insert(id, track).await;
    }

    /// Invalide une track du cache
    pub async fn invalidate_track(&self, id: &str) {
        self.tracks.invalidate(id).await;
    }

    // ============ Artists ============

    /// Récupère un artiste depuis le cache
    pub async fn get_artist(&self, id: &str) -> Option<Artist> {
        self.artists.get(id).await
    }

    /// Ajoute un artiste au cache
    pub async fn put_artist(&self, id: String, artist: Artist) {
        self.artists.insert(id, artist).await;
    }

    /// Invalide un artiste du cache
    pub async fn invalidate_artist(&self, id: &str) {
        self.artists.invalidate(id).await;
    }

    // ============ Playlists ============

    /// Récupère une playlist depuis le cache
    pub async fn get_playlist(&self, id: &str) -> Option<Playlist> {
        self.playlists.get(id).await
    }

    /// Ajoute une playlist au cache
    pub async fn put_playlist(&self, id: String, playlist: Playlist) {
        self.playlists.insert(id, playlist).await;
    }

    /// Invalide une playlist du cache
    pub async fn invalidate_playlist(&self, id: &str) {
        self.playlists.invalidate(id).await;
    }

    // ============ Recherches ============

    /// Récupère un résultat de recherche depuis le cache
    pub async fn get_search(&self, query: &str) -> Option<SearchResult> {
        self.searches.get(query).await
    }

    /// Ajoute un résultat de recherche au cache
    pub async fn put_search(&self, query: String, result: SearchResult) {
        self.searches.insert(query, result).await;
    }

    /// Invalide un résultat de recherche du cache
    pub async fn invalidate_search(&self, query: &str) {
        self.searches.invalidate(query).await;
    }

    // ============ URLs de streaming ============

    /// Récupère une URL de streaming depuis le cache
    pub async fn get_stream_url(&self, track_id: &str) -> Option<StreamInfo> {
        self.stream_urls.get(track_id).await
    }

    /// Ajoute une URL de streaming au cache
    pub async fn put_stream_url(&self, track_id: String, info: StreamInfo) {
        self.stream_urls.insert(track_id, info).await;
    }

    /// Invalide une URL de streaming du cache
    pub async fn invalidate_stream_url(&self, track_id: &str) {
        self.stream_urls.invalidate(track_id).await;
    }

    // ============ Maintenance ============

    /// Vide tous les caches
    pub async fn clear_all(&self) {
        self.albums.invalidate_all();
        self.tracks.invalidate_all();
        self.artists.invalidate_all();
        self.playlists.invalidate_all();
        self.searches.invalidate_all();
        self.stream_urls.invalidate_all();
    }

    /// Retourne des statistiques sur le cache
    pub async fn stats(&self) -> CacheStats {
        CacheStats {
            albums_count: self.albums.entry_count(),
            tracks_count: self.tracks.entry_count(),
            artists_count: self.artists.entry_count(),
            playlists_count: self.playlists.entry_count(),
            searches_count: self.searches.entry_count(),
            stream_urls_count: self.stream_urls.entry_count(),
        }
    }
}

impl Default for QobuzCache {
    fn default() -> Self {
        Self::new()
    }
}

/// Statistiques du cache
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CacheStats {
    /// Nombre d'albums en cache
    pub albums_count: u64,
    /// Nombre de tracks en cache
    pub tracks_count: u64,
    /// Nombre d'artistes en cache
    pub artists_count: u64,
    /// Nombre de playlists en cache
    pub playlists_count: u64,
    /// Nombre de recherches en cache
    pub searches_count: u64,
    /// Nombre d'URLs de streaming en cache
    pub stream_urls_count: u64,
}

impl CacheStats {
    /// Retourne le nombre total d'entrées en cache
    pub fn total_count(&self) -> u64 {
        self.albums_count
            + self.tracks_count
            + self.artists_count
            + self.playlists_count
            + self.searches_count
            + self.stream_urls_count
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::Artist;

    #[tokio::test]
    async fn test_cache_basic_operations() {
        let cache = QobuzCache::new();

        let artist = Artist::new("123", "Test Artist");

        // Test insertion
        cache.put_artist("123".to_string(), artist.clone()).await;

        // Test récupération
        let retrieved = cache.get_artist("123").await;
        assert!(retrieved.is_some());
        assert_eq!(retrieved.unwrap().name, "Test Artist");

        // Test invalidation
        cache.invalidate_artist("123").await;
        let after_invalidation = cache.get_artist("123").await;
        assert!(after_invalidation.is_none());
    }

    #[tokio::test]
    async fn test_cache_stats() {
        let cache = QobuzCache::new();

        let artist = Artist::new("123", "Test Artist");
        cache.put_artist("123".to_string(), artist).await;

        let stats = cache.stats().await;
        assert_eq!(stats.artists_count, 1);
        assert_eq!(stats.albums_count, 0);
    }

    #[tokio::test]
    async fn test_cache_clear_all() {
        let cache = QobuzCache::new();

        let artist = Artist::new("123", "Test Artist");
        cache.put_artist("123".to_string(), artist).await;

        cache.clear_all().await;

        let stats = cache.stats().await;
        assert_eq!(stats.total_count(), 0);
    }
}
========= End of pmoqobuz/src/cache.rs ===========

=============== pmoqobuz/src/client.rs ============
//! Client principal pour interagir avec l'API Qobuz
//!
//! Ce module fournit un client haut-niveau avec authentification et cache intégré.

use crate::api::auth::AuthInfo;
use crate::api::QobuzApi;
use crate::cache::QobuzCache;
use crate::error::{QobuzError, Result};
use crate::models::*;
use pmoconfig::Config;
use std::sync::Arc;
use tracing::{debug, info};

/// App ID Qobuz par défaut (peut être overridé)
const DEFAULT_APP_ID: &str = "1401488693436528";

/// Client Qobuz haut-niveau avec cache
pub struct QobuzClient {
    /// API bas-niveau
    api: QobuzApi,
    /// Cache en mémoire
    cache: Arc<QobuzCache>,
    /// Informations d'authentification
    auth_info: Option<AuthInfo>,
}

impl QobuzClient {
    /// Crée un nouveau client et authentifie avec les credentials fournis
    ///
    /// # Arguments
    ///
    /// * `username` - Email ou nom d'utilisateur Qobuz
    /// * `password` - Mot de passe
    ///
    /// # Exemple
    ///
    /// ```rust,no_run
    /// use pmoqobuz::QobuzClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> anyhow::Result<()> {
    ///     let client = QobuzClient::new("user@example.com", "password").await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn new(username: &str, password: &str) -> Result<Self> {
        Self::with_app_id(DEFAULT_APP_ID, username, password).await
    }

    /// Crée un nouveau client avec un App ID personnalisé
    pub async fn with_app_id(app_id: &str, username: &str, password: &str) -> Result<Self> {
        info!("Creating Qobuz client with app ID: {}", app_id);

        let mut api = QobuzApi::new(app_id)?;
        let auth_info = api.login(username, password).await?;

        Ok(Self {
            api,
            cache: Arc::new(QobuzCache::new()),
            auth_info: Some(auth_info),
        })
    }

    /// Crée un client en utilisant la configuration de pmoconfig
    ///
    /// # Exemple
    ///
    /// ```rust,no_run
    /// use pmoqobuz::QobuzClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> anyhow::Result<()> {
    ///     let client = QobuzClient::from_config().await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn from_config() -> Result<Self> {
        let config = pmoconfig::get_config();
        Self::from_config_obj(config.as_ref()).await
    }

    /// Crée un client depuis un objet Config spécifique
    pub async fn from_config_obj(config: &Config) -> Result<Self> {
        let (username, password) = config.get_qobuz_credentials()?;
        Self::new(&username, &password).await
    }

    /// Définit le format audio par défaut
    pub fn set_format(&mut self, format: AudioFormat) {
        self.api.set_format(format);
    }

    /// Retourne le format audio configuré
    pub fn format(&self) -> AudioFormat {
        self.api.format()
    }

    /// Retourne les informations d'authentification
    pub fn auth_info(&self) -> Option<&AuthInfo> {
        self.auth_info.as_ref()
    }

    /// Retourne une référence au cache
    pub fn cache(&self) -> Arc<QobuzCache> {
        self.cache.clone()
    }

    // ============ Albums ============

    /// Récupère un album par son ID
    pub async fn get_album(&self, album_id: &str) -> Result<Album> {
        // Vérifier le cache d'abord
        if let Some(album) = self.cache.get_album(album_id).await {
            debug!("Album {} found in cache", album_id);
            return Ok(album);
        }

        // Sinon, récupérer depuis l'API
        let album = self.api.get_album(album_id).await?;

        // Mettre en cache
        self.cache
            .put_album(album_id.to_string(), album.clone())
            .await;

        Ok(album)
    }

    /// Récupère les tracks d'un album
    pub async fn get_album_tracks(&self, album_id: &str) -> Result<Vec<Track>> {
        let tracks = self.api.get_album_tracks(album_id).await?;

        // Mettre les tracks en cache
        for track in &tracks {
            self.cache.put_track(track.id.clone(), track.clone()).await;
        }

        Ok(tracks)
    }

    // ============ Tracks ============

    /// Récupère une track par son ID
    pub async fn get_track(&self, track_id: &str) -> Result<Track> {
        if let Some(track) = self.cache.get_track(track_id).await {
            debug!("Track {} found in cache", track_id);
            return Ok(track);
        }

        let track = self.api.get_track(track_id).await?;
        self.cache
            .put_track(track_id.to_string(), track.clone())
            .await;

        Ok(track)
    }

    /// Récupère l'URL de streaming d'une track
    pub async fn get_stream_url(&self, track_id: &str) -> Result<String> {
        // Vérifier le cache d'abord
        if let Some(info) = self.cache.get_stream_url(track_id).await {
            if info.expires_at > chrono::Utc::now() {
                debug!("Stream URL for track {} found in cache", track_id);
                return Ok(info.url);
            }
        }

        // Sinon, récupérer depuis l'API
        let info = self.api.get_file_url(track_id).await?;
        let url = info.url.clone();

        // Mettre en cache
        self.cache.put_stream_url(track_id.to_string(), info).await;

        Ok(url)
    }

    // ============ Artists ============

    /// Récupère un artiste par son ID
    pub async fn get_artist(&self, artist_id: &str) -> Result<Artist> {
        if let Some(artist) = self.cache.get_artist(artist_id).await {
            debug!("Artist {} found in cache", artist_id);
            return Ok(artist);
        }

        // Pour récupérer un artiste, on doit passer par get_artist_albums
        let albums = self.api.get_artist_albums(artist_id).await?;

        if let Some(first_album) = albums.first() {
            let artist = first_album.artist.clone();
            self.cache
                .put_artist(artist_id.to_string(), artist.clone())
                .await;
            Ok(artist)
        } else {
            Err(QobuzError::NotFound(format!(
                "Artist {} not found",
                artist_id
            )))
        }
    }

    /// Récupère les albums d'un artiste
    pub async fn get_artist_albums(&self, artist_id: &str) -> Result<Vec<Album>> {
        self.api.get_artist_albums(artist_id).await
    }

    /// Récupère les artistes similaires
    pub async fn get_similar_artists(&self, artist_id: &str) -> Result<Vec<Artist>> {
        self.api.get_similar_artists(artist_id).await
    }

    // ============ Playlists ============

    /// Récupère une playlist par son ID
    pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
        if let Some(playlist) = self.cache.get_playlist(playlist_id).await {
            debug!("Playlist {} found in cache", playlist_id);
            return Ok(playlist);
        }

        let playlist = self.api.get_playlist(playlist_id).await?;
        self.cache
            .put_playlist(playlist_id.to_string(), playlist.clone())
            .await;

        Ok(playlist)
    }

    /// Récupère les tracks d'une playlist
    pub async fn get_playlist_tracks(&self, playlist_id: &str) -> Result<Vec<Track>> {
        self.api.get_playlist_tracks(playlist_id).await
    }

    // ============ Catalogue ============

    /// Récupère la liste des genres
    pub async fn get_genres(&self) -> Result<Vec<Genre>> {
        self.api.get_genres().await
    }

    /// Récupère les albums featured (nouveautés, éditeur, etc.)
    pub async fn get_featured_albums(
        &self,
        genre_id: Option<&str>,
        type_: &str,
    ) -> Result<Vec<Album>> {
        self.api.get_featured_albums(genre_id, type_).await
    }

    /// Récupère les playlists featured
    pub async fn get_featured_playlists(
        &self,
        genre_id: Option<&str>,
        tags: Option<&str>,
    ) -> Result<Vec<Playlist>> {
        self.api.get_featured_playlists(genre_id, tags).await
    }

    // ============ Recherche ============

    /// Recherche dans le catalogue Qobuz
    ///
    /// # Arguments
    ///
    /// * `query` - Termes de recherche
    /// * `type_` - Type de recherche : None (tous), Some("albums"), Some("artists"), Some("tracks"), Some("playlists")
    pub async fn search(&self, query: &str, type_: Option<&str>) -> Result<SearchResult> {
        // Créer une clé de cache
        let cache_key = format!("{}:{}", query, type_.unwrap_or("all"));

        // Vérifier le cache
        if let Some(result) = self.cache.get_search(&cache_key).await {
            debug!("Search results for '{}' found in cache", query);
            return Ok(result);
        }

        // Sinon, rechercher via l'API
        let result = self.api.search(query, type_).await?;

        // Mettre en cache
        self.cache.put_search(cache_key, result.clone()).await;

        Ok(result)
    }

    /// Recherche des albums
    pub async fn search_albums(&self, query: &str) -> Result<Vec<Album>> {
        let result = self.search(query, Some("albums")).await?;
        Ok(result.albums)
    }

    /// Recherche des artistes
    pub async fn search_artists(&self, query: &str) -> Result<Vec<Artist>> {
        let result = self.search(query, Some("artists")).await?;
        Ok(result.artists)
    }

    /// Recherche des tracks
    pub async fn search_tracks(&self, query: &str) -> Result<Vec<Track>> {
        let result = self.search(query, Some("tracks")).await?;
        Ok(result.tracks)
    }

    /// Recherche des playlists
    pub async fn search_playlists(&self, query: &str) -> Result<Vec<Playlist>> {
        let result = self.search(query, Some("playlists")).await?;
        Ok(result.playlists)
    }

    // ============ Favoris ============

    /// Récupère les albums favoris de l'utilisateur
    pub async fn get_favorite_albums(&self) -> Result<Vec<Album>> {
        self.api.get_favorite_albums().await
    }

    /// Récupère les artistes favoris de l'utilisateur
    pub async fn get_favorite_artists(&self) -> Result<Vec<Artist>> {
        self.api.get_favorite_artists().await
    }

    /// Récupère les tracks favorites de l'utilisateur
    pub async fn get_favorite_tracks(&self) -> Result<Vec<Track>> {
        self.api.get_favorite_tracks().await
    }

    /// Récupère les playlists de l'utilisateur
    pub async fn get_user_playlists(&self) -> Result<Vec<Playlist>> {
        self.api.get_user_playlists().await
    }

    /// Ajoute un album aux favoris
    pub async fn add_favorite_album(&self, album_id: &str) -> Result<()> {
        self.api.add_favorite_album(album_id).await
    }

    /// Supprime un album des favoris
    pub async fn remove_favorite_album(&self, album_id: &str) -> Result<()> {
        self.api.remove_favorite_album(album_id).await
    }

    /// Ajoute un track aux favoris
    pub async fn add_favorite_track(&self, track_id: &str) -> Result<()> {
        self.api.add_favorite_track(track_id).await
    }

    /// Supprime un track des favoris
    pub async fn remove_favorite_track(&self, track_id: &str) -> Result<()> {
        self.api.remove_favorite_track(track_id).await
    }

    /// Ajoute un track à une playlist
    pub async fn add_to_playlist(&self, playlist_id: &str, track_id: &str) -> Result<()> {
        self.api.add_to_playlist(playlist_id, track_id).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_app_id() {
        assert!(!DEFAULT_APP_ID.is_empty());
    }

    #[test]
    fn test_audio_format() {
        assert_eq!(AudioFormat::default(), AudioFormat::Flac_Lossless);
    }
}
========= End of pmoqobuz/src/client.rs ===========

=============== pmoqobuz/src/error.rs ============
//! Gestion des erreurs pour le client Qobuz

use thiserror::Error;

/// Type Result personnalisé pour pmoqobuz
pub type Result<T> = std::result::Result<T, QobuzError>;

/// Erreurs possibles lors de l'utilisation du client Qobuz
#[derive(Error, Debug)]
pub enum QobuzError {
    /// Erreur d'authentification (credentials invalides)
    #[error("Authentication failed: {0}")]
    Unauthorized(String),

    /// Ressource non trouvée (album, track, etc.)
    #[error("Resource not found: {0}")]
    NotFound(String),

    /// Erreur HTTP
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    /// Erreur de parsing JSON
    #[error("JSON parsing error: {0}")]
    JsonParse(#[from] serde_json::Error),

    /// Erreur de configuration
    #[error("Configuration error: {0}")]
    Config(#[from] anyhow::Error),

    /// Erreur de l'API Qobuz
    #[error("Qobuz API error (code {code}): {message}")]
    ApiError { code: u16, message: String },

    /// Quota dépassé (rate limiting)
    #[error("Rate limit exceeded, please try again later")]
    RateLimitExceeded,

    /// Contenu non disponible dans la région de l'utilisateur
    #[error("Content not available in your region")]
    NotAvailable,

    /// Abonnement insuffisant pour accéder au contenu
    #[error("Subscription level insufficient: {0}")]
    SubscriptionRequired(String),

    /// Erreur de cache
    #[error("Cache error: {0}")]
    Cache(String),

    /// Erreur d'export DIDL
    #[error("DIDL export error: {0}")]
    DidlExport(String),

    /// Erreur générique
    #[error("Qobuz error: {0}")]
    Other(String),
}

impl QobuzError {
    /// Crée une erreur API depuis un code de statut HTTP et un message
    pub fn from_status_code(code: u16, message: impl Into<String>) -> Self {
        match code {
            401 | 403 => Self::Unauthorized(message.into()),
            404 => Self::NotFound(message.into()),
            429 => Self::RateLimitExceeded,
            _ => Self::ApiError {
                code,
                message: message.into(),
            },
        }
    }

    /// Vérifie si l'erreur est une erreur de credentials
    pub fn is_auth_error(&self) -> bool {
        matches!(self, QobuzError::Unauthorized(_))
    }

    /// Vérifie si l'erreur est une erreur de rate limiting
    pub fn is_rate_limit(&self) -> bool {
        matches!(self, QobuzError::RateLimitExceeded)
    }
}
========= End of pmoqobuz/src/error.rs ===========

=============== pmoqobuz/src/lib.rs ============
//! # pmoqobuz - Client Qobuz pour PMOMusic
//!
//! Cette crate fournit un client Rust pour l'API Qobuz, inspiré de l'implémentation Python d'upmpdcli,
//! avec un système de cache en mémoire et une intégration avec les autres modules PMOMusic.
//!
//! ## Vue d'ensemble
//!
//! `pmoqobuz` permet d'accéder aux fonctionnalités de Qobuz :
//! - Authentification avec les credentials configurés
//! - Navigation dans le catalogue (albums, artistes, playlists, tracks)
//! - Recherche dans le catalogue
//! - Accès aux favoris de l'utilisateur
//! - Cache en mémoire pour minimiser les requêtes API
//! - Export des objets en format DIDL-Lite (via `pmodidl`)
//! - Cache des images d'albums (via `pmocovers`)
//!
//! ## Architecture
//!
//! La crate suit le pattern d'extension des autres crates PMO :
//! - `QobuzClient` : Client principal avec authentification et cache
//! - `models` : Structures de données (Album, Track, Artist, etc.)
//! - `api` : Couche d'accès à l'API REST Qobuz
//! - `cache` : Système de cache en mémoire avec TTL
//! - `didl` : Export des objets en format DIDL-Lite
//!
//! ## Structure des modules
//!
//! ```text
//! pmoqobuz/
//! ├── src/
//! │   ├── lib.rs              # Module principal (ce fichier)
//! │   ├── client.rs           # Client Qobuz principal
//! │   ├── models.rs           # Structures de données
//! │   ├── api/
//! │   │   ├── mod.rs          # API client
//! │   │   ├── auth.rs         # Authentification
//! │   │   ├── catalog.rs      # Accès au catalogue
//! │   │   └── user.rs         # API utilisateur (favoris)
//! │   ├── cache.rs            # Cache en mémoire
//! │   ├── didl.rs             # Export DIDL-Lite
//! │   └── error.rs            # Gestion des erreurs
//! ```
//!
//! ## Utilisation
//!
//! ### Exemple basique avec configuration automatique
//!
//! ```rust,no_run
//! use pmoqobuz::QobuzClient;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//!     // Utilise automatiquement la config depuis pmoconfig
//!     let client = QobuzClient::from_config().await?;
//!
//!     // Rechercher des albums
//!     let results = client.search_albums("Miles Davis").await?;
//!     for album in results {
//!         println!("{} - {}", album.artist.name, album.title);
//!     }
//!
//!     Ok(())
//! }
//! ```
//!
//! ### Exemple avec credentials personnalisés
//!
//! ```rust,no_run
//! use pmoqobuz::QobuzClient;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//!     let client = QobuzClient::new("user@example.com", "password").await?;
//!
//!     // Obtenir les albums favoris
//!     let favorites = client.get_favorite_albums().await?;
//!
//!     Ok(())
//! }
//! ```
//!
//! ### Export DIDL-Lite
//!
//! ```rust,no_run
//! use pmoqobuz::QobuzClient;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//!     let client = QobuzClient::from_config().await?;
//!
//!     let album = client.get_album("12345").await?;
//!     let didl_container = album.to_didl_container("parent_id")?;
//!
//!     Ok(())
//! }
//! ```
//!
//! ## Cache
//!
//! Le client utilise un cache en mémoire avec TTL pour minimiser les requêtes à l'API Qobuz :
//! - Albums : 1 heure
//! - Tracks : 1 heure
//! - Artistes : 1 heure
//! - Playlists : 30 minutes
//! - Résultats de recherche : 15 minutes
//! - URLs de streaming : 5 minutes
//!
//! ## Intégration pmocovers et pmoaudiocache
//!
//! La feature `cache` active le support complet du cache pour les images et l'audio.
//!
//! ### Cache d'images (pmocovers)
//!
//! Les images de couverture sont automatiquement téléchargées et converties en WebP :
//!
//! ```rust,no_run
//! use pmoqobuz::{QobuzSource, QobuzClient};
//! use pmocovers::Cache as CoverCache;
//! use std::sync::Arc;
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let client = QobuzClient::from_config().await?;
//! let cover_cache = Arc::new(CoverCache::new("./cache/covers", 500)?);
//!
//! let source = QobuzSource::new_with_cache(
//!     client,
//!     "http://localhost:8080",
//!     Some(cover_cache),
//!     None,
//! );
//! # Ok(())
//! # }
//! ```
//!
//! ### Cache audio (pmoaudiocache)
//!
//! L'audio haute résolution est téléchargé et caché localement avec métadonnées enrichies :
//!
//! ```rust,no_run
//! use pmoqobuz::{QobuzSource, QobuzClient};
//! use pmocovers::Cache as CoverCache;
//! use pmoaudiocache::AudioCache;
//! use std::sync::Arc;
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let client = QobuzClient::from_config().await?;
//! let cover_cache = Arc::new(CoverCache::new("./cache/covers", 500)?);
//! let audio_cache = Arc::new(AudioCache::new("./cache/audio", 100)?);
//!
//! let source = QobuzSource::new_with_cache(
//!     client.clone(),
//!     "http://localhost:8080",
//!     Some(cover_cache),
//!     Some(audio_cache),
//! );
//!
//! // Add a track with caching
//! let tracks = client.get_favorite_tracks().await?;
//! if let Some(track) = tracks.first() {
//!     let track_id = source.add_track(track).await?;
//!     // Audio and cover are now cached with rich metadata
//!
//!     // Resolve URI (returns cached version if available)
//!     let uri = source.resolve_uri(&track_id).await?;
//! }
//! # Ok(())
//! # }
//! ```
//!
//! ### Métadonnées enrichies
//!
//! Qobuz fournit des métadonnées détaillées qui sont préservées dans le cache :
//! - Titre, artiste, album
//! - Numéro de piste et de disque
//! - Année de sortie
//! - Genre(s)
//! - Label
//! - Qualité audio (sample rate, bit depth, channels)
//! - Durée
//!
//! ### Exemple complet
//!
//! Voir `examples/with_cache.rs` pour un exemple complet d'utilisation avec cache.
//!
//! ## Formats audio supportés
//!
//! Qobuz propose plusieurs formats :
//! - Format 5 : MP3 320 kbps
//! - Format 6 : FLAC 16 bit / 44.1 kHz (CD Quality)
//! - Format 7 : FLAC 24 bit / jusqu'à 96 kHz (Hi-Res)
//! - Format 27 : FLAC 24 bit / jusqu'à 192 kHz (Hi-Res+)
//!
//! ## Gestion des erreurs
//!
//! La crate utilise `thiserror` pour définir des erreurs typées :
//!
//! ```rust,ignore
//! use pmoqobuz::{QobuzClient, QobuzError};
//!
//! match client.get_album("invalid").await {
//!     Ok(album) => println!("Album: {}", album.title),
//!     Err(QobuzError::NotFound) => println!("Album not found"),
//!     Err(QobuzError::Unauthorized) => println!("Authentication failed"),
//!     Err(e) => println!("Error: {}", e),
//! }
//! ```
//!
//! ## Voir aussi
//!
//! - [`pmodidl`] : Format DIDL-Lite
//! - [`pmocovers`] : Cache d'images
//! - [`pmoaudiocache`] : Cache audio
//! - [`pmoconfig`] : Configuration
//! - [`pmoserver`] : Serveur HTTP

pub mod api;
pub mod cache;
pub mod client;
pub mod didl;
pub mod error;
pub mod models;
pub mod source;

// Extension pmoserver (feature-gated)
#[cfg(feature = "pmoserver")]
pub mod api_rest;

#[cfg(feature = "pmoserver")]
pub mod pmoserver_ext;

#[cfg(feature = "pmoserver")]
mod pmoserver_impl;

pub use client::QobuzClient;
pub use error::{QobuzError, Result};
pub use models::{Album, Artist, AudioFormat, Genre, Playlist, SearchResult, Track};
pub use source::QobuzSource;

/// Ré-exporte les types DIDL pour faciliter l'utilisation
pub use didl::ToDIDL;

/// Ré-exporte le trait d'extension pmoserver
#[cfg(feature = "pmoserver")]
pub use pmoserver_ext::QobuzServerExt;
========= End of pmoqobuz/src/lib.rs ===========

=============== pmoqobuz/src/models.rs ============
//! Structures de données pour représenter les objets Qobuz

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Représente un artiste Qobuz
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Artist {
    /// Identifiant unique de l'artiste
    pub id: String,
    /// Nom de l'artiste
    pub name: String,
    /// URL de l'image de l'artiste (optionnelle)
    #[serde(default)]
    pub image: Option<String>,
    /// URL de l'image cachée localement (via pmocovers)
    #[serde(skip)]
    pub image_cached: Option<String>,
}

/// Représente un album Qobuz
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Album {
    /// Identifiant unique de l'album
    pub id: String,
    /// Titre de l'album
    pub title: String,
    /// Artiste principal de l'album
    pub artist: Artist,
    /// Nombre de pistes
    #[serde(default)]
    pub tracks_count: Option<u32>,
    /// Durée totale en secondes
    #[serde(default)]
    pub duration: Option<u32>,
    /// Date de sortie (format ISO 8601)
    #[serde(default)]
    pub release_date: Option<String>,
    /// URL de l'image de couverture
    #[serde(default)]
    pub image: Option<String>,
    /// URL de l'image cachée localement (via pmocovers)
    #[serde(skip)]
    pub image_cached: Option<String>,
    /// Indique si l'album est disponible pour le streaming
    #[serde(default = "default_true")]
    pub streamable: bool,
    /// Description de l'album
    #[serde(default)]
    pub description: Option<String>,
    /// Taux d'échantillonnage maximum (Hz)
    #[serde(default)]
    pub maximum_sampling_rate: Option<f64>,
    /// Profondeur de bits maximale
    #[serde(default)]
    pub maximum_bit_depth: Option<u32>,
    /// Genre(s) de l'album
    #[serde(default)]
    pub genres: Vec<String>,
    /// Label de l'album
    #[serde(default)]
    pub label: Option<String>,
}

/// Représente une piste (track) Qobuz
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Track {
    /// Identifiant unique de la piste
    pub id: String,
    /// Titre de la piste
    pub title: String,
    /// Artiste de la piste (peut différer de l'artiste de l'album)
    pub performer: Option<Artist>,
    /// Album contenant la piste
    pub album: Option<Album>,
    /// Durée en secondes
    pub duration: u32,
    /// Numéro de piste
    pub track_number: u32,
    /// Numéro de disque (pour les albums multi-disques)
    pub media_number: u32,
    /// Indique si la piste est disponible pour le streaming
    #[serde(default = "default_true")]
    pub streamable: bool,
    /// Type MIME du fichier audio (déterminé après obtention de l'URL)
    #[serde(skip)]
    pub mime_type: Option<String>,
    /// Fréquence d'échantillonnage (Hz)
    #[serde(skip)]
    pub sample_rate: Option<u32>,
    /// Profondeur de bits
    #[serde(skip)]
    pub bit_depth: Option<u32>,
    /// Nombre de canaux audio
    #[serde(skip)]
    pub channels: Option<u8>,
}

/// Représente une playlist Qobuz
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Playlist {
    /// Identifiant unique de la playlist
    pub id: String,
    /// Nom de la playlist
    pub name: String,
    /// Description de la playlist
    #[serde(default)]
    pub description: Option<String>,
    /// Nombre de pistes
    #[serde(default)]
    pub tracks_count: Option<u32>,
    /// Durée totale en secondes
    #[serde(default)]
    pub duration: Option<u32>,
    /// URL de l'image de la playlist
    #[serde(default)]
    pub image: Option<String>,
    /// URL de l'image cachée localement
    #[serde(skip)]
    pub image_cached: Option<String>,
    /// Indique si c'est une playlist publique
    #[serde(default)]
    pub is_public: bool,
    /// Propriétaire de la playlist
    #[serde(default)]
    pub owner: Option<PlaylistOwner>,
}

/// Propriétaire d'une playlist
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaylistOwner {
    /// Identifiant de l'utilisateur
    pub id: u64,
    /// Nom de l'utilisateur
    pub name: String,
}

/// Représente un genre musical
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Genre {
    /// Identifiant du genre (peut être None pour "All Genres")
    pub id: Option<u32>,
    /// Nom du genre
    pub name: String,
    /// Genres enfants
    #[serde(default)]
    pub children: Vec<Genre>,
}

/// Résultats de recherche
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SearchResult {
    /// Albums trouvés
    #[serde(default)]
    pub albums: Vec<Album>,
    /// Artistes trouvés
    #[serde(default)]
    pub artists: Vec<Artist>,
    /// Pistes trouvées
    #[serde(default)]
    pub tracks: Vec<Track>,
    /// Playlists trouvées
    #[serde(default)]
    pub playlists: Vec<Playlist>,
}

/// Informations sur un fichier de streaming
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamInfo {
    /// URL de streaming
    pub url: String,
    /// Type MIME
    pub mime_type: String,
    /// Fréquence d'échantillonnage (Hz)
    pub sampling_rate: u32,
    /// Profondeur de bits
    pub bit_depth: u32,
    /// Format ID Qobuz
    pub format_id: u8,
    /// Date d'expiration de l'URL
    #[serde(skip)]
    pub expires_at: DateTime<Utc>,
}

/// Format audio demandé pour le streaming
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
#[allow(non_camel_case_types)]
pub enum AudioFormat {
    /// MP3 320 kbps
    Mp3_320 = 5,
    /// FLAC 16 bit / 44.1 kHz (CD Quality)
    Flac_Lossless = 6,
    /// FLAC 24 bit / jusqu'à 96 kHz (Hi-Res)
    Flac_HiRes_96 = 7,
    /// FLAC 24 bit / jusqu'à 192 kHz (Hi-Res+)
    Flac_HiRes_192 = 27,
}

impl AudioFormat {
    /// Retourne l'ID du format pour l'API Qobuz
    pub fn id(&self) -> u8 {
        *self as u8
    }

    /// Retourne une description lisible du format
    pub fn description(&self) -> &'static str {
        match self {
            AudioFormat::Mp3_320 => "MP3 320 kbps",
            AudioFormat::Flac_Lossless => "FLAC 16 bit / 44.1 kHz",
            AudioFormat::Flac_HiRes_96 => "FLAC 24 bit / up to 96 kHz",
            AudioFormat::Flac_HiRes_192 => "FLAC 24 bit / up to 192 kHz",
        }
    }

    /// Retourne le type MIME associé
    pub fn mime_type(&self) -> &'static str {
        match self {
            AudioFormat::Mp3_320 => "audio/mpeg",
            _ => "audio/flac",
        }
    }
}

impl Default for AudioFormat {
    fn default() -> Self {
        AudioFormat::Flac_Lossless
    }
}

// Helper functions
fn default_true() -> bool {
    true
}

impl Artist {
    /// Crée un nouvel artiste avec un ID et un nom
    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            name: name.into(),
            image: None,
            image_cached: None,
        }
    }
}

impl Album {
    /// Retourne un titre formaté avec les informations audio si disponibles
    pub fn formatted_title(&self) -> String {
        if let (Some(rate), Some(depth)) = (self.maximum_sampling_rate, self.maximum_bit_depth) {
            format!("{} ({:.0}/{} bit)", self.title, rate / 1000.0, depth)
        } else {
            self.title.clone()
        }
    }

    /// Vérifie si l'album est disponible pour le streaming
    pub fn is_available(&self) -> bool {
        self.streamable
    }
}

impl Track {
    /// Retourne l'artiste à afficher (performer ou artiste de l'album)
    pub fn display_artist(&self) -> Option<&Artist> {
        self.performer
            .as_ref()
            .or_else(|| self.album.as_ref().map(|a| &a.artist))
    }

    /// Retourne le nom de l'album si disponible
    pub fn album_name(&self) -> Option<&str> {
        self.album.as_ref().map(|a| a.title.as_str())
    }

    /// Vérifie si la piste est disponible pour le streaming
    pub fn is_available(&self) -> bool {
        self.streamable
    }
}

impl SearchResult {
    /// Crée un résultat de recherche vide
    pub fn new() -> Self {
        Self::default()
    }

    /// Retourne le nombre total de résultats
    pub fn total_count(&self) -> usize {
        self.albums.len() + self.artists.len() + self.tracks.len() + self.playlists.len()
    }

    /// Vérifie si la recherche n'a retourné aucun résultat
    pub fn is_empty(&self) -> bool {
        self.total_count() == 0
    }
}
========= End of pmoqobuz/src/models.rs ===========

=============== pmoqobuz/src/didl.rs ============
//! Export des objets Qobuz en format DIDL-Lite
//!
//! Ce module permet de convertir les structures Qobuz (Album, Track, etc.)
//! en objets DIDL-Lite compatibles avec UPnP/DLNA.

use crate::error::{QobuzError, Result};
use crate::models::{Album, Playlist, Track};
use pmodidl::{Container, Item, Resource};

/// Trait pour convertir un objet Qobuz en DIDL-Lite
pub trait ToDIDL {
    /// Convertit l'objet en Container DIDL
    fn to_didl_container(&self, parent_id: &str) -> Result<Container>;

    /// Convertit l'objet en Item DIDL
    fn to_didl_item(&self, parent_id: &str) -> Result<Item>;
}

impl ToDIDL for Album {
    /// Convertit un album en Container DIDL
    ///
    /// # Arguments
    ///
    /// * `parent_id` - ID du container parent
    ///
    /// # Exemple
    ///
    /// ```rust,ignore
    /// let album = client.get_album("12345").await?;
    /// let container = album.to_didl_container("0$qobuz$albums")?;
    /// ```
    fn to_didl_container(&self, parent_id: &str) -> Result<Container> {
        let id = format!("0$qobuz$album${}", self.id);

        Ok(Container {
            id,
            parent_id: parent_id.to_string(),
            restricted: Some("1".to_string()),
            child_count: self.tracks_count.map(|c| c.to_string()),
            searchable: Some("1".to_string()),
            title: self.formatted_title(),
            class: "object.container.album.musicAlbum".to_string(),
            containers: Vec::new(),
            items: Vec::new(),
        })
    }

    /// Un album ne peut pas être converti directement en Item
    fn to_didl_item(&self, _parent_id: &str) -> Result<Item> {
        Err(QobuzError::DidlExport(
            "Album cannot be converted to Item, use to_didl_container instead".to_string(),
        ))
    }
}

impl ToDIDL for Track {
    /// Une track ne peut pas être convertie en Container
    fn to_didl_container(&self, _parent_id: &str) -> Result<Container> {
        Err(QobuzError::DidlExport(
            "Track cannot be converted to Container, use to_didl_item instead".to_string(),
        ))
    }

    /// Convertit une track en Item DIDL
    ///
    /// # Arguments
    ///
    /// * `parent_id` - ID du container parent
    ///
    /// # Exemple
    ///
    /// ```rust,ignore
    /// let track = client.get_track("98765").await?;
    /// let item = track.to_didl_item("0$qobuz$album$12345")?;
    /// ```
    fn to_didl_item(&self, parent_id: &str) -> Result<Item> {
        let id = format!("0$qobuz$track${}", self.id);

        // Déterminer l'artiste à afficher
        let artist_name = self
            .display_artist()
            .map(|a| a.name.clone())
            .or_else(|| self.album.as_ref().map(|a| a.artist.name.clone()));

        // Déterminer l'album
        let album_name = self.album_name().map(|s| s.to_string());

        // Déterminer l'image de couverture
        let album_art = self
            .album
            .as_ref()
            .and_then(|a| a.image_cached.clone().or_else(|| a.image.clone()));

        // Créer la ressource (URL de streaming)
        // Note: L'URL sera remplie plus tard via get_stream_url
        let resource = Resource {
            protocol_info: format!(
                "http-get:*:{}:*",
                self.mime_type.as_deref().unwrap_or("audio/flac")
            ),
            bits_per_sample: self.bit_depth.map(|b| b.to_string()),
            sample_frequency: self.sample_rate.map(|r| r.to_string()),
            nr_audio_channels: self.channels.map(|c| c.to_string()),
            duration: Some(format_duration(self.duration)),
            url: format!("qobuz://track/{}", self.id), // URL symbolique
        };

        Ok(Item {
            id,
            parent_id: parent_id.to_string(),
            restricted: Some("1".to_string()),
            title: self.title.clone(),
            creator: artist_name.clone(),
            class: "object.item.audioItem.musicTrack".to_string(),
            artist: artist_name,
            album: album_name,
            genre: None, // Qobuz ne fournit pas le genre au niveau track
            album_art,
            album_art_pk: None,
            date: self.album.as_ref().and_then(|a| a.release_date.clone()),
            original_track_number: Some(self.track_number.to_string()),
            resources: vec![resource],
            descriptions: Vec::new(),
        })
    }
}

impl ToDIDL for Playlist {
    /// Convertit une playlist en Container DIDL
    fn to_didl_container(&self, parent_id: &str) -> Result<Container> {
        let id = format!("0$qobuz$playlist${}", self.id);

        Ok(Container {
            id,
            parent_id: parent_id.to_string(),
            restricted: Some("1".to_string()),
            child_count: self.tracks_count.map(|c| c.to_string()),
            searchable: Some("1".to_string()),
            title: self.name.clone(),
            class: "object.container.playlistContainer".to_string(),
            containers: Vec::new(),
            items: Vec::new(),
        })
    }

    /// Une playlist ne peut pas être convertie en Item
    fn to_didl_item(&self, _parent_id: &str) -> Result<Item> {
        Err(QobuzError::DidlExport(
            "Playlist cannot be converted to Item, use to_didl_container instead".to_string(),
        ))
    }
}

/// Formate une durée en secondes au format HH:MM:SS
fn format_duration(seconds: u32) -> String {
    let hours = seconds / 3600;
    let minutes = (seconds % 3600) / 60;
    let secs = seconds % 60;
    format!("{:02}:{:02}:{:02}", hours, minutes, secs)
}

/// Convertit une liste de tracks en items DIDL
pub fn tracks_to_didl_items(tracks: &[Track], parent_id: &str) -> Result<Vec<Item>> {
    tracks
        .iter()
        .map(|track| track.to_didl_item(parent_id))
        .collect()
}

/// Convertit une liste d'albums en containers DIDL
pub fn albums_to_didl_containers(albums: &[Album], parent_id: &str) -> Result<Vec<Container>> {
    albums
        .iter()
        .map(|album| album.to_didl_container(parent_id))
        .collect()
}

/// Convertit une liste de playlists en containers DIDL
pub fn playlists_to_didl_containers(
    playlists: &[Playlist],
    parent_id: &str,
) -> Result<Vec<Container>> {
    playlists
        .iter()
        .map(|playlist| playlist.to_didl_container(parent_id))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::{Album, Artist, Track};

    #[test]
    fn test_album_to_didl_container() {
        let album = Album {
            id: "123".to_string(),
            title: "Test Album".to_string(),
            artist: Artist::new("456", "Test Artist"),
            tracks_count: Some(10),
            duration: Some(3000),
            release_date: Some("2024-01-01".to_string()),
            image: None,
            image_cached: None,
            streamable: true,
            description: None,
            maximum_sampling_rate: Some(96000.0),
            maximum_bit_depth: Some(24),
            genres: vec![],
            label: None,
        };

        let container = album.to_didl_container("parent").unwrap();
        assert_eq!(container.id, "0$qobuz$album$123");
        assert_eq!(container.parent_id, "parent");
        assert!(container.title.contains("Test Album"));
    }

    #[test]
    fn test_track_to_didl_item() {
        let track = Track {
            id: "789".to_string(),
            title: "Test Track".to_string(),
            performer: Some(Artist::new("456", "Test Artist")),
            album: None,
            duration: 180,
            track_number: 1,
            media_number: 1,
            streamable: true,
            mime_type: Some("audio/flac".to_string()),
            sample_rate: Some(44100),
            bit_depth: Some(16),
            channels: Some(2),
        };

        let item = track.to_didl_item("parent").unwrap();
        assert_eq!(item.id, "0$qobuz$track$789");
        assert_eq!(item.parent_id, "parent");
        assert_eq!(item.title, "Test Track");
    }

    #[test]
    fn test_format_duration() {
        assert_eq!(format_duration(0), "00:00:00");
        assert_eq!(format_duration(90), "00:01:30");
        assert_eq!(format_duration(3665), "01:01:05");
    }
}
========= End of pmoqobuz/src/didl.rs ===========

=============== pmoqobuz/src/source.rs ============
//! Music source implementation for Qobuz
//!
//! This module implements the [`pmosource::MusicSource`] trait for Qobuz,
//! providing a complete music catalog browsing and searching experience.

use crate::client::QobuzClient;
use crate::didl::ToDIDL;
use crate::models::Track;
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
use pmocovers::Cache as CoverCache;
use pmodidl::{Container, Item};
use pmosource::SourceCacheManager;
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
use std::sync::Arc;
use std::time::SystemTime;

/// Default image for Qobuz (300x300 WebP, embedded in binary)
const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");

/// Qobuz music source with full MusicSource trait implementation
///
/// This struct combines a [`QobuzClient`] for API access with browsing and
/// navigation capabilities, implementing the complete [`MusicSource`] trait.
///
/// # Features
///
/// - **Catalog Navigation**: Browse albums, artists, playlists, favorites
/// - **Search**: Full-text search across the Qobuz catalog
/// - **URI Resolution**: Resolves track streaming URIs with authentication
/// - **DIDL-Lite Export**: Converts albums, tracks, and playlists to UPnP formats
/// - **Caching**: Integrated with QobuzClient's cache for performance
///
/// # Architecture
///
/// Unlike streaming sources like Radio Paradise, Qobuz is a catalog-based source:
/// - Root container has multiple sub-containers (Albums, Artists, Favorites, etc.)
/// - No FIFO support (it's a static catalog, not a dynamic stream)
/// - Hierarchical browsing: Root → Category → Albums → Tracks
///
/// # Examples
///
/// ```no_run
/// use pmoqobuz::{QobuzSource, QobuzClient};
/// use pmosource::MusicSource;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let client = QobuzClient::from_config().await?;
///     let source = QobuzSource::new(client);
///
///     println!("Source: {}", source.name());
///     println!("Supports FIFO: {}", source.supports_fifo());
///
///     // Browse root container
///     let root = source.root_container().await?;
///     println!("Root: {} with {} children", root.title, root.child_count.unwrap_or_default());
///
///     Ok(())
/// }
/// ```
#[derive(Clone)]
pub struct QobuzSource {
    inner: Arc<QobuzSourceInner>,
}

struct QobuzSourceInner {
    /// Qobuz API client
    client: QobuzClient,

    /// Cache manager (centralisé)
    cache_manager: SourceCacheManager,

    /// Update tracking
    update_counter: tokio::sync::RwLock<u32>,
    last_change: tokio::sync::RwLock<SystemTime>,
}

impl std::fmt::Debug for QobuzSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("QobuzSource").finish()
    }
}

impl QobuzSource {
    /// Create a new Qobuz source from the cache registry
    ///
    /// This is the recommended way to create a source when using the UPnP server.
    /// The caches are automatically retrieved from the global registry.
    ///
    /// # Arguments
    ///
    /// * `client` - Authenticated Qobuz API client
    ///
    /// # Errors
    ///
    /// Returns an error if the caches are not initialized in the registry
    #[cfg(feature = "server")]
    pub fn from_registry(client: QobuzClient) -> Result<Self> {
        let cache_manager = SourceCacheManager::from_registry("qobuz".to_string())?;

        Ok(Self {
            inner: Arc::new(QobuzSourceInner {
                client,
                cache_manager,
                update_counter: tokio::sync::RwLock::new(0),
                last_change: tokio::sync::RwLock::new(SystemTime::now()),
            }),
        })
    }

    /// Create a new Qobuz source with explicit caches (for tests)
    ///
    /// # Arguments
    ///
    /// * `client` - Authenticated Qobuz API client
    /// * `cover_cache` - Cover image cache (required)
    /// * `audio_cache` - Audio cache (required)
    pub fn new(
        client: QobuzClient,
        cover_cache: Arc<CoverCache>,
        audio_cache: Arc<AudioCache>,
    ) -> Self {
        let cache_manager = SourceCacheManager::new("qobuz".to_string(), cover_cache, audio_cache);

        Self {
            inner: Arc::new(QobuzSourceInner {
                client,
                cache_manager,
                update_counter: tokio::sync::RwLock::new(0),
                last_change: tokio::sync::RwLock::new(SystemTime::now()),
            }),
        }
    }

    /// Get the Qobuz client
    pub fn client(&self) -> &QobuzClient {
        &self.inner.client
    }

    /// Add a track from Qobuz with caching
    ///
    /// This method downloads and caches both cover art and audio data.
    pub async fn add_track(&self, track: &Track) -> Result<String> {
        let track_id = format!("qobuz://track/{}", track.id);

        // Get streaming URL
        let stream_url = self
            .inner
            .client
            .get_stream_url(&track.id)
            .await
            .map_err(|e| MusicSourceError::UriResolutionError(e.to_string()))?;

        // 1. Cache cover via manager
        let cached_cover_pk = if let Some(ref album) = track.album {
            if let Some(ref image_url) = album.image {
                self.inner.cache_manager.cache_cover(image_url).await.ok()
            } else {
                None
            }
        } else {
            None
        };

        // 2. Prepare rich metadata from Qobuz track
        let metadata = AudioMetadata {
            title: Some(track.title.clone()),
            artist: track.performer.as_ref().map(|p| p.name.clone()),
            album: track.album.as_ref().map(|a| a.title.clone()),
            duration_secs: Some(track.duration as u64),
            year: track.album.as_ref().and_then(|a| {
                a.release_date
                    .as_ref()
                    .and_then(|d| d.split('-').next()?.parse().ok())
            }),
            track_number: Some(track.track_number),
            track_total: track.album.as_ref().and_then(|a| a.tracks_count),
            disc_number: Some(track.media_number),
            disc_total: None,
            genre: track.album.as_ref().and_then(|a| {
                if !a.genres.is_empty() {
                    Some(a.genres.join(", "))
                } else {
                    None
                }
            }),
            sample_rate: track.sample_rate,
            channels: track.channels,
            bitrate: None,
            conversion: None,
        };

        // 3. Cache audio via manager
        let cached_audio_pk = self
            .inner
            .cache_manager
            .cache_audio(&stream_url, Some(metadata))
            .await
            .ok();

        // 4. Store metadata
        self.inner
            .cache_manager
            .update_metadata(
                track_id.clone(),
                pmosource::TrackMetadata {
                    original_uri: stream_url,
                    cached_audio_pk,
                    cached_cover_pk,
                },
            )
            .await;

        Ok(track_id)
    }

    /// Increment update counter (called on catalog changes)
    async fn increment_update_id(&self) {
        let mut counter = self.inner.update_counter.write().await;
        *counter = counter.wrapping_add(1);
        let mut last = self.inner.last_change.write().await;
        *last = SystemTime::now();
    }

    /// Parse object_id to determine what to browse
    ///
    /// Object IDs follow these patterns:
    /// - "qobuz" or "0" → Root container
    /// - "qobuz:favorites" → User's favorite albums
    /// - "qobuz:album:{id}" → Tracks in album
    /// - "qobuz:playlist:{id}" → Tracks in playlist
    fn parse_object_id(&self, object_id: &str) -> ObjectIdType {
        if object_id == "qobuz" || object_id == "0" {
            return ObjectIdType::Root;
        }

        let parts: Vec<&str> = object_id.split(':').collect();
        match parts.as_slice() {
            ["qobuz", "favorites"] => ObjectIdType::Favorites,
            ["qobuz", "album", id] => ObjectIdType::Album(id.to_string()),
            ["qobuz", "playlist", id] => ObjectIdType::Playlist(id.to_string()),
            ["qobuz", "artist", id] => ObjectIdType::Artist(id.to_string()),
            _ => ObjectIdType::Unknown,
        }
    }
}

#[derive(Debug)]
enum ObjectIdType {
    Root,
    Favorites,
    Album(String),
    Playlist(String),
    Artist(String),
    Unknown,
}

#[async_trait]
impl MusicSource for QobuzSource {
    fn name(&self) -> &str {
        "Qobuz"
    }

    fn id(&self) -> &str {
        "qobuz"
    }

    fn default_image(&self) -> &[u8] {
        DEFAULT_IMAGE
    }

    async fn root_container(&self) -> Result<Container> {
        // Create the root container with sub-containers for different categories
        Ok(Container {
            id: "qobuz".to_string(),
            parent_id: "0".to_string(),
            restricted: Some("1".to_string()),
            child_count: Some("2".to_string()), // Favorites + Search (simplified)
            searchable: Some("1".to_string()),
            title: "Qobuz".to_string(),
            class: "object.container".to_string(),
            containers: vec![
                // Favorites container
                Container {
                    id: "qobuz:favorites".to_string(),
                    parent_id: "qobuz".to_string(),
                    restricted: Some("1".to_string()),
                    child_count: None, // Will be determined when browsed
                    searchable: Some("1".to_string()),
                    title: "My Favorites".to_string(),
                    class: "object.container".to_string(),
                    containers: vec![],
                    items: vec![],
                },
            ],
            items: vec![],
        })
    }

    async fn browse(&self, object_id: &str) -> Result<BrowseResult> {
        match self.parse_object_id(object_id) {
            ObjectIdType::Root => {
                // Return the root container's children
                let root = self.root_container().await?;
                Ok(BrowseResult::Containers(root.containers))
            }

            ObjectIdType::Favorites => {
                // Get user's favorite albums
                let albums = self
                    .inner
                    .client
                    .get_favorite_albums()
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                let containers: Vec<Container> = albums
                    .into_iter()
                    .filter_map(|album| album.to_didl_container("qobuz:favorites").ok())
                    .collect();

                Ok(BrowseResult::Containers(containers))
            }

            ObjectIdType::Album(album_id) => {
                // Get tracks in album
                let tracks = self
                    .inner
                    .client
                    .get_album_tracks(&album_id)
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                let items: Vec<Item> = tracks
                    .into_iter()
                    .filter_map(|track| {
                        track
                            .to_didl_item(&format!("qobuz:album:{}", album_id))
                            .ok()
                    })
                    .collect();

                Ok(BrowseResult::Items(items))
            }

            ObjectIdType::Playlist(playlist_id) => {
                // Get tracks in playlist
                let tracks = self
                    .inner
                    .client
                    .get_playlist_tracks(&playlist_id)
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                let items: Vec<Item> = tracks
                    .into_iter()
                    .filter_map(|track| {
                        track
                            .to_didl_item(&format!("qobuz:playlist:{}", playlist_id))
                            .ok()
                    })
                    .collect();

                Ok(BrowseResult::Items(items))
            }

            ObjectIdType::Artist(artist_id) => {
                // Get albums by artist
                let albums = self
                    .inner
                    .client
                    .get_artist_albums(&artist_id)
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                let containers: Vec<Container> = albums
                    .into_iter()
                    .filter_map(|album| {
                        album
                            .to_didl_container(&format!("qobuz:artist:{}", artist_id))
                            .ok()
                    })
                    .collect();

                Ok(BrowseResult::Containers(containers))
            }

            ObjectIdType::Unknown => Err(MusicSourceError::ObjectNotFound(object_id.to_string())),
        }
    }

    async fn resolve_uri(&self, object_id: &str) -> Result<String> {
        // Try cache manager first
        if let Ok(uri) = self.inner.cache_manager.resolve_uri(object_id).await {
            return Ok(uri);
        }

        // If not cached, extract track ID and get streaming URL from Qobuz
        let track_id = object_id
            .strip_prefix("qobuz://track/")
            .unwrap_or(object_id);

        self.inner
            .client
            .get_stream_url(track_id)
            .await
            .map_err(|e| MusicSourceError::UriResolutionError(e.to_string()))
    }

    fn supports_fifo(&self) -> bool {
        // Qobuz is a catalog, not a dynamic stream
        false
    }

    async fn append_track(&self, _track: Item) -> Result<()> {
        Err(MusicSourceError::FifoNotSupported)
    }

    async fn remove_oldest(&self) -> Result<Option<Item>> {
        Err(MusicSourceError::FifoNotSupported)
    }

    async fn update_id(&self) -> u32 {
        *self.inner.update_counter.read().await
    }

    async fn last_change(&self) -> Option<SystemTime> {
        Some(*self.inner.last_change.read().await)
    }

    async fn get_items(&self, offset: usize, count: usize) -> Result<Vec<Item>> {
        // For Qobuz, "get_items" returns favorite tracks with pagination
        let all_tracks = self
            .inner
            .client
            .get_favorite_tracks()
            .await
            .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

        let items: Vec<Item> = all_tracks
            .into_iter()
            .skip(offset)
            .take(count)
            .filter_map(|track| track.to_didl_item("qobuz:favorites").ok())
            .collect();

        Ok(items)
    }

    async fn search(&self, query: &str) -> Result<BrowseResult> {
        // Search across Qobuz catalog
        let results = self
            .inner
            .client
            .search(query, None)
            .await
            .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

        // Convert albums to containers and tracks to items
        let containers: Vec<Container> = results
            .albums
            .into_iter()
            .filter_map(|album| album.to_didl_container("qobuz").ok())
            .collect();

        let items: Vec<Item> = results
            .tracks
            .into_iter()
            .filter_map(|track| track.to_didl_item("qobuz").ok())
            .collect();

        if !containers.is_empty() || !items.is_empty() {
            Ok(BrowseResult::Mixed { containers, items })
        } else {
            Ok(BrowseResult::Items(vec![]))
        }
    }

    // ============= Extended Features Implementation =============

    fn capabilities(&self) -> pmosource::SourceCapabilities {
        pmosource::SourceCapabilities {
            supports_fifo: false,
            supports_search: true,
            supports_favorites: true,
            supports_playlists: true,
            supports_user_content: false,
            supports_high_res_audio: true,
            max_sample_rate: Some(192_000), // Qobuz supports up to 192kHz
            supports_multiple_formats: true,
            supports_advanced_search: false, // TODO: Qobuz API supports it, not yet implemented
            supports_pagination: true,
        }
    }

    async fn get_available_formats(&self, object_id: &str) -> Result<Vec<pmosource::AudioFormat>> {
        use pmosource::AudioFormat;

        // Extract track ID from object_id
        let track_id = if let Some(id) = object_id.strip_prefix("qobuz://track/") {
            id
        } else {
            object_id
        };

        // Get track details from Qobuz
        let track = self
            .inner
            .client
            .get_track(track_id)
            .await
            .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

        // Qobuz provides multiple formats based on subscription
        let mut formats = vec![];

        // MP3 320 (format_id 5) - available to all
        formats.push(AudioFormat {
            format_id: "mp3-320".to_string(),
            mime_type: "audio/mpeg".to_string(),
            sample_rate: Some(44100),
            bit_depth: None,
            bitrate: Some(320),
            channels: Some(2),
        });

        // FLAC 16/44.1 (format_id 6) - CD quality
        formats.push(AudioFormat {
            format_id: "flac-16-44".to_string(),
            mime_type: "audio/flac".to_string(),
            sample_rate: Some(44100),
            bit_depth: Some(16),
            bitrate: None,
            channels: Some(2),
        });

        // Hi-Res formats (if available for this track)
        if let Some(sample_rate) = track.sample_rate {
            if sample_rate > 44100 {
                // FLAC 24-bit Hi-Res
                let bit_depth = track.bit_depth.map(|d| d as u8).or(Some(24));

                formats.push(AudioFormat {
                    format_id: format!("flac-{}-{}", bit_depth.unwrap_or(24), sample_rate / 1000),
                    mime_type: "audio/flac".to_string(),
                    sample_rate: Some(sample_rate),
                    bit_depth,
                    bitrate: None,
                    channels: track.channels,
                });
            }
        }

        Ok(formats)
    }

    async fn get_cache_status(&self, object_id: &str) -> Result<pmosource::CacheStatus> {
        self.inner.cache_manager.get_cache_status(object_id).await
    }

    async fn cache_item(&self, object_id: &str) -> Result<pmosource::CacheStatus> {
        // Extract track ID
        let track_id = object_id
            .strip_prefix("qobuz://track/")
            .unwrap_or(object_id);

        // Get track details from Qobuz
        let track = self
            .inner
            .client
            .get_track(track_id)
            .await
            .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

        // Add track to cache (via manager)
        let cached_id = self.add_track(&track).await?;

        // Return the cache status
        self.get_cache_status(&cached_id).await
    }

    async fn add_favorite(&self, object_id: &str) -> Result<()> {
        // Parse object_id to determine type
        let parts: Vec<&str> = object_id.split(':').collect();

        match parts.as_slice() {
            ["qobuz", "album", id] | ["qobuz://album", id] => {
                self.inner
                    .client
                    .add_favorite_album(id)
                    .await
                    .map_err(|e| MusicSourceError::FavoritesError(e.to_string()))?;
            }
            ["qobuz", "track", id] | ["qobuz://track", id] => {
                self.inner
                    .client
                    .add_favorite_track(id)
                    .await
                    .map_err(|e| MusicSourceError::FavoritesError(e.to_string()))?;
            }
            _ => {
                return Err(MusicSourceError::NotSupported(
                    "Favorites only supported for albums and tracks".to_string(),
                ));
            }
        }

        self.increment_update_id().await;
        Ok(())
    }

    async fn remove_favorite(&self, object_id: &str) -> Result<()> {
        // Parse object_id to determine type
        let parts: Vec<&str> = object_id.split(':').collect();

        match parts.as_slice() {
            ["qobuz", "album", id] | ["qobuz://album", id] => {
                self.inner
                    .client
                    .remove_favorite_album(id)
                    .await
                    .map_err(|e| MusicSourceError::FavoritesError(e.to_string()))?;
            }
            ["qobuz", "track", id] | ["qobuz://track", id] => {
                self.inner
                    .client
                    .remove_favorite_track(id)
                    .await
                    .map_err(|e| MusicSourceError::FavoritesError(e.to_string()))?;
            }
            _ => {
                return Err(MusicSourceError::NotSupported(
                    "Favorites only supported for albums and tracks".to_string(),
                ));
            }
        }

        self.increment_update_id().await;
        Ok(())
    }

    async fn is_favorite(&self, object_id: &str) -> Result<bool> {
        // Parse object_id to determine type
        let parts: Vec<&str> = object_id.split(':').collect();

        match parts.as_slice() {
            ["qobuz", "album", id] | ["qobuz://album", id] => {
                let favorites = self
                    .inner
                    .client
                    .get_favorite_albums()
                    .await
                    .map_err(|e| MusicSourceError::FavoritesError(e.to_string()))?;

                Ok(favorites.iter().any(|album| album.id == *id))
            }
            ["qobuz", "track", id] | ["qobuz://track", id] => {
                let favorites = self
                    .inner
                    .client
                    .get_favorite_tracks()
                    .await
                    .map_err(|e| MusicSourceError::FavoritesError(e.to_string()))?;

                Ok(favorites.iter().any(|track| track.id == *id))
            }
            _ => Err(MusicSourceError::NotSupported(
                "Favorites only supported for albums and tracks".to_string(),
            )),
        }
    }

    async fn get_user_playlists(&self) -> Result<Vec<Container>> {
        let playlists = self
            .inner
            .client
            .get_user_playlists()
            .await
            .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;

        let containers: Vec<Container> = playlists
            .into_iter()
            .filter_map(|playlist| playlist.to_didl_container("qobuz").ok())
            .collect();

        Ok(containers)
    }

    async fn add_to_playlist(&self, playlist_id: &str, item_id: &str) -> Result<()> {
        // Extract track ID from item_id
        let track_id = if let Some(id) = item_id.strip_prefix("qobuz://track/") {
            id
        } else if let Some(id) = item_id.strip_prefix("qobuz:track:") {
            id
        } else {
            item_id
        };

        self.inner
            .client
            .add_to_playlist(playlist_id, track_id)
            .await
            .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;

        self.increment_update_id().await;
        Ok(())
    }

    async fn get_item_count(&self, object_id: &str) -> Result<usize> {
        match self.parse_object_id(object_id) {
            ObjectIdType::Album(album_id) => {
                let album = self
                    .inner
                    .client
                    .get_album(&album_id)
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                Ok(album.tracks_count.unwrap_or(0) as usize)
            }
            ObjectIdType::Playlist(playlist_id) => {
                let playlist = self
                    .inner
                    .client
                    .get_playlist(&playlist_id)
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                Ok(playlist.tracks_count.unwrap_or(0) as usize)
            }
            _ => {
                // Fall back to default implementation
                let result = self.browse(object_id).await?;
                Ok(result.count())
            }
        }
    }

    async fn browse_paginated(
        &self,
        object_id: &str,
        offset: usize,
        limit: usize,
    ) -> Result<BrowseResult> {
        match self.parse_object_id(object_id) {
            ObjectIdType::Album(album_id) => {
                // Qobuz returns all tracks, so we slice them
                let tracks = self
                    .inner
                    .client
                    .get_album_tracks(&album_id)
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                let items: Vec<Item> = tracks
                    .into_iter()
                    .skip(offset)
                    .take(limit)
                    .filter_map(|track| {
                        track
                            .to_didl_item(&format!("qobuz:album:{}", album_id))
                            .ok()
                    })
                    .collect();

                Ok(BrowseResult::Items(items))
            }
            ObjectIdType::Favorites => {
                let albums = self
                    .inner
                    .client
                    .get_favorite_albums()
                    .await
                    .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;

                let containers: Vec<Container> = albums
                    .into_iter()
                    .skip(offset)
                    .take(limit)
                    .filter_map(|album| album.to_didl_container("qobuz:favorites").ok())
                    .collect();

                Ok(BrowseResult::Containers(containers))
            }
            _ => {
                // Fall back to default implementation
                self.browse(object_id).await
            }
        }
    }

    async fn statistics(&self) -> Result<pmosource::SourceStatistics> {
        let mut stats = pmosource::SourceStatistics::default();

        // Try to get favorite counts
        if let Ok(albums) = self.inner.client.get_favorite_albums().await {
            stats.total_containers = Some(albums.len());
        }

        if let Ok(tracks) = self.inner.client.get_favorite_tracks().await {
            stats.total_items = Some(tracks.len());
        }

        // Get cache statistics from manager
        let cache_stats = self.inner.cache_manager.statistics().await;
        stats.cached_items = Some(cache_stats.cached_tracks);

        Ok(stats)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_image_present() {
        assert!(DEFAULT_IMAGE.len() > 0, "Default image should not be empty");

        // Check WebP magic bytes (RIFF...WEBP)
        assert!(
            DEFAULT_IMAGE.len() >= 12,
            "Image too small to be valid WebP"
        );
        assert_eq!(&DEFAULT_IMAGE[0..4], b"RIFF", "Missing RIFF header");
        assert_eq!(&DEFAULT_IMAGE[8..12], b"WEBP", "Missing WEBP signature");
    }

    // Note: We can't easily test parse_object_id without creating a real client
    // which requires authentication. The parsing logic is simple enough that
    // it's covered by integration tests.
}
========= End of pmoqobuz/src/source.rs ===========

=============== pmoqobuz/src/api_rest.rs ============
//! Endpoints API REST pour Qobuz
//!
//! Ce module définit les handlers HTTP pour accéder aux fonctionnalités Qobuz.

#[cfg(feature = "pmoserver")]
use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::{IntoResponse, Response},
    Json, Router,
};

#[cfg(feature = "pmoserver")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "pmoserver")]
use std::sync::Arc;

#[cfg(feature = "pmoserver")]
use crate::{client::QobuzClient, error::QobuzError, models::*};

/// État partagé de l'application
#[cfg(feature = "pmoserver")]
#[derive(Clone)]
pub struct QobuzState {
    pub client: Arc<QobuzClient>,
    #[cfg(feature = "covers")]
    pub cover_cache: Option<Arc<pmocovers::Cache>>,
}

/// Paramètres de recherche
#[cfg(feature = "pmoserver")]
#[derive(Debug, Deserialize)]
pub struct SearchParams {
    /// Requête de recherche
    pub q: String,
    /// Type de recherche (albums, artists, tracks, playlists)
    #[serde(rename = "type")]
    pub search_type: Option<String>,
}

/// Paramètres pour featured albums
#[cfg(feature = "pmoserver")]
#[derive(Debug, Deserialize)]
pub struct FeaturedAlbumsParams {
    /// ID du genre (optionnel)
    pub genre_id: Option<String>,
    /// Type (new-releases, ideal-discography, etc.)
    #[serde(rename = "type", default = "default_featured_type")]
    pub type_: String,
}

#[cfg(feature = "pmoserver")]
fn default_featured_type() -> String {
    "new-releases".to_string()
}

/// Paramètres pour featured playlists
#[cfg(feature = "pmoserver")]
#[derive(Debug, Deserialize)]
pub struct FeaturedPlaylistsParams {
    /// ID du genre (optionnel)
    pub genre_id: Option<String>,
    /// Tags (optionnel)
    pub tags: Option<String>,
}

/// Crée le router Axum avec tous les endpoints Qobuz
#[cfg(feature = "pmoserver")]
pub fn create_router(state: QobuzState) -> Router {
    Router::new()
        // Albums
        .route("/albums/:id", axum::routing::get(get_album))
        .route("/albums/:id/tracks", axum::routing::get(get_album_tracks))
        // Tracks
        .route("/tracks/:id", axum::routing::get(get_track))
        .route("/tracks/:id/stream", axum::routing::get(get_stream_url))
        // Artists
        .route("/artists/:id/albums", axum::routing::get(get_artist_albums))
        .route(
            "/artists/:id/similar",
            axum::routing::get(get_similar_artists),
        )
        // Playlists
        .route("/playlists/:id", axum::routing::get(get_playlist))
        .route(
            "/playlists/:id/tracks",
            axum::routing::get(get_playlist_tracks),
        )
        // Recherche
        .route("/search", axum::routing::get(search))
        // Favoris
        .route("/favorites/albums", axum::routing::get(get_favorite_albums))
        .route(
            "/favorites/artists",
            axum::routing::get(get_favorite_artists),
        )
        .route("/favorites/tracks", axum::routing::get(get_favorite_tracks))
        .route(
            "/favorites/playlists",
            axum::routing::get(get_user_playlists),
        )
        // Catalogue
        .route("/genres", axum::routing::get(get_genres))
        .route("/featured/albums", axum::routing::get(get_featured_albums))
        .route(
            "/featured/playlists",
            axum::routing::get(get_featured_playlists),
        )
        // Cache
        .route("/cache/stats", axum::routing::get(get_cache_stats))
        .with_state(state)
}

// ============ Handlers ============

#[cfg(feature = "pmoserver")]
async fn get_album(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Album>, AppError> {
    let mut album = state.client.get_album(&id).await?;

    #[cfg(feature = "covers")]
    if let Some(ref cover_cache) = state.cover_cache {
        album = cache_album_image(album, cover_cache).await;
    }

    Ok(Json(album))
}

#[cfg(feature = "pmoserver")]
async fn get_album_tracks(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Vec<Track>>, AppError> {
    let tracks = state.client.get_album_tracks(&id).await?;
    Ok(Json(tracks))
}

#[cfg(feature = "pmoserver")]
async fn get_track(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Track>, AppError> {
    let track = state.client.get_track(&id).await?;
    Ok(Json(track))
}

#[cfg(feature = "pmoserver")]
async fn get_stream_url(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, AppError> {
    let url = state.client.get_stream_url(&id).await?;
    Ok(Json(serde_json::json!({ "url": url })))
}

#[cfg(feature = "pmoserver")]
async fn get_artist_albums(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Vec<Album>>, AppError> {
    let mut albums = state.client.get_artist_albums(&id).await?;

    #[cfg(feature = "covers")]
    if let Some(ref cover_cache) = state.cover_cache {
        albums = cache_albums_images(albums, cover_cache).await;
    }

    Ok(Json(albums))
}

#[cfg(feature = "pmoserver")]
async fn get_similar_artists(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Vec<Artist>>, AppError> {
    let artists = state.client.get_similar_artists(&id).await?;
    Ok(Json(artists))
}

#[cfg(feature = "pmoserver")]
async fn get_playlist(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Playlist>, AppError> {
    let playlist = state.client.get_playlist(&id).await?;
    Ok(Json(playlist))
}

#[cfg(feature = "pmoserver")]
async fn get_playlist_tracks(
    State(state): State<QobuzState>,
    Path(id): Path<String>,
) -> Result<Json<Vec<Track>>, AppError> {
    let tracks = state.client.get_playlist_tracks(&id).await?;
    Ok(Json(tracks))
}

#[cfg(feature = "pmoserver")]
async fn search(
    State(state): State<QobuzState>,
    Query(params): Query<SearchParams>,
) -> Result<Json<SearchResult>, AppError> {
    let mut result = state
        .client
        .search(&params.q, params.search_type.as_deref())
        .await?;

    #[cfg(feature = "covers")]
    if let Some(ref cover_cache) = state.cover_cache {
        result.albums = cache_albums_images(result.albums, cover_cache).await;
    }

    Ok(Json(result))
}

#[cfg(feature = "pmoserver")]
async fn get_favorite_albums(
    State(state): State<QobuzState>,
) -> Result<Json<Vec<Album>>, AppError> {
    let mut albums = state.client.get_favorite_albums().await?;

    #[cfg(feature = "covers")]
    if let Some(ref cover_cache) = state.cover_cache {
        albums = cache_albums_images(albums, cover_cache).await;
    }

    Ok(Json(albums))
}

#[cfg(feature = "pmoserver")]
async fn get_favorite_artists(
    State(state): State<QobuzState>,
) -> Result<Json<Vec<Artist>>, AppError> {
    let artists = state.client.get_favorite_artists().await?;
    Ok(Json(artists))
}

#[cfg(feature = "pmoserver")]
async fn get_favorite_tracks(
    State(state): State<QobuzState>,
) -> Result<Json<Vec<Track>>, AppError> {
    let tracks = state.client.get_favorite_tracks().await?;
    Ok(Json(tracks))
}

#[cfg(feature = "pmoserver")]
async fn get_user_playlists(
    State(state): State<QobuzState>,
) -> Result<Json<Vec<Playlist>>, AppError> {
    let playlists = state.client.get_user_playlists().await?;
    Ok(Json(playlists))
}

#[cfg(feature = "pmoserver")]
async fn get_genres(State(state): State<QobuzState>) -> Result<Json<Vec<Genre>>, AppError> {
    let genres = state.client.get_genres().await?;
    Ok(Json(genres))
}

#[cfg(feature = "pmoserver")]
async fn get_featured_albums(
    State(state): State<QobuzState>,
    Query(params): Query<FeaturedAlbumsParams>,
) -> Result<Json<Vec<Album>>, AppError> {
    let mut albums = state
        .client
        .get_featured_albums(params.genre_id.as_deref(), &params.type_)
        .await?;

    #[cfg(feature = "covers")]
    if let Some(ref cover_cache) = state.cover_cache {
        albums = cache_albums_images(albums, cover_cache).await;
    }

    Ok(Json(albums))
}

#[cfg(feature = "pmoserver")]
async fn get_featured_playlists(
    State(state): State<QobuzState>,
    Query(params): Query<FeaturedPlaylistsParams>,
) -> Result<Json<Vec<Playlist>>, AppError> {
    let playlists = state
        .client
        .get_featured_playlists(params.genre_id.as_deref(), params.tags.as_deref())
        .await?;
    Ok(Json(playlists))
}

#[cfg(feature = "pmoserver")]
async fn get_cache_stats(
    State(state): State<QobuzState>,
) -> Result<Json<crate::cache::CacheStats>, AppError> {
    let stats = state.client.cache().stats().await;
    Ok(Json(stats))
}

// ============ Helpers pour le cache d'images ============

#[cfg(all(feature = "pmoserver", feature = "covers"))]
async fn cache_album_image(mut album: Album, cover_cache: &Arc<pmocovers::Cache>) -> Album {
    if let Some(ref image_url) = album.image {
        match cover_cache.add_from_url(image_url, None).await {
            Ok(pk) => {
                album.image_cached = Some(format!("/covers/images/{}", pk));
            }
            Err(e) => {
                tracing::warn!("Failed to cache album image: {}", e);
            }
        }
    }
    album
}

#[cfg(all(feature = "pmoserver", feature = "covers"))]
async fn cache_albums_images(
    albums: Vec<Album>,
    cover_cache: &Arc<pmocovers::Cache>,
) -> Vec<Album> {
    let mut cached_albums = Vec::with_capacity(albums.len());
    for album in albums {
        cached_albums.push(cache_album_image(album, cover_cache).await);
    }
    cached_albums
}

// ============ Gestion des erreurs ============

#[cfg(feature = "pmoserver")]
struct AppError(QobuzError);

#[cfg(feature = "pmoserver")]
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self.0 {
            QobuzError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, self.0.to_string()),
            QobuzError::NotFound(_) => (StatusCode::NOT_FOUND, self.0.to_string()),
            QobuzError::RateLimitExceeded => (StatusCode::TOO_MANY_REQUESTS, self.0.to_string()),
            _ => (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()),
        };

        let body = Json(serde_json::json!({
            "error": message
        }));

        (status, body).into_response()
    }
}

#[cfg(feature = "pmoserver")]
impl<E> From<E> for AppError
where
    E: Into<QobuzError>,
{
    fn from(err: E) -> Self {
        Self(err.into())
    }
}
========= End of pmoqobuz/src/api_rest.rs ===========

=============== pmoqobuz/src/pmoserver_impl.rs ============
//! Implémentation du trait QobuzServerExt pour pmoserver::Server
//!
//! Ce module enrichit `pmoserver::Server` avec les fonctionnalités du client Qobuz en
//! implémentant le trait [`QobuzServerExt`](crate::QobuzServerExt). Cette implémentation
//! permet d'initialiser facilement le client Qobuz et d'enregistrer les routes HTTP.
//!
//! ## Architecture
//!
//! `pmoqobuz` étend `pmoserver::Server` sans que `pmoserver` connaisse `pmoqobuz`.
//! C'est le pattern d'extension : `pmoqobuz` ajoute des fonctionnalités à un type
//! externe via un trait, similaire au pattern utilisé par `pmocovers` pour `CoverCacheExt`.
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoqobuz::QobuzServerExt;
//! use pmoserver::ServerBuilder;
//!
//! # async fn example() -> anyhow::Result<()> {
//! let mut server = ServerBuilder::new_configured().build();
//!
//! // Le trait QobuzServerExt est automatiquement disponible
//! let client = server.init_qobuz_client_configured().await?;
//!
//! server.start().await;
//! # Ok(())
//! # }
//! ```

use crate::api_rest::{create_router, QobuzState};
use crate::client::QobuzClient;
use crate::pmoserver_ext::QobuzServerExt;
use anyhow::Result;
use pmoconfig::Config;
use pmoserver::Server;
use std::sync::Arc;
use tracing::info;

impl QobuzServerExt for Server {
    async fn init_qobuz_client(
        &mut self,
        username: &str,
        password: &str,
    ) -> Result<Arc<QobuzClient>> {
        info!("Initializing Qobuz client for user: {}", username);

        // Créer le client Qobuz
        let client = QobuzClient::new(username, password).await?;
        let client = Arc::new(client);

        // Créer l'état de l'API sans cache d'images
        let state = QobuzState {
            client: client.clone(),
            #[cfg(feature = "covers")]
            cover_cache: None,
        };

        // Créer le router et l'enregistrer
        let router = create_router(state);
        self.add_router("/qobuz", router).await;

        info!("Qobuz client initialized successfully");
        info!("API endpoints available at /qobuz/*");

        Ok(client)
    }

    async fn init_qobuz_client_configured(&mut self) -> Result<Arc<QobuzClient>> {
        info!("Initializing Qobuz client from configuration");

        // Récupérer les credentials depuis la config
        let config = pmoconfig::get_config();
        let (username, password) = config.get_qobuz_credentials()?;

        self.init_qobuz_client(&username, &password).await
    }

    #[cfg(feature = "covers")]
    async fn init_qobuz_client_with_covers(
        &mut self,
        username: &str,
        password: &str,
        cover_cache: Arc<pmocovers::Cache>,
    ) -> Result<Arc<QobuzClient>> {
        info!("Initializing Qobuz client with pmocovers integration");

        // Créer le client Qobuz
        let client = QobuzClient::new(username, password).await?;
        let client = Arc::new(client);

        info!("pmocovers integration enabled - album images will be cached automatically");

        // Créer l'état de l'API avec le cache
        let state = QobuzState {
            client: client.clone(),
            cover_cache: Some(cover_cache),
        };

        // Créer le router et l'enregistrer
        let router = create_router(state);
        self.add_router("/qobuz", router).await;

        info!("Qobuz client initialized successfully with covers");
        info!("API endpoints available at /qobuz/*");

        Ok(client)
    }

    #[cfg(feature = "covers")]
    async fn init_qobuz_client_configured_with_covers(
        &mut self,
        cover_cache: Arc<pmocovers::Cache>,
    ) -> Result<Arc<QobuzClient>> {
        info!("Initializing Qobuz client from configuration with pmocovers");

        // Récupérer les credentials depuis la config
        let config = pmoconfig::get_config();
        let (username, password) = config.get_qobuz_credentials()?;

        self.init_qobuz_client_with_covers(&username, &password, cover_cache)
            .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_trait_implemented() {
        // Ce test vérifie simplement que le trait est bien implémenté
        // Les tests fonctionnels nécessiteraient un serveur et des credentials réels
    }
}
========= End of pmoqobuz/src/pmoserver_impl.rs ===========

=============== pmoqobuz/src/api/auth.rs ============
//! Module d'authentification pour l'API Qobuz

use super::QobuzApi;
use crate::error::{QobuzError, Result};
use serde::{Deserialize, Serialize};
use tracing::{debug, info};

/// Réponse de l'endpoint /user/login
#[derive(Debug, Deserialize)]
struct LoginResponse {
    user: UserInfo,
    user_auth_token: String,
}

/// Informations utilisateur retournées par l'API
#[derive(Debug, Deserialize)]
struct UserInfo {
    id: u64,
    #[serde(default)]
    email: Option<String>,
    #[serde(default)]
    firstname: Option<String>,
    #[serde(default)]
    lastname: Option<String>,
    credential: CredentialInfo,
}

/// Informations sur les credentials de l'utilisateur
#[derive(Debug, Deserialize)]
struct CredentialInfo {
    #[serde(default)]
    parameters: Option<CredentialParameters>,
}

/// Paramètres du niveau d'abonnement
#[derive(Debug, Deserialize)]
struct CredentialParameters {
    #[serde(default)]
    short_label: Option<String>,
}

/// Informations d'authentification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthInfo {
    /// Token d'authentification
    pub token: String,
    /// ID utilisateur
    pub user_id: String,
    /// Label de l'abonnement (ex: "Studio", "Hi-Fi", etc.)
    pub subscription_label: Option<String>,
}

impl QobuzApi {
    /// Authentifie l'utilisateur avec username et password
    ///
    /// # Arguments
    ///
    /// * `username` - Email ou nom d'utilisateur Qobuz
    /// * `password` - Mot de passe
    ///
    /// # Returns
    ///
    /// Retourne les informations d'authentification si le login est réussi
    ///
    /// # Errors
    ///
    /// * `QobuzError::Unauthorized` - Credentials invalides
    /// * `QobuzError::SubscriptionRequired` - Compte gratuit (non éligible)
    pub async fn login(&mut self, username: &str, password: &str) -> Result<AuthInfo> {
        info!("Attempting to login to Qobuz as {}", username);

        let params = [("username", username), ("password", password)];

        let response: LoginResponse = self.post("/user/login", &params).await?;

        // Vérifier que l'utilisateur a un abonnement valide
        if response.user.credential.parameters.is_none() {
            return Err(QobuzError::SubscriptionRequired(
                "Free accounts are not eligible for streaming".to_string(),
            ));
        }

        let user_id = response.user.id.to_string();
        let subscription_label = response
            .user
            .credential
            .parameters
            .and_then(|p| p.short_label);

        debug!(
            "Login successful - User ID: {}, Subscription: {:?}",
            user_id, subscription_label
        );

        // Stocker les informations d'authentification
        self.set_auth_token(response.user_auth_token.clone(), user_id.clone());

        Ok(AuthInfo {
            token: response.user_auth_token,
            user_id,
            subscription_label,
        })
    }

    /// Vérifie si le client est authentifié
    pub fn is_authenticated(&self) -> bool {
        self.user_auth_token.is_some() && self.user_id.is_some()
    }

    /// Déconnecte l'utilisateur
    pub fn logout(&mut self) {
        debug!("Logging out");
        self.user_auth_token = None;
        self.user_id = None;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_authenticated() {
        let mut api = QobuzApi::new("test_app_id").unwrap();
        assert!(!api.is_authenticated());

        api.set_auth_token("token".to_string(), "user123".to_string());
        assert!(api.is_authenticated());

        api.logout();
        assert!(!api.is_authenticated());
    }
}
========= End of pmoqobuz/src/api/auth.rs ===========

=============== pmoqobuz/src/api/catalog.rs ============
//! Module d'accès au catalogue Qobuz (albums, tracks, artistes, playlists)

use super::QobuzApi;
use crate::error::Result;
use crate::models::*;
use serde::Deserialize;
use tracing::debug;

/// Réponse paginée de l'API
#[derive(Debug, Deserialize)]
struct PaginatedResponse<T> {
    items: Vec<T>,
    #[serde(default)]
    total: Option<u32>,
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
}

/// Réponse de l'endpoint /album/get
#[derive(Debug, Deserialize)]
pub(crate) struct AlbumResponse {
    id: String,
    title: String,
    artist: ArtistResponse,
    #[serde(default)]
    tracks_count: Option<u32>,
    #[serde(default)]
    duration: Option<u32>,
    #[serde(default)]
    release_date_original: Option<String>,
    #[serde(default)]
    image: Option<ImageResponse>,
    #[serde(default = "default_streamable")]
    streamable: bool,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    maximum_sampling_rate: Option<f64>,
    #[serde(default)]
    maximum_bit_depth: Option<u32>,
    #[serde(default)]
    genre: Option<GenreResponse>,
    #[serde(default)]
    label: Option<LabelResponse>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
}

/// Réponse de l'endpoint /track/get
#[derive(Debug, Deserialize)]
pub(crate) struct TrackResponse {
    id: String,
    title: String,
    #[serde(default)]
    performer: Option<ArtistResponse>,
    #[serde(default)]
    artist: Option<ArtistResponse>,
    #[serde(default)]
    album: Option<AlbumResponse>,
    duration: u32,
    track_number: u32,
    media_number: u32,
    #[serde(default = "default_streamable")]
    streamable: bool,
}

/// Réponse artiste
#[derive(Debug, Deserialize)]
pub(crate) struct ArtistResponse {
    id: u64,
    name: String,
    #[serde(default)]
    image: Option<ImageResponse>,
    #[serde(default)]
    albums: Option<PaginatedResponse<AlbumResponse>>,
}

/// Réponse image
#[derive(Debug, Deserialize)]
struct ImageResponse {
    #[serde(default)]
    large: Option<String>,
}

/// Réponse genre
#[derive(Debug, Deserialize)]
struct GenreResponse {
    #[serde(default)]
    id: Option<u32>,
    name: String,
}

/// Réponse label
#[derive(Debug, Deserialize)]
struct LabelResponse {
    name: String,
}

/// Réponse playlist
#[derive(Debug, Deserialize)]
pub(crate) struct PlaylistResponse {
    id: u64,
    name: String,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    tracks_count: Option<u32>,
    #[serde(default)]
    duration: Option<u32>,
    #[serde(default)]
    images300: Option<Vec<String>>,
    #[serde(default)]
    is_public: bool,
    #[serde(default)]
    owner: Option<OwnerResponse>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
}

/// Réponse propriétaire
#[derive(Debug, Deserialize)]
struct OwnerResponse {
    id: u64,
    name: String,
}

/// Réponse genres list
#[derive(Debug, Deserialize)]
struct GenresResponse {
    genres: PaginatedResponse<GenreResponse>,
}

/// Réponse albums featured
#[derive(Debug, Deserialize)]
struct FeaturedAlbumsResponse {
    albums: PaginatedResponse<AlbumResponse>,
}

/// Réponse playlists featured
#[derive(Debug, Deserialize)]
struct FeaturedPlaylistsResponse {
    playlists: PaginatedResponse<PlaylistResponse>,
}

/// Réponse search
#[derive(Debug, Deserialize)]
struct SearchResponse {
    #[serde(default)]
    albums: Option<PaginatedResponse<AlbumResponse>>,
    #[serde(default)]
    artists: Option<PaginatedResponse<ArtistResponse>>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
    #[serde(default)]
    playlists: Option<PaginatedResponse<PlaylistResponse>>,
}

/// Réponse track file URL
#[derive(Debug, Deserialize)]
struct FileUrlResponse {
    url: String,
    mime_type: String,
    sampling_rate: u32,
    bit_depth: u32,
    format_id: u8,
}

fn default_streamable() -> bool {
    true
}

impl QobuzApi {
    /// Récupère les détails d'un album
    pub async fn get_album(&self, album_id: &str) -> Result<Album> {
        debug!("Fetching album {}", album_id);
        let params = [("album_id", album_id)];
        let response: AlbumResponse = self.get("/album/get", &params).await?;
        Ok(Self::parse_album(response))
    }

    /// Récupère les tracks d'un album
    pub async fn get_album_tracks(&self, album_id: &str) -> Result<Vec<Track>> {
        debug!("Fetching tracks for album {}", album_id);
        let params = [("album_id", album_id)];
        let mut response: AlbumResponse = self.get("/album/get", &params).await?;

        if let Some(tracks) = response.tracks.take() {
            let album = Self::parse_album(response);
            Ok(tracks
                .items
                .into_iter()
                .map(|t| Self::parse_track(t, Some(album.clone())))
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les détails d'une track
    pub async fn get_track(&self, track_id: &str) -> Result<Track> {
        debug!("Fetching track {}", track_id);
        let params = [("track_id", track_id)];
        let response: TrackResponse = self.get("/track/get", &params).await?;
        Ok(Self::parse_track(response, None))
    }

    /// Récupère l'URL de streaming d'une track
    pub async fn get_file_url(&self, track_id: &str) -> Result<StreamInfo> {
        debug!("Fetching file URL for track {}", track_id);
        let format_id = self.format_id.id().to_string();
        let params = [
            ("track_id", track_id),
            ("format_id", &format_id),
            ("intent", "stream"),
        ];
        let response: FileUrlResponse = self.get("/track/getFileUrl", &params).await?;

        Ok(StreamInfo {
            url: response.url,
            mime_type: response.mime_type,
            sampling_rate: response.sampling_rate,
            bit_depth: response.bit_depth,
            format_id: response.format_id,
            expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
        })
    }

    /// Récupère les albums d'un artiste
    pub async fn get_artist_albums(&self, artist_id: &str) -> Result<Vec<Album>> {
        debug!("Fetching albums for artist {}", artist_id);
        let params = [("artist_id", artist_id), ("extra", "albums")];
        let response: ArtistResponse = self.get("/artist/get", &params).await?;

        if let Some(albums) = response.albums {
            Ok(albums
                .items
                .into_iter()
                .map(Self::parse_album)
                .filter(|a| a.streamable)
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les artistes similaires
    pub async fn get_similar_artists(&self, artist_id: &str) -> Result<Vec<Artist>> {
        debug!("Fetching similar artists for {}", artist_id);
        let params = [("artist_id", artist_id)];

        #[derive(Debug, Deserialize)]
        struct SimilarArtistsResponse {
            artists: PaginatedResponse<ArtistResponse>,
        }

        let response: SimilarArtistsResponse =
            self.get("/artist/getSimilarArtists", &params).await?;
        Ok(response
            .artists
            .items
            .into_iter()
            .map(Self::parse_artist)
            .collect())
    }

    /// Récupère les détails d'une playlist
    pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
        debug!("Fetching playlist {}", playlist_id);
        let params = [("playlist_id", playlist_id)];
        let response: PlaylistResponse = self.get("/playlist/get", &params).await?;
        Ok(Self::parse_playlist(response))
    }

    /// Récupère les tracks d'une playlist
    pub async fn get_playlist_tracks(&self, playlist_id: &str) -> Result<Vec<Track>> {
        debug!("Fetching tracks for playlist {}", playlist_id);
        let params = [("playlist_id", playlist_id), ("extra", "tracks")];
        let response: PlaylistResponse = self.get("/playlist/get", &params).await?;

        if let Some(tracks) = response.tracks {
            Ok(tracks
                .items
                .into_iter()
                .map(|t| Self::parse_track(t, None))
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère la liste des genres
    pub async fn get_genres(&self) -> Result<Vec<Genre>> {
        debug!("Fetching genres");
        let response: GenresResponse = self.get("/genre/list", &[]).await?;
        Ok(response
            .genres
            .items
            .into_iter()
            .map(Self::parse_genre)
            .collect())
    }

    /// Récupère les albums featured (nouveautés, éditeur, etc.)
    pub async fn get_featured_albums(
        &self,
        genre_id: Option<&str>,
        type_: &str,
    ) -> Result<Vec<Album>> {
        debug!("Fetching featured albums (type: {})", type_);
        let mut params = vec![("type", type_), ("limit", "100")];

        if let Some(gid) = genre_id {
            params.push(("genre_ids", gid));
        }

        let response: FeaturedAlbumsResponse = self.get("/album/getFeatured", &params).await?;
        Ok(response
            .albums
            .items
            .into_iter()
            .map(Self::parse_album)
            .filter(|a| a.streamable)
            .collect())
    }

    /// Récupère les playlists featured
    pub async fn get_featured_playlists(
        &self,
        genre_id: Option<&str>,
        tags: Option<&str>,
    ) -> Result<Vec<Playlist>> {
        debug!("Fetching featured playlists");
        let mut params = vec![("type", "editor-picks"), ("limit", "100")];

        if let Some(gid) = genre_id {
            params.push(("genre_ids", gid));
        }
        if let Some(t) = tags {
            params.push(("tags", t));
        }

        let response: FeaturedPlaylistsResponse =
            self.get("/playlist/getFeatured", &params).await?;
        Ok(response
            .playlists
            .items
            .into_iter()
            .map(Self::parse_playlist)
            .collect())
    }

    /// Recherche dans le catalogue
    pub async fn search(&self, query: &str, type_: Option<&str>) -> Result<SearchResult> {
        debug!("Searching for '{}' (type: {:?})", query, type_);
        let mut params = vec![("query", query), ("limit", "200")];

        if let Some(t) = type_ {
            params.push(("type", t));
        }

        let response: SearchResponse = self.get("/catalog/search", &params).await?;

        Ok(SearchResult {
            albums: response
                .albums
                .map(|a| {
                    a.items
                        .into_iter()
                        .map(Self::parse_album)
                        .filter(|album| album.streamable)
                        .collect()
                })
                .unwrap_or_default(),
            artists: response
                .artists
                .map(|a| a.items.into_iter().map(Self::parse_artist).collect())
                .unwrap_or_default(),
            tracks: response
                .tracks
                .map(|t| {
                    t.items
                        .into_iter()
                        .map(|track| Self::parse_track(track, None))
                        .filter(|track| track.streamable)
                        .collect()
                })
                .unwrap_or_default(),
            playlists: response
                .playlists
                .map(|p| p.items.into_iter().map(Self::parse_playlist).collect())
                .unwrap_or_default(),
        })
    }

    // Fonctions de parsing publiques (utilisées aussi par le module user)

    pub(crate) fn parse_album(response: AlbumResponse) -> Album {
        Album {
            id: response.id,
            title: response.title,
            artist: Self::parse_artist(response.artist),
            tracks_count: response.tracks_count,
            duration: response.duration,
            release_date: response.release_date_original,
            image: response.image.and_then(|i| i.large),
            image_cached: None,
            streamable: response.streamable,
            description: response.description,
            maximum_sampling_rate: response.maximum_sampling_rate,
            maximum_bit_depth: response.maximum_bit_depth,
            genres: response.genre.map(|g| vec![g.name]).unwrap_or_default(),
            label: response.label.map(|l| l.name),
        }
    }

    pub(crate) fn parse_track(response: TrackResponse, album: Option<Album>) -> Track {
        let performer = response
            .performer
            .or(response.artist)
            .map(Self::parse_artist);

        let album = album.or_else(|| response.album.map(Self::parse_album));

        Track {
            id: response.id,
            title: response.title,
            performer,
            album,
            duration: response.duration,
            track_number: response.track_number,
            media_number: response.media_number,
            streamable: response.streamable,
            mime_type: None,
            sample_rate: None,
            bit_depth: None,
            channels: None,
        }
    }

    pub(crate) fn parse_artist(response: ArtistResponse) -> Artist {
        Artist {
            id: response.id.to_string(),
            name: response.name,
            image: response.image.and_then(|i| i.large),
            image_cached: None,
        }
    }

    pub(crate) fn parse_playlist(response: PlaylistResponse) -> Playlist {
        Playlist {
            id: response.id.to_string(),
            name: response.name,
            description: response.description,
            tracks_count: response.tracks_count,
            duration: response.duration,
            image: response.images300.and_then(|imgs| imgs.first().cloned()),
            image_cached: None,
            is_public: response.is_public,
            owner: response.owner.map(|o| PlaylistOwner {
                id: o.id,
                name: o.name,
            }),
        }
    }

    pub(crate) fn parse_genre(response: GenreResponse) -> Genre {
        Genre {
            id: response.id,
            name: response.name,
            children: Vec::new(),
        }
    }
}
========= End of pmoqobuz/src/api/catalog.rs ===========

=============== pmoqobuz/src/api/user.rs ============
//! Module d'accès aux données utilisateur (favoris)

use super::catalog::{AlbumResponse, ArtistResponse, PlaylistResponse, TrackResponse};
use super::QobuzApi;
use crate::error::{QobuzError, Result};
use crate::models::*;
use serde::Deserialize;
use tracing::debug;

/// Réponse paginée
#[derive(Debug, Deserialize)]
struct PaginatedResponse<T> {
    items: Vec<T>,
}

/// Réponse de l'endpoint /favorite/getUserFavorites
#[derive(Debug, Deserialize)]
struct FavoritesResponse {
    #[serde(default)]
    albums: Option<PaginatedResponse<AlbumResponse>>,
    #[serde(default)]
    artists: Option<PaginatedResponse<ArtistResponse>>,
    #[serde(default)]
    tracks: Option<PaginatedResponse<TrackResponse>>,
}

/// Réponse de l'endpoint /playlist/getUserPlaylists
#[derive(Debug, Deserialize)]
struct UserPlaylistsResponse {
    playlists: PaginatedResponse<PlaylistResponse>,
}

impl QobuzApi {
    /// Vérifie que l'utilisateur est authentifié
    fn ensure_authenticated(&self) -> Result<&str> {
        self.user_id
            .as_deref()
            .ok_or_else(|| QobuzError::Unauthorized("Not authenticated".to_string()))
    }

    /// Récupère les albums favoris de l'utilisateur
    pub async fn get_favorite_albums(&self) -> Result<Vec<Album>> {
        let user_id = self.ensure_authenticated()?;
        debug!("Fetching favorite albums for user {}", user_id);

        let params = [("user_id", user_id), ("type", "albums"), ("limit", "1000")];

        let response: FavoritesResponse = self.get("/favorite/getUserFavorites", &params).await?;

        if let Some(albums) = response.albums {
            Ok(albums
                .items
                .into_iter()
                .map(QobuzApi::parse_album)
                .filter(|a| a.streamable)
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les artistes favoris de l'utilisateur
    pub async fn get_favorite_artists(&self) -> Result<Vec<Artist>> {
        let user_id = self.ensure_authenticated()?;
        debug!("Fetching favorite artists for user {}", user_id);

        let params = [("user_id", user_id), ("type", "artists"), ("limit", "1000")];

        let response: FavoritesResponse = self.get("/favorite/getUserFavorites", &params).await?;

        if let Some(artists) = response.artists {
            Ok(artists
                .items
                .into_iter()
                .map(QobuzApi::parse_artist)
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les tracks favorites de l'utilisateur
    pub async fn get_favorite_tracks(&self) -> Result<Vec<Track>> {
        let user_id = self.ensure_authenticated()?;
        debug!("Fetching favorite tracks for user {}", user_id);

        let params = [("user_id", user_id), ("type", "tracks"), ("limit", "1000")];

        let response: FavoritesResponse = self.get("/favorite/getUserFavorites", &params).await?;

        if let Some(tracks) = response.tracks {
            Ok(tracks
                .items
                .into_iter()
                .map(|t| QobuzApi::parse_track(t, None))
                .filter(|t| t.streamable)
                .collect())
        } else {
            Ok(Vec::new())
        }
    }

    /// Récupère les playlists de l'utilisateur
    pub async fn get_user_playlists(&self) -> Result<Vec<Playlist>> {
        let user_id = self.ensure_authenticated()?;
        debug!("Fetching playlists for user {}", user_id);

        let params = [("user_id", user_id), ("limit", "1000")];

        let response: UserPlaylistsResponse =
            self.get("/playlist/getUserPlaylists", &params).await?;

        Ok(response
            .playlists
            .items
            .into_iter()
            .map(QobuzApi::parse_playlist)
            .collect())
    }

    /// Ajoute un album aux favoris
    pub async fn add_favorite_album(&self, album_id: &str) -> Result<()> {
        let user_id = self.ensure_authenticated()?;
        debug!(
            "Adding album {} to favorites for user {}",
            album_id, user_id
        );

        let params = [("album_id", album_id), ("user_id", user_id)];

        self.get::<serde_json::Value>("/favorite/create", &params)
            .await?;
        Ok(())
    }

    /// Supprime un album des favoris
    pub async fn remove_favorite_album(&self, album_id: &str) -> Result<()> {
        let user_id = self.ensure_authenticated()?;
        debug!(
            "Removing album {} from favorites for user {}",
            album_id, user_id
        );

        let params = [("album_ids", album_id), ("user_id", user_id)];

        self.get::<serde_json::Value>("/favorite/delete", &params)
            .await?;
        Ok(())
    }

    /// Ajoute un track aux favoris
    pub async fn add_favorite_track(&self, track_id: &str) -> Result<()> {
        let user_id = self.ensure_authenticated()?;
        debug!(
            "Adding track {} to favorites for user {}",
            track_id, user_id
        );

        let params = [("track_id", track_id), ("user_id", user_id)];

        self.get::<serde_json::Value>("/favorite/create", &params)
            .await?;
        Ok(())
    }

    /// Supprime un track des favoris
    pub async fn remove_favorite_track(&self, track_id: &str) -> Result<()> {
        let user_id = self.ensure_authenticated()?;
        debug!(
            "Removing track {} from favorites for user {}",
            track_id, user_id
        );

        let params = [("track_ids", track_id), ("user_id", user_id)];

        self.get::<serde_json::Value>("/favorite/delete", &params)
            .await?;
        Ok(())
    }

    /// Ajoute un track à une playlist
    pub async fn add_to_playlist(&self, playlist_id: &str, track_id: &str) -> Result<()> {
        let user_id = self.ensure_authenticated()?;
        debug!(
            "Adding track {} to playlist {} for user {}",
            track_id, playlist_id, user_id
        );

        let params = [("playlist_id", playlist_id), ("track_ids", track_id)];

        self.get::<serde_json::Value>("/playlist/addTracks", &params)
            .await?;
        Ok(())
    }
}
========= End of pmoqobuz/src/api/user.rs ===========

=============== pmoqobuz/src/api/mod.rs ============
//! Couche d'accès à l'API REST Qobuz
//!
//! Ce module fournit une interface bas-niveau pour communiquer avec l'API Qobuz.

pub mod auth;
pub mod catalog;
pub mod user;

use crate::error::{QobuzError, Result};
use crate::models::AudioFormat;
use reqwest::{Client, Response};
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::time::Duration;
use tracing::{debug, warn};

/// URL de base de l'API Qobuz
const API_BASE_URL: &str = "https://www.qobuz.com/api.json/0.2";

/// Client API bas-niveau pour communiquer avec Qobuz
pub struct QobuzApi {
    /// Client HTTP
    client: Client,
    /// App ID pour l'authentification
    app_id: String,
    /// Token d'authentification utilisateur
    user_auth_token: Option<String>,
    /// ID utilisateur
    user_id: Option<String>,
    /// Format audio par défaut
    format_id: AudioFormat,
}

impl QobuzApi {
    /// Crée une nouvelle instance de l'API
    pub fn new(app_id: impl Into<String>) -> Result<Self> {
        let client = Client::builder()
            .timeout(Duration::from_secs(30))
            .user_agent(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0",
            )
            .build()?;

        Ok(Self {
            client,
            app_id: app_id.into(),
            user_auth_token: None,
            user_id: None,
            format_id: AudioFormat::default(),
        })
    }

    /// Définit le token d'authentification
    pub fn set_auth_token(&mut self, token: String, user_id: String) {
        self.user_auth_token = Some(token);
        self.user_id = Some(user_id);
    }

    /// Définit le format audio par défaut
    pub fn set_format(&mut self, format: AudioFormat) {
        self.format_id = format;
    }

    /// Retourne le format audio configuré
    pub fn format(&self) -> AudioFormat {
        self.format_id
    }

    /// Retourne l'App ID
    pub fn app_id(&self) -> &str {
        &self.app_id
    }

    /// Retourne le token d'authentification si disponible
    pub fn auth_token(&self) -> Option<&str> {
        self.user_auth_token.as_deref()
    }

    /// Retourne l'ID utilisateur si disponible
    pub fn user_id(&self) -> Option<&str> {
        self.user_id.as_deref()
    }

    /// Effectue une requête GET à l'API
    pub(crate) async fn get<T: DeserializeOwned>(
        &self,
        endpoint: &str,
        params: &[(&str, &str)],
    ) -> Result<T> {
        self.request("GET", endpoint, params).await
    }

    /// Effectue une requête POST à l'API
    pub(crate) async fn post<T: DeserializeOwned>(
        &self,
        endpoint: &str,
        params: &[(&str, &str)],
    ) -> Result<T> {
        self.request("POST", endpoint, params).await
    }

    /// Effectue une requête à l'API (générique)
    async fn request<T: DeserializeOwned>(
        &self,
        method: &str,
        endpoint: &str,
        params: &[(&str, &str)],
    ) -> Result<T> {
        let url = format!("{}{}", API_BASE_URL, endpoint);

        debug!("{} {} with {} params", method, url, params.len());

        let mut request = if method == "GET" {
            self.client.get(&url)
        } else {
            self.client.post(&url)
        };

        // Ajouter les headers
        request = request.header("X-App-Id", &self.app_id);

        if let Some(ref token) = self.user_auth_token {
            request = request.header("X-User-Auth-Token", token);
        }

        // Ajouter les paramètres
        if method == "GET" {
            request = request.query(params);
        } else {
            request = request.form(params);
        }

        // Envoyer la requête
        let response = request.send().await?;
        self.handle_response(response).await
    }

    /// Traite la réponse HTTP
    async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
        let status = response.status();
        let status_code = status.as_u16();

        debug!("Response status: {}", status);

        if !status.is_success() {
            let error_text = response.text().await.unwrap_or_default();
            warn!("API error ({}): {}", status_code, error_text);
            return Err(QobuzError::from_status_code(status_code, error_text));
        }

        let text = response.text().await?;

        // Vérifier si la réponse contient une erreur Qobuz
        if let Ok(json) = serde_json::from_str::<Value>(&text) {
            if let Some(status_obj) = json.get("status") {
                if status_obj == "error" {
                    let message = json
                        .get("message")
                        .and_then(|m| m.as_str())
                        .unwrap_or("Unknown error");
                    warn!("Qobuz API error: {}", message);
                    return Err(QobuzError::ApiError {
                        code: status_code,
                        message: message.to_string(),
                    });
                }
            }
        }

        // Parser la réponse
        serde_json::from_str(&text).map_err(|e| {
            warn!("Failed to parse response: {}", e);
            QobuzError::JsonParse(e)
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_api_creation() {
        let api = QobuzApi::new("test_app_id").unwrap();
        assert_eq!(api.app_id(), "test_app_id");
        assert!(api.auth_token().is_none());
    }

    #[test]
    fn test_set_auth_token() {
        let mut api = QobuzApi::new("test_app_id").unwrap();
        api.set_auth_token("test_token".to_string(), "user123".to_string());
        assert_eq!(api.auth_token(), Some("test_token"));
        assert_eq!(api.user_id(), Some("user123"));
    }

    #[test]
    fn test_set_format() {
        let mut api = QobuzApi::new("test_app_id").unwrap();
        api.set_format(AudioFormat::Flac_HiRes_96);
        assert_eq!(api.format(), AudioFormat::Flac_HiRes_96);
    }
}
========= End of pmoqobuz/src/api/mod.rs ===========

=============== pmoqobuz/src/pmoserver_ext.rs ============
//! Extension de pmoserver::Server pour intégrer le client Qobuz
//!
//! Ce module fournit un trait d'extension permettant d'ajouter facilement
//! le client Qobuz et ses endpoints à un serveur pmoserver.

use crate::client::QobuzClient;
use anyhow::Result;
use std::sync::Arc;

/// Trait d'extension pour ajouter le support Qobuz à un serveur pmoserver
///
/// Ce trait permet à `pmoqobuz` d'ajouter des méthodes d'extension sur
/// `pmoserver::Server` sans que pmoserver dépende de pmoqobuz.
///
/// # Architecture
///
/// Similaire au pattern utilisé par `pmocovers` avec `CoverCacheExt`, ce trait permet
/// une extension propre et découplée :
///
/// - `pmoserver` définit un serveur HTTP générique
/// - `pmoqobuz` étend ce serveur avec des fonctionnalités Qobuz via ce trait
/// - Le serveur n'a pas besoin de connaître `pmoqobuz`
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoqobuz::QobuzServerExt;
/// use pmoserver::ServerBuilder;
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
///     let mut server = ServerBuilder::new_configured().build();
///
///     // Initialise le client Qobuz depuis la config
///     server.init_qobuz_client_configured().await?;
///
///     server.start().await;
///     server.wait().await;
///     Ok(())
/// }
/// ```
pub trait QobuzServerExt {
    /// Initialise le client Qobuz et enregistre les routes HTTP
    ///
    /// # Arguments
    ///
    /// * `username` - Email ou nom d'utilisateur Qobuz
    /// * `password` - Mot de passe
    ///
    /// # Returns
    ///
    /// * `Arc<QobuzClient>` - Instance partagée du client
    ///
    /// # Routes enregistrées
    ///
    /// - `GET /qobuz/albums/{id}` - Détails d'un album
    /// - `GET /qobuz/albums/{id}/tracks` - Tracks d'un album
    /// - `GET /qobuz/tracks/{id}` - Détails d'une track
    /// - `GET /qobuz/tracks/{id}/stream` - URL de streaming
    /// - `GET /qobuz/artists/{id}` - Détails d'un artiste
    /// - `GET /qobuz/artists/{id}/albums` - Albums d'un artiste
    /// - `GET /qobuz/playlists/{id}` - Détails d'une playlist
    /// - `GET /qobuz/playlists/{id}/tracks` - Tracks d'une playlist
    /// - `GET /qobuz/search` - Recherche (query params: q, type)
    /// - `GET /qobuz/favorites/albums` - Albums favoris
    /// - `GET /qobuz/favorites/artists` - Artistes favoris
    /// - `GET /qobuz/favorites/tracks` - Tracks favoris
    /// - `GET /qobuz/favorites/playlists` - Playlists utilisateur
    /// - `GET /qobuz/genres` - Liste des genres
    /// - `GET /qobuz/featured/albums` - Albums featured
    /// - `GET /qobuz/featured/playlists` - Playlists featured
    /// - `GET /qobuz/cache/stats` - Statistiques du cache
    /// - `GET /swagger-ui` - Documentation interactive
    async fn init_qobuz_client(
        &mut self,
        username: &str,
        password: &str,
    ) -> Result<Arc<QobuzClient>>;

    /// Initialise le client Qobuz avec la configuration par défaut
    ///
    /// Utilise automatiquement les credentials de `pmoconfig::Config` :
    /// - `accounts.qobuz.username` pour le nom d'utilisateur
    /// - `accounts.qobuz.password` pour le mot de passe
    ///
    /// # Returns
    ///
    /// * `Arc<QobuzClient>` - Instance partagée du client
    ///
    /// # Exemple
    ///
    /// ```rust,no_run
    /// use pmoqobuz::QobuzServerExt;
    /// use pmoserver::ServerBuilder;
    ///
    /// #[tokio::main]
    /// async fn main() -> anyhow::Result<()> {
    ///     let mut server = ServerBuilder::new_configured().build();
    ///
    ///     // Utilise automatiquement la config
    ///     server.init_qobuz_client_configured().await?;
    ///
    ///     server.start().await;
    ///     Ok(())
    /// }
    /// ```
    async fn init_qobuz_client_configured(&mut self) -> Result<Arc<QobuzClient>>;

    /// Initialise le client Qobuz avec intégration pmocovers
    ///
    /// Les images d'albums seront automatiquement ajoutées au cache pmocovers fourni.
    ///
    /// # Arguments
    ///
    /// * `username` - Email ou nom d'utilisateur Qobuz
    /// * `password` - Mot de passe
    /// * `cover_cache` - Instance du cache pmocovers à utiliser
    ///
    /// # Returns
    ///
    /// * `Arc<QobuzClient>` - Instance partagée du client avec cache d'images
    ///
    /// # Exemple
    ///
    /// ```rust,no_run
    /// use pmoqobuz::QobuzServerExt;
    /// use pmocovers::CoverCacheExt;
    /// use pmoserver::ServerBuilder;
    ///
    /// #[tokio::main]
    /// async fn main() -> anyhow::Result<()> {
    ///     let mut server = ServerBuilder::new_configured().build();
    ///
    ///     // D'abord initialiser le cache d'images
    ///     let cache = server.init_cover_cache_configured().await?;
    ///
    ///     // Puis initialiser Qobuz avec le cache
    ///     server.init_qobuz_client_with_covers("user", "pass", cache).await?;
    ///
    ///     server.start().await;
    ///     Ok(())
    /// }
    /// ```
    #[cfg(feature = "covers")]
    async fn init_qobuz_client_with_covers(
        &mut self,
        username: &str,
        password: &str,
        cover_cache: Arc<pmocovers::Cache>,
    ) -> Result<Arc<QobuzClient>>;

    /// Initialise le client Qobuz avec intégration pmocovers depuis la configuration
    ///
    /// # Arguments
    ///
    /// * `cover_cache` - Instance du cache pmocovers à utiliser
    ///
    /// # Exemple
    ///
    /// ```rust,no_run
    /// use pmoqobuz::QobuzServerExt;
    /// use pmocovers::CoverCacheExt;
    /// use pmoserver::ServerBuilder;
    ///
    /// #[tokio::main]
    /// async fn main() -> anyhow::Result<()> {
    ///     let mut server = ServerBuilder::new_configured().build();
    ///
    ///     // D'abord initialiser le cache
    ///     let cache = server.init_cover_cache_configured().await?;
    ///
    ///     // Puis initialiser Qobuz avec le cache
    ///     server.init_qobuz_client_configured_with_covers(cache).await?;
    ///
    ///     server.start().await;
    ///     Ok(())
    /// }
    /// ```
    #[cfg(feature = "covers")]
    async fn init_qobuz_client_configured_with_covers(
        &mut self,
        cover_cache: Arc<pmocovers::Cache>,
    ) -> Result<Arc<QobuzClient>>;
}

// L'implémentation du trait sera dans un module séparé (pmoserver_impl.rs)
// pour éviter les dépendances circulaires
========= End of pmoqobuz/src/pmoserver_ext.rs ===========

