Refactoring de pmoflac -factorisation de code ogg et opus
This commit is contained in:
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user