From 876b1e01479762f1dddb0c3f2cd175b5c9117e12 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 28 Dec 2025 09:56:05 +0100 Subject: [PATCH] update de qobuz pour utiliser les sources lazy --- .claude-env | 3 + pmoplaylist/src/manager.rs | 22 +++ pmoplaylist/src/persistence/mod.rs | 22 +++ pmoqobuz/src/cache.rs | 31 ++++ pmoqobuz/src/client.rs | 12 +- pmoqobuz/src/didl.rs | 14 +- pmoqobuz/src/source.rs | 227 +++++++++++++++++++++++++---- pmoupnp/src/config_ext.rs | 128 ++++++++++++++++ pmoupnp/src/lib.rs | 2 + rust-analyzer.toml | 46 ++++++ 10 files changed, 467 insertions(+), 40 deletions(-) create mode 100644 .claude-env create mode 100644 pmoupnp/src/config_ext.rs create mode 100644 rust-analyzer.toml diff --git a/.claude-env b/.claude-env new file mode 100644 index 00000000..05f64415 --- /dev/null +++ b/.claude-env @@ -0,0 +1,3 @@ +# Configuration PATH pour Claude Code +# Ce fichier sera lu automatiquement pour configurer l'environnement +export PATH="/Users/coissac/mamba/condabin:/opt/homebrew/lib/ruby/gems/3.4.0/bin:/opt/homebrew/opt/ruby/bin:/Users/coissac/go/bin:/Users/coissac/.cargo/bin:/Users/coissac/.modular/pkg/packages.modular.com_mojo/bin:/Applications/quarto/bin:/Users/coissac/.vscode-oss/extensions/vadimcn.vscode-lldb-1.12.0/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pmk/env/global/bin:/opt/X11/bin:/usr/local/dorado/dorado-0.9.5-osx-arm64/bin:/usr/local/go/bin:/usr/local/src/last-main/bin:/Users/coissac/travail/__MOI__/GO/obitools4/build:/opt/podman/bin:/Applications/quarto/bin:/Users/coissac/.cargo/bin:/Users/coissac/.vscode-oss/extensions/vadimcn.vscode-lldb-1.12.0/bin:/Users/coissac/.vscode-oss/extensions/ms-python.debugpy-2025.14.1-darwin-arm64/bundled/scripts/noConfigScripts:/Users/coissac/.orbstack/bin" diff --git a/pmoplaylist/src/manager.rs b/pmoplaylist/src/manager.rs index 4fc643b0..87168119 100644 --- a/pmoplaylist/src/manager.rs +++ b/pmoplaylist/src/manager.rs @@ -224,6 +224,28 @@ impl PlaylistManager { .await } + /// Récupère l'âge d'une playlist depuis sa création + pub async fn get_playlist_age(&self, id: &str) -> Result> { + use std::time::{SystemTime, UNIX_EPOCH}; + + let persistence = match &self.inner.persistence { + Some(p) => p, + None => return Ok(None), + }; + + let created_at_nanos = persistence.get_playlist_created_at(id).await?; + + if let Some(nanos) = created_at_nanos { + let created_at = UNIX_EPOCH + Duration::from_nanos(nanos as u64); + let age = SystemTime::now() + .duration_since(created_at) + .unwrap_or(Duration::ZERO); + Ok(Some(age)) + } else { + Ok(None) + } + } + /// Enregistre un callback d'évènement playlist (update, track joué). /// /// Retourne un jeton (u64) pour désenregistrer plus tard. diff --git a/pmoplaylist/src/persistence/mod.rs b/pmoplaylist/src/persistence/mod.rs index 881dcea3..3dfb2883 100644 --- a/pmoplaylist/src/persistence/mod.rs +++ b/pmoplaylist/src/persistence/mod.rs @@ -259,6 +259,28 @@ impl PersistenceManager { Ok(ids) } + /// Récupère le timestamp de création d'une playlist + pub async fn get_playlist_created_at(&self, id: &str) -> Result> { + let conn = self.conn.lock().unwrap(); + + let mut stmt = conn + .prepare("SELECT created_at FROM playlists WHERE id = ?1") + .map_err(|e| { + crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e)) + })?; + + let result = stmt.query_row(params![id], |row| row.get(0)); + + match result { + Ok(created_at) => Ok(Some(created_at)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(crate::Error::PersistenceError(format!( + "Failed to get created_at: {}", + e + ))), + } + } + /// Supprime tous les tracks contenant un cache_pk donné pub async fn remove_by_cache_pk(&self, cache_pk: &str) -> Result<()> { let conn = self.conn.lock().unwrap(); diff --git a/pmoqobuz/src/cache.rs b/pmoqobuz/src/cache.rs index 4bb0061f..e3543751 100644 --- a/pmoqobuz/src/cache.rs +++ b/pmoqobuz/src/cache.rs @@ -14,6 +14,8 @@ pub struct QobuzCache { albums: Arc>, /// Cache des tracks (TTL: 1 heure) tracks: Arc>, + /// Cache des tracks d'un album complet (TTL: 1 heure) + album_tracks: Arc>>, /// Cache des artistes (TTL: 1 heure) artists: Arc>, /// Cache des playlists (TTL: 30 minutes) @@ -45,6 +47,12 @@ impl QobuzCache { .time_to_live(Duration::from_secs(3600)) // 1 heure .build(), ), + album_tracks: Arc::new( + MokaCache::builder() + .max_capacity(max_capacity) + .time_to_live(Duration::from_secs(3600)) // 1 heure + .build(), + ), artists: Arc::new( MokaCache::builder() .max_capacity(max_capacity / 2) @@ -106,6 +114,23 @@ impl QobuzCache { self.tracks.invalidate(id).await; } + // ============ Album Tracks (liste complète des tracks d'un album) ============ + + /// Récupère la liste complète des tracks d'un album depuis le cache + pub async fn get_album_tracks(&self, album_id: &str) -> Option> { + self.album_tracks.get(album_id).await + } + + /// Ajoute la liste complète des tracks d'un album au cache + pub async fn put_album_tracks(&self, album_id: String, tracks: Vec) { + self.album_tracks.insert(album_id, tracks).await; + } + + /// Invalide la liste des tracks d'un album du cache + pub async fn invalidate_album_tracks(&self, album_id: &str) { + self.album_tracks.invalidate(album_id).await; + } + // ============ Artists ============ /// Récupère un artiste depuis le cache @@ -180,6 +205,7 @@ impl QobuzCache { pub async fn clear_all(&self) { self.albums.invalidate_all(); self.tracks.invalidate_all(); + self.album_tracks.invalidate_all(); self.artists.invalidate_all(); self.playlists.invalidate_all(); self.searches.invalidate_all(); @@ -190,6 +216,7 @@ impl QobuzCache { pub async fn stats(&self) -> CacheStats { self.albums.run_pending_tasks().await; self.tracks.run_pending_tasks().await; + self.album_tracks.run_pending_tasks().await; self.artists.run_pending_tasks().await; self.playlists.run_pending_tasks().await; self.searches.run_pending_tasks().await; @@ -198,6 +225,7 @@ impl QobuzCache { CacheStats { albums_count: self.albums.entry_count(), tracks_count: self.tracks.entry_count(), + album_tracks_count: self.album_tracks.entry_count(), artists_count: self.artists.entry_count(), playlists_count: self.playlists.entry_count(), searches_count: self.searches.entry_count(), @@ -219,6 +247,8 @@ pub struct CacheStats { pub albums_count: u64, /// Nombre de tracks en cache pub tracks_count: u64, + /// Nombre de listes complètes de tracks d'albums en cache + pub album_tracks_count: u64, /// Nombre d'artistes en cache pub artists_count: u64, /// Nombre de playlists en cache @@ -234,6 +264,7 @@ impl CacheStats { pub fn total_count(&self) -> u64 { self.albums_count + self.tracks_count + + self.album_tracks_count + self.artists_count + self.playlists_count + self.searches_count diff --git a/pmoqobuz/src/client.rs b/pmoqobuz/src/client.rs index ebc4ff30..285628d9 100644 --- a/pmoqobuz/src/client.rs +++ b/pmoqobuz/src/client.rs @@ -584,14 +584,24 @@ impl QobuzClient { /// Récupère les tracks d'un album pub async fn get_album_tracks(&self, album_id: &str) -> Result> { + // Vérifier le cache d'abord + if let Some(tracks) = self.cache.get_album_tracks(album_id).await { + debug!("Album tracks for {} found in cache", album_id); + return Ok(tracks); + } + + // Sinon, récupérer depuis l'API let tracks = self .call_with_auth_repair("get_album_tracks", || self.api.get_album_tracks(album_id)) .await?; - // Mettre les tracks en cache + // Mettre les tracks en cache (individuellement ET la liste complète) for track in &tracks { self.cache.put_track(track.id.clone(), track.clone()).await; } + self.cache + .put_album_tracks(album_id.to_string(), tracks.clone()) + .await; Ok(tracks) } diff --git a/pmoqobuz/src/didl.rs b/pmoqobuz/src/didl.rs index 2b8866c5..db077cc6 100644 --- a/pmoqobuz/src/didl.rs +++ b/pmoqobuz/src/didl.rs @@ -27,10 +27,10 @@ impl ToDIDL for Album { /// /// ```rust,ignore /// let album = client.get_album("12345").await?; - /// let container = album.to_didl_container("0$qobuz$albums")?; + /// let container = album.to_didl_container("qobuz:favorites")?; /// ``` fn to_didl_container(&self, parent_id: &str) -> Result { - let id = format!("0$qobuz$album${}", self.id); + let id = format!("qobuz:album:{}", self.id); Ok(Container { id, @@ -71,10 +71,10 @@ impl ToDIDL for Track { /// /// ```rust,ignore /// let track = client.get_track("98765").await?; - /// let item = track.to_didl_item("0$qobuz$album$12345")?; + /// let item = track.to_didl_item("qobuz:album:12345")?; /// ``` fn to_didl_item(&self, parent_id: &str) -> Result { - let id = format!("0$qobuz$track${}", self.id); + let id = format!("qobuz:track:{}", self.id); // Déterminer l'artiste à afficher let artist_name = self @@ -128,7 +128,7 @@ impl ToDIDL for Track { impl ToDIDL for Playlist { /// Convertit une playlist en Container DIDL fn to_didl_container(&self, parent_id: &str) -> Result { - let id = format!("0$qobuz$playlist${}", self.id); + let id = format!("qobuz:playlist:{}", self.id); Ok(Container { id, @@ -211,7 +211,7 @@ mod tests { }; let container = album.to_didl_container("parent").unwrap(); - assert_eq!(container.id, "0$qobuz$album$123"); + assert_eq!(container.id, "qobuz:album:123"); assert_eq!(container.parent_id, "parent"); assert!(container.title.contains("Test Album")); } @@ -234,7 +234,7 @@ mod tests { }; let item = track.to_didl_item("parent").unwrap(); - assert_eq!(item.id, "0$qobuz$track$789"); + assert_eq!(item.id, "qobuz:track:789"); assert_eq!(item.parent_id, "parent"); assert_eq!(item.title, "Test Track"); } diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index 2738f0f0..b7f2d3a0 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -14,7 +14,10 @@ use pmosource::SourceCacheManager; use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result}; use serde_json::json; use std::sync::Arc; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; + +/// TTL pour les playlists d'albums (7 jours) +const ALBUM_PLAYLIST_TTL: Duration = Duration::from_secs(7 * 24 * 3600); /// Default image for Qobuz (300x300 WebP, embedded in binary) const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp"); @@ -322,6 +325,13 @@ impl QobuzSource { ); } + // Stocker le track_id Qobuz pour reconstruction DIDL ultérieure + let _ = self.inner.cache_manager.set_audio_metadata( + &cached_audio_pk, + "qobuz_track_id", + json!(track.id), + ); + // 4. Store metadata self.inner .cache_manager @@ -398,7 +408,7 @@ impl QobuzSource { // 3. Batch insert into playlist (single DB transaction) let playlist_manager = pmoplaylist::PlaylistManager(); let writer = playlist_manager - .get_write_handle(playlist_id.to_string()) + .get_persistent_write_handle(playlist_id.to_string()) .await .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; @@ -489,7 +499,7 @@ impl QobuzSource { // 3. Batch insert into playlist (single DB transaction) let playlist_manager = pmoplaylist::PlaylistManager(); let writer = playlist_manager - .get_write_handle(playlist_id.to_string()) + .get_persistent_write_handle(playlist_id.to_string()) .await .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; @@ -511,6 +521,181 @@ impl QobuzSource { Ok(lazy_pks.len()) } + /// Vérifie si une playlist d'album existe et est valide (non expirée ET non vide) + async fn is_album_playlist_valid(&self, playlist_id: &str) -> Result { + let playlist_manager = pmoplaylist::PlaylistManager(); + + if !playlist_manager.exists(playlist_id).await { + return Ok(false); + } + + // Vérifier l'âge + match playlist_manager.get_playlist_age(playlist_id).await { + Ok(Some(age)) if age < ALBUM_PLAYLIST_TTL => { + // Playlist non expirée, vérifier qu'elle contient des tracks + match playlist_manager.get_read_handle(playlist_id).await { + Ok(reader) => { + let count = reader.remaining().await.unwrap_or(0); + Ok(count > 0) // Valide seulement si non vide + } + Err(_) => Ok(false), + } + } + _ => Ok(false), + } + } + + /// Adapte les Items d'une playlist pour correspondre au schéma UPnP Qobuz + async fn adapt_playlist_items_to_qobuz( + &self, + items: Vec, + album_id: &str, + ) -> Result> { + use tracing::warn; + + let parent_id = format!("qobuz:album:{}", album_id); + + let mut adapted = Vec::with_capacity(items.len()); + + for mut item in items { + // Extraire cache_pk depuis l'URL du resource + let cache_pk = if let Some(resource) = item.resources.first() { + resource.url + .strip_prefix("/audio/flac/") + .map(|s| s.to_string()) + } else { + None + }; + + if let Some(pk) = cache_pk { + // Récupérer track_id depuis metadata + if let Ok(Some(track_id_value)) = self.inner.cache_manager.get_audio_metadata(&pk, "qobuz_track_id") { + if let Some(track_id) = track_id_value.as_str() { + item.id = format!("qobuz:track:{}", track_id); + } else { + warn!("qobuz_track_id not a string for {}", pk); + } + } else { + warn!("No qobuz_track_id metadata for {}", pk); + } + } + + item.parent_id = parent_id.clone(); + adapted.push(item); + } + + Ok(adapted) + } + + /// Récupère ou crée une playlist lazy pour un album + async fn get_or_create_album_playlist_items( + &self, + album_id: &str, + limit: usize, + ) -> Result> { + use tracing::{debug, info}; + + let playlist_id = format!("qobuz-album-{}", album_id); + let playlist_manager = pmoplaylist::PlaylistManager(); + + // Vérifier validité (existe ET non expirée) + let is_valid = self.is_album_playlist_valid(&playlist_id).await?; + + if is_valid { + debug!("Album playlist {} found and valid", playlist_id); + + let reader = playlist_manager + .get_read_handle(&playlist_id) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + let items = reader + .to_items(limit) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + return self.adapt_playlist_items_to_qobuz(items, album_id).await; + } + + // Playlist invalide/inexistante : (re)créer + info!("Album playlist {} creating/refreshing", playlist_id); + + // 1. Métadonnées album + let album = self + .inner + .client + .get_album(album_id) + .await + .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?; + + // 2. Cache cover + let cover_pk = if let Some(ref image_url) = album.image { + self.inner + .cache_manager + .cache_cover(image_url) + .await + .ok() + } else { + None + }; + + // 3. Créer ou récupérer playlist + let writer = if playlist_manager.exists(&playlist_id).await { + let writer = playlist_manager + .get_persistent_write_handle(playlist_id.clone()) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + writer + .flush() + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + writer + } else { + playlist_manager + .create_persistent_playlist_with_role( + playlist_id.clone(), + pmoplaylist::PlaylistRole::Album, + ) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))? + }; + + // 4. Métadonnées playlist + writer + .set_title(album.title.clone()) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + if let Some(pk) = cover_pk { + writer + .set_cover_pk(Some(pk)) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + } + + // IMPORTANT: Libérer le write lock avant d'appeler add_album_to_playlist + drop(writer); + + // 5. Ajouter tracks (réutilise add_album_to_playlist existant) + self.add_album_to_playlist(&playlist_id, album_id).await?; + + // 6. Récupérer items + let reader = playlist_manager + .get_read_handle(&playlist_id) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + let items = reader + .to_items(limit) + .await + .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; + + // 7. Adapter IDs + self.adapt_playlist_items_to_qobuz(items, album_id).await + } + /// Increment update counter (called on catalog changes) async fn increment_update_id(&self) { let mut counter = self.inner.update_counter.write().await; @@ -620,22 +805,9 @@ impl MusicSource for QobuzSource { } 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 = tracks - .into_iter() - .filter_map(|track| { - track - .to_didl_item(&format!("qobuz:album:{}", album_id)) - .ok() - }) - .collect(); + let items = self + .get_or_create_album_playlist_items(&album_id, usize::MAX) + .await?; Ok(BrowseResult::Items(items)) } @@ -1041,23 +1213,14 @@ impl MusicSource for QobuzSource { ) -> Result { 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 all_items = self + .get_or_create_album_playlist_items(&album_id, usize::MAX) + .await?; - let items: Vec = tracks + let items: Vec = all_items .into_iter() .skip(offset) .take(limit) - .filter_map(|track| { - track - .to_didl_item(&format!("qobuz:album:{}", album_id)) - .ok() - }) .collect(); Ok(BrowseResult::Items(items)) diff --git a/pmoupnp/src/config_ext.rs b/pmoupnp/src/config_ext.rs new file mode 100644 index 00000000..cd239673 --- /dev/null +++ b/pmoupnp/src/config_ext.rs @@ -0,0 +1,128 @@ +//! Extension pour intégrer la configuration UPnP dans pmoconfig +//! +//! Ce module fournit le trait `UpnpConfigExt` qui permet d'ajouter facilement +//! des méthodes de configuration UPnP à pmoconfig::Config. +//! +//! Il suit le même pattern que pmocache/src/config_ext.rs pour la cohérence. + +use anyhow::Result; +use pmoconfig::Config; +use serde_yaml::Value; + +// Constantes par défaut pour les noms UPnP +const DEFAULT_MANUFACTURER: &str = "PMOMusic"; +const DEFAULT_UDN_PREFIX: &str = "pmomusic"; +const DEFAULT_MODEL_NAME_PREFIX: &str = "PMOMusic"; +const DEFAULT_FRIENDLY_NAME_PREFIX: &str = "PMOMusic"; + +/// Trait d'extension pour ajouter la configuration UPnP à pmoconfig +/// +/// Ce trait étend `pmoconfig::Config` avec des méthodes pour configurer +/// les noms et identifiants des devices UPnP. +/// +/// # Exemple +/// +/// ```rust,ignore +/// use pmoconfig::get_config; +/// use pmoupnp::UpnpConfigExt; +/// +/// let config = get_config(); +/// let manufacturer = config.get_upnp_manufacturer()?; +/// let udn_prefix = config.get_upnp_udn_prefix()?; +/// ``` +pub trait UpnpConfigExt { + /// Récupère le fabricant pour les devices UPnP + /// + /// # Returns + /// + /// Le nom du fabricant à afficher dans les descripteurs UPnP (défaut: "PMOMusic") + fn get_upnp_manufacturer(&self) -> Result; + + /// Définit le fabricant pour les devices UPnP + fn set_upnp_manufacturer(&self, manufacturer: String) -> Result<()>; + + /// Récupère le préfixe UDN pour les devices UPnP + /// + /// # Returns + /// + /// Le préfixe utilisé pour générer les UDN (défaut: "pmomusic") + fn get_upnp_udn_prefix(&self) -> Result; + + /// Définit le préfixe UDN pour les devices UPnP + fn set_upnp_udn_prefix(&self, prefix: String) -> Result<()>; + + /// Récupère le préfixe pour les noms de modèle des devices UPnP + /// + /// # Returns + /// + /// Le préfixe utilisé pour construire les model names (défaut: "PMOMusic") + fn get_upnp_model_name_prefix(&self) -> Result; + + /// Définit le préfixe pour les noms de modèle des devices UPnP + fn set_upnp_model_name_prefix(&self, prefix: String) -> Result<()>; + + /// Récupère le préfixe pour les noms conviviaux des devices UPnP + /// + /// # Returns + /// + /// Le préfixe utilisé pour construire les friendly names (défaut: "PMOMusic") + fn get_upnp_friendly_name_prefix(&self) -> Result; + + /// Définit le préfixe pour les noms conviviaux des devices UPnP + fn set_upnp_friendly_name_prefix(&self, prefix: String) -> Result<()>; +} + +impl UpnpConfigExt for Config { + fn get_upnp_manufacturer(&self) -> Result { + match self.get_value(&["host", "upnp", "manufacturer"]) { + Ok(Value::String(s)) if !s.is_empty() => Ok(s), + _ => Ok(DEFAULT_MANUFACTURER.to_string()), + } + } + + fn set_upnp_manufacturer(&self, manufacturer: String) -> Result<()> { + self.set_value( + &["host", "upnp", "manufacturer"], + Value::String(manufacturer), + ) + } + + fn get_upnp_udn_prefix(&self) -> Result { + match self.get_value(&["host", "upnp", "udn_prefix"]) { + Ok(Value::String(s)) if !s.is_empty() => Ok(s), + _ => Ok(DEFAULT_UDN_PREFIX.to_string()), + } + } + + fn set_upnp_udn_prefix(&self, prefix: String) -> Result<()> { + self.set_value(&["host", "upnp", "udn_prefix"], Value::String(prefix)) + } + + fn get_upnp_model_name_prefix(&self) -> Result { + match self.get_value(&["host", "upnp", "model_name_prefix"]) { + Ok(Value::String(s)) if !s.is_empty() => Ok(s), + _ => Ok(DEFAULT_MODEL_NAME_PREFIX.to_string()), + } + } + + fn set_upnp_model_name_prefix(&self, prefix: String) -> Result<()> { + self.set_value( + &["host", "upnp", "model_name_prefix"], + Value::String(prefix), + ) + } + + fn get_upnp_friendly_name_prefix(&self) -> Result { + match self.get_value(&["host", "upnp", "friendly_name_prefix"]) { + Ok(Value::String(s)) if !s.is_empty() => Ok(s), + _ => Ok(DEFAULT_FRIENDLY_NAME_PREFIX.to_string()), + } + } + + fn set_upnp_friendly_name_prefix(&self, prefix: String) -> Result<()> { + self.set_value( + &["host", "upnp", "friendly_name_prefix"], + Value::String(prefix), + ) + } +} diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs index 8340a145..c9349e82 100644 --- a/pmoupnp/src/lib.rs +++ b/pmoupnp/src/lib.rs @@ -3,6 +3,7 @@ mod object_trait; pub mod actions; pub mod cache_registry; +pub mod config_ext; pub mod devices; pub mod services; pub mod soap; @@ -20,6 +21,7 @@ use std::{collections::HashMap, sync::Arc}; pub use pmoaudiocache::get_audio_cache; pub use pmocovers::get_cover_cache; +pub use crate::config_ext::UpnpConfigExt; pub use crate::object_trait::*; pub use crate::upnp_server::UpnpServerExt; diff --git a/rust-analyzer.toml b/rust-analyzer.toml new file mode 100644 index 00000000..3fa2e20a --- /dev/null +++ b/rust-analyzer.toml @@ -0,0 +1,46 @@ +# Configuration rust-analyzer pour gros workspace (23 crates) +# Optimisé pour éviter les crashs et limiter l'utilisation mémoire + +# Limiter l'analyse en arrière-plan +[checkOnSave] +enable = true +# N'analyser que la cible par défaut (pas tous les targets) +allTargets = false +# Utiliser clippy au lieu de cargo check (optionnel, commentez si trop lent) +# command = "clippy" + +# Désactiver les build scripts pour réduire la charge +[cargo] +buildScripts.enable = false +# Ne charger que les crates nécessaires +loadOutDirsFromCheck = false + +# Désactiver les proc-macros si elles causent des problèmes +[procMacro] +enable = true +# Si les crashs persistent, passez à false ci-dessus + +# Limiter la complétion +[completion] +limit = 50 + +# Désactiver certains diagnostics coûteux +[diagnostics] +disabled = [ + "unresolved-proc-macro", + "macro-error", +] + +# Optimisations de performance +[inlayHints] +# Réduire les hints pour améliorer les perfs +maxLength = 25 + +# Limiter la profondeur d'analyse des types +[typing] +autoClosingAngleBrackets.enable = false + +# Pour les très gros workspaces, décommenter pour analyser moins de crates +# [linkedProjects] +# Spécifier uniquement les crates que vous éditez activement +# Par exemple : ["PMOMusic/Cargo.toml", "pmocontrol/Cargo.toml"]