2025-10-27 12:32:31 +01:00
|
|
|
|
//! PlaylistManager : gestionnaire singleton central de toutes les playlists
|
|
|
|
|
|
|
|
|
|
|
|
use crate::handle::{ReadHandle, WriteHandle};
|
|
|
|
|
|
use crate::persistence::PersistenceManager;
|
|
|
|
|
|
use crate::playlist::core::PlaylistConfig;
|
|
|
|
|
|
use crate::playlist::Playlist;
|
|
|
|
|
|
use crate::Result;
|
|
|
|
|
|
use once_cell::sync::OnceCell;
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
|
|
|
|
|
|
|
/// Singleton PlaylistManager
|
|
|
|
|
|
static PLAYLIST_MANAGER: OnceCell<PlaylistManager> = OnceCell::new();
|
|
|
|
|
|
|
2025-11-05 20:02:01 +00:00
|
|
|
|
/// Registre global du cache audio
|
|
|
|
|
|
static AUDIO_CACHE: OnceCell<Arc<pmoaudiocache::Cache>> = OnceCell::new();
|
|
|
|
|
|
|
2025-10-27 12:32:31 +01:00
|
|
|
|
/// Structure interne du manager
|
|
|
|
|
|
struct ManagerInner {
|
|
|
|
|
|
playlists: RwLock<HashMap<String, Arc<Playlist>>>,
|
|
|
|
|
|
persistence: Option<Arc<PersistenceManager>>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Gestionnaire central de playlists
|
|
|
|
|
|
pub struct PlaylistManager {
|
|
|
|
|
|
inner: Arc<ManagerInner>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl PlaylistManager {
|
|
|
|
|
|
/// Initialise le gestionnaire (<28> appeler une seule fois au d<>marrage)
|
|
|
|
|
|
fn init(db_path: PathBuf) -> Result<Self> {
|
|
|
|
|
|
// Initialiser la persistance
|
|
|
|
|
|
let persistence = Arc::new(PersistenceManager::new(&db_path)?);
|
|
|
|
|
|
|
|
|
|
|
|
let manager = Self {
|
|
|
|
|
|
inner: Arc::new(ManagerInner {
|
|
|
|
|
|
playlists: RwLock::new(HashMap::new()),
|
|
|
|
|
|
persistence: Some(persistence.clone()),
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Lancer la task d'<27>viction en background
|
|
|
|
|
|
let manager_clone = manager.clone();
|
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
|
manager_clone.eviction_task().await;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Ok(manager)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Initialise avec la configuration de pmoconfig
|
|
|
|
|
|
#[cfg(feature = "pmoconfig")]
|
|
|
|
|
|
fn init_with_config() -> Result<Self> {
|
|
|
|
|
|
use crate::config_ext::PlaylistConfigExt;
|
|
|
|
|
|
|
|
|
|
|
|
let config = pmoconfig::get_config();
|
|
|
|
|
|
let db_path = config.playlist_db_path();
|
|
|
|
|
|
|
|
|
|
|
|
Self::init(db_path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Retourne le singleton
|
|
|
|
|
|
pub fn get() -> &'static PlaylistManager {
|
|
|
|
|
|
#[cfg(feature = "pmoconfig")]
|
|
|
|
|
|
{
|
|
|
|
|
|
PLAYLIST_MANAGER.get_or_init(|| {
|
|
|
|
|
|
Self::init_with_config().expect("Failed to initialize PlaylistManager")
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "pmoconfig"))]
|
|
|
|
|
|
{
|
2025-10-27 22:06:24 +01:00
|
|
|
|
PLAYLIST_MANAGER
|
|
|
|
|
|
.get()
|
|
|
|
|
|
.expect("PlaylistManager not initialized. Call init() first.")
|
2025-10-27 12:32:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Cr<43>e une playlist persistante (erreur si existe d<>j<EFBFBD>)
|
|
|
|
|
|
pub async fn create_persistent_playlist(&self, id: String) -> Result<WriteHandle> {
|
|
|
|
|
|
let mut playlists = self.inner.playlists.write().await;
|
|
|
|
|
|
|
|
|
|
|
|
if playlists.contains_key(&id) {
|
|
|
|
|
|
return Err(crate::Error::PlaylistAlreadyExists(id));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let playlist = Arc::new(Playlist::new(
|
|
|
|
|
|
id.clone(),
|
|
|
|
|
|
id.clone(), // Titre = id par d<>faut
|
|
|
|
|
|
PlaylistConfig::default(),
|
|
|
|
|
|
true, // persistent
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
// Acqu<71>rir le write lock
|
|
|
|
|
|
let write_token = playlist
|
|
|
|
|
|
.acquire_write_lock()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|_| crate::Error::WriteLockHeld(id.clone()))?;
|
|
|
|
|
|
|
|
|
|
|
|
playlists.insert(id.clone(), playlist.clone());
|
|
|
|
|
|
drop(playlists);
|
|
|
|
|
|
|
|
|
|
|
|
// Sauvegarder la structure vide
|
|
|
|
|
|
if let Some(persistence) = &self.inner.persistence {
|
|
|
|
|
|
let title = playlist.title().await;
|
|
|
|
|
|
let core = playlist.core.read().await;
|
2025-10-27 22:06:24 +01:00
|
|
|
|
persistence
|
|
|
|
|
|
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
2025-10-27 12:32:31 +01:00
|
|
|
|
.await?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(WriteHandle::new(playlist, write_token))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// R<>cup<75>re un write handle (cr<63>e <20>ph<70>m<EFBFBD>re si n'existe pas)
|
|
|
|
|
|
pub async fn get_write_handle(&self, id: String) -> Result<WriteHandle> {
|
|
|
|
|
|
let mut playlists = self.inner.playlists.write().await;
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(playlist) = playlists.get(&id) {
|
|
|
|
|
|
// Playlist existe, tenter d'acqu<71>rir le lock
|
|
|
|
|
|
let write_token = playlist
|
|
|
|
|
|
.acquire_write_lock()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|_| crate::Error::WriteLockHeld(id.clone()))?;
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(WriteHandle::new(playlist.clone(), write_token));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// N'existe pas, cr<63>er <20>ph<70>m<EFBFBD>re
|
|
|
|
|
|
let playlist = Arc::new(Playlist::new(
|
|
|
|
|
|
id.clone(),
|
|
|
|
|
|
id.clone(),
|
|
|
|
|
|
PlaylistConfig::default(),
|
|
|
|
|
|
false, // <20>ph<70>m<EFBFBD>re
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
let write_token = playlist
|
|
|
|
|
|
.acquire_write_lock()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|_| crate::Error::WriteLockHeld(id.clone()))?;
|
|
|
|
|
|
|
|
|
|
|
|
playlists.insert(id, playlist.clone());
|
|
|
|
|
|
drop(playlists);
|
|
|
|
|
|
|
|
|
|
|
|
Ok(WriteHandle::new(playlist, write_token))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// R<>cup<75>re un write handle persistant (cr<63>e si n'existe pas)
|
|
|
|
|
|
pub async fn get_persistent_write_handle(&self, id: String) -> Result<WriteHandle> {
|
|
|
|
|
|
let playlists = self.inner.playlists.read().await;
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(playlist) = playlists.get(&id) {
|
|
|
|
|
|
// Playlist existe
|
|
|
|
|
|
if !playlist.persistent {
|
|
|
|
|
|
return Err(crate::Error::PlaylistNotPersistent(id));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let write_token = playlist
|
|
|
|
|
|
.acquire_write_lock()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|_| crate::Error::WriteLockHeld(id.clone()))?;
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(WriteHandle::new(playlist.clone(), write_token));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
drop(playlists);
|
|
|
|
|
|
|
|
|
|
|
|
// N'existe pas, cr<63>er persistent
|
|
|
|
|
|
self.create_persistent_playlist(id).await
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// R<>cup<75>re un read handle (ressuscite depuis DB si besoin)
|
|
|
|
|
|
pub async fn get_read_handle(&self, id: &str) -> Result<ReadHandle> {
|
|
|
|
|
|
let playlists = self.inner.playlists.read().await;
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(playlist) = playlists.get(id) {
|
|
|
|
|
|
if !playlist.is_alive() {
|
|
|
|
|
|
return Err(crate::Error::PlaylistDeleted(id.to_string()));
|
|
|
|
|
|
}
|
|
|
|
|
|
return Ok(ReadHandle::new(playlist.clone()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
drop(playlists);
|
|
|
|
|
|
|
|
|
|
|
|
// 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? {
|
|
|
|
|
|
// Reconstruire la playlist
|
|
|
|
|
|
let mut playlists = self.inner.playlists.write().await;
|
|
|
|
|
|
|
2025-10-27 22:06:24 +01:00
|
|
|
|
let playlist = Arc::new(Playlist::new(id.to_string(), title.clone(), config, true));
|
2025-10-27 12:32:31 +01:00
|
|
|
|
|
|
|
|
|
|
// Restaurer les tracks
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut core = playlist.core.write().await;
|
|
|
|
|
|
core.tracks = tracks;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
playlists.insert(id.to_string(), playlist.clone());
|
|
|
|
|
|
drop(playlists);
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(ReadHandle::new(playlist));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Err(crate::Error::PlaylistNotFound(id.to_string()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Supprime une playlist d<>finitivement
|
|
|
|
|
|
pub async fn delete_playlist(&self, id: &str) -> Result<()> {
|
|
|
|
|
|
let mut playlists = self.inner.playlists.write().await;
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(playlist) = playlists.remove(id) {
|
|
|
|
|
|
playlist.mark_deleted();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
drop(playlists);
|
|
|
|
|
|
|
|
|
|
|
|
// Supprimer de la DB
|
|
|
|
|
|
if let Some(persistence) = &self.inner.persistence {
|
|
|
|
|
|
persistence.delete_playlist(id).await?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Liste toutes les playlists
|
|
|
|
|
|
pub async fn list_playlists(&self) -> Vec<String> {
|
|
|
|
|
|
let playlists = self.inner.playlists.read().await;
|
|
|
|
|
|
playlists.keys().cloned().collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// V<>rifie si une playlist existe
|
|
|
|
|
|
pub async fn exists(&self, id: &str) -> bool {
|
|
|
|
|
|
self.inner.playlists.read().await.contains_key(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Retourne la r<>f<EFBFBD>rence au PersistenceManager
|
|
|
|
|
|
pub(crate) fn persistence(&self) -> Option<&Arc<PersistenceManager>> {
|
|
|
|
|
|
self.inner.persistence.as_ref()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Task d'<27>viction en background
|
|
|
|
|
|
async fn eviction_task(&self) {
|
|
|
|
|
|
loop {
|
|
|
|
|
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
|
|
|
|
|
|
|
|
|
|
let playlists = self.inner.playlists.read().await;
|
|
|
|
|
|
|
|
|
|
|
|
for playlist in playlists.values() {
|
|
|
|
|
|
if !playlist.is_alive() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut core = playlist.core.write().await;
|
|
|
|
|
|
let initial_len = core.len();
|
|
|
|
|
|
core.evict();
|
|
|
|
|
|
let new_len = core.len();
|
|
|
|
|
|
drop(core);
|
|
|
|
|
|
|
|
|
|
|
|
// Si des morceaux ont <20>t<EFBFBD> <20>vict<63>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 core = playlist.core.read().await;
|
2025-10-27 22:06:24 +01:00
|
|
|
|
let _ = persistence
|
|
|
|
|
|
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
|
|
|
|
|
.await;
|
2025-10-27 12:32:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
inner: self.inner.clone(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Helper pour supprimer une playlist (appel<65> depuis WriteHandle)
|
|
|
|
|
|
pub(crate) async fn delete_playlist_internal(id: &str) -> Result<()> {
|
|
|
|
|
|
PlaylistManager::get().delete_playlist(id).await
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 20:02:01 +00:00
|
|
|
|
/// Enregistre le cache audio global
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Cette fonction doit être appelée au démarrage de l'application
|
|
|
|
|
|
/// pour rendre le cache audio disponible au PlaylistManager.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Examples
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ```rust,ignore
|
|
|
|
|
|
/// use pmoplaylist::register_audio_cache;
|
|
|
|
|
|
/// use pmoaudiocache::Cache as AudioCache;
|
|
|
|
|
|
/// use std::sync::Arc;
|
|
|
|
|
|
///
|
|
|
|
|
|
/// let audio_cache = Arc::new(AudioCache::new("./cache", 1000)?);
|
|
|
|
|
|
/// register_audio_cache(audio_cache);
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
pub fn register_audio_cache(cache: Arc<pmoaudiocache::Cache>) {
|
|
|
|
|
|
let _ = AUDIO_CACHE.set(cache);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 12:32:31 +01:00
|
|
|
|
/// Helper pour acc<63>der au cache audio
|
|
|
|
|
|
pub(crate) fn audio_cache() -> Result<Arc<pmoaudiocache::Cache>> {
|
2025-11-05 20:02:01 +00:00
|
|
|
|
AUDIO_CACHE
|
|
|
|
|
|
.get()
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.or_else(|| {
|
2025-11-05 20:48:52 +00:00
|
|
|
|
// Fallback: essayer le registre global de pmoaudiocache
|
|
|
|
|
|
pmoaudiocache::get_audio_cache()
|
2025-11-05 20:02:01 +00:00
|
|
|
|
})
|
|
|
|
|
|
.ok_or_else(|| crate::Error::ManagerNotInitialized)
|
2025-10-27 12:32:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Fonction raccourcie pour acc<63>der au singleton
|
|
|
|
|
|
pub fn PlaylistManager() -> &'static PlaylistManager {
|
|
|
|
|
|
PlaylistManager::get()
|
|
|
|
|
|
}
|