correction for lazy playlist and lazy cache

This commit is contained in:
2025-12-15 11:18:58 +01:00
parent 4bc80dfd08
commit 90399b408a
28 changed files with 349 additions and 39126 deletions

View File

@@ -48,15 +48,9 @@ pub fn is_lazy_pk(pk: &str) -> bool {
#[derive(Debug, Clone)]
pub enum CacheEvent {
/// Un fichier a été servi via HTTP
Served {
pk: String,
format: String,
},
Served { pk: String, format: String },
/// Un fichier lazy a été téléchargé et est maintenant disponible
LazyDownloaded {
lazy_pk: String,
real_pk: String,
},
LazyDownloaded { lazy_pk: String, real_pk: String },
}
/// Informations transmises lors de la diffusion d'un élément du cache via HTTP.
@@ -1358,7 +1352,7 @@ impl<C: CacheConfig> Cache<C> {
let lazy_pk = generate_lazy_pk(url);
// Vérifier si ce lazy_pk existe déjà (collision improbable mais...)
if let Ok(Some(_)) = self.db.get_pk_by_lazy_pk(&lazy_pk) {
if let Ok(true) = self.db.has_lazy_entry(&lazy_pk) {
bail!("Lazy PK collision for URL: {}", url);
}

View File

@@ -4,7 +4,7 @@ use std::{
sync::Arc,
};
use crate::{CacheConfig, DB};
use crate::{cache::is_lazy_pk, CacheConfig, DB};
/// Trait générique pour les caches de fichiers
///
@@ -145,6 +145,29 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
/// Ceci permet le progressive caching: les fichiers en cours de download sont acceptés
/// dès que le prebuffer est atteint, sans attendre le marker de completion.
async fn is_valid_pk(&self, pk: &str) -> bool {
if is_lazy_pk(pk) {
match self.get_database().has_lazy_entry(pk) {
Ok(true) => {
tracing::debug!(
"is_valid_pk({}): Lazy entry present in DB, download deferred",
pk
);
return true;
}
Ok(false) => {
tracing::warn!(
"is_valid_pk({}): Lazy pk not registered in DB, rejecting",
pk
);
return false;
}
Err(e) => {
tracing::error!("is_valid_pk({}): Error while checking lazy pk: {}", pk, e);
return false;
}
}
}
if self.get_database().get(pk, false).is_err() {
tracing::debug!("is_valid_pk({}): DB entry not found", pk);
return false;

View File

@@ -765,11 +765,11 @@ impl DB {
// LAZY PK SUPPORT
// ============================================================================
/// Ajoute une entrée en mode lazy (pk = NULL, lazy_pk rempli)
/// Ajoute une entrée en mode lazy (pk = lazy_pk tant que non téléchargé)
///
/// Utilisé pour créer des entries sans télécharger le fichier.
/// Le lazy_pk est calculé à partir de l'URL, le real pk sera calculé
/// lors du téléchargement effectif.
/// Le lazy_pk est calculé à partir de l'URL et sert temporairement
/// également de pk pour satisfaire les contraintes de clé étrangère.
///
/// # Arguments
///
@@ -784,12 +784,9 @@ impl DB {
) -> rusqlite::Result<()> {
let conn = self.lock_conn("add_lazy");
// Créer une entry avec pk = NULL et lazy_pk rempli
// On utilise une astuce: insérer avec lazy_pk comme clé temporaire
// puis mettre pk à NULL
conn.execute(
"INSERT INTO asset (pk, lazy_pk, id, collection, hits, last_used)
VALUES (NULL, ?1, ?2, ?3, 0, ?4)",
VALUES (?1, ?1, ?2, ?3, 0, ?4)",
params![lazy_pk, id, collection, Utc::now().to_rfc3339()],
)?;
@@ -805,16 +802,46 @@ impl DB {
/// # Returns
///
/// * `Ok(Some(pk))` - Le real pk si le fichier a été téléchargé
/// * `Ok(None)` - Pas encore téléchargé (pk = NULL) ou lazy_pk inconnu
/// * `Ok(None)` - Pas encore téléchargé ou lazy_pk inconnu
pub fn get_pk_by_lazy_pk(&self, lazy_pk: &str) -> rusqlite::Result<Option<String>> {
let conn = self.lock_conn("get_pk_by_lazy_pk");
conn.query_row(
"SELECT pk FROM asset WHERE lazy_pk = ?1",
[lazy_pk],
|row| row.get(0),
)
.optional()
let result: Option<(String, Option<String>)> = conn
.query_row(
"SELECT pk, lazy_pk FROM asset WHERE lazy_pk = ?1",
[lazy_pk],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()?;
Ok(result.and_then(|(pk, lazy)| {
if let Some(lazy_pk_value) = lazy {
if pk == lazy_pk_value {
None
} else {
Some(pk)
}
} else {
Some(pk)
}
}))
}
/// Vérifie l'existence d'une entrée lazy sans télécharger le fichier
///
/// Retourne `true` si une ligne avec `lazy_pk` existe, même si `pk` est `NULL`.
pub fn has_lazy_entry(&self, lazy_pk: &str) -> rusqlite::Result<bool> {
let conn = self.lock_conn("has_lazy_entry");
let exists: Option<i64> = conn
.query_row(
"SELECT 1 FROM asset WHERE lazy_pk = ?1 LIMIT 1",
[lazy_pk],
|row| row.get(0),
)
.optional()?;
Ok(exists.is_some())
}
/// Transition d'une entry lazy vers downloaded (ajoute le real pk)
@@ -827,43 +854,51 @@ impl DB {
///
/// * `lazy_pk` - Le lazy PK de l'entry originale
/// * `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 tx = conn.transaction()?;
// 1. Récupérer les infos de l'entry lazy
let (collection, id): (Option<String>, Option<String>) = tx
// 1. Récupérer l'entry lazy (pk = lazy_pk tant que pas téléchargé)
let (old_pk, collection, id, hits): (String, Option<String>, Option<String>, i32) = tx
.query_row(
"SELECT collection, id FROM asset WHERE lazy_pk = ?1 AND pk IS NULL",
"SELECT pk, collection, id, hits FROM asset WHERE lazy_pk = ?1",
[lazy_pk],
|row| Ok((row.get(0)?, row.get(1)?)),
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)
.optional()?
.ok_or_else(|| Error::QueryReturnedNoRows)?;
// 2. Supprimer l'entry lazy (pk = NULL)
tx.execute(
"DELETE FROM asset WHERE lazy_pk = ?1 AND pk IS NULL",
[lazy_pk],
)?;
if old_pk == real_pk {
// Rien à faire si déjà commuté
return Ok(());
}
// 3. Créer nouvelle entry avec pk rempli ET lazy_pk
// Si le real_pk existe déjà (téléchargé via eager mode), on met juste à jour
let now = Utc::now().to_rfc3339();
let hits_to_add = if hits > 0 { hits } else { 1 };
// 2. Créer/mettre à jour l'entry avec le real pk
tx.execute(
"INSERT INTO asset (pk, lazy_pk, collection, id, hits, last_used)
VALUES (?1, ?2, ?3, ?4, 1, ?5)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(pk) DO UPDATE SET
lazy_pk = excluded.lazy_pk,
last_used = excluded.last_used,
hits = hits + 1",
params![real_pk, lazy_pk, collection, id, Utc::now().to_rfc3339()],
collection = COALESCE(excluded.collection, collection),
id = COALESCE(excluded.id, id),
hits = hits + excluded.hits,
last_used = excluded.last_used",
params![real_pk, lazy_pk, collection, id, hits_to_add, now],
)?;
// 3. Re-pointer les métadonnées vers le real pk
tx.execute(
"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()
}
@@ -890,11 +925,11 @@ impl DB {
// Chercher via origin_url dans metadata
// On joint avec asset pour récupérer pk et lazy_pk
let result: Option<(Option<String>, Option<String>)> = conn
let raw: Option<(String, Option<String>)> = conn
.query_row(
"SELECT a.pk, a.lazy_pk
FROM asset a
JOIN metadata m ON (a.pk = m.pk OR a.lazy_pk = m.pk)
JOIN metadata m ON a.pk = m.pk
WHERE m.key = 'origin_url' AND m.value = ?1
LIMIT 1",
[url],
@@ -902,6 +937,18 @@ impl DB {
)
.optional()?;
let result = raw.map(|(pk, lazy_pk)| {
if let Some(ref lazy) = lazy_pk {
if pk == *lazy {
(None, Some(lazy.clone()))
} else {
(Some(pk), lazy_pk)
}
} else {
(Some(pk), lazy_pk)
}
});
Ok(result)
}
@@ -932,13 +979,7 @@ impl DB {
///
/// * `lazy_pk` - Le lazy PK de l'entry
/// * `origin_url` - L'URL d'origine à stocker
pub fn set_origin_url_for_lazy(
&self,
lazy_pk: &str,
origin_url: &str,
) -> rusqlite::Result<()> {
// Pour les entries lazy, on stocke l'origin_url avec lazy_pk comme clé
// dans la table metadata (au lieu de pk qui est NULL)
pub fn set_origin_url_for_lazy(&self, lazy_pk: &str, origin_url: &str) -> rusqlite::Result<()> {
self.set_a_metadata_by_key(lazy_pk, "origin_url", Value::String(origin_url.to_owned()))
}

View File

@@ -195,7 +195,12 @@ async fn serve_lazy_audio_file<C: CacheConfig>(
format!("/cache/{}/{}/{}", C::cache_type(), real_pk, param)
};
tracing::debug!("Lazy PK {} downloaded as {}, redirecting to {}", lazy_pk, real_pk, redirect_url);
tracing::debug!(
"Lazy PK {} downloaded as {}, redirecting to {}",
lazy_pk,
real_pk,
redirect_url
);
Redirect::temporary(&redirect_url).into_response()
}