la crate des playlists

This commit is contained in:
2025-10-27 12:32:31 +01:00
parent a0e5d92bf9
commit 92c35116f8
23 changed files with 1943 additions and 837 deletions

View File

@@ -0,0 +1,111 @@
//! PlaylistCore : structure FIFO avec éviction automatique
use super::record::Record;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
/// Configuration d'une playlist
#[derive(Debug, Clone)]
pub struct PlaylistConfig {
pub max_size: Option<usize>,
pub default_ttl: Option<Duration>,
}
impl Default for PlaylistConfig {
fn default() -> Self {
Self {
max_size: None,
default_ttl: None,
}
}
}
/// Noyau de la playlist (structure interne protégée par RwLock)
pub struct PlaylistCore {
pub tracks: VecDeque<Arc<Record>>,
pub config: PlaylistConfig,
}
impl PlaylistCore {
/// Crée un nouveau core
pub fn new(config: PlaylistConfig) -> Self {
Self {
tracks: VecDeque::new(),
config,
}
}
/// Ajoute un record et applique l'éviction
pub fn push(&mut self, record: Record) {
self.tracks.push_back(Arc::new(record));
self.evict();
}
/// Ajoute plusieurs records de manière atomique
pub fn push_all(&mut self, records: Vec<Record>) {
for record in records {
self.tracks.push_back(Arc::new(record));
}
self.evict();
}
/// Nettoie les morceaux expirés et applique la limite de taille
pub fn evict(&mut self) {
// 1. Supprimer les morceaux périmés par TTL
self.tracks.retain(|record| {
!record.is_expired(self.config.default_ttl)
});
// 2. Appliquer la limite de taille (FIFO)
if let Some(max) = self.config.max_size {
while self.tracks.len() > max {
self.tracks.pop_front();
}
}
}
/// Vide complètement la playlist
pub fn clear(&mut self) {
self.tracks.clear();
}
/// Nombre de morceaux
pub fn len(&self) -> usize {
self.tracks.len()
}
/// Vérifie si la playlist est vide
pub fn is_empty(&self) -> bool {
self.tracks.is_empty()
}
/// Récupère un record par index
pub fn get(&self, index: usize) -> Option<Arc<Record>> {
self.tracks.get(index).cloned()
}
/// Snapshot de tous les records
pub fn snapshot(&self) -> Vec<Arc<Record>> {
self.tracks.iter().cloned().collect()
}
/// Supprime un record par cache_pk (retourne true si supprimé)
pub fn remove_by_cache_pk(&mut self, cache_pk: &str) -> bool {
let initial_len = self.tracks.len();
self.tracks.retain(|r| r.cache_pk != cache_pk);
self.tracks.len() != initial_len
}
/// Met à jour la capacité maximale
pub fn set_capacity(&mut self, max_size: Option<usize>) {
self.config.max_size = max_size;
self.evict();
}
/// Met à jour le TTL par défaut
pub fn set_default_ttl(&mut self, ttl: Option<Duration>) {
self.config.default_ttl = ttl;
self.evict();
}
}

View File

@@ -0,0 +1,102 @@
//! Playlist interne (non exposée publiquement)
pub mod core;
pub mod record;
use self::core::{PlaylistConfig, PlaylistCore};
use self::record::Record;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Weak};
use std::time::SystemTime;
use tokio::sync::RwLock;
/// État d'une playlist
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum PlaylistState {
Active = 0,
Deleted = 1,
}
impl From<u8> for PlaylistState {
fn from(value: u8) -> Self {
match value {
1 => PlaylistState::Deleted,
_ => PlaylistState::Active,
}
}
}
/// Playlist interne (gérée par le PlaylistManager)
pub struct Playlist {
pub id: String,
title: RwLock<String>,
state: Arc<AtomicU8>,
pub core: Arc<RwLock<PlaylistCore>>,
pub persistent: bool,
last_change: RwLock<SystemTime>,
writer_lock: RwLock<Option<Weak<()>>>,
}
impl Playlist {
/// Crée une nouvelle playlist
pub fn new(id: String, title: String, config: PlaylistConfig, persistent: bool) -> Self {
Self {
id,
title: RwLock::new(title),
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
core: Arc::new(RwLock::new(PlaylistCore::new(config))),
persistent,
last_change: RwLock::new(SystemTime::now()),
writer_lock: RwLock::new(None),
}
}
/// Vérifie si la playlist est active
pub fn is_alive(&self) -> bool {
PlaylistState::from(self.state.load(Ordering::SeqCst)) == PlaylistState::Active
}
/// Marque la playlist comme supprimée
pub fn mark_deleted(&self) {
self.state.store(PlaylistState::Deleted as u8, Ordering::SeqCst);
}
/// Met à jour le timestamp de dernière modification
pub async fn touch(&self) {
*self.last_change.write().await = SystemTime::now();
}
/// Récupère le titre
pub async fn title(&self) -> String {
self.title.read().await.clone()
}
/// Change le titre
pub async fn set_title(&self, title: String) {
*self.title.write().await = title;
self.touch().await;
}
/// Timestamp du dernier changement
pub async fn last_change(&self) -> SystemTime {
*self.last_change.read().await
}
/// Tente d'acquérir le write lock
pub async fn acquire_write_lock(&self) -> Result<Arc<()>, ()> {
let mut guard = self.writer_lock.write().await;
// Vérifier si un writer existe déjà
if let Some(weak) = guard.as_ref() {
if weak.strong_count() > 0 {
return Err(()); // Lock déjà pris
}
}
// Créer un nouveau token
let token = Arc::new(());
*guard = Some(Arc::downgrade(&token));
Ok(token)
}
}

View File

@@ -0,0 +1,61 @@
//! Record : entrée dans la playlist pointant vers le cache audio
use std::time::{Duration, SystemTime};
/// Un enregistrement dans la playlist
///
/// Contient uniquement une référence (pk) vers une entrée dans pmoaudiocache
/// et des informations de gestion (timestamp, TTL).
#[derive(Debug, Clone)]
pub struct Record {
/// Clé primaire dans pmoaudiocache
pub cache_pk: String,
/// Timestamp d'ajout à la playlist (en nanosecondes depuis epoch)
pub added_at: SystemTime,
/// Durée de vie optionnelle (surcharge le TTL par défaut)
pub ttl: Option<Duration>,
}
impl Record {
/// Crée un nouveau record
pub fn new(cache_pk: String) -> Self {
Self {
cache_pk,
added_at: SystemTime::now(),
ttl: None,
}
}
/// Crée un record avec un TTL personnalisé
pub fn with_ttl(cache_pk: String, ttl: Duration) -> Self {
Self {
cache_pk,
added_at: SystemTime::now(),
ttl: Some(ttl),
}
}
/// Vérifie si le record est expiré
pub fn is_expired(&self, default_ttl: Option<Duration>) -> bool {
let now = SystemTime::now();
let age = now.duration_since(self.added_at).unwrap_or_default();
if let Some(ttl) = self.ttl {
age >= ttl
} else if let Some(default_ttl) = default_ttl {
age >= default_ttl
} else {
false
}
}
/// Retourne le timestamp en nanosecondes depuis epoch
pub fn added_at_nanos(&self) -> i64 {
self.added_at
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i64
}
}