Ajout de la gestion des couvertures d'albums par le FlacCacheSink

This commit is contained in:
2025-11-03 14:52:04 +01:00
parent 157baadcfd
commit 0603da2998
17 changed files with 93 additions and 35 deletions

View File

@@ -0,0 +1,80 @@
//! Exemple de configuration multiroom avec BufferNode
//!
//! Démontre l'utilisation du buffer circulaire pour synchroniser
//! plusieurs sorties avec des délais différents
use pmoaudio::{BufferNode, SinkNode, SourceNode};
#[tokio::main]
async fn main() {
println!("=== Multiroom Demo ===\n");
// Buffer avec capacité pour gérer les délais
let (buffer, buffer_tx) = BufferNode::new(50, 10);
// Créer 3 sorties avec délais différents
let (sink1, sink1_tx) = SinkNode::new("Room 1 (no delay)".to_string(), 10);
let (sink2, sink2_tx) = SinkNode::new("Room 2 (5 chunks delay)".to_string(), 10);
let (sink3, sink3_tx) = SinkNode::new("Room 3 (10 chunks delay)".to_string(), 10);
buffer.add_subscriber_with_offset(sink1_tx, 0).await;
buffer.add_subscriber_with_offset(sink2_tx, 5).await;
buffer.add_subscriber_with_offset(sink3_tx, 10).await;
// Spawn buffer et sinks
tokio::spawn(async move {
buffer.run().await.unwrap();
});
let sink1_handle = tokio::spawn(async move {
let stats = sink1.run_with_stats().await.unwrap();
stats.display();
stats
});
let sink2_handle = tokio::spawn(async move {
let stats = sink2.run_with_stats().await.unwrap();
stats.display();
stats
});
let sink3_handle = tokio::spawn(async move {
let stats = sink3.run_with_stats().await.unwrap();
stats.display();
stats
});
// Générer de l'audio dans une tâche séparée
println!("Generating audio for multiroom playback...\n");
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(buffer_tx);
source
.generate_chunks(30, 4800, 48000, 440.0)
.await
.unwrap();
});
println!("Waiting for all rooms to finish...\n");
// Attendre toutes les sorties
let stats1 = sink1_handle.await.unwrap();
let stats2 = sink2_handle.await.unwrap();
let stats3 = sink3_handle.await.unwrap();
println!("\n=== Multiroom Summary ===");
println!(
"{}: {} chunks received",
stats1.name, stats1.chunks_received
);
println!(
"{}: {} chunks received",
stats2.name, stats2.chunks_received
);
println!(
"{}: {} chunks received",
stats3.name, stats3.chunks_received
);
println!("\nNote: Delayed rooms receive fewer chunks due to the offset");
}

View File

