Refactoring de pmoflac -factorisation de code ogg et opus

This commit is contained in:
2025-10-27 22:06:24 +01:00
parent e82979561a
commit 5b60fdbe1d
24 changed files with 751 additions and 793 deletions

View File

@@ -5,31 +5,31 @@
pub enum Error {
#[error("Playlist not found: {0}")]
PlaylistNotFound(String),
#[error("Playlist deleted: {0}")]
PlaylistDeleted(String),
#[error("Playlist already exists: {0}")]
PlaylistAlreadyExists(String),
#[error("Playlist is not persistent: {0}")]
PlaylistNotPersistent(String),
#[error("Write lock already held for playlist: {0}")]
WriteLockHeld(String),
#[error("Cache entry not found: {0}")]
CacheEntryNotFound(String),
#[error("Cache error: {0}")]
CacheError(String),
#[error("Persistence error: {0}")]
PersistenceError(String),
#[error("PlaylistManager not initialized")]
ManagerNotInitialized,
#[error(transparent)]
Other(#[from] anyhow::Error),
}

View File

@@ -22,7 +22,7 @@ impl ReadHandle {
cursor: AtomicUsize::new(0),
}
}
/// Pop le prochain morceau (avance le curseur)
///
/// Skip automatiquement les entrées invalides dans le cache.
@@ -31,24 +31,24 @@ impl ReadHandle {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let pos = self.cursor.load(Ordering::SeqCst);
let core = self.playlist.core.read().await;
// Fin de playlist ?
if pos >= core.len() {
return Ok(None);
}
let record = match core.get(pos) {
Some(r) => r,
None => return Ok(None),
};
let cache_pk = record.cache_pk.clone();
drop(core);
// Vérifier validité dans le cache
let cache = crate::manager::audio_cache()?;
if cache.is_valid_pk(&cache_pk) {
@@ -61,31 +61,33 @@ impl ReadHandle {
let mut core = self.playlist.core.write().await;
core.remove_by_cache_pk(&cache_pk);
drop(core);
// Sauvegarder si persistante
if self.playlist.persistent {
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
let title = self.playlist.title().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
continue;
}
}
}
/// Peek le prochain morceau sans avancer le curseur
pub async fn peek(&self) -> Result<Option<PlaylistTrack>> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let pos = self.cursor.load(Ordering::SeqCst);
let core = self.playlist.core.read().await;
match core.get(pos) {
Some(record) => {
// Vérifier validité
@@ -99,28 +101,28 @@ impl ReadHandle {
None => Ok(None),
}
}
/// Position actuelle du curseur
pub fn position(&self) -> usize {
self.cursor.load(Ordering::SeqCst)
}
/// Nombre de morceaux restants (compte uniquement les valides)
pub async fn remaining(&self) -> Result<usize> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let pos = self.cursor.load(Ordering::SeqCst);
let core = self.playlist.core.read().await;
if pos >= core.len() {
return Ok(0);
}
let cache = crate::manager::audio_cache()?;
let mut count = 0;
for i in pos..core.len() {
if let Some(record) = core.get(i) {
if cache.is_valid_pk(&record.cache_pk) {
@@ -128,38 +130,38 @@ impl ReadHandle {
}
}
}
Ok(count)
}
/// Crée un nouveau handle avec cursor à 0
pub fn get_new_handle(&self) -> Result<ReadHandle> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
Ok(ReadHandle::new(self.playlist.clone()))
}
/// Vérifie si la playlist est vivante
pub fn is_alive(&self) -> bool {
self.playlist.is_alive()
}
/// ID de la playlist
pub fn id(&self) -> &str {
&self.playlist.id
}
/// Génère un Container DIDL-Lite
pub async fn to_container(&self) -> Result<Container> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let title = self.playlist.title().await;
let remaining = self.remaining().await?;
Ok(Container {
id: self.playlist.id.clone(),
parent_id: "0".to_string(),
@@ -172,48 +174,48 @@ impl ReadHandle {
items: vec![],
})
}
/// Génère des Items DIDL-Lite depuis la position actuelle
pub async fn to_items(&self, limit: usize) -> Result<Vec<Item>> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let pos = self.cursor.load(Ordering::SeqCst);
let core = self.playlist.core.read().await;
let cache = crate::manager::audio_cache()?;
// Récupérer base_url depuis le cache (via route_for)
let mut items = Vec::new();
let mut idx = 0;
for i in pos..core.len() {
if items.len() >= limit {
break;
}
let record = match core.get(i) {
Some(r) => r,
None => continue,
};
// Vérifier validité
if !cache.is_valid_pk(&record.cache_pk) {
continue;
}
// Charger métadonnées
let metadata = match pmoaudiocache::get_metadata(&*cache, &record.cache_pk) {
Ok(m) => m,
Err(_) => continue,
};
// Construire l'URL via route_for
let url = cache.route_for(&record.cache_pk, None);
// Créer le Resource DIDL
let resource = metadata.to_didl_resource(url);
// Créer l'Item
let item = Item {
id: format!("{}:{}", self.playlist.id, pos + idx),
@@ -232,11 +234,11 @@ impl ReadHandle {
resources: vec![resource],
descriptions: vec![],
};
items.push(item);
idx += 1;
}
Ok(items)
}
}

