debugage lazy cache
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3281,6 +3281,7 @@ name = "pmocache"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"axum 0.8.7",
|
"axum 0.8.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3603,6 +3604,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"pmoaudiocache",
|
"pmoaudiocache",
|
||||||
|
"pmocache",
|
||||||
"pmoconfig",
|
"pmoconfig",
|
||||||
"pmocovers",
|
"pmocovers",
|
||||||
"pmodidl",
|
"pmodidl",
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Enregistrer les sources musicales
|
// Enregistrer les sources musicales
|
||||||
info!("🎵 Registering music sources...");
|
info!("🎵 Registering music sources...");
|
||||||
|
|
||||||
// // Enregistrer Qobuz
|
// Enregistrer Qobuz pour activer les lazy providers (QOBUZ:PK)
|
||||||
// if let Err(e) = server.write().await.register_qobuz().await {
|
if let Err(e) = server.write().await.register_qobuz().await {
|
||||||
// tracing::warn!("⚠️ Failed to register Qobuz: {}", e);
|
tracing::warn!("⚠️ Failed to register Qobuz source: {}", e);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Initialiser les canaux de streaming Radio Paradise (pipelines + routes HTTP)
|
// Initialiser les canaux de streaming Radio Paradise (pipelines + routes HTTP)
|
||||||
info!("📻 Initializing Radio Paradise streaming channels...");
|
info!("📻 Initializing Radio Paradise streaming channels...");
|
||||||
|
|||||||
@@ -87,7 +87,13 @@
|
|||||||
<div v-else class="music-icon">🎵</div>
|
<div v-else class="music-icon">🎵</div>
|
||||||
<div class="track-overlay">
|
<div class="track-overlay">
|
||||||
<span class="hits" v-if="!isLazyTrack(track)">{{ track.hits }} plays</span>
|
<span class="hits" v-if="!isLazyTrack(track)">{{ track.hits }} plays</span>
|
||||||
<span class="hits lazy" v-else>Lazy</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="hits lazy"
|
||||||
|
:class="lazyProviderClass(track)"
|
||||||
|
>
|
||||||
|
{{ lazyBadgeLabel(track) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
@@ -102,7 +108,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pk">
|
<div class="pk">
|
||||||
{{ track.pk }}
|
{{ track.pk }}
|
||||||
<span v-if="isLazyTrack(track)" class="lazy-tag">lazy</span>
|
<span
|
||||||
|
v-if="isLazyTrack(track)"
|
||||||
|
class="lazy-tag"
|
||||||
|
:class="lazyProviderClass(track)"
|
||||||
|
>
|
||||||
|
<span class="lazy-label">Lazy</span>
|
||||||
|
<span class="lazy-provider-name" v-if="lazyProviderName(track)">
|
||||||
|
{{ lazyProviderName(track) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span v-if="durationMs(track) !== undefined">
|
<span v-if="durationMs(track) !== undefined">
|
||||||
@@ -125,7 +140,9 @@
|
|||||||
Last used: {{ formatDate(track.last_used) }}
|
Last used: {{ formatDate(track.last_used) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="lazy-warning" v-if="isLazyTrack(track)">
|
<div class="lazy-warning" v-if="isLazyTrack(track)">
|
||||||
Audio not downloaded yet. First playback (or forcing download) will fetch it automatically.
|
Audio not downloaded yet
|
||||||
|
<span v-if="lazyProviderName(track)">({{ lazyProviderName(track) }} provider)</span>.
|
||||||
|
First playback (or forcing download) will fetch it automatically.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-actions">
|
<div class="track-actions">
|
||||||
@@ -279,13 +296,30 @@ import {
|
|||||||
getCoverUrl,
|
getCoverUrl,
|
||||||
} from "../services/audioCache";
|
} from "../services/audioCache";
|
||||||
|
|
||||||
|
interface LazyDisplayInfo {
|
||||||
|
prefix: string;
|
||||||
|
display: string;
|
||||||
|
className: string;
|
||||||
|
isLegacy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAZY_PROVIDER_LABELS: Record<string, string> = {
|
||||||
|
QOBUZ: "Qobuz",
|
||||||
|
};
|
||||||
|
const LEGACY_LAZY_INFO: LazyDisplayInfo = {
|
||||||
|
prefix: "legacy",
|
||||||
|
display: "Legacy",
|
||||||
|
className: "lazy-provider-legacy",
|
||||||
|
isLegacy: true,
|
||||||
|
};
|
||||||
|
|
||||||
// --- États ---
|
// --- États ---
|
||||||
const tracks = ref<AudioCacheEntry[]>([]);
|
const tracks = ref<AudioCacheEntry[]>([]);
|
||||||
const selectedTrack = ref<AudioCacheEntry | null>(null);
|
const selectedTrack = ref<AudioCacheEntry | null>(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const sortBy = ref<"hits" | "last_used" | "recent">("hits");
|
const sortBy = ref<"hits" | "last_used" | "recent">("hits");
|
||||||
const audioPlayer = ref<HTMLAudioElement | null>(null);
|
const audioPlayer = ref<HTMLAudioElement | null>(null);
|
||||||
const LAZY_PREFIX = "L:";
|
const LEGACY_LAZY_PREFIX = "L:";
|
||||||
|
|
||||||
// Formulaire d'ajout
|
// Formulaire d'ajout
|
||||||
const newTrackUrl = ref("");
|
const newTrackUrl = ref("");
|
||||||
@@ -531,13 +565,73 @@ function handleCoverError(pk: string) {
|
|||||||
failedCovers.value.add(pk);
|
failedCovers.value.add(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prettifyLazyProvider(prefix: string): string {
|
||||||
|
if (LAZY_PROVIDER_LABELS[prefix]) {
|
||||||
|
return LAZY_PROVIDER_LABELS[prefix];
|
||||||
|
}
|
||||||
|
const normalized = prefix.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return prefix.trim().toUpperCase() || "Lazy";
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLazyClass(prefix: string): string {
|
||||||
|
return `lazy-provider-${prefix.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLazyInfoFromPk(pk: string | undefined | null): LazyDisplayInfo | undefined {
|
||||||
|
if (!pk) return undefined;
|
||||||
|
if (pk.startsWith(LEGACY_LAZY_PREFIX)) {
|
||||||
|
return LEGACY_LAZY_INFO;
|
||||||
|
}
|
||||||
|
const separatorIndex = pk.indexOf(":");
|
||||||
|
if (separatorIndex <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const prefix = pk.slice(0, separatorIndex);
|
||||||
|
if (!prefix) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
prefix,
|
||||||
|
display: prettifyLazyProvider(prefix),
|
||||||
|
className: buildLazyClass(prefix),
|
||||||
|
isLegacy: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLazyInfo(track: AudioCacheEntry | null | undefined): LazyDisplayInfo | undefined {
|
||||||
|
if (!track?.pk) return undefined;
|
||||||
|
return extractLazyInfoFromPk(track.pk);
|
||||||
|
}
|
||||||
|
|
||||||
function isLazyTrack(track: AudioCacheEntry | null | undefined): boolean {
|
function isLazyTrack(track: AudioCacheEntry | null | undefined): boolean {
|
||||||
return !!track?.pk && track.pk.startsWith(LAZY_PREFIX);
|
return !!getLazyInfo(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lazyProviderName(track: AudioCacheEntry | null | undefined): string | undefined {
|
||||||
|
return getLazyInfo(track)?.display;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lazyProviderClass(track: AudioCacheEntry | null | undefined): string {
|
||||||
|
return getLazyInfo(track)?.className ?? "lazy-provider-generic";
|
||||||
|
}
|
||||||
|
|
||||||
|
function lazyBadgeLabel(track: AudioCacheEntry | null | undefined): string {
|
||||||
|
const info = getLazyInfo(track);
|
||||||
|
if (!info) return "";
|
||||||
|
return info.isLegacy ? "Lazy" : `Lazy - ${info.display}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackStatusLabel(track: AudioCacheEntry | null): string {
|
function trackStatusLabel(track: AudioCacheEntry | null): string {
|
||||||
if (isLazyTrack(track)) {
|
const info = getLazyInfo(track);
|
||||||
return "Lazy (audio pending download)";
|
if (info) {
|
||||||
|
const provider = info.isLegacy ? "" : ` - ${info.display}`;
|
||||||
|
return `Lazy${provider} (audio pending download)`;
|
||||||
}
|
}
|
||||||
return "Cached";
|
return "Cached";
|
||||||
}
|
}
|
||||||
@@ -822,11 +916,14 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-overlay .hits.lazy {
|
.track-overlay .hits.lazy {
|
||||||
background: rgba(255, 152, 0, 0.85);
|
display: inline-flex;
|
||||||
color: #000;
|
align-items: center;
|
||||||
|
background: rgba(156, 39, 176, 0.85);
|
||||||
|
color: #fff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.6rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-info {
|
.track-info {
|
||||||
@@ -869,15 +966,30 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lazy-tag {
|
.lazy-tag {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
padding: 0.1rem 0.4rem;
|
padding: 0.15rem 0.6rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #ff9800;
|
background: rgba(156, 39, 176, 0.25);
|
||||||
color: #000;
|
color: #f5f5f5;
|
||||||
font-size: 0.65rem;
|
font-size: 0.7rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
|
border: 1px solid rgba(156, 39, 176, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-tag .lazy-label {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-tag .lazy-provider-name {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
@@ -911,6 +1023,27 @@ button:disabled {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-overlay .hits.lazy.lazy-provider-legacy,
|
||||||
|
.lazy-tag.lazy-provider-legacy {
|
||||||
|
background: rgba(255, 152, 0, 0.85);
|
||||||
|
color: #000;
|
||||||
|
border-color: rgba(255, 193, 7, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-overlay .hits.lazy.lazy-provider-qobuz,
|
||||||
|
.lazy-tag.lazy-provider-qobuz {
|
||||||
|
background: rgba(76, 175, 80, 0.9);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(165, 214, 167, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-overlay .hits.lazy.lazy-provider-generic,
|
||||||
|
.lazy-tag.lazy-provider-generic {
|
||||||
|
background: rgba(103, 58, 183, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(179, 157, 219, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.track-actions {
|
.track-actions {
|
||||||
padding: 0 1rem 1rem;
|
padding: 0 1rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use crate::metadata_ext::AudioTrackMetadataExt;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use pmocache::download::TransformMetadata;
|
use pmocache::download::TransformMetadata;
|
||||||
use pmocache::CacheConfig;
|
use pmocache::CacheConfig;
|
||||||
use serde_json::Value;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Configuration pour le cache audio
|
/// Configuration pour le cache audio
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ hex = "0.4"
|
|||||||
|
|
||||||
# Utilitaires
|
# Utilitaires
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
async-trait = "0.1"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
@@ -8,17 +8,23 @@ use crate::db::DB;
|
|||||||
use crate::download::{
|
use crate::download::{
|
||||||
download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
|
download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
|
||||||
};
|
};
|
||||||
|
use crate::lazy::{lazy_prefix_from_pk, LazyEntryRemoteData, LazyProvider};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use serde_json::{Number, Value};
|
use serde_json::{Number, Value};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, RwLock as StdRwLock};
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||||
use tokio::sync::{broadcast, RwLock};
|
use tokio::sync::{broadcast, RwLock};
|
||||||
use tracing;
|
use tracing;
|
||||||
|
|
||||||
|
enum FinalizeMode<'a> {
|
||||||
|
InsertNew,
|
||||||
|
ConvertLazy { lazy_pk: &'a str },
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LAZY PK SUPPORT
|
// LAZY PK SUPPORT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -41,7 +47,11 @@ pub fn generate_lazy_pk(url: &str) -> String {
|
|||||||
|
|
||||||
/// Vérifie si un PK est en mode lazy
|
/// Vérifie si un PK est en mode lazy
|
||||||
pub fn is_lazy_pk(pk: &str) -> bool {
|
pub fn is_lazy_pk(pk: &str) -> bool {
|
||||||
pk.starts_with(LAZY_PK_PREFIX)
|
if pk.starts_with(LAZY_PK_PREFIX) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_prefix_from_pk(pk).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Events émis par le cache pour notifier les changements d'état
|
/// Events émis par le cache pour notifier les changements d'état
|
||||||
@@ -136,6 +146,8 @@ pub struct Cache<C: CacheConfig> {
|
|||||||
min_prebuffer_size: u64,
|
min_prebuffer_size: u64,
|
||||||
/// LAZY PK SUPPORT: Channel pour broadcaster les events (lazy downloads, etc.)
|
/// LAZY PK SUPPORT: Channel pour broadcaster les events (lazy downloads, etc.)
|
||||||
served_tx: Option<broadcast::Sender<CacheEvent>>,
|
served_tx: Option<broadcast::Sender<CacheEvent>>,
|
||||||
|
/// Providers responsables de préfixes lazy spécifiques
|
||||||
|
lazy_providers: StdRwLock<HashMap<String, Arc<dyn LazyProvider>>>,
|
||||||
/// Phantom data pour le type de configuration
|
/// Phantom data pour le type de configuration
|
||||||
_phantom: std::marker::PhantomData<C>,
|
_phantom: std::marker::PhantomData<C>,
|
||||||
}
|
}
|
||||||
@@ -242,6 +254,7 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
download: Arc<Download>,
|
download: Arc<Download>,
|
||||||
collection: Option<&str>,
|
collection: Option<&str>,
|
||||||
origin_url: Option<&str>,
|
origin_url: Option<&str>,
|
||||||
|
mode: FinalizeMode<'_>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// Attendre le prébuffering (pour le cache progressif)
|
// Attendre le prébuffering (pour le cache progressif)
|
||||||
if self.min_prebuffer_size > 0 {
|
if self.min_prebuffer_size > 0 {
|
||||||
@@ -256,10 +269,17 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter à la DB une fois le prébuffer terminé
|
// Ajouter ou commuter la DB selon le mode
|
||||||
self.db.add(pk, None, collection)?;
|
match mode {
|
||||||
if let Some(url) = origin_url {
|
FinalizeMode::InsertNew => {
|
||||||
self.db.set_origin_url(pk, url)?;
|
self.db.add(pk, None, collection)?;
|
||||||
|
if let Some(url) = origin_url {
|
||||||
|
self.db.set_origin_url(pk, url)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FinalizeMode::ConvertLazy { lazy_pk } => {
|
||||||
|
self.db.update_lazy_to_downloaded(lazy_pk, pk)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sauvegarder les métadonnées techniques du transformer
|
// Sauvegarder les métadonnées techniques du transformer
|
||||||
@@ -400,6 +420,7 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
transformer_factory,
|
transformer_factory,
|
||||||
min_prebuffer_size: DEFAULT_PREBUFFER_SIZE,
|
min_prebuffer_size: DEFAULT_PREBUFFER_SIZE,
|
||||||
served_tx: Some(served_tx),
|
served_tx: Some(served_tx),
|
||||||
|
lazy_providers: StdRwLock::new(HashMap::new()),
|
||||||
_phantom: std::marker::PhantomData,
|
_phantom: std::marker::PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -432,6 +453,34 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
self.min_prebuffer_size
|
self.min_prebuffer_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enregistre un provider responsable d'un préfixe de lazy PK.
|
||||||
|
pub fn register_lazy_provider(&self, provider: Arc<dyn LazyProvider>) {
|
||||||
|
let prefix = provider.lazy_prefix().to_string();
|
||||||
|
let mut guard = self
|
||||||
|
.lazy_providers
|
||||||
|
.write()
|
||||||
|
.expect("lazy provider registry poisoned");
|
||||||
|
guard.insert(prefix, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Désenregistre un provider à partir de son préfixe.
|
||||||
|
pub fn unregister_lazy_provider(&self, prefix: &str) {
|
||||||
|
let mut guard = self
|
||||||
|
.lazy_providers
|
||||||
|
.write()
|
||||||
|
.expect("lazy provider registry poisoned");
|
||||||
|
guard.remove(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_for_lazy_pk(&self, lazy_pk: &str) -> Option<Arc<dyn LazyProvider>> {
|
||||||
|
let prefix = lazy_prefix_from_pk(lazy_pk)?;
|
||||||
|
let guard = self
|
||||||
|
.lazy_providers
|
||||||
|
.read()
|
||||||
|
.expect("lazy provider registry poisoned");
|
||||||
|
guard.get(prefix).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// S'abonne aux diffusions HTTP pour un `pk` donné.
|
/// S'abonne aux diffusions HTTP pour un `pk` donné.
|
||||||
///
|
///
|
||||||
/// La callback est appelée à chaque fois qu'un élément est servi avec succès via les routes
|
/// La callback est appelée à chaque fois qu'un élément est servi avec succès via les routes
|
||||||
@@ -611,10 +660,63 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finaliser avec prébuffering et nettoyage
|
// Finaliser avec prébuffering et nettoyage
|
||||||
self.finalize_download(&pk, download, collection, Some(url))
|
self.finalize_download(&pk, download, collection, Some(url), FinalizeMode::InsertNew)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Télécharge un fichier lazy et commute l'entrée existante
|
||||||
|
pub async fn download_lazy_from_url(
|
||||||
|
&self,
|
||||||
|
lazy_pk: &str,
|
||||||
|
url: &str,
|
||||||
|
collection: Option<&str>,
|
||||||
|
) -> Result<String> {
|
||||||
|
// Si déjà converti, retourner directement
|
||||||
|
if let Ok(Some(real_pk)) = self.db.get_pk_by_lazy_pk(lazy_pk) {
|
||||||
|
return Ok(real_pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si l'URL pointe déjà vers un fichier complet, commuter sans re-télécharger
|
||||||
|
if let Ok(Some(existing_pk)) = self.db.get_pk_by_origin_url(url) {
|
||||||
|
if existing_pk != lazy_pk && self.check_cached_and_complete(&existing_pk).await? {
|
||||||
|
self.db.update_lazy_to_downloaded(lazy_pk, &existing_pk)?;
|
||||||
|
return Ok(existing_pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Télécharger les 2048 premiers octets pour calculer le pk
|
||||||
|
let header = crate::download::peek_header(url, 2048)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Failed to peek header: {}", e))?;
|
||||||
|
let pk = crate::cache_trait::pk_from_content_header(&header);
|
||||||
|
|
||||||
|
// 2. Si déjà en cache (complet), commuter directement
|
||||||
|
if self.check_cached_and_complete(&pk).await? {
|
||||||
|
self.db.update_lazy_to_downloaded(lazy_pk, &pk)?;
|
||||||
|
return Ok(pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Lancer le téléchargement complet
|
||||||
|
tracing::debug!("Starting lazy download for pk {} (lazy {})", pk, lazy_pk);
|
||||||
|
let file_path = self.get_file_path(&pk);
|
||||||
|
let transformer = self.transformer_factory.as_ref().map(|f| f());
|
||||||
|
let download = download_with_transformer(&file_path, url, transformer);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut downloads = self.downloads.write().await;
|
||||||
|
downloads.insert(pk.clone(), download.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.finalize_download(
|
||||||
|
&pk,
|
||||||
|
download,
|
||||||
|
collection,
|
||||||
|
Some(url),
|
||||||
|
FinalizeMode::ConvertLazy { lazy_pk },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Ajoute un fichier à partir d'un flux asynchrone.
|
/// Ajoute un fichier à partir d'un flux asynchrone.
|
||||||
///
|
///
|
||||||
/// Cette méthode utilise le même système d'identifiants basé sur le contenu que `add_from_url`.
|
/// Cette méthode utilise le même système d'identifiants basé sur le contenu que `add_from_url`.
|
||||||
@@ -725,7 +827,7 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finaliser avec prébuffering et nettoyage
|
// Finaliser avec prébuffering et nettoyage
|
||||||
self.finalize_download(&pk, download, collection, source_uri)
|
self.finalize_download(&pk, download, collection, source_uri, FinalizeMode::InsertNew)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,71 +1399,95 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ajoute une URL en mode deferred (pas de download immédiat)
|
/// Garantit l'existence d'une entrée lazy spécifique.
|
||||||
///
|
pub async fn ensure_lazy_entry(
|
||||||
/// Vérifie d'abord si l'URL existe déjà en DB :
|
&self,
|
||||||
/// - Si eager (déjà téléchargé) : retourne le lazy_pk si existe, sinon le real pk
|
lazy_pk: &str,
|
||||||
/// - Si lazy (pas encore téléchargé) : retourne le lazy pk existant
|
collection: Option<&str>,
|
||||||
/// - Sinon : crée nouvelle entry lazy
|
origin_url: Option<&str>,
|
||||||
///
|
) -> Result<()> {
|
||||||
/// # Arguments
|
if let Ok(true) = self.db.has_lazy_entry(lazy_pk) {
|
||||||
///
|
self.db.update_hit_by_lazy_pk(lazy_pk)?;
|
||||||
/// * `url` - URL à cacher
|
} else {
|
||||||
/// * `collection` - Collection optionnelle
|
self.db.add_lazy(lazy_pk, None, collection)?;
|
||||||
///
|
}
|
||||||
/// # Returns
|
|
||||||
///
|
if let Some(url) = origin_url {
|
||||||
/// PK (lazy ou real selon l'état)
|
self.db.set_origin_url_for_lazy(lazy_pk, url)?;
|
||||||
///
|
}
|
||||||
/// # Example
|
|
||||||
///
|
Ok(())
|
||||||
/// ```rust,no_run
|
}
|
||||||
/// let cache = Cache::<AudioConfig>::new("./cache", 1000)?;
|
|
||||||
/// let lazy_pk = cache.add_from_url_deferred("https://example.com/track.mp3", Some("qobuz")).await?;
|
/// Récupère auprès du provider les métadonnées/couvertures associées.
|
||||||
/// // → Returns "L:abc123..." (lazy PK)
|
pub async fn fetch_lazy_provider_data(&self, lazy_pk: &str) -> Result<LazyEntryRemoteData> {
|
||||||
/// // Fichier pas encore téléchargé, juste métadonnées en DB
|
if let Some(provider) = self.provider_for_lazy_pk(lazy_pk) {
|
||||||
/// ```
|
let metadata = provider.metadata(lazy_pk).await?;
|
||||||
|
let cover_url = provider.cover_url(lazy_pk).await?;
|
||||||
|
Ok(LazyEntryRemoteData { metadata, cover_url })
|
||||||
|
} else {
|
||||||
|
Ok(LazyEntryRemoteData::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Résout l'URL d'origine pour un lazy PK, via la DB ou un provider.
|
||||||
|
pub async fn resolve_lazy_url(&self, lazy_pk: &str) -> Result<String> {
|
||||||
|
if let Ok(Some(url)) = self.db.get_origin_url(lazy_pk) {
|
||||||
|
return Ok(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(provider) = self.provider_for_lazy_pk(lazy_pk) {
|
||||||
|
return provider.get_url(lazy_pk).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("No origin URL or lazy provider registered for {}", lazy_pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Télécharge un lazy PK en résolvant automatiquement son URL.
|
||||||
|
pub async fn download_lazy(&self, lazy_pk: &str, collection: Option<&str>) -> Result<String> {
|
||||||
|
let (existing_pk, _lazy_sec, existing_collection) = self
|
||||||
|
.db
|
||||||
|
.get_entry_by_pk_or_lazy_pk(lazy_pk)?
|
||||||
|
.ok_or_else(|| anyhow!("Lazy pk {} not found in DB", lazy_pk))?;
|
||||||
|
|
||||||
|
let url = self.resolve_lazy_url(lazy_pk).await?;
|
||||||
|
let collection = collection.or(existing_collection.as_deref());
|
||||||
|
|
||||||
|
if let Some(real_pk) = existing_pk {
|
||||||
|
if real_pk != lazy_pk && self.check_cached_and_complete(&real_pk).await? {
|
||||||
|
return Ok(real_pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.download_lazy_from_url(lazy_pk, &url, collection).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajoute une URL sans lancer immédiatement le téléchargement.
|
||||||
pub async fn add_from_url_deferred(
|
pub async fn add_from_url_deferred(
|
||||||
&self,
|
&self,
|
||||||
url: &str,
|
url: &str,
|
||||||
collection: Option<&str>,
|
collection: Option<&str>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// 1. Vérifier si URL déjà en cache
|
|
||||||
if let Ok(Some((pk_opt, lazy_pk_opt))) = self.db.get_entry_by_url(url) {
|
if let Ok(Some((pk_opt, lazy_pk_opt))) = self.db.get_entry_by_url(url) {
|
||||||
// URL existe déjà
|
|
||||||
if let Some(pk) = pk_opt {
|
if let Some(pk) = pk_opt {
|
||||||
// Fichier déjà téléchargé (eager ou lazy→eager)
|
|
||||||
tracing::debug!("URL {} already downloaded with pk {}", url, pk);
|
|
||||||
self.db.update_hit(&pk)?;
|
self.db.update_hit(&pk)?;
|
||||||
|
|
||||||
// Retourner lazy_pk si existe (pour compatibilité Control Point)
|
|
||||||
// sinon retourner pk
|
|
||||||
if let Some(lpk) = lazy_pk_opt {
|
if let Some(lpk) = lazy_pk_opt {
|
||||||
return Ok(lpk);
|
return Ok(lpk);
|
||||||
}
|
}
|
||||||
return Ok(pk);
|
return Ok(pk);
|
||||||
} else if let Some(lpk) = lazy_pk_opt {
|
} else if let Some(lpk) = lazy_pk_opt {
|
||||||
// Entry lazy existante (pas encore téléchargé)
|
|
||||||
tracing::debug!("URL {} already in lazy mode with pk {}", url, lpk);
|
|
||||||
self.db.update_hit_by_lazy_pk(&lpk)?;
|
self.db.update_hit_by_lazy_pk(&lpk)?;
|
||||||
return Ok(lpk);
|
return Ok(lpk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. URL inconnue → créer nouvelle entry lazy
|
let lazy_pk = format!("L:{}", generate_lazy_pk(url));
|
||||||
let lazy_pk = generate_lazy_pk(url);
|
|
||||||
|
|
||||||
// Vérifier si ce lazy_pk existe déjà (collision improbable mais...)
|
|
||||||
if let Ok(true) = self.db.has_lazy_entry(&lazy_pk) {
|
if let Ok(true) = self.db.has_lazy_entry(&lazy_pk) {
|
||||||
bail!("Lazy PK collision for URL: {}", url);
|
bail!("Lazy PK collision for URL: {}", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Ajouter en DB
|
self.ensure_lazy_entry(&lazy_pk, collection, Some(url)).await?;
|
||||||
self.db.add_lazy(&lazy_pk, None, collection)?;
|
|
||||||
self.db.set_origin_url_for_lazy(&lazy_pk, url)?;
|
|
||||||
|
|
||||||
tracing::debug!("Created new lazy pk {} for URL {}", lazy_pk, url);
|
tracing::debug!("Created new lazy pk {} for URL {}", lazy_pk, url);
|
||||||
|
|
||||||
Ok(lazy_pk)
|
Ok(lazy_pk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ impl DB {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn init(path: &Path) -> Result<Self, rusqlite::Error> {
|
pub fn init(path: &Path) -> Result<Self, rusqlite::Error> {
|
||||||
let conn = Connection::open(path)?;
|
let conn = Connection::open(path)?;
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON", [])?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS asset (
|
"CREATE TABLE IF NOT EXISTS asset (
|
||||||
@@ -130,9 +131,10 @@ impl DB {
|
|||||||
value_type TEXT NOT NULL CHECK (value_type IN ('string','number','boolean','null')),
|
value_type TEXT NOT NULL CHECK (value_type IN ('string','number','boolean','null')),
|
||||||
value TEXT,
|
value TEXT,
|
||||||
PRIMARY KEY (pk, key),
|
PRIMARY KEY (pk, key),
|
||||||
FOREIGN KEY (pk) REFERENCES asset (pk) ON DELETE CASCADE
|
FOREIGN KEY (pk) REFERENCES asset (pk) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
)"
|
)",
|
||||||
, [])?;
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
// Créer un index sur la collection pour les requêtes rapides
|
// Créer un index sur la collection pour les requêtes rapides
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -856,11 +858,9 @@ impl DB {
|
|||||||
/// * `real_pk` - Le real PK calculé après téléchargement
|
/// * `real_pk` - Le real PK calculé après téléchargement
|
||||||
pub fn update_lazy_to_downloaded(&self, lazy_pk: &str, real_pk: &str) -> rusqlite::Result<()> {
|
pub fn update_lazy_to_downloaded(&self, lazy_pk: &str, real_pk: &str) -> rusqlite::Result<()> {
|
||||||
let mut conn = self.lock_conn("update_lazy_to_downloaded");
|
let mut conn = self.lock_conn("update_lazy_to_downloaded");
|
||||||
|
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
|
|
||||||
// 1. Récupérer l'entry lazy (pk = lazy_pk tant que pas téléchargé)
|
let (current_pk, collection, id, hits): (String, Option<String>, Option<String>, i32) = tx
|
||||||
let (old_pk, collection, id, hits): (String, Option<String>, Option<String>, i32) = tx
|
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT pk, collection, id, hits FROM asset WHERE lazy_pk = ?1",
|
"SELECT pk, collection, id, hits FROM asset WHERE lazy_pk = ?1",
|
||||||
[lazy_pk],
|
[lazy_pk],
|
||||||
@@ -869,35 +869,40 @@ impl DB {
|
|||||||
.optional()?
|
.optional()?
|
||||||
.ok_or_else(|| Error::QueryReturnedNoRows)?;
|
.ok_or_else(|| Error::QueryReturnedNoRows)?;
|
||||||
|
|
||||||
if old_pk == real_pk {
|
if current_pk == real_pk {
|
||||||
// Rien à faire si déjà commuté
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Utc::now().to_rfc3339();
|
let now = Utc::now().to_rfc3339();
|
||||||
let hits_to_add = if hits > 0 { hits } else { 1 };
|
let hits_to_add = if hits > 0 { hits } else { 1 };
|
||||||
|
|
||||||
// 2. Créer/mettre à jour l'entry avec le real pk
|
// Supprimer d'éventuelles métadonnées résiduelles associées au futur pk réel
|
||||||
tx.execute(
|
// (peut arriver si un ancien téléchargement a laissé des traces sans asset correspondant).
|
||||||
"INSERT INTO asset (pk, lazy_pk, collection, id, hits, last_used)
|
tx.execute("DELETE FROM metadata WHERE pk = ?1", [real_pk])?;
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
|
||||||
ON CONFLICT(pk) DO UPDATE SET
|
let updated = tx.execute(
|
||||||
lazy_pk = excluded.lazy_pk,
|
"UPDATE asset
|
||||||
collection = COALESCE(excluded.collection, collection),
|
SET pk = ?1,
|
||||||
id = COALESCE(excluded.id, id),
|
lazy_pk = ?2,
|
||||||
hits = hits + excluded.hits,
|
collection = COALESCE(?3, collection),
|
||||||
last_used = excluded.last_used",
|
id = COALESCE(?4, id),
|
||||||
params![real_pk, lazy_pk, collection, id, hits_to_add, now],
|
hits = hits + ?5,
|
||||||
|
last_used = ?6
|
||||||
|
WHERE lazy_pk = ?7",
|
||||||
|
params![
|
||||||
|
real_pk,
|
||||||
|
lazy_pk,
|
||||||
|
collection,
|
||||||
|
id,
|
||||||
|
hits_to_add,
|
||||||
|
now,
|
||||||
|
lazy_pk
|
||||||
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 3. Re-pointer les métadonnées vers le real pk
|
if updated == 0 {
|
||||||
tx.execute(
|
return Err(Error::QueryReturnedNoRows);
|
||||||
"UPDATE metadata SET pk = ?1 WHERE pk = ?2",
|
}
|
||||||
params![real_pk, old_pk],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 4. Supprimer l'ancienne entry lazy
|
|
||||||
tx.execute("DELETE FROM asset WHERE pk = ?1", [old_pk])?;
|
|
||||||
|
|
||||||
tx.commit()
|
tx.commit()
|
||||||
}
|
}
|
||||||
@@ -952,6 +957,20 @@ impl DB {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retourne une entry à partir d'un pk ou lazy_pk.
|
||||||
|
pub fn get_entry_by_pk_or_lazy_pk(
|
||||||
|
&self,
|
||||||
|
value: &str,
|
||||||
|
) -> rusqlite::Result<Option<(Option<String>, Option<String>, Option<String>)>> {
|
||||||
|
let conn = self.lock_conn("get_entry_by_pk_or_lazy_pk");
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT pk, lazy_pk, collection FROM asset WHERE pk = ?1 OR lazy_pk = ?1 LIMIT 1",
|
||||||
|
[value],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
/// Met à jour le compteur d'accès pour une entry lazy (pk = NULL)
|
/// Met à jour le compteur d'accès pour une entry lazy (pk = NULL)
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
42
pmocache/src/lazy.rs
Normal file
42
pmocache/src/lazy.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// Retourne le préfixe d'un lazy PK (`PREFIX:VALUE`)
|
||||||
|
pub fn lazy_prefix_from_pk(lazy_pk: &str) -> Option<&str> {
|
||||||
|
lazy_pk.split_once(':').map(|(prefix, _)| prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Données optionnelles pouvant être fournies par un [`LazyProvider`]
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct LazyEntryRemoteData {
|
||||||
|
pub metadata: Option<Value>,
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait générique décrivant un fournisseur de lazy PK.
|
||||||
|
///
|
||||||
|
/// Chaque implémentation est responsable d'un préfixe particulier (ex: `QOBUZ`).
|
||||||
|
/// Lorsque le cache rencontre un lazy PK dont le préfixe correspond,
|
||||||
|
/// il délègue au provider pour résoudre l'URL et récupérer les informations
|
||||||
|
/// nécessaires (métadonnées, couverture, etc.).
|
||||||
|
#[async_trait]
|
||||||
|
pub trait LazyProvider: Send + Sync {
|
||||||
|
/// Préfixe associé (sans le `:` final).
|
||||||
|
fn lazy_prefix(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Retourne l'URL de téléchargement actuelle pour ce lazy PK.
|
||||||
|
async fn get_url(&self, lazy_pk: &str) -> Result<String>;
|
||||||
|
|
||||||
|
/// Métadonnées optionnelles à associer immédiatement à l'entrée lazy.
|
||||||
|
async fn metadata(&self, lazy_pk: &str) -> Result<Option<Value>> {
|
||||||
|
let _ = lazy_pk;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL de couverture éventuelle pour permettre un cache eager des jaquettes.
|
||||||
|
async fn cover_url(&self, lazy_pk: &str) -> Result<Option<String>> {
|
||||||
|
let _ = lazy_pk;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,7 @@ pub mod cache;
|
|||||||
pub mod cache_trait;
|
pub mod cache_trait;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
|
pub mod lazy;
|
||||||
pub mod metadata_macros;
|
pub mod metadata_macros;
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
@@ -134,6 +135,7 @@ pub use cache::{
|
|||||||
CacheSubscription,
|
CacheSubscription,
|
||||||
};
|
};
|
||||||
pub use cache_trait::{pk_from_content_header, FileCache};
|
pub use cache_trait::{pk_from_content_header, FileCache};
|
||||||
|
pub use lazy::{lazy_prefix_from_pk, LazyEntryRemoteData, LazyProvider};
|
||||||
pub use db::{CacheEntry, DB};
|
pub use db::{CacheEntry, DB};
|
||||||
pub use download::{
|
pub use download::{
|
||||||
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
||||||
|
|||||||
@@ -110,15 +110,38 @@ async fn get_file_with_param<C: CacheConfig + 'static>(
|
|||||||
serve_file_with_streaming(&cache, &pk, ¶m, content_type, param_generator).await
|
serve_file_with_streaming(&cache, &pk, ¶m, content_type, param_generator).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "pmoserver")]
|
||||||
|
async fn serve_finalized_pk<C: CacheConfig>(
|
||||||
|
cache: &Arc<Cache<C>>,
|
||||||
|
pk: &str,
|
||||||
|
param: &str,
|
||||||
|
content_type: &'static str,
|
||||||
|
) -> Response {
|
||||||
|
let qualifier = param.to_string();
|
||||||
|
let file_path = cache.get_file_path_with_qualifier(pk, param);
|
||||||
|
|
||||||
|
if let Err(e) = cache.db.update_hit(pk) {
|
||||||
|
warn!("Error updating hit count for {}: {}", pk, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = serve_complete_file(file_path, content_type).await;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
cache.notify_broadcast(pk, &qualifier).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
/// Handler spécifique pour les lazy PK
|
/// Handler spécifique pour les lazy PK
|
||||||
///
|
///
|
||||||
/// Gère le téléchargement on-demand des fichiers lazy :
|
/// Gère le téléchargement on-demand des fichiers lazy :
|
||||||
/// 1. Fast path : vérifie si déjà téléchargé
|
/// 1. Fast path : vérifie si déjà téléchargé
|
||||||
/// 2. Récupère l'origin_url depuis la DB
|
/// 2. Résout l'URL via la DB ou un provider
|
||||||
/// 3. Lance le téléchargement et calcule le real pk
|
/// 3. Lance le téléchargement et calcule le real pk
|
||||||
/// 4. Met à jour la DB (lazy → downloaded)
|
/// 4. Met à jour la DB (lazy → downloaded)
|
||||||
/// 5. Broadcast l'event pour PK switching
|
/// 5. Broadcast l'event pour PK switching
|
||||||
/// 6. Redirige vers le real pk
|
/// 6. Sert directement le fichier téléchargé
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
async fn serve_lazy_audio_file<C: CacheConfig>(
|
async fn serve_lazy_audio_file<C: CacheConfig>(
|
||||||
cache: &Arc<Cache<C>>,
|
cache: &Arc<Cache<C>>,
|
||||||
@@ -126,54 +149,20 @@ async fn serve_lazy_audio_file<C: CacheConfig>(
|
|||||||
param: &str,
|
param: &str,
|
||||||
content_type: &'static str,
|
content_type: &'static str,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
use axum::response::Redirect;
|
|
||||||
|
|
||||||
tracing::info!("Lazy download triggered for pk: {}", lazy_pk);
|
tracing::info!("Lazy download triggered for pk: {}", lazy_pk);
|
||||||
|
|
||||||
// 1. Vérifier si déjà téléchargé (fast path)
|
// 1. Vérifier si déjà téléchargé (fast path)
|
||||||
if let Ok(Some(real_pk)) = cache.db.get_pk_by_lazy_pk(lazy_pk) {
|
if let Ok(Some(real_pk)) = cache.db.get_pk_by_lazy_pk(lazy_pk) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Lazy PK {} already downloaded as {}, redirecting",
|
"Lazy PK {} already downloaded as {}, serving immediately",
|
||||||
lazy_pk,
|
lazy_pk,
|
||||||
real_pk
|
real_pk
|
||||||
);
|
);
|
||||||
|
return serve_finalized_pk(cache, &real_pk, param, content_type).await;
|
||||||
// Construire l'URL de redirection
|
|
||||||
let redirect_url = if param == C::default_param() {
|
|
||||||
format!(
|
|
||||||
"/{}/{}/{}",
|
|
||||||
C::cache_name(),
|
|
||||||
C::cache_type(),
|
|
||||||
real_pk
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"/{}/{}/{}/{}",
|
|
||||||
C::cache_name(),
|
|
||||||
C::cache_type(),
|
|
||||||
real_pk,
|
|
||||||
param
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
return Redirect::temporary(&redirect_url).into_response();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Récupérer origin_url
|
// 2. Télécharger en résolvant l'URL via la DB ou un provider
|
||||||
let origin_url = match cache.db.get_origin_url(lazy_pk) {
|
let real_pk = match cache.download_lazy(lazy_pk, None).await {
|
||||||
Ok(Some(url)) => url,
|
|
||||||
Ok(None) => {
|
|
||||||
tracing::error!("Lazy PK {} has no origin_url", lazy_pk);
|
|
||||||
return (StatusCode::NOT_FOUND, "Origin URL not found").into_response();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Error getting origin_url for {}: {}", lazy_pk, e);
|
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 3. Lancer download complet (cela calcule le VRAI pk basé sur content[512:2048])
|
|
||||||
let real_pk = match cache.add_from_url(&origin_url, None).await {
|
|
||||||
Ok(pk) => pk,
|
Ok(pk) => pk,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to download lazy file: {}", e);
|
tracing::error!("Failed to download lazy file: {}", e);
|
||||||
@@ -185,46 +174,11 @@ async fn serve_lazy_audio_file<C: CacheConfig>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Mettre à jour DB : lazy_pk → real_pk mapping
|
// 4. Broadcast event pour prefetch ET commutation PK
|
||||||
if let Err(e) = cache.db.update_lazy_to_downloaded(lazy_pk, &real_pk) {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to update DB for lazy transition {} → {}: {}",
|
|
||||||
lazy_pk,
|
|
||||||
real_pk,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
// Continuer quand même, le fichier est téléchargé
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Broadcast event pour prefetch ET commutation PK
|
|
||||||
cache.broadcast_lazy_downloaded(lazy_pk, &real_pk).await;
|
cache.broadcast_lazy_downloaded(lazy_pk, &real_pk).await;
|
||||||
|
|
||||||
// 6. Rediriger vers la vraie URL
|
// 5. Servir directement le fichier téléchargé
|
||||||
let redirect_url = if param == C::default_param() {
|
serve_finalized_pk(cache, &real_pk, param, content_type).await
|
||||||
format!(
|
|
||||||
"/{}/{}/{}",
|
|
||||||
C::cache_name(),
|
|
||||||
C::cache_type(),
|
|
||||||
real_pk
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"/{}/{}/{}/{}",
|
|
||||||
C::cache_name(),
|
|
||||||
C::cache_type(),
|
|
||||||
real_pk,
|
|
||||||
param
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
"Lazy PK {} downloaded as {}, redirecting to {}",
|
|
||||||
lazy_pk,
|
|
||||||
real_pk,
|
|
||||||
redirect_url
|
|
||||||
);
|
|
||||||
|
|
||||||
Redirect::temporary(&redirect_url).into_response()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fonction utilitaire pour servir un fichier avec streaming progressif
|
/// Fonction utilitaire pour servir un fichier avec streaming progressif
|
||||||
|
|||||||
@@ -63,15 +63,17 @@ pmosource = { path = "../pmosource" }
|
|||||||
|
|
||||||
# Playlist management
|
# Playlist management
|
||||||
pmoplaylist = { path = "../pmoplaylist" }
|
pmoplaylist = { path = "../pmoplaylist" }
|
||||||
|
pmocache = { path = "../pmocache" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["async-trait-support"]
|
||||||
# Feature pour activer les extensions pmoserver
|
# Feature pour activer les extensions pmoserver
|
||||||
pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"]
|
pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"]
|
||||||
# Feature pour activer le support serveur (cache registry)
|
# Feature pour activer le support serveur (cache registry)
|
||||||
server = ["pmosource/server"]
|
server = ["pmosource/server"]
|
||||||
# Feature cache (deprecated - toujours actif maintenant)
|
# Feature cache (deprecated - toujours actif maintenant)
|
||||||
cache = []
|
cache = []
|
||||||
|
async-trait-support = ["dep:async-trait"]
|
||||||
disk-cache = ["dep:rusqlite", "dep:async-trait"]
|
disk-cache = ["dep:rusqlite", "dep:async-trait"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@@ -81,7 +83,6 @@ mockito = "1.0"
|
|||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
# Pour les exemples
|
# Pour les exemples
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
pmocache = { path = "../pmocache" }
|
|
||||||
# Pour l'exemple spoofer
|
# Pour l'exemple spoofer
|
||||||
|
|
||||||
# Specify that the with_cache example requires the cache feature
|
# Specify that the with_cache example requires the cache feature
|
||||||
|
|||||||
91
pmoqobuz/src/lazy_provider.rs
Normal file
91
pmoqobuz/src/lazy_provider.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use crate::client::QobuzClient;
|
||||||
|
use crate::models::Track;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use pmoaudiocache::AudioMetadata;
|
||||||
|
use pmocache::lazy::LazyProvider;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct QobuzLazyProvider {
|
||||||
|
client: Arc<QobuzClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QobuzLazyProvider {
|
||||||
|
pub fn new(client: Arc<QobuzClient>) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_id_from_lazy<'a>(&self, lazy_pk: &'a str) -> Result<&'a str> {
|
||||||
|
match lazy_pk.split_once(':') {
|
||||||
|
Some((prefix, value)) if prefix.eq_ignore_ascii_case("QOBUZ") => Ok(value),
|
||||||
|
_ => Err(anyhow!("Invalid Qobuz lazy pk {}", lazy_pk)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_track(&self, lazy_pk: &str) -> Result<Track> {
|
||||||
|
let track_id = self.track_id_from_lazy(lazy_pk)?;
|
||||||
|
self.client
|
||||||
|
.get_track(track_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Failed to fetch track {}: {}", track_id, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_metadata(track: &Track) -> AudioMetadata {
|
||||||
|
AudioMetadata {
|
||||||
|
title: Some(track.title.clone()),
|
||||||
|
artist: track.performer.as_ref().map(|p| p.name.clone()),
|
||||||
|
album: track.album.as_ref().map(|a| a.title.clone()),
|
||||||
|
duration_secs: Some(track.duration as u64),
|
||||||
|
year: track.album.as_ref().and_then(|a| {
|
||||||
|
a.release_date
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.split('-').next()?.parse().ok())
|
||||||
|
}),
|
||||||
|
track_number: Some(track.track_number),
|
||||||
|
track_total: track.album.as_ref().and_then(|a| a.tracks_count),
|
||||||
|
disc_number: Some(track.media_number),
|
||||||
|
disc_total: None,
|
||||||
|
genre: track.album.as_ref().and_then(|a| {
|
||||||
|
if !a.genres.is_empty() {
|
||||||
|
Some(a.genres.join(", "))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
sample_rate: track.sample_rate,
|
||||||
|
channels: track.channels,
|
||||||
|
bitrate: None,
|
||||||
|
conversion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl LazyProvider for QobuzLazyProvider {
|
||||||
|
fn lazy_prefix(&self) -> &'static str {
|
||||||
|
"QOBUZ"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_url(&self, lazy_pk: &str) -> Result<String> {
|
||||||
|
let track_id = self.track_id_from_lazy(lazy_pk)?;
|
||||||
|
self.client
|
||||||
|
.get_stream_url(track_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Failed to resolve stream URL for {}: {}", track_id, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn metadata(&self, lazy_pk: &str) -> Result<Option<Value>> {
|
||||||
|
let track = self.fetch_track(lazy_pk).await?;
|
||||||
|
let metadata = Self::build_metadata(&track);
|
||||||
|
let value = serde_json::to_value(metadata)?;
|
||||||
|
Ok(Some(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cover_url(&self, lazy_pk: &str) -> Result<Option<String>> {
|
||||||
|
let track = self.fetch_track(lazy_pk).await?;
|
||||||
|
Ok(track
|
||||||
|
.album
|
||||||
|
.and_then(|a| a.image)
|
||||||
|
.and_then(|url| if url.is_empty() { None } else { Some(url) }))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -214,6 +214,7 @@ pub mod didl;
|
|||||||
#[cfg(feature = "disk-cache")]
|
#[cfg(feature = "disk-cache")]
|
||||||
pub mod disk_cache;
|
pub mod disk_cache;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
mod lazy_provider;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
use crate::client::QobuzClient;
|
use crate::client::QobuzClient;
|
||||||
use crate::didl::ToDIDL;
|
use crate::didl::ToDIDL;
|
||||||
|
use crate::lazy_provider::QobuzLazyProvider;
|
||||||
use crate::models::Track;
|
use crate::models::Track;
|
||||||
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
@@ -71,7 +72,7 @@ pub struct QobuzSource {
|
|||||||
|
|
||||||
struct QobuzSourceInner {
|
struct QobuzSourceInner {
|
||||||
/// Qobuz API client
|
/// Qobuz API client
|
||||||
client: QobuzClient,
|
client: Arc<QobuzClient>,
|
||||||
|
|
||||||
/// Cache manager (centralisé)
|
/// Cache manager (centralisé)
|
||||||
cache_manager: SourceCacheManager,
|
cache_manager: SourceCacheManager,
|
||||||
@@ -103,6 +104,8 @@ impl QobuzSource {
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn from_registry(client: QobuzClient) -> Result<Self> {
|
pub fn from_registry(client: QobuzClient) -> Result<Self> {
|
||||||
let cache_manager = SourceCacheManager::from_registry("qobuz".to_string())?;
|
let cache_manager = SourceCacheManager::from_registry("qobuz".to_string())?;
|
||||||
|
let client = Arc::new(client);
|
||||||
|
cache_manager.register_lazy_provider(Arc::new(QobuzLazyProvider::new(client.clone())));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: Arc::new(QobuzSourceInner {
|
inner: Arc::new(QobuzSourceInner {
|
||||||
@@ -127,6 +130,8 @@ impl QobuzSource {
|
|||||||
audio_cache: Arc<AudioCache>,
|
audio_cache: Arc<AudioCache>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let cache_manager = SourceCacheManager::new("qobuz".to_string(), cover_cache, audio_cache);
|
let cache_manager = SourceCacheManager::new("qobuz".to_string(), cover_cache, audio_cache);
|
||||||
|
let client = Arc::new(client);
|
||||||
|
cache_manager.register_lazy_provider(Arc::new(QobuzLazyProvider::new(client.clone())));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(QobuzSourceInner {
|
inner: Arc::new(QobuzSourceInner {
|
||||||
@@ -243,7 +248,9 @@ impl QobuzSource {
|
|||||||
pub async fn add_track_lazy(&self, track: &Track) -> Result<(String, String)> {
|
pub async fn add_track_lazy(&self, track: &Track) -> Result<(String, String)> {
|
||||||
let track_id = format!("qobuz://track/{}", track.id);
|
let track_id = format!("qobuz://track/{}", track.id);
|
||||||
|
|
||||||
// Get streaming URL
|
let lazy_pk = format!("QOBUZ:{}", track.id);
|
||||||
|
|
||||||
|
// Get streaming URL (for metadata fallback)
|
||||||
let stream_url = self
|
let stream_url = self
|
||||||
.inner
|
.inner
|
||||||
.client
|
.client
|
||||||
@@ -290,15 +297,19 @@ impl QobuzSource {
|
|||||||
conversion: None,
|
conversion: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Cache audio LAZILY (KEY CHANGE: use cache_audio_lazy)
|
// 3. Cache audio LAZILY avec un provider
|
||||||
let cached_audio_pk = self
|
let cached_audio_pk = self
|
||||||
.inner
|
.inner
|
||||||
.cache_manager
|
.cache_manager
|
||||||
.cache_audio_lazy(&stream_url, Some(metadata))
|
.cache_audio_lazy_with_provider(
|
||||||
|
&lazy_pk,
|
||||||
|
Some(metadata.clone()),
|
||||||
|
cached_cover_pk.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
MusicSourceError::CacheError(format!(
|
MusicSourceError::CacheError(format!(
|
||||||
"Failed to cache lazy track {}: {}",
|
"Failed to register lazy track {}: {}",
|
||||||
track.title, e
|
track.title, e
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
Binary file not shown.
@@ -52,7 +52,7 @@ def main():
|
|||||||
mime_type = file_url_data.get('mime_type', '')
|
mime_type = file_url_data.get('mime_type', '')
|
||||||
|
|
||||||
print(f" ✓ Success!")
|
print(f" ✓ Success!")
|
||||||
print(f" URL: {url[:80]}...")
|
print(f" URL: {url}...")
|
||||||
print(f" MIME type: {mime_type}")
|
print(f" MIME type: {mime_type}")
|
||||||
|
|
||||||
print("\n=== Test completed successfully! ===")
|
print("\n=== Test completed successfully! ===")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pmoplaylist = { path = "../pmoplaylist" }
|
|||||||
# Optional cache integrations
|
# Optional cache integrations
|
||||||
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
|
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
|
||||||
pmocovers = { path = "../pmocovers", optional = true }
|
pmocovers = { path = "../pmocovers", optional = true }
|
||||||
|
pmocache = { path = "../pmocache", optional = true }
|
||||||
|
|
||||||
# Serialization (required for cache metadata)
|
# Serialization (required for cache metadata)
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -49,5 +50,5 @@ futures = { version = "0.3", optional = true }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cache"]
|
default = ["cache"]
|
||||||
cache = ["pmoaudiocache", "pmocovers"]
|
cache = ["pmoaudiocache", "pmocovers", "pmocache"]
|
||||||
server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "utoipa", "tracing", "lazy_static", "tokio-stream", "futures"]
|
server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "utoipa", "tracing", "lazy_static", "tokio-stream", "futures"]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
use crate::{CacheStatus, MusicSourceError, Result};
|
use crate::{CacheStatus, MusicSourceError, Result};
|
||||||
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
||||||
|
use pmocache::lazy::LazyProvider;
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
use serde_json::{json, Value as JsonValue};
|
use serde_json::{json, Value as JsonValue};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -130,6 +131,11 @@ impl SourceCacheManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enregistre un provider lazy supplémentaire auprès du cache audio.
|
||||||
|
pub fn register_lazy_provider(&self, provider: Arc<dyn LazyProvider>) {
|
||||||
|
self.audio_cache.register_lazy_provider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
/// Résoudre l'URI d'une piste (priorité au cache)
|
/// Résoudre l'URI d'une piste (priorité au cache)
|
||||||
///
|
///
|
||||||
/// Retourne l'URI du fichier audio en cache si disponible,
|
/// Retourne l'URI du fichier audio en cache si disponible,
|
||||||
@@ -259,21 +265,79 @@ impl SourceCacheManager {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| MusicSourceError::CacheError(e.to_string()))?;
|
.map_err(|e| MusicSourceError::CacheError(e.to_string()))?;
|
||||||
|
|
||||||
// Store metadata in lazy_pk metadata table if provided
|
// Seed individual metadata entries directly (no redundant JSON blob)
|
||||||
if let Some(meta) = metadata {
|
if let Some(meta) = metadata {
|
||||||
// Serialize metadata to JSON and store
|
|
||||||
if let Ok(json) = serde_json::to_value(&meta) {
|
|
||||||
let _ = self
|
|
||||||
.audio_cache
|
|
||||||
.db
|
|
||||||
.set_a_metadata_by_key(&lazy_pk, "audio_metadata", json);
|
|
||||||
}
|
|
||||||
self.seed_audio_metadata(&lazy_pk, &meta);
|
self.seed_audio_metadata(&lazy_pk, &meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(lazy_pk)
|
Ok(lazy_pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crée une entrée lazy basée sur un provider enregistré.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `lazy_pk` - Identifiant logique (préfixe provider + valeur)
|
||||||
|
/// * `metadata` - Métadonnées optionnelles connues à l'avance
|
||||||
|
/// * `cover_pk_hint` - PK éventuel d'une cover déjà cachée
|
||||||
|
pub async fn cache_audio_lazy_with_provider(
|
||||||
|
&self,
|
||||||
|
lazy_pk: &str,
|
||||||
|
metadata: Option<AudioMetadata>,
|
||||||
|
cover_pk_hint: Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
self.audio_cache
|
||||||
|
.ensure_lazy_entry(lazy_pk, Some(&self.collection_id), None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| MusicSourceError::CacheError(e.to_string()))?;
|
||||||
|
|
||||||
|
let needs_provider_metadata = metadata.is_none();
|
||||||
|
let needs_provider_cover = cover_pk_hint.is_none();
|
||||||
|
|
||||||
|
let provider_data = if needs_provider_metadata || needs_provider_cover {
|
||||||
|
Some(
|
||||||
|
self.audio_cache
|
||||||
|
.fetch_lazy_provider_data(lazy_pk)
|
||||||
|
.await
|
||||||
|
.map_err(|e| MusicSourceError::CacheError(e.to_string()))?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut final_metadata = metadata;
|
||||||
|
if final_metadata.is_none() {
|
||||||
|
if let Some(data) = provider_data.as_ref() {
|
||||||
|
if let Some(value) = data.metadata.as_ref() {
|
||||||
|
if let Ok(meta) = serde_json::from_value::<AudioMetadata>(value.clone()) {
|
||||||
|
final_metadata = Some(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut final_cover_pk = cover_pk_hint;
|
||||||
|
if final_cover_pk.is_none() {
|
||||||
|
if let Some(data) = provider_data.as_ref() {
|
||||||
|
if let Some(url) = data.cover_url.as_ref() {
|
||||||
|
if let Ok(pk) = self.cache_cover(url).await {
|
||||||
|
final_cover_pk = Some(pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(meta) = final_metadata.as_ref() {
|
||||||
|
self.seed_audio_metadata(lazy_pk, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cover_pk) = final_cover_pk {
|
||||||
|
let _ = self.set_audio_metadata(lazy_pk, "cover_pk", json!(cover_pk));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lazy_pk.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Cache un flux audio via un reader asynchrone
|
/// Cache un flux audio via un reader asynchrone
|
||||||
pub async fn cache_audio_from_reader<R>(
|
pub async fn cache_audio_from_reader<R>(
|
||||||
&self,
|
&self,
|
||||||
@@ -323,7 +387,7 @@ impl SourceCacheManager {
|
|||||||
/// Pré-remplit les métadonnées audio pour une entrée lazy
|
/// Pré-remplit les métadonnées audio pour une entrée lazy
|
||||||
fn seed_audio_metadata(&self, audio_pk: &str, metadata: &AudioMetadata) {
|
fn seed_audio_metadata(&self, audio_pk: &str, metadata: &AudioMetadata) {
|
||||||
let audio_pk = audio_pk.to_string();
|
let audio_pk = audio_pk.to_string();
|
||||||
let mut store = |key: &str, value: JsonValue| {
|
let store = |key: &str, value: JsonValue| {
|
||||||
if let Err(e) = self.audio_cache.db.set_a_metadata(&audio_pk, key, value) {
|
if let Err(e) = self.audio_cache.db.set_a_metadata(&audio_pk, key, value) {
|
||||||
log_metadata_warning(key, &audio_pk, &e.to_string());
|
log_metadata_warning(key, &audio_pk, &e.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user