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 40950164e9
commit 83d7520840
17 changed files with 93 additions and 35 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
Cargo.lock generated
View File

@@ -2660,6 +2660,7 @@ dependencies = [
"async-trait",
"pmoaudio",
"pmoaudiocache",
"pmocovers",
"pmoflac",
"pmometadata",
"pmoplaylist",

View File

@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
# Core audio types
pmoaudio = { path = "../pmoaudio" }
pmocovers = { path = "../pmocovers"}
# Optional dependencies for cache-sink feature
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
@@ -14,7 +15,6 @@ pmometadata = { path = "../pmometadata", optional = true }
# Optional dependency for playlist integration
pmoplaylist = { path = "../pmoplaylist", optional = true }
# Async runtime
tokio = { version = "1.0", features = ["full"] }
tokio-util = { version = "0.7" }

View File

@@ -19,6 +19,7 @@ use tokio::{
sync::{mpsc, RwLock},
};
use tokio_util::sync::CancellationToken;
use tracing::warn;
/// Sink qui encode les `AudioSegment` reçus au format FLAC et les stocke dans le cache audio.
///
@@ -33,6 +34,7 @@ pub struct FlacCacheSink {
tx: mpsc::Sender<Arc<AudioSegment>>,
rx: mpsc::Receiver<Arc<AudioSegment>>,
cache: Arc<pmoaudiocache::Cache>,
covers: Arc<pmocovers::Cache>,
collection: Option<String>,
encoder_options: EncoderOptions,
pcm_buffer_capacity: usize,
@@ -46,8 +48,8 @@ impl FlacCacheSink {
/// # Arguments
///
/// * `cache` - Arc vers le cache audio où stocker les fichiers FLAC encodés
pub fn new(cache: Arc<pmoaudiocache::Cache>) -> Self {
Self::with_channel_size(cache, DEFAULT_CHANNEL_SIZE)
pub fn new(cache: Arc<pmoaudiocache::Cache>, covers: Arc<pmocovers::Cache>) -> Self {
Self::with_channel_size(cache, covers, DEFAULT_CHANNEL_SIZE)
}
/// Crée un sink FLAC cache avec une taille de buffer MPSC personnalisée.
@@ -56,8 +58,12 @@ impl FlacCacheSink {
///
/// * `cache` - Arc vers le cache audio
/// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure)
pub fn with_channel_size(cache: Arc<pmoaudiocache::Cache>, channel_size: usize) -> Self {
Self::with_config(cache, channel_size, EncoderOptions::default(), None)
pub fn with_channel_size(
cache: Arc<pmoaudiocache::Cache>,
covers: Arc<pmocovers::Cache>,
channel_size: usize,
) -> Self {
Self::with_config(cache, covers, channel_size, EncoderOptions::default(), None)
}
/// Crée un sink FLAC cache avec une configuration complète.
@@ -70,6 +76,7 @@ impl FlacCacheSink {
/// * `collection` - Collection optionnelle à laquelle appartiennent les fichiers
pub fn with_config(
cache: Arc<pmoaudiocache::Cache>,
covers: Arc<pmocovers::Cache>,
channel_size: usize,
encoder_options: EncoderOptions,
collection: Option<String>,
@@ -79,6 +86,7 @@ impl FlacCacheSink {
tx,
rx,
cache,
covers,
collection,
encoder_options,
pcm_buffer_capacity: 8,
@@ -109,6 +117,7 @@ impl FlacCacheSink {
tx: _,
mut rx,
cache,
covers,
collection,
encoder_options,
pcm_buffer_capacity,
@@ -182,7 +191,9 @@ impl FlacCacheSink {
let copy_future = async {
tokio::io::copy(&mut flac_stream, &mut flac_buffer)
.await
.map_err(|e| AudioError::ProcessingError(format!("FLAC write failed: {}", e)))?;
.map_err(|e| {
AudioError::ProcessingError(format!("FLAC write failed: {}", e))
})?;
flac_stream
.wait()
.await
@@ -191,8 +202,10 @@ impl FlacCacheSink {
};
// Attendre les deux tâches en parallèle
let (copy_result, pump_result): (Result<(), AudioError>, Result<(u64, u64, f64, StopReason), AudioError>) =
tokio::join!(copy_future, pump_future);
let (copy_result, pump_result): (
Result<(), AudioError>,
Result<(u64, u64, f64, StopReason), AudioError>,
) = tokio::join!(copy_future, pump_future);
copy_result?;
let (chunks, samples, duration_sec, stop_reason) = pump_result?;
@@ -200,7 +213,12 @@ impl FlacCacheSink {
let flac_reader = Cursor::new(flac_buffer.clone());
let collection_ref = collection.as_deref();
let pk = cache
.add_from_reader(None, flac_reader, Some(flac_buffer.len() as u64), collection_ref)
.add_from_reader(
None,
flac_reader,
Some(flac_buffer.len() as u64),
collection_ref,
)
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to add to cache: {}", e))
@@ -219,15 +237,42 @@ impl FlacCacheSink {
e
))
})?;
let url = match dest_metadata.read().await.get_cover_url().await {
Ok(url) => url,
Err(e) if e.is_transient() => None,
Err(_) => {
warn!("Cannot obtain cover for audio asset {}", pk);
None
}
};
if url.is_some() {
let _ = match covers
.add_from_url(&url.unwrap(), collection.as_deref())
.await
{
Ok(pk_covers) => {
dest_metadata
.write()
.await
.set_cover_pk(Some(pk_covers))
.await
}
Err(_) => {
warn!("Cannot obtain cover for audio asset {}", pk);
Ok(Some(()))
}
};
}
}
// Ajouter à la playlist si enregistrée
#[cfg(feature = "playlist")]
if let Some(ref playlist_handle) = playlist_handle {
playlist_handle.push(pk.clone()).await
.map_err(|e| AudioError::ProcessingError(
format!("Failed to add to playlist: {}", e)
))?;
playlist_handle.push(pk.clone()).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to add to playlist: {}", e))
})?;
}
// Ajouter les stats de cette track
@@ -600,10 +645,7 @@ impl AudioPipelineNode for FlacCacheSink {
panic!("FlacCacheSink is a terminal sink and cannot have children");
}
async fn run(
self: Box<Self>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
async fn run(self: Box<Self>, stop_token: CancellationToken) -> Result<(), AudioError> {
self.run_internal(stop_token).await?;
Ok(())
}

View File

@@ -763,7 +763,7 @@ mod tests {
if let Some(marker) = segment.as_sync_marker() {
if let crate::SyncMarker::TrackBoundary { metadata, .. } = &**marker {
// Vérifier que le titre extrait est "Test Radio Stream"
if let Some(title) = metadata.get_title().await.ok().flatten() {
if let Some(title) = metadata.read().await.get_title().await.ok().flatten() {
assert_eq!(title, "Test Radio Stream");
found_metadata = true;
}
@@ -888,7 +888,7 @@ mod tests {
while let Some(segment) = rx.recv().await {
if let Some(marker) = segment.as_sync_marker() {
if let crate::SyncMarker::TrackBoundary { metadata, .. } = &**marker {
if let Some(title) = metadata.get_title().await.ok().flatten() {
if let Some(title) = metadata.read().await.get_title().await.ok().flatten() {
assert_eq!(title, "my-song.flac");
found_title = true;
}

View File

@@ -1,5 +1,11 @@
//! Tests d'intégration pour le pipeline audio complet
//!
//! NOTE: Ces tests utilisent l'ancienne API (BufferNode, DecoderNode, etc.)
//! qui a été temporairement désactivée. Ils doivent être réécrits pour
//! utiliser la nouvelle architecture de pipeline (FileSource, HttpSource, FlacFileSink, etc.)
// Désactivé temporairement - ancienne API non disponible
/*
use pmoaudio::{AudioChunk, BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode};
#[tokio::test]
@@ -160,3 +166,4 @@ async fn test_arc_sharing() {
assert_eq!(stats2.chunks_received, 5);
assert_eq!(stats1.total_samples, stats2.total_samples);
}
*/

View File

@@ -274,24 +274,31 @@ mod tests {
let pk = "track-test";
cache.db.add(pk, None, None).unwrap();
let mut meta = cache.track_metadata(pk);
let track = cache.track_metadata(pk);
meta.set_title(Some("Title".into())).await.unwrap();
meta.set_artist(Some("Artist".into())).await.unwrap();
meta.set_album(Some("Album".into())).await.unwrap();
meta.set_year(Some(2024)).await.unwrap();
meta.set_duration(Some(Duration::from_secs(90)))
.await
.unwrap();
meta.set_track_id(Some("trk".into())).await.unwrap();
meta.set_channel_id(Some("chn".into())).await.unwrap();
meta.set_event(Some("event".into())).await.unwrap();
meta.set_rating(Some(4.5)).await.unwrap();
meta.set_cover_url(Some("http://cover".into()))
.await
.unwrap();
meta.set_cover_pk(Some("cover123".into())).await.unwrap();
{
let mut meta = track.write().await;
meta.set_title(Some("Title".into())).await.unwrap();
meta.set_artist(Some("Artist".into())).await.unwrap();
meta.set_album(Some("Album".into())).await.unwrap();
meta.set_year(Some(2024)).await.unwrap();
meta.set_duration(Some(Duration::from_secs(90)))
.await
.unwrap();
meta.set_track_id(Some("trk".into())).await.unwrap();
meta.set_channel_id(Some("chn".into())).await.unwrap();
meta.set_event(Some("event".into())).await.unwrap();
meta.set_rating(Some(4.5)).await.unwrap();
meta.set_cover_url(Some("http://cover".into()))
.await
.unwrap();
meta.set_cover_pk(Some("cover123".into())).await.unwrap();
}
{
let meta = track.read().await;
assert_eq!(meta.get_title().await.unwrap(), Some("Title".into()));
assert_eq!(meta.get_artist().await.unwrap(), Some("Artist".into()));
assert_eq!(meta.get_album().await.unwrap(), Some("Album".into()));
@@ -311,4 +318,5 @@ mod tests {
assert_eq!(meta.get_cover_pk().await.unwrap(), Some("cover123".into()));
assert!(meta.get_updated_at().await.unwrap().is_some());
}
}
}