View File

@@ -22,41 +22,41 @@ impl WriteHandle {
_write_token: write_token,
}
}
/// Ajoute un morceau à la playlist
pub async fn push(&self, cache_pk: String) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
// Vérifier que le pk existe dans le cache
let cache = crate::manager::audio_cache()?;
if !cache.is_valid_pk(&cache_pk) {
return Err(crate::Error::CacheEntryNotFound(cache_pk));
}
// Ajouter à la playlist
let record = Record::new(cache_pk);
let mut core = self.playlist.core.write().await;
core.push(record);
drop(core);
self.playlist.touch().await;
// Sauvegarder si persistante
if self.playlist.persistent {
self.save_to_db().await?;
}
Ok(())
}
/// Ajoute plusieurs morceaux de manière atomique
pub async fn push_set(&self, cache_pks: Vec<String>) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
// Vérifier tous les pks d'abord
let cache = crate::manager::audio_cache()?;
for pk in &cache_pks {
@@ -64,195 +64,194 @@ impl WriteHandle {
return Err(crate::Error::CacheEntryNotFound(pk.clone()));
}
}
// Créer tous les records
let records: Vec<Record> = cache_pks.into_iter()
.map(Record::new)
.collect();
let records: Vec<Record> = cache_pks.into_iter().map(Record::new).collect();
// Ajouter atomiquement
let mut core = self.playlist.core.write().await;
core.push_all(records);
drop(core);
self.playlist.touch().await;
// Une seule sauvegarde pour tout le batch
if self.playlist.persistent {
self.save_to_db().await?;
}
Ok(())
}
/// Vide la playlist
pub async fn flush(&self) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let mut core = self.playlist.core.write().await;
core.clear();
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
Ok(())
}
/// Supprime la playlist définitivement
pub async fn delete(self) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
// Marquer comme supprimée
self.playlist.mark_deleted();
// Supprimer du manager
crate::manager::delete_playlist_internal(&self.playlist.id).await?;
Ok(())
}
/// Change le titre
pub async fn set_title(&self, title: String) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
self.playlist.set_title(title).await;
if self.playlist.persistent {
self.save_to_db().await?;
}
Ok(())
}
/// Change la capacité maximale
pub async fn set_capacity(&self, max_size: Option<usize>) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let mut core = self.playlist.core.write().await;
core.set_capacity(max_size);
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
Ok(())
}
/// Change le TTL par défaut
pub async fn set_default_ttl(&self, ttl: Option<Duration>) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let mut core = self.playlist.core.write().await;
core.set_default_ttl(ttl);
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
Ok(())
}
/// Clone vers une nouvelle playlist persistante
pub async fn clone_as_persistent(&self, new_id: String) -> Result<WriteHandle> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
// Récupérer les données actuelles
let title = self.playlist.title().await;
let core = self.playlist.core.read().await;
let config = core.config.clone();
let tracks = core.snapshot();
drop(core);
// Créer la nouvelle playlist persistante
let manager = crate::manager::PlaylistManager();
let mut new_handle = manager.create_persistent_playlist(new_id).await?;
// Copier le titre et la config
new_handle.set_title(title).await?;
new_handle.set_capacity(config.max_size).await?;
new_handle.set_default_ttl(config.default_ttl).await?;
// Copier tous les morceaux
let pks: Vec<String> = tracks.iter()
.map(|r| r.cache_pk.clone())
.collect();
let pks: Vec<String> = tracks.iter().map(|r| r.cache_pk.clone()).collect();
new_handle.push_set(pks).await?;
Ok(new_handle)
}
// Métadonnées
pub fn id(&self) -> &str {
&self.playlist.id
}
pub async fn title(&self) -> String {
self.playlist.title().await
}
pub fn is_persistent(&self) -> bool {
self.playlist.persistent
}
pub async fn capacity(&self) -> Option<usize> {
let core = self.playlist.core.read().await;
core.config.max_size
}
pub async fn default_ttl(&self) -> Option<Duration> {
let core = self.playlist.core.read().await;
core.config.default_ttl
}
pub async fn len(&self) -> usize {
let core = self.playlist.core.read().await;
core.len()
}
pub async fn is_empty(&self) -> bool {
let core = self.playlist.core.read().await;
core.is_empty()
}
pub async fn last_change(&self) -> SystemTime {
self.playlist.last_change().await
}
// Helpers internes
async fn save_to_db(&self) -> Result<()> {
let manager = crate::manager::PlaylistManager();
let persistence = manager.persistence()
let persistence = manager
.persistence()
.ok_or_else(|| crate::Error::PersistenceError("No persistence manager".into()))?;
let title = self.playlist.title().await;
let core = self.playlist.core.read().await;
let config = &core.config;
let tracks = &core.tracks;
persistence.save_playlist(&self.playlist.id, &title, config, tracks).await
persistence
.save_playlist(&self.playlist.id, &title, config, tracks)
.await
}
}