@@ -0,0 +1,168 @@
//! Exemple complet de pipeline multiroom avec contrôle de volume
//!
//! Ce programme démontre :
//! - Une source audio unique
//! - Deux branches de sortie : Chromecast et DiskSink
//! - Un volume master avec deux VolumeNodes secondaires synchronisés
//! - Système d'événements pour la communication entre nodes
use pmoaudio::{
ChromecastConfig, ChromecastSink, DiskSink, DiskSinkConfig, SourceNode, VolumeNode,
};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== PMOAudio Multiroom Volume Demo ===\n");
// Configuration
let sample_rate = 48000u32;
let chunk_size = 4800usize; // 100ms à 48kHz
let num_chunks = 50; // 5 secondes de lecture
let frequency = 440.0; // La 440 Hz
// ===== 1. Créer la source audio =====
println!("1. Creating audio source...");
let mut source = SourceNode::new();
// ===== 2. Créer le volume master =====
println!("2. Creating master volume node...");
let (mut master_volume, master_tx) = VolumeNode::new("master".to_string(), 1.0, 50);
let master_handle = master_volume.get_handle();
// Channel pour les événements du volume master
let (master_event_tx, master_event_rx_chromecast) = mpsc::channel(10);
let (_, master_event_rx_disk) = mpsc::channel(10);
master_volume.subscribe_volume_events(master_event_tx);
source.add_subscriber(master_tx);
// ===== 3. Créer les branches de sortie =====
// Branche 1: Chromecast avec volume secondaire
println!("3a. Creating Chromecast output branch...");
let (mut chromecast_volume, chromecast_volume_tx) =
VolumeNode::new("chromecast_volume".to_string(), 0.8, 50);
chromecast_volume.set_master_volume_source(master_event_rx_chromecast);
let chromecast_config = ChromecastConfig {
device_address: "192.168.1.100".to_string(),
device_name: "Living Room".to_string(),
..Default::default()
};
let (chromecast_sink, chromecast_sink_tx) =
ChromecastSink::new("chromecast1".to_string(), chromecast_config, 50);
chromecast_volume.add_subscriber(chromecast_sink_tx);
master_volume.add_subscriber(chromecast_volume_tx);
// Branche 2: DiskSink avec volume secondaire
println!("3b. Creating DiskSink output branch...");
let (mut disk_volume, disk_volume_tx) = VolumeNode::new("disk_volume".to_string(), 0.9, 50);
disk_volume.set_master_volume_source(master_event_rx_disk);
let disk_config = DiskSinkConfig {
output_dir: std::env::temp_dir().join("pmoaudio_demo"),
filename: Some("multiroom_output.wav".to_string()),
..Default::default()
};
let (disk_sink, disk_sink_tx) = DiskSink::new("disk1".to_string(), disk_config, 50);
disk_volume.add_subscriber(disk_sink_tx);
master_volume.add_subscriber(disk_volume_tx);
// ===== 4. Lancer tous les nodes =====
println!("4. Starting pipeline nodes...\n");
// Spawn master volume
let master_volume_handle = tokio::spawn(async move {
master_volume.run().await.unwrap();
});
// Spawn chromecast branch
let chromecast_volume_handle = tokio::spawn(async move {
chromecast_volume.run().await.unwrap();
});
let chromecast_sink_handle = tokio::spawn(async move {
let stats = chromecast_sink.run().await.unwrap();
stats.display();
});
// Spawn disk branch
let disk_volume_handle = tokio::spawn(async move {
disk_volume.run().await.unwrap();
});
let disk_sink_handle = tokio::spawn(async move {
let stats = disk_sink.run().await.unwrap();
stats.display();
});
// ===== 5. Contrôler le volume pendant la lecture =====
let master_handle_clone = master_handle.clone();
tokio::spawn(async move {
// Attendre un peu, puis diminuer le volume
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!("\n>>> Decreasing master volume to 0.7");
master_handle_clone.set_volume(0.7).await;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!(">>> Decreasing master volume to 0.4");
master_handle_clone.set_volume(0.4).await;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!(">>> Increasing master volume back to 1.0");
master_handle_clone.set_volume(1.0).await;
});
// ===== 6. Générer et envoyer les chunks audio =====
println!("5. Generating and streaming audio...");
tokio::spawn(async move {
source
.generate_chunks(num_chunks, chunk_size, sample_rate, frequency)
.await
.unwrap();
println!("\n>>> Audio generation complete!");
});
// ===== 7. Attendre la fin de tous les nodes =====
println!("6. Waiting for all nodes to complete...\n");
// Attendre que les sinks terminent
chromecast_sink_handle.await?;
disk_sink_handle.await?;
// Nettoyer
master_volume_handle.abort();
chromecast_volume_handle.abort();
disk_volume_handle.abort();
println!("\n=== Demo completed successfully! ===");
println!("\nSummary:");
println!(
"- Generated {} chunks of {} samples each",
num_chunks, chunk_size
);
println!(
"- Total duration: {:.2} seconds",
(num_chunks as usize * chunk_size) as f32 / sample_rate as f32
);
println!("- Output to Chromecast: Living Room (192.168.1.100)");
println!(
"- Output to file: {}",
std::env::temp_dir()
.join("pmoaudio_demo")
.join("multiroom_output.wav")
.display()
);
println!("- Master volume control demonstrated with live changes");
println!("\nAll streams received synchronized volume updates!");
Ok(())
}

View File

