Skip to content

Commit b48fe0f

Browse files
mm-elmazaniclaude
andcommitted
[PERF] Ajout de systèmes de cache pour optimiser les performances
- Cache thumbnail base64 (évite re-encodage à chaque update) - Cache skin actif (évite lecture fichier HTML à chaque requête) - Cache liste des skins (TTL 60s côté serveur, 30s côté GUI) - Cache images preview dans GUI (évite rechargement images) - Placeholder créé une seule fois et réutilisé Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3133954 commit b48fe0f

3 files changed

Lines changed: 169 additions & 41 deletions

File tree

server.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,16 @@ def get_skin_html(skin_name):
171171
return None
172172

173173

174-
def list_available_skins():
175-
"""Liste tous les skins disponibles avec leurs informations"""
174+
def list_available_skins(force_refresh=False):
175+
"""Liste tous les skins disponibles avec leurs informations (avec cache)"""
176+
global _skins_list_cache
177+
178+
# Utiliser le cache s'il est valide (cache de 60 secondes ou jusqu'à invalidation)
179+
if (not force_refresh
180+
and _skins_list_cache["skins"] is not None
181+
and (time.time() - _skins_list_cache["last_update"]) < 60):
182+
return _skins_list_cache["skins"]
183+
176184
skins_dir = Path("skins")
177185
available_skins = []
178186

@@ -212,11 +220,17 @@ def list_available_skins():
212220

213221
available_skins.append(skin_info)
214222

223+
# Mettre à jour le cache
224+
_skins_list_cache["skins"] = available_skins
225+
_skins_list_cache["last_update"] = time.time()
226+
215227
return available_skins
216228

217229

218230
def set_active_skin(skin_name):
219231
"""Change le skin actif et le sauvegarde dans la configuration"""
232+
global _active_skin_cache
233+
220234
skin_config_file = Path("config") / "active_skin.json"
221235

222236
# Vérifier que le skin existe
@@ -232,6 +246,10 @@ def set_active_skin(skin_name):
232246
with open(skin_config_file, 'w', encoding='utf-8') as f:
233247
json.dump(config, f, indent=2, ensure_ascii=False)
234248

249+
# Invalider le cache du skin
250+
_active_skin_cache["skin_id"] = None
251+
_active_skin_cache["html_content"] = None
252+
235253
print(f"[OK] Skin actif change pour : {skin_name}")
236254
return True, f"Skin changé pour : {skin_name}"
237255
except Exception as e:
@@ -272,6 +290,28 @@ def set_active_skin(skin_name):
272290
# Event pour le graceful shutdown
273291
shutdown_event = threading.Event()
274292

293+
# ============================================================================
294+
# CACHE SYSTÈME
295+
# ============================================================================
296+
297+
# Cache pour le thumbnail (évite de re-encoder en base64 si la piste n'a pas changé)
298+
_thumbnail_cache = {
299+
"track_key": None, # (title, artist, album) pour identifier la piste
300+
"thumbnail": "" # Le thumbnail en base64
301+
}
302+
303+
# Cache pour le skin actif (évite de relire le fichier à chaque requête)
304+
_active_skin_cache = {
305+
"skin_id": None,
306+
"html_content": None
307+
}
308+
309+
# Cache pour la liste des skins
310+
_skins_list_cache = {
311+
"skins": None,
312+
"last_update": 0
313+
}
314+
275315
# ============================================================================
276316
# FILTRE MÉDIA
277317
# ============================================================================
@@ -320,6 +360,8 @@ def is_app_allowed(app_id: str) -> bool:
320360

321361
async def get_media_info() -> Optional[Dict]:
322362
"""Récupère les informations de la piste en cours depuis Windows Media API"""
363+
global _thumbnail_cache
364+
323365
try:
324366
sessions = await MediaManager.request_async()
325367
current_session = sessions.get_current_session()
@@ -339,45 +381,60 @@ async def get_media_info() -> Optional[Dict]:
339381
playback_info = current_session.get_playback_info()
340382
timeline_props = current_session.get_timeline_properties()
341383

