Première version testable avec la construction d'une image docker #22
@@ -117,7 +117,7 @@ use pmoaudio::{
|
||||
};
|
||||
use pmoaudiocache::Cache as AudioCache;
|
||||
use pmoflac::{decode_audio_stream, StreamInfo};
|
||||
use pmoplaylist::ReadHandle;
|
||||
use pmoplaylist::{PlaylistRole, ReadHandle};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
|
||||
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 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::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -137,6 +137,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
writer
|
||||
.set_title(format!("Radio Paradise - Channel {}", channel_id))
|
||||
.await?;
|
||||
writer.set_role(PlaylistRole::Radio).await?;
|
||||
writer.flush().await?; // Vider la playlist si elle existait
|
||||
tracing::debug!("Playlist created and flushed");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{client::RadioParadiseClient, models::EventId};
|
||||
use anyhow::Result;
|
||||
use pmoaudiocache::Cache as AudioCache;
|
||||
use pmocovers::Cache as CoversCache;
|
||||
use pmoplaylist::{PlaylistManager, ReadHandle, WriteHandle};
|
||||
use pmoplaylist::{PlaylistManager, PlaylistRole, ReadHandle, WriteHandle};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
@@ -115,7 +115,7 @@ impl RadioParadisePlaylistFeeder {
|
||||
) -> Result<(Self, ReadHandle)> {
|
||||
let manager = PlaylistManager::get();
|
||||
let write_handle = manager
|
||||
.create_persistent_playlist(playlist_id.clone())
|
||||
.create_persistent_playlist_with_role(playlist_id.clone(), PlaylistRole::Radio)
|
||||
.await?;
|
||||
let read_handle = manager.get_read_handle(&playlist_id).await?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use pmoaudiocache::{register_audio_cache, AudioCacheConfigExt, Cache as AudioCac
|
||||
use pmoconfig::get_config;
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use pmocovers::CoverCacheConfigExt;
|
||||
use pmoplaylist::PlaylistManager;
|
||||
use pmoplaylist::{PlaylistManager, PlaylistRole};
|
||||
use pmoqobuz::{QobuzClient, QobuzSource};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -107,7 +107,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let playlist_manager = PlaylistManager();
|
||||
let playlist_id = {
|
||||
let writer = playlist_manager
|
||||
.create_persistent_playlist("lazy-test".to_string())
|
||||
.create_persistent_playlist_with_role("lazy-test".to_string(), PlaylistRole::Album)
|
||||
.await?;
|
||||
writer.id().to_string()
|
||||
}; // Drop writer here to release the lock
|
||||
|
||||
@@ -402,6 +402,11 @@ impl QobuzSource {
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
writer
|
||||
.set_role(pmoplaylist::PlaylistRole::Album)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
writer
|
||||
.push_lazy_batch(lazy_pks.clone())
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user