Files
pmomusic/pmomediaserver/src/source_registry.rs
2025-10-29 22:35:53 +01:00

388 lines
10 KiB
Rust

//! # Source Registry - Gestionnaire de sources musicales
//!
//! Ce module fournit un registre centralisé pour gérer les différentes sources musicales
//! (MusicSource) qui peuvent être diffusées par le MediaServer.
//!
//! ## Fonctionnalités
//!
//! - **Enregistrement de sources** : Ajout de sources musicales au registre
//! - **Accès aux sources** : Récupération des sources enregistrées par ID
//! - **Navigation multi-sources** : Combine les sources dans une hiérarchie unique
//! - **Thread-safe** : Utilise Arc et RwLock pour un accès concurrent
use pmosource::MusicSource;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// Registre des sources musicales
///
/// Ce registre maintient une liste de toutes les sources musicales enregistrées
/// et permet de les récupérer par leur ID unique.
///
/// # Thread Safety
///
/// Le registre utilise `Arc<RwLock<...>>` pour permettre un accès concurrent sécurisé.
/// Plusieurs lecteurs peuvent accéder simultanément aux sources, mais l'enregistrement
/// de nouvelles sources nécessite un verrou exclusif.
///
/// # Examples
///
/// ```ignore
/// use pmomediaserver::source_registry::SourceRegistry;
///
/// let registry = SourceRegistry::new();
///
/// // Enregistrer une source
/// let source = Arc::new(MyMusicSource::new());
/// registry.register(source).await;
///
/// // Récupérer une source
/// if let Some(source) = registry.get("my-source-id").await {
/// let root = source.root_container().await?;
/// }
/// ```
#[derive(Clone)]
pub struct SourceRegistry {
sources: Arc<RwLock<HashMap<String, Arc<dyn MusicSource>>>>,
}
impl SourceRegistry {
/// Crée un nouveau registre vide
///
/// # Examples
///
/// ```
/// use pmomediaserver::source_registry::SourceRegistry;
///
/// let registry = SourceRegistry::new();
/// ```
pub fn new() -> Self {
Self {
sources: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Enregistre une nouvelle source musicale
///
/// La source est identifiée par son ID unique (retourné par `source.id()`).
/// Si une source avec le même ID existe déjà, elle sera remplacée.
///
/// # Arguments
///
/// * `source` - La source musicale à enregistrer (doit implémenter `MusicSource`)
///
/// # Examples
///
/// ```ignore
/// let source = Arc::new(RadioParadise::new());
/// registry.register(source).await;
/// ```
pub async fn register(&self, source: Arc<dyn MusicSource>) {
let id = source.id().to_string();
let mut sources = self.sources.write().await;
tracing::info!(
source_id = %id,
source_name = %source.name(),
"Registering music source"
);
sources.insert(id, source);
}
/// Récupère une source par son ID
///
/// # Arguments
///
/// * `id` - L'ID unique de la source
///
/// # Returns
///
/// Un `Arc` vers la source si elle existe, ou `None` si aucune source avec cet ID
/// n'est enregistrée.
///
/// # Examples
///
/// ```ignore
/// if let Some(source) = registry.get("radio-paradise").await {
/// println!("Found: {}", source.name());
/// }
/// ```
pub async fn get(&self, id: &str) -> Option<Arc<dyn MusicSource>> {
let sources = self.sources.read().await;
sources.get(id).cloned()
}
/// Liste toutes les sources enregistrées
///
/// # Returns
///
/// Un vecteur contenant des clones de toutes les sources enregistrées.
///
/// # Examples
///
/// ```ignore
/// let all_sources = registry.list_all().await;
/// for source in all_sources {
/// println!("Source: {} ({})", source.name(), source.id());
/// }
/// ```
pub async fn list_all(&self) -> Vec<Arc<dyn MusicSource>> {
let sources = self.sources.read().await;
sources.values().cloned().collect()
}
/// Compte le nombre de sources enregistrées
///
/// # Returns
///
/// Le nombre total de sources dans le registre.
///
/// # Examples
///
/// ```ignore
/// let count = registry.count().await;
/// println!("Total sources: {}", count);
/// ```
pub async fn count(&self) -> usize {
let sources = self.sources.read().await;
sources.len()
}
/// Supprime une source du registre
///
/// # Arguments
///
/// * `id` - L'ID de la source à supprimer
///
/// # Returns
///
/// `true` si la source a été supprimée, `false` si elle n'existait pas.
///
/// # Examples
///
/// ```ignore
/// if registry.remove("old-source").await {
/// println!("Source removed");
/// }
/// ```
pub async fn remove(&self, id: &str) -> bool {
let mut sources = self.sources.write().await;
if sources.remove(id).is_some() {
tracing::info!(source_id = %id, "Removed music source");
true
} else {
false
}
}
/// Vérifie si une source est enregistrée
///
/// # Arguments
///
/// * `id` - L'ID de la source à vérifier
///
/// # Returns
///
/// `true` si la source existe, `false` sinon.
///
/// # Examples
///
/// ```ignore
/// if registry.contains("qobuz").await {
/// // La source Qobuz est disponible
/// }
/// ```
pub async fn contains(&self, id: &str) -> bool {
let sources = self.sources.read().await;
sources.contains_key(id)
}
}
impl Default for SourceRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pmodidl::{Container, Item};
use pmosource::{BrowseResult, MusicSource, Result};
use std::time::SystemTime;
#[derive(Debug)]
struct TestSource {
id: String,
name: String,
}
impl TestSource {
fn new(id: &str, name: &str) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
}
}
}
#[async_trait::async_trait]
impl MusicSource for TestSource {
fn name(&self) -> &str {
&self.name
}
fn id(&self) -> &str {
&self.id
}
fn default_image(&self) -> &[u8] {
&[]
}
async fn root_container(&self) -> Result<Container> {
Ok(Container {
id: self.id.clone(),
parent_id: "0".to_string(),
restricted: Some("1".to_string()),
child_count: Some("0".to_string()),
searchable: Some("1".to_string()),
title: self.name.clone(),
class: "object.container".to_string(),
containers: vec![],
items: vec![],
})
}
async fn browse(&self, _object_id: &str) -> Result<BrowseResult> {
Ok(BrowseResult::Items(vec![]))
}
async fn resolve_uri(&self, object_id: &str) -> Result<String> {
Ok(format!("http://example.com/{}", object_id))
}
fn supports_fifo(&self) -> bool {
false
}
async fn append_track(&self, _track: Item) -> Result<()> {
Err(pmosource::MusicSourceError::FifoNotSupported)
}
async fn remove_oldest(&self) -> Result<Option<Item>> {
Err(pmosource::MusicSourceError::FifoNotSupported)
}
async fn update_id(&self) -> u32 {
0
}
async fn last_change(&self) -> Option<SystemTime> {
None
}
async fn get_items(&self, _offset: usize, _count: usize) -> Result<Vec<Item>> {
Ok(vec![])
}
}
#[tokio::test]
async fn test_register_and_get() {
let registry = SourceRegistry::new();
let source = Arc::new(TestSource::new("test-1", "Test Source 1"));
registry.register(source.clone()).await;
let retrieved = registry.get("test-1").await;
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.id(), "test-1");
assert_eq!(retrieved.name(), "Test Source 1");
}
#[tokio::test]
async fn test_list_all() {
let registry = SourceRegistry::new();
registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
registry
.register(Arc::new(TestSource::new("test-2", "Test 2")))
.await;
registry
.register(Arc::new(TestSource::new("test-3", "Test 3")))
.await;
let sources = registry.list_all().await;
assert_eq!(sources.len(), 3);
}
#[tokio::test]
async fn test_count() {
let registry = SourceRegistry::new();
assert_eq!(registry.count().await, 0);
registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
assert_eq!(registry.count().await, 1);
registry
.register(Arc::new(TestSource::new("test-2", "Test 2")))
.await;
assert_eq!(registry.count().await, 2);
}
#[tokio::test]
async fn test_remove() {
let registry = SourceRegistry::new();
registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
assert!(registry.contains("test-1").await);
assert!(registry.remove("test-1").await);
assert!(!registry.contains("test-1").await);
assert!(!registry.remove("test-1").await);
}
#[tokio::test]
async fn test_contains() {
let registry = SourceRegistry::new();
assert!(!registry.contains("test-1").await);
registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
assert!(registry.contains("test-1").await);
assert!(!registry.contains("test-2").await);
}
#[tokio::test]
async fn test_replace_source() {
let registry = SourceRegistry::new();
registry
.register(Arc::new(TestSource::new("test-1", "Old Name")))
.await;
let old = registry.get("test-1").await.unwrap();
assert_eq!(old.name(), "Old Name");
registry
.register(Arc::new(TestSource::new("test-1", "New Name")))
.await;
let new = registry.get("test-1").await.unwrap();
assert_eq!(new.name(), "New Name");
assert_eq!(registry.count().await, 1);
}
}