342-
# Récupérer la pochette d'album
343-
thumbnail_base64 = ""
344-
if info.thumbnail: # type: ignore[union-attr]
345-
try:
346-
thumb_stream_ref = info.thumbnail # type: ignore[union-attr]
347-
thumb_read_buffer = await thumb_stream_ref.open_read_async()
348-
349-
buffer = Buffer(thumb_read_buffer.size)
350-
await thumb_read_buffer.read_async(
351-
buffer,
352-
buffer.capacity,
353-
InputStreamOptions.READ_AHEAD
354-
)
355-
356-
reader = DataReader.from_buffer(buffer)
357-
byte_array = bytearray(buffer.length)
358-
reader.read_bytes(byte_array)
359-
360-
thumbnail_base64 = "data:image/jpeg;base64," + base64.b64encode(byte_array).decode('utf-8')
361-
except Exception as e:
362-
# Pas grave si la pochette n'est pas disponible
363-
pass
384+
# Extraire les infos de base
385+
title = info.title or "Unknown Title" # type: ignore[union-attr]
386+
artist = info.artist or "Unknown Artist" # type: ignore[union-attr]
387+
album = info.album_title or "" # type: ignore[union-attr]
388+
389+
# Clé unique pour identifier la piste
390+
track_key = (title, artist, album)
391+
392+
# Utiliser le cache du thumbnail si la piste n'a pas changé
393+
if _thumbnail_cache["track_key"] == track_key and _thumbnail_cache["thumbnail"]:
394+
thumbnail_base64 = _thumbnail_cache["thumbnail"]
395+
else:
396+
# Nouvelle piste, récupérer la pochette
397+
thumbnail_base64 = ""
398+
if info.thumbnail: # type: ignore[union-attr]
399+
try:
400+
thumb_stream_ref = info.thumbnail # type: ignore[union-attr]
401+
thumb_read_buffer = await thumb_stream_ref.open_read_async()
402+
403+
buffer = Buffer(thumb_read_buffer.size)
404+
await thumb_read_buffer.read_async(
405+
buffer,
406+
buffer.capacity,
407+
InputStreamOptions.READ_AHEAD
408+
)
409+
410+
reader = DataReader.from_buffer(buffer)
411+
byte_array = bytearray(buffer.length)
412+
reader.read_bytes(byte_array)
413+
414+
thumbnail_base64 = "data:image/jpeg;base64," + base64.b64encode(byte_array).decode('utf-8')
415+
except Exception:
416+
pass
417+
418+
# Mettre à jour le cache
419+
_thumbnail_cache["track_key"] = track_key
420+
_thumbnail_cache["thumbnail"] = thumbnail_base64
364421

365422
# Convertir les temps (timedelta) en secondes
366423
position_seconds = int(timeline_props.position.total_seconds()) if timeline_props.position else 0
367424
duration_seconds = int(timeline_props.end_time.total_seconds()) if timeline_props.end_time else 0
368425

369426
return {
370-
"title": info.title or "Unknown Title", # type: ignore[union-attr]
371-
"artist": info.artist or "Unknown Artist", # type: ignore[union-attr]
372-
"album": info.album_title or "", # type: ignore[union-attr]
427+
"title": title,
428+
"artist": artist,
429+
"album": album,
373430
"thumbnail": thumbnail_base64,
374431
"is_playing": playback_info.playback_status == 4, # 4 = Playing
375432
"position": position_seconds,
376433
"duration": duration_seconds,
377434
"source_app": source_app_id
378435
}
379436

380-
except Exception as e:
437+
except Exception:
381438
# Pas de musique en cours ou erreur
382439
return None
383440

@@ -647,15 +704,28 @@ def update_track_info():
647704

648705
@app.route('/')
649706
def index():
650-
"""Page d'accueil avec l'overlay - charge le skin actif"""
707+
"""Page d'accueil avec l'overlay - charge le skin actif (avec cache)"""
708+
global _active_skin_cache
709+
651710
active_skin = load_active_skin()
711+
712+
# Utiliser le cache si le skin n'a pas changé
713+
if (_active_skin_cache["skin_id"] == active_skin
714+
and _active_skin_cache["html_content"] is not None):
715+
return _active_skin_cache["html_content"]
716+
717+
# Charger le nouveau skin
652718
skin_html = get_skin_html(active_skin)
653719

654720
# Si le skin n'est pas trouvé, utiliser le template par défaut
655721
if skin_html is None:
656722
print(f"[WARN] Skin {active_skin} introuvable, utilisation du template par defaut")
657723
return render_template_string(OVERLAY_HTML)
658724

725+
# Mettre à jour le cache
726+
_active_skin_cache["skin_id"] = active_skin
727+
_active_skin_cache["html_content"] = skin_html
728+
659729
return skin_html
660730

661731

src/gui.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ def __init__(self, root):
4343
# Référence pour l'image de preview (éviter garbage collection)
4444
self.preview_image = None
4545

46+
# Cache pour les images de preview {skin_id: PhotoImage}
47+
self._preview_cache: Dict[str, any] = {}
48+
49+
# Cache pour le placeholder par défaut
50+
self._placeholder_image = None
51+
4652
# System Tray
4753
self.tray_icon = None
4854
self.setup_system_tray()
@@ -574,9 +580,16 @@ def update_preview(self, skin):
574580
self.load_preview_image(skin.get('id', ''))
575581

576582
def load_preview_image(self, skin_id):
577-
"""Charge l'image de preview si disponible, sinon affiche un placeholder"""
583+
"""Charge l'image de preview si disponible, sinon affiche un placeholder (avec cache)"""
578584
if not skin_id:
579-
self.create_placeholder_preview("Aucun skin")
585+
self._show_placeholder()
586+
return
587+
588+
# Vérifier le cache
589+
if skin_id in self._preview_cache:
590+
photo = self._preview_cache[skin_id]
591+
self.preview_image_label.config(image=photo)
592+
self.preview_image = photo
580593
return
581594