@@ -0,0 +1,126 @@
//! Exemple de pipeline audio stéréo complet avec tous les nodes
//!
//! Pipeline: SourceNode → DecoderNode → DspNode → BufferNode → TimerNode → SinkNode(s)
use pmoaudio::{BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
println!("=== PMOAudio Pipeline Demo ===\n");
// Créer le pipeline de nodes
// 2. DecoderNode - passthrough dans cet exemple
let (mut decoder, decoder_tx) = DecoderNode::new(10);
// 3. DspNode - applique un gain de 0.5
let (mut dsp, dsp_tx) = DspNode::new(10, 0.5);
// 4. BufferNode - buffer circulaire pour multiroom
let (mut buffer, buffer_tx) = BufferNode::new(100, 10);
// 5. TimerNode - calcule la position temporelle
let (mut timer, timer_tx) = TimerNode::new(10);
// 6. SinkNodes - deux destinations finales
let (sink1, sink1_tx) = SinkNode::new("Main Output".to_string(), 10);
let (sink2, sink2_tx) = SinkNode::new("Secondary Output".to_string(), 10);
// Ajouter un abonné au BufferNode avec offset (multiroom simulation)
let (sink3, sink3_tx) = SinkNode::new("Delayed Output".to_string(), 10);
buffer.add_subscriber_with_offset(sink3_tx, 5).await; // 5 chunks de retard
// Connecter le pipeline
decoder.add_subscriber(dsp_tx);
dsp.add_subscriber(buffer_tx);
buffer.add_next_subscriber(timer_tx); // BufferNode -> TimerNode
timer.add_subscriber(sink1_tx);
timer.add_subscriber(sink2_tx);
// Obtenir un handle pour lire la position du TimerNode
let timer_handle = timer.get_position_handle();
// Spawn tous les nodes
let decoder_handle = tokio::spawn(async move {
decoder.run_passthrough().await.unwrap();
});
let dsp_handle = tokio::spawn(async move {
dsp.run().await.unwrap();
});
let buffer_handle = tokio::spawn(async move {
buffer.run().await.unwrap();
});
let timer_handle_task = tokio::spawn(async move {
timer.run().await.unwrap();
});
let sink1_handle = tokio::spawn(async move {
let stats = sink1.run_with_stats().await.unwrap();
stats.display();
stats
});
let sink2_handle = tokio::spawn(async move {
sink2.run_silent().await.unwrap();
});
let sink3_handle = tokio::spawn(async move {
let stats = sink3.run_with_stats().await.unwrap();
stats.display();
stats
});
// Spawn une tâche pour afficher la position périodiquement
let position_monitor = tokio::spawn(async move {
for _ in 0..10 {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let position = timer_handle.position_sec().await;
let samples = timer_handle.elapsed_samples().await;
println!("Position: {:.3} sec ({} samples)", position, samples);
}
});
// Générer des chunks audio
println!("Generating audio chunks...\n");
let chunk_size = 4800; // 100ms à 48kHz
let sample_rate = 48000;
let frequency = 440.0; // La 440Hz
// Source node dans une tâche séparée
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(decoder_tx);
// Générer 50 chunks (environ 5 secondes)
source
.generate_chunks(50, chunk_size, sample_rate, frequency)
.await
.unwrap();
println!("\nChunks sent. Processing...\n");
});
// Attendre que tous les nodes terminent
decoder_handle.await.unwrap();
dsp_handle.await.unwrap();
buffer_handle.await.unwrap();
timer_handle_task.await.unwrap();
let stats1 = sink1_handle.await.unwrap();
sink2_handle.await.unwrap();
let stats3 = sink3_handle.await.unwrap();
position_monitor.await.unwrap();
println!("\n=== Pipeline Demo Complete ===");
println!(
"Main output processed: {} chunks, {:.3} sec",
stats1.chunks_received, stats1.total_duration_sec
);
println!(
"Delayed output processed: {} chunks, {:.3} sec",
stats3.chunks_received, stats3.total_duration_sec
);
}

View File

