Amélioration des pmoplaylist
This commit is contained in:
@@ -117,7 +117,7 @@ use pmoaudio::{
|
|||||||
};
|
};
|
||||||
use pmoaudiocache::Cache as AudioCache;
|
use pmoaudiocache::Cache as AudioCache;
|
||||||
use pmoflac::{decode_audio_stream, StreamInfo};
|
use pmoflac::{decode_audio_stream, StreamInfo};
|
||||||
use pmoplaylist::ReadHandle;
|
use pmoplaylist::{PlaylistRole, ReadHandle};
|
||||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||||
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
|
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ use pmoaudiocache::{
|
|||||||
};
|
};
|
||||||
use pmocovers::{new_cache_with_consolidation as new_cover_cache, register_cover_cache};
|
use pmocovers::{new_cache_with_consolidation as new_cover_cache, register_cover_cache};
|
||||||
use pmoparadise::{RadioParadiseClient, RadioParadiseStreamSource};
|
use pmoparadise::{RadioParadiseClient, RadioParadiseStreamSource};
|
||||||
use pmoplaylist::register_audio_cache as register_playlist_audio_cache;
|
use pmoplaylist::{register_audio_cache as register_playlist_audio_cache, PlaylistRole};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -137,6 +137,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
writer
|
writer
|
||||||
.set_title(format!("Radio Paradise - Channel {}", channel_id))
|
.set_title(format!("Radio Paradise - Channel {}", channel_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
writer.set_role(PlaylistRole::Radio).await?;
|
||||||
writer.flush().await?; // Vider la playlist si elle existait
|
writer.flush().await?; // Vider la playlist si elle existait
|
||||||
tracing::debug!("Playlist created and flushed");
|
tracing::debug!("Playlist created and flushed");
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{client::RadioParadiseClient, models::EventId};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use pmoaudiocache::Cache as AudioCache;
|
use pmoaudiocache::Cache as AudioCache;
|
||||||
use pmocovers::Cache as CoversCache;
|
use pmocovers::Cache as CoversCache;
|
||||||
use pmoplaylist::{PlaylistManager, ReadHandle, WriteHandle};
|
use pmoplaylist::{PlaylistManager, PlaylistRole, ReadHandle, WriteHandle};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -115,7 +115,7 @@ impl RadioParadisePlaylistFeeder {
|
|||||||
) -> Result<(Self, ReadHandle)> {
|
) -> Result<(Self, ReadHandle)> {
|
||||||
let manager = PlaylistManager::get();
|
let manager = PlaylistManager::get();
|
||||||
let write_handle = manager
|
let write_handle = manager
|
||||||
.create_persistent_playlist(playlist_id.clone())
|
.create_persistent_playlist_with_role(playlist_id.clone(), PlaylistRole::Radio)
|
||||||
.await?;
|
.await?;
|
||||||
let read_handle = manager.get_read_handle(&playlist_id).await?;
|
let read_handle = manager.get_read_handle(&playlist_id).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ impl ReadHandle {
|
|||||||
if self.playlist.persistent {
|
if self.playlist.persistent {
|
||||||
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
|
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
|
let role = self.playlist.role().await;
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let _ = persistence
|
let _ = persistence
|
||||||
.save_playlist(&self.playlist.id, &title, &core.config, &core.tracks)
|
.save_playlist(&self.playlist.id, &title, &role, &core.config, &core.tracks)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! WriteHandle : accès exclusif en écriture à une playlist
|
//! WriteHandle : accès exclusif en écriture à une playlist
|
||||||
|
|
||||||
use crate::playlist::record::Record;
|
use crate::playlist::record::Record;
|
||||||
use crate::playlist::Playlist;
|
use crate::playlist::{Playlist, PlaylistRole};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use pmocache::cache_trait::FileCache;
|
use pmocache::cache_trait::FileCache;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -196,6 +196,23 @@ impl WriteHandle {
|
|||||||
Ok(())
|
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
|
/// Change la capacité maximale
|
||||||
pub async fn set_capacity(&self, max_size: Option<usize>) -> Result<()> {
|
pub async fn set_capacity(&self, max_size: Option<usize>) -> Result<()> {
|
||||||
if !self.playlist.is_alive() {
|
if !self.playlist.is_alive() {
|
||||||
@@ -266,6 +283,7 @@ impl WriteHandle {
|
|||||||
|
|
||||||
// Récupérer les données actuelles
|
// Récupérer les données actuelles
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
|
let role = self.playlist.role().await;
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let config = core.config.clone();
|
let config = core.config.clone();
|
||||||
let tracks = core.snapshot();
|
let tracks = core.snapshot();
|
||||||
@@ -273,7 +291,9 @@ impl WriteHandle {
|
|||||||
|
|
||||||
// Créer la nouvelle playlist persistante
|
// Créer la nouvelle playlist persistante
|
||||||
let manager = crate::manager::PlaylistManager();
|
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
|
// Copier le titre et la config
|
||||||
new_handle.set_title(title).await?;
|
new_handle.set_title(title).await?;
|
||||||
@@ -297,6 +317,10 @@ impl WriteHandle {
|
|||||||
self.playlist.title().await
|
self.playlist.title().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn role(&self) -> PlaylistRole {
|
||||||
|
self.playlist.role().await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_persistent(&self) -> bool {
|
pub fn is_persistent(&self) -> bool {
|
||||||
self.playlist.persistent
|
self.playlist.persistent
|
||||||
}
|
}
|
||||||
@@ -474,12 +498,13 @@ impl WriteHandle {
|
|||||||
.ok_or_else(|| crate::Error::PersistenceError("No persistence manager".into()))?;
|
.ok_or_else(|| crate::Error::PersistenceError("No persistence manager".into()))?;
|
||||||
|
|
||||||
let title = self.playlist.title().await;
|
let title = self.playlist.title().await;
|
||||||
|
let role = self.playlist.role().await;
|
||||||
let core = self.playlist.core.read().await;
|
let core = self.playlist.core.read().await;
|
||||||
let config = &core.config;
|
let config = &core.config;
|
||||||
let tracks = &core.tracks;
|
let tracks = &core.tracks;
|
||||||
|
|
||||||
persistence
|
persistence
|
||||||
.save_playlist(&self.playlist.id, &title, config, tracks)
|
.save_playlist(&self.playlist.id, &title, &role, config, tracks)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ pub use error::{Error, Result};
|
|||||||
pub use handle::{ReadHandle, WriteHandle};
|
pub use handle::{ReadHandle, WriteHandle};
|
||||||
pub use manager::{register_audio_cache, PlaylistManager, PlaylistManager as Manager};
|
pub use manager::{register_audio_cache, PlaylistManager, PlaylistManager as Manager};
|
||||||
pub use manager::{subscribe_events, PlaylistEvent, PlaylistEventEnvelope, PlaylistEventKind};
|
pub use manager::{subscribe_events, PlaylistEvent, PlaylistEventEnvelope, PlaylistEventKind};
|
||||||
|
pub use playlist::PlaylistRole;
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
pub use sse::playlist_events_router;
|
pub use sse::playlist_events_router;
|
||||||
pub use track::PlaylistTrack;
|
pub use track::PlaylistTrack;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use crate::handle::{ReadHandle, WriteHandle};
|
use crate::handle::{ReadHandle, WriteHandle};
|
||||||
use crate::persistence::PersistenceManager;
|
use crate::persistence::PersistenceManager;
|
||||||
use crate::playlist::core::PlaylistConfig;
|
use crate::playlist::core::PlaylistConfig;
|
||||||
use crate::playlist::Playlist;
|
use crate::playlist::{Playlist, PlaylistRole};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use pmocache::{CacheBroadcastEvent, CacheEvent, CacheSubscription};
|
use pmocache::{CacheBroadcastEvent, CacheEvent, CacheSubscription};
|
||||||
@@ -138,8 +138,12 @@ impl PlaylistManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cr<43>e une playlist persistante (erreur si existe d<>j<EFBFBD>)
|
/// Cr<43>e une playlist persistante (erreur si existe d<>j<EFBFBD>) avec rôle personnalisé
|
||||||
pub async fn create_persistent_playlist(&self, id: String) -> Result<WriteHandle> {
|
pub async fn create_persistent_playlist_with_role(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
role: PlaylistRole,
|
||||||
|
) -> Result<WriteHandle> {
|
||||||
let mut playlists = self.inner.playlists.write().await;
|
let mut playlists = self.inner.playlists.write().await;
|
||||||
|
|
||||||
if playlists.contains_key(&id) {
|
if playlists.contains_key(&id) {
|
||||||
@@ -148,9 +152,10 @@ impl PlaylistManager {
|
|||||||
|
|
||||||
let playlist = Arc::new(Playlist::new(
|
let playlist = Arc::new(Playlist::new(
|
||||||
id.clone(),
|
id.clone(),
|
||||||
id.clone(), // Titre = id par d<EFBFBD>faut
|
id.clone(), // Titre = id par défaut
|
||||||
PlaylistConfig::default(),
|
PlaylistConfig::default(),
|
||||||
true, // persistent
|
true, // persistent
|
||||||
|
role,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Acqu<71>rir le write lock
|
// Acqu<71>rir le write lock
|
||||||
@@ -165,15 +170,22 @@ impl PlaylistManager {
|
|||||||
// Sauvegarder la structure vide
|
// Sauvegarder la structure vide
|
||||||
if let Some(persistence) = &self.inner.persistence {
|
if let Some(persistence) = &self.inner.persistence {
|
||||||
let title = playlist.title().await;
|
let title = playlist.title().await;
|
||||||
|
let role = playlist.role().await;
|
||||||
let core = playlist.core.read().await;
|
let core = playlist.core.read().await;
|
||||||
persistence
|
persistence
|
||||||
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
.save_playlist(&playlist.id, &title, &role, &core.config, &core.tracks)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(WriteHandle::new(playlist, write_token))
|
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é).
|
/// Enregistre un callback d'évènement playlist (update, track joué).
|
||||||
///
|
///
|
||||||
/// Retourne un jeton (u64) pour désenregistrer plus tard.
|
/// Retourne un jeton (u64) pour désenregistrer plus tard.
|
||||||
@@ -394,7 +406,8 @@ impl PlaylistManager {
|
|||||||
id.clone(),
|
id.clone(),
|
||||||
id.clone(),
|
id.clone(),
|
||||||
PlaylistConfig::default(),
|
PlaylistConfig::default(),
|
||||||
false, // <EFBFBD>ph<EFBFBD>m<EFBFBD>re
|
false, // éphémère
|
||||||
|
PlaylistRole::User,
|
||||||
));
|
));
|
||||||
|
|
||||||
let write_token = playlist
|
let write_token = playlist
|
||||||
@@ -430,11 +443,12 @@ impl PlaylistManager {
|
|||||||
|
|
||||||
// Pas en mémoire, essayer de charger depuis la DB
|
// Pas en mémoire, essayer de charger depuis la DB
|
||||||
if let Some(persistence) = &self.inner.persistence {
|
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
|
// Reconstruire la playlist
|
||||||
let mut playlists = self.inner.playlists.write().await;
|
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
|
// Restaurer les tracks
|
||||||
{
|
{
|
||||||
@@ -477,11 +491,12 @@ impl PlaylistManager {
|
|||||||
|
|
||||||
// Pas en m<>moire, essayer de ressusciter depuis la DB
|
// Pas en m<>moire, essayer de ressusciter depuis la DB
|
||||||
if let Some(persistence) = &self.inner.persistence {
|
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
|
// Reconstruire la playlist
|
||||||
let mut playlists = self.inner.playlists.write().await;
|
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
|
// Restaurer les tracks
|
||||||
{
|
{
|
||||||
@@ -767,13 +782,14 @@ impl PlaylistManager {
|
|||||||
let new_len = core.len();
|
let new_len = core.len();
|
||||||
drop(core);
|
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 new_len < initial_len && playlist.persistent {
|
||||||
if let Some(persistence) = &self.inner.persistence {
|
if let Some(persistence) = &self.inner.persistence {
|
||||||
let title = playlist.title().await;
|
let title = playlist.title().await;
|
||||||
|
let role = playlist.role().await;
|
||||||
let core = playlist.core.read().await;
|
let core = playlist.core.read().await;
|
||||||
let _ = persistence
|
let _ = persistence
|
||||||
.save_playlist(&playlist.id, &title, &core.config, &core.tracks)
|
.save_playlist(&playlist.id, &title, &role, &core.config, &core.tracks)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
use crate::playlist::core::PlaylistConfig;
|
use crate::playlist::core::PlaylistConfig;
|
||||||
use crate::playlist::record::Record;
|
use crate::playlist::record::Record;
|
||||||
|
use crate::playlist::PlaylistRole;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use rusqlite::{params, Connection};
|
use rusqlite::{params, Connection};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Gestionnaire de persistance (une base pour toutes les playlists)
|
/// Gestionnaire de persistance (une base pour toutes les playlists)
|
||||||
pub struct PersistenceManager {
|
pub struct PersistenceManager {
|
||||||
@@ -33,6 +35,7 @@ impl PersistenceManager {
|
|||||||
"CREATE TABLE IF NOT EXISTS playlists (
|
"CREATE TABLE IF NOT EXISTS playlists (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
max_size INTEGER,
|
max_size INTEGER,
|
||||||
default_ttl_secs INTEGER,
|
default_ttl_secs INTEGER,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
@@ -80,6 +83,7 @@ impl PersistenceManager {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
title: &str,
|
title: &str,
|
||||||
|
role: &PlaylistRole,
|
||||||
config: &PlaylistConfig,
|
config: &PlaylistConfig,
|
||||||
tracks: &VecDeque<Arc<Record>>,
|
tracks: &VecDeque<Arc<Record>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -92,13 +96,14 @@ impl PersistenceManager {
|
|||||||
|
|
||||||
// Upsert playlist metadata
|
// Upsert playlist metadata
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO playlists (id, title, max_size, default_ttl_secs, created_at, last_modified)
|
"INSERT OR REPLACE INTO playlists (id, title, role, max_size, default_ttl_secs, created_at, last_modified)
|
||||||
VALUES (?1, ?2, ?3, ?4,
|
VALUES (?1, ?2, ?3, ?4, ?5,
|
||||||
COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?5),
|
COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?6),
|
||||||
?5)",
|
?6)",
|
||||||
params![
|
params![
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
role.as_str(),
|
||||||
config.max_size.map(|s| s as i64),
|
config.max_size.map(|s| s as i64),
|
||||||
config.default_ttl.map(|d| d.as_secs() as i64),
|
config.default_ttl.map(|d| d.as_secs() as i64),
|
||||||
now_nanos,
|
now_nanos,
|
||||||
@@ -135,23 +140,28 @@ impl PersistenceManager {
|
|||||||
pub async fn load_playlist(
|
pub async fn load_playlist(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> Result<Option<(String, PlaylistConfig, VecDeque<Arc<Record>>)>> {
|
) -> Result<Option<(String, PlaylistRole, PlaylistConfig, VecDeque<Arc<Record>>)>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
|
||||||
// Charger les métadonnées
|
// Charger les métadonnées
|
||||||
let mut stmt = conn
|
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| {
|
.map_err(|e| {
|
||||||
crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e))
|
crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let result = stmt.query_row(params![id], |row| {
|
let result = stmt.query_row(params![id], |row| {
|
||||||
let title: String = row.get(0)?;
|
let title: String = row.get(0)?;
|
||||||
let max_size: Option<i64> = row.get(1)?;
|
let role_raw: String = row.get(1)?;
|
||||||
let default_ttl_secs: Option<i64> = row.get(2)?;
|
let max_size: Option<i64> = row.get(2)?;
|
||||||
|
let default_ttl_secs: Option<i64> = row.get(3)?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
title,
|
title,
|
||||||
|
PlaylistRole::from_str(&role_raw)
|
||||||
|
.unwrap_or_else(|_| PlaylistRole::custom(role_raw)),
|
||||||
PlaylistConfig {
|
PlaylistConfig {
|
||||||
max_size: max_size.map(|s| s as usize),
|
max_size: max_size.map(|s| s as usize),
|
||||||
default_ttl: default_ttl_secs.map(|s| Duration::from_secs(s as u64)),
|
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,
|
Ok(data) => data,
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -202,7 +212,7 @@ impl PersistenceManager {
|
|||||||
tracks.push_back(Arc::new(record));
|
tracks.push_back(Arc::new(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some((title, config, tracks)))
|
Ok(Some((title, role, config, tracks)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime une playlist
|
/// Supprime une playlist
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ pub mod core;
|
|||||||
pub mod record;
|
pub mod record;
|
||||||
|
|
||||||
use self::core::{PlaylistConfig, PlaylistCore};
|
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::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
@@ -30,6 +33,7 @@ impl From<u8> for PlaylistState {
|
|||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
title: RwLock<String>,
|
title: RwLock<String>,
|
||||||
|
role: RwLock<PlaylistRole>,
|
||||||
state: Arc<AtomicU8>,
|
state: Arc<AtomicU8>,
|
||||||
pub core: Arc<RwLock<PlaylistCore>>,
|
pub core: Arc<RwLock<PlaylistCore>>,
|
||||||
pub persistent: bool,
|
pub persistent: bool,
|
||||||
@@ -39,10 +43,17 @@ pub struct Playlist {
|
|||||||
|
|
||||||
impl Playlist {
|
impl Playlist {
|
||||||
/// Crée une nouvelle 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 {
|
Self {
|
||||||
id,
|
id,
|
||||||
title: RwLock::new(title),
|
title: RwLock::new(title),
|
||||||
|
role: RwLock::new(role),
|
||||||
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
|
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
|
||||||
core: Arc::new(RwLock::new(PlaylistCore::new(config))),
|
core: Arc::new(RwLock::new(PlaylistCore::new(config))),
|
||||||
persistent,
|
persistent,
|
||||||
@@ -78,6 +89,17 @@ impl Playlist {
|
|||||||
self.touch().await;
|
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
|
/// Timestamp du dernier changement
|
||||||
pub async fn last_change(&self) -> SystemTime {
|
pub async fn last_change(&self) -> SystemTime {
|
||||||
*self.last_change.read().await
|
*self.last_change.read().await
|
||||||
@@ -100,3 +122,90 @@ impl Playlist {
|
|||||||
Ok(token)
|
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())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use pmoaudiocache::{register_audio_cache, AudioCacheConfigExt, Cache as AudioCac
|
|||||||
use pmoconfig::get_config;
|
use pmoconfig::get_config;
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
use pmocovers::CoverCacheConfigExt;
|
use pmocovers::CoverCacheConfigExt;
|
||||||
use pmoplaylist::PlaylistManager;
|
use pmoplaylist::{PlaylistManager, PlaylistRole};
|
||||||
use pmoqobuz::{QobuzClient, QobuzSource};
|
use pmoqobuz::{QobuzClient, QobuzSource};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -107,7 +107,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let playlist_manager = PlaylistManager();
|
let playlist_manager = PlaylistManager();
|
||||||
let playlist_id = {
|
let playlist_id = {
|
||||||
let writer = playlist_manager
|
let writer = playlist_manager
|
||||||
.create_persistent_playlist("lazy-test".to_string())
|
.create_persistent_playlist_with_role("lazy-test".to_string(), PlaylistRole::Album)
|
||||||
.await?;
|
.await?;
|
||||||
writer.id().to_string()
|
writer.id().to_string()
|
||||||
}; // Drop writer here to release the lock
|
}; // Drop writer here to release the lock
|
||||||
|
|||||||
@@ -402,6 +402,11 @@ impl QobuzSource {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||||
|
|
||||||
|
writer
|
||||||
|
.set_role(pmoplaylist::PlaylistRole::Album)
|
||||||
|
.await
|
||||||
|
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||||
|
|
||||||
writer
|
writer
|
||||||
.push_lazy_batch(lazy_pks.clone())
|
.push_lazy_batch(lazy_pks.clone())
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user