Amélioration des pmoplaylist

This commit is contained in:
2025-12-17 08:31:47 +01:00
parent d6920703a1
commit ca36a102e5
11 changed files with 201 additions and 33 deletions

View File

@@ -67,9 +67,10 @@ impl ReadHandle {
if self.playlist.persistent {
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
let title = self.playlist.title().await;
let role = self.playlist.role().await;
let core = self.playlist.core.read().await;
let _ = persistence
.save_playlist(&self.playlist.id, &title, &core.config, &core.tracks)
.save_playlist(&self.playlist.id, &title, &role, &core.config, &core.tracks)
.await;
}
}

View File

@@ -1,7 +1,7 @@
//! WriteHandle : accès exclusif en écriture à une playlist
use crate::playlist::record::Record;
use crate::playlist::Playlist;
use crate::playlist::{Playlist, PlaylistRole};
use crate::Result;
use pmocache::cache_trait::FileCache;
use std::sync::Arc;
@@ -196,6 +196,23 @@ impl WriteHandle {
Ok(())
}
/// Modifie le rôle logique de la playlist
pub async fn set_role(&self, role: PlaylistRole) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
self.playlist.set_role(role).await;
if self.playlist.persistent {
self.save_to_db().await?;
}
crate::manager::PlaylistManager().notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Change la capacité maximale
pub async fn set_capacity(&self, max_size: Option<usize>) -> Result<()> {
if !self.playlist.is_alive() {
@@ -266,6 +283,7 @@ impl WriteHandle {
// Récupérer les données actuelles
let title = self.playlist.title().await;
let role = self.playlist.role().await;
let core = self.playlist.core.read().await;
let config = core.config.clone();
let tracks = core.snapshot();
@@ -273,7 +291,9 @@ impl WriteHandle {
// Créer la nouvelle playlist persistante
let manager = crate::manager::PlaylistManager();
let new_handle = manager.create_persistent_playlist(new_id).await?;
let new_handle = manager
.create_persistent_playlist_with_role(new_id, role)
.await?;
// Copier le titre et la config
new_handle.set_title(title).await?;
@@ -297,6 +317,10 @@ impl WriteHandle {
self.playlist.title().await
}
pub async fn role(&self) -> PlaylistRole {
self.playlist.role().await
}
pub fn is_persistent(&self) -> bool {
self.playlist.persistent
}
@@ -474,12 +498,13 @@ impl WriteHandle {
.ok_or_else(|| crate::Error::PersistenceError("No persistence manager".into()))?;
let title = self.playlist.title().await;
let role = self.playlist.role().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)
.save_playlist(&self.playlist.id, &title, &role, config, tracks)
.await
}
}

View File

@@ -63,6 +63,7 @@ pub use error::{Error, Result};
pub use handle::{ReadHandle, WriteHandle};
pub use manager::{register_audio_cache, PlaylistManager, PlaylistManager as Manager};
pub use manager::{subscribe_events, PlaylistEvent, PlaylistEventEnvelope, PlaylistEventKind};
pub use playlist::PlaylistRole;
#[cfg(feature = "pmoserver")]
pub use sse::playlist_events_router;
pub use track::PlaylistTrack;

View File

@@ -3,7 +3,7 @@
use crate::handle::{ReadHandle, WriteHandle};
use crate::persistence::PersistenceManager;
use crate::playlist::core::PlaylistConfig;
use crate::playlist::Playlist;
use crate::playlist::{Playlist, PlaylistRole};
use crate::Result;
use once_cell::sync::OnceCell;
use pmocache::{CacheBroadcastEvent, CacheEvent, CacheSubscription};
@@ -138,8 +138,12 @@ impl PlaylistManager {
}
}
/// Cr<43>e une playlist persistante (erreur si existe d<>j<EFBFBD>)
pub async fn create_persistent_playlist(&self, id: String) -> Result<WriteHandle> {
/// Cr<43>e une playlist persistante (erreur si existe d<>j<EFBFBD>) avec rôle personnalisé
pub async fn create_persistent_playlist_with_role(
&self,
id: String,
role: PlaylistRole,
) -> Result<WriteHandle> {
let mut playlists = self.inner.playlists.write().await;
if playlists.contains_key(&id) {
@@ -148,9 +152,10 @@ impl PlaylistManager {
let playlist = Arc::new(Playlist::new(
id.clone(),
id.clone(), // Titre = id par d<EFBFBD>faut
id.clone(), // Titre = id par défaut
PlaylistConfig::default(),
true, // persistent
role,
));
// Acqu<71>rir le write lock
@@ -165,15 +170,22 @@ impl PlaylistManager {
// Sauvegarder la structure vide
if let Some(persistence) = &self.inner.persistence {
let title = playlist.title().await;
let role = playlist.role().await;
let core = playlist.core.read().await;
persistence
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
.save_playlist(&playlist.id, &title, &role, &core.config, &core.tracks)
.await?;
}
Ok(WriteHandle::new(playlist, write_token))
}
/// Cr<43>e une playlist persistante avec rôle par défaut (user)
pub async fn create_persistent_playlist(&self, id: String) -> Result<WriteHandle> {
self.create_persistent_playlist_with_role(id, PlaylistRole::User)
.await
}
/// Enregistre un callback d'évènement playlist (update, track joué).
///
/// Retourne un jeton (u64) pour désenregistrer plus tard.
@@ -394,7 +406,8 @@ impl PlaylistManager {
id.clone(),
id.clone(),
PlaylistConfig::default(),
false, // <EFBFBD>ph<EFBFBD>m<EFBFBD>re
false, // éphémère
PlaylistRole::User,
));
let write_token = playlist
@@ -430,11 +443,12 @@ impl PlaylistManager {
// Pas en mémoire, essayer de charger depuis la DB
if let Some(persistence) = &self.inner.persistence {
if let Some((title, config, tracks)) = persistence.load_playlist(&id).await? {
if let Some((title, role, config, tracks)) = persistence.load_playlist(&id).await? {
// Reconstruire la playlist
let mut playlists = self.inner.playlists.write().await;
let playlist = Arc::new(Playlist::new(id.clone(), title.clone(), config, true));
let playlist =
Arc::new(Playlist::new(id.clone(), title.clone(), config, true, role));
// Restaurer les tracks
{
@@ -477,11 +491,12 @@ impl PlaylistManager {
// Pas en m<>moire, essayer de ressusciter depuis la DB
if let Some(persistence) = &self.inner.persistence {
if let Some((title, config, tracks)) = persistence.load_playlist(id).await? {
if let Some((title, role, config, tracks)) = persistence.load_playlist(id).await? {
// 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, role));
// Restaurer les tracks
{
@@ -767,13 +782,14 @@ impl PlaylistManager {
let new_len = core.len();
drop(core);
// Si des morceaux ont <EFBFBD>t<EFBFBD> <20>vict<EFBFBD>s et la playlist est persistante
// Si des morceaux ont été évictés et la playlist est persistante
if new_len < initial_len && playlist.persistent {
if let Some(persistence) = &self.inner.persistence {
let title = playlist.title().await;
let role = playlist.role().await;
let core = playlist.core.read().await;
let _ = persistence
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
.save_playlist(&playlist.id, &title, &role, &core.config, &core.tracks)
.await;
}
}

View File

@@ -2,12 +2,14 @@
use crate::playlist::core::PlaylistConfig;
use crate::playlist::record::Record;
use crate::playlist::PlaylistRole;
use crate::Result;
use rusqlite::{params, Connection};
use std::collections::VecDeque;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::str::FromStr;
/// Gestionnaire de persistance (une base pour toutes les playlists)
pub struct PersistenceManager {
@@ -33,6 +35,7 @@ impl PersistenceManager {
"CREATE TABLE IF NOT EXISTS playlists (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
role TEXT NOT NULL,
max_size INTEGER,
default_ttl_secs INTEGER,
created_at INTEGER NOT NULL,
@@ -80,6 +83,7 @@ impl PersistenceManager {
&self,
id: &str,
title: &str,
role: &PlaylistRole,
config: &PlaylistConfig,
tracks: &VecDeque<Arc<Record>>,
) -> Result<()> {
@@ -92,13 +96,14 @@ impl PersistenceManager {
// Upsert playlist metadata
conn.execute(
"INSERT OR REPLACE INTO playlists (id, title, max_size, default_ttl_secs, created_at, last_modified)
VALUES (?1, ?2, ?3, ?4,
COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?5),
?5)",
"INSERT OR REPLACE INTO playlists (id, title, role, max_size, default_ttl_secs, created_at, last_modified)
VALUES (?1, ?2, ?3, ?4, ?5,
COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?6),
?6)",
params![
id,
title,
role.as_str(),
config.max_size.map(|s| s as i64),
config.default_ttl.map(|d| d.as_secs() as i64),
now_nanos,
@@ -135,23 +140,28 @@ impl PersistenceManager {
pub async fn load_playlist(
&self,
id: &str,
) -> Result<Option<(String, PlaylistConfig, VecDeque<Arc<Record>>)>> {
) -> Result<Option<(String, PlaylistRole, 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")
.prepare(
"SELECT title, role, 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)?;
let role_raw: String = row.get(1)?;
let max_size: Option<i64> = row.get(2)?;
let default_ttl_secs: Option<i64> = row.get(3)?;
Ok((
title,
PlaylistRole::from_str(&role_raw)
.unwrap_or_else(|_| PlaylistRole::custom(role_raw)),
PlaylistConfig {
max_size: max_size.map(|s| s as usize),
default_ttl: default_ttl_secs.map(|s| Duration::from_secs(s as u64)),
@@ -159,7 +169,7 @@ impl PersistenceManager {
))
});
let (title, config) = match result {
let (title, role, config) = match result {
Ok(data) => data,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => {
@@ -202,7 +212,7 @@ impl PersistenceManager {
tracks.push_back(Arc::new(record));
}
Ok(Some((title, config, tracks)))
Ok(Some((title, role, config, tracks)))
}
/// Supprime une playlist

View File

@@ -4,6 +4,9 @@ pub mod core;
pub mod record;
use self::core::{PlaylistConfig, PlaylistCore};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Weak};
use std::time::SystemTime;
@@ -30,6 +33,7 @@ impl From<u8> for PlaylistState {
pub struct Playlist {
pub id: String,
title: RwLock<String>,
role: RwLock<PlaylistRole>,
state: Arc<AtomicU8>,
pub core: Arc<RwLock<PlaylistCore>>,
pub persistent: bool,
@@ -39,10 +43,17 @@ pub struct Playlist {
impl Playlist {
/// Crée une nouvelle playlist
pub fn new(id: String, title: String, config: PlaylistConfig, persistent: bool) -> Self {
pub fn new(
id: String,
title: String,
config: PlaylistConfig,
persistent: bool,
role: PlaylistRole,
) -> Self {
Self {
id,
title: RwLock::new(title),
role: RwLock::new(role),
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
core: Arc::new(RwLock::new(PlaylistCore::new(config))),
persistent,
@@ -78,6 +89,17 @@ impl Playlist {
self.touch().await;
}
/// Retourne le rôle
pub async fn role(&self) -> PlaylistRole {
self.role.read().await.clone()
}
/// Change le rôle
pub async fn set_role(&self, role: PlaylistRole) {
*self.role.write().await = role;
self.touch().await;
}
/// Timestamp du dernier changement
pub async fn last_change(&self) -> SystemTime {
*self.last_change.read().await
@@ -100,3 +122,90 @@ impl Playlist {
Ok(token)
}
}
/// Rôle logique d'une playlist
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlaylistRole {
User,
Album,
Radio,
Source,
Custom(String),
}
impl PlaylistRole {
pub const fn user() -> Self {
PlaylistRole::User
}
pub const fn album() -> Self {
PlaylistRole::Album
}
pub const fn radio() -> Self {
PlaylistRole::Radio
}
pub const fn source() -> Self {
PlaylistRole::Source
}
pub fn custom<S: Into<String>>(value: S) -> Self {
PlaylistRole::Custom(value.into())
}
pub fn as_str(&self) -> &str {
match self {
PlaylistRole::User => "user",
PlaylistRole::Album => "album",
PlaylistRole::Radio => "radio",
PlaylistRole::Source => "source",
PlaylistRole::Custom(value) => value.as_str(),
}
}
}
impl Default for PlaylistRole {
fn default() -> Self {
PlaylistRole::User
}
}
impl fmt::Display for PlaylistRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for PlaylistRole {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for PlaylistRole {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(PlaylistRole::from_str(&value).unwrap_or_else(|_| PlaylistRole::Custom(value)))
}
}
impl FromStr for PlaylistRole {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"user" => Ok(PlaylistRole::User),
"album" => Ok(PlaylistRole::Album),
"radio" => Ok(PlaylistRole::Radio),
"source" => Ok(PlaylistRole::Source),
other => Ok(PlaylistRole::Custom(other.to_string())),
}
}
}