Debug le menu debug
This commit is contained in:
264
pmocache/src/cache.rs
Normal file
264
pmocache/src/cache.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
//! Module de gestion du cache générique
|
||||
//!
|
||||
//! Ce module fournit une interface générique pour gérer un cache de fichiers
|
||||
//! avec métadonnées dans une base de données SQLite.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use sha1::{Sha1, Digest};
|
||||
use tokio::sync::Mutex;
|
||||
use crate::db::DB;
|
||||
|
||||
/// Configuration du cache
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheConfig {
|
||||
/// Répertoire de stockage
|
||||
pub dir: PathBuf,
|
||||
/// Limite de taille du cache (nombre d'éléments)
|
||||
pub limit: usize,
|
||||
/// Nom de la table dans la base de données
|
||||
pub table_name: String,
|
||||
/// Extension des fichiers dans le cache
|
||||
pub file_extension: String,
|
||||
}
|
||||
|
||||
impl CacheConfig {
|
||||
/// Crée une nouvelle configuration de cache
|
||||
pub fn new(dir: &str, limit: usize, table_name: &str, file_extension: &str) -> Self {
|
||||
Self {
|
||||
dir: PathBuf::from(dir),
|
||||
limit,
|
||||
table_name: table_name.to_string(),
|
||||
file_extension: file_extension.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache générique pour stocker des fichiers avec métadonnées
|
||||
///
|
||||
/// Gère le téléchargement, le stockage et la récupération de fichiers
|
||||
/// avec une base de données SQLite pour les métadonnées.
|
||||
///
|
||||
/// Note : Ce type est conçu pour être utilisé derrière un `Arc<Cache>`.
|
||||
/// Les méthodes prennent `&self` et utilisent des `Arc` et `Mutex` internes
|
||||
/// pour la synchronisation.
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub(crate) config: CacheConfig,
|
||||
pub db: Arc<DB>,
|
||||
mu: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
/// Crée un nouveau cache avec la configuration spécifiée
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - Configuration du cache
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmocache::cache::{Cache, CacheConfig};
|
||||
///
|
||||
/// let config = CacheConfig::new("./cache", 1000, "my_cache", "webp");
|
||||
/// let cache = Cache::new(config).unwrap();
|
||||
/// ```
|
||||
pub fn new(config: CacheConfig) -> Result<Self> {
|
||||
std::fs::create_dir_all(&config.dir)?;
|
||||
let db = DB::init(
|
||||
&config.dir.join("cache.db"),
|
||||
&config.table_name
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
db: Arc::new(db),
|
||||
mu: Arc::new(Mutex::new(())),
|
||||
})
|
||||
}
|
||||
|
||||
/// Télécharge un fichier depuis une URL et l'ajoute au cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL du fichier à télécharger
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// La clé primaire (pk) du fichier dans le cache
|
||||
pub async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let response = reqwest::get(url).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("Bad status: {}", response.status()));
|
||||
}
|
||||
|
||||
let data = response.bytes().await?;
|
||||
self.add(url, &data, collection).await
|
||||
}
|
||||
|
||||
/// S'assure qu'un fichier est présent dans le cache
|
||||
///
|
||||
/// Si le fichier existe déjà, retourne sa clé. Sinon, le télécharge.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL du fichier
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
pub async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let pk = pk_from_url(url);
|
||||
|
||||
if self.db.get(&pk).is_ok() {
|
||||
let file_path = self.file_path(&pk);
|
||||
if file_path.exists() {
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
self.add_from_url(url, collection).await
|
||||
}
|
||||
|
||||
/// Ajoute des données au cache
|
||||
///
|
||||
/// Cette méthode doit être surchargée par les implémentations spécifiques
|
||||
/// pour gérer la conversion et le stockage des données.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL source du fichier
|
||||
/// * `data` - Données brutes à stocker
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
pub async fn add(&self, url: &str, data: &[u8], collection: Option<&str>) -> Result<String> {
|
||||
let pk = pk_from_url(url);
|
||||
let file_path = self.file_path(&pk);
|
||||
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
if !file_path.exists() {
|
||||
// Par défaut, on stocke les données telles quelles
|
||||
tokio::fs::write(&file_path, data).await?;
|
||||
}
|
||||
|
||||
self.db.add(&pk, url, collection)?;
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
/// Récupère le chemin d'un fichier dans le cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire du fichier
|
||||
pub async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
self.db.get(pk)?;
|
||||
self.db.update_hit(pk)?;
|
||||
|
||||
let file_path = self.file_path(pk);
|
||||
if file_path.exists() {
|
||||
Ok(file_path)
|
||||
} else {
|
||||
Err(anyhow!("File not found"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère tous les fichiers d'une collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Identifiant de la collection
|
||||
pub async fn get_collection(&self, collection: &str) -> Result<Vec<PathBuf>> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
let entries = self.db.get_by_collection(collection)?;
|
||||
let mut paths = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let path = self.file_path(&entry.pk);
|
||||
if path.exists() {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
/// Supprime tous les fichiers et entrées du cache
|
||||
pub async fn purge(&self) -> Result<()> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
let mut entries = tokio::fs::read_dir(&self.config.dir).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if entry.path().is_file() && entry.path() != self.config.dir.join("cache.db") {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.db.purge().map_err(|e| anyhow!("Database error: {}", e))
|
||||
}
|
||||
|
||||
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les fichiers manquants
|
||||
pub async fn consolidate(&self) -> Result<()> {
|
||||
// Récupérer la liste des entrées à traiter
|
||||
let entries = {
|
||||
let _lock = self.mu.lock().await;
|
||||
self.db.get_all()?
|
||||
};
|
||||
|
||||
// Supprimer les entrées sans fichiers correspondants
|
||||
for entry in entries {
|
||||
let file_path = self.file_path(&entry.pk);
|
||||
if !file_path.exists() {
|
||||
match reqwest::get(&entry.source_url).await {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
let data = response.bytes().await?;
|
||||
self.add(&entry.source_url, &data, entry.collection.as_deref()).await?;
|
||||
}
|
||||
_ => {
|
||||
let _lock = self.mu.lock().await;
|
||||
self.db.delete(&entry.pk)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer les fichiers sans entrées DB correspondantes
|
||||
let _lock = self.mu.lock().await;
|
||||
let mut dir_entries = tokio::fs::read_dir(&self.config.dir).await?;
|
||||
while let Some(entry) = dir_entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path != self.config.dir.join("cache.db") {
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
let pk = file_name.trim_end_matches(&format!(".{}", self.config.file_extension));
|
||||
if self.db.get(pk).is_err() {
|
||||
tokio::fs::remove_file(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retourne le répertoire du cache
|
||||
pub fn cache_dir(&self) -> String {
|
||||
self.config.dir.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Construit le chemin complet d'un fichier dans le cache
|
||||
fn file_path(&self, pk: &str) -> PathBuf {
|
||||
self.config.dir.join(format!("{}.{}", pk, self.config.file_extension))
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère une clé primaire à partir d'une URL
|
||||
///
|
||||
/// Utilise SHA1 pour hasher l'URL et retourne les 8 premiers octets en hexadécimal.
|
||||
pub fn pk_from_url(url: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
hex::encode(&result[..8])
|
||||
}
|
||||
251
pmocache/src/db.rs
Normal file
251
pmocache/src/db.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
//! Module de gestion de la base de données SQLite pour le cache
|
||||
//!
|
||||
//! Ce module fournit une interface générique pour gérer les métadonnées
|
||||
//! des éléments en cache, avec tracking des accès et des statistiques.
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use chrono::Utc;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Entrée de cache représentant un élément dans la base de données
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[cfg_attr(feature = "openapi", derive(ToSchema))]
|
||||
pub struct CacheEntry {
|
||||
/// Clé primaire unique de l'élément (hash SHA1 de l'URL)
|
||||
#[cfg_attr(feature = "openapi", schema(example = "1a2b3c4d5e6f7a8b"))]
|
||||
pub pk: String,
|
||||
/// URL source de l'élément
|
||||
#[cfg_attr(feature = "openapi", schema(example = "https://example.com/resource"))]
|
||||
pub source_url: String,
|
||||
/// Collection à laquelle appartient l'élément (optionnel)
|
||||
#[cfg_attr(feature = "openapi", schema(example = "album:123"))]
|
||||
pub collection: Option<String>,
|
||||
/// Nombre d'accès à l'élément
|
||||
#[cfg_attr(feature = "openapi", schema(example = 42))]
|
||||
pub hits: i32,
|
||||
/// Date/heure du dernier accès (RFC3339)
|
||||
#[cfg_attr(feature = "openapi", schema(example = "2025-01-15T10:30:00Z"))]
|
||||
pub last_used: Option<String>,
|
||||
}
|
||||
|
||||
/// Base de données SQLite pour le cache
|
||||
///
|
||||
/// Gère les métadonnées des éléments en cache :
|
||||
/// - Clés primaires (pk) et URLs sources
|
||||
/// - Statistiques d'utilisation (hits, last_used)
|
||||
/// - Opérations CRUD de base
|
||||
#[derive(Debug)]
|
||||
pub struct DB {
|
||||
conn: Mutex<Connection>,
|
||||
table_name: String,
|
||||
}
|
||||
|
||||
impl DB {
|
||||
/// Initialise une nouvelle base de données avec une table personnalisée
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Chemin vers le fichier de base de données SQLite
|
||||
/// * `table_name` - Nom de la table à créer
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmocache::db::DB;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let db = DB::init(Path::new("cache.db"), "my_cache").unwrap();
|
||||
/// ```
|
||||
pub fn init(path: &Path, table_name: &str) -> Result<Self, rusqlite::Error> {
|
||||
let conn = Connection::open(path)?;
|
||||
|
||||
let create_table_sql = format!(
|
||||
"CREATE TABLE IF NOT EXISTS {} (
|
||||
pk TEXT PRIMARY KEY,
|
||||
source_url TEXT,
|
||||
collection TEXT,
|
||||
hits INTEGER DEFAULT 0,
|
||||
last_used TEXT
|
||||
)",
|
||||
table_name
|
||||
);
|
||||
|
||||
conn.execute(&create_table_sql, [])?;
|
||||
|
||||
// Créer un index sur la collection pour les requêtes rapides
|
||||
let create_index_sql = format!(
|
||||
"CREATE INDEX IF NOT EXISTS idx_{}_collection ON {} (collection)",
|
||||
table_name, table_name
|
||||
);
|
||||
|
||||
conn.execute(&create_index_sql, [])?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
table_name: table_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Ajoute ou met à jour une entrée dans la base de données
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire de l'élément
|
||||
/// * `url` - URL source de l'élément
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient l'élément
|
||||
pub fn add(&self, pk: &str, url: &str, collection: Option<&str>) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!(
|
||||
"INSERT INTO {} (pk, source_url, collection, hits, last_used)
|
||||
VALUES (?1, ?2, ?3, 0, ?4)
|
||||
ON CONFLICT(pk) DO UPDATE SET
|
||||
source_url = excluded.source_url,
|
||||
collection = excluded.collection,
|
||||
last_used = excluded.last_used",
|
||||
self.table_name
|
||||
);
|
||||
|
||||
conn.execute(
|
||||
&sql,
|
||||
params![pk, url, collection, Utc::now().to_rfc3339()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Récupère une entrée de la base de données par sa clé
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire de l'élément à récupérer
|
||||
pub fn get(&self, pk: &str) -> rusqlite::Result<CacheEntry> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!(
|
||||
"SELECT pk, source_url, collection, hits, last_used FROM {} WHERE pk = ?1",
|
||||
self.table_name
|
||||
);
|
||||
|
||||
conn.query_row(
|
||||
&sql,
|
||||
[pk],
|
||||
|row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
source_url: row.get(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Met à jour le compteur d'accès et la date du dernier accès
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire de l'élément
|
||||
pub fn update_hit(&self, pk: &str) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!(
|
||||
"UPDATE {} SET hits = hits + 1, last_used = ?1 WHERE pk = ?2",
|
||||
self.table_name
|
||||
);
|
||||
|
||||
conn.execute(
|
||||
&sql,
|
||||
params![Utc::now().to_rfc3339(), pk],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Purge toutes les entrées de la base de données
|
||||
pub fn purge(&self) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!("DELETE FROM {}", self.table_name);
|
||||
conn.execute(&sql, [])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Récupère toutes les entrées, triées par nombre d'accès décroissant
|
||||
pub fn get_all(&self) -> rusqlite::Result<Vec<CacheEntry>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!(
|
||||
"SELECT pk, source_url, collection, hits, last_used FROM {} ORDER BY hits DESC",
|
||||
self.table_name
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
||||
let entries = stmt.query_map([], |row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
source_url: row.get(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Récupère toutes les entrées d'une collection spécifique
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Identifiant de la collection
|
||||
pub fn get_by_collection(&self, collection: &str) -> rusqlite::Result<Vec<CacheEntry>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!(
|
||||
"SELECT pk, source_url, collection, hits, last_used FROM {} WHERE collection = ?1 ORDER BY hits DESC",
|
||||
self.table_name
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
||||
let entries = stmt.query_map([collection], |row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
source_url: row.get(1)?,
|
||||
collection: row.get(2)?,
|
||||
hits: row.get(3)?,
|
||||
last_used: row.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Supprime toutes les entrées d'une collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Identifiant de la collection à supprimer
|
||||
pub fn delete_collection(&self, collection: &str) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!("DELETE FROM {} WHERE collection = ?1", self.table_name);
|
||||
conn.execute(&sql, [collection])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Supprime une entrée de la base de données
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire de l'élément à supprimer
|
||||
pub fn delete(&self, pk: &str) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let sql = format!("DELETE FROM {} WHERE pk = ?1", self.table_name);
|
||||
conn.execute(&sql, [pk])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
116
pmocache/src/lib.rs
Normal file
116
pmocache/src/lib.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
//! # pmocache - Système de cache générique pour PMOMusic
|
||||
//!
|
||||
//! Cette crate fournit un système de cache générique avec support de base de données SQLite
|
||||
//! et stockage sur disque. Elle est utilisée comme base pour des caches spécialisés comme
|
||||
//! `pmocovers` (cache d'images) et `pmoaudiocache` (cache de pistes audio).
|
||||
//!
|
||||
//! ## Vue d'ensemble
|
||||
//!
|
||||
//! `pmocache` fournit les composants de base pour :
|
||||
//! - Stocker des fichiers sur disque avec une base de données SQLite pour les métadonnées
|
||||
//! - Gérer des collections d'éléments (albums, playlists, etc.)
|
||||
//! - Suivre les statistiques d'utilisation (hits, dernière utilisation)
|
||||
//! - Télécharger automatiquement depuis des URLs
|
||||
//! - Consolider et purger le cache
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! `pmocache` est conçu comme une base générique :
|
||||
//!
|
||||
//! ```text
|
||||
//! pmocache (générique)
|
||||
//! ├── db.rs - Base de données SQLite générique
|
||||
//! └── cache.rs - Système de cache générique
|
||||
//!
|
||||
//! pmocovers (spécialisé pour les images)
|
||||
//! └── Utilise pmocache + conversion WebP
|
||||
//!
|
||||
//! pmoaudiocache (spécialisé pour l'audio)
|
||||
//! └── Utilise pmocache + métadonnées audio
|
||||
//! ```
|
||||
//!
|
||||
//! ## Utilisation
|
||||
//!
|
||||
//! ### Exemple basique
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocache::cache::{Cache, CacheConfig};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let config = CacheConfig::new("./cache", 1000, "my_cache", "dat");
|
||||
//! let cache = Cache::new(config)?;
|
||||
//!
|
||||
//! // Ajouter un fichier depuis une URL
|
||||
//! let pk = cache.add_from_url("http://example.com/file.dat", None).await?;
|
||||
//! println!("Fichier ajouté avec clé: {}", pk);
|
||||
//!
|
||||
//! // Récupérer le fichier
|
||||
//! let path = cache.get(&pk).await?;
|
||||
//! println!("Fichier stocké à: {:?}", path);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Utilisation avec des collections
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocache::cache::{Cache, CacheConfig};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let config = CacheConfig::new("./cache", 1000, "audio", "flac");
|
||||
//! let cache = Cache::new(config)?;
|
||||
//!
|
||||
//! // Ajouter des pistes d'un album
|
||||
//! let album_id = "album:the_wall";
|
||||
//! cache.add_from_url("http://example.com/track1.flac", Some(album_id)).await?;
|
||||
//! cache.add_from_url("http://example.com/track2.flac", Some(album_id)).await?;
|
||||
//!
|
||||
//! // Récupérer toutes les pistes de l'album
|
||||
//! let tracks = cache.get_collection(album_id).await?;
|
||||
//! println!("Album contient {} pistes", tracks.len());
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Structure des fichiers
|
||||
//!
|
||||
//! ```text
|
||||
//! cache/
|
||||
//! ├── cache.db # Base de données SQLite
|
||||
//! ├── 1a2b3c4d.webp # Fichier 1
|
||||
//! └── 5e6f7a8b.flac # Fichier 2
|
||||
//! ```
|
||||
//!
|
||||
//! ## Schéma de base de données
|
||||
//!
|
||||
//! ```sql
|
||||
//! CREATE TABLE {table_name} (
|
||||
//! pk TEXT PRIMARY KEY, -- Clé unique (hash SHA1 de l'URL)
|
||||
//! source_url TEXT, -- URL source
|
||||
//! collection TEXT, -- Collection (album, playlist, etc.)
|
||||
//! hits INTEGER DEFAULT 0, -- Nombre d'accès
|
||||
//! last_used TEXT -- Dernière utilisation (RFC3339)
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! ## Dépendances principales
|
||||
//!
|
||||
//! - `rusqlite` : Base de données SQLite
|
||||
//! - `reqwest` : Téléchargement HTTP
|
||||
//! - `sha1` : Génération de clés
|
||||
//! - `tokio` : Runtime asynchrone
|
||||
//!
|
||||
//! ## Voir aussi
|
||||
//!
|
||||
//! - [`pmocovers`] : Cache d'images avec conversion WebP
|
||||
//! - [`pmoaudiocache`] : Cache de pistes audio
|
||||
|
||||
pub mod db;
|
||||
pub mod cache;
|
||||
|
||||
pub use db::{DB, CacheEntry};
|
||||
pub use cache::{Cache, CacheConfig, pk_from_url};
|
||||
Reference in New Issue
Block a user