Refactoring de pmoflac -factorisation de code ogg et opus
This commit is contained in:
@@ -3,8 +3,8 @@
|
|||||||
//! Ce module utilise la macro `define_metadata_properties!` de pmocache
|
//! 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.
|
//! pour générer automatiquement des méthodes d'accès typées aux métadonnées audio.
|
||||||
|
|
||||||
use pmocache::define_metadata_properties;
|
|
||||||
use crate::AudioConfig;
|
use crate::AudioConfig;
|
||||||
|
use pmocache::define_metadata_properties;
|
||||||
|
|
||||||
// Génération automatique du trait AudioMetadataExt avec toutes les propriétés audio
|
// Génération automatique du trait AudioMetadataExt avec toutes les propriétés audio
|
||||||
define_metadata_properties! {
|
define_metadata_properties! {
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ async fn stream_flac_to_flac(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Encoder/Decoder error: {}", e))?;
|
.map_err(|e| format!("Encoder/Decoder error: {}", e))?;
|
||||||
|
|
||||||
tracing::debug!("Streaming FLAC conversion complete: {} bytes", total_written);
|
tracing::debug!(
|
||||||
|
"Streaming FLAC conversion complete: {} bytes",
|
||||||
|
total_written
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -182,9 +185,7 @@ async fn buffer_and_convert_to_flac(
|
|||||||
// 2. Si c'est déjà du FLAC, on l'écrit directement
|
// 2. Si c'est déjà du FLAC, on l'écrit directement
|
||||||
if buffer.len() >= 4 && &buffer[0..4] == b"fLaC" {
|
if buffer.len() >= 4 && &buffer[0..4] == b"fLaC" {
|
||||||
tracing::debug!("Input is already FLAC, writing directly");
|
tracing::debug!("Input is already FLAC, writing directly");
|
||||||
file.write_all(&buffer)
|
file.write_all(&buffer).await.map_err(|e| e.to_string())?;
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
file.flush().await.map_err(|e| e.to_string())?;
|
file.flush().await.map_err(|e| e.to_string())?;
|
||||||
progress(buffer.len() as u64);
|
progress(buffer.len() as u64);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -193,11 +194,10 @@ async fn buffer_and_convert_to_flac(
|
|||||||
tracing::debug!("Converting to FLAC with Symphonia + pmoflac");
|
tracing::debug!("Converting to FLAC with Symphonia + pmoflac");
|
||||||
|
|
||||||
// 3. Décoder l'audio avec Symphonia (dans un blocking task car c'est CPU-intensive)
|
// 3. Décoder l'audio avec Symphonia (dans un blocking task car c'est CPU-intensive)
|
||||||
let (samples, channels, sample_rate, bits_per_sample) = tokio::task::spawn_blocking(move || {
|
let (samples, channels, sample_rate, bits_per_sample) =
|
||||||
decode_with_symphonia_sync(buffer)
|
tokio::task::spawn_blocking(move || decode_with_symphonia_sync(buffer))
|
||||||
})
|
.await
|
||||||
.await
|
.map_err(|e| format!("Decode task panicked: {}", e))??;
|
||||||
.map_err(|e| format!("Decode task panicked: {}", e))??;
|
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Decoded {} samples, {} channels, {} Hz, {} bits",
|
"Decoded {} samples, {} channels, {} Hz, {} bits",
|
||||||
@@ -341,9 +341,7 @@ fn decode_with_symphonia_sync(buffer: Vec<u8>) -> Result<(Vec<i32>, usize, u32,
|
|||||||
decoder.reset();
|
decoder.reset();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(SymphoniaError::IoError(e))
|
Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||||
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -490,10 +488,7 @@ impl tokio::io::AsyncRead for StreamToAsyncRead {
|
|||||||
self.chunk_offset = 0;
|
self.chunk_offset = 0;
|
||||||
}
|
}
|
||||||
Poll::Ready(Some(Err(e))) => {
|
Poll::Ready(Some(Err(e))) => {
|
||||||
return Poll::Ready(Err(std::io::Error::new(
|
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, e)));
|
||||||
std::io::ErrorKind::Other,
|
|
||||||
e,
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Poll::Ready(None) => {
|
Poll::Ready(None) => {
|
||||||
// Stream terminé
|
// Stream terminé
|
||||||
|
|||||||
@@ -141,8 +141,7 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
|||||||
///
|
///
|
||||||
/// `true` si l'entrée existe en base de données et que le fichier est présent
|
/// `true` si l'entrée existe en base de données et que le fichier est présent
|
||||||
fn is_valid_pk(&self, pk: &str) -> bool {
|
fn is_valid_pk(&self, pk: &str) -> bool {
|
||||||
self.get_database().get(pk, false).is_ok()
|
self.get_database().get(pk, false).is_ok() && self.file_path(pk).exists()
|
||||||
&& self.file_path(pk).exists()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,4 +175,3 @@ pub fn pk_from_content_header(header: &[u8]) -> String {
|
|||||||
let result = hasher.finalize();
|
let result = hasher.finalize();
|
||||||
hex::encode(&result[..16]) // 16 octets = 32 caractères hex
|
hex::encode(&result[..16]) // 16 octets = 32 caractères hex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ pub enum DecodeAudioError {
|
|||||||
#[error("MP3 decode error: {0}")]
|
#[error("MP3 decode error: {0}")]
|
||||||
Mp3(#[from] Mp3Error),
|
Mp3(#[from] Mp3Error),
|
||||||
#[error("Ogg/Vorbis decode error: {0}")]
|
#[error("Ogg/Vorbis decode error: {0}")]
|
||||||
Vorbis(#[from] OggError),
|
Vorbis(OggError),
|
||||||
#[error("Ogg/Opus decode error: {0}")]
|
#[error("Ogg/Opus decode error: {0}")]
|
||||||
Opus(#[from] OggOpusError),
|
Opus(OggOpusError),
|
||||||
#[error("WAV decode error: {0}")]
|
#[error("WAV decode error: {0}")]
|
||||||
Wav(#[from] WavError),
|
Wav(#[from] WavError),
|
||||||
#[error("AIFF decode error: {0}")]
|
#[error("AIFF decode error: {0}")]
|
||||||
@@ -67,11 +67,15 @@ where
|
|||||||
DecodedAudioStream::Mp3(stream)
|
DecodedAudioStream::Mp3(stream)
|
||||||
}
|
}
|
||||||
DetectedFormat::OggVorbis => {
|
DetectedFormat::OggVorbis => {
|
||||||
let stream = decode_ogg_vorbis_stream(prefixed).await?;
|
let stream = decode_ogg_vorbis_stream(prefixed)
|
||||||
|
.await
|
||||||
|
.map_err(DecodeAudioError::Vorbis)?;
|
||||||
DecodedAudioStream::OggVorbis(stream)
|
DecodedAudioStream::OggVorbis(stream)
|
||||||
}
|
}
|
||||||
DetectedFormat::OggOpus => {
|
DetectedFormat::OggOpus => {
|
||||||
let stream = decode_ogg_opus_stream(prefixed).await?;
|
let stream = decode_ogg_opus_stream(prefixed)
|
||||||
|
.await
|
||||||
|
.map_err(DecodeAudioError::Opus)?;
|
||||||
DecodedAudioStream::OggOpus(stream)
|
DecodedAudioStream::OggOpus(stream)
|
||||||
}
|
}
|
||||||
DetectedFormat::Wav => {
|
DetectedFormat::Wav => {
|
||||||
@@ -113,10 +117,10 @@ impl DecodedAudioStream {
|
|||||||
DecodedAudioStream::Flac(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedAudioStream::Flac(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
DecodedAudioStream::Mp3(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedAudioStream::Mp3(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
DecodedAudioStream::OggVorbis(inner) => {
|
DecodedAudioStream::OggVorbis(inner) => {
|
||||||
inner.wait().await.map_err(DecodeAudioError::from)
|
inner.wait().await.map_err(DecodeAudioError::Vorbis)
|
||||||
}
|
}
|
||||||
DecodedAudioStream::OggOpus(inner) => {
|
DecodedAudioStream::OggOpus(inner) => {
|
||||||
inner.wait().await.map_err(DecodeAudioError::from)
|
inner.wait().await.map_err(DecodeAudioError::Opus)
|
||||||
}
|
}
|
||||||
DecodedAudioStream::Wav(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedAudioStream::Wav(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
DecodedAudioStream::Aiff(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedAudioStream::Aiff(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
@@ -184,8 +188,8 @@ impl DecodedReader {
|
|||||||
match self {
|
match self {
|
||||||
DecodedReader::Flac(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedReader::Flac(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
DecodedReader::Mp3(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedReader::Mp3(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
DecodedReader::OggVorbis(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedReader::OggVorbis(inner) => inner.wait().await.map_err(DecodeAudioError::Vorbis),
|
||||||
DecodedReader::OggOpus(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedReader::OggOpus(inner) => inner.wait().await.map_err(DecodeAudioError::Opus),
|
||||||
DecodedReader::Wav(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedReader::Wav(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
DecodedReader::Aiff(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
DecodedReader::Aiff(inner) => inner.wait().await.map_err(DecodeAudioError::from),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ pub mod encoder;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod mp3;
|
pub mod mp3;
|
||||||
pub mod ogg;
|
pub mod ogg;
|
||||||
|
mod ogg_common;
|
||||||
pub mod opus;
|
pub mod opus;
|
||||||
mod pcm;
|
mod pcm;
|
||||||
mod stream;
|
mod stream;
|
||||||
|
|||||||
@@ -102,8 +102,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
io,
|
||||||
io::{self, Read},
|
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
@@ -121,56 +120,23 @@ use tokio::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::ChannelReader,
|
common::ChannelReader,
|
||||||
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
||||||
|
ogg_common::{OggContainerError, OggPacketReader, OggReaderOptions},
|
||||||
pcm::StreamInfo,
|
pcm::StreamInfo,
|
||||||
stream::ManagedAsyncReader,
|
stream::ManagedAsyncReader,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Maximum number of bytes to scan when searching for Ogg sync pattern.
|
/// Shared error alias for the Vorbis decoder.
|
||||||
///
|
pub type OggError = OggContainerError;
|
||||||
/// This prevents unbounded memory growth when processing streams with
|
|
||||||
/// large amounts of garbage data before the first valid Ogg page.
|
|
||||||
const MAX_SYNC_SEARCH: usize = 64 * 1024;
|
|
||||||
|
|
||||||
/// Errors that can occur while decoding Ogg/Vorbis data.
|
impl From<header::HeaderReadError> for OggContainerError {
|
||||||
#[derive(thiserror::Error, Debug, Clone)]
|
|
||||||
pub enum OggError {
|
|
||||||
#[error("I/O error ({kind:?}): {message}")]
|
|
||||||
Io {
|
|
||||||
kind: io::ErrorKind,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
#[error("Ogg/Vorbis decode error: {0}")]
|
|
||||||
Decode(String),
|
|
||||||
#[error("internal channel closed unexpectedly")]
|
|
||||||
ChannelClosed,
|
|
||||||
#[error("{role} task failed: {details}")]
|
|
||||||
TaskJoin { role: &'static str, details: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for OggError {
|
|
||||||
fn from(err: io::Error) -> Self {
|
|
||||||
OggError::Io {
|
|
||||||
kind: err.kind(),
|
|
||||||
message: err.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<header::HeaderReadError> for OggError {
|
|
||||||
fn from(err: header::HeaderReadError) -> Self {
|
fn from(err: header::HeaderReadError) -> Self {
|
||||||
OggError::Decode(err.to_string())
|
OggContainerError::Decode(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<audio::AudioReadError> for OggError {
|
impl From<audio::AudioReadError> for OggContainerError {
|
||||||
fn from(err: audio::AudioReadError) -> Self {
|
fn from(err: audio::AudioReadError) -> Self {
|
||||||
OggError::Decode(err.to_string())
|
OggContainerError::Decode(err.to_string())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for OggError {
|
|
||||||
fn from(value: String) -> Self {
|
|
||||||
OggError::Decode(value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +228,8 @@ where
|
|||||||
let (info_tx, info_rx) = oneshot::channel::<Result<StreamInfo, OggError>>();
|
let (info_tx, info_rx) = oneshot::channel::<Result<StreamInfo, OggError>>();
|
||||||
|
|
||||||
let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), OggError> {
|
let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), OggError> {
|
||||||
let channel_reader = ChannelReader::<OggError>::new(ingest_rx);
|
let channel_reader = ChannelReader::<OggContainerError>::new(ingest_rx);
|
||||||
let mut packet_reader = StreamingPacketReader::new(channel_reader);
|
let mut packet_reader = OggPacketReader::new(channel_reader, OggReaderOptions::default());
|
||||||
|
|
||||||
// Read Vorbis headers (3 packets: identification, comment, setup)
|
// Read Vorbis headers (3 packets: identification, comment, setup)
|
||||||
let ident_packet = packet_reader
|
let ident_packet = packet_reader
|
||||||
@@ -346,314 +312,3 @@ where
|
|||||||
|
|
||||||
Ok(OggDecodedStream { info, reader })
|
Ok(OggDecodedStream { info, reader })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streaming packet reader that assembles Vorbis packets from Ogg pages.
|
|
||||||
///
|
|
||||||
/// This reader parses Ogg pages manually without requiring seek operations,
|
|
||||||
/// making it suitable for truly streaming scenarios. It handles:
|
|
||||||
/// - Searching for Ogg sync pattern ("OggS")
|
|
||||||
/// - Parsing page headers and segment tables
|
|
||||||
/// - Validating CRC32 checksums
|
|
||||||
/// - Assembling multi-page packets
|
|
||||||
/// - Detecting end-of-stream
|
|
||||||
struct StreamingPacketReader<E>
|
|
||||||
where
|
|
||||||
E: std::error::Error + std::fmt::Display,
|
|
||||||
{
|
|
||||||
reader: ChannelReader<E>,
|
|
||||||
current_packet: Vec<u8>,
|
|
||||||
queue: VecDeque<Vec<u8>>,
|
|
||||||
finished: bool,
|
|
||||||
eos_seen: bool,
|
|
||||||
stream_serial: Option<u32>,
|
|
||||||
sync_buffer: Vec<u8>,
|
|
||||||
synced: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> StreamingPacketReader<E>
|
|
||||||
where
|
|
||||||
E: std::error::Error + std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn new(reader: ChannelReader<E>) -> Self {
|
|
||||||
Self {
|
|
||||||
reader,
|
|
||||||
current_packet: Vec::new(),
|
|
||||||
queue: VecDeque::new(),
|
|
||||||
finished: false,
|
|
||||||
eos_seen: false,
|
|
||||||
stream_serial: None,
|
|
||||||
sync_buffer: Vec::new(),
|
|
||||||
synced: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the next complete Vorbis packet, or None if the stream has ended.
|
|
||||||
fn next_packet(&mut self) -> Result<Option<Vec<u8>>, OggError> {
|
|
||||||
loop {
|
|
||||||
if let Some(packet) = self.queue.pop_front() {
|
|
||||||
return Ok(Some(packet));
|
|
||||||
}
|
|
||||||
if self.finished {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
self.read_page()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads bytes, first from sync_buffer then from the underlying reader.
|
|
||||||
fn read_bytes(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
||||||
if buf.is_empty() {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut total = 0;
|
|
||||||
|
|
||||||
// First, consume from sync_buffer
|
|
||||||
if !self.sync_buffer.is_empty() {
|
|
||||||
let to_copy = buf.len().min(self.sync_buffer.len());
|
|
||||||
buf[..to_copy].copy_from_slice(&self.sync_buffer[..to_copy]);
|
|
||||||
self.sync_buffer.drain(..to_copy);
|
|
||||||
total += to_copy;
|
|
||||||
if total == buf.len() {
|
|
||||||
return Ok(total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then read from underlying reader
|
|
||||||
while total < buf.len() {
|
|
||||||
match Read::read(&mut self.reader, &mut buf[total..])? {
|
|
||||||
0 => break,
|
|
||||||
n => total += n,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(total)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads exactly buf.len() bytes or returns error.
|
|
||||||
fn read_exact_from_source(&mut self, buf: &mut [u8]) -> Result<bool, OggError> {
|
|
||||||
let mut offset = 0;
|
|
||||||
while offset < buf.len() {
|
|
||||||
let n = self.read_bytes(&mut buf[offset..])?;
|
|
||||||
if n == 0 {
|
|
||||||
return if offset == 0 {
|
|
||||||
Ok(false) // Clean EOF
|
|
||||||
} else {
|
|
||||||
Err(OggError::Decode("unexpected EOF while reading page".into()))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
offset += n;
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Searches for the Ogg sync pattern ("OggS") in the stream.
|
|
||||||
///
|
|
||||||
/// This is called before reading the first page to handle streams that
|
|
||||||
/// have garbage bytes at the beginning (e.g., HTTP headers, ID3 tags).
|
|
||||||
/// It buffers up to MAX_SYNC_SEARCH bytes while searching.
|
|
||||||
fn find_sync(&mut self) -> Result<(), OggError> {
|
|
||||||
if self.synced {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
while self.sync_buffer.len() < MAX_SYNC_SEARCH {
|
|
||||||
let mut chunk = [0u8; 1024];
|
|
||||||
let n = Read::read(&mut self.reader, &mut chunk)?;
|
|
||||||
if n == 0 {
|
|
||||||
return Err(OggError::Decode(
|
|
||||||
"EOF reached while searching for Ogg sync pattern".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.sync_buffer.extend_from_slice(&chunk[..n]);
|
|
||||||
|
|
||||||
// Search for "OggS" pattern
|
|
||||||
if let Some(pos) = self
|
|
||||||
.sync_buffer
|
|
||||||
.windows(4)
|
|
||||||
.position(|window| window == b"OggS")
|
|
||||||
{
|
|
||||||
// Found sync! Remove garbage bytes before it
|
|
||||||
self.sync_buffer.drain(..pos);
|
|
||||||
self.synced = true;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If buffer is getting large and still no sync, keep only last 3 bytes
|
|
||||||
// (in case "OggS" is split across chunk boundary)
|
|
||||||
if self.sync_buffer.len() >= MAX_SYNC_SEARCH {
|
|
||||||
let keep_len = 3.min(self.sync_buffer.len());
|
|
||||||
self.sync_buffer.drain(..self.sync_buffer.len() - keep_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(OggError::Decode(format!(
|
|
||||||
"No Ogg sync pattern found in first {} bytes",
|
|
||||||
MAX_SYNC_SEARCH
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads a single Ogg page and processes its packets.
|
|
||||||
///
|
|
||||||
/// This method:
|
|
||||||
/// 1. Ensures we're synced to "OggS" pattern
|
|
||||||
/// 2. Reads the 27-byte page header
|
|
||||||
/// 3. Validates the CRC32 checksum
|
|
||||||
/// 4. Reads the segment table
|
|
||||||
/// 5. Reads the page data
|
|
||||||
/// 6. Assembles packets from segments
|
|
||||||
fn read_page(&mut self) -> Result<(), OggError> {
|
|
||||||
// Ensure we've found the sync pattern
|
|
||||||
self.find_sync()?;
|
|
||||||
|
|
||||||
// Read 27-byte page header
|
|
||||||
let mut header = [0u8; 27];
|
|
||||||
if !self.read_exact_from_source(&mut header)? {
|
|
||||||
self.finished = true;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Ogg page header
|
|
||||||
if &header[0..4] != b"OggS" {
|
|
||||||
return Err(OggError::Decode("invalid Ogg capture pattern".into()));
|
|
||||||
}
|
|
||||||
if header[4] != 0 {
|
|
||||||
return Err(OggError::Decode("unsupported Ogg version".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let header_type = header[5];
|
|
||||||
let bitstream_serial = u32::from_le_bytes([header[14], header[15], header[16], header[17]]);
|
|
||||||
|
|
||||||
// Enforce single bitstream
|
|
||||||
if let Some(serial) = self.stream_serial {
|
|
||||||
if serial != bitstream_serial {
|
|
||||||
return Err(OggError::Decode(
|
|
||||||
"multiple logical streams are not supported".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.stream_serial = Some(bitstream_serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read segment table
|
|
||||||
let page_segments = header[26] as usize;
|
|
||||||
let mut segment_table = vec![0u8; page_segments];
|
|
||||||
self.read_exact_from_source(&mut segment_table)?;
|
|
||||||
|
|
||||||
// Calculate page data length
|
|
||||||
let data_len: usize = segment_table.iter().map(|&v| v as usize).sum();
|
|
||||||
let mut data = vec![0u8; data_len];
|
|
||||||
self.read_exact_from_source(&mut data)?;
|
|
||||||
|
|
||||||
// Validate CRC32
|
|
||||||
let expected_crc = u32::from_le_bytes([header[22], header[23], header[24], header[25]]);
|
|
||||||
let mut crc_header = header;
|
|
||||||
crc_header[22..26].copy_from_slice(&[0, 0, 0, 0]); // Zero out CRC field
|
|
||||||
|
|
||||||
let mut crc = crc::vorbis_crc32_update(0, &crc_header);
|
|
||||||
crc = crc::vorbis_crc32_update(crc, &segment_table);
|
|
||||||
crc = crc::vorbis_crc32_update(crc, &data);
|
|
||||||
|
|
||||||
if crc != expected_crc {
|
|
||||||
return Err(OggError::Decode(format!(
|
|
||||||
"CRC32 mismatch: expected 0x{:08x}, got 0x{:08x}",
|
|
||||||
expected_crc, crc
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate continuation flags
|
|
||||||
if header_type & 0x01 != 0 && self.current_packet.is_empty() {
|
|
||||||
return Err(OggError::Decode(
|
|
||||||
"unexpected continuation flag without existing packet".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if header_type & 0x01 == 0 && !self.current_packet.is_empty() {
|
|
||||||
return Err(OggError::Decode(
|
|
||||||
"dangling packet without continuation flag".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assemble packets from segments
|
|
||||||
let mut offset: usize = 0;
|
|
||||||
for &seg_len in &segment_table {
|
|
||||||
let len = seg_len as usize;
|
|
||||||
let end = offset
|
|
||||||
.checked_add(len)
|
|
||||||
.ok_or_else(|| OggError::Decode("segment length overflow".into()))?;
|
|
||||||
if end > data.len() {
|
|
||||||
return Err(OggError::Decode("segment exceeds page data".into()));
|
|
||||||
}
|
|
||||||
self.current_packet.extend_from_slice(&data[offset..end]);
|
|
||||||
offset = end;
|
|
||||||
|
|
||||||
// Packet complete when segment is less than 255 bytes
|
|
||||||
if seg_len < 255 {
|
|
||||||
let packet = std::mem::take(&mut self.current_packet);
|
|
||||||
self.queue.push_back(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset != data.len() {
|
|
||||||
return Err(OggError::Decode("page data not fully consumed".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for end-of-stream
|
|
||||||
if header_type & 0x04 != 0 {
|
|
||||||
self.eos_seen = true;
|
|
||||||
if self.current_packet.is_empty() {
|
|
||||||
self.finished = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CRC32 calculation for Ogg pages.
|
|
||||||
///
|
|
||||||
/// This module implements the CRC32 algorithm used by the Ogg container format.
|
|
||||||
/// The polynomial is 0x04c11db7 with initial value 0 and no final XOR.
|
|
||||||
mod crc {
|
|
||||||
/// Precomputed CRC32 lookup table for Ogg.
|
|
||||||
///
|
|
||||||
/// Generated using the polynomial 0x04c11db7.
|
|
||||||
const fn get_tbl_elem(idx: u32) -> u32 {
|
|
||||||
let mut r: u32 = idx << 24;
|
|
||||||
let mut i = 0;
|
|
||||||
while i < 8 {
|
|
||||||
r = (r << 1) ^ (-(((r >> 31) & 1) as i32) as u32 & 0x04c11db7);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
r
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn lookup_array() -> [u32; 0x100] {
|
|
||||||
let mut lup_arr: [u32; 0x100] = [0; 0x100];
|
|
||||||
let mut i = 0;
|
|
||||||
while i < 0x100 {
|
|
||||||
lup_arr[i] = get_tbl_elem(i as u32);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
lup_arr
|
|
||||||
}
|
|
||||||
|
|
||||||
static CRC_LOOKUP_ARRAY: &[u32] = &lookup_array();
|
|
||||||
|
|
||||||
/// Updates the CRC32 value with new data.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cur` - Current CRC32 value (use 0 for initial call)
|
|
||||||
/// * `array` - Data to include in CRC calculation
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Updated CRC32 value
|
|
||||||
pub fn vorbis_crc32_update(cur: u32, array: &[u8]) -> u32 {
|
|
||||||
let mut ret: u32 = cur;
|
|
||||||
for av in array {
|
|
||||||
ret = (ret << 8) ^ CRC_LOOKUP_ARRAY[(*av as u32 ^ (ret >> 24)) as usize];
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
426
pmoflac/src/ogg_common.rs
Normal file
426
pmoflac/src/ogg_common.rs
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
//! # Common Ogg Container Parsing
|
||||||
|
//!
|
||||||
|
//! This module provides shared functionality for parsing Ogg containers,
|
||||||
|
//! used by both Ogg/Vorbis and Ogg/Opus decoders.
|
||||||
|
//!
|
||||||
|
//! ## Features
|
||||||
|
//!
|
||||||
|
//! - **Streaming packet assembly**: Reads Ogg pages and assembles multi-page packets
|
||||||
|
//! - **Optional CRC32 validation**: Can validate page integrity
|
||||||
|
//! - **Optional sync search**: Can search for "OggS" pattern in streams with garbage
|
||||||
|
//! - **Shared error type**: Uses `OggContainerError` for consistent error reporting
|
||||||
|
//!
|
||||||
|
//! ## Architecture
|
||||||
|
//!
|
||||||
|
//! The `OggPacketReader` reads Ogg pages incrementally:
|
||||||
|
//! 1. Optionally searches for "OggS" sync pattern
|
||||||
|
//! 2. Reads 27-byte page headers
|
||||||
|
//! 3. Optionally validates CRC32 checksums
|
||||||
|
//! 4. Reads segment tables and page data
|
||||||
|
//! 5. Assembles packets from segments (handling multi-page packets)
|
||||||
|
//! 6. Returns complete packets to the decoder
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{self, Read},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::common::ChannelReader;
|
||||||
|
|
||||||
|
/// Maximum number of bytes to scan when searching for Ogg sync pattern.
|
||||||
|
///
|
||||||
|
/// This prevents unbounded memory growth when processing streams with
|
||||||
|
/// large amounts of garbage data before the first valid Ogg page.
|
||||||
|
const MAX_SYNC_SEARCH: usize = 64 * 1024;
|
||||||
|
|
||||||
|
/// Configuration options for Ogg packet reader.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OggReaderOptions {
|
||||||
|
/// Whether to validate CRC32 checksums of Ogg pages.
|
||||||
|
///
|
||||||
|
/// Vorbis typically validates CRC, Opus often doesn't.
|
||||||
|
pub validate_crc: bool,
|
||||||
|
|
||||||
|
/// Whether to search for "OggS" sync pattern at start of stream.
|
||||||
|
///
|
||||||
|
/// Useful for streams that may have garbage bytes before valid data.
|
||||||
|
pub find_sync: bool,
|
||||||
|
|
||||||
|
/// Maximum bytes to search for sync pattern (only used if find_sync is true).
|
||||||
|
pub max_sync_search: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OggReaderOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
validate_crc: true,
|
||||||
|
find_sync: true,
|
||||||
|
max_sync_search: MAX_SYNC_SEARCH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared error type for Ogg container parsing.
|
||||||
|
#[derive(thiserror::Error, Debug, Clone)]
|
||||||
|
pub enum OggContainerError {
|
||||||
|
#[error("I/O error ({kind:?}): {message}")]
|
||||||
|
Io {
|
||||||
|
kind: io::ErrorKind,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
#[error("ogg container error: {0}")]
|
||||||
|
Decode(String),
|
||||||
|
#[error("internal channel closed unexpectedly")]
|
||||||
|
ChannelClosed,
|
||||||
|
#[error("{role} task failed: {details}")]
|
||||||
|
TaskJoin { role: &'static str, details: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for OggContainerError {
|
||||||
|
fn from(err: io::Error) -> Self {
|
||||||
|
OggContainerError::Io {
|
||||||
|
kind: err.kind(),
|
||||||
|
message: err.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for OggContainerError {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
OggContainerError::Decode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for OggContainerError {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
OggContainerError::Decode(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streaming Ogg packet reader that assembles packets from Ogg pages.
|
||||||
|
///
|
||||||
|
/// This reader parses Ogg pages manually without requiring seek operations,
|
||||||
|
/// making it suitable for truly streaming scenarios.
|
||||||
|
///
|
||||||
|
/// # Features
|
||||||
|
///
|
||||||
|
/// - Searches for Ogg sync pattern ("OggS")
|
||||||
|
/// - Parses page headers and segment tables
|
||||||
|
/// - Validates CRC32 checksums (optional)
|
||||||
|
/// - Assembles multi-page packets
|
||||||
|
/// - Detects end-of-stream
|
||||||
|
/// - Enforces single logical bitstream
|
||||||
|
pub struct OggPacketReader {
|
||||||
|
reader: ChannelReader<OggContainerError>,
|
||||||
|
current_packet: Vec<u8>,
|
||||||
|
queue: VecDeque<Vec<u8>>,
|
||||||
|
finished: bool,
|
||||||
|
stream_serial: Option<u32>,
|
||||||
|
sync_buffer: Vec<u8>,
|
||||||
|
synced: bool,
|
||||||
|
options: OggReaderOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OggPacketReader {
|
||||||
|
/// Creates a new Ogg packet reader with the given options.
|
||||||
|
pub fn new(reader: ChannelReader<OggContainerError>, options: OggReaderOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
reader,
|
||||||
|
current_packet: Vec::new(),
|
||||||
|
queue: VecDeque::new(),
|
||||||
|
finished: false,
|
||||||
|
stream_serial: None,
|
||||||
|
sync_buffer: Vec::new(),
|
||||||
|
synced: !options.find_sync, // If we don't need to find sync, we're already synced
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the next complete packet, or None if the stream has ended.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if:
|
||||||
|
/// - I/O error occurs
|
||||||
|
/// - Ogg page structure is invalid
|
||||||
|
/// - CRC32 validation fails (if enabled)
|
||||||
|
/// - Multiple logical bitstreams detected
|
||||||
|
pub fn next_packet(&mut self) -> Result<Option<Vec<u8>>, OggContainerError> {
|
||||||
|
loop {
|
||||||
|
if let Some(packet) = self.queue.pop_front() {
|
||||||
|
return Ok(Some(packet));
|
||||||
|
}
|
||||||
|
if self.finished {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
self.read_page()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads bytes, first from sync_buffer then from the underlying reader.
|
||||||
|
fn read_bytes(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut total = 0;
|
||||||
|
|
||||||
|
// First, consume from sync_buffer
|
||||||
|
if !self.sync_buffer.is_empty() {
|
||||||
|
let to_copy = buf.len().min(self.sync_buffer.len());
|
||||||
|
buf[..to_copy].copy_from_slice(&self.sync_buffer[..to_copy]);
|
||||||
|
self.sync_buffer.drain(..to_copy);
|
||||||
|
total += to_copy;
|
||||||
|
if total == buf.len() {
|
||||||
|
return Ok(total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then read from underlying reader
|
||||||
|
while total < buf.len() {
|
||||||
|
match Read::read(&mut self.reader, &mut buf[total..])? {
|
||||||
|
0 => break,
|
||||||
|
n => total += n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads exactly buf.len() bytes or returns error.
|
||||||
|
fn read_exact_from_source(&mut self, buf: &mut [u8]) -> Result<bool, OggContainerError> {
|
||||||
|
let mut offset = 0;
|
||||||
|
while offset < buf.len() {
|
||||||
|
let n = self
|
||||||
|
.read_bytes(&mut buf[offset..])
|
||||||
|
.map_err(OggContainerError::from)?;
|
||||||
|
if n == 0 {
|
||||||
|
return if offset == 0 {
|
||||||
|
Ok(false) // Clean EOF
|
||||||
|
} else {
|
||||||
|
Err(OggContainerError::Decode(
|
||||||
|
"unexpected EOF while reading page".into(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
offset += n;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches for the Ogg sync pattern ("OggS") in the stream.
|
||||||
|
///
|
||||||
|
/// This is called before reading the first page to handle streams that
|
||||||
|
/// have garbage bytes at the beginning (e.g., HTTP headers, ID3 tags).
|
||||||
|
/// It buffers up to max_sync_search bytes while searching.
|
||||||
|
fn find_sync(&mut self) -> Result<(), OggContainerError> {
|
||||||
|
if self.synced {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
while self.sync_buffer.len() < self.options.max_sync_search {
|
||||||
|
let mut chunk = [0u8; 1024];
|
||||||
|
let n = Read::read(&mut self.reader, &mut chunk).map_err(OggContainerError::from)?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"EOF reached while searching for Ogg sync pattern".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.sync_buffer.extend_from_slice(&chunk[..n]);
|
||||||
|
|
||||||
|
// Search for "OggS" pattern
|
||||||
|
if let Some(pos) = self
|
||||||
|
.sync_buffer
|
||||||
|
.windows(4)
|
||||||
|
.position(|window| window == b"OggS")
|
||||||
|
{
|
||||||
|
// Found sync! Remove garbage bytes before it
|
||||||
|
self.sync_buffer.drain(..pos);
|
||||||
|
self.synced = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If buffer is getting large and still no sync, keep only last 3 bytes
|
||||||
|
// (in case "OggS" is split across chunk boundary)
|
||||||
|
if self.sync_buffer.len() >= self.options.max_sync_search {
|
||||||
|
let keep_len = 3.min(self.sync_buffer.len());
|
||||||
|
self.sync_buffer.drain(..self.sync_buffer.len() - keep_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(OggContainerError::Decode(format!(
|
||||||
|
"No Ogg sync pattern found in first {} bytes",
|
||||||
|
self.options.max_sync_search
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a single Ogg page and processes its packets.
|
||||||
|
///
|
||||||
|
/// This method:
|
||||||
|
/// 1. Ensures we're synced to "OggS" pattern (if enabled)
|
||||||
|
/// 2. Reads the 27-byte page header
|
||||||
|
/// 3. Validates the CRC32 checksum (if enabled)
|
||||||
|
/// 4. Reads the segment table
|
||||||
|
/// 5. Reads the page data
|
||||||
|
/// 6. Assembles packets from segments
|
||||||
|
fn read_page(&mut self) -> Result<(), OggContainerError> {
|
||||||
|
// Ensure we've found the sync pattern (if required)
|
||||||
|
if self.options.find_sync {
|
||||||
|
self.find_sync()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read 27-byte page header
|
||||||
|
let mut header = [0u8; 27];
|
||||||
|
if !self.read_exact_from_source(&mut header)? {
|
||||||
|
self.finished = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Ogg page header
|
||||||
|
if &header[0..4] != b"OggS" {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"invalid Ogg capture pattern".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if header[4] != 0 {
|
||||||
|
return Err(OggContainerError::Decode("unsupported Ogg version".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_type = header[5];
|
||||||
|
let bitstream_serial = u32::from_le_bytes([header[14], header[15], header[16], header[17]]);
|
||||||
|
|
||||||
|
// Enforce single bitstream
|
||||||
|
if let Some(serial) = self.stream_serial {
|
||||||
|
if serial != bitstream_serial {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"multiple logical streams are not supported".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.stream_serial = Some(bitstream_serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read segment table
|
||||||
|
let page_segments = header[26] as usize;
|
||||||
|
let mut segment_table = vec![0u8; page_segments];
|
||||||
|
self.read_exact_from_source(&mut segment_table)?;
|
||||||
|
|
||||||
|
// Calculate page data length
|
||||||
|
let data_len: usize = segment_table.iter().map(|&v| v as usize).sum();
|
||||||
|
let mut data = vec![0u8; data_len];
|
||||||
|
self.read_exact_from_source(&mut data)?;
|
||||||
|
|
||||||
|
// Validate CRC32 if enabled
|
||||||
|
if self.options.validate_crc {
|
||||||
|
let expected_crc = u32::from_le_bytes([header[22], header[23], header[24], header[25]]);
|
||||||
|
let mut crc_header = header;
|
||||||
|
crc_header[22..26].copy_from_slice(&[0, 0, 0, 0]); // Zero out CRC field
|
||||||
|
|
||||||
|
let mut crc = crc::vorbis_crc32_update(0, &crc_header);
|
||||||
|
crc = crc::vorbis_crc32_update(crc, &segment_table);
|
||||||
|
crc = crc::vorbis_crc32_update(crc, &data);
|
||||||
|
|
||||||
|
if crc != expected_crc {
|
||||||
|
return Err(OggContainerError::Decode(format!(
|
||||||
|
"CRC32 mismatch: expected 0x{expected_crc:08x}, got 0x{crc:08x}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate continuation flags
|
||||||
|
if header_type & 0x01 != 0 && self.current_packet.is_empty() {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"unexpected continuation flag without existing packet".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if header_type & 0x01 == 0 && !self.current_packet.is_empty() {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"dangling packet without continuation flag".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble packets from segments
|
||||||
|
let mut offset: usize = 0;
|
||||||
|
for &seg_len in &segment_table {
|
||||||
|
let len = seg_len as usize;
|
||||||
|
let end = offset
|
||||||
|
.checked_add(len)
|
||||||
|
.ok_or_else(|| OggContainerError::Decode("segment length overflow".into()))?;
|
||||||
|
if end > data.len() {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"segment exceeds page data".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.current_packet.extend_from_slice(&data[offset..end]);
|
||||||
|
offset = end;
|
||||||
|
|
||||||
|
// Packet complete when segment is less than 255 bytes
|
||||||
|
if seg_len < 255 {
|
||||||
|
let packet = std::mem::take(&mut self.current_packet);
|
||||||
|
self.queue.push_back(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset != data.len() {
|
||||||
|
return Err(OggContainerError::Decode(
|
||||||
|
"page data not fully consumed".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for end-of-stream
|
||||||
|
if header_type & 0x04 != 0 && self.current_packet.is_empty() {
|
||||||
|
self.finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CRC32 calculation for Ogg pages.
|
||||||
|
///
|
||||||
|
/// This module implements the CRC32 algorithm used by the Ogg container format.
|
||||||
|
/// The polynomial is 0x04c11db7 with initial value 0 and no final XOR.
|
||||||
|
pub(crate) mod crc {
|
||||||
|
/// Precomputed CRC32 lookup table for Ogg.
|
||||||
|
///
|
||||||
|
/// Generated using the polynomial 0x04c11db7.
|
||||||
|
const fn get_tbl_elem(idx: u32) -> u32 {
|
||||||
|
let mut r: u32 = idx << 24;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < 8 {
|
||||||
|
r = (r << 1) ^ (-(((r >> 31) & 1) as i32) as u32 & 0x04c11db7);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn lookup_array() -> [u32; 0x100] {
|
||||||
|
let mut lup_arr: [u32; 0x100] = [0; 0x100];
|
||||||
|
let mut i = 0;
|
||||||
|
while i < 0x100 {
|
||||||
|
lup_arr[i] = get_tbl_elem(i as u32);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
lup_arr
|
||||||
|
}
|
||||||
|
|
||||||
|
static CRC_LOOKUP_ARRAY: &[u32] = &lookup_array();
|
||||||
|
|
||||||
|
/// Updates the CRC32 value with new data.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cur` - Current CRC32 value (use 0 for initial call)
|
||||||
|
/// * `array` - Data to include in CRC calculation
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Updated CRC32 value
|
||||||
|
pub fn vorbis_crc32_update(cur: u32, array: &[u8]) -> u32 {
|
||||||
|
let mut ret: u32 = cur;
|
||||||
|
for av in array {
|
||||||
|
ret = (ret << 8) ^ CRC_LOOKUP_ARRAY[(*av as u32 ^ (ret >> 24)) as usize];
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
//! 100% streaming (no seeking or buffering entire files).
|
//! 100% streaming (no seeking or buffering entire files).
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Read},
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
@@ -19,6 +19,7 @@ use tokio::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::ChannelReader,
|
common::ChannelReader,
|
||||||
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
||||||
|
ogg_common::{OggContainerError, OggPacketReader, OggReaderOptions},
|
||||||
pcm::StreamInfo,
|
pcm::StreamInfo,
|
||||||
stream::ManagedAsyncReader,
|
stream::ManagedAsyncReader,
|
||||||
};
|
};
|
||||||
@@ -26,38 +27,12 @@ use crate::{
|
|||||||
/// Maximum number of samples per Opus frame at 48 kHz (120 ms).
|
/// Maximum number of samples per Opus frame at 48 kHz (120 ms).
|
||||||
const MAX_FRAME_SAMPLES: usize = 5760;
|
const MAX_FRAME_SAMPLES: usize = 5760;
|
||||||
|
|
||||||
/// Errors that can occur while decoding Ogg/Opus data.
|
/// Shared error alias for the Opus decoder.
|
||||||
#[derive(thiserror::Error, Debug, Clone)]
|
pub type OggOpusError = OggContainerError;
|
||||||
pub enum OggOpusError {
|
|
||||||
#[error("I/O error ({kind:?}): {message}")]
|
|
||||||
Io {
|
|
||||||
kind: io::ErrorKind,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
#[error("Ogg/Opus decode error: {0}")]
|
|
||||||
Decode(String),
|
|
||||||
#[error("internal channel closed unexpectedly")]
|
|
||||||
ChannelClosed,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for OggOpusError {
|
impl From<OpusError> for OggContainerError {
|
||||||
fn from(err: io::Error) -> Self {
|
|
||||||
OggOpusError::Io {
|
|
||||||
kind: err.kind(),
|
|
||||||
message: err.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OpusError> for OggOpusError {
|
|
||||||
fn from(err: OpusError) -> Self {
|
fn from(err: OpusError) -> Self {
|
||||||
OggOpusError::Decode(err.to_string())
|
OggContainerError::Decode(err.to_string())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for OggOpusError {
|
|
||||||
fn from(value: String) -> Self {
|
|
||||||
OggOpusError::Decode(value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +82,15 @@ where
|
|||||||
let (info_tx, info_rx) = oneshot::channel::<Result<StreamInfo, OggOpusError>>();
|
let (info_tx, info_rx) = oneshot::channel::<Result<StreamInfo, OggOpusError>>();
|
||||||
|
|
||||||
let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), OggOpusError> {
|
let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), OggOpusError> {
|
||||||
let channel_reader = ChannelReader::<OggOpusError>::new(ingest_rx);
|
let channel_reader = ChannelReader::<OggContainerError>::new(ingest_rx);
|
||||||
let mut packet_reader = StreamingPacketReader::new(channel_reader);
|
let mut packet_reader = OggPacketReader::new(
|
||||||
|
channel_reader,
|
||||||
|
OggReaderOptions {
|
||||||
|
validate_crc: false,
|
||||||
|
find_sync: false,
|
||||||
|
..OggReaderOptions::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let header_packet = packet_reader
|
let header_packet = packet_reader
|
||||||
.next_packet()?
|
.next_packet()?
|
||||||
@@ -275,141 +257,3 @@ impl OpusTags {
|
|||||||
Ok(OpusTags)
|
Ok(OpusTags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streaming Ogg packet reader reused for Opus packets.
|
|
||||||
struct StreamingPacketReader<E>
|
|
||||||
where
|
|
||||||
E: std::error::Error + std::fmt::Display,
|
|
||||||
{
|
|
||||||
reader: ChannelReader<E>,
|
|
||||||
current_packet: Vec<u8>,
|
|
||||||
pending_packets: std::collections::VecDeque<Vec<u8>>,
|
|
||||||
finished: bool,
|
|
||||||
stream_serial: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> StreamingPacketReader<E>
|
|
||||||
where
|
|
||||||
E: std::error::Error + std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn new(reader: ChannelReader<E>) -> Self {
|
|
||||||
Self {
|
|
||||||
reader,
|
|
||||||
current_packet: Vec::new(),
|
|
||||||
pending_packets: std::collections::VecDeque::new(),
|
|
||||||
finished: false,
|
|
||||||
stream_serial: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_packet(&mut self) -> Result<Option<Vec<u8>>, OggOpusError> {
|
|
||||||
loop {
|
|
||||||
if let Some(packet) = self.pending_packets.pop_front() {
|
|
||||||
return Ok(Some(packet));
|
|
||||||
}
|
|
||||||
if self.finished {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
self.read_page()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_page(&mut self) -> Result<(), OggOpusError> {
|
|
||||||
let mut header = [0u8; 27];
|
|
||||||
if !read_exact_or_eof(&mut self.reader, &mut header)? {
|
|
||||||
self.finished = true;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if &header[0..4] != b"OggS" {
|
|
||||||
return Err(OggOpusError::Decode("invalid Ogg capture pattern".into()));
|
|
||||||
}
|
|
||||||
if header[4] != 0 {
|
|
||||||
return Err(OggOpusError::Decode("unsupported Ogg version".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let header_type = header[5];
|
|
||||||
let bitstream_serial = u32::from_le_bytes([header[14], header[15], header[16], header[17]]);
|
|
||||||
|
|
||||||
if let Some(serial) = self.stream_serial {
|
|
||||||
if serial != bitstream_serial {
|
|
||||||
return Err(OggOpusError::Decode(
|
|
||||||
"multiple logical Ogg streams are unsupported".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.stream_serial = Some(bitstream_serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
let page_segments = header[26] as usize;
|
|
||||||
let mut segment_table = vec![0u8; page_segments];
|
|
||||||
read_exact_checked(&mut self.reader, &mut segment_table)?;
|
|
||||||
|
|
||||||
let data_len: usize = segment_table.iter().map(|&v| v as usize).sum();
|
|
||||||
let mut data = vec![0u8; data_len];
|
|
||||||
read_exact_checked(&mut self.reader, &mut data)?;
|
|
||||||
|
|
||||||
if header_type & 0x01 != 0 && self.current_packet.is_empty() {
|
|
||||||
return Err(OggOpusError::Decode(
|
|
||||||
"continuation flag set without existing packet".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if header_type & 0x01 == 0 && !self.current_packet.is_empty() {
|
|
||||||
return Err(OggOpusError::Decode(
|
|
||||||
"expected continuation flag for unfinished packet".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut offset = 0usize;
|
|
||||||
for &seg_len in &segment_table {
|
|
||||||
let len = seg_len as usize;
|
|
||||||
let end = offset
|
|
||||||
.checked_add(len)
|
|
||||||
.ok_or_else(|| OggOpusError::Decode("segment length overflow".into()))?;
|
|
||||||
if end > data.len() {
|
|
||||||
return Err(OggOpusError::Decode("segment exceeds page data".into()));
|
|
||||||
}
|
|
||||||
self.current_packet.extend_from_slice(&data[offset..end]);
|
|
||||||
offset = end;
|
|
||||||
|
|
||||||
if seg_len < 255 {
|
|
||||||
let packet = std::mem::take(&mut self.current_packet);
|
|
||||||
self.pending_packets.push_back(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset != data.len() {
|
|
||||||
return Err(OggOpusError::Decode("page data not fully consumed".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if header_type & 0x04 != 0 && self.current_packet.is_empty() {
|
|
||||||
self.finished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_exact_or_eof<R: Read>(reader: &mut R, buf: &mut [u8]) -> io::Result<bool> {
|
|
||||||
let mut read = 0;
|
|
||||||
while read < buf.len() {
|
|
||||||
match reader.read(&mut buf[read..])? {
|
|
||||||
0 if read == 0 => return Ok(false),
|
|
||||||
0 => {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
"unexpected EOF while reading",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
n => read += n,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_exact_checked<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<(), OggOpusError> {
|
|
||||||
if !read_exact_or_eof(reader, buf)? {
|
|
||||||
return Err(OggOpusError::Decode("unexpected EOF in Ogg stream".into()));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ fn to_didl_lite(containers: &[Container], items: &[pmodidl::Item]) -> Result<Str
|
|||||||
items: items.to_vec(),
|
items: items.to_vec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let body =
|
let body = quick_xml::se::to_string(&didl)
|
||||||
quick_xml::se::to_string(&didl).map_err(|e| format!("Failed to serialize DIDL-Lite: {}", e))?;
|
.map_err(|e| format!("Failed to serialize DIDL-Lite: {}", e))?;
|
||||||
Ok(format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>{}", body))
|
Ok(format!(
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{}",
|
||||||
|
body
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler pour le service ContentDirectory
|
/// Handler pour le service ContentDirectory
|
||||||
@@ -99,8 +102,8 @@ impl ContentHandler {
|
|||||||
if object_id == "0" {
|
if object_id == "0" {
|
||||||
// Retourner le container racine
|
// Retourner le container racine
|
||||||
let root = self.build_root_container().await;
|
let root = self.build_root_container().await;
|
||||||
let didl = to_didl_lite(&[root], &[])?;
|
let didl = to_didl_lite(&[root], &[])?;
|
||||||
Ok((didl, 1, 1, 1))
|
Ok((didl, 1, 1, 1))
|
||||||
} else {
|
} else {
|
||||||
// Essayer de trouver l'objet dans les sources
|
// Essayer de trouver l'objet dans les sources
|
||||||
// Vérifier si c'est un container racine d'une source
|
// Vérifier si c'est un container racine d'une source
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub fn init() -> Result<()> {
|
|||||||
/// PCM chunk with decoded audio data
|
/// PCM chunk with decoded audio data
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PCMChunk {
|
pub struct PCMChunk {
|
||||||
pub samples: Vec<i16>, // Interleaved 16-bit samples
|
pub samples: Vec<i16>, // Interleaved 16-bit samples
|
||||||
pub sample_rate: u32,
|
pub sample_rate: u32,
|
||||||
pub channels: u32,
|
pub channels: u32,
|
||||||
pub position_ms: u64,
|
pub position_ms: u64,
|
||||||
@@ -52,7 +52,7 @@ impl ProgressiveDecoder {
|
|||||||
let mut buffer = vec![0u8; 8192];
|
let mut buffer = vec![0u8; 8192];
|
||||||
loop {
|
loop {
|
||||||
match stream.read(&mut buffer) {
|
match stream.read(&mut buffer) {
|
||||||
Ok(0) => break, // EOF
|
Ok(0) => break, // EOF
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
let chunk = Bytes::copy_from_slice(&buffer[..n]);
|
let chunk = Bytes::copy_from_slice(&buffer[..n]);
|
||||||
if tx.send(Ok(chunk)).is_err() {
|
if tx.send(Ok(chunk)).is_err() {
|
||||||
|
|||||||
@@ -881,7 +881,7 @@ fn ms_to_frames(ms: u64, sample_rate: u32) -> usize {
|
|||||||
|
|
||||||
fn decode_block_audio(data: Vec<u8>) -> anyhow::Result<DecodedBlock> {
|
fn decode_block_audio(data: Vec<u8>) -> anyhow::Result<DecodedBlock> {
|
||||||
use symphonia::core::audio::SampleBuffer;
|
use symphonia::core::audio::SampleBuffer;
|
||||||
use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions};
|
use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
|
||||||
use symphonia::core::errors::Error as SymphoniaError;
|
use symphonia::core::errors::Error as SymphoniaError;
|
||||||
use symphonia::core::formats::FormatOptions;
|
use symphonia::core::formats::FormatOptions;
|
||||||
use symphonia::core::io::MediaSourceStream;
|
use symphonia::core::io::MediaSourceStream;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! cargo run -p pmoplaylist --example basic_usage
|
//! cargo run -p pmoplaylist --example basic_usage
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use pmoplaylist::{DEFAULT_IMAGE, FifoPlaylist, Track};
|
use pmoplaylist::{FifoPlaylist, Track, DEFAULT_IMAGE};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
//! cargo run -p pmoplaylist --example http_server_integration
|
//! cargo run -p pmoplaylist --example http_server_integration
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use pmoplaylist::{DEFAULT_IMAGE, FifoPlaylist, Track};
|
use pmoplaylist::{FifoPlaylist, Track, DEFAULT_IMAGE};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
//! cargo run -p pmoplaylist --example radio_streaming
|
//! cargo run -p pmoplaylist --example radio_streaming
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use pmoplaylist::{DEFAULT_IMAGE, FifoPlaylist, Track};
|
use pmoplaylist::{FifoPlaylist, Track, DEFAULT_IMAGE};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,31 @@
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Playlist not found: {0}")]
|
#[error("Playlist not found: {0}")]
|
||||||
PlaylistNotFound(String),
|
PlaylistNotFound(String),
|
||||||
|
|
||||||
#[error("Playlist deleted: {0}")]
|
#[error("Playlist deleted: {0}")]
|
||||||
PlaylistDeleted(String),
|
PlaylistDeleted(String),
|
||||||
|
|
||||||
#[error("Playlist already exists: {0}")]
|
#[error("Playlist already exists: {0}")]
|
||||||
PlaylistAlreadyExists(String),
|
PlaylistAlreadyExists(String),
|
||||||
|
|
||||||
#[error("Playlist is not persistent: {0}")]
|
#[error("Playlist is not persistent: {0}")]
|
||||||
PlaylistNotPersistent(String),
|
PlaylistNotPersistent(String),
|
||||||
|
|
||||||
#[error("Write lock already held for playlist: {0}")]
|
#[error("Write lock already held for playlist: {0}")]
|
||||||
WriteLockHeld(String),
|
WriteLockHeld(String),
|
||||||
|
|
||||||
#[error("Cache entry not found: {0}")]
|
#[error("Cache entry not found: {0}")]
|
||||||
CacheEntryNotFound(String),
|
CacheEntryNotFound(String),
|
||||||
|
|
||||||
#[error("Cache error: {0}")]
|
#[error("Cache error: {0}")]
|
||||||
CacheError(String),
|
CacheError(String),
|
||||||
|
|
||||||
#[error("Persistence error: {0}")]
|
#[error("Persistence error: {0}")]
|
||||||
PersistenceError(String),
|
PersistenceError(String),
|
||||||
|
|
||||||
#[error("PlaylistManager not initialized")]
|
#[error("PlaylistManager not initialized")]
|
||||||
ManagerNotInitialized,
|
ManagerNotInitialized,
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl ReadHandle {
|
|||||||
cursor: AtomicUsize::new(0),
|
cursor: AtomicUsize::new(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pop le prochain morceau (avance le curseur)
|
/// Pop le prochain morceau (avance le curseur)
|
||||||
///
|
///
|
||||||
/// Skip automatiquement les entrées invalides dans le cache.
|
/// Skip automatiquement les entrées invalides dans le cache.
|
||||||
@@ -31,24 +31,24 @@ impl ReadHandle {
|
|||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = self.cursor.load(Ordering::SeqCst);
|
let pos = self.cursor.load(Ordering::SeqCst);
|
||||||
|
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
|
|
||||||
// Fin de playlist ?
|
// Fin de playlist ?
|
||||||
if pos >= core.len() {
|
if pos >= core.len() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let record = match core.get(pos) {
|
let record = match core.get(pos) {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cache_pk = record.cache_pk.clone();
|
let cache_pk = record.cache_pk.clone();
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
// Vérifier validité dans le cache
|
// Vérifier validité dans le cache
|
||||||
let cache = crate::manager::audio_cache()?;
|
let cache = crate::manager::audio_cache()?;
|
||||||
if cache.is_valid_pk(&cache_pk) {
|
if cache.is_valid_pk(&cache_pk) {
|
||||||
@@ -61,31 +61,33 @@ impl ReadHandle {
|
|||||||
let mut core = self.playlist.core.write().await;
|
let mut core = self.playlist.core.write().await;
|
||||||
core.remove_by_cache_pk(&cache_pk);
|
core.remove_by_cache_pk(&cache_pk);
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
// Sauvegarder si persistante
|
// Sauvegarder si persistante
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
|
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let _ = persistence.save_playlist(&self.playlist.id, &title, &core.config, &core.tracks).await;
|
let _ = persistence
|
||||||
|
.save_playlist(&self.playlist.id, &title, &core.config, &core.tracks)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ne pas avancer le curseur, continuer avec la position actuelle
|
// Ne pas avancer le curseur, continuer avec la position actuelle
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peek le prochain morceau sans avancer le curseur
|
/// Peek le prochain morceau sans avancer le curseur
|
||||||
pub async fn peek(&self) -> Result<Option<PlaylistTrack>> {
|
pub async fn peek(&self) -> Result<Option<PlaylistTrack>> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = self.cursor.load(Ordering::SeqCst);
|
let pos = self.cursor.load(Ordering::SeqCst);
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
|
|
||||||
match core.get(pos) {
|
match core.get(pos) {
|
||||||
Some(record) => {
|
Some(record) => {
|
||||||
// Vérifier validité
|
// Vérifier validité
|
||||||
@@ -99,28 +101,28 @@ impl ReadHandle {
|
|||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Position actuelle du curseur
|
/// Position actuelle du curseur
|
||||||
pub fn position(&self) -> usize {
|
pub fn position(&self) -> usize {
|
||||||
self.cursor.load(Ordering::SeqCst)
|
self.cursor.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nombre de morceaux restants (compte uniquement les valides)
|
/// Nombre de morceaux restants (compte uniquement les valides)
|
||||||
pub async fn remaining(&self) -> Result<usize> {
|
pub async fn remaining(&self) -> Result<usize> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = self.cursor.load(Ordering::SeqCst);
|
let pos = self.cursor.load(Ordering::SeqCst);
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
|
|
||||||
if pos >= core.len() {
|
if pos >= core.len() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache = crate::manager::audio_cache()?;
|
let cache = crate::manager::audio_cache()?;
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
for i in pos..core.len() {
|
for i in pos..core.len() {
|
||||||
if let Some(record) = core.get(i) {
|
if let Some(record) = core.get(i) {
|
||||||
if cache.is_valid_pk(&record.cache_pk) {
|
if cache.is_valid_pk(&record.cache_pk) {
|
||||||
@@ -128,38 +130,38 @@ impl ReadHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crée un nouveau handle avec cursor à 0
|
/// Crée un nouveau handle avec cursor à 0
|
||||||
pub fn get_new_handle(&self) -> Result<ReadHandle> {
|
pub fn get_new_handle(&self) -> Result<ReadHandle> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ReadHandle::new(self.playlist.clone()))
|
Ok(ReadHandle::new(self.playlist.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vérifie si la playlist est vivante
|
/// Vérifie si la playlist est vivante
|
||||||
pub fn is_alive(&self) -> bool {
|
pub fn is_alive(&self) -> bool {
|
||||||
self.playlist.is_alive()
|
self.playlist.is_alive()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ID de la playlist
|
/// ID de la playlist
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> &str {
|
||||||
&self.playlist.id
|
&self.playlist.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Génère un Container DIDL-Lite
|
/// Génère un Container DIDL-Lite
|
||||||
pub async fn to_container(&self) -> Result<Container> {
|
pub async fn to_container(&self) -> Result<Container> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
let remaining = self.remaining().await?;
|
let remaining = self.remaining().await?;
|
||||||
|
|
||||||
Ok(Container {
|
Ok(Container {
|
||||||
id: self.playlist.id.clone(),
|
id: self.playlist.id.clone(),
|
||||||
parent_id: "0".to_string(),
|
parent_id: "0".to_string(),
|
||||||
@@ -172,48 +174,48 @@ impl ReadHandle {
|
|||||||
items: vec![],
|
items: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Génère des Items DIDL-Lite depuis la position actuelle
|
/// Génère des Items DIDL-Lite depuis la position actuelle
|
||||||
pub async fn to_items(&self, limit: usize) -> Result<Vec<Item>> {
|
pub async fn to_items(&self, limit: usize) -> Result<Vec<Item>> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = self.cursor.load(Ordering::SeqCst);
|
let pos = self.cursor.load(Ordering::SeqCst);
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let cache = crate::manager::audio_cache()?;
|
let cache = crate::manager::audio_cache()?;
|
||||||
|
|
||||||
// Récupérer base_url depuis le cache (via route_for)
|
// Récupérer base_url depuis le cache (via route_for)
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
|
|
||||||
for i in pos..core.len() {
|
for i in pos..core.len() {
|
||||||
if items.len() >= limit {
|
if items.len() >= limit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let record = match core.get(i) {
|
let record = match core.get(i) {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vérifier validité
|
// Vérifier validité
|
||||||
if !cache.is_valid_pk(&record.cache_pk) {
|
if !cache.is_valid_pk(&record.cache_pk) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger métadonnées
|
// Charger métadonnées
|
||||||
let metadata = match pmoaudiocache::get_metadata(&*cache, &record.cache_pk) {
|
let metadata = match pmoaudiocache::get_metadata(&*cache, &record.cache_pk) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construire l'URL via route_for
|
// Construire l'URL via route_for
|
||||||
let url = cache.route_for(&record.cache_pk, None);
|
let url = cache.route_for(&record.cache_pk, None);
|
||||||
|
|
||||||
// Créer le Resource DIDL
|
// Créer le Resource DIDL
|
||||||
let resource = metadata.to_didl_resource(url);
|
let resource = metadata.to_didl_resource(url);
|
||||||
|
|
||||||
// Créer l'Item
|
// Créer l'Item
|
||||||
let item = Item {
|
let item = Item {
|
||||||
id: format!("{}:{}", self.playlist.id, pos + idx),
|
id: format!("{}:{}", self.playlist.id, pos + idx),
|
||||||
@@ -232,11 +234,11 @@ impl ReadHandle {
|
|||||||
resources: vec![resource],
|
resources: vec![resource],
|
||||||
descriptions: vec![],
|
descriptions: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,41 +22,41 @@ impl WriteHandle {
|
|||||||
_write_token: write_token,
|
_write_token: write_token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ajoute un morceau à la playlist
|
/// Ajoute un morceau à la playlist
|
||||||
pub async fn push(&self, cache_pk: String) -> Result<()> {
|
pub async fn push(&self, cache_pk: String) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que le pk existe dans le cache
|
// Vérifier que le pk existe dans le cache
|
||||||
let cache = crate::manager::audio_cache()?;
|
let cache = crate::manager::audio_cache()?;
|
||||||
if !cache.is_valid_pk(&cache_pk) {
|
if !cache.is_valid_pk(&cache_pk) {
|
||||||
return Err(crate::Error::CacheEntryNotFound(cache_pk));
|
return Err(crate::Error::CacheEntryNotFound(cache_pk));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter à la playlist
|
// Ajouter à la playlist
|
||||||
let record = Record::new(cache_pk);
|
let record = Record::new(cache_pk);
|
||||||
let mut core = self.playlist.core.write().await;
|
let mut core = self.playlist.core.write().await;
|
||||||
core.push(record);
|
core.push(record);
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
self.playlist.touch().await;
|
self.playlist.touch().await;
|
||||||
|
|
||||||
// Sauvegarder si persistante
|
// Sauvegarder si persistante
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
self.save_to_db().await?;
|
self.save_to_db().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ajoute plusieurs morceaux de manière atomique
|
/// Ajoute plusieurs morceaux de manière atomique
|
||||||
pub async fn push_set(&self, cache_pks: Vec<String>) -> Result<()> {
|
pub async fn push_set(&self, cache_pks: Vec<String>) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier tous les pks d'abord
|
// Vérifier tous les pks d'abord
|
||||||
let cache = crate::manager::audio_cache()?;
|
let cache = crate::manager::audio_cache()?;
|
||||||
for pk in &cache_pks {
|
for pk in &cache_pks {
|
||||||
@@ -64,195 +64,194 @@ impl WriteHandle {
|
|||||||
return Err(crate::Error::CacheEntryNotFound(pk.clone()));
|
return Err(crate::Error::CacheEntryNotFound(pk.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer tous les records
|
// Créer tous les records
|
||||||
let records: Vec<Record> = cache_pks.into_iter()
|
let records: Vec<Record> = cache_pks.into_iter().map(Record::new).collect();
|
||||||
.map(Record::new)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Ajouter atomiquement
|
// Ajouter atomiquement
|
||||||
let mut core = self.playlist.core.write().await;
|
let mut core = self.playlist.core.write().await;
|
||||||
core.push_all(records);
|
core.push_all(records);
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
self.playlist.touch().await;
|
self.playlist.touch().await;
|
||||||
|
|
||||||
// Une seule sauvegarde pour tout le batch
|
// Une seule sauvegarde pour tout le batch
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
self.save_to_db().await?;
|
self.save_to_db().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vide la playlist
|
/// Vide la playlist
|
||||||
pub async fn flush(&self) -> Result<()> {
|
pub async fn flush(&self) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut core = self.playlist.core.write().await;
|
let mut core = self.playlist.core.write().await;
|
||||||
core.clear();
|
core.clear();
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
self.playlist.touch().await;
|
self.playlist.touch().await;
|
||||||
|
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
self.save_to_db().await?;
|
self.save_to_db().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime la playlist définitivement
|
/// Supprime la playlist définitivement
|
||||||
pub async fn delete(self) -> Result<()> {
|
pub async fn delete(self) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marquer comme supprimée
|
// Marquer comme supprimée
|
||||||
self.playlist.mark_deleted();
|
self.playlist.mark_deleted();
|
||||||
|
|
||||||
// Supprimer du manager
|
// Supprimer du manager
|
||||||
crate::manager::delete_playlist_internal(&self.playlist.id).await?;
|
crate::manager::delete_playlist_internal(&self.playlist.id).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change le titre
|
/// Change le titre
|
||||||
pub async fn set_title(&self, title: String) -> Result<()> {
|
pub async fn set_title(&self, title: String) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.playlist.set_title(title).await;
|
self.playlist.set_title(title).await;
|
||||||
|
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
self.save_to_db().await?;
|
self.save_to_db().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change la capacité maximale
|
/// Change la capacité maximale
|
||||||
pub async fn set_capacity(&self, max_size: Option<usize>) -> Result<()> {
|
pub async fn set_capacity(&self, max_size: Option<usize>) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut core = self.playlist.core.write().await;
|
let mut core = self.playlist.core.write().await;
|
||||||
core.set_capacity(max_size);
|
core.set_capacity(max_size);
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
self.playlist.touch().await;
|
self.playlist.touch().await;
|
||||||
|
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
self.save_to_db().await?;
|
self.save_to_db().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change le TTL par défaut
|
/// Change le TTL par défaut
|
||||||
pub async fn set_default_ttl(&self, ttl: Option<Duration>) -> Result<()> {
|
pub async fn set_default_ttl(&self, ttl: Option<Duration>) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut core = self.playlist.core.write().await;
|
let mut core = self.playlist.core.write().await;
|
||||||
core.set_default_ttl(ttl);
|
core.set_default_ttl(ttl);
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
self.playlist.touch().await;
|
self.playlist.touch().await;
|
||||||
|
|
||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
self.save_to_db().await?;
|
self.save_to_db().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone vers une nouvelle playlist persistante
|
/// Clone vers une nouvelle playlist persistante
|
||||||
pub async fn clone_as_persistent(&self, new_id: String) -> Result<WriteHandle> {
|
pub async fn clone_as_persistent(&self, new_id: String) -> Result<WriteHandle> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les données actuelles
|
// Récupérer les données actuelles
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let config = core.config.clone();
|
let config = core.config.clone();
|
||||||
let tracks = core.snapshot();
|
let tracks = core.snapshot();
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
// Créer la nouvelle playlist persistante
|
// Créer la nouvelle playlist persistante
|
||||||
let manager = crate::manager::PlaylistManager();
|
let manager = crate::manager::PlaylistManager();
|
||||||
let mut new_handle = manager.create_persistent_playlist(new_id).await?;
|
let mut new_handle = manager.create_persistent_playlist(new_id).await?;
|
||||||
|
|
||||||
// Copier le titre et la config
|
// Copier le titre et la config
|
||||||
new_handle.set_title(title).await?;
|
new_handle.set_title(title).await?;
|
||||||
new_handle.set_capacity(config.max_size).await?;
|
new_handle.set_capacity(config.max_size).await?;
|
||||||
new_handle.set_default_ttl(config.default_ttl).await?;
|
new_handle.set_default_ttl(config.default_ttl).await?;
|
||||||
|
|
||||||
// Copier tous les morceaux
|
// Copier tous les morceaux
|
||||||
let pks: Vec<String> = tracks.iter()
|
let pks: Vec<String> = tracks.iter().map(|r| r.cache_pk.clone()).collect();
|
||||||
.map(|r| r.cache_pk.clone())
|
|
||||||
.collect();
|
|
||||||
new_handle.push_set(pks).await?;
|
new_handle.push_set(pks).await?;
|
||||||
|
|
||||||
Ok(new_handle)
|
Ok(new_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Métadonnées
|
// Métadonnées
|
||||||
|
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> &str {
|
||||||
&self.playlist.id
|
&self.playlist.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn title(&self) -> String {
|
pub async fn title(&self) -> String {
|
||||||
self.playlist.title().await
|
self.playlist.title().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_persistent(&self) -> bool {
|
pub fn is_persistent(&self) -> bool {
|
||||||
self.playlist.persistent
|
self.playlist.persistent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn capacity(&self) -> Option<usize> {
|
pub async fn capacity(&self) -> Option<usize> {
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
core.config.max_size
|
core.config.max_size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn default_ttl(&self) -> Option<Duration> {
|
pub async fn default_ttl(&self) -> Option<Duration> {
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
core.config.default_ttl
|
core.config.default_ttl
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn len(&self) -> usize {
|
pub async fn len(&self) -> usize {
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
core.len()
|
core.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_empty(&self) -> bool {
|
pub async fn is_empty(&self) -> bool {
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
core.is_empty()
|
core.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn last_change(&self) -> SystemTime {
|
pub async fn last_change(&self) -> SystemTime {
|
||||||
self.playlist.last_change().await
|
self.playlist.last_change().await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers internes
|
// Helpers internes
|
||||||
|
|
||||||
async fn save_to_db(&self) -> Result<()> {
|
async fn save_to_db(&self) -> Result<()> {
|
||||||
let manager = crate::manager::PlaylistManager();
|
let manager = crate::manager::PlaylistManager();
|
||||||
let persistence = manager.persistence()
|
let persistence = manager
|
||||||
|
.persistence()
|
||||||
.ok_or_else(|| crate::Error::PersistenceError("No persistence manager".into()))?;
|
.ok_or_else(|| crate::Error::PersistenceError("No persistence manager".into()))?;
|
||||||
|
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let config = &core.config;
|
let config = &core.config;
|
||||||
let tracks = &core.tracks;
|
let tracks = &core.tracks;
|
||||||
|
|
||||||
persistence.save_playlist(&self.playlist.id, &title, config, tracks).await
|
persistence
|
||||||
|
.save_playlist(&self.playlist.id, &title, config, tracks)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ impl PlaylistManager {
|
|||||||
|
|
||||||
#[cfg(not(feature = "pmoconfig"))]
|
#[cfg(not(feature = "pmoconfig"))]
|
||||||
{
|
{
|
||||||
PLAYLIST_MANAGER.get().expect("PlaylistManager not initialized. Call init() first.")
|
PLAYLIST_MANAGER
|
||||||
|
.get()
|
||||||
|
.expect("PlaylistManager not initialized. Call init() first.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +104,8 @@ impl PlaylistManager {
|
|||||||
if let Some(persistence) = &self.inner.persistence {
|
if let Some(persistence) = &self.inner.persistence {
|
||||||
let title = playlist.title().await;
|
let title = playlist.title().await;
|
||||||
let core = playlist.core.read().await;
|
let core = playlist.core.read().await;
|
||||||
persistence.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
persistence
|
||||||
|
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,12 +188,7 @@ impl PlaylistManager {
|
|||||||
// Reconstruire la playlist
|
// Reconstruire la playlist
|
||||||
let mut playlists = self.inner.playlists.write().await;
|
let mut playlists = self.inner.playlists.write().await;
|
||||||
|
|
||||||
let playlist = Arc::new(Playlist::new(
|
let playlist = Arc::new(Playlist::new(id.to_string(), title.clone(), config, true));
|
||||||
id.to_string(),
|
|
||||||
title.clone(),
|
|
||||||
config,
|
|
||||||
true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Restaurer les tracks
|
// Restaurer les tracks
|
||||||
{
|
{
|
||||||
@@ -265,7 +263,9 @@ impl PlaylistManager {
|
|||||||
if let Some(persistence) = &self.inner.persistence {
|
if let Some(persistence) = &self.inner.persistence {
|
||||||
let title = playlist.title().await;
|
let title = playlist.title().await;
|
||||||
let core = playlist.core.read().await;
|
let core = playlist.core.read().await;
|
||||||
let _ = persistence.save_playlist(&playlist.id, &title, &core.config, &core.tracks).await;
|
let _ = persistence
|
||||||
|
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,8 +286,7 @@ pub(crate) async fn delete_playlist_internal(id: &str) -> Result<()> {
|
|||||||
|
|
||||||
/// Helper pour acc<63>der au cache audio
|
/// Helper pour acc<63>der au cache audio
|
||||||
pub(crate) fn audio_cache() -> Result<Arc<pmoaudiocache::Cache>> {
|
pub(crate) fn audio_cache() -> Result<Arc<pmoaudiocache::Cache>> {
|
||||||
pmoupnp::get_audio_cache()
|
pmoupnp::get_audio_cache().ok_or_else(|| crate::Error::ManagerNotInitialized)
|
||||||
.ok_or_else(|| crate::Error::ManagerNotInitialized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fonction raccourcie pour acc<63>der au singleton
|
/// Fonction raccourcie pour acc<63>der au singleton
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ impl PersistenceManager {
|
|||||||
pub fn new(db_path: &Path) -> Result<Self> {
|
pub fn new(db_path: &Path) -> Result<Self> {
|
||||||
// Créer le répertoire parent si nécessaire
|
// Créer le répertoire parent si nécessaire
|
||||||
if let Some(parent) = db_path.parent() {
|
if let Some(parent) = db_path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent).map_err(|e| {
|
||||||
.map_err(|e| crate::Error::PersistenceError(format!("Failed to create directory: {}", e)))?;
|
crate::Error::PersistenceError(format!("Failed to create directory: {}", e))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = Connection::open(db_path)
|
let conn = Connection::open(db_path).map_err(|e| {
|
||||||
.map_err(|e| crate::Error::PersistenceError(format!("Failed to open database: {}", e)))?;
|
crate::Error::PersistenceError(format!("Failed to open database: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Créer les tables
|
// Créer les tables
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS playlists (
|
"CREATE TABLE IF NOT EXISTS playlists (
|
||||||
@@ -37,8 +39,11 @@ impl PersistenceManager {
|
|||||||
last_modified INTEGER NOT NULL
|
last_modified INTEGER NOT NULL
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to create playlists table: {}", e)))?;
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to create playlists table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS tracks (
|
"CREATE TABLE IF NOT EXISTS tracks (
|
||||||
playlist_id TEXT NOT NULL,
|
playlist_id TEXT NOT NULL,
|
||||||
@@ -48,23 +53,28 @@ impl PersistenceManager {
|
|||||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to create tracks table: {}", e)))?;
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to create tracks table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_tracks_playlist ON tracks(playlist_id, added_at)",
|
"CREATE INDEX IF NOT EXISTS idx_tracks_playlist ON tracks(playlist_id, added_at)",
|
||||||
[],
|
[],
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to create index: {}", e)))?;
|
)
|
||||||
|
.map_err(|e| crate::Error::PersistenceError(format!("Failed to create index: {}", e)))?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_tracks_cache_pk ON tracks(cache_pk)",
|
"CREATE INDEX IF NOT EXISTS idx_tracks_cache_pk ON tracks(cache_pk)",
|
||||||
[],
|
[],
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to create index: {}", e)))?;
|
)
|
||||||
|
.map_err(|e| crate::Error::PersistenceError(format!("Failed to create index: {}", e)))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
conn: Arc::new(Mutex::new(conn)),
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sauvegarde une playlist complète
|
/// Sauvegarde une playlist complète
|
||||||
pub async fn save_playlist(
|
pub async fn save_playlist(
|
||||||
&self,
|
&self,
|
||||||
@@ -74,12 +84,12 @@ impl PersistenceManager {
|
|||||||
tracks: &VecDeque<Arc<Record>>,
|
tracks: &VecDeque<Arc<Record>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
|
||||||
let now_nanos = SystemTime::now()
|
let now_nanos = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_nanos() as i64;
|
.as_nanos() as i64;
|
||||||
|
|
||||||
// Upsert playlist metadata
|
// Upsert playlist metadata
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO playlists (id, title, max_size, default_ttl_secs, created_at, last_modified)
|
"INSERT OR REPLACE INTO playlists (id, title, max_size, default_ttl_secs, created_at, last_modified)
|
||||||
@@ -94,13 +104,13 @@ impl PersistenceManager {
|
|||||||
now_nanos,
|
now_nanos,
|
||||||
],
|
],
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to save playlist: {}", e)))?;
|
).map_err(|e| crate::Error::PersistenceError(format!("Failed to save playlist: {}", e)))?;
|
||||||
|
|
||||||
// Supprimer les anciens tracks
|
// Supprimer les anciens tracks
|
||||||
conn.execute(
|
conn.execute("DELETE FROM tracks WHERE playlist_id = ?1", params![id])
|
||||||
"DELETE FROM tracks WHERE playlist_id = ?1",
|
.map_err(|e| {
|
||||||
params![id],
|
crate::Error::PersistenceError(format!("Failed to delete old tracks: {}", e))
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to delete old tracks: {}", e)))?;
|
})?;
|
||||||
|
|
||||||
// Insérer les nouveaux tracks
|
// Insérer les nouveaux tracks
|
||||||
for record in tracks {
|
for record in tracks {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -112,26 +122,34 @@ impl PersistenceManager {
|
|||||||
&record.cache_pk,
|
&record.cache_pk,
|
||||||
record.ttl.map(|d| d.as_secs() as i64),
|
record.ttl.map(|d| d.as_secs() as i64),
|
||||||
],
|
],
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to insert track: {}", e)))?;
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to insert track: {}", e))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Charge une playlist
|
/// Charge une playlist
|
||||||
pub async fn load_playlist(&self, id: &str) -> Result<Option<(String, PlaylistConfig, VecDeque<Arc<Record>>)>> {
|
pub async fn load_playlist(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<Option<(String, PlaylistConfig, VecDeque<Arc<Record>>)>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
|
||||||
// Charger les métadonnées
|
// Charger les métadonnées
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn
|
||||||
"SELECT title, max_size, default_ttl_secs FROM playlists WHERE id = ?1"
|
.prepare("SELECT title, max_size, default_ttl_secs FROM playlists WHERE id = ?1")
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let result = stmt.query_row(params![id], |row| {
|
let result = stmt.query_row(params![id], |row| {
|
||||||
let title: String = row.get(0)?;
|
let title: String = row.get(0)?;
|
||||||
let max_size: Option<i64> = row.get(1)?;
|
let max_size: Option<i64> = row.get(1)?;
|
||||||
let default_ttl_secs: Option<i64> = row.get(2)?;
|
let default_ttl_secs: Option<i64> = row.get(2)?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
title,
|
title,
|
||||||
PlaylistConfig {
|
PlaylistConfig {
|
||||||
@@ -140,76 +158,91 @@ impl PersistenceManager {
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
let (title, config) = match result {
|
let (title, config) = match result {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||||
Err(e) => return Err(crate::Error::PersistenceError(format!("Failed to load playlist: {}", e))),
|
Err(e) => {
|
||||||
|
return Err(crate::Error::PersistenceError(format!(
|
||||||
|
"Failed to load playlist: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Charger les tracks
|
// Charger les tracks
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT added_at, cache_pk, ttl_secs FROM tracks WHERE playlist_id = ?1 ORDER BY added_at ASC"
|
"SELECT added_at, cache_pk, ttl_secs FROM tracks WHERE playlist_id = ?1 ORDER BY added_at ASC"
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e)))?;
|
).map_err(|e| crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e)))?;
|
||||||
|
|
||||||
let rows = stmt.query_map(params![id], |row| {
|
let rows = stmt
|
||||||
let added_at_nanos: i64 = row.get(0)?;
|
.query_map(params![id], |row| {
|
||||||
let cache_pk: String = row.get(1)?;
|
let added_at_nanos: i64 = row.get(0)?;
|
||||||
let ttl_secs: Option<i64> = row.get(2)?;
|
let cache_pk: String = row.get(1)?;
|
||||||
|
let ttl_secs: Option<i64> = row.get(2)?;
|
||||||
let added_at = UNIX_EPOCH + Duration::from_nanos(added_at_nanos as u64);
|
|
||||||
let ttl = ttl_secs.map(|s| Duration::from_secs(s as u64));
|
let added_at = UNIX_EPOCH + Duration::from_nanos(added_at_nanos as u64);
|
||||||
|
let ttl = ttl_secs.map(|s| Duration::from_secs(s as u64));
|
||||||
Ok(Record {
|
|
||||||
cache_pk,
|
Ok(Record {
|
||||||
added_at,
|
cache_pk,
|
||||||
ttl,
|
added_at,
|
||||||
|
ttl,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}).map_err(|e| crate::Error::PersistenceError(format!("Failed to query tracks: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to query tracks: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut tracks = VecDeque::new();
|
let mut tracks = VecDeque::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let record = row.map_err(|e| crate::Error::PersistenceError(format!("Failed to read track: {}", e)))?;
|
let record = row.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to read track: {}", e))
|
||||||
|
})?;
|
||||||
tracks.push_back(Arc::new(record));
|
tracks.push_back(Arc::new(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some((title, config, tracks)))
|
Ok(Some((title, config, tracks)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime une playlist
|
/// Supprime une playlist
|
||||||
pub async fn delete_playlist(&self, id: &str) -> Result<()> {
|
pub async fn delete_playlist(&self, id: &str) -> Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
conn.execute(
|
conn.execute("DELETE FROM playlists WHERE id = ?1", params![id])
|
||||||
"DELETE FROM playlists WHERE id = ?1",
|
.map_err(|e| {
|
||||||
params![id],
|
crate::Error::PersistenceError(format!("Failed to delete playlist: {}", e))
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to delete playlist: {}", e)))?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Liste toutes les playlists persistantes
|
/// Liste toutes les playlists persistantes
|
||||||
pub async fn list_playlist_ids(&self) -> Result<Vec<String>> {
|
pub async fn list_playlist_ids(&self) -> Result<Vec<String>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let mut stmt = conn.prepare("SELECT id FROM playlists")
|
let mut stmt = conn.prepare("SELECT id FROM playlists").map_err(|e| {
|
||||||
.map_err(|e| crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e)))?;
|
crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e))
|
||||||
|
})?;
|
||||||
let rows = stmt.query_map([], |row| row.get(0))
|
|
||||||
.map_err(|e| crate::Error::PersistenceError(format!("Failed to query playlists: {}", e)))?;
|
let rows = stmt.query_map([], |row| row.get(0)).map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to query playlists: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut ids = Vec::new();
|
let mut ids = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
ids.push(row.map_err(|e| crate::Error::PersistenceError(format!("Failed to read id: {}", e)))?);
|
ids.push(row.map_err(|e| {
|
||||||
|
crate::Error::PersistenceError(format!("Failed to read id: {}", e))
|
||||||
|
})?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime tous les tracks contenant un cache_pk donné
|
/// Supprime tous les tracks contenant un cache_pk donné
|
||||||
pub async fn remove_by_cache_pk(&self, cache_pk: &str) -> Result<()> {
|
pub async fn remove_by_cache_pk(&self, cache_pk: &str) -> Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
conn.execute(
|
conn.execute("DELETE FROM tracks WHERE cache_pk = ?1", params![cache_pk])
|
||||||
"DELETE FROM tracks WHERE cache_pk = ?1",
|
.map_err(|e| {
|
||||||
params![cache_pk],
|
crate::Error::PersistenceError(format!("Failed to remove tracks: {}", e))
|
||||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to remove tracks: {}", e)))?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ impl PlaylistCore {
|
|||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ajoute un record et applique l'éviction
|
/// Ajoute un record et applique l'éviction
|
||||||
pub fn push(&mut self, record: Record) {
|
pub fn push(&mut self, record: Record) {
|
||||||
self.tracks.push_back(Arc::new(record));
|
self.tracks.push_back(Arc::new(record));
|
||||||
self.evict();
|
self.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ajoute plusieurs records de manière atomique
|
/// Ajoute plusieurs records de manière atomique
|
||||||
pub fn push_all(&mut self, records: Vec<Record>) {
|
pub fn push_all(&mut self, records: Vec<Record>) {
|
||||||
for record in records {
|
for record in records {
|
||||||
@@ -49,14 +49,13 @@ impl PlaylistCore {
|
|||||||
}
|
}
|
||||||
self.evict();
|
self.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nettoie les morceaux expirés et applique la limite de taille
|
/// Nettoie les morceaux expirés et applique la limite de taille
|
||||||
pub fn evict(&mut self) {
|
pub fn evict(&mut self) {
|
||||||
// 1. Supprimer les morceaux périmés par TTL
|
// 1. Supprimer les morceaux périmés par TTL
|
||||||
self.tracks.retain(|record| {
|
self.tracks
|
||||||
!record.is_expired(self.config.default_ttl)
|
.retain(|record| !record.is_expired(self.config.default_ttl));
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Appliquer la limite de taille (FIFO)
|
// 2. Appliquer la limite de taille (FIFO)
|
||||||
if let Some(max) = self.config.max_size {
|
if let Some(max) = self.config.max_size {
|
||||||
while self.tracks.len() > max {
|
while self.tracks.len() > max {
|
||||||
@@ -64,45 +63,45 @@ impl PlaylistCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vide complètement la playlist
|
/// Vide complètement la playlist
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.tracks.clear();
|
self.tracks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nombre de morceaux
|
/// Nombre de morceaux
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.tracks.len()
|
self.tracks.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vérifie si la playlist est vide
|
/// Vérifie si la playlist est vide
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.tracks.is_empty()
|
self.tracks.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère un record par index
|
/// Récupère un record par index
|
||||||
pub fn get(&self, index: usize) -> Option<Arc<Record>> {
|
pub fn get(&self, index: usize) -> Option<Arc<Record>> {
|
||||||
self.tracks.get(index).cloned()
|
self.tracks.get(index).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot de tous les records
|
/// Snapshot de tous les records
|
||||||
pub fn snapshot(&self) -> Vec<Arc<Record>> {
|
pub fn snapshot(&self) -> Vec<Arc<Record>> {
|
||||||
self.tracks.iter().cloned().collect()
|
self.tracks.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime un record par cache_pk (retourne true si supprimé)
|
/// Supprime un record par cache_pk (retourne true si supprimé)
|
||||||
pub fn remove_by_cache_pk(&mut self, cache_pk: &str) -> bool {
|
pub fn remove_by_cache_pk(&mut self, cache_pk: &str) -> bool {
|
||||||
let initial_len = self.tracks.len();
|
let initial_len = self.tracks.len();
|
||||||
self.tracks.retain(|r| r.cache_pk != cache_pk);
|
self.tracks.retain(|r| r.cache_pk != cache_pk);
|
||||||
self.tracks.len() != initial_len
|
self.tracks.len() != initial_len
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Met à jour la capacité maximale
|
/// Met à jour la capacité maximale
|
||||||
pub fn set_capacity(&mut self, max_size: Option<usize>) {
|
pub fn set_capacity(&mut self, max_size: Option<usize>) {
|
||||||
self.config.max_size = max_size;
|
self.config.max_size = max_size;
|
||||||
self.evict();
|
self.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Met à jour le TTL par défaut
|
/// Met à jour le TTL par défaut
|
||||||
pub fn set_default_ttl(&mut self, ttl: Option<Duration>) {
|
pub fn set_default_ttl(&mut self, ttl: Option<Duration>) {
|
||||||
self.config.default_ttl = ttl;
|
self.config.default_ttl = ttl;
|
||||||
|
|||||||
@@ -51,49 +51,50 @@ impl Playlist {
|
|||||||
writer_lock: RwLock::new(None),
|
writer_lock: RwLock::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vérifie si la playlist est active
|
/// Vérifie si la playlist est active
|
||||||
pub fn is_alive(&self) -> bool {
|
pub fn is_alive(&self) -> bool {
|
||||||
PlaylistState::from(self.state.load(Ordering::SeqCst)) == PlaylistState::Active
|
PlaylistState::from(self.state.load(Ordering::SeqCst)) == PlaylistState::Active
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marque la playlist comme supprimée
|
/// Marque la playlist comme supprimée
|
||||||
pub fn mark_deleted(&self) {
|
pub fn mark_deleted(&self) {
|
||||||
self.state.store(PlaylistState::Deleted as u8, Ordering::SeqCst);
|
self.state
|
||||||
|
.store(PlaylistState::Deleted as u8, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Met à jour le timestamp de dernière modification
|
/// Met à jour le timestamp de dernière modification
|
||||||
pub async fn touch(&self) {
|
pub async fn touch(&self) {
|
||||||
*self.last_change.write().await = SystemTime::now();
|
*self.last_change.write().await = SystemTime::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère le titre
|
/// Récupère le titre
|
||||||
pub async fn title(&self) -> String {
|
pub async fn title(&self) -> String {
|
||||||
self.title.read().await.clone()
|
self.title.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change le titre
|
/// Change le titre
|
||||||
pub async fn set_title(&self, title: String) {
|
pub async fn set_title(&self, title: String) {
|
||||||
*self.title.write().await = title;
|
*self.title.write().await = title;
|
||||||
self.touch().await;
|
self.touch().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timestamp du dernier changement
|
/// Timestamp du dernier changement
|
||||||
pub async fn last_change(&self) -> SystemTime {
|
pub async fn last_change(&self) -> SystemTime {
|
||||||
*self.last_change.read().await
|
*self.last_change.read().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tente d'acquérir le write lock
|
/// Tente d'acquérir le write lock
|
||||||
pub async fn acquire_write_lock(&self) -> Result<Arc<()>, ()> {
|
pub async fn acquire_write_lock(&self) -> Result<Arc<()>, ()> {
|
||||||
let mut guard = self.writer_lock.write().await;
|
let mut guard = self.writer_lock.write().await;
|
||||||
|
|
||||||
// Vérifier si un writer existe déjà
|
// Vérifier si un writer existe déjà
|
||||||
if let Some(weak) = guard.as_ref() {
|
if let Some(weak) = guard.as_ref() {
|
||||||
if weak.strong_count() > 0 {
|
if weak.strong_count() > 0 {
|
||||||
return Err(()); // Lock déjà pris
|
return Err(()); // Lock déjà pris
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer un nouveau token
|
// Créer un nouveau token
|
||||||
let token = Arc::new(());
|
let token = Arc::new(());
|
||||||
*guard = Some(Arc::downgrade(&token));
|
*guard = Some(Arc::downgrade(&token));
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ use std::time::{Duration, SystemTime};
|
|||||||
pub struct Record {
|
pub struct Record {
|
||||||
/// Clé primaire dans pmoaudiocache
|
/// Clé primaire dans pmoaudiocache
|
||||||
pub cache_pk: String,
|
pub cache_pk: String,
|
||||||
|
|
||||||
/// Timestamp d'ajout à la playlist (en nanosecondes depuis epoch)
|
/// Timestamp d'ajout à la playlist (en nanosecondes depuis epoch)
|
||||||
pub added_at: SystemTime,
|
pub added_at: SystemTime,
|
||||||
|
|
||||||
/// Durée de vie optionnelle (surcharge le TTL par défaut)
|
/// Durée de vie optionnelle (surcharge le TTL par défaut)
|
||||||
pub ttl: Option<Duration>,
|
pub ttl: Option<Duration>,
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ impl Record {
|
|||||||
ttl: None,
|
ttl: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crée un record avec un TTL personnalisé
|
/// Crée un record avec un TTL personnalisé
|
||||||
pub fn with_ttl(cache_pk: String, ttl: Duration) -> Self {
|
pub fn with_ttl(cache_pk: String, ttl: Duration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -36,12 +36,12 @@ impl Record {
|
|||||||
ttl: Some(ttl),
|
ttl: Some(ttl),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vérifie si le record est expiré
|
/// Vérifie si le record est expiré
|
||||||
pub fn is_expired(&self, default_ttl: Option<Duration>) -> bool {
|
pub fn is_expired(&self, default_ttl: Option<Duration>) -> bool {
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
let age = now.duration_since(self.added_at).unwrap_or_default();
|
let age = now.duration_since(self.added_at).unwrap_or_default();
|
||||||
|
|
||||||
if let Some(ttl) = self.ttl {
|
if let Some(ttl) = self.ttl {
|
||||||
age >= ttl
|
age >= ttl
|
||||||
} else if let Some(default_ttl) = default_ttl {
|
} else if let Some(default_ttl) = default_ttl {
|
||||||
@@ -50,7 +50,7 @@ impl Record {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne le timestamp en nanosecondes depuis epoch
|
/// Retourne le timestamp en nanosecondes depuis epoch
|
||||||
pub fn added_at_nanos(&self) -> i64 {
|
pub fn added_at_nanos(&self) -> i64 {
|
||||||
self.added_at
|
self.added_at
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! PlaylistTrack : résultat d'un pop() avec helpers pour accéder au cache
|
//! PlaylistTrack : résultat d'un pop() avec helpers pour accéder au cache
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use pmocache::cache_trait::FileCache;
|
|
||||||
use pmoaudiocache::AudioMetadataExt;
|
use pmoaudiocache::AudioMetadataExt;
|
||||||
|
use pmocache::cache_trait::FileCache;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Un morceau récupéré depuis une playlist
|
/// Un morceau récupéré depuis une playlist
|
||||||
|
|||||||
@@ -149,8 +149,7 @@ impl UpnpObject for DeviceInstance {
|
|||||||
|
|
||||||
// UDN
|
// UDN
|
||||||
let mut udn = Element::new("UDN");
|
let mut udn = Element::new("UDN");
|
||||||
udn.children
|
udn.children.push(XMLNode::Text(self.udn_with_prefix()));
|
||||||
.push(XMLNode::Text(self.udn_with_prefix()));
|
|
||||||
elem.children.push(XMLNode::Element(udn));
|
elem.children.push(XMLNode::Element(udn));
|
||||||
|
|
||||||
// serviceList
|
// serviceList
|
||||||
|
|||||||
Reference in New Issue
Block a user