Files
pmomusic/pmoplaylist/src/handle/write.rs
2025-12-28 16:31:02 +01:00

565 lines
17 KiB
Rust

//! WriteHandle : accès exclusif en écriture à une playlist
use crate::playlist::record::Record;
use crate::playlist::{Playlist, PlaylistRole};
use crate::Result;
use pmocache::cache_trait::FileCache;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
/// Handle d'écriture sur une playlist (exclusif)
pub struct WriteHandle {
playlist: Arc<Playlist>,
_write_token: Arc<()>,
}
impl WriteHandle {
/// Crée un nouveau handle d'écriture
pub(crate) fn new(playlist: Arc<Playlist>, write_token: Arc<()>) -> Self {
Self {
playlist,
_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).await {
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);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
// Sauvegarder si persistante
if self.playlist.persistent {
self.save_to_db().await?;
}
// Notifier le manager
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Ajoute un morceau avec un TTL personnalisé
pub async fn push_with_ttl(&self, cache_pk: String, ttl: Duration) -> 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).await {
return Err(crate::Error::CacheEntryNotFound(cache_pk));
}
// Ajouter à la playlist avec TTL
let record = Record::with_ttl(cache_pk, ttl);
let mut core = self.playlist.core.write().await;
core.push(record);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
// Sauvegarder si persistante
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
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 {
if !cache.is_valid_pk(pk).await {
return Err(crate::Error::CacheEntryNotFound(pk.clone()));
}
}
// Créer tous les records
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);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
// Une seule sauvegarde pour tout le batch
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
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();
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Supprime un morceau par sa cache_pk. Retourne true si un élément a été retiré.
pub async fn remove_track(&self, cache_pk: &str) -> Result<bool> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let mut core = self.playlist.core.write().await;
let removed = core.remove_by_cache_pk(cache_pk);
let snapshot = core.snapshot();
drop(core);
if removed {
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
}
Ok(removed)
}
/// Met à jour 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);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Met à jour 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);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
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
// Nettoyer les index puis supprimer du manager
let manager = crate::manager::PlaylistManager();
manager.rebuild_track_index(&self.playlist.id, &[]).await;
crate::manager::delete_playlist_internal(&self.playlist.id).await?;
// Notifier la suppression
manager.notify_playlist_changed(&self.playlist.id);
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?;
}
crate::manager::PlaylistManager().notify_playlist_changed(&self.playlist.id);
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(())
}
/// Met à jour la cover (cover_pk) associée à la playlist.
pub async fn set_cover_pk(&self, cover_pk: Option<String>) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
self.playlist.set_cover_pk(cover_pk).await;
if self.playlist.persistent {
self.save_to_db().await?;
}
crate::manager::PlaylistManager().notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Vérifie si la playlist contient déjà un pk
pub async fn contains_pk(&self, cache_pk: &str) -> Result<bool> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let core = self.playlist.core.read().await;
Ok(core.tracks.iter().any(|record| record.cache_pk == cache_pk))
}
/// 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 role = self.playlist.role().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 new_handle = manager
.create_persistent_playlist_with_role(new_id, role)
.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();
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 async fn role(&self) -> PlaylistRole {
self.playlist.role().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
}
// ============================================================================
// LAZY PK SUPPORT
// ============================================================================
/// Ajoute un track sans valider l'existence du fichier
///
/// À utiliser pour les lazy PK qui seront téléchargés on-demand.
/// Contrairement à `push()`, cette méthode ne vérifie pas si le fichier
/// existe dans le cache avant de l'ajouter.
///
/// # Arguments
///
/// * `cache_pk` - PK du fichier (peut être un lazy PK "L:...")
pub async fn push_lazy(&self, cache_pk: String) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
// PAS de validation is_valid_pk()
let record = Record::new(cache_pk);
let mut core = self.playlist.core.write().await;
core.push(record);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Version batch pour ajouter plusieurs lazy PK
///
/// Plus efficace que push_lazy() en boucle car ne reconstruit
/// l'index qu'une seule fois à la fin.
///
/// # Arguments
///
/// * `cache_pks` - Liste de PKs à ajouter (peuvent être des lazy PK)
pub async fn push_lazy_batch(&self, cache_pks: Vec<String>) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let records: Vec<Record> = cache_pks.into_iter().map(Record::new).collect();
let mut core = self.playlist.core.write().await;
core.push_all(records);
let snapshot = core.snapshot();
drop(core);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
manager.notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Commute un cache_pk vers un nouveau PK
///
/// Utilisé quand un lazy PK est téléchargé et devient un real PK.
/// Met à jour tous les records qui utilisent l'ancien PK.
///
/// # Arguments
///
/// * `old_pk` - L'ancien PK (typiquement un lazy PK "L:...")
/// * `new_pk` - Le nouveau PK (real pk calculé après téléchargement)
///
/// # Example
///
/// ```rust,no_run
/// // Appelé quand un lazy PK est téléchargé
/// writer.update_cache_pk("L:abc123", "xyz789").await?;
/// ```
pub async fn update_cache_pk(&self, old_pk: &str, new_pk: &str) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
let mut core = self.playlist.core.write().await;
let mut updated = false;
// Parcourir tous les records et recréer ceux qui correspondent
// Les records sont dans des Arc, donc on doit les recréer pour les modifier
for i in 0..core.tracks.len() {
if let Some(record_arc) = core.tracks.get(i) {
if record_arc.cache_pk == old_pk {
// Créer un nouveau record avec le nouveau PK
let mut new_record = (**record_arc).clone();
new_record.cache_pk = new_pk.to_string();
core.tracks[i] = Arc::new(new_record);
updated = true;
}
}
}
let snapshot = core.snapshot();
drop(core);
if updated {
tracing::debug!(
"Updated {} -> {} in playlist {}",
old_pk,
new_pk,
self.playlist.id
);
self.playlist.touch().await;
if self.playlist.persistent {
self.save_to_db().await?;
}
let manager = crate::manager::PlaylistManager();
manager
.rebuild_track_index(&self.playlist.id, &snapshot)
.await;
// PK swap uniquement - pas de notification UPnP pour éviter le reload
manager.notify_playlist_pk_updated(&self.playlist.id, old_pk, new_pk);
}
Ok(())
}
// Helpers internes
async fn save_to_db(&self) -> Result<()> {
let manager = crate::manager::PlaylistManager();
let persistence = manager
.persistence()
.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;
let cover_pk = self.playlist.cover_pk().await;
persistence
.save_playlist(
&self.playlist.id,
&title,
&role,
cover_pk.as_deref(),
config,
tracks,
)
.await
}
}