lazy cache webapp
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
<h2>🎵 Audio Cache Manager</h2>
|
<h2>🎵 Audio Cache Manager</h2>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<span>{{ tracks.length }} tracks</span>
|
<span>{{ tracks.length }} tracks</span>
|
||||||
|
<span v-if="lazyTracksCount > 0">{{ lazyTracksCount }} lazy pending</span>
|
||||||
<span v-if="totalHits > 0">{{ totalHits }} hits</span>
|
<span v-if="totalHits > 0">{{ totalHits }} hits</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +86,8 @@
|
|||||||
/>
|
/>
|
||||||
<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">{{ track.hits }} plays</span>
|
<span class="hits" v-if="!isLazyTrack(track)">{{ track.hits }} plays</span>
|
||||||
|
<span class="hits lazy" v-else>Lazy</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
@@ -98,7 +100,10 @@
|
|||||||
<div class="track-album" v-if="track.metadata?.album">
|
<div class="track-album" v-if="track.metadata?.album">
|
||||||
{{ track.metadata.album }}
|
{{ track.metadata.album }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pk">{{ track.pk }}</div>
|
<div class="pk">
|
||||||
|
{{ track.pk }}
|
||||||
|
<span v-if="isLazyTrack(track)" class="lazy-tag">lazy</span>
|
||||||
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span v-if="durationMs(track) !== undefined">
|
<span v-if="durationMs(track) !== undefined">
|
||||||
{{ formatDuration(durationMs(track)!) }}
|
{{ formatDuration(durationMs(track)!) }}
|
||||||
@@ -119,12 +124,15 @@
|
|||||||
<div class="last-used" v-if="track.last_used">
|
<div class="last-used" v-if="track.last_used">
|
||||||
Last used: {{ formatDate(track.last_used) }}
|
Last used: {{ formatDate(track.last_used) }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="lazy-warning" v-if="isLazyTrack(track)">
|
||||||
|
Audio not downloaded yet. First playback (or forcing download) will fetch it automatically.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-actions">
|
<div class="track-actions">
|
||||||
<button
|
<button
|
||||||
@click.stop="playTrack(track.pk)"
|
@click.stop="playTrack(track.pk)"
|
||||||
class="btn-play"
|
class="btn-play"
|
||||||
title="Play"
|
:title="isLazyTrack(track) ? 'Trigger download & play once available' : 'Play'"
|
||||||
>
|
>
|
||||||
▶️
|
▶️
|
||||||
</button>
|
</button>
|
||||||
@@ -136,6 +144,30 @@
|
|||||||
>
|
>
|
||||||
{{ deletingTracks.has(track.pk) ? "..." : "🗑️" }}
|
{{ deletingTracks.has(track.pk) ? "..." : "🗑️" }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="downloadTrack(track.pk)"
|
||||||
|
class="btn-secondary"
|
||||||
|
:disabled="isLazyTrack(track)"
|
||||||
|
:title="
|
||||||
|
isLazyTrack(track)
|
||||||
|
? 'Download available once audio finished downloading'
|
||||||
|
: 'Download original file'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
⬇️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="copyTrackUrl(track.pk)"
|
||||||
|
class="btn-secondary"
|
||||||
|
:disabled="isLazyTrack(track)"
|
||||||
|
:title="
|
||||||
|
isLazyTrack(track)
|
||||||
|
? 'URL available after download completes'
|
||||||
|
: 'Copy stream URL'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,6 +207,7 @@
|
|||||||
<div class="cache-section">
|
<div class="cache-section">
|
||||||
<h4>Cache Info</h4>
|
<h4>Cache Info</h4>
|
||||||
<p><strong>PK:</strong> {{ selectedTrack.pk }}</p>
|
<p><strong>PK:</strong> {{ selectedTrack.pk }}</p>
|
||||||
|
<p><strong>Status:</strong> {{ trackStatusLabel(selectedTrack) }}</p>
|
||||||
<p v-if="resolveTrackOrigin(selectedTrack)">
|
<p v-if="resolveTrackOrigin(selectedTrack)">
|
||||||
<strong>Source URL:</strong>
|
<strong>Source URL:</strong>
|
||||||
<a :href="resolveTrackOrigin(selectedTrack)" target="_blank">{{ resolveTrackOrigin(selectedTrack) }}</a>
|
<a :href="resolveTrackOrigin(selectedTrack)" target="_blank">{{ resolveTrackOrigin(selectedTrack) }}</a>
|
||||||
@@ -188,10 +221,18 @@
|
|||||||
<button @click="playTrack(selectedTrack.pk)" class="btn-play">
|
<button @click="playTrack(selectedTrack.pk)" class="btn-play">
|
||||||
▶️ Play
|
▶️ Play
|
||||||
</button>
|
</button>
|
||||||
<button @click="downloadTrack(selectedTrack.pk)" class="btn-secondary">
|
<button
|
||||||
|
@click="downloadTrack(selectedTrack.pk)"
|
||||||
|
class="btn-secondary"
|
||||||
|
:disabled="isLazyTrack(selectedTrack)"
|
||||||
|
>
|
||||||
⬇️ Download
|
⬇️ Download
|
||||||
</button>
|
</button>
|
||||||
<button @click="copyTrackUrl(selectedTrack.pk)" class="btn-secondary">
|
<button
|
||||||
|
@click="copyTrackUrl(selectedTrack.pk)"
|
||||||
|
class="btn-secondary"
|
||||||
|
:disabled="isLazyTrack(selectedTrack)"
|
||||||
|
>
|
||||||
📋 Copy URL
|
📋 Copy URL
|
||||||
</button>
|
</button>
|
||||||
<button @click="handleDeleteTrack(selectedTrack.pk); selectedTrack = null" class="btn-danger">
|
<button @click="handleDeleteTrack(selectedTrack.pk); selectedTrack = null" class="btn-danger">
|
||||||
@@ -244,6 +285,7 @@ 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:";
|
||||||
|
|
||||||
// Formulaire d'ajout
|
// Formulaire d'ajout
|
||||||
const newTrackUrl = ref("");
|
const newTrackUrl = ref("");
|
||||||
@@ -266,6 +308,9 @@ const failedCovers = ref(new Set<string>());
|
|||||||
|
|
||||||
// --- Computed ---
|
// --- Computed ---
|
||||||
const totalHits = computed(() => tracks.value.reduce((sum, t) => sum + t.hits, 0));
|
const totalHits = computed(() => tracks.value.reduce((sum, t) => sum + t.hits, 0));
|
||||||
|
const lazyTracksCount = computed(() =>
|
||||||
|
tracks.value.reduce((acc, track) => (isLazyTrack(track) ? acc + 1 : acc), 0)
|
||||||
|
);
|
||||||
|
|
||||||
const sortedTracks = computed(() => {
|
const sortedTracks = computed(() => {
|
||||||
const arr = [...tracks.value];
|
const arr = [...tracks.value];
|
||||||
@@ -410,10 +455,20 @@ function handleAudioError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downloadTrack(pk: string) {
|
function downloadTrack(pk: string) {
|
||||||
|
const track = getTrackByPk(pk);
|
||||||
|
if (track && isLazyTrack(track)) {
|
||||||
|
alert("This track is still in lazy cache. Play it once to download the audio before exporting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.open(getOriginalTrackUrl(pk), "_blank");
|
window.open(getOriginalTrackUrl(pk), "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyTrackUrl(pk: string) {
|
function copyTrackUrl(pk: string) {
|
||||||
|
const track = getTrackByPk(pk);
|
||||||
|
if (track && isLazyTrack(track)) {
|
||||||
|
alert("URL available after the lazy audio has been downloaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigator.clipboard.writeText(window.location.origin + getTrackUrl(pk));
|
navigator.clipboard.writeText(window.location.origin + getTrackUrl(pk));
|
||||||
alert("✅ URL copied!");
|
alert("✅ URL copied!");
|
||||||
}
|
}
|
||||||
@@ -476,6 +531,21 @@ function handleCoverError(pk: string) {
|
|||||||
failedCovers.value.add(pk);
|
failedCovers.value.add(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLazyTrack(track: AudioCacheEntry | null | undefined): boolean {
|
||||||
|
return !!track?.pk && track.pk.startsWith(LAZY_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackStatusLabel(track: AudioCacheEntry | null): string {
|
||||||
|
if (isLazyTrack(track)) {
|
||||||
|
return "Lazy (audio pending download)";
|
||||||
|
}
|
||||||
|
return "Cached";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackByPk(pk: string): AudioCacheEntry | undefined {
|
||||||
|
return tracks.value.find((t) => t.pk === pk);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshTracks();
|
refreshTracks();
|
||||||
});
|
});
|
||||||
@@ -751,6 +821,14 @@ button:disabled {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-overlay .hits.lazy {
|
||||||
|
background: rgba(255, 152, 0, 0.85);
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.track-info {
|
.track-info {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -790,6 +868,18 @@ button:disabled {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lazy-tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ff9800;
|
||||||
|
color: #000;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -811,6 +901,16 @@ button:disabled {
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lazy-warning {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #ffb347;
|
||||||
|
background: rgba(255, 152, 0, 0.15);
|
||||||
|
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.track-actions {
|
.track-actions {
|
||||||
padding: 0 1rem 1rem;
|
padding: 0 1rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -140,9 +140,20 @@ async fn serve_lazy_audio_file<C: CacheConfig>(
|
|||||||
|
|
||||||
// Construire l'URL de redirection
|
// Construire l'URL de redirection
|
||||||
let redirect_url = if param == C::default_param() {
|
let redirect_url = if param == C::default_param() {
|
||||||
format!("/cache/{}/{}", C::cache_type(), real_pk)
|
format!(
|
||||||
|
"/{}/{}/{}",
|
||||||
|
C::cache_name(),
|
||||||
|
C::cache_type(),
|
||||||
|
real_pk
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("/cache/{}/{}/{}", C::cache_type(), real_pk, param)
|
format!(
|
||||||
|
"/{}/{}/{}/{}",
|
||||||
|
C::cache_name(),
|
||||||
|
C::cache_type(),
|
||||||
|
real_pk,
|
||||||
|
param
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
return Redirect::temporary(&redirect_url).into_response();
|
return Redirect::temporary(&redirect_url).into_response();
|
||||||
@@ -190,9 +201,20 @@ async fn serve_lazy_audio_file<C: CacheConfig>(
|
|||||||
|
|
||||||
// 6. Rediriger vers la vraie URL
|
// 6. Rediriger vers la vraie URL
|
||||||
let redirect_url = if param == C::default_param() {
|
let redirect_url = if param == C::default_param() {
|
||||||
format!("/cache/{}/{}", C::cache_type(), real_pk)
|
format!(
|
||||||
|
"/{}/{}/{}",
|
||||||
|
C::cache_name(),
|
||||||
|
C::cache_type(),
|
||||||
|
real_pk
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("/cache/{}/{}/{}", C::cache_type(), real_pk, param)
|
format!(
|
||||||
|
"/{}/{}/{}/{}",
|
||||||
|
C::cache_name(),
|
||||||
|
C::cache_type(),
|
||||||
|
real_pk,
|
||||||
|
param
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
|||||||
@@ -16,8 +16,10 @@
|
|||||||
//! cargo run -p pmoqobuz --example lazy_loading
|
//! cargo run -p pmoqobuz --example lazy_loading
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use pmoaudiocache::Cache as AudioCache;
|
use pmoaudiocache::{register_audio_cache, AudioCacheConfigExt, Cache as AudioCache};
|
||||||
|
use pmoconfig::get_config;
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
|
use pmocovers::CoverCacheConfigExt;
|
||||||
use pmoplaylist::PlaylistManager;
|
use pmoplaylist::PlaylistManager;
|
||||||
use pmoqobuz::{QobuzClient, QobuzSource};
|
use pmoqobuz::{QobuzClient, QobuzSource};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -43,13 +45,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Step 2: Initialize caches
|
// Step 2: Initialize caches
|
||||||
println!("💾 Initializing caches...");
|
println!("💾 Initializing caches...");
|
||||||
let cover_cache = Arc::new(CoverCache::new("./cache/qobuz-covers", 500)?);
|
let config = get_config();
|
||||||
let audio_cache = Arc::new(AudioCache::new("./cache/qobuz-audio", 100)?);
|
|
||||||
|
let (cover_cache, audio_cache, using_global) =
|
||||||
|
match (config.create_cover_cache(), config.create_audio_cache()) {
|
||||||
|
(Ok(covers), Ok(audio)) => (covers, audio, true),
|
||||||
|
(covers_res, audio_res) => {
|
||||||
|
let err_cover = covers_res.err();
|
||||||
|
let err_audio = audio_res.err();
|
||||||
|
if let Some(err) = err_cover {
|
||||||
|
println!(
|
||||||
|
" ⚠️ Could not open configured cover cache: {}. Falling back to ./cache",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(err) = err_audio {
|
||||||
|
println!(
|
||||||
|
" ⚠️ Could not open configured audio cache: {}. Falling back to ./cache",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cover_cache = Arc::new(CoverCache::new("./cache/qobuz-covers", 500)?);
|
||||||
|
let audio_cache = Arc::new(AudioCache::new("./cache/qobuz-audio", 100)?);
|
||||||
|
(cover_cache, audio_cache, false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// IMPORTANT: Register audio cache globally so PlaylistManager can access it
|
// IMPORTANT: Register audio cache globally so PlaylistManager can access it
|
||||||
|
register_audio_cache(audio_cache.clone());
|
||||||
pmoplaylist::register_audio_cache(audio_cache.clone());
|
pmoplaylist::register_audio_cache(audio_cache.clone());
|
||||||
|
|
||||||
println!("✅ Caches initialized\n");
|
if using_global {
|
||||||
|
println!("✅ Caches initialized from global configuration\n");
|
||||||
|
} else {
|
||||||
|
println!("✅ Local caches initialized in ./cache (global config unavailable)\n");
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: Create QobuzSource with caches
|
// Step 3: Create QobuzSource with caches
|
||||||
let source = QobuzSource::new(client, cover_cache.clone(), audio_cache.clone());
|
let source = QobuzSource::new(client, cover_cache.clone(), audio_cache.clone());
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use pmocovers::Cache as CoverCache;
|
|||||||
use pmodidl::{Container, Item};
|
use pmodidl::{Container, Item};
|
||||||
use pmosource::SourceCacheManager;
|
use pmosource::SourceCacheManager;
|
||||||
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
|
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
|
||||||
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
@@ -203,6 +204,13 @@ impl QobuzSource {
|
|||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
if let (Some(ref audio_pk), Some(ref cover_pk)) = (&cached_audio_pk, &cached_cover_pk) {
|
||||||
|
let _ =
|
||||||
|
self.inner
|
||||||
|
.cache_manager
|
||||||
|
.set_audio_metadata(audio_pk, "cover_pk", json!(cover_pk));
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Store metadata
|
// 4. Store metadata
|
||||||
self.inner
|
self.inner
|
||||||
.cache_manager
|
.cache_manager
|
||||||
@@ -295,6 +303,14 @@ impl QobuzSource {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if let Some(ref cover_pk) = cached_cover_pk {
|
||||||
|
let _ = self.inner.cache_manager.set_audio_metadata(
|
||||||
|
&cached_audio_pk,
|
||||||
|
"cover_pk",
|
||||||
|
json!(cover_pk),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Store metadata
|
// 4. Store metadata
|
||||||
self.inner
|
self.inner
|
||||||
.cache_manager
|
.cache_manager
|
||||||
|
|||||||
@@ -21,12 +21,28 @@
|
|||||||
use crate::{CacheStatus, MusicSourceError, Result};
|
use crate::{CacheStatus, MusicSourceError, Result};
|
||||||
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::{json, Value as JsonValue};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
fn log_metadata_warning(key: &str, audio_pk: &str, error: &str) {
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to store metadata {} for {}: {}",
|
||||||
|
key,
|
||||||
|
audio_pk,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "server"))]
|
||||||
|
{
|
||||||
|
let _ = (key, audio_pk, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Métadonnées d'une piste en cache
|
/// Métadonnées d'une piste en cache
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TrackMetadata {
|
pub struct TrackMetadata {
|
||||||
@@ -252,6 +268,7 @@ impl SourceCacheManager {
|
|||||||
.db
|
.db
|
||||||
.set_a_metadata_by_key(&lazy_pk, "audio_metadata", json);
|
.set_a_metadata_by_key(&lazy_pk, "audio_metadata", json);
|
||||||
}
|
}
|
||||||
|
self.seed_audio_metadata(&lazy_pk, &meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(lazy_pk)
|
Ok(lazy_pk)
|
||||||
@@ -303,6 +320,62 @@ impl SourceCacheManager {
|
|||||||
cache.remove(track_id);
|
cache.remove(track_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pré-remplit les métadonnées audio pour une entrée lazy
|
||||||
|
fn seed_audio_metadata(&self, audio_pk: &str, metadata: &AudioMetadata) {
|
||||||
|
let audio_pk = audio_pk.to_string();
|
||||||
|
let mut store = |key: &str, value: JsonValue| {
|
||||||
|
if let Err(e) = self.audio_cache.db.set_a_metadata(&audio_pk, key, value) {
|
||||||
|
log_metadata_warning(key, &audio_pk, &e.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(title) = metadata.title.as_ref() {
|
||||||
|
store("title", JsonValue::String(title.clone()));
|
||||||
|
}
|
||||||
|
if let Some(artist) = metadata.artist.as_ref() {
|
||||||
|
store("artist", JsonValue::String(artist.clone()));
|
||||||
|
}
|
||||||
|
if let Some(album) = metadata.album.as_ref() {
|
||||||
|
store("album", JsonValue::String(album.clone()));
|
||||||
|
}
|
||||||
|
if let Some(year) = metadata.year {
|
||||||
|
store("year", json!(year));
|
||||||
|
}
|
||||||
|
if let Some(track_number) = metadata.track_number {
|
||||||
|
store("track_number", json!(track_number));
|
||||||
|
}
|
||||||
|
if let Some(track_total) = metadata.track_total {
|
||||||
|
store("track_total", json!(track_total));
|
||||||
|
}
|
||||||
|
if let Some(disc_number) = metadata.disc_number {
|
||||||
|
store("disc_number", json!(disc_number));
|
||||||
|
}
|
||||||
|
if let Some(disc_total) = metadata.disc_total {
|
||||||
|
store("disc_total", json!(disc_total));
|
||||||
|
}
|
||||||
|
if let Some(genre) = metadata.genre.as_ref() {
|
||||||
|
store("genre", JsonValue::String(genre.clone()));
|
||||||
|
}
|
||||||
|
if let Some(duration_secs) = metadata.duration_secs {
|
||||||
|
store("duration_secs", json!(duration_secs));
|
||||||
|
store("duration_ms", json!(duration_secs * 1000));
|
||||||
|
}
|
||||||
|
if let Some(sample_rate) = metadata.sample_rate {
|
||||||
|
store("sample_rate", json!(sample_rate));
|
||||||
|
}
|
||||||
|
if let Some(channels) = metadata.channels {
|
||||||
|
store("channels", json!(channels));
|
||||||
|
}
|
||||||
|
if let Some(bitrate) = metadata.bitrate {
|
||||||
|
store("bitrate", json!(bitrate));
|
||||||
|
}
|
||||||
|
if let Some(conversion) = metadata.conversion.as_ref() {
|
||||||
|
if let Ok(value) = serde_json::to_value(conversion) {
|
||||||
|
store("conversion", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stocke une métadonnée personnalisée pour un fichier audio caché
|
/// Stocke une métadonnée personnalisée pour un fichier audio caché
|
||||||
///
|
///
|
||||||
/// Permet de stocker des métadonnées arbitraires (clé/valeur JSON) associées
|
/// Permet de stocker des métadonnées arbitraires (clé/valeur JSON) associées
|
||||||
|
|||||||
Reference in New Issue
Block a user