@@ -0,0 +1,96 @@
//! Quick Start - Démonstration rapide des nouvelles fonctionnalités
//!
//! Cet exemple montre l'utilisation des principales nouvelles fonctionnalités :
//! - VolumeNode avec contrôle dynamique
//! - DiskSink pour écriture sur disque
//! - Pipeline simple et efficace
use pmoaudio::{AudioFileFormat, DiskSink, DiskSinkConfig, SourceNode, VolumeNode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== PMOAudio Quick Start ===\n");
// 1. Créer la source audio (génère un signal de test)
let mut source = SourceNode::new();
// 2. Créer un VolumeNode pour contrôler le volume
let (mut volume, volume_tx) = VolumeNode::new("main".to_string(), 0.8, 10);
let volume_handle = volume.get_handle();
// 3. Créer un DiskSink pour écrire sur disque
let output_dir = std::env::temp_dir().join("pmoaudio_quickstart");
let config = DiskSinkConfig {
output_dir: output_dir.clone(),
filename: Some("quickstart_output.wav".to_string()),
format: AudioFileFormat::Wav,
buffer_size: 50,
};
let (disk_sink, disk_tx) = DiskSink::new("disk".to_string(), config, 10);
// 4. Connecter le pipeline : Source → Volume → DiskSink
source.add_subscriber(volume_tx);
volume.add_subscriber(disk_tx);
println!("Pipeline configured:");
println!(" SourceNode → VolumeNode (vol=0.8) → DiskSink");
println!(" Output: {}/quickstart_output.wav\n", output_dir.display());
// 5. Lancer les nodes
let volume_handle_clone = volume_handle.clone();
tokio::spawn(async move {
volume.run().await.unwrap();
});
let disk_handle = tokio::spawn(async move {
let stats = disk_sink.run().await.unwrap();
println!("\nDiskSink Statistics:");
stats.display();
stats
});
// 6. Démonstration du contrôle de volume pendant la lecture
tokio::spawn(async move {
println!("Generating audio with volume changes...");
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
println!(" → Volume: 0.8 (initial)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
volume_handle_clone.set_volume(0.5).await;
println!(" → Volume: 0.5 (decreased)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
volume_handle_clone.set_volume(1.0).await;
println!(" → Volume: 1.0 (maximum)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
volume_handle_clone.set_volume(0.3).await;
println!(" → Volume: 0.3 (low)");
});
// 7. Générer l'audio (10 chunks de 4800 samples à 48kHz = ~1 seconde)
source
.generate_chunks(
10, // nombre de chunks
4800, // samples par chunk (100ms @ 48kHz)
48000, // sample rate
440.0, // fréquence (La 440 Hz)
)
.await?;
// 8. Attendre la fin du traitement
let stats = disk_handle.await?;
// 9. Résumé
println!("\n=== Summary ===");
println!("✓ Audio file generated successfully");
println!("{} chunks written", stats.chunks_written);
println!("✓ Duration: {:.2} seconds", stats.total_duration_sec);
println!("✓ Volume was dynamically adjusted during playback");
println!("\nYou can play the file with:");
println!(" ffplay {}/quickstart_output.wav", output_dir.display());
Ok(())
}

View File

@@ -0,0 +1,53 @@
//! Exemple simple de pipeline audio : Source → Timer → Sink
//!
//! Démontre l'utilisation basique du pipeline avec calcul de position
use pmoaudio::{SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
println!("=== Simple Pipeline Example ===\n");
// Créer les nodes
let (mut timer, timer_tx) = TimerNode::new(10);
let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
// Connecter
timer.add_subscriber(sink_tx);
// Handle pour monitorer la position
let timer_handle = timer.get_position_handle();
// Spawn timer et sink
tokio::spawn(async move {
timer.run().await.unwrap();
});
let sink_handle = tokio::spawn(async move {
let stats = sink.run_with_stats().await.unwrap();
stats.display();
stats
});
// Générer quelques secondes d'audio dans une tâche séparée
println!("Generating 440Hz sine wave...\n");
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(timer_tx);
source
.generate_chunks(30, 4800, 48000, 440.0) // ~3 secondes
.await
.unwrap();
// La source est drop ici, fermant le channel
});
// Attendre la fin
let stats = sink_handle.await.unwrap();
let final_position = timer_handle.position_sec().await;
println!("\nFinal position: {:.3} seconds", final_position);
println!("Total duration: {:.3} seconds", stats.total_duration_sec);
}

View File

@@ -0,0 +1,52 @@
//! Exemple de streaming audio en temps réel
//!
//! Démontre l'utilisation du pipeline avec génération de chunks
//! en temps réel avec timing approprié
use pmoaudio::{SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
println!("=== Streaming Demo ===\n");
println!("Streaming audio in real-time for 3 seconds...\n");
let mut source = SourceNode::new();
let (mut timer, timer_tx) = TimerNode::new(20);
let (sink, sink_tx) = SinkNode::new("Streaming Output".to_string(), 20);
source.add_subscriber(timer_tx);
timer.add_subscriber(sink_tx);
let timer_handle = timer.get_position_handle();
// Spawn le pipeline
tokio::spawn(async move {
timer.run().await.unwrap();
});
let sink_handle = tokio::spawn(async move {
sink.run_with_logging().await.unwrap();
});
// Monitor la position
let monitor_handle = tokio::spawn(async move {
for _ in 0..15 {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let position = timer_handle.position_sec().await;
println!("Playback position: {:.3} sec", position);
}
});
// Stream des chunks avec timing réel
// 100ms par chunk à 48kHz = 4800 samples
source
.stream_chunks(4800, 48000, 440.0, 3000) // 3 secondes
.await
.unwrap();
println!("\nStreaming complete.");
// Attendre la fin
sink_handle.await.unwrap();
monitor_handle.await.unwrap();
}

View File

@@ -0,0 +1,58 @@
//! Exemple simple de contrôle de volume
//!
//! Démontre l'utilisation du VolumeNode avec changements dynamiques
use pmoaudio::{SinkNode, SourceNode, VolumeNode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Volume Control Demo ===\n");
// Créer la source
let mut source = SourceNode::new();
// Créer le volume node
let (mut volume, volume_tx) = VolumeNode::new("main".to_string(), 1.0, 10);
let volume_handle = volume.get_handle();
// Créer le sink
let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
// Connecter le pipeline
source.add_subscriber(volume_tx);
volume.add_subscriber(sink_tx);
// Lancer les nodes
tokio::spawn(async move { volume.run().await.unwrap() });
let sink_handle = tokio::spawn(async move { sink.run_with_stats().await.unwrap() });
// Contrôler le volume pendant la lecture
let volume_control = tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
println!("Setting volume to 0.5");
volume_handle.set_volume(0.5).await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
println!("Setting volume to 0.2");
volume_handle.set_volume(0.2).await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
println!("Setting volume to 1.0");
volume_handle.set_volume(1.0).await;
});
// Générer l'audio
source
.generate_chunks(20, 4800, 48000, 440.0)
.await
.unwrap();
volume_control.await?;
let stats = sink_handle.await?;
println!("\nFinal statistics:");
stats.display();
Ok(())
}