582595
# Chercher preview.png dans le dossier du skin
@@ -593,17 +606,28 @@ def load_preview_image(self, skin_id):
593606

594607
photo = ImageTk.PhotoImage(img)
595608

609+
# Mettre en cache
610+
self._preview_cache[skin_id] = photo
611+
596612
self.preview_image_label.config(image=photo)
597613
self.preview_image = photo # Garder la référence
598614
except Exception:
599615
# En cas d'erreur, afficher le placeholder
600-
self.create_placeholder_preview(skin_id)
616+
self._show_placeholder()
601617
else:
602-
# Pas de preview.png, créer un placeholder
603-
self.create_placeholder_preview(skin_id)
618+
# Pas de preview.png, afficher le placeholder
619+
self._show_placeholder()
604620

605-
def create_placeholder_preview(self, text):
606-
"""Crée une image placeholder avec le texte donné"""
621+
def _show_placeholder(self):
622+
"""Affiche le placeholder en utilisant le cache"""
623+
if self._placeholder_image is None:
624+
self._placeholder_image = self._create_placeholder_image()
625+
626+
self.preview_image_label.config(image=self._placeholder_image)
627+
self.preview_image = self._placeholder_image
628+
629+
def _create_placeholder_image(self):
630+
"""Crée l'image placeholder une seule fois"""
607631
# Créer une image de fond (taille augmentée)
608632
width, height = 500, 300
609633
img = Image.new('RGB', (width, height), color='#1a1a2e')
@@ -639,9 +663,11 @@ def create_placeholder_preview(self, text):
639663
draw.text((x, y), label, fill='#888888', font=font)
640664

641665
# Convertir en PhotoImage
642-
photo = ImageTk.PhotoImage(img)
643-
self.preview_image_label.config(image=photo)
644-
self.preview_image = photo # Garder la référence
666+
return ImageTk.PhotoImage(img)
667+
668+
def create_placeholder_preview(self, text):
669+
"""Crée une image placeholder (utilise le cache)"""
670+
self._show_placeholder()
645671

646672
# ========================================================================
647673
# MÉTHODES DE GESTION DES PARAMÈTRES (utilise ConfigManager)

src/skin_manager.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import json
7+
import time
78
import requests
89
from pathlib import Path
910

@@ -19,6 +20,11 @@ def __init__(self, base_dir=None):
1920
self.config_dir = self.base_dir / "config"
2021
self.active_skin_file = self.config_dir / "active_skin.json"
2122

23+
# Cache pour la liste des skins
24+
self._skins_cache = None
25+
self._skins_cache_time = 0
26+
self._cache_ttl = 30 # 30 secondes de TTL
27+
2228
def load_skins_from_api(self, server_url):
2329
"""
2430
Charge la liste des skins depuis l'API Flask
@@ -38,13 +44,30 @@ def load_skins_from_api(self, server_url):
3844
pass
3945
return None
4046

41-
def load_skins_from_files(self):
47+
def load_skins_from_files(self, force_refresh=False):
4248
"""
43-
Charge la liste des skins directement depuis les fichiers (fallback)
49+
Charge la liste des skins directement depuis les fichiers (avec cache)
50+
51+
Args:
52+
force_refresh: Forcer le rechargement du cache
4453
4554
Returns:
4655
dict: {"skins": [...], "active_skin": "..."}
4756
"""
57+
current_time = time.time()
58+
59+
# Utiliser le cache s'il est valide
60+
if (not force_refresh
61+
and self._skins_cache is not None
62+
and (current_time - self._skins_cache_time) < self._cache_ttl):
63+
# Mettre à jour le skin actif (peut changer sans que la liste change)
64+
active_skin = self.get_active_skin_id()
65+
return {
66+
"skins": self._skins_cache,
67+
"active_skin": active_skin,
68+
"count": len(self._skins_cache)
69+
}
70+
4871
skins = []
4972

5073
if not self.skins_dir.exists():
@@ -78,6 +101,10 @@ def load_skins_from_files(self):
78101

79102
skins.append(skin_info)
80103

104+
# Mettre à jour le cache
105+
self._skins_cache = skins
106+
self._skins_cache_time = current_time
107+
81108
# Charger le skin actif
82109
active_skin = self.get_active_skin_id()
83110

@@ -87,6 +114,11 @@ def load_skins_from_files(self):
87114
"count": len(skins)
88115
}
89116

117+
def invalidate_cache(self):
118+
"""Invalide le cache des skins"""
119+
self._skins_cache = None
120+
self._skins_cache_time = 0
121+
90122
def get_active_skin_id(self):
91123
"""
92124
Lit le skin actif depuis le fichier de configuration

0 commit comments

Comments
 (0)