- Add qobuz_debug to .gitignore - Bump PMOMusic version to 0.3.26 - Add .cargo/ copy in Dockerfile for registry config - Implement infinite scroll in ServerDrawer.vue with IntersectionObserver and sentinel element - Add source/source_version fields to playlists for cache invalidation - Update pmocache and pmoplaylist DB schemas with versioning (SCHEMA_VERSION) - Add Qobuz API metadata fetching and pagination for playlist tracks - Implement album/playlist cache invalidation based on released_at/updated_at timestamps - Refactor adapt_playlist_items_to_qobuz → adapt_items_to_qobuz with cleaner logic - Add debug mode to save Qobuz API responses when QOBUZ_DEBUG_DIR is set
267 lines
7.1 KiB
Rust
267 lines
7.1 KiB
Rust
//! Playlist interne (non exposée publiquement)
|
|
|
|
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;
|
|
use tokio::sync::RwLock;
|
|
|
|
/// État d'une playlist
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
enum PlaylistState {
|
|
Active = 0,
|
|
Deleted = 1,
|
|
}
|
|
|
|
impl From<u8> for PlaylistState {
|
|
fn from(value: u8) -> Self {
|
|
match value {
|
|
1 => PlaylistState::Deleted,
|
|
_ => PlaylistState::Active,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Playlist interne (gérée par le PlaylistManager)
|
|
pub struct Playlist {
|
|
pub id: String,
|
|
title: RwLock<String>,
|
|
role: RwLock<PlaylistRole>,
|
|
cover_pk: RwLock<Option<String>>,
|
|
artist: RwLock<Option<String>>,
|
|
/// Source externe (ex: "qobuz")
|
|
source: RwLock<Option<String>>,
|
|
/// Version de la source (ex: timestamp updated_at de Qobuz)
|
|
source_version: RwLock<Option<String>>,
|
|
state: Arc<AtomicU8>,
|
|
pub core: Arc<RwLock<PlaylistCore>>,
|
|
pub persistent: bool,
|
|
last_change: RwLock<SystemTime>,
|
|
writer_lock: RwLock<Option<Weak<()>>>,
|
|
}
|
|
|
|
impl Playlist {
|
|
/// Crée une nouvelle playlist
|
|
pub fn new(
|
|
id: String,
|
|
title: String,
|
|
config: PlaylistConfig,
|
|
persistent: bool,
|
|
role: PlaylistRole,
|
|
cover_pk: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
title: RwLock::new(title),
|
|
role: RwLock::new(role),
|
|
cover_pk: RwLock::new(cover_pk),
|
|
artist: RwLock::new(None),
|
|
source: RwLock::new(None),
|
|
source_version: RwLock::new(None),
|
|
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
|
|
core: Arc::new(RwLock::new(PlaylistCore::new(config))),
|
|
persistent,
|
|
last_change: RwLock::new(SystemTime::now()),
|
|
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);
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// Retourne la cover associée à la playlist.
|
|
pub async fn cover_pk(&self) -> Option<String> {
|
|
self.cover_pk.read().await.clone()
|
|
}
|
|
|
|
/// Modifie la cover (PK) de la playlist.
|
|
pub async fn set_cover_pk(&self, value: Option<String>) {
|
|
*self.cover_pk.write().await = value;
|
|
self.touch().await;
|
|
}
|
|
|
|
/// Retourne l'artiste associé à la playlist.
|
|
pub async fn artist(&self) -> Option<String> {
|
|
self.artist.read().await.clone()
|
|
}
|
|
|
|
/// Modifie l'artiste de la playlist.
|
|
pub async fn set_artist(&self, value: Option<String>) {
|
|
*self.artist.write().await = value;
|
|
self.touch().await;
|
|
}
|
|
|
|
/// Retourne la source externe associée à la playlist.
|
|
pub async fn source(&self) -> Option<String> {
|
|
self.source.read().await.clone()
|
|
}
|
|
|
|
/// Modifie la source de la playlist.
|
|
pub async fn set_source(&self, value: Option<String>) {
|
|
*self.source.write().await = value;
|
|
self.touch().await;
|
|
}
|
|
|
|
/// Retourne la version de la source (ex: updated_at timestamp).
|
|
pub async fn source_version(&self) -> Option<String> {
|
|
self.source_version.read().await.clone()
|
|
}
|
|
|
|
/// Modifie la version de la source.
|
|
pub async fn set_source_version(&self, value: Option<String>) {
|
|
*self.source_version.write().await = value;
|
|
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));
|
|
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())),
|
|
}
|
|
}
|
|
}
|