View File

@@ -0,0 +1,170 @@
//! Exemple d'utilisation basique de pmoplaylist
//!
//! Pour exécuter cet exemple :
//! ```bash
//! cargo run -p pmoplaylist --example basic_usage
//! ```
use pmoplaylist::{FifoPlaylist, Track, DEFAULT_IMAGE};
#[tokio::main]
async fn main() {
println!("=== Exemple pmoplaylist ===\n");
// 1. Créer une playlist FIFO
println!("1. Création d'une playlist avec capacité de 5 tracks...");
let playlist = FifoPlaylist::new(
"my-radio".to_string(),
"Ma Radio Préférée".to_string(),
5,
DEFAULT_IMAGE,
);
println!(" ✓ Playlist créée: {}", playlist.title().await);
println!(" ✓ ID: {}", playlist.id().await);
println!(" ✓ Capacité: 5 tracks");
println!(" ✓ Update ID initial: {}\n", playlist.update_id().await);
// 2. Ajouter des tracks
println!("2. Ajout de 3 tracks...");
let tracks = vec![
Track::new(
"track-1",
"Bohemian Rhapsody",
"http://example.com/queen/bohemian.flac",
)
.with_artist("Queen")
.with_album("A Night at the Opera")
.with_duration(354)
.with_image("http://example.com/covers/queen-anato.jpg"),
Track::new(
"track-2",
"Stairway to Heaven",
"http://example.com/zeppelin/stairway.mp3",
)
.with_artist("Led Zeppelin")
.with_album("Led Zeppelin IV")
.with_duration(482),
Track::new(
"track-3",
"Hotel California",
"http://example.com/eagles/hotel.flac",
)
.with_artist("Eagles")
.with_album("Hotel California")
.with_duration(391),
];
for track in tracks {
playlist.append_track(track.clone()).await;
println!(
" ✓ Ajouté: {} - {}",
track.title,
track.artist.unwrap_or_default()
);
}
println!("\n Total tracks: {}", playlist.len().await);
println!(" Update ID: {}\n", playlist.update_id().await);
// 3. Tester le comportement FIFO
println!("3. Test du comportement FIFO (capacité = 5)...");
println!(" Ajout de 4 tracks supplémentaires...");
for i in 4..=7 {
let track = Track::new(
format!("track-{}", i),
format!("Song Number {}", i),
format!("http://example.com/songs/{}.mp3", i),
);
playlist.append_track(track).await;
}
println!(
" ✓ Total tracks (limité par capacité): {}",
playlist.len().await
);
// Afficher les tracks actuels
let items = playlist.get_items(0, 10).await;
println!("\n Tracks actuels dans la FIFO:");
for (idx, track) in items.iter().enumerate() {
println!(" {}. {} ({})", idx + 1, track.title, track.id);
}
println!(" (Les tracks 1 et 2 ont été supprimés automatiquement)\n");
// 4. Supprimer le plus ancien
println!("4. Suppression du track le plus ancien...");
if let Some(removed) = playlist.remove_oldest().await {
println!(" ✓ Supprimé: {} ({})", removed.title, removed.id);
}
println!(" Total tracks: {}", playlist.len().await);
println!(" Update ID: {}\n", playlist.update_id().await);
// 5. Supprimer par ID
println!("5. Suppression d'un track par ID (track-5)...");
if playlist.remove_by_id("track-5").await {
println!(" ✓ Track supprimé");
}
println!(" Total tracks: {}", playlist.len().await);
println!(" Update ID: {}\n", playlist.update_id().await);
// 6. Générer un Container DIDL-Lite
println!("6. Génération du Container DIDL-Lite...");
let container = playlist.as_container().await;
println!(" Container:");
println!(" - ID: {}", container.id);
println!(" - Parent ID: {}", container.parent_id);
println!(" - Title: {}", container.title);
println!(" - Class: {}", container.class);
println!(
" - Child Count: {}\n",
container.child_count.unwrap_or_default()
);
// 7. Générer des Items DIDL-Lite
println!("7. Génération des Items DIDL-Lite...");
let didl_items = playlist
.as_objects(0, 10, Some("http://myserver/api/default-image"))
.await;
println!(" Items DIDL-Lite:");
for (idx, item) in didl_items.iter().enumerate() {
println!("\n Item {}:", idx + 1);
println!(" - ID: {}", item.id);
println!(" - Title: {}", item.title);
println!(" - Artist: {}", item.artist.as_deref().unwrap_or("N/A"));
println!(" - Album: {}", item.album.as_deref().unwrap_or("N/A"));
println!(" - Class: {}", item.class);
println!(" - Parent ID: {}", item.parent_id);
if !item.resources.is_empty() {
println!(" - Resource URI: {}", item.resources[0].url);
if let Some(ref duration) = item.resources[0].duration {
println!(" - Duration: {}", duration);
}
}
if let Some(ref art) = item.album_art {
println!(" - Album Art: {}", art);
}
}
// 8. Image par défaut
println!("\n8. Image par défaut...");
let default_image = playlist.default_image().await;
println!(
" ✓ Taille de l'image par défaut: {} bytes",
default_image.len()
);
println!(" (Cette image peut être servie via un endpoint HTTP)\n");
// 9. Vider la playlist
println!("9. Vidage de la playlist...");
playlist.clear().await;
println!(" ✓ Playlist vidée");
println!(" Total tracks: {}", playlist.len().await);
println!(" Is empty: {}", playlist.is_empty().await);
println!(" Update ID final: {}\n", playlist.update_id().await);
println!("=== Exemple terminé ===");
}

