Implémentation des pmometadata dans pmocacheaudio
This commit is contained in:
@@ -79,7 +79,9 @@
|
||||
pub mod cache;
|
||||
pub mod metadata;
|
||||
pub mod metadata_ext;
|
||||
pub mod nodes;
|
||||
pub mod streaming;
|
||||
pub mod track_metadata;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod openapi;
|
||||
@@ -90,7 +92,9 @@ pub mod config_ext;
|
||||
// Re-exports principaux
|
||||
pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache};
|
||||
pub use metadata::AudioMetadata;
|
||||
pub use metadata_ext::AudioMetadataExt;
|
||||
pub use metadata_ext::{AudioMetadataExt, AudioTrackMetadataExt};
|
||||
pub use nodes::{FlacCacheSink, FlacCacheSinkStats};
|
||||
pub use track_metadata::AudioCacheTrackMetadata;
|
||||
|
||||
#[cfg(feature = "pmoconfig")]
|
||||
pub use config_ext::AudioCacheConfigExt;
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
//! Ce module utilise la macro `define_metadata_properties!` de pmocache
|
||||
//! pour générer automatiquement des méthodes d'accès typées aux métadonnées audio.
|
||||
|
||||
use crate::AudioConfig;
|
||||
use crate::{AudioCacheTrackMetadata, AudioConfig};
|
||||
use pmocache::define_metadata_properties;
|
||||
use pmometadata::TrackMetadata;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// Génération automatique du trait AudioMetadataExt avec toutes les propriétés audio
|
||||
define_metadata_properties! {
|
||||
@@ -33,3 +36,15 @@ define_metadata_properties! {
|
||||
bit_depth: i64 as i64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fournit un accès direct à une implémentation `TrackMetadata` basée sur le cache.
|
||||
pub trait AudioTrackMetadataExt {
|
||||
fn track_metadata(&self, pk: impl Into<String>) -> Arc<RwLock<dyn TrackMetadata>>;
|
||||
}
|
||||
|
||||
impl AudioTrackMetadataExt for Arc<pmocache::Cache<AudioConfig>> {
|
||||
fn track_metadata(&self, pk: impl Into<String>) -> Arc<RwLock<dyn TrackMetadata>> {
|
||||
let metadata = AudioCacheTrackMetadata::new(self.clone(), pk);
|
||||
Arc::new(RwLock::new(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
560
pmoaudiocache/src/nodes/flac_cache_sink.rs
Normal file
560
pmoaudiocache/src/nodes/flac_cache_sink.rs
Normal file
@@ -0,0 +1,560 @@
|
||||
//! Sink qui encode les AudioSegment au format FLAC et les stocke dans le cache audio
|
||||
|
||||
use crate::metadata_ext::AudioTrackMetadataExt;
|
||||
use pmoaudio::{
|
||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
|
||||
type_constraints::TypeRequirement,
|
||||
AudioChunk, AudioSegment, SyncMarker, _AudioSegment,
|
||||
};
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::Cursor,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
io::{self, AsyncRead, ReadBuf},
|
||||
sync::{mpsc, RwLock},
|
||||
};
|
||||
|
||||
/// Sink qui encode les `AudioSegment` reçus au format FLAC et les stocke dans le cache audio.
|
||||
///
|
||||
/// Ce sink :
|
||||
/// - Filtre les chunks audio et ignore les autres syncmarkers (sauf TrackBoundary et EndOfStream)
|
||||
/// - Crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré
|
||||
/// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit)
|
||||
/// - Copie les métadonnées du TrackBoundary dans le cache après ingestion
|
||||
/// - Termine l'encodage proprement quand il reçoit EndOfStream
|
||||
pub struct FlacCacheSink {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
cache: Arc<crate::Cache>,
|
||||
collection: Option<String>,
|
||||
encoder_options: EncoderOptions,
|
||||
pcm_buffer_capacity: usize,
|
||||
}
|
||||
|
||||
impl FlacCacheSink {
|
||||
/// Crée un sink FLAC cache avec les options par défaut (compression 5, buffer de 16 segments).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache` - Arc vers le cache audio où stocker les fichiers FLAC encodés
|
||||
pub fn new(cache: Arc<crate::Cache>) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(cache, DEFAULT_CHANNEL_SIZE)
|
||||
}
|
||||
|
||||
/// Crée un sink FLAC cache avec une taille de buffer MPSC personnalisée.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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<crate::Cache>,
|
||||
channel_size: usize,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_config(cache, channel_size, EncoderOptions::default(), None)
|
||||
}
|
||||
|
||||
/// Crée un sink FLAC cache avec une configuration complète.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache` - Arc vers le cache audio
|
||||
/// * `channel_size` - Taille du buffer MPSC
|
||||
/// * `encoder_options` - Options d'encodage FLAC (compression, etc.)
|
||||
/// * `collection` - Collection optionnelle à laquelle appartiennent les fichiers
|
||||
pub fn with_config(
|
||||
cache: Arc<crate::Cache>,
|
||||
channel_size: usize,
|
||||
encoder_options: EncoderOptions,
|
||||
collection: Option<String>,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let sink = Self {
|
||||
rx,
|
||||
cache,
|
||||
collection,
|
||||
encoder_options,
|
||||
pcm_buffer_capacity: 8,
|
||||
};
|
||||
(sink, tx)
|
||||
}
|
||||
|
||||
/// Lance l'encodage et l'ingestion dans le cache.
|
||||
///
|
||||
/// Cette méthode crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré.
|
||||
/// Les métadonnées du TrackBoundary sont copiées dans le cache après l'ingestion.
|
||||
pub async fn run(self) -> Result<FlacCacheSinkStats, AudioError> {
|
||||
let FlacCacheSink {
|
||||
mut rx,
|
||||
cache,
|
||||
collection,
|
||||
encoder_options,
|
||||
pcm_buffer_capacity,
|
||||
} = self;
|
||||
|
||||
let mut all_tracks = Vec::new();
|
||||
let mut track_number = 0;
|
||||
|
||||
loop {
|
||||
// Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary
|
||||
let (first_segment, track_metadata) =
|
||||
match wait_for_first_audio_chunk_with_metadata(&mut rx).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
// Plus d'audio disponible
|
||||
if all_tracks.is_empty() {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"No audio data received".into(),
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Extraire les informations du premier chunk
|
||||
let first_chunk = first_segment.as_chunk().unwrap();
|
||||
let sample_rate = first_chunk.sample_rate();
|
||||
let bits_per_sample = get_chunk_bit_depth(first_chunk);
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample,
|
||||
};
|
||||
if let Err(err) = format.validate() {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Invalid PCM format: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
// Créer le pipeline d'encodage pour cette track
|
||||
let (pcm_tx, pcm_rx) = mpsc::channel::<Vec<u8>>(pcm_buffer_capacity);
|
||||
|
||||
// Préparer les options d'encodage avec les métadonnées du TrackBoundary
|
||||
let mut options_with_metadata = encoder_options.clone();
|
||||
options_with_metadata.metadata = track_metadata.clone();
|
||||
|
||||
// Créer l'encoder
|
||||
let reader = ByteStreamReader::new(pcm_rx);
|
||||
let mut flac_stream = encode_flac_stream(reader, format, options_with_metadata)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("FLAC encode init failed: {}", e))
|
||||
})?;
|
||||
|
||||
// Créer un buffer pour collecter le FLAC encodé
|
||||
let mut flac_buffer = Vec::new();
|
||||
|
||||
// Exécuter pump et copy en parallèle
|
||||
let pump_future = pump_track_segments(
|
||||
first_segment,
|
||||
&mut rx,
|
||||
pcm_tx,
|
||||
bits_per_sample,
|
||||
sample_rate,
|
||||
);
|
||||
let copy_future = async {
|
||||
tokio::io::copy(&mut flac_stream, &mut flac_buffer)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("FLAC write failed: {}", e)))?;
|
||||
flac_stream
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Encoder failed: {}", e)))?;
|
||||
Ok::<_, AudioError>(())
|
||||
};
|
||||
|
||||
// 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);
|
||||
copy_result?;
|
||||
let (chunks, samples, duration_sec, stop_reason) = pump_result?;
|
||||
|
||||
// Ingérer le FLAC dans le cache
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to add to cache: {}", e))
|
||||
})?;
|
||||
|
||||
// Copier les métadonnées du TrackBoundary dans le cache
|
||||
if let Some(src_metadata) = track_metadata {
|
||||
let dest_metadata = cache.track_metadata(&pk);
|
||||
|
||||
// Utiliser copy_metadata_into pour copier toutes les métadonnées
|
||||
pmometadata::copy_metadata_into(&src_metadata, &dest_metadata)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AudioError::ProcessingError(format!(
|
||||
"Failed to copy metadata to cache: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Ajouter les stats de cette track
|
||||
all_tracks.push(TrackStats {
|
||||
pk,
|
||||
track_number,
|
||||
chunks_received: chunks,
|
||||
total_samples: samples,
|
||||
total_duration_sec: duration_sec,
|
||||
});
|
||||
|
||||
// Vérifier le stop_reason pour savoir si on continue
|
||||
match stop_reason {
|
||||
StopReason::TrackBoundary(_metadata) => {
|
||||
// Continuer avec la prochaine track
|
||||
track_number += 1;
|
||||
continue;
|
||||
}
|
||||
StopReason::EndOfStream | StopReason::ChannelClosed => {
|
||||
// Fin de l'encodage
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FlacCacheSinkStats { tracks: all_tracks })
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal retourné par pump_segments indiquant pourquoi l'encodage s'est arrêté.
|
||||
enum StopReason {
|
||||
TrackBoundary(Arc<RwLock<dyn pmometadata::TrackMetadata>>),
|
||||
EndOfStream,
|
||||
ChannelClosed,
|
||||
}
|
||||
|
||||
/// Attend et retourne le premier chunk audio avec les métadonnées du TrackBoundary si présent.
|
||||
/// Retourne une erreur si EndOfStream est reçu avant tout audio.
|
||||
async fn wait_for_first_audio_chunk_with_metadata(
|
||||
rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
|
||||
) -> Result<
|
||||
(
|
||||
Arc<AudioSegment>,
|
||||
Option<Arc<RwLock<dyn pmometadata::TrackMetadata>>>,
|
||||
),
|
||||
AudioError,
|
||||
> {
|
||||
let mut track_metadata: Option<Arc<RwLock<dyn pmometadata::TrackMetadata>>> = None;
|
||||
|
||||
loop {
|
||||
let segment = rx
|
||||
.recv()
|
||||
.await
|
||||
.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?;
|
||||
|
||||
match &segment.segment {
|
||||
_AudioSegment::Chunk(chunk) => {
|
||||
if chunk.len() == 0 {
|
||||
return Err(AudioError::ProcessingError("Received empty chunk".into()));
|
||||
}
|
||||
return Ok((segment, track_metadata));
|
||||
}
|
||||
_AudioSegment::Sync(marker) => match **marker {
|
||||
SyncMarker::TrackBoundary { ref metadata, .. } => {
|
||||
// Capturer les métadonnées du TrackBoundary
|
||||
track_metadata = Some(metadata.clone());
|
||||
continue;
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"EndOfStream received before any audio".into(),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
// Ignorer TopZeroSync, Heartbeat, etc.
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pompe les segments pour une seule track (s'arrête au TrackBoundary).
|
||||
async fn pump_track_segments(
|
||||
first_segment: Arc<AudioSegment>,
|
||||
rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
|
||||
pcm_tx: mpsc::Sender<Vec<u8>>,
|
||||
bits_per_sample: u8,
|
||||
expected_rate: u32,
|
||||
) -> Result<(u64, u64, f64, StopReason), AudioError> {
|
||||
let mut chunks = 0u64;
|
||||
let mut samples = 0u64;
|
||||
let mut duration_sec = 0.0f64;
|
||||
|
||||
// Traiter le premier segment
|
||||
if let Some(chunk) = first_segment.as_chunk() {
|
||||
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
|
||||
if !pcm_bytes.is_empty() {
|
||||
pcm_tx
|
||||
.send(pcm_bytes)
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
chunks += 1;
|
||||
samples += chunk.len() as u64;
|
||||
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
||||
}
|
||||
}
|
||||
|
||||
// Boucle sur les segments suivants
|
||||
loop {
|
||||
let segment = match rx.recv().await {
|
||||
Some(seg) => seg,
|
||||
None => {
|
||||
drop(pcm_tx); // Fermer le channel PCM
|
||||
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed));
|
||||
}
|
||||
};
|
||||
|
||||
match &segment.segment {
|
||||
_AudioSegment::Chunk(chunk) => {
|
||||
// Vérifier la cohérence du sample rate
|
||||
if chunk.sample_rate() != expected_rate {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"FlacCacheSink: inconsistent sample rate ({} vs {})",
|
||||
chunk.sample_rate(),
|
||||
expected_rate
|
||||
)));
|
||||
}
|
||||
|
||||
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
|
||||
if pcm_bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
pcm_tx
|
||||
.send(pcm_bytes)
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
|
||||
chunks += 1;
|
||||
samples += chunk.len() as u64;
|
||||
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
||||
}
|
||||
_AudioSegment::Sync(marker) => match &**marker {
|
||||
SyncMarker::TrackBoundary { metadata, .. } => {
|
||||
drop(pcm_tx); // Fermer le channel PCM
|
||||
return Ok((
|
||||
chunks,
|
||||
samples,
|
||||
duration_sec,
|
||||
StopReason::TrackBoundary(metadata.clone()),
|
||||
));
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
drop(pcm_tx); // Fermer le channel PCM
|
||||
return Ok((chunks, samples, duration_sec, StopReason::EndOfStream));
|
||||
}
|
||||
_ => {} // Ignorer les autres syncmarkers
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Détermine la profondeur de bit d'un chunk audio
|
||||
fn get_chunk_bit_depth(chunk: &AudioChunk) -> u8 {
|
||||
match chunk {
|
||||
AudioChunk::I16(_) => 16,
|
||||
AudioChunk::I24(_) => 24,
|
||||
AudioChunk::I32(_) => 32,
|
||||
AudioChunk::F32(_) => 32, // Les flottants seront convertis en 32-bit
|
||||
AudioChunk::F64(_) => 32, // Les flottants seront convertis en 32-bit
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit un chunk audio en bytes PCM avec la profondeur de bit spécifiée
|
||||
fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result<Vec<u8>, AudioError> {
|
||||
// Vérifier que le chunk est de type entier
|
||||
match chunk {
|
||||
AudioChunk::F32(_) | AudioChunk::F64(_) => {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"FlacCacheSink only supports integer audio chunks (I16, I24, I32)".into(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let len = chunk.len();
|
||||
let bytes_per_frame = (bits_per_sample / 8) as usize * 2; // 2 channels
|
||||
let mut bytes = Vec::with_capacity(len * bytes_per_frame);
|
||||
|
||||
// Convertir selon le type du chunk
|
||||
match (chunk, bits_per_sample) {
|
||||
// I16 source
|
||||
(AudioChunk::I16(data), 16) => {
|
||||
for frame in data.frames() {
|
||||
bytes.extend_from_slice(&frame[0].to_le_bytes());
|
||||
bytes.extend_from_slice(&frame[1].to_le_bytes());
|
||||
}
|
||||
}
|
||||
(AudioChunk::I16(data), 24) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0] as i32) << 8;
|
||||
let right = (frame[1] as i32) << 8;
|
||||
bytes.extend_from_slice(&left.to_le_bytes()[..3]);
|
||||
bytes.extend_from_slice(&right.to_le_bytes()[..3]);
|
||||
}
|
||||
}
|
||||
(AudioChunk::I16(data), 32) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0] as i32) << 16;
|
||||
let right = (frame[1] as i32) << 16;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// I24 source
|
||||
(AudioChunk::I24(data), 16) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0].as_i32() >> 8) as i16;
|
||||
let right = (frame[1].as_i32() >> 8) as i16;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
(AudioChunk::I24(data), 24) => {
|
||||
for frame in data.frames() {
|
||||
bytes.extend_from_slice(&frame[0].as_i32().to_le_bytes()[..3]);
|
||||
bytes.extend_from_slice(&frame[1].as_i32().to_le_bytes()[..3]);
|
||||
}
|
||||
}
|
||||
(AudioChunk::I24(data), 32) => {
|
||||
for frame in data.frames() {
|
||||
let left = frame[0].as_i32() << 8;
|
||||
let right = frame[1].as_i32() << 8;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// I32 source
|
||||
(AudioChunk::I32(data), 16) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0] >> 16) as i16;
|
||||
let right = (frame[1] >> 16) as i16;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
(AudioChunk::I32(data), 24) => {
|
||||
for frame in data.frames() {
|
||||
let left = frame[0] >> 8;
|
||||
let right = frame[1] >> 8;
|
||||
bytes.extend_from_slice(&left.to_le_bytes()[..3]);
|
||||
bytes.extend_from_slice(&right.to_le_bytes()[..3]);
|
||||
}
|
||||
}
|
||||
(AudioChunk::I32(data), 32) => {
|
||||
for frame in data.frames() {
|
||||
bytes.extend_from_slice(&frame[0].to_le_bytes());
|
||||
bytes.extend_from_slice(&frame[1].to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bits_per_sample: {}",
|
||||
bits_per_sample
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
struct ByteStreamReader {
|
||||
rx: mpsc::Receiver<Vec<u8>>,
|
||||
buffer: VecDeque<u8>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl ByteStreamReader {
|
||||
fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
rx,
|
||||
buffer: VecDeque::new(),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for ByteStreamReader {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
loop {
|
||||
if !self.buffer.is_empty() {
|
||||
let to_copy = self.buffer.len().min(buf.remaining());
|
||||
if to_copy == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// VecDeque::make_contiguous pour copier efficacement
|
||||
let slice = self.buffer.make_contiguous();
|
||||
buf.put_slice(&slice[..to_copy]);
|
||||
self.buffer.drain(..to_copy);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if self.finished {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.rx).poll_recv(cx) {
|
||||
Poll::Ready(Some(bytes)) => {
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
self.buffer.extend(bytes);
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
self.finished = true;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques pour une track individuelle.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackStats {
|
||||
pub pk: String,
|
||||
pub track_number: usize,
|
||||
pub chunks_received: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
}
|
||||
|
||||
/// Statistiques produites par le `FlacCacheSink`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlacCacheSinkStats {
|
||||
pub tracks: Vec<TrackStats>,
|
||||
}
|
||||
|
||||
impl TypedAudioNode for FlacCacheSink {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
// FlacCacheSink accepte n'importe quel type entier (I16, I24, I32)
|
||||
// mais rejette les chunks flottants
|
||||
Some(TypeRequirement::any_integer())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
// FlacCacheSink est un sink, il ne produit pas d'audio
|
||||
None
|
||||
}
|
||||
}
|
||||
8
pmoaudiocache/src/nodes/mod.rs
Normal file
8
pmoaudiocache/src/nodes/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Nodes audio pour pmoaudiocache
|
||||
//!
|
||||
//! Ce module fournit des nodes audio spécialisés qui étendent pmoaudio
|
||||
//! pour intégrer le cache audio.
|
||||
|
||||
pub mod flac_cache_sink;
|
||||
|
||||
pub use flac_cache_sink::{FlacCacheSink, FlacCacheSinkStats};
|
||||
314
pmoaudiocache/src/track_metadata.rs
Normal file
314
pmoaudiocache/src/track_metadata.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use pmometadata::{MetadataError, MetadataResult, TrackMetadata};
|
||||
use serde_json::{Number, Value};
|
||||
|
||||
fn map_db_err(err: rusqlite::Error) -> MetadataError {
|
||||
MetadataError::Backend(err.to_string())
|
||||
}
|
||||
|
||||
pub struct AudioCacheTrackMetadata {
|
||||
cache: Arc<crate::Cache>,
|
||||
pk: String,
|
||||
}
|
||||
|
||||
impl AudioCacheTrackMetadata {
|
||||
pub fn new(cache: Arc<crate::Cache>, pk: impl Into<String>) -> Self {
|
||||
Self {
|
||||
cache,
|
||||
pk: pk.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_raw(&self, key: &str) -> Result<Option<Value>, MetadataError> {
|
||||
self.cache
|
||||
.db
|
||||
.get_a_metadata(&self.pk, key)
|
||||
.map_err(map_db_err)
|
||||
}
|
||||
|
||||
fn write_raw(&self, key: &str, value: Value) -> Result<(), MetadataError> {
|
||||
self.cache
|
||||
.db
|
||||
.set_a_metadata(&self.pk, key, value)
|
||||
.map_err(map_db_err)
|
||||
}
|
||||
|
||||
fn read_string(&self, key: &str) -> Result<Option<String>, MetadataError> {
|
||||
match self.read_raw(key)? {
|
||||
Some(Value::String(s)) => Ok(Some(s)),
|
||||
Some(Value::Null) | None => Ok(None),
|
||||
Some(other) => Err(MetadataError::Backend(format!(
|
||||
"metadata {key} for {} is not a string ({other})",
|
||||
self.pk
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string(&self, key: &str, value: Option<String>) -> Result<(), MetadataError> {
|
||||
let json = value.map(Value::String).unwrap_or(Value::Null);
|
||||
self.write_raw(key, json)
|
||||
}
|
||||
|
||||
fn read_number(&self, key: &str) -> Result<Option<Number>, MetadataError> {
|
||||
match self.read_raw(key)? {
|
||||
Some(Value::Number(n)) => Ok(Some(n)),
|
||||
Some(Value::Null) | None => Ok(None),
|
||||
Some(other) => Err(MetadataError::Backend(format!(
|
||||
"metadata {key} for {} is not a number ({other})",
|
||||
self.pk
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_number(&self, key: &str, value: Option<i64>) -> Result<(), MetadataError> {
|
||||
let json = match value {
|
||||
Some(n) => Value::Number(Number::from(n)),
|
||||
None => Value::Null,
|
||||
};
|
||||
self.write_raw(key, json)
|
||||
}
|
||||
|
||||
fn write_u64(&self, key: &str, value: Option<u64>) -> Result<(), MetadataError> {
|
||||
let json = match value {
|
||||
Some(n) => Value::Number(Number::from(n)),
|
||||
None => Value::Null,
|
||||
};
|
||||
self.write_raw(key, json)
|
||||
}
|
||||
|
||||
fn write_f64(&self, key: &str, value: Option<f64>) -> Result<(), MetadataError> {
|
||||
let json = match value {
|
||||
Some(v) => Number::from_f64(v)
|
||||
.map(Value::Number)
|
||||
.ok_or_else(|| MetadataError::Backend(format!("invalid float for {key}")))?,
|
||||
None => Value::Null,
|
||||
};
|
||||
self.write_raw(key, json)
|
||||
}
|
||||
|
||||
fn read_duration(&self) -> Result<Option<Duration>, MetadataError> {
|
||||
match self.read_number("duration_secs")? {
|
||||
Some(n) => match n.as_u64() {
|
||||
Some(secs) => Ok(Some(Duration::from_secs(secs))),
|
||||
None => Err(MetadataError::Backend(format!(
|
||||
"duration_secs for {} out of range",
|
||||
self.pk
|
||||
))),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_duration(&self, value: Option<Duration>) -> Result<(), MetadataError> {
|
||||
self.write_u64("duration_secs", value.map(|d| d.as_secs()))
|
||||
}
|
||||
|
||||
fn read_timestamp(&self) -> Result<Option<SystemTime>, MetadataError> {
|
||||
match self.read_number("updated_at")? {
|
||||
Some(n) => match n.as_u64() {
|
||||
Some(secs) => Ok(Some(UNIX_EPOCH + Duration::from_secs(secs))),
|
||||
None => Err(MetadataError::Backend(format!(
|
||||
"updated_at for {} out of range",
|
||||
self.pk
|
||||
))),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_timestamp(&self, when: SystemTime) -> Result<(), MetadataError> {
|
||||
let secs = when
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| MetadataError::Backend(e.to_string()))?
|
||||
.as_secs();
|
||||
self.write_u64("updated_at", Some(secs))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TrackMetadata for AudioCacheTrackMetadata {
|
||||
async fn get_title(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("title")?)
|
||||
}
|
||||
|
||||
async fn set_title(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("title", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_artist(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("artist")?)
|
||||
}
|
||||
|
||||
async fn set_artist(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("artist", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_album(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("album")?)
|
||||
}
|
||||
|
||||
async fn set_album(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("album", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_year(&self) -> MetadataResult<u32> {
|
||||
Ok(match self.read_number("year")? {
|
||||
Some(n) => n
|
||||
.as_i64()
|
||||
.and_then(|v| u32::try_from(v).ok())
|
||||
.map(Some)
|
||||
.unwrap_or(None),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_year(&mut self, value: Option<u32>) -> MetadataResult<()> {
|
||||
self.write_number("year", value.map(|v| v as i64))?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_duration(&self) -> MetadataResult<Duration> {
|
||||
Ok(self.read_duration()?)
|
||||
}
|
||||
|
||||
async fn set_duration(&mut self, value: Option<Duration>) -> MetadataResult<()> {
|
||||
self.write_duration(value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_track_id(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("track_id")?)
|
||||
}
|
||||
|
||||
async fn set_track_id(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("track_id", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_channel_id(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("channel_id")?)
|
||||
}
|
||||
|
||||
async fn set_channel_id(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("channel_id", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_event(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("event")?)
|
||||
}
|
||||
|
||||
async fn set_event(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("event", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_rating(&self) -> MetadataResult<f32> {
|
||||
Ok(match self.read_number("rating")? {
|
||||
Some(n) => n.as_f64().map(|v| v as f32),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_rating(&mut self, value: Option<f32>) -> MetadataResult<()> {
|
||||
self.write_f64("rating", value.map(|v| v as f64))?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_cover_url(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("cover_url")?)
|
||||
}
|
||||
|
||||
async fn set_cover_url(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("cover_url", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_cover_pk(&self) -> MetadataResult<String> {
|
||||
Ok(self.read_string("cover_pk")?)
|
||||
}
|
||||
|
||||
async fn set_cover_pk(&mut self, value: Option<String>) -> MetadataResult<()> {
|
||||
self.write_string("cover_pk", value)?;
|
||||
let _ = self.touch().await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn get_updated_at(&self) -> MetadataResult<SystemTime> {
|
||||
Ok(self.read_timestamp()?)
|
||||
}
|
||||
|
||||
async fn touch(&mut self) -> MetadataResult<()> {
|
||||
self.write_timestamp(SystemTime::now())?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cache::new_cache;
|
||||
use crate::metadata_ext::AudioTrackMetadataExt;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn roundtrip_metadata() {
|
||||
let dir = tempdir().unwrap();
|
||||
let cache = Arc::new(new_cache(dir.path().to_str().unwrap(), 4).unwrap());
|
||||
let pk = "track-test";
|
||||
cache.db.add(pk, None, None).unwrap();
|
||||
|
||||
let mut meta = 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();
|
||||
|
||||
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()));
|
||||
assert_eq!(meta.get_year().await.unwrap(), Some(2024));
|
||||
assert_eq!(
|
||||
meta.get_duration().await.unwrap(),
|
||||
Some(Duration::from_secs(90))
|
||||
);
|
||||
assert_eq!(meta.get_track_id().await.unwrap(), Some("trk".into()));
|
||||
assert_eq!(meta.get_channel_id().await.unwrap(), Some("chn".into()));
|
||||
assert_eq!(meta.get_event().await.unwrap(), Some("event".into()));
|
||||
assert_eq!(meta.get_rating().await.unwrap(), Some(4.5));
|
||||
assert_eq!(
|
||||
meta.get_cover_url().await.unwrap(),
|
||||
Some("http://cover".into())
|
||||
);
|
||||
assert_eq!(meta.get_cover_pk().await.unwrap(), Some("cover123".into()));
|
||||
assert!(meta.get_updated_at().await.unwrap().is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user