feat: add Qobuz playlist caching with versioning and infinite scroll UI
- 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
This commit is contained in:
@@ -32,6 +32,8 @@ pub(crate) struct AlbumResponse {
|
||||
#[serde(default)]
|
||||
release_date_original: Option<String>,
|
||||
#[serde(default)]
|
||||
released_at: Option<i64>,
|
||||
#[serde(default)]
|
||||
image: Option<ImageResponse>,
|
||||
#[serde(default = "default_streamable")]
|
||||
streamable: bool,
|
||||
@@ -120,6 +122,8 @@ pub(crate) struct PlaylistResponse {
|
||||
#[serde(default)]
|
||||
owner: Option<OwnerResponse>,
|
||||
#[serde(default)]
|
||||
updated_at: Option<i64>,
|
||||
#[serde(default)]
|
||||
tracks: Option<PaginatedResponse<TrackResponse>>,
|
||||
}
|
||||
|
||||
@@ -317,6 +321,16 @@ impl QobuzApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Récupère les métadonnées d'une playlist sans les tracks (appel léger)
|
||||
///
|
||||
/// Utile pour vérifier `updated_at` avant de décider si le cache est valide.
|
||||
pub async fn get_playlist_metadata(&self, playlist_id: &str) -> Result<Playlist> {
|
||||
debug!("Fetching playlist metadata {}", playlist_id);
|
||||
let params = [("playlist_id", playlist_id)];
|
||||
let response: PlaylistResponse = self.get("/playlist/get", ¶ms).await?;
|
||||
Ok(Self::parse_playlist(response))
|
||||
}
|
||||
|
||||
/// Récupère les détails d'une playlist
|
||||
pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
|
||||
debug!("Fetching playlist {}", playlist_id);
|
||||
@@ -328,18 +342,36 @@ impl QobuzApi {
|
||||
/// Récupère les tracks d'une playlist
|
||||
pub async fn get_playlist_tracks(&self, playlist_id: &str) -> Result<Vec<Track>> {
|
||||
debug!("Fetching tracks for playlist {}", playlist_id);
|
||||
let params = [("playlist_id", playlist_id), ("extra", "tracks")];
|
||||
let response: PlaylistResponse = self.get("/playlist/get", ¶ms).await?;
|
||||
const PAGE_SIZE: u32 = 50;
|
||||
let mut all_tracks = Vec::new();
|
||||
let mut offset = 0u32;
|
||||
|
||||
if let Some(tracks) = response.tracks {
|
||||
Ok(tracks
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|t| Self::parse_track(t, None))
|
||||
.collect())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
loop {
|
||||
let offset_str = offset.to_string();
|
||||
let limit_str = PAGE_SIZE.to_string();
|
||||
let params = [
|
||||
("playlist_id", playlist_id),
|
||||
("extra", "tracks"),
|
||||
("offset", offset_str.as_str()),
|
||||
("limit", limit_str.as_str()),
|
||||
];
|
||||
let response: PlaylistResponse = self.get("/playlist/get", ¶ms).await?;
|
||||
|
||||
if let Some(tracks) = response.tracks {
|
||||
let total = tracks.total.unwrap_or(0);
|
||||
let count = tracks.items.len() as u32;
|
||||
all_tracks.extend(tracks.items.into_iter().map(|t| Self::parse_track(t, None)));
|
||||
offset += count;
|
||||
if count == 0 || offset >= total {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Fetched {} tracks total for playlist {}", all_tracks.len(), playlist_id);
|
||||
Ok(all_tracks)
|
||||
}
|
||||
|
||||
/// Récupère la liste des genres
|
||||
@@ -493,6 +525,7 @@ impl QobuzApi {
|
||||
tracks_count: response.tracks_count,
|
||||
duration: response.duration,
|
||||
release_date: response.release_date_original,
|
||||
released_at: response.released_at,
|
||||
image: response.image.and_then(|i| i.large),
|
||||
image_cached: None,
|
||||
streamable: response.streamable,
|
||||
@@ -547,6 +580,7 @@ impl QobuzApi {
|
||||
image: response.images300.and_then(|imgs| imgs.first().cloned()),
|
||||
image_cached: None,
|
||||
is_public: response.is_public,
|
||||
updated_at: response.updated_at,
|
||||
owner: response.owner.map(|o| PlaylistOwner {
|
||||
id: o.id.parse().unwrap_or(0),
|
||||
name: o.name,
|
||||
|
||||
@@ -17,6 +17,49 @@ use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
/// Si la variable d'environnement `QOBUZ_DEBUG_DIR` est définie, sauvegarde
|
||||
/// le JSON brut de chaque réponse API dans ce dossier.
|
||||
fn debug_save_response(endpoint: &str, params: &[(&str, &str)], text: &str) {
|
||||
let Ok(dir) = std::env::var("QOBUZ_DEBUG_DIR") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dir = std::path::Path::new(&dir);
|
||||
if let Err(e) = std::fs::create_dir_all(dir) {
|
||||
debug!("QOBUZ_DEBUG_DIR: cannot create dir: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Construire un nom de fichier lisible : endpoint + params clés
|
||||
let endpoint_slug = endpoint.trim_start_matches('/').replace('/', "_");
|
||||
let param_slug: String = params
|
||||
.iter()
|
||||
.filter(|(k, _)| !["app_id", "user_auth_token", "request_ts", "request_sig"].contains(k))
|
||||
.map(|(k, v)| format!("{}-{}", k, v.chars().take(20).collect::<String>()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("_");
|
||||
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
|
||||
let filename = format!("{}_{}_{}.json", endpoint_slug, param_slug, ts);
|
||||
let path = dir.join(&filename);
|
||||
|
||||
// Pretty-print si possible
|
||||
let content = serde_json::from_str::<Value>(text)
|
||||
.ok()
|
||||
.and_then(|v| serde_json::to_string_pretty(&v).ok())
|
||||
.unwrap_or_else(|| text.to_string());
|
||||
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
debug!("QOBUZ_DEBUG_DIR: cannot write {}: {}", filename, e);
|
||||
} else {
|
||||
debug!("QOBUZ_DEBUG_DIR: saved {}", filename);
|
||||
}
|
||||
}
|
||||
|
||||
pub use spoofer::Spoofer;
|
||||
|
||||
/// URL de base de l'API Qobuz
|
||||
@@ -270,7 +313,7 @@ impl QobuzApi {
|
||||
|
||||
// Envoyer la requête
|
||||
let response = request.send().await?;
|
||||
self.handle_response(response, endpoint).await
|
||||
self.handle_response(response, endpoint, params).await
|
||||
}
|
||||
|
||||
/// Traite la réponse HTTP
|
||||
@@ -278,6 +321,7 @@ impl QobuzApi {
|
||||
&self,
|
||||
response: Response,
|
||||
endpoint: &str,
|
||||
params: &[(&str, &str)],
|
||||
) -> Result<T> {
|
||||
let status = response.status();
|
||||
let status_code = status.as_u16();
|
||||
@@ -294,6 +338,7 @@ impl QobuzApi {
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
debug_save_response(endpoint, params, &text);
|
||||
|
||||
// Vérifier si la réponse contient une erreur Qobuz
|
||||
if let Ok(json) = serde_json::from_str::<Value>(&text) {
|
||||
|
||||
@@ -54,6 +54,9 @@ pub struct Album {
|
||||
/// Date de sortie (format ISO 8601)
|
||||
#[serde(default)]
|
||||
pub release_date: Option<String>,
|
||||
/// Timestamp Unix de sortie (released_at)
|
||||
#[serde(default)]
|
||||
pub released_at: Option<i64>,
|
||||
/// URL de l'image de couverture
|
||||
#[serde(default)]
|
||||
pub image: Option<String>,
|
||||
@@ -141,6 +144,9 @@ pub struct Playlist {
|
||||
/// Indique si c'est une playlist publique
|
||||
#[serde(default)]
|
||||
pub is_public: bool,
|
||||
/// Timestamp de dernière modification (Unix epoch secondes)
|
||||
#[serde(default)]
|
||||
pub updated_at: Option<i64>,
|
||||
/// Propriétaire de la playlist
|
||||
#[serde(default)]
|
||||
pub owner: Option<PlaylistOwner>,
|
||||
|
||||
@@ -14,10 +14,9 @@ use pmosource::SourceCacheManager;
|
||||
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// TTL pour les playlists d'albums (7 jours)
|
||||
const ALBUM_PLAYLIST_TTL: Duration = Duration::from_secs(7 * 24 * 3600);
|
||||
|
||||
/// Trait pour les types dont on peut cacher la cover image.
|
||||
trait CoverCacheable {
|
||||
@@ -563,54 +562,41 @@ impl QobuzSource {
|
||||
}
|
||||
|
||||
/// Vérifie si une playlist d'album existe et est valide (non expirée ET non vide)
|
||||
async fn is_album_playlist_valid(&self, playlist_id: &str) -> Result<bool> {
|
||||
/// Retourne la source_version d'une pmoplaylist si elle existe en mémoire ou en DB.
|
||||
async fn cached_source_version(&self, playlist_id: &str) -> Option<String> {
|
||||
let playlist_manager = pmoplaylist::PlaylistManager();
|
||||
|
||||
if !playlist_manager.exists(playlist_id).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Vérifier l'âge
|
||||
match playlist_manager.get_playlist_age(playlist_id).await {
|
||||
Ok(Some(age)) if age < ALBUM_PLAYLIST_TTL => {
|
||||
// Playlist non expirée, vérifier qu'elle contient des tracks
|
||||
match playlist_manager.get_read_handle(playlist_id).await {
|
||||
Ok(reader) => {
|
||||
let count = reader.remaining().await.unwrap_or(0);
|
||||
Ok(count > 0) // Valide seulement si non vide
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
_ => Ok(false),
|
||||
return None;
|
||||
}
|
||||
playlist_manager
|
||||
.get_read_handle(playlist_id)
|
||||
.await
|
||||
.ok()?
|
||||
.source_version()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adapte les Items d'une playlist pour correspondre au schéma UPnP Qobuz
|
||||
async fn adapt_playlist_items_to_qobuz(
|
||||
/// Adapte des Items DIDL issus d'une pmoplaylist pour le schéma UPnP Qobuz.
|
||||
///
|
||||
/// - Résout l'ID de la track (`qobuz:track:<id>`) depuis les métadonnées cache
|
||||
/// - Rend absolues les URLs relatives (resource + cover)
|
||||
/// - Affecte le `parent_id` fourni
|
||||
async fn adapt_items_to_qobuz(
|
||||
&self,
|
||||
items: Vec<Item>,
|
||||
album_id: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<Vec<Item>> {
|
||||
use tracing::warn;
|
||||
|
||||
let parent_id = format!("qobuz:album:{}", album_id);
|
||||
|
||||
let mut adapted = Vec::with_capacity(items.len());
|
||||
|
||||
for mut item in items {
|
||||
// Extraire cache_pk depuis l'URL du resource
|
||||
let cache_pk = if let Some(resource) = item.resources.first() {
|
||||
resource
|
||||
.url
|
||||
.strip_prefix("/audio/flac/")
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cache_pk = item
|
||||
.resources
|
||||
.first()
|
||||
.and_then(|r| r.url.strip_prefix("/audio/flac/").map(|s| s.to_string()));
|
||||
|
||||
if let Some(pk) = cache_pk {
|
||||
// Récupérer track_id depuis metadata
|
||||
if let Ok(Some(track_id_value)) = self
|
||||
.inner
|
||||
.cache_manager
|
||||
@@ -625,9 +611,6 @@ impl QobuzSource {
|
||||
warn!("No qobuz_track_id metadata for {}", pk);
|
||||
}
|
||||
|
||||
// Convertir URL relative en URL absolue
|
||||
// From: /audio/flac/QOBUZ:123
|
||||
// To: http://192.168.0.138:8080/audio/flac/QOBUZ:123
|
||||
if let Some(resource) = item.resources.first_mut() {
|
||||
if resource.url.starts_with('/') {
|
||||
resource.url = format!("{}{}", self.inner.base_url, resource.url);
|
||||
@@ -635,21 +618,59 @@ impl QobuzSource {
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir l'URL de la cover en URL absolue si elle est relative
|
||||
if let Some(art) = item.album_art.as_mut() {
|
||||
if art.starts_with('/') {
|
||||
*art = format!("{}{}", self.inner.base_url, art);
|
||||
}
|
||||
}
|
||||
|
||||
item.parent_id = parent_id.clone();
|
||||
item.parent_id = parent_id.to_string();
|
||||
adapted.push(item);
|
||||
}
|
||||
|
||||
Ok(adapted)
|
||||
}
|
||||
|
||||
/// Récupère ou crée une playlist lazy pour un album
|
||||
/// Ouvre un WriteHandle sur une pmoplaylist persistante, en la créant si absente
|
||||
/// ou en la vidant (flush) si elle existe déjà.
|
||||
async fn get_or_flush_playlist_writer(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
role: pmoplaylist::PlaylistRole,
|
||||
) -> Result<pmoplaylist::WriteHandle> {
|
||||
let playlist_manager = pmoplaylist::PlaylistManager();
|
||||
|
||||
if playlist_manager.exists(playlist_id).await {
|
||||
let writer = playlist_manager
|
||||
.get_persistent_write_handle(playlist_id.to_string())
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
Ok(writer)
|
||||
} else {
|
||||
playlist_manager
|
||||
.create_persistent_playlist_with_role(playlist_id.to_string(), role)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Met en cache la cover si l'URL est présente.
|
||||
async fn cache_cover_opt(&self, image_url: Option<&str>) -> Option<String> {
|
||||
self.inner
|
||||
.cache_manager
|
||||
.cache_cover(image_url?)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Récupère ou crée une playlist lazy pour un album, avec invalidation par version.
|
||||
///
|
||||
/// `source_version` = `"{released_at}_{tracks_count}"` — change uniquement en cas de
|
||||
/// réédition avec pistes supplémentaires. Cache valide à vie si la version correspond.
|
||||
async fn get_or_create_album_playlist_items(
|
||||
&self,
|
||||
album_id: &str,
|
||||
@@ -659,30 +680,11 @@ impl QobuzSource {
|
||||
|
||||
let playlist_id = format!("qobuz-album-{}", album_id);
|
||||
let playlist_manager = pmoplaylist::PlaylistManager();
|
||||
let parent_id = format!("qobuz:album:{}", album_id);
|
||||
|
||||
// Vérifier validité (existe ET non expirée)
|
||||
let is_valid = self.is_album_playlist_valid(&playlist_id).await?;
|
||||
let cached_version = self.cached_source_version(&playlist_id).await;
|
||||
|
||||
if is_valid {
|
||||
debug!("Album playlist {} found and valid", playlist_id);
|
||||
|
||||
let reader = playlist_manager
|
||||
.get_read_handle(&playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
let items = reader
|
||||
.to_items(limit)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
return self.adapt_playlist_items_to_qobuz(items, album_id).await;
|
||||
}
|
||||
|
||||
// Playlist invalide/inexistante : (re)créer
|
||||
info!("Album playlist {} creating/refreshing", playlist_id);
|
||||
|
||||
// 1. Métadonnées album
|
||||
// Charger les métadonnées album (contient déjà les tracks)
|
||||
let album = self
|
||||
.inner
|
||||
.client
|
||||
@@ -690,47 +692,49 @@ impl QobuzSource {
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
// 2. Cache cover
|
||||
let cover_pk = if let Some(ref image_url) = album.image {
|
||||
self.inner.cache_manager.cache_cover(image_url).await.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let album_version = Some(format!(
|
||||
"{}_{}",
|
||||
album.released_at.unwrap_or(0),
|
||||
album.tracks_count.unwrap_or(0)
|
||||
));
|
||||
|
||||
// 3. Créer ou récupérer playlist
|
||||
let writer = if playlist_manager.exists(&playlist_id).await {
|
||||
let writer = playlist_manager
|
||||
.get_persistent_write_handle(playlist_id.clone())
|
||||
if cached_version.is_some() && cached_version == album_version {
|
||||
debug!("Album playlist {} cache valid (version {:?})", playlist_id, album_version);
|
||||
let reader = playlist_manager
|
||||
.get_read_handle(&playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
writer
|
||||
.flush()
|
||||
let items = reader
|
||||
.to_items(limit)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
return self.adapt_items_to_qobuz(items, &parent_id).await;
|
||||
}
|
||||
|
||||
writer
|
||||
} else {
|
||||
playlist_manager
|
||||
.create_persistent_playlist_with_role(
|
||||
playlist_id.clone(),
|
||||
pmoplaylist::PlaylistRole::Album,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?
|
||||
};
|
||||
info!("Album playlist {} creating/refreshing (version {:?})", playlist_id, album_version);
|
||||
|
||||
let cover_pk = self.cache_cover_opt(album.image.as_deref()).await;
|
||||
|
||||
let writer = self
|
||||
.get_or_flush_playlist_writer(&playlist_id, pmoplaylist::PlaylistRole::Album)
|
||||
.await?;
|
||||
|
||||
// 4. Métadonnées playlist
|
||||
writer
|
||||
.set_title(album.title.clone())
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
writer
|
||||
.set_artist(Some(album.artist.name.clone()))
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
writer
|
||||
.set_source(Some("qobuz".to_string()))
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
writer
|
||||
.set_source_version(album_version)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
if let Some(pk) = cover_pk {
|
||||
writer
|
||||
.set_cover_pk(Some(pk))
|
||||
@@ -741,22 +745,115 @@ impl QobuzSource {
|
||||
// IMPORTANT: Libérer le write lock avant d'appeler add_album_to_playlist
|
||||
drop(writer);
|
||||
|
||||
// 5. Ajouter tracks (réutilise add_album_to_playlist existant)
|
||||
self.add_album_to_playlist(&playlist_id, album_id).await?;
|
||||
|
||||
// 6. Récupérer items
|
||||
let reader = playlist_manager
|
||||
.get_read_handle(&playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
let items = reader
|
||||
.to_items(limit)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
// 7. Adapter IDs
|
||||
self.adapt_playlist_items_to_qobuz(items, album_id).await
|
||||
self.adapt_items_to_qobuz(items, &parent_id).await
|
||||
}
|
||||
|
||||
/// Récupère ou crée une pmoplaylist pour une playlist Qobuz, avec invalidation par updated_at.
|
||||
///
|
||||
/// - Si la playlist est en cache et que `updated_at` n'a pas changé → retourne le cache.
|
||||
/// - Sinon → recharge toutes les tracks depuis Qobuz et reconstruit la pmoplaylist.
|
||||
async fn get_or_create_qobuz_playlist_items(
|
||||
&self,
|
||||
qobuz_playlist_id: &str,
|
||||
) -> Result<Vec<Item>> {
|
||||
use tracing::{debug, info};
|
||||
|
||||
let pmo_playlist_id = format!("qobuz-playlist-{}", qobuz_playlist_id);
|
||||
let playlist_manager = pmoplaylist::PlaylistManager();
|
||||
|
||||
let cached_version = self.cached_source_version(&pmo_playlist_id).await;
|
||||
|
||||
// Obtenir les métadonnées depuis Qobuz pour comparer updated_at
|
||||
let qobuz_meta: crate::models::Playlist = self
|
||||
.inner
|
||||
.client
|
||||
.get_playlist(qobuz_playlist_id)
|
||||
.await
|
||||
.map_err(|e: crate::error::QobuzError| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let qobuz_version = qobuz_meta.updated_at.map(|t: i64| t.to_string());
|
||||
|
||||
// Cache valide si version correspond
|
||||
let cache_valid = cached_version.is_some() && cached_version == qobuz_version;
|
||||
|
||||
let parent_id = format!("qobuz:playlist:{}", qobuz_playlist_id);
|
||||
|
||||
if cache_valid {
|
||||
debug!("Qobuz playlist {} cache valid (version {:?})", pmo_playlist_id, qobuz_version);
|
||||
let reader = playlist_manager
|
||||
.get_read_handle(&pmo_playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
let (items, _total) = reader
|
||||
.to_items_paged(0, usize::MAX)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
return self.adapt_items_to_qobuz(items, &parent_id).await;
|
||||
}
|
||||
|
||||
info!("Qobuz playlist {} creating/refreshing (version {:?})", pmo_playlist_id, qobuz_version);
|
||||
|
||||
let tracks = self
|
||||
.inner
|
||||
.client
|
||||
.get_playlist_tracks(qobuz_playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let cover_pk = self.cache_cover_opt(qobuz_meta.image.as_deref()).await;
|
||||
|
||||
let writer = self
|
||||
.get_or_flush_playlist_writer(&pmo_playlist_id, pmoplaylist::PlaylistRole::Source)
|
||||
.await?;
|
||||
|
||||
writer
|
||||
.set_title(qobuz_meta.name.clone())
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
writer
|
||||
.set_source(Some("qobuz".to_string()))
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
writer
|
||||
.set_source_version(qobuz_version)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
if let Some(pk) = cover_pk {
|
||||
writer
|
||||
.set_cover_pk(Some(pk))
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
}
|
||||
|
||||
let tracks = self.cache_covers(tracks).await;
|
||||
let lazy_pks: Vec<String> = tracks.iter().map(|t| format!("QOBUZ:{}", t.id)).collect();
|
||||
writer
|
||||
.push_lazy_batch(lazy_pks)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
drop(writer);
|
||||
|
||||
let reader = playlist_manager
|
||||
.get_read_handle(&pmo_playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
let (items, _total) = reader
|
||||
.to_items_paged(0, usize::MAX)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
|
||||
|
||||
self.adapt_items_to_qobuz(items, &parent_id).await
|
||||
}
|
||||
|
||||
/// Increment update counter (called on catalog changes)
|
||||
@@ -1623,22 +1720,9 @@ impl MusicSource for QobuzSource {
|
||||
}
|
||||
|
||||
ObjectIdType::Playlist(playlist_id) => {
|
||||
let tracks = self
|
||||
.inner
|
||||
.client
|
||||
.get_playlist_tracks(&playlist_id)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let tracks = self.cache_covers(tracks).await;
|
||||
let items: Vec<Item> = tracks
|
||||
.into_iter()
|
||||
.filter_map(|track| {
|
||||
track
|
||||
.to_didl_item(&format!("qobuz:playlist:{}", playlist_id))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
let items = self
|
||||
.get_or_create_qobuz_playlist_items(&playlist_id)
|
||||
.await?;
|
||||
|
||||
Ok(BrowseResult::Items(items))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user