View File

@@ -0,0 +1,217 @@
//! Exemple d'intégration avec un serveur HTTP
//!
//! Cet exemple montre comment exposer une playlist FIFO via des endpoints HTTP simples.
//! Dans un vrai MediaServer UPnP, ces endpoints seraient appelés par le protocole ContentDirectory.
//!
//! Pour exécuter :
//! ```bash
//! cargo run -p pmoplaylist --example http_server_integration
//! ```
use pmoplaylist::{FifoPlaylist, Track, DEFAULT_IMAGE};
use std::sync::Arc;
#[tokio::main]
async fn main() {
println!("=== Intégration HTTP Server ===\n");
// Créer une playlist partagée
let playlist = Arc::new(FifoPlaylist::new(
"my-radio".to_string(),
"My Internet Radio".to_string(),
20,
DEFAULT_IMAGE,
));
println!("📻 Playlist créée: {}", playlist.title().await);
println!("🆔 ID: {}\n", playlist.id().await);
// Ajouter quelques tracks initiaux
println!("📝 Ajout de tracks initiaux...");
let initial_tracks = vec![
("The Beatles", "Come Together", "Abbey Road", 259),
("Nirvana", "Smells Like Teen Spirit", "Nevermind", 301),
("Queen", "Bohemian Rhapsody", "A Night at the Opera", 354),
];
for (idx, (artist, title, album, duration)) in initial_tracks.iter().enumerate() {
playlist
.append_track(
Track::new(
format!("track-{}", idx),
*title,
format!("http://media.server/music/{}.flac", idx),
)
.with_artist(*artist)
.with_album(*album)
.with_duration(*duration)
.with_image(format!("http://media.server/covers/{}.jpg", idx)),
)
.await;
println!("{} - {}", artist, title);
}
println!();
// Simuler différents endpoints HTTP
// 1. GET /playlist/container - Retourne le container DIDL-Lite
println!("🌐 Endpoint: GET /playlist/container");
simulate_get_container(playlist.clone()).await;
println!();
// 2. GET /playlist/items?offset=0&count=10 - Retourne les items
println!("🌐 Endpoint: GET /playlist/items?offset=0&count=10");
simulate_get_items(playlist.clone(), 0, 10).await;
println!();
// 3. GET /playlist/metadata - Retourne les métadonnées
println!("🌐 Endpoint: GET /playlist/metadata");
simulate_get_metadata(playlist.clone()).await;
println!();
// 4. POST /playlist/track - Ajoute un nouveau track
println!("🌐 Endpoint: POST /playlist/track");
let new_track = Track::new(
"track-new-1",
"Stairway to Heaven",
"http://media.server/music/stairway.flac",
)
.with_artist("Led Zeppelin")
.with_album("Led Zeppelin IV")
.with_duration(482);
simulate_add_track(playlist.clone(), new_track).await;
println!();
// 5. DELETE /playlist/oldest - Supprime le plus ancien
println!("🌐 Endpoint: DELETE /playlist/oldest");
simulate_delete_oldest(playlist.clone()).await;
println!();
// 6. GET /playlist/default-image - Retourne l'image par défaut
println!("🌐 Endpoint: GET /playlist/default-image");
simulate_get_default_image(playlist.clone()).await;
println!();
// 7. Vérifier l'état final
println!("📊 État final:");
let final_items = playlist.get_items(0, 10).await;
println!(" Total tracks: {}", playlist.len().await);
println!(" Update ID: {}", playlist.update_id().await);
println!("\n Tracks actuels:");
for (idx, track) in final_items.iter().enumerate() {
let artist = track.artist.as_deref().unwrap_or("Unknown");
println!(" {}. {} - {}", idx + 1, artist, track.title);
}
println!("\n=== Exemple terminé ===");
}
/// Simule GET /playlist/container
async fn simulate_get_container(playlist: Arc<FifoPlaylist>) {
let container = playlist.as_container().await;
println!(" Response (JSON representation):");
println!(" {{");
println!(" \"id\": \"{}\",", container.id);
println!(" \"parentId\": \"{}\",", container.parent_id);
println!(" \"title\": \"{}\",", container.title);
println!(" \"class\": \"{}\",", container.class);
println!(
" \"childCount\": {}",
container.child_count.unwrap_or_default()
);
println!(" }}");
}
/// Simule GET /playlist/items?offset=X&count=Y
async fn simulate_get_items(playlist: Arc<FifoPlaylist>, offset: usize, count: usize) {
let items = playlist
.as_objects(offset, count, Some("http://media.server/api/default-image"))
.await;
println!(" Response: {} items", items.len());
println!(" [");
for (idx, item) in items.iter().enumerate() {
println!(" {{");
println!(" \"id\": \"{}\",", item.id);
println!(" \"title\": \"{}\",", item.title);
println!(
" \"artist\": \"{}\",",
item.artist.as_deref().unwrap_or("")
);
println!(
" \"album\": \"{}\",",
item.album.as_deref().unwrap_or("")
);
println!(" \"class\": \"{}\",", item.class);
if !item.resources.is_empty() {
println!(" \"uri\": \"{}\",", item.resources[0].url);
}
print!(" }}");
if idx < items.len() - 1 {
println!(",");
} else {
println!();
}
}
println!(" ]");
}
/// Simule GET /playlist/metadata
async fn simulate_get_metadata(playlist: Arc<FifoPlaylist>) {
let update_id = playlist.update_id().await;
let last_change = playlist.last_change().await;
let count = playlist.len().await;
let id = playlist.id().await;
let title = playlist.title().await;
println!(" Response:");
println!(" {{");
println!(" \"id\": \"{}\",", id);
println!(" \"title\": \"{}\",", title);
println!(" \"trackCount\": {},", count);
println!(" \"updateId\": {},", update_id);
println!(" \"lastChange\": \"{:?}\"", last_change);
println!(" }}");
}
/// Simule POST /playlist/track
async fn simulate_add_track(playlist: Arc<FifoPlaylist>, track: Track) {
let old_update_id = playlist.update_id().await;
playlist.append_track(track.clone()).await;
let new_update_id = playlist.update_id().await;
println!(
" Track added: {} - {}",
track.artist.as_deref().unwrap_or("Unknown"),
track.title
);
println!(" Update ID: {}{}", old_update_id, new_update_id);
println!(" Response: 201 Created");
}
/// Simule DELETE /playlist/oldest
async fn simulate_delete_oldest(playlist: Arc<FifoPlaylist>) {
if let Some(removed) = playlist.remove_oldest().await {
println!(" Track removed: {} ({})", removed.title, removed.id);
println!(" New update ID: {}", playlist.update_id().await);
println!(" Response: 200 OK");
} else {
println!(" No tracks to remove");
println!(" Response: 404 Not Found");
}
}
/// Simule GET /playlist/default-image
async fn simulate_get_default_image(playlist: Arc<FifoPlaylist>) {
let image_bytes = playlist.default_image().await;
println!(" Response:");
println!(" Content-Type: image/webp");
println!(" Content-Length: {} bytes", image_bytes.len());
println!(" Status: 200 OK");
println!(" (Image WebP {} bytes ready to serve)", image_bytes.len());
}

