update de qobuz pour utiliser les sources lazy
This commit is contained in:
3
.claude-env
Normal file
3
.claude-env
Normal file
@@ -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"
|
||||
@@ -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<Option<Duration>> {
|
||||
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.
|
||||
|
||||
@@ -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<Option<i64>> {
|
||||
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();
|
||||
|
||||
@@ -14,6 +14,8 @@ pub struct QobuzCache {
|
||||
albums: Arc<MokaCache<String, Album>>,
|
||||
/// Cache des tracks (TTL: 1 heure)
|
||||
tracks: Arc<MokaCache<String, Track>>,
|
||||
/// Cache des tracks d'un album complet (TTL: 1 heure)
|
||||
album_tracks: Arc<MokaCache<String, Vec<Track>>>,
|
||||
/// Cache des artistes (TTL: 1 heure)
|
||||
artists: Arc<MokaCache<String, Artist>>,
|
||||
/// 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<Vec<Track>> {
|
||||
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<Track>) {
|
||||
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
|
||||
|
||||
@@ -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<Vec<Track>> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -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<Container> {
|
||||
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<Item> {
|
||||
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<Container> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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<bool> {
|
||||
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<Item>,
|
||||
album_id: &str,
|
||||
) -> Result<Vec<Item>> {
|
||||
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<Vec<Item>> {
|
||||
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<Item> = 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<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 all_items = self
|
||||
.get_or_create_album_playlist_items(&album_id, usize::MAX)
|
||||
.await?;
|
||||
|
||||
let items: Vec<Item> = tracks
|
||||
let items: Vec<Item> = 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))
|
||||
|
||||
128
pmoupnp/src/config_ext.rs
Normal file
128
pmoupnp/src/config_ext.rs
Normal file
@@ -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<String>;
|
||||
|
||||
/// 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<String>;
|
||||
|
||||
/// 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<String>;
|
||||
|
||||
/// 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<String>;
|
||||
|
||||
/// 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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
46
rust-analyzer.toml
Normal file
46
rust-analyzer.toml
Normal file
@@ -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"]
|
||||
Reference in New Issue
Block a user