Amélioration de la vue playlist
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ export interface AudioCacheMetadata {
|
||||
|
||||
export interface AudioCacheEntry {
|
||||
pk: string;
|
||||
lazy_pk?: string | null;
|
||||
id: string | null;
|
||||
hits: number;
|
||||
last_used: string | null;
|
||||
|
||||
@@ -3,16 +3,24 @@ export interface PlaylistSummary {
|
||||
title: string;
|
||||
role: string;
|
||||
persistent: boolean;
|
||||
cover_pk?: string | null;
|
||||
cover_url?: string | null;
|
||||
track_count: number;
|
||||
max_size?: number | null;
|
||||
default_ttl_secs?: number | null;
|
||||
last_change: string;
|
||||
}
|
||||
|
||||
import type { AudioCacheMetadata } from "./audioCache";
|
||||
|
||||
export interface PlaylistTrack {
|
||||
cache_pk: string;
|
||||
added_at: string;
|
||||
ttl_secs?: number | null;
|
||||
lazy_pk?: string | null;
|
||||
metadata?: AudioCacheMetadata | null;
|
||||
cover_url?: string | null;
|
||||
cover_source?: string | null;
|
||||
}
|
||||
|
||||
export interface PlaylistDetail {
|
||||
@@ -29,6 +37,7 @@ export interface CreatePlaylistPayload {
|
||||
id: string;
|
||||
title?: string;
|
||||
role?: string;
|
||||
cover_pk?: string;
|
||||
persistent?: boolean;
|
||||
max_size?: number;
|
||||
default_ttl_secs?: number;
|
||||
@@ -39,6 +48,7 @@ export interface UpdatePlaylistPayload {
|
||||
role?: string;
|
||||
max_size?: number | null;
|
||||
default_ttl_secs?: number | null;
|
||||
cover_pk?: string | null;
|
||||
}
|
||||
|
||||
export interface AddTracksPayload {
|
||||
|
||||
@@ -24,6 +24,9 @@ pub struct CacheEntry {
|
||||
/// Clé primaire unique de l'élément (hash SHA1 de l'URL)
|
||||
#[cfg_attr(feature = "openapi", schema(example = "1a2b3c4d5e6f7a8b"))]
|
||||
pub pk: String,
|
||||
/// Lazy PK historique associé (si l'élément provient d'un téléchargement différé)
|
||||
#[cfg_attr(feature = "openapi", schema(example = "L:QOBUZ:123456"))]
|
||||
pub lazy_pk: Option<String>,
|
||||
/// URL source de l'élément
|
||||
#[cfg_attr(feature = "openapi", schema(example = "https://example.com/resource"))]
|
||||
pub id: Option<String>,
|
||||
@@ -486,17 +489,18 @@ impl DB {
|
||||
let mut entry = {
|
||||
let conn = self.lock_conn("get");
|
||||
conn.query_row(
|
||||
"SELECT pk, id, collection, hits, last_used \
|
||||
"SELECT pk, lazy_pk, id, collection, hits, last_used \
|
||||
FROM asset \
|
||||
WHERE pk = ?1",
|
||||
[pk],
|
||||
|row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
id: row.get::<_, Option<String>>(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
lazy_pk: row.get::<_, Option<String>>(1)?,
|
||||
id: row.get::<_, Option<String>>(2)?,
|
||||
collection: row.get(3)?,
|
||||
hits: row.get(4)?,
|
||||
last_used: row.get(5)?,
|
||||
metadata: None,
|
||||
})
|
||||
},
|
||||
@@ -526,17 +530,18 @@ impl DB {
|
||||
let mut entry = {
|
||||
let conn = self.lock_conn("get_from_id");
|
||||
conn.query_row(
|
||||
"SELECT pk, id, collection, hits, last_used \
|
||||
"SELECT pk, lazy_pk, id, collection, hits, last_used \
|
||||
FROM asset \
|
||||
WHERE collection = ?1 AND id = ?2",
|
||||
params![collection, id],
|
||||
|row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
id: row.get::<_, Option<String>>(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
lazy_pk: row.get::<_, Option<String>>(1)?,
|
||||
id: row.get::<_, Option<String>>(2)?,
|
||||
collection: row.get(3)?,
|
||||
hits: row.get(4)?,
|
||||
last_used: row.get(5)?,
|
||||
metadata: None,
|
||||
})
|
||||
},
|
||||
@@ -627,7 +632,7 @@ impl DB {
|
||||
let conn = self.lock_conn("get_all");
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT pk, id, collection, hits, last_used
|
||||
"SELECT pk, lazy_pk, id, collection, hits, last_used
|
||||
FROM asset
|
||||
ORDER BY hits DESC",
|
||||
)?;
|
||||
@@ -635,10 +640,11 @@ impl DB {
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
id: row.get::<_, Option<String>>(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
lazy_pk: row.get::<_, Option<String>>(1)?,
|
||||
id: row.get::<_, Option<String>>(2)?,
|
||||
collection: row.get(3)?,
|
||||
hits: row.get(4)?,
|
||||
last_used: row.get(5)?,
|
||||
metadata: None,
|
||||
})
|
||||
})?;
|
||||
@@ -670,17 +676,18 @@ impl DB {
|
||||
let conn = self.lock_conn("get_by_collection");
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT pk, id, collection, hits, last_used
|
||||
"SELECT pk, lazy_pk, id, collection, hits, last_used
|
||||
FROM asset
|
||||
WHERE collection = ?1 ORDER BY hits DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([collection], |row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
id: row.get::<_, Option<String>>(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
lazy_pk: row.get::<_, Option<String>>(1)?,
|
||||
id: row.get::<_, Option<String>>(2)?,
|
||||
collection: row.get(3)?,
|
||||
hits: row.get(4)?,
|
||||
last_used: row.get(5)?,
|
||||
metadata: None,
|
||||
})
|
||||
})?;
|
||||
@@ -741,7 +748,7 @@ impl DB {
|
||||
let conn = self.lock_conn("get_oldest");
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT pk, id, collection, hits, last_used
|
||||
"SELECT pk, lazy_pk, id, collection, hits, last_used
|
||||
FROM asset
|
||||
ORDER BY last_used ASC, hits ASC
|
||||
LIMIT ?1",
|
||||
@@ -751,10 +758,11 @@ impl DB {
|
||||
.query_map([limit], |row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
id: row.get::<_, Option<String>>(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
lazy_pk: row.get::<_, Option<String>>(1)?,
|
||||
id: row.get::<_, Option<String>>(2)?,
|
||||
collection: row.get(3)?,
|
||||
hits: row.get(4)?,
|
||||
last_used: row.get(5)?,
|
||||
metadata: None,
|
||||
})
|
||||
})?
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! API REST pour la gestion des playlists.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use axum::{
|
||||
@@ -11,6 +12,7 @@ use axum::{
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::manager::{PlaylistOverview, PlaylistSnapshot, PlaylistTrackSnapshot};
|
||||
@@ -41,6 +43,10 @@ pub struct PlaylistSummaryResponse {
|
||||
#[schema(value_type = String)]
|
||||
pub role: PlaylistRole,
|
||||
pub persistent: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_pk: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_url: Option<String>,
|
||||
pub track_count: usize,
|
||||
pub max_size: Option<usize>,
|
||||
pub default_ttl_secs: Option<u64>,
|
||||
@@ -62,6 +68,14 @@ pub struct PlaylistTrackResponse {
|
||||
pub cache_pk: String,
|
||||
pub added_at: DateTime<Utc>,
|
||||
pub ttl_secs: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lazy_pk: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_source: Option<String>,
|
||||
}
|
||||
|
||||
/// Requête pour créer une playlist persistante/éphémère.
|
||||
@@ -71,6 +85,8 @@ pub struct CreatePlaylistRequest {
|
||||
pub title: Option<String>,
|
||||
#[schema(value_type = String)]
|
||||
pub role: Option<PlaylistRole>,
|
||||
#[schema(example = "abc123")]
|
||||
pub cover_pk: Option<String>,
|
||||
#[schema(example = true)]
|
||||
pub persistent: Option<bool>,
|
||||
pub max_size: Option<usize>,
|
||||
@@ -85,6 +101,8 @@ pub struct UpdatePlaylistRequest {
|
||||
pub role: Option<PlaylistRole>,
|
||||
pub max_size: Option<Option<usize>>,
|
||||
pub default_ttl_secs: Option<Option<u64>>,
|
||||
/// Utiliser `null` explicite pour supprimer la cover, ou omettre pour ne pas modifier.
|
||||
pub cover_pk: Option<Option<String>>,
|
||||
}
|
||||
|
||||
/// Requête pour ajouter des morceaux dans une playlist.
|
||||
@@ -153,6 +171,7 @@ pub async fn create_playlist(Json(req): Json<CreatePlaylistRequest>) -> Response
|
||||
|
||||
let result = async move {
|
||||
let id = req.id.clone();
|
||||
let normalized_cover_pk = normalize_cover_pk(req.cover_pk.clone());
|
||||
if persistent {
|
||||
let writer = manager
|
||||
.create_persistent_playlist_with_role(id.clone(), role)
|
||||
@@ -164,6 +183,9 @@ pub async fn create_playlist(Json(req): Json<CreatePlaylistRequest>) -> Response
|
||||
req.default_ttl_secs,
|
||||
)
|
||||
.await?;
|
||||
if let Some(cover_pk) = normalized_cover_pk.clone() {
|
||||
writer.set_cover_pk(Some(cover_pk)).await?;
|
||||
}
|
||||
} else {
|
||||
let writer = manager.get_write_handle(id.clone()).await?;
|
||||
if let Some(role) = requested_role {
|
||||
@@ -176,6 +198,9 @@ pub async fn create_playlist(Json(req): Json<CreatePlaylistRequest>) -> Response
|
||||
req.default_ttl_secs,
|
||||
)
|
||||
.await?;
|
||||
if let Some(cover_pk) = normalized_cover_pk {
|
||||
writer.set_cover_pk(Some(cover_pk)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
manager.playlist_snapshot(&req.id).await
|
||||
@@ -236,6 +261,7 @@ pub async fn update_playlist(
|
||||
role,
|
||||
max_size,
|
||||
default_ttl_secs,
|
||||
cover_pk,
|
||||
} = req;
|
||||
|
||||
let manager = crate::manager::PlaylistManager();
|
||||
@@ -256,6 +282,13 @@ pub async fn update_playlist(
|
||||
if let Some(ttl) = default_ttl_secs {
|
||||
writer.set_default_ttl(ttl.map(Duration::from_secs)).await?;
|
||||
}
|
||||
if let Some(cover_pk) = cover_pk {
|
||||
let normalized = match cover_pk {
|
||||
Some(value) => normalize_cover_pk(Some(value)),
|
||||
None => None,
|
||||
};
|
||||
writer.set_cover_pk(normalized).await?;
|
||||
}
|
||||
|
||||
manager.playlist_snapshot(&playlist_id).await
|
||||
}
|
||||
@@ -440,21 +473,78 @@ fn apply_metadata_updates(
|
||||
}
|
||||
}
|
||||
|
||||
fn playlist_track_to_response(track: &PlaylistTrackSnapshot) -> PlaylistTrackResponse {
|
||||
PlaylistTrackResponse {
|
||||
fn playlist_track_to_response(
|
||||
track: &PlaylistTrackSnapshot,
|
||||
audio_cache: Option<&Arc<pmoaudiocache::Cache>>,
|
||||
) -> PlaylistTrackResponse {
|
||||
let mut response = PlaylistTrackResponse {
|
||||
cache_pk: track.cache_pk.clone(),
|
||||
added_at: system_time_to_datetime(track.added_at),
|
||||
ttl_secs: track.ttl.map(|ttl| ttl.as_secs()),
|
||||
lazy_pk: None,
|
||||
metadata: None,
|
||||
cover_url: None,
|
||||
cover_source: None,
|
||||
};
|
||||
|
||||
if let Some(cache) = audio_cache {
|
||||
if let Ok(mut entry) = cache.db.get(&track.cache_pk, true) {
|
||||
if let Some(lazy_pk) = entry.lazy_pk.take() {
|
||||
if lazy_pk != response.cache_pk {
|
||||
response.lazy_pk = Some(lazy_pk);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(metadata) = entry.metadata {
|
||||
if let Some((url, source)) = resolve_cover_from_metadata(&metadata) {
|
||||
response.cover_url = Some(url);
|
||||
response.cover_source = Some(source);
|
||||
}
|
||||
response.metadata = Some(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn cover_url_from_pk(pk: &str) -> String {
|
||||
format!("/covers/image/{}/256", pk)
|
||||
}
|
||||
|
||||
fn normalize_cover_pk(input: Option<String>) -> Option<String> {
|
||||
input.and_then(|pk| {
|
||||
let trimmed = pk.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_cover_from_metadata(metadata: &Value) -> Option<(String, String)> {
|
||||
if let Some(cover_pk) = metadata.get("cover_pk").and_then(Value::as_str) {
|
||||
return Some((cover_url_from_pk(cover_pk), "cover_pk".to_string()));
|
||||
}
|
||||
|
||||
if let Some(url) = metadata.get("cover_url").and_then(Value::as_str) {
|
||||
return Some((url.to_string(), "cover_url".to_string()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl From<PlaylistOverview> for PlaylistSummaryResponse {
|
||||
fn from(value: PlaylistOverview) -> Self {
|
||||
let cover_pk = value.cover_pk.clone();
|
||||
Self {
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
role: value.role,
|
||||
persistent: value.persistent,
|
||||
cover_pk: cover_pk.clone(),
|
||||
cover_url: cover_pk.as_deref().map(cover_url_from_pk),
|
||||
track_count: value.track_count,
|
||||
max_size: value.max_size,
|
||||
default_ttl_secs: value.default_ttl.map(|ttl| ttl.as_secs()),
|
||||
@@ -466,10 +556,11 @@ impl From<PlaylistOverview> for PlaylistSummaryResponse {
|
||||
impl From<PlaylistSnapshot> for PlaylistDetailResponse {
|
||||
fn from(value: PlaylistSnapshot) -> Self {
|
||||
let summary = PlaylistSummaryResponse::from(value.overview);
|
||||
let audio_cache = crate::manager::audio_cache().ok();
|
||||
let tracks = value
|
||||
.tracks
|
||||
.iter()
|
||||
.map(playlist_track_to_response)
|
||||
.map(|track| playlist_track_to_response(track, audio_cache.as_ref()))
|
||||
.collect();
|
||||
Self { summary, tracks }
|
||||
}
|
||||
|
||||
@@ -68,12 +68,14 @@ impl ReadHandle {
|
||||
if let Some(persistence) = crate::manager::PlaylistManager().persistence() {
|
||||
let title = self.playlist.title().await;
|
||||
let role = self.playlist.role().await;
|
||||
let cover_pk = self.playlist.cover_pk().await;
|
||||
let core = self.playlist.core.read().await;
|
||||
let _ = persistence
|
||||
.save_playlist(
|
||||
&self.playlist.id,
|
||||
&title,
|
||||
&role,
|
||||
cover_pk.as_deref(),
|
||||
&core.config,
|
||||
&core.tracks,
|
||||
)
|
||||
|
||||
@@ -292,6 +292,23 @@ impl WriteHandle {
|
||||
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() {
|
||||
@@ -530,8 +547,17 @@ impl WriteHandle {
|
||||
let config = &core.config;
|
||||
let tracks = &core.tracks;
|
||||
|
||||
let cover_pk = self.playlist.cover_pk().await;
|
||||
|
||||
persistence
|
||||
.save_playlist(&self.playlist.id, &title, &role, config, tracks)
|
||||
.save_playlist(
|
||||
&self.playlist.id,
|
||||
&title,
|
||||
&role,
|
||||
cover_pk.as_deref(),
|
||||
config,
|
||||
tracks,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ pub struct PlaylistOverview {
|
||||
pub title: String,
|
||||
pub role: PlaylistRole,
|
||||
pub persistent: bool,
|
||||
pub cover_pk: Option<String>,
|
||||
pub track_count: usize,
|
||||
pub max_size: Option<usize>,
|
||||
pub default_ttl: Option<Duration>,
|
||||
@@ -184,6 +185,7 @@ impl PlaylistManager {
|
||||
PlaylistConfig::default(),
|
||||
true, // persistent
|
||||
role,
|
||||
None,
|
||||
));
|
||||
|
||||
// Acqu<71>rir le write lock
|
||||
@@ -199,9 +201,17 @@ impl PlaylistManager {
|
||||
if let Some(persistence) = &self.inner.persistence {
|
||||
let title = playlist.title().await;
|
||||
let role = playlist.role().await;
|
||||
let cover_pk = playlist.cover_pk().await;
|
||||
let core = playlist.core.read().await;
|
||||
persistence
|
||||
.save_playlist(&playlist.id, &title, &role, &core.config, &core.tracks)
|
||||
.save_playlist(
|
||||
&playlist.id,
|
||||
&title,
|
||||
&role,
|
||||
cover_pk.as_deref(),
|
||||
&core.config,
|
||||
&core.tracks,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -436,6 +446,7 @@ impl PlaylistManager {
|
||||
PlaylistConfig::default(),
|
||||
false, // éphémère
|
||||
PlaylistRole::User,
|
||||
None,
|
||||
));
|
||||
|
||||
let write_token = playlist
|
||||
@@ -471,12 +482,20 @@ impl PlaylistManager {
|
||||
|
||||
// Pas en mémoire, essayer de charger depuis la DB
|
||||
if let Some(persistence) = &self.inner.persistence {
|
||||
if let Some((title, role, config, tracks)) = persistence.load_playlist(&id).await? {
|
||||
if let Some((title, role, config, cover_pk, 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, role));
|
||||
let playlist = Arc::new(Playlist::new(
|
||||
id.clone(),
|
||||
title.clone(),
|
||||
config,
|
||||
true,
|
||||
role,
|
||||
cover_pk,
|
||||
));
|
||||
|
||||
// Restaurer les tracks
|
||||
{
|
||||
@@ -519,7 +538,9 @@ impl PlaylistManager {
|
||||
|
||||
// Pas en m<>moire, essayer de ressusciter depuis la DB
|
||||
if let Some(persistence) = &self.inner.persistence {
|
||||
if let Some((title, role, config, tracks)) = persistence.load_playlist(id).await? {
|
||||
if let Some((title, role, config, cover_pk, tracks)) =
|
||||
persistence.load_playlist(id).await?
|
||||
{
|
||||
// Reconstruire la playlist
|
||||
let mut playlists = self.inner.playlists.write().await;
|
||||
|
||||
@@ -529,6 +550,7 @@ impl PlaylistManager {
|
||||
config,
|
||||
true,
|
||||
role,
|
||||
cover_pk,
|
||||
));
|
||||
|
||||
// Restaurer les tracks
|
||||
@@ -587,6 +609,7 @@ impl PlaylistManager {
|
||||
let playlist = self.ensure_playlist_loaded(id).await?;
|
||||
let title = playlist.title().await;
|
||||
let role = playlist.role().await;
|
||||
let cover_pk = playlist.cover_pk().await;
|
||||
let persistent = playlist.persistent;
|
||||
let last_change = playlist.last_change().await;
|
||||
let core = playlist.core.read().await;
|
||||
@@ -598,6 +621,7 @@ impl PlaylistManager {
|
||||
title,
|
||||
role,
|
||||
persistent,
|
||||
cover_pk,
|
||||
track_count,
|
||||
max_size: config.max_size,
|
||||
default_ttl: config.default_ttl,
|
||||
@@ -916,9 +940,17 @@ impl PlaylistManager {
|
||||
if let Some(persistence) = &self.inner.persistence {
|
||||
let title = playlist.title().await;
|
||||
let role = playlist.role().await;
|
||||
let cover_pk = playlist.cover_pk().await;
|
||||
let core = playlist.core.read().await;
|
||||
let _ = persistence
|
||||
.save_playlist(&playlist.id, &title, &role, &core.config, &core.tracks)
|
||||
.save_playlist(
|
||||
&playlist.id,
|
||||
&title,
|
||||
&role,
|
||||
cover_pk.as_deref(),
|
||||
&core.config,
|
||||
&core.tracks,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ impl PersistenceManager {
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
cover_pk TEXT,
|
||||
max_size INTEGER,
|
||||
default_ttl_secs INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
@@ -84,6 +85,7 @@ impl PersistenceManager {
|
||||
id: &str,
|
||||
title: &str,
|
||||
role: &PlaylistRole,
|
||||
cover_pk: Option<&str>,
|
||||
config: &PlaylistConfig,
|
||||
tracks: &VecDeque<Arc<Record>>,
|
||||
) -> Result<()> {
|
||||
@@ -96,19 +98,21 @@ impl PersistenceManager {
|
||||
|
||||
// Upsert playlist metadata
|
||||
conn.execute(
|
||||
"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)",
|
||||
"INSERT OR REPLACE INTO playlists (id, title, role, cover_pk, max_size, default_ttl_secs, created_at, last_modified)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6,
|
||||
COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?7),
|
||||
?7)",
|
||||
params![
|
||||
id,
|
||||
title,
|
||||
role.as_str(),
|
||||
cover_pk,
|
||||
config.max_size.map(|s| s as i64),
|
||||
config.default_ttl.map(|d| d.as_secs() as i64),
|
||||
now_nanos,
|
||||
],
|
||||
).map_err(|e| crate::Error::PersistenceError(format!("Failed to save playlist: {}", e)))?;
|
||||
)
|
||||
.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])
|
||||
@@ -140,21 +144,31 @@ impl PersistenceManager {
|
||||
pub async fn load_playlist(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Option<(String, PlaylistRole, PlaylistConfig, VecDeque<Arc<Record>>)>> {
|
||||
) -> Result<
|
||||
Option<(
|
||||
String,
|
||||
PlaylistRole,
|
||||
PlaylistConfig,
|
||||
Option<String>,
|
||||
VecDeque<Arc<Record>>,
|
||||
)>,
|
||||
> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
// Charger les métadonnées
|
||||
let mut stmt = conn
|
||||
.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 mut stmt = conn.prepare(
|
||||
"SELECT title, role, cover_pk, 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 role_raw: String = row.get(1)?;
|
||||
let max_size: Option<i64> = row.get(2)?;
|
||||
let default_ttl_secs: Option<i64> = row.get(3)?;
|
||||
let cover_pk: Option<String> = row.get(2)?;
|
||||
let max_size: Option<i64> = row.get(3)?;
|
||||
let default_ttl_secs: Option<i64> = row.get(4)?;
|
||||
|
||||
Ok((
|
||||
title,
|
||||
@@ -164,10 +178,11 @@ impl PersistenceManager {
|
||||
max_size: max_size.map(|s| s as usize),
|
||||
default_ttl: default_ttl_secs.map(|s| Duration::from_secs(s as u64)),
|
||||
},
|
||||
cover_pk,
|
||||
))
|
||||
});
|
||||
|
||||
let (title, role, config) = match result {
|
||||
let (title, role, config, cover_pk) = match result {
|
||||
Ok(data) => data,
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||
Err(e) => {
|
||||
@@ -210,7 +225,7 @@ impl PersistenceManager {
|
||||
tracks.push_back(Arc::new(record));
|
||||
}
|
||||
|
||||
Ok(Some((title, role, config, tracks)))
|
||||
Ok(Some((title, role, config, cover_pk, tracks)))
|
||||
}
|
||||
|
||||
/// Supprime une playlist
|
||||
|
||||
@@ -34,6 +34,7 @@ pub struct Playlist {
|
||||
pub id: String,
|
||||
title: RwLock<String>,
|
||||
role: RwLock<PlaylistRole>,
|
||||
cover_pk: RwLock<Option<String>>,
|
||||
state: Arc<AtomicU8>,
|
||||
pub core: Arc<RwLock<PlaylistCore>>,
|
||||
pub persistent: bool,
|
||||
@@ -49,11 +50,13 @@ impl Playlist {
|
||||
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),
|
||||
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
|
||||
core: Arc::new(RwLock::new(PlaylistCore::new(config))),
|
||||
persistent,
|
||||
@@ -100,6 +103,17 @@ impl Playlist {
|
||||
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;
|
||||
}
|
||||
|
||||
/// Timestamp du dernier changement
|
||||
pub async fn last_change(&self) -> SystemTime {
|
||||
*self.last_change.read().await
|
||||
|
||||
Reference in New Issue
Block a user