View File

@@ -0,0 +1,198 @@
//! Exemple simulant une radio en streaming
//!
//! Cet exemple démontre :
//! - L'utilisation de FifoPlaylist dans un contexte multi-thread
//! - La simulation d'un flux radio continu
//! - La surveillance des changements via update_id
//!
//! Pour exécuter :
//! ```bash
//! cargo run -p pmoplaylist --example radio_streaming
//! ```
use pmoplaylist::{FifoPlaylist, Track, DEFAULT_IMAGE};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
println!("=== Simulation Radio en Streaming ===\n");
// Créer une radio avec historique limité à 10 tracks
let radio = FifoPlaylist::new(
"radio-paradise".to_string(),
"Radio Paradise - Main Mix".to_string(),
10,
DEFAULT_IMAGE,
);
println!("📻 Radio créée: {}", radio.title().await);
println!("📊 Capacité: 10 tracks (historique limité)");
println!("🆔 ID: {}\n", radio.id().await);
// Cloner pour les différentes tâches
let radio_streamer = radio.clone();
let radio_monitor = radio.clone();
let radio_client = radio.clone();
// Tâche 1: Simuler le streaming (ajoute des tracks régulièrement)
let streamer = tokio::spawn(async move {
println!("🎵 [STREAMER] Démarrage du flux radio...\n");
let tracks_data = vec![
("Radiohead", "Paranoid Android", "OK Computer", 383),
("Massive Attack", "Teardrop", "Mezzanine", 329),
(
"Pink Floyd",
"Shine On You Crazy Diamond",
"Wish You Were Here",
810,
),
("Portishead", "Glory Box", "Dummy", 305),
("Dire Straits", "Sultans of Swing", "Dire Straits", 349),
("The Cure", "Pictures of You", "Disintegration", 428),
("David Bowie", "Heroes", "Heroes", 371),
(
"Talking Heads",
"Once in a Lifetime",
"Remain in Light",
259,
),
("Fleetwood Mac", "Dreams", "Rumours", 257),
(
"The Smiths",
"There Is a Light That Never Goes Out",
"The Queen Is Dead",
244,
),
("Joy Division", "Love Will Tear Us Apart", "Closer", 206),
("New Order", "Blue Monday", "Power, Corruption & Lies", 448),
("Depeche Mode", "Enjoy the Silence", "Violator", 376),
("R.E.M.", "Losing My Religion", "Out of Time", 269),
(
"U2",
"Where the Streets Have No Name",
"The Joshua Tree",
337,
),
];
for (idx, (artist, title, album, duration)) in tracks_data.iter().enumerate() {
let track = Track::new(
format!("radio-track-{}", idx),
*title,
format!("http://stream.radioparadise.com/track/{}", idx),
)
.with_artist(*artist)
.with_album(*album)
.with_duration(*duration);
radio_streamer.append_track(track).await;
println!("🎵 [STREAMER] Now Playing: {} - {}", artist, title);
// Simuler l'attente entre les tracks
sleep(Duration::from_millis(500)).await;
}
println!("\n🎵 [STREAMER] Fin du streaming");
});
// Tâche 2: Monitorer les changements (update_id)
let monitor = tokio::spawn(async move {
sleep(Duration::from_millis(100)).await;
println!("👁️ [MONITOR] Surveillance des changements...\n");
let mut last_update_id = 0;
let mut iterations = 0;
loop {
let current_update_id = radio_monitor.update_id().await;
let count = radio_monitor.len().await;
if current_update_id != last_update_id {
println!(
"👁️ [MONITOR] Changement détecté! Update ID: {}{} | Tracks: {}",
last_update_id, current_update_id, count
);
last_update_id = current_update_id;
}
iterations += 1;
if iterations >= 50 {
break;
}
sleep(Duration::from_millis(200)).await;
}
println!("\n👁️ [MONITOR] Fin de la surveillance");
});
// Tâche 3: Client consultant l'historique
let client = tokio::spawn(async move {
sleep(Duration::from_millis(2000)).await;
println!("\n📱 [CLIENT] Consultation de l'historique de la radio...\n");
// Consulter plusieurs fois pendant le streaming
for i in 0..3 {
sleep(Duration::from_millis(2000)).await;
let history = radio_client.get_items(0, 10).await;
let update_id = radio_client.update_id().await;
println!(
"📱 [CLIENT] Consultation #{} (Update ID: {})",
i + 1,
update_id
);
println!(" Historique actuel ({} tracks):", history.len());
for (idx, track) in history.iter().enumerate() {
let artist = track.artist.as_deref().unwrap_or("Unknown");
println!(" {}. {} - {}", idx + 1, artist, track.title);
}
println!();
}
// Générer le container DIDL-Lite à la fin
println!("📱 [CLIENT] Génération du Container DIDL-Lite...");
let container = radio_client.as_container().await;
println!(" Container ID: {}", container.id);
println!(" Title: {}", container.title);
println!(
" Child Count: {}",
container.child_count.unwrap_or_default()
);
println!("\n📱 [CLIENT] Fin de la consultation");
});
// Attendre que toutes les tâches se terminent
let _ = tokio::join!(streamer, monitor, client);
// Afficher l'état final
println!("\n=== État Final ===");
println!("📊 Total tracks dans la radio: {}", radio.len().await);
println!("🆔 Update ID final: {}", radio.update_id().await);
let final_history = radio.get_items(0, 10).await;
println!("\n🎵 Historique final (10 derniers tracks):");
for (idx, track) in final_history.iter().enumerate() {
let artist = track.artist.as_deref().unwrap_or("Unknown");
let duration_min = track.duration.map(|d| d / 60).unwrap_or(0);
let duration_sec = track.duration.map(|d| d % 60).unwrap_or(0);
println!(
" {}. {} - {} ({}:{:02})",
idx + 1,
artist,
track.title,
duration_min,
duration_sec
);
}
println!("\n=== Simulation terminée ===");
}