View File

@@ -70,7 +70,9 @@ impl PlaylistManager {
#[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 {
let title = playlist.title().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?;
}
@@ -185,12 +188,7 @@ impl PlaylistManager {
// Reconstruire la playlist
let mut playlists = self.inner.playlists.write().await;
let playlist = Arc::new(Playlist::new(
id.to_string(),
title.clone(),
config,
true,
));
let playlist = Arc::new(Playlist::new(id.to_string(), title.clone(), config, true));
// Restaurer les tracks
{
@@ -265,7 +263,9 @@ impl PlaylistManager {
if let Some(persistence) = &self.inner.persistence {
let title = playlist.title().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
pub(crate) fn audio_cache() -> Result<Arc<pmoaudiocache::Cache>> {
pmoupnp::get_audio_cache()
.ok_or_else(|| crate::Error::ManagerNotInitialized)
pmoupnp::get_audio_cache().ok_or_else(|| crate::Error::ManagerNotInitialized)
}
/// Fonction raccourcie pour acc<63>der au singleton

View File

@@ -19,13 +19,15 @@ impl PersistenceManager {
pub fn new(db_path: &Path) -> Result<Self> {
// Créer le répertoire parent si nécessaire
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| crate::Error::PersistenceError(format!("Failed to create directory: {}", e)))?;
std::fs::create_dir_all(parent).map_err(|e| {
crate::Error::PersistenceError(format!("Failed to create directory: {}", e))
})?;
}
let conn = Connection::open(db_path)
.map_err(|e| crate::Error::PersistenceError(format!("Failed to open database: {}", e)))?;
let conn = Connection::open(db_path).map_err(|e| {
crate::Error::PersistenceError(format!("Failed to open database: {}", e))
})?;
// Créer les tables
conn.execute(
"CREATE TABLE IF NOT EXISTS playlists (
@@ -37,8 +39,11 @@ impl PersistenceManager {
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(
"CREATE TABLE IF NOT EXISTS tracks (
playlist_id TEXT NOT NULL,
@@ -48,23 +53,28 @@ impl PersistenceManager {
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(
"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(
"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 {
conn: Arc::new(Mutex::new(conn)),
})
}
/// Sauvegarde une playlist complète
pub async fn save_playlist(
&self,
@@ -74,12 +84,12 @@ impl PersistenceManager {
tracks: &VecDeque<Arc<Record>>,
) -> Result<()> {
let conn = self.conn.lock().unwrap();
let now_nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i64;
// Upsert playlist metadata
conn.execute(
"INSERT OR REPLACE INTO playlists (id, title, max_size, default_ttl_secs, created_at, last_modified)
@@ -94,13 +104,13 @@ impl PersistenceManager {
now_nanos,
],
).map_err(|e| crate::Error::PersistenceError(format!("Failed to save playlist: {}", e)))?;
// Supprimer les anciens tracks
conn.execute(
"DELETE FROM tracks WHERE playlist_id = ?1",
params![id],
).map_err(|e| crate::Error::PersistenceError(format!("Failed to delete old tracks: {}", e)))?;
conn.execute("DELETE FROM tracks WHERE playlist_id = ?1", params![id])
.map_err(|e| {
crate::Error::PersistenceError(format!("Failed to delete old tracks: {}", e))
})?;
// Insérer les nouveaux tracks
for record in tracks {
conn.execute(
@@ -112,26 +122,34 @@ impl PersistenceManager {
&record.cache_pk,
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(())
}
/// 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();
// Charger les métadonnées
let mut stmt = conn.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)))?;
let mut stmt = conn
.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))
})?;
let result = stmt.query_row(params![id], |row| {
let title: String = row.get(0)?;
let max_size: Option<i64> = row.get(1)?;
let default_ttl_secs: Option<i64> = row.get(2)?;
Ok((
title,
PlaylistConfig {
@@ -140,76 +158,91 @@ impl PersistenceManager {
},
))
});
let (title, config) = match result {
Ok(data) => data,
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
let mut stmt = conn.prepare(
"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)))?;
let rows = stmt.query_map(params![id], |row| {
let added_at_nanos: i64 = row.get(0)?;
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));
Ok(Record {
cache_pk,
added_at,
ttl,
let rows = stmt
.query_map(params![id], |row| {
let added_at_nanos: i64 = row.get(0)?;
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));
Ok(Record {
cache_pk,
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();
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));
}
Ok(Some((title, config, tracks)))
}
/// Supprime une playlist
pub async fn delete_playlist(&self, id: &str) -> Result<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"DELETE FROM playlists WHERE id = ?1",
params![id],
).map_err(|e| crate::Error::PersistenceError(format!("Failed to delete playlist: {}", e)))?;
conn.execute("DELETE FROM playlists WHERE id = ?1", params![id])
.map_err(|e| {
crate::Error::PersistenceError(format!("Failed to delete playlist: {}", e))
})?;
Ok(())
}
/// Liste toutes les playlists persistantes
pub async fn list_playlist_ids(&self) -> Result<Vec<String>> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare("SELECT id FROM playlists")
.map_err(|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 mut stmt = conn.prepare("SELECT id FROM playlists").map_err(|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 mut ids = Vec::new();
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)
}
/// Supprime tous les tracks contenant un cache_pk donné
pub async fn remove_by_cache_pk(&self, cache_pk: &str) -> Result<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"DELETE FROM tracks WHERE cache_pk = ?1",
params![cache_pk],
).map_err(|e| crate::Error::PersistenceError(format!("Failed to remove tracks: {}", e)))?;
conn.execute("DELETE FROM tracks WHERE cache_pk = ?1", params![cache_pk])
.map_err(|e| {
crate::Error::PersistenceError(format!("Failed to remove tracks: {}", e))
})?;
Ok(())
}
}

View File

@@ -35,13 +35,13 @@ impl PlaylistCore {
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 {
@@ -49,14 +49,13 @@ impl PlaylistCore {
}
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)
});
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 {
@@ -64,45 +63,45 @@ impl PlaylistCore {
}
}
}
/// 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;

View File

@@ -51,49 +51,50 @@ impl Playlist {
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);
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));

View File

@@ -10,10 +10,10 @@ use std::time::{Duration, SystemTime};
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>,
}
@@ -27,7 +27,7 @@ impl Record {
ttl: None,
}
}
/// Crée un record avec un TTL personnalisé
pub fn with_ttl(cache_pk: String, ttl: Duration) -> Self {
Self {
@@ -36,12 +36,12 @@ impl Record {
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 {
@@ -50,7 +50,7 @@ impl Record {
false
}
}
/// Retourne le timestamp en nanosecondes depuis epoch
pub fn added_at_nanos(&self) -> i64 {
self.added_at

View File

@@ -1,8 +1,8 @@
//! PlaylistTrack : résultat d'un pop() avec helpers pour accéder au cache
use crate::Result;
use pmocache::cache_trait::FileCache;
use pmoaudiocache::AudioMetadataExt;
use pmocache::cache_trait::FileCache;
use std::path::PathBuf;
/// Un morceau récupéré depuis une playlist