ajoute à pmoqobuz la feature cache
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2393,6 +2393,7 @@ dependencies = [
|
||||
"hex",
|
||||
"mockito",
|
||||
"moka",
|
||||
"pmoaudiocache",
|
||||
"pmoconfig",
|
||||
"pmocovers",
|
||||
"pmodidl",
|
||||
|
||||
@@ -37,6 +37,9 @@ pmoconfig = { path = "../pmoconfig" }
|
||||
# Intégration avec pmocovers pour le cache d'images
|
||||
pmocovers = { path = "../pmocovers", optional = true }
|
||||
|
||||
# Intégration avec pmoaudiocache pour le cache audio
|
||||
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
|
||||
|
||||
# Intégration avec pmodidl pour l'export DIDL
|
||||
pmodidl = { path = "../pmodidl" }
|
||||
|
||||
@@ -56,6 +59,8 @@ default = []
|
||||
pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"]
|
||||
# Feature pour activer le cache d'images via pmocovers
|
||||
covers = ["dep:pmocovers"]
|
||||
# Feature pour activer le cache complet (images + audio)
|
||||
cache = ["dep:pmocovers", "dep:pmoaudiocache"]
|
||||
|
||||
[dev-dependencies]
|
||||
# Tests
|
||||
|
||||
@@ -10,8 +10,9 @@ Client Rust pour l'API Qobuz avec cache en mémoire, inspiré de l'implémentati
|
||||
- ✅ **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`)
|
||||
- 🔄 **API HTTP** : Endpoints REST via pmoserver (feature `pmoserver`)
|
||||
- ✅ **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
|
||||
|
||||
@@ -143,12 +144,62 @@ println!("Total: {}", stats.total_count());
|
||||
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 l'exemple :
|
||||
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
|
||||
@@ -185,6 +236,12 @@ Générez la documentation :
|
||||
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
|
||||
@@ -193,6 +250,8 @@ cargo doc -p pmoqobuz --open
|
||||
- `moka` : Cache en mémoire avec TTL
|
||||
- `pmodidl` : Export DIDL-Lite
|
||||
- `pmoconfig` : Configuration
|
||||
- `pmocovers` : Cache d'images (optionnel)
|
||||
- `pmoaudiocache` : Cache audio (optionnel)
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
158
pmoqobuz/examples/with_cache.rs
Normal file
158
pmoqobuz/examples/with_cache.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
//! 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 pmoqobuz::{QobuzClient, QobuzSource};
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use pmoaudiocache::AudioCache;
|
||||
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(())
|
||||
}
|
||||
@@ -105,16 +105,83 @@
|
||||
//! - Résultats de recherche : 15 minutes
|
||||
//! - URLs de streaming : 5 minutes
|
||||
//!
|
||||
//! ## Intégration pmocovers
|
||||
//! ## Intégration pmocovers et pmoaudiocache
|
||||
//!
|
||||
//! Les images d'albums sont automatiquement cachées via `pmocovers` (feature `covers`) :
|
||||
//! La feature `cache` active le support complet du cache pour les images et l'audio.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! let album = client.get_album("12345").await?;
|
||||
//! // L'image est automatiquement ajoutée au cache pmocovers
|
||||
//! let cover_url = album.cover_url_cached; // URL vers le cache local
|
||||
//! ### 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 :
|
||||
@@ -142,6 +209,7 @@
|
||||
//!
|
||||
//! - [`pmodidl`] : Format DIDL-Lite
|
||||
//! - [`pmocovers`] : Cache d'images
|
||||
//! - [`pmoaudiocache`] : Cache audio
|
||||
//! - [`pmoconfig`] : Configuration
|
||||
//! - [`pmoserver`] : Serveur HTTP
|
||||
|
||||
|
||||
@@ -8,10 +8,16 @@ use crate::didl::ToDIDL;
|
||||
use crate::models::{Album, Track};
|
||||
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
|
||||
use pmodidl::{Container, Item};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
use pmocovers::Cache as CoverCache;
|
||||
#[cfg(feature = "cache")]
|
||||
use pmoaudiocache::{AudioCache, AudioMetadata};
|
||||
|
||||
/// Default image for Qobuz (300x300 WebP, embedded in binary)
|
||||
const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");
|
||||
|
||||
@@ -65,11 +71,34 @@ struct QobuzSourceInner {
|
||||
/// Qobuz API client
|
||||
client: QobuzClient,
|
||||
|
||||
/// Cache server base URL for URI resolution
|
||||
cache_base_url: String,
|
||||
|
||||
/// Track metadata cache (track_id -> TrackMetadata)
|
||||
track_cache: RwLock<HashMap<String, TrackMetadata>>,
|
||||
|
||||
/// Cover image cache (optional)
|
||||
#[cfg(feature = "cache")]
|
||||
cover_cache: Option<Arc<CoverCache>>,
|
||||
|
||||
/// Audio cache (optional)
|
||||
#[cfg(feature = "cache")]
|
||||
audio_cache: Option<Arc<AudioCache>>,
|
||||
|
||||
/// Update tracking
|
||||
update_counter: RwLock<u32>,
|
||||
last_change: RwLock<SystemTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TrackMetadata {
|
||||
original_uri: String,
|
||||
#[cfg(feature = "cache")]
|
||||
cached_audio_pk: Option<String>,
|
||||
#[cfg(feature = "cache")]
|
||||
cached_cover_pk: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for QobuzSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("QobuzSource").finish()
|
||||
@@ -82,6 +111,7 @@ impl QobuzSource {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `client` - Authenticated Qobuz API client
|
||||
/// * `cache_base_url` - Base URL for the cache server (e.g., "http://localhost:8080")
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -91,14 +121,72 @@ impl QobuzSource {
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = QobuzClient::from_config().await?;
|
||||
/// let source = QobuzSource::new(client);
|
||||
/// let source = QobuzSource::new(client, "http://localhost:8080");
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new(client: QobuzClient) -> Self {
|
||||
pub fn new(client: QobuzClient, cache_base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(QobuzSourceInner {
|
||||
client,
|
||||
cache_base_url: cache_base_url.into(),
|
||||
track_cache: RwLock::new(HashMap::new()),
|
||||
#[cfg(feature = "cache")]
|
||||
cover_cache: None,
|
||||
#[cfg(feature = "cache")]
|
||||
audio_cache: None,
|
||||
update_counter: RwLock::new(0),
|
||||
last_change: RwLock::new(SystemTime::now()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Qobuz source with caching support
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `client` - Authenticated Qobuz API client
|
||||
/// * `cache_base_url` - Base URL for the cache server (e.g., "http://localhost:8080")
|
||||
/// * `cover_cache` - Optional cover image cache
|
||||
/// * `audio_cache` - Optional audio cache
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoqobuz::{QobuzSource, QobuzClient};
|
||||
/// use pmocovers::Cache as CoverCache;
|
||||
/// use pmoaudiocache::AudioCache;
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = QobuzClient::from_config().await?;
|
||||
/// let cover_cache = Arc::new(CoverCache::new("/tmp/qobuz-covers").await?);
|
||||
/// let audio_cache = Arc::new(AudioCache::new("/tmp/qobuz-audio").await?);
|
||||
///
|
||||
/// let source = QobuzSource::new_with_cache(
|
||||
/// client,
|
||||
/// "http://localhost:8080",
|
||||
/// Some(cover_cache),
|
||||
/// Some(audio_cache),
|
||||
/// );
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "cache")]
|
||||
pub fn new_with_cache(
|
||||
client: QobuzClient,
|
||||
cache_base_url: impl Into<String>,
|
||||
cover_cache: Option<Arc<CoverCache>>,
|
||||
audio_cache: Option<Arc<AudioCache>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(QobuzSourceInner {
|
||||
client,
|
||||
cache_base_url: cache_base_url.into(),
|
||||
track_cache: RwLock::new(HashMap::new()),
|
||||
cover_cache,
|
||||
audio_cache,
|
||||
update_counter: RwLock::new(0),
|
||||
last_change: RwLock::new(SystemTime::now()),
|
||||
}),
|
||||
@@ -110,6 +198,118 @@ impl QobuzSource {
|
||||
&self.inner.client
|
||||
}
|
||||
|
||||
/// Add a track from Qobuz with optional caching
|
||||
///
|
||||
/// This method is used to add a Qobuz track to the internal cache,
|
||||
/// downloading and caching both cover art and audio data if caching is enabled.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `track` - The Qobuz track to add
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the track ID that was used for caching.
|
||||
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()))?;
|
||||
|
||||
// Cache cover image
|
||||
#[cfg(feature = "cache")]
|
||||
let cached_cover_pk = if let Some(ref cover_cache) = self.inner.cover_cache {
|
||||
if let Some(ref album) = track.album {
|
||||
if let Some(ref image_url) = album.image {
|
||||
match cover_cache.add_from_url(image_url).await {
|
||||
Ok(pk) => {
|
||||
tracing::info!("Successfully cached cover for track {}: {}", track_id, pk);
|
||||
Some(pk)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to cache cover image {}: {}", image_url, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Cache audio asynchronously
|
||||
#[cfg(feature = "cache")]
|
||||
let cached_audio_pk = if let Some(ref audio_cache) = self.inner.audio_cache {
|
||||
// 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| {
|
||||
// Parse year from ISO date (e.g., "2023-01-15")
|
||||
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, // Qobuz doesn't provide bitrate directly
|
||||
};
|
||||
|
||||
// Cache the audio asynchronously
|
||||
match audio_cache.add_from_url(&stream_url, Some(metadata)).await {
|
||||
Ok((pk, _)) => {
|
||||
tracing::info!("Successfully cached audio for track {}: {}", track_id, pk);
|
||||
Some(pk)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to cache audio for track {}: {}", track_id, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Store metadata
|
||||
{
|
||||
let mut cache = self.inner.track_cache.write().await;
|
||||
cache.insert(
|
||||
track_id.clone(),
|
||||
TrackMetadata {
|
||||
original_uri: stream_url,
|
||||
#[cfg(feature = "cache")]
|
||||
cached_audio_pk,
|
||||
#[cfg(feature = "cache")]
|
||||
cached_cover_pk,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -284,7 +484,21 @@ impl MusicSource for QobuzSource {
|
||||
}
|
||||
|
||||
async fn resolve_uri(&self, object_id: &str) -> Result<String> {
|
||||
// Extract track ID from object_id
|
||||
// Check if we have cached metadata for this track
|
||||
let cache = self.inner.track_cache.read().await;
|
||||
|
||||
if let Some(metadata) = cache.get(object_id) {
|
||||
// Priority 1: Use cached audio if available
|
||||
#[cfg(feature = "cache")]
|
||||
if let Some(ref pk) = metadata.cached_audio_pk {
|
||||
return Ok(format!("{}/audio/tracks/{}/stream", self.inner.cache_base_url, pk));
|
||||
}
|
||||
|
||||
// Priority 2: Return original stream URI (already fetched)
|
||||
return Ok(metadata.original_uri.clone());
|
||||
}
|
||||
|
||||
// If not in cache, extract track ID and get streaming URL from Qobuz
|
||||
// Object IDs for tracks follow pattern: "qobuz://track/{id}"
|
||||
let track_id = if let Some(id) = object_id.strip_prefix("qobuz://track/") {
|
||||
id
|
||||
|
||||
Reference in New Issue
Block a user