325 lines
10 KiB
Rust
325 lines
10 KiB
Rust
//! Système de cache en mémoire pour les données Qobuz
|
|
//!
|
|
//! Ce module fournit un cache en mémoire avec TTL pour minimiser les requêtes à l'API Qobuz.
|
|
|
|
use crate::models::{Album, Artist, Playlist, SearchResult, StreamInfo, Track};
|
|
use moka::future::Cache as MokaCache;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
/// Cache principal pour les données Qobuz
|
|
#[derive(Clone)]
|
|
pub struct QobuzCache {
|
|
/// Cache des albums (TTL: 1 heure)
|
|
albums: Arc<MokaCache<String, Album>>,
|
|
/// Cache des tracks (TTL: 1 heure)
|
|
tracks: Arc<MokaCache<String, Track>>,
|
|
/// Cache des tracks d'un album complet (TTL: 1 heure)
|
|
album_tracks: Arc<MokaCache<String, Vec<Track>>>,
|
|
/// Cache des artistes (TTL: 1 heure)
|
|
artists: Arc<MokaCache<String, Artist>>,
|
|
/// Cache des playlists (TTL: 30 minutes)
|
|
playlists: Arc<MokaCache<String, Playlist>>,
|
|
/// Cache des résultats de recherche (TTL: 15 minutes)
|
|
searches: Arc<MokaCache<String, SearchResult>>,
|
|
/// Cache des URLs de streaming (TTL: 5 minutes)
|
|
stream_urls: Arc<MokaCache<String, StreamInfo>>,
|
|
}
|
|
|
|
impl QobuzCache {
|
|
/// Crée un nouveau cache avec les paramètres par défaut
|
|
pub fn new() -> Self {
|
|
Self::with_capacity(1000)
|
|
}
|
|
|
|
/// Crée un nouveau cache avec une capacité spécifique
|
|
pub fn with_capacity(max_capacity: u64) -> Self {
|
|
Self {
|
|
albums: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity)
|
|
.time_to_live(Duration::from_secs(3600)) // 1 heure
|
|
.build(),
|
|
),
|
|
tracks: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity * 2)
|
|
.time_to_live(Duration::from_secs(3600)) // 1 heure
|
|
.build(),
|
|
),
|
|
album_tracks: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity)
|
|
.time_to_live(Duration::from_secs(3600)) // 1 heure
|
|
.build(),
|
|
),
|
|
artists: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity / 2)
|
|
.time_to_live(Duration::from_secs(3600)) // 1 heure
|
|
.build(),
|
|
),
|
|
playlists: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity / 4)
|
|
.time_to_live(Duration::from_secs(1800)) // 30 minutes
|
|
.build(),
|
|
),
|
|
searches: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity / 2)
|
|
.time_to_live(Duration::from_secs(900)) // 15 minutes
|
|
.build(),
|
|
),
|
|
stream_urls: Arc::new(
|
|
MokaCache::builder()
|
|
.max_capacity(max_capacity / 4)
|
|
.time_to_live(Duration::from_secs(300)) // 5 minutes
|
|
.build(),
|
|
),
|
|
}
|
|
}
|
|
|
|
// ============ Albums ============
|
|
|
|
/// Récupère un album depuis le cache
|
|
pub async fn get_album(&self, id: &str) -> Option<Album> {
|
|
self.albums.get(id).await
|
|
}
|
|
|
|
/// Ajoute un album au cache
|
|
pub async fn put_album(&self, id: String, album: Album) {
|
|
self.albums.insert(id, album).await;
|
|
}
|
|
|
|
/// Invalide un album du cache
|
|
pub async fn invalidate_album(&self, id: &str) {
|
|
self.albums.invalidate(id).await;
|
|
}
|
|
|
|
// ============ Tracks ============
|
|
|
|
/// Récupère une track depuis le cache
|
|
pub async fn get_track(&self, id: &str) -> Option<Track> {
|
|
self.tracks.get(id).await
|
|
}
|
|
|
|
/// Ajoute une track au cache
|
|
pub async fn put_track(&self, id: String, track: Track) {
|
|
self.tracks.insert(id, track).await;
|
|
}
|
|
|
|
/// Invalide une track du cache
|
|
pub async fn invalidate_track(&self, id: &str) {
|
|
self.tracks.invalidate(id).await;
|
|
}
|
|
|
|
// ============ Album Tracks (liste complète des tracks d'un album) ============
|
|
|
|
/// Récupère la liste complète des tracks d'un album depuis le cache
|
|
pub async fn get_album_tracks(&self, album_id: &str) -> Option<Vec<Track>> {
|
|
self.album_tracks.get(album_id).await
|
|
}
|
|
|
|
/// Ajoute la liste complète des tracks d'un album au cache
|
|
pub async fn put_album_tracks(&self, album_id: String, tracks: Vec<Track>) {
|
|
self.album_tracks.insert(album_id, tracks).await;
|
|
}
|
|
|
|
/// Invalide la liste des tracks d'un album du cache
|
|
pub async fn invalidate_album_tracks(&self, album_id: &str) {
|
|
self.album_tracks.invalidate(album_id).await;
|
|
}
|
|
|
|
// ============ Artists ============
|
|
|
|
/// Récupère un artiste depuis le cache
|
|
pub async fn get_artist(&self, id: &str) -> Option<Artist> {
|
|
self.artists.get(id).await
|
|
}
|
|
|
|
/// Ajoute un artiste au cache
|
|
pub async fn put_artist(&self, id: String, artist: Artist) {
|
|
self.artists.insert(id, artist).await;
|
|
}
|
|
|
|
/// Invalide un artiste du cache
|
|
pub async fn invalidate_artist(&self, id: &str) {
|
|
self.artists.invalidate(id).await;
|
|
}
|
|
|
|
// ============ Playlists ============
|
|
|
|
/// Récupère une playlist depuis le cache
|
|
pub async fn get_playlist(&self, id: &str) -> Option<Playlist> {
|
|
self.playlists.get(id).await
|
|
}
|
|
|
|
/// Ajoute une playlist au cache
|
|
pub async fn put_playlist(&self, id: String, playlist: Playlist) {
|
|
self.playlists.insert(id, playlist).await;
|
|
}
|
|
|
|
/// Invalide une playlist du cache
|
|
pub async fn invalidate_playlist(&self, id: &str) {
|
|
self.playlists.invalidate(id).await;
|
|
}
|
|
|
|
// ============ Recherches ============
|
|
|
|
/// Récupère un résultat de recherche depuis le cache
|
|
pub async fn get_search(&self, query: &str) -> Option<SearchResult> {
|
|
self.searches.get(query).await
|
|
}
|
|
|
|
/// Ajoute un résultat de recherche au cache
|
|
pub async fn put_search(&self, query: String, result: SearchResult) {
|
|
self.searches.insert(query, result).await;
|
|
}
|
|
|
|
/// Invalide un résultat de recherche du cache
|
|
pub async fn invalidate_search(&self, query: &str) {
|
|
self.searches.invalidate(query).await;
|
|
}
|
|
|
|
// ============ URLs de streaming ============
|
|
|
|
/// Récupère une URL de streaming depuis le cache
|
|
pub async fn get_stream_url(&self, track_id: &str) -> Option<StreamInfo> {
|
|
self.stream_urls.get(track_id).await
|
|
}
|
|
|
|
/// Ajoute une URL de streaming au cache
|
|
pub async fn put_stream_url(&self, track_id: String, info: StreamInfo) {
|
|
self.stream_urls.insert(track_id, info).await;
|
|
}
|
|
|
|
/// Invalide une URL de streaming du cache
|
|
pub async fn invalidate_stream_url(&self, track_id: &str) {
|
|
self.stream_urls.invalidate(track_id).await;
|
|
}
|
|
|
|
// ============ Maintenance ============
|
|
|
|
/// Vide tous les caches
|
|
pub async fn clear_all(&self) {
|
|
self.albums.invalidate_all();
|
|
self.tracks.invalidate_all();
|
|
self.album_tracks.invalidate_all();
|
|
self.artists.invalidate_all();
|
|
self.playlists.invalidate_all();
|
|
self.searches.invalidate_all();
|
|
self.stream_urls.invalidate_all();
|
|
}
|
|
|
|
/// Retourne des statistiques sur le cache
|
|
pub async fn stats(&self) -> CacheStats {
|
|
self.albums.run_pending_tasks().await;
|
|
self.tracks.run_pending_tasks().await;
|
|
self.album_tracks.run_pending_tasks().await;
|
|
self.artists.run_pending_tasks().await;
|
|
self.playlists.run_pending_tasks().await;
|
|
self.searches.run_pending_tasks().await;
|
|
self.stream_urls.run_pending_tasks().await;
|
|
|
|
CacheStats {
|
|
albums_count: self.albums.entry_count(),
|
|
tracks_count: self.tracks.entry_count(),
|
|
album_tracks_count: self.album_tracks.entry_count(),
|
|
artists_count: self.artists.entry_count(),
|
|
playlists_count: self.playlists.entry_count(),
|
|
searches_count: self.searches.entry_count(),
|
|
stream_urls_count: self.stream_urls.entry_count(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for QobuzCache {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Statistiques du cache
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct CacheStats {
|
|
/// Nombre d'albums en cache
|
|
pub albums_count: u64,
|
|
/// Nombre de tracks en cache
|
|
pub tracks_count: u64,
|
|
/// Nombre de listes complètes de tracks d'albums en cache
|
|
pub album_tracks_count: u64,
|
|
/// Nombre d'artistes en cache
|
|
pub artists_count: u64,
|
|
/// Nombre de playlists en cache
|
|
pub playlists_count: u64,
|
|
/// Nombre de recherches en cache
|
|
pub searches_count: u64,
|
|
/// Nombre d'URLs de streaming en cache
|
|
pub stream_urls_count: u64,
|
|
}
|
|
|
|
impl CacheStats {
|
|
/// Retourne le nombre total d'entrées en cache
|
|
pub fn total_count(&self) -> u64 {
|
|
self.albums_count
|
|
+ self.tracks_count
|
|
+ self.album_tracks_count
|
|
+ self.artists_count
|
|
+ self.playlists_count
|
|
+ self.searches_count
|
|
+ self.stream_urls_count
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::models::Artist;
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_basic_operations() {
|
|
let cache = QobuzCache::new();
|
|
|
|
let artist = Artist::new("123", "Test Artist");
|
|
|
|
// Test insertion
|
|
cache.put_artist("123".to_string(), artist.clone()).await;
|
|
|
|
// Test récupération
|
|
let retrieved = cache.get_artist("123").await;
|
|
assert!(retrieved.is_some());
|
|
assert_eq!(retrieved.unwrap().name, "Test Artist");
|
|
|
|
// Test invalidation
|
|
cache.invalidate_artist("123").await;
|
|
let after_invalidation = cache.get_artist("123").await;
|
|
assert!(after_invalidation.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_stats() {
|
|
let cache = QobuzCache::new();
|
|
|
|
let artist = Artist::new("123", "Test Artist");
|
|
cache.put_artist("123".to_string(), artist).await;
|
|
|
|
let stats = cache.stats().await;
|
|
assert_eq!(stats.artists_count, 1);
|
|
assert_eq!(stats.albums_count, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_clear_all() {
|
|
let cache = QobuzCache::new();
|
|
|
|
let artist = Artist::new("123", "Test Artist");
|
|
cache.put_artist("123".to_string(), artist).await;
|
|
|
|
cache.clear_all().await;
|
|
|
|
let stats = cache.stats().await;
|
|
assert_eq!(stats.total_count(), 0);
|
|
}
|
|
}
|