v1.9.5 bug 1fichier et musique repetée resolu

This commit is contained in:
skymike03
2025-07-13 21:46:46 +02:00
parent d553cc0825
commit 78343143ad
6 changed files with 896 additions and 886 deletions
+294 -275
View File
@@ -1,19 +1,18 @@
import pygame# type: ignore
import os import os
os.environ["SDL_FBDEV"] = "/dev/fb0" os.environ["SDL_FBDEV"] = "/dev/fb0"
import pygame # type: ignore
import asyncio import asyncio
import platform import platform
import logging import logging
import requests import requests
import config from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_virtual_keyboard, draw_popup_result_download, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, draw_history_list, draw_clear_history_dialog, draw_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, draw_gradient, THEME_COLORS
from config import logger from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates
from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_gradient, draw_virtual_keyboard, draw_popup_result_download, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, draw_history_list, draw_clear_history_dialog, draw_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, THEME_COLORS, draw_music_popup
from network import test_internet, download_rom, check_extension_before_download, extract_zip, check_for_updates
from controls import handle_controls, validate_menu_state from controls import handle_controls, validate_menu_state
from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS
from utils import play_random_music, load_sources, detect_non_pc from utils import detect_non_pc, load_sources, check_extension_before_download, extract_zip, play_random_music
from history import load_history from history import load_history
from config import OTA_data_ZIP import config
from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT, OTA_data_ZIP
# Configuration du logging # Configuration du logging
log_dir = "/userdata/roms/ports/RGSX/logs" log_dir = "/userdata/roms/ports/RGSX/logs"
@@ -34,44 +33,35 @@ except Exception as e:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Initialisation de Pygame # Initialisation de Pygame et des polices
pygame.init() pygame.init()
config.init_font() config.init_font()
pygame.joystick.init() pygame.joystick.init()
pygame.mouse.set_visible(True) pygame.mouse.set_visible(True)
# Détection du système # Détection du système non-PC
config.is_non_pc = detect_non_pc() config.is_non_pc = detect_non_pc()
# Initialisation des polices
try:
config.font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36)
config.title_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48)
config.search_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48)
config.progress_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36)
config.small_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 28)
logger.debug("Police Pixel-UniCode chargée")
except:
config.font = pygame.font.SysFont("arial", 48)
config.title_font = pygame.font.SysFont("arial", 60)
config.search_font = pygame.font.SysFont("arial", 60)
config.progress_font = pygame.font.SysFont("arial", 36)
config.small_font = pygame.font.SysFont("arial", 28)
logger.debug("Police Arial chargée")
# Initialisation de l’écran # Initialisation de l’écran
screen = init_display() screen = init_display()
pygame.display.set_caption("RGSX") pygame.display.set_caption("RGSX")
clock = pygame.time.Clock()
# Afficher un écran de chargement initial # Initialisation des polices
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"]) try:
loading_text = config.font.render("Initialisation...", True, (255, 255, 255)) config.font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36) # Police principale
text_rect = loading_text.get_rect(center=(config.screen_width // 2, config.screen_height // 2)) config.title_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48) # Police pour les titres
screen.blit(loading_text, text_rect) config.search_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48) # Police pour la recherche
pygame.display.flip() config.progress_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36) # Police pour l'affichage de la progression
logger.debug("Écran de chargement initial affiché") config.small_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 28) # Police pour les petits textes
logger.debug("Police Pixel-UniCode chargée")
except:
config.font = pygame.font.SysFont("arial", 48) # Police fallback
config.title_font = pygame.font.SysFont("arial", 60) # Police fallback pour les titres
config.search_font = pygame.font.SysFont("arial", 60) # Police fallback pour la recherche
config.progress_font = pygame.font.SysFont("arial", 36) # Police fallback pour l'affichage de la progression
config.small_font = pygame.font.SysFont("arial", 28) # Police fallback pour les petits textes
logger.debug("Police Arial chargée")
# Mise à jour de la résolution dans config # Mise à jour de la résolution dans config
config.screen_width, config.screen_height = pygame.display.get_surface().get_size() config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
@@ -83,6 +73,25 @@ config.selected_platform = 0
config.selected_key = (0, 0) config.selected_key = (0, 0)
config.transition_state = "none" config.transition_state = "none"
# Initialisation des variables de répétition
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = 0
# Initialisation des variables pour la popup de musique
current_music_name = None
music_popup_start_time = 0
# Dossier musique Batocera
music_folder = "/userdata/roms/ports/RGSX/assets/music"
music_files = [f for f in os.listdir(music_folder) if f.lower().endswith(('.ogg', '.mp3'))]
current_music = None # Variable pour suivre la musique en cours
if music_files:
current_music = play_random_music(music_files, music_folder, current_music)
else:
logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/music")
# Chargement de l'historique # Chargement de l'historique
config.history = load_history() config.history = load_history()
logger.debug(f"Historique chargé: {len(config.history)} entrées") logger.debug(f"Historique chargé: {len(config.history)} entrées")
@@ -101,14 +110,14 @@ if pygame.joystick.get_count() > 0:
joystick.init() joystick.init()
logger.debug("Gamepad initialisé") logger.debug("Gamepad initialisé")
# Initialisation de pygame.mixer # Initialisation du mixer Pygame
pygame.mixer.pre_init(44100, -16, 2, 4096)
pygame.mixer.init() pygame.mixer.init()
# Jouer la première musique au démarrage
play_random_music()
# Boucle principale # Boucle principale
async def main(): async def main():
global current_music, music_files, music_folder
logger.debug("Début main") logger.debug("Début main")
running = True running = True
loading_step = "none" loading_step = "none"
@@ -117,33 +126,27 @@ async def main():
config.debounce_delay = 50 config.debounce_delay = 50
config.update_triggered = False config.update_triggered = False
last_redraw_time = pygame.time.get_ticks() last_redraw_time = pygame.time.get_ticks()
config.last_frame_time = pygame.time.get_ticks() # Initialisation pour éviter erreur
screen = init_display()
clock = pygame.time.Clock() clock = pygame.time.Clock()
# Variables pour la progression simulée
check_ota_start_time = None
load_sources_start_time = None
SIMULATED_CHECK_OTA_DURATION = 5.0
SIMULATED_LOAD_SOURCES_DURATION = 3.0
while running: while running:
clock.tick(60) clock.tick(60) # Limite à 60 FPS
if config.update_triggered: if config.update_triggered:
logger.debug("Mise à jour déclenchée, arrêt de la boucle principale") logger.debug("Mise à jour déclenchée, arrêt de la boucle principale")
break break
current_time = pygame.time.get_ticks() current_time = pygame.time.get_ticks()
current_time_sec = current_time / 1000.0
# Forcer redraw toutes les 100 ms dans download_progress # Forcer redraw toutes les 100 ms dans download_progress
if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100: if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100:
config.needs_redraw = True config.needs_redraw = True
last_redraw_time = current_time last_redraw_time = current_time
# Gestion du popup timer # Gestion de la fin du popup
delta_time = current_time - config.last_frame_time
config.last_frame_time = current_time
if config.menu_state == "restart_popup" and config.popup_timer > 0: if config.menu_state == "restart_popup" and config.popup_timer > 0:
config.popup_timer -= delta_time config.popup_timer -= (current_time - config.last_frame_time)
config.needs_redraw = True config.needs_redraw = True
if config.popup_timer <= 0: if config.popup_timer <= 0:
config.menu_state = validate_menu_state(config.previous_menu_state) config.menu_state = validate_menu_state(config.previous_menu_state)
@@ -155,15 +158,18 @@ async def main():
# Gestion des événements # Gestion des événements
events = pygame.event.get() events = pygame.event.get()
for event in events: for event in events:
if event.type == pygame.USEREVENT + 1: # Événement de fin de musique
logger.debug("Fin de la musique détectée, lecture d'une nouvelle musique aléatoire")
current_music = play_random_music(music_files, music_folder, current_music)
continue
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
config.menu_state = "confirm_exit" config.menu_state = "confirm_exit"
config.confirm_selection = 0 config.confirm_selection = 0
config.needs_redraw = True config.needs_redraw = True
logger.debug("Événement QUIT détecté, passage à confirm_exit") logger.debug("Événement QUIT détecté, passage à confirm_exit")
continue continue
elif event.type == pygame.USEREVENT + 1:
logger.debug("Fin de la musique actuelle, passage à la suivante")
play_random_music()
start_config = config.controls_config.get("start", {}) start_config = config.controls_config.get("start", {})
if start_config and ( if start_config and (
(event.type == pygame.KEYDOWN and start_config.get("type") == "key" and event.key == start_config.get("value")) or (event.type == pygame.KEYDOWN and start_config.get("type") == "key" and event.key == start_config.get("value")) or
@@ -179,9 +185,9 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Ouverture menu pause depuis {config.previous_menu_state}") logger.debug(f"Ouverture menu pause depuis {config.previous_menu_state}")
continue continue
if config.menu_state == "pause_menu": if config.menu_state == "pause_menu":
handle_controls(event, sources, joystick, screen) action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}") logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}")
continue continue
@@ -201,12 +207,13 @@ async def main():
continue continue
if config.menu_state == "confirm_clear_history": if config.menu_state == "confirm_clear_history":
handle_controls(event, sources, joystick, screen) action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans confirm_clear_history: {event.type}") logger.debug(f"Événement transmis à handle_controls dans confirm_clear_history: {event.type}")
continue continue
if config.menu_state == "redownload_game_cache": if config.menu_state == "redownload_game_cache":
handle_controls(event, sources, joystick, screen) action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}") logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}")
continue continue
@@ -223,21 +230,57 @@ async def main():
platform = config.platforms[config.current_platform] platform = config.platforms[config.current_platform]
url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None
if url: if url:
logger.debug(f"Vérification de l'extension pour {game_name}, URL: {url}") logger.debug(f"Vérification pour {game_name}, URL: {url}")
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name) if is_1fichier_url(url):
if not is_supported: if not config.API_KEY_1FICHIER:
config.pending_download = (url, platform, game_name, is_zip_non_supported) config.previous_menu_state = config.menu_state
config.menu_state = "extension_warning" config.menu_state = "error"
config.extension_confirm_selection = 0 config.error_message = (
config.needs_redraw = True "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt"
logger.debug(f"Extension non reconnue, passage à extension_warning pour {game_name}") )
config.needs_redraw = True
logger.error("Clé API 1fichier absente")
config.pending_download = None
continue
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
if not is_supported:
config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non reconnue pour lien 1fichier, passage à extension_warning pour {game_name}")
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
success, message = download_from_1fichier(url, platform, game_name, is_zip_non_supported)
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Téléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}")
else: else:
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
config.download_tasks[task] = (task, url, game_name, platform) if not is_supported:
config.menu_state = "download_progress" config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.pending_download = None config.menu_state = "extension_warning"
config.needs_redraw = True config.extension_confirm_selection = 0
logger.debug(f"Téléchargement démarré pour {game_name}, passage à download_progress") config.needs_redraw = True
logger.debug(f"Extension non reconnue, passage à extension_warning pour {game_name}")
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
success, message = download_rom(url, platform, game_name, is_zip_non_supported)
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Téléchargement terminé pour {game_name}, succès={success}, message={message}")
elif action == "redownload" and config.menu_state == "history" and config.history: elif action == "redownload" and config.menu_state == "history" and config.history:
entry = config.history[config.current_history_item] entry = config.history[config.current_history_item]
platform = entry["platform"] platform = entry["platform"]
@@ -245,20 +288,57 @@ async def main():
for game in config.games: for game in config.games:
if game[0] == game_name and config.platforms[config.current_platform] == platform: if game[0] == game_name and config.platforms[config.current_platform] == platform:
url = game[1] url = game[1]
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name) logger.debug(f"Vérification pour retéléchargement de {game_name}, URL: {url}")
if not is_supported: if is_1fichier_url(url):
config.pending_download = (url, platform, game_name, is_zip_non_supported) if not config.API_KEY_1FICHIER:
config.menu_state = "extension_warning" config.previous_menu_state = config.menu_state
config.extension_confirm_selection = 0 config.menu_state = "error"
config.needs_redraw = True config.error_message = (
logger.debug(f"Extension non reconnue pour retéléchargement, passage à extension_warning pour {game_name}") "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt"
)
config.needs_redraw = True
logger.error("Clé API 1fichier absente")
config.pending_download = None
continue
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
if not is_supported:
config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non reconnue pour lien 1fichier, passage à extension_warning pour {game_name}")
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
success, message = download_from_1fichier(url, platform, game_name, is_zip_non_supported)
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Retéléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}")
else: else:
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
config.download_tasks[task] = (task, url, game_name, platform) if not is_supported:
config.menu_state = "download_progress" config.pending_download = (url, platform, game_name, is_zip_non_supported)
config.pending_download = None config.menu_state = "extension_warning"
config.needs_redraw = True config.extension_confirm_selection = 0
logger.debug(f"Retéléchargement démarré pour {game_name}, passage à download_progress") config.needs_redraw = True
logger.debug(f"Extension non reconnue pour retéléchargement, passage à extension_warning pour {game_name}")
else:
config.previous_menu_state = config.menu_state
logger.debug(f"Previous menu state défini: {config.previous_menu_state}")
success, message = download_rom(url, platform, game_name, is_zip_non_supported)
config.download_result_message = message
config.download_result_error = not success
config.download_result_start_time = pygame.time.get_ticks()
config.menu_state = "download_result"
config.download_progress.clear()
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Retéléchargement terminé pour {game_name}, succès={success}, message={message}")
break break
# Gestion des téléchargements # Gestion des téléchargements
@@ -295,197 +375,6 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Fin popup download_result, retour à {config.menu_state}") logger.debug(f"Fin popup download_result, retour à {config.menu_state}")
# Gestion de l'état loading
if config.menu_state == "loading":
logger.debug(f"Étape chargement : {loading_step}")
if loading_step == "none":
loading_step = "init_sources"
config.current_loading_system = "Chargement des sources..."
config.loading_progress = 0.0
config.needs_redraw = True
load_sources_start_time = current_time_sec
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "init_sources":
if load_sources_start_time is None:
load_sources_start_time = current_time_sec
# Simuler la progression pour init_sources
elapsed = current_time_sec - load_sources_start_time
progress = min(0.0 + (5.0 * elapsed / SIMULATED_LOAD_SOURCES_DURATION), 5.0)
config.loading_progress = progress
config.needs_redraw = True
logger.debug(f"Progression simulée init_sources : {config.loading_progress}%")
# Exécuter load_sources
sources = load_sources()
if not sources:
config.menu_state = "error"
config.error_message = "Échec du chargement de sources.json"
config.needs_redraw = True
logger.debug("Erreur : Échec du chargement de sources.json")
else:
loading_step = "test_internet"
config.current_loading_system = "Test de connexion..."
config.loading_progress = 5.0
load_sources_start_time = None
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "test_internet":
logger.debug("Exécution de test_internet()")
if test_internet():
loading_step = "check_ota"
config.current_loading_system = "Vérification des mises à jour..."
config.loading_progress = 5.0
check_ota_start_time = current_time_sec
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else:
config.menu_state = "error"
config.error_message = "Pas de connexion Internet. Vérifiez votre réseau."
config.needs_redraw = True
logger.debug(f"Erreur : {config.error_message}")
elif loading_step == "check_ota":
if check_ota_start_time is None:
check_ota_start_time = current_time_sec
# Simuler la progression pour check_ota
elapsed = current_time_sec - check_ota_start_time
progress = min(5.0 + (25.0 * elapsed / SIMULATED_CHECK_OTA_DURATION), 30.0)
config.loading_progress = progress
config.needs_redraw = True
logger.debug(f"Progression simulée check_ota : {config.loading_progress}%")
# Exécuter check_for_updates
success, message = await check_for_updates()
logger.debug(f"Résultat de check_for_updates : success={success}, message={message}")
if not success:
config.menu_state = "error"
config.error_message = message
config.needs_redraw = True
logger.debug(f"Erreur OTA : {message}")
else:
loading_step = "check_data"
config.current_loading_system = "Téléchargement des jeux et images..."
config.loading_progress = 30.0
check_ota_start_time = None
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "check_data":
games_data_dir = "/userdata/roms/ports/RGSX/games"
is_data_empty = not os.path.exists(games_data_dir) or not any(os.scandir(games_data_dir))
if is_data_empty:
config.current_loading_system = "Téléchargement du Dossier Data initial..."
config.loading_progress = 30.0
config.needs_redraw = True
logger.debug("Dossier Data vide, début du téléchargement du ZIP")
try:
zip_path = "/userdata/roms/ports/RGSX.zip"
headers = {'User-Agent': 'Mozilla/5.0'}
with requests.get(OTA_data_ZIP, stream=True, headers=headers, timeout=30) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale du ZIP : {total_size} octets")
downloaded = 0
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
with open(zip_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
config.download_progress[OTA_data_ZIP] = {
"downloaded_size": downloaded,
"total_size": total_size,
"status": "Téléchargement",
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
}
config.loading_progress = 30.0 + (40.0 * downloaded / total_size) if total_size > 0 else 30.0
config.needs_redraw = True
await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {zip_path}")
config.current_loading_system = "Extraction du Dossier Data initial..."
config.loading_progress = 70.0
config.needs_redraw = True
dest_dir = "/userdata/roms/ports/RGSX"
success, message = extract_zip(zip_path, dest_dir, OTA_data_ZIP)
if success:
logger.debug(f"Extraction réussie : {message}")
config.loading_progress = 70.0
config.needs_redraw = True
else:
raise Exception(f"Échec de l'extraction : {message}")
except Exception as e:
logger.error(f"Erreur lors du téléchargement/extraction du Dossier Data : {str(e)}")
config.menu_state = "error"
config.error_message = f"Échec du téléchargement/extraction du Dossier Data : {str(e)}"
config.needs_redraw = True
loading_step = "load_sources"
if os.path.exists(zip_path):
os.remove(zip_path)
continue
if os.path.exists(zip_path):
os.remove(zip_path)
logger.debug(f"Fichier ZIP {zip_path} supprimé")
loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 70.0
load_sources_start_time = current_time_sec
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else:
loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 70.0
load_sources_start_time = current_time_sec
config.needs_redraw = True
logger.debug(f"Dossier Data non vide, passage à {loading_step}")
elif loading_step == "load_sources":
if load_sources_start_time is None:
load_sources_start_time = current_time_sec
# Simuler la progression pour load_sources
elapsed = current_time_sec - load_sources_start_time
progress = min(70.0 + (30.0 * elapsed / SIMULATED_LOAD_SOURCES_DURATION), 100.0)
config.loading_progress = progress
config.needs_redraw = True
logger.debug(f"Progression simulée load_sources : {config.loading_progress}%")
# Exécuter load_sources
sources = load_sources()
if not sources:
config.menu_state = "error"
config.error_message = "Échec du chargement de sources.json"
config.needs_redraw = True
logger.debug("Erreur : Échec du chargement de sources.json")
else:
config.menu_state = "platform"
config.loading_progress = 0.0
config.current_loading_system = ""
load_sources_start_time = None
config.needs_redraw = True
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
# Gestion de l'état de transition
if config.transition_state == "to_game":
config.transition_progress += 1
if config.transition_progress >= config.transition_duration:
config.menu_state = "game"
config.transition_state = "idle"
config.transition_progress = 0.0
config.needs_redraw = True
logger.debug("Transition terminée, passage à game")
# Affichage # Affichage
if config.needs_redraw: if config.needs_redraw:
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"]) draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
@@ -513,6 +402,7 @@ async def main():
draw_extension_warning(screen) draw_extension_warning(screen)
elif config.menu_state == "pause_menu": elif config.menu_state == "pause_menu":
draw_pause_menu(screen, config.selected_option) draw_pause_menu(screen, config.selected_option)
logger.debug("Rendu de draw_pause_menu")
elif config.menu_state == "controls_help": elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state) draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history": elif config.menu_state == "history":
@@ -529,12 +419,12 @@ async def main():
config.needs_redraw = True config.needs_redraw = True
logger.error(f"État de menu non valide détecté: {config.menu_state}, retour à platform") logger.error(f"État de menu non valide détecté: {config.menu_state}, retour à platform")
draw_controls(screen, config.menu_state) draw_controls(screen, config.menu_state)
draw_music_popup(screen)
pygame.display.flip() pygame.display.flip()
config.needs_redraw = False config.needs_redraw = False
# Gestion de l'état controls_mapping # Gestion de l'état controls_mapping
if config.menu_state == "controls_mapping": if config.menu_state == "controls_mapping":
logger.debug("Avant appel de map_controls")
try: try:
success = map_controls(screen) success = map_controls(screen)
logger.debug(f"map_controls terminé, succès={success}") logger.debug(f"map_controls terminé, succès={success}")
@@ -542,6 +432,7 @@ async def main():
config.controls_config = load_controls_config() config.controls_config = load_controls_config()
config.menu_state = "loading" config.menu_state = "loading"
config.needs_redraw = True config.needs_redraw = True
logger.debug("Passage à l'état loading après mappage")
else: else:
config.menu_state = "error" config.menu_state = "error"
config.error_message = "Échec du mappage des contrôles" config.error_message = "Échec du mappage des contrôles"
@@ -553,6 +444,134 @@ async def main():
config.error_message = f"Erreur dans map_controls: {str(e)}" config.error_message = f"Erreur dans map_controls: {str(e)}"
config.needs_redraw = True config.needs_redraw = True
# Gestion de l'état loading
elif config.menu_state == "loading":
if loading_step == "none":
loading_step = "test_internet"
config.current_loading_system = "Test de connexion..."
config.loading_progress = 0.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "test_internet":
logger.debug("Exécution de test_internet()")
if test_internet():
loading_step = "check_ota"
config.current_loading_system = "Mise à jour en cours... Patientez si l'ecran reste figé.. Puis relancer l'application une fois qu'elle est terminée."
config.loading_progress = 20.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else:
config.menu_state = "error"
config.error_message = "Pas de connexion Internet. Vérifiez votre réseau."
config.needs_redraw = True
logger.debug(f"Erreur : {config.error_message}")
elif loading_step == "check_ota":
logger.debug("Exécution de check_for_updates()")
success, message = await check_for_updates()
logger.debug(f"Résultat de check_for_updates : success={success}, message={message}")
if not success:
config.menu_state = "error"
config.error_message = message
config.needs_redraw = True
logger.debug(f"Erreur OTA : {message}")
else:
loading_step = "check_data"
config.current_loading_system = "Téléchargement des jeux et images ..."
config.loading_progress = 50.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
elif loading_step == "check_data":
games_data_dir = "/userdata/roms/ports/RGSX/games"
is_data_empty = not os.path.exists(games_data_dir) or not any(os.scandir(games_data_dir))
if is_data_empty:
config.current_loading_system = "Téléchargement du Dossier Data initial..."
config.loading_progress = 30.0
config.needs_redraw = True
logger.debug("Dossier Data vide, début du téléchargement du ZIP")
try:
zip_path = "/userdata/roms/ports/RGSX.zip"
headers = {'User-Agent': 'Mozilla/5.0'}
with requests.get(OTA_data_ZIP, stream=True, headers=headers, timeout=30) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale du ZIP : {total_size} octets")
downloaded = 0
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
with open(zip_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
config.download_progress[OTA_data_ZIP] = {
"downloaded_size": downloaded,
"total_size": total_size,
"status": "Téléchargement",
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
}
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
config.needs_redraw = True
await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {zip_path}")
config.current_loading_system = "Extraction du Dossier Data initial..."
config.loading_progress = 60.0
config.needs_redraw = True
dest_dir = "/userdata/roms/ports/RGSX"
success, message = extract_zip(zip_path, dest_dir, OTA_data_ZIP)
if success:
logger.debug(f"Extraction réussie : {message}")
config.loading_progress = 70.0
config.needs_redraw = True
else:
raise Exception(f"Échec de l'extraction : {message}")
except Exception as e:
logger.error(f"Erreur lors du téléchargement/extraction du Dossier Data : {str(e)}")
config.menu_state = "error"
config.error_message = f"Échec du téléchargement/extraction du Dossier Data : {str(e)}"
config.needs_redraw = True
loading_step = "load_sources"
if os.path.exists(zip_path):
os.remove(zip_path)
continue
if os.path.exists(zip_path):
os.remove(zip_path)
logger.debug(f"Fichier ZIP {zip_path} supprimé")
loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
else:
loading_step = "load_sources"
config.current_loading_system = "Chargement des systèmes..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Dossier Data non vide, passage à {loading_step}")
elif loading_step == "load_sources":
sources = load_sources()
if not sources:
config.menu_state = "error"
config.error_message = "Échec du chargement de sources.json"
config.needs_redraw = True
logger.debug("Erreur : Échec du chargement de sources.json")
else:
config.menu_state = "platform"
config.loading_progress = 100.0
config.current_loading_system = ""
config.needs_redraw = True
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
# Gestion de l'état de transition
if config.transition_state == "to_game":
config.transition_progress += 1
if config.transition_progress >= config.transition_duration:
config.menu_state = "game"
config.transition_state = "idle"
config.transition_progress = 0.0
config.needs_redraw = True
logger.debug("Transition terminée, passage à game")
config.last_frame_time = current_time
clock.tick(60) clock.tick(60)
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
+20 -7
View File
@@ -5,7 +5,8 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Version actuelle de l'application # Version actuelle de l'application
app_version = "1.9.4" app_version = "1.9.5"
# URL du serveur OTA # URL du serveur OTA
OTA_SERVER_URL = "https://retrogamesets.fr/softs" OTA_SERVER_URL = "https://retrogamesets.fr/softs"
@@ -13,17 +14,22 @@ OTA_VERSION_ENDPOINT = f"{OTA_SERVER_URL}/version.json"
OTA_UPDATE_SCRIPT = f"{OTA_SERVER_URL}/rgsx-update.sh" OTA_UPDATE_SCRIPT = f"{OTA_SERVER_URL}/rgsx-update.sh"
OTA_data_ZIP = f"{OTA_SERVER_URL}/rgsx-data.zip" OTA_data_ZIP = f"{OTA_SERVER_URL}/rgsx-data.zip"
# Constantes pour la répétition automatique dans pause_menu
REPEAT_DELAY = 300 # Délai initial avant répétition (ms)
REPEAT_INTERVAL = 150 # Intervalle entre répétitions (ms), augmenté pour réduire la fréquence
REPEAT_ACTION_DEBOUNCE = 100 # Délai anti-rebond pour répétitions (ms), augmenté pour éviter les répétitions excessives
# Variables d'état # Variables d'état
platforms = [] # Liste des plateformes chargées depuis sources.json platforms = []
current_platform = 0 # Index de la plateforme actuellement sélectionnée current_platform = 0
platform_names = {} # {platform_id: platform_name} platform_names = {} # {platform_id: platform_name}
games = [] # Liste des jeux chargés pour la plateforme actuelle games = []
current_game = 0 # Index du jeu actuellement sélectionné current_game = 0
menu_state = "" # État actuel du menu (par exemple, "main_menu", "game_list", "settings", etc.) menu_state = "popup"
confirm_choice = False confirm_choice = False
scroll_offset = 0 scroll_offset = 0
visible_games = 15 visible_games = 15
popup_start_time = 0 popup_start_time = 0
last_progress_update = 0 last_progress_update = 0
needs_redraw = True needs_redraw = True
@@ -39,6 +45,10 @@ download_result_start_time = 0
loading_progress = 0.0 loading_progress = 0.0
current_loading_system = "" current_loading_system = ""
error_message = "" error_message = ""
repeat_action = None
repeat_start_time = 0
repeat_last_action = 0
repeat_key = None
filtered_games = [] filtered_games = []
search_mode = False search_mode = False
search_query = "" search_query = ""
@@ -90,6 +100,9 @@ CONTROLS_CONFIG_PATH = "/userdata/saves/ports/rgsx/controls.json"
"""Chemin du fichier de configuration des contrôles.""" """Chemin du fichier de configuration des contrôles."""
HISTORY_PATH = "/userdata/saves/ports/rgsx/history.json" HISTORY_PATH = "/userdata/saves/ports/rgsx/history.json"
"""Chemin du fichier de l'historique des téléchargements.""" """Chemin du fichier de l'historique des téléchargements."""
JSON_EXTENSIONS = "/userdata/roms/ports/RGSX/rom_extensions.json"
"""Chemin du fichier JSON des extensions de ROMs."""
def init_font(): def init_font():
"""Initialise les polices après pygame.init().""" """Initialise les polices après pygame.init()."""
+144 -89
View File
@@ -6,19 +6,19 @@ import asyncio
import json import json
import os import os
from display import draw_validation_transition from display import draw_validation_transition
from network import download_rom, check_extension_before_download, download_from_1fichier, is_1fichier_url, is_extension_supported,load_extensions_json,sanitize_filename from network import download_rom, download_from_1fichier, is_1fichier_url
from utils import load_games from utils import load_games, check_extension_before_download, is_extension_supported, load_extensions_json, sanitize_filename
from history import load_history, clear_history from history import load_history, clear_history
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Constantes pour la répétition automatique # Constantes pour la répétition automatique
REPEAT_DELAY = 300 # Délai initial avant répétition (ms) REPEAT_DELAY = 100 # Délai initial avant répétition (ms)
REPEAT_INTERVAL = 100 # Intervalle entre répétitions (ms) REPEAT_INTERVAL = 100 # Intervalle entre répétitions (ms)
JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms) JOYHAT_DEBOUNCE = 0 # Délai anti-rebond pour JOYHATMOTION (ms)
JOYAXIS_DEBOUNCE = 50 # Délai anti-rebond pour JOYAXISMOTION (ms) JOYAXIS_DEBOUNCE = 50 # Délai anti-rebond pour JOYAXISMOTION (ms)
REPEAT_ACTION_DEBOUNCE = 50 # Délai anti-rebond pour répétitions up/down/left/right (ms) REPEAT_ACTION_DEBOUNCE = 0 # Délai anti-rebond pour répétitions up/down/left/right (ms)
# Liste des états valides # Liste des états valides
VALID_STATES = [ VALID_STATES = [
@@ -177,6 +177,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "up"): elif is_input_matched(event, "up"):
if current_grid_index - GRID_COLS >= 0: if current_grid_index - GRID_COLS >= 0:
config.selected_platform -= GRID_COLS config.selected_platform -= GRID_COLS
@@ -185,6 +186,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "left"): elif is_input_matched(event, "left"):
if col > 0: if col > 0:
config.selected_platform -= 1 config.selected_platform -= 1
@@ -193,6 +195,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif config.current_page > 0: elif config.current_page > 0:
config.current_page -= 1 config.current_page -= 1
config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + (GRID_COLS - 1) config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + (GRID_COLS - 1)
@@ -203,6 +206,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "right"): elif is_input_matched(event, "right"):
if col < GRID_COLS - 1 and current_grid_index < max_index: if col < GRID_COLS - 1 and current_grid_index < max_index:
config.selected_platform += 1 config.selected_platform += 1
@@ -211,6 +215,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif (config.current_page + 1) * systems_per_page < len(config.platforms): elif (config.current_page + 1) * systems_per_page < len(config.platforms):
config.current_page += 1 config.current_page += 1
config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS
@@ -221,6 +226,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "page_down"): elif is_input_matched(event, "page_down"):
if (config.current_page + 1) * systems_per_page < len(config.platforms): if (config.current_page + 1) * systems_per_page < len(config.platforms):
config.current_page += 1 config.current_page += 1
@@ -232,6 +238,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_start_time = 0 config.repeat_start_time = 0
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
#logger.debug("Page suivante, répétition réinitialisée") #logger.debug("Page suivante, répétition réinitialisée")
elif is_input_matched(event, "page_up"): elif is_input_matched(event, "page_up"):
if config.current_page > 0: if config.current_page > 0:
@@ -244,6 +251,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_start_time = 0 config.repeat_start_time = 0
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
#logger.debug("Page précédente, répétition réinitialisée") #logger.debug("Page précédente, répétition réinitialisée")
elif is_input_matched(event, "page_up"): elif is_input_matched(event, "page_up"):
if config.current_page > 0: if config.current_page > 0:
@@ -256,6 +264,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_start_time = 0 config.repeat_start_time = 0
config.repeat_last_action = current_time config.repeat_last_action = current_time
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
#logger.debug("Page précédente, répétition réinitialisée") #logger.debug("Page précédente, répétition réinitialisée")
elif is_input_matched(event, "progress"): elif is_input_matched(event, "progress"):
if config.download_tasks: if config.download_tasks:
@@ -454,55 +463,7 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "history" config.menu_state = "history"
config.needs_redraw = True config.needs_redraw = True
logger.debug("Ouverture history depuis game") logger.debug("Ouverture history depuis game")
elif is_input_matched(event, "confirm"):
if games:
config.pending_download = check_extension_before_download(
games[config.current_game][0],
config.platforms[config.current_platform],
games[config.current_game][1]
)
if config.pending_download:
url, platform, game_name, is_zip_non_supported = config.pending_download
is_supported = is_extension_supported(
sanitize_filename(game_name),
platform,
load_extensions_json()
)
if not is_supported:
config.previous_menu_state = config.menu_state # Ajouter cette ligne
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
else:
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
config.previous_menu_state = config.menu_state # Ajouter cette ligne
config.menu_state = "error"
config.error_message = (
"Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un editeur de texte et coller la clé API"
)
config.needs_redraw = True
logger.error("Clé API 1fichier absente, téléchargement impossible.")
config.pending_download = None
return action
loop = asyncio.get_running_loop()
task = loop.run_in_executor(None, download_from_1fichier, url, platform, game_name, is_zip_non_supported)
else:
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform)
config.previous_menu_state = config.menu_state # Ajouter cette ligne
config.menu_state = "download_progress"
config.needs_redraw = True
logger.debug(f"Début du téléchargement: {game_name} pour {platform} depuis {url}")
config.pending_download = None
action = "download"
else:
config.menu_state = "error"
config.error_message = "Extension non supportée ou erreur de téléchargement"
config.pending_download = None
config.needs_redraw = True
logger.error(f"config.pending_download est None pour {games[config.current_game][0]}")
elif is_input_matched(event, "cancel"): elif is_input_matched(event, "cancel"):
config.menu_state = "platform" config.menu_state = "platform"
config.current_game = 0 config.current_game = 0
@@ -515,6 +476,135 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True config.needs_redraw = True
logger.debug("Passage à redownload_game_cache depuis game") logger.debug("Passage à redownload_game_cache depuis game")
# Sélectionner un jeu , evenent confirm
elif is_input_matched(event, "confirm"):
if games:
url = games[config.current_game][1]
game_name = games[config.current_game][0]
platform = config.platforms[config.current_platform]
logger.debug(f"Vérification pour {game_name}, URL: {url}")
# Vérifier d'abord si c'est un lien 1fichier
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
config.previous_menu_state = config.menu_state
config.menu_state = "error"
config.error_message = (
"Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un editeur de texte et coller la clé API"
)
config.needs_redraw = True
logger.error("Clé API 1fichier absente, téléchargement impossible.")
config.pending_download = None
return action
# Vérifier l'extension pour les liens 1fichier
config.pending_download = check_extension_before_download(url, platform, game_name)
if config.pending_download:
is_supported = is_extension_supported(
sanitize_filename(game_name),
platform,
load_extensions_json()
)
if not is_supported:
config.previous_menu_state = config.menu_state
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
else:
loop = asyncio.get_running_loop()
task = loop.run_in_executor(None, download_from_1fichier, url, platform, game_name, config.pending_download[3])
config.download_tasks[task] = (task, url, game_name, platform)
config.previous_menu_state = config.menu_state
config.menu_state = "download_progress"
config.needs_redraw = True
logger.debug(f"Début du téléchargement 1fichier: {game_name} pour {platform} depuis {url}")
config.pending_download = None
action = "download"
else:
config.menu_state = "error"
config.error_message = "Extension non supportée ou erreur de téléchargement"
config.pending_download = None
config.needs_redraw = True
logger.error(f"config.pending_download est None pour {game_name}")
else:
# Vérifier l'extension pour les liens non-1fichier
config.pending_download = check_extension_before_download(url, platform, game_name)
if config.pending_download:
is_supported = is_extension_supported(
sanitize_filename(game_name),
platform,
load_extensions_json()
)
if not is_supported:
config.previous_menu_state = config.menu_state
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
else:
task = asyncio.create_task(download_rom(url, platform, game_name, config.pending_download[3]))
config.download_tasks[task] = (task, url, game_name, platform)
config.previous_menu_state = config.menu_state
config.menu_state = "download_progress"
config.needs_redraw = True
logger.debug(f"Début du téléchargement: {game_name} pour {platform} depuis {url}")
config.pending_download = None
action = "download"
else:
config.menu_state = "error"
config.error_message = "Extension non supportée ou erreur de téléchargement"
config.pending_download = None
config.needs_redraw = True
logger.error(f"config.pending_download est None pour {game_name}")
# Avertissement extension
elif config.menu_state == "extension_warning":
if is_input_matched(event, "confirm"):
if config.extension_confirm_selection == 1:
if config.pending_download and len(config.pending_download) == 4:
url, platform, game_name, is_zip_non_supported = config.pending_download
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
config.previous_menu_state = config.menu_state
config.menu_state = "error"
config.error_message = (
"Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt"
)
config.needs_redraw = True
logger.error("Clé API 1fichier absente, téléchargement impossible.")
config.pending_download = None
return action
loop = asyncio.get_running_loop()
task = loop.run_in_executor(None, download_from_1fichier, url, platform, game_name, is_zip_non_supported)
else:
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform)
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.menu_state = "download_progress"
config.needs_redraw = True
logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}")
config.pending_download = None
action = "download"
else:
config.menu_state = "error"
config.error_message = "Données de téléchargement invalides"
config.pending_download = None
config.needs_redraw = True
logger.error("config.pending_download invalide")
else:
config.pending_download = None
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
logger.debug(f"Retour à {config.menu_state} depuis extension_warning")
elif is_input_matched(event, "left") or is_input_matched(event, "right"):
config.extension_confirm_selection = 1 - config.extension_confirm_selection
config.needs_redraw = True
#logger.debug(f"Changement sélection extension_warning: {config.extension_confirm_selection}")
elif is_input_matched(event, "cancel"):
config.pending_download = None
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
logger.debug(f"Retour à {config.menu_state} depuis extension_warning")
#Historique #Historique
elif config.menu_state == "history": elif config.menu_state == "history":
history = config.history history = config.history
@@ -667,41 +757,6 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True config.needs_redraw = True
#logger.debug(f"Changement sélection confirm_exit: {config.confirm_selection}") #logger.debug(f"Changement sélection confirm_exit: {config.confirm_selection}")
# Avertissement extension
elif config.menu_state == "extension_warning":
if is_input_matched(event, "confirm"):
if config.extension_confirm_selection == 1:
if config.pending_download and len(config.pending_download) == 4:
url, platform, game_name, is_zip_non_supported = config.pending_download
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform)
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.menu_state = "download_progress"
config.needs_redraw = True
logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}")
config.pending_download = None
action = "download"
else:
config.menu_state = "error"
config.error_message = "Données de téléchargement invalides"
config.pending_download = None
config.needs_redraw = True
logger.error("config.pending_download invalide")
else:
config.pending_download = None
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
logger.debug(f"Retour à {config.menu_state} depuis extension_warning")
elif is_input_matched(event, "left") or is_input_matched(event, "right"):
config.extension_confirm_selection = 1 - config.extension_confirm_selection
config.needs_redraw = True
#logger.debug(f"Changement sélection extension_warning: {config.extension_confirm_selection}")
elif is_input_matched(event, "cancel"):
config.pending_download = None
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
logger.debug(f"Retour à {config.menu_state} depuis extension_warning")
# Menu pause # Menu pause
elif config.menu_state == "pause_menu": elif config.menu_state == "pause_menu":
logger.debug(f"État pause_menu, selected_option={config.selected_option}, événement={event.type}, valeur={getattr(event, 'value', None)}") logger.debug(f"État pause_menu, selected_option={config.selected_option}, événement={event.type}, valeur={getattr(event, 'value', None)}")
+14 -56
View File
@@ -1,6 +1,5 @@
import pygame # type: ignore import pygame # type: ignore
import config import config
import os
from utils import truncate_text_middle, wrap_text, load_system_image from utils import truncate_text_middle, wrap_text, load_system_image
import logging import logging
import math import math
@@ -152,7 +151,9 @@ def draw_loading_screen(screen):
"Bienvenue dans RGSX", "Bienvenue dans RGSX",
"It's dangerous to go alone, take all you need!", "It's dangerous to go alone, take all you need!",
"Mais ne téléchargez que des jeux", "Mais ne téléchargez que des jeux",
"dont vous possédez les originaux !" "dont vous possédez les originaux !",
"RGSX n'est pas responsable des contenus téléchargés,",
"et n'heberge pas de ROMs.",
] ]
margin_horizontal = int(config.screen_width * 0.025) margin_horizontal = int(config.screen_width * 0.025)
@@ -699,7 +700,7 @@ def draw_popup_result_download(screen, message, is_error):
"""Affiche une popup avec un message de résultat.""" """Affiche une popup avec un message de résultat."""
screen.blit(OVERLAY, (0, 0)) screen.blit(OVERLAY, (0, 0))
if message is None: if message is None:
message = "Téléchargement annulé" message = "Téléchargement annulé par l'utilisateur."
logger.debug(f"Message popup : {message}, is_error={is_error}") logger.debug(f"Message popup : {message}, is_error={is_error}")
# Réduire la largeur maximale pour le wrapping # Réduire la largeur maximale pour le wrapping
wrapped_message = wrap_text(message, config.small_font, config.screen_width - 160) wrapped_message = wrap_text(message, config.small_font, config.screen_width - 160)
@@ -849,18 +850,18 @@ def draw_pause_menu(screen, selected_option):
def draw_controls_help(screen, previous_state): def draw_controls_help(screen, previous_state):
"""Affiche la liste des contrôles avec un style moderne.""" """Affiche la liste des contrôles avec un style moderne."""
common_controls = { common_controls = {
"confirm": lambda action: f"{get_control_display('confirm', 'Entrée/A/Croix')} : {action}", "confirm": lambda action: f"{get_control_display('confirm', 'Entrée/A')} : {action}",
"cancel": lambda action: f"{get_control_display('cancel', 'Échap/B/Rond')} : {action}", "cancel": lambda action: f"{get_control_display('cancel', 'Échap/B')} : {action}",
"start": lambda: f"{get_control_display('start', 'Start/')} : Menu", "start": lambda: f"{get_control_display('start', 'Start')} : Menu",
"progress": lambda action: f"{get_control_display('progress', 'X/Carré')} : {action}", "progress": lambda action: f"{get_control_display('progress', 'X')} : {action}",
"up": lambda action: f"{get_control_display('up', 'Flèche Haut')} : {action}", "up": lambda action: f"{get_control_display('up', 'Flèche Haut')} : {action}",
"down": lambda action: f"{get_control_display('down', 'Flèche Bas')} : {action}", "down": lambda action: f"{get_control_display('down', 'Flèche Bas')} : {action}",
"page_up": lambda action: f"{get_control_display('page_up', 'Q/LB/L1')} : {action}", "page_up": lambda action: f"{get_control_display('page_up', 'Q/LB')} : {action}",
"page_down": lambda action: f"{get_control_display('page_down', 'E/RB/R1')} : {action}", "page_down": lambda action: f"{get_control_display('page_down', 'E/RB')} : {action}",
"filter": lambda action: f"{get_control_display('filter', 'Select')} : {action}", "filter": lambda action: f"{get_control_display('filter', 'Select')} : {action}",
"history": lambda action: f"{get_control_display('history', 'H/Y/Triangle')} : {action}", "history": lambda action: f"{get_control_display('history', 'H')} : {action}",
"delete": lambda: f"{get_control_display('delete', 'Backspace/LT/L2')} : Supprimer", "delete": lambda: f"{get_control_display('delete', 'Retour Arrière')} : Supprimer",
"space": lambda: f"{get_control_display('space', 'Espace/RT/R2')} : Espace" "space": lambda: f"{get_control_display('space', 'Espace')} : Espace"
} }
state_controls = { state_controls = {
@@ -1041,47 +1042,4 @@ def draw_popup(screen):
countdown_text = f"Ce message se fermera dans {remaining_time} seconde{'s' if remaining_time != 1 else ''}" countdown_text = f"Ce message se fermera dans {remaining_time} seconde{'s' if remaining_time != 1 else ''}"
countdown_surface = config.small_font.render(countdown_text, True, THEME_COLORS["text"]) countdown_surface = config.small_font.render(countdown_text, True, THEME_COLORS["text"])
countdown_rect = countdown_surface.get_rect(center=(config.screen_width // 2, popup_y + margin_top_bottom + len(text_lines) * line_height + line_height // 2)) countdown_rect = countdown_surface.get_rect(center=(config.screen_width // 2, popup_y + margin_top_bottom + len(text_lines) * line_height + line_height // 2))
screen.blit(countdown_surface, countdown_rect) screen.blit(countdown_surface, countdown_rect)
# Variables globales pour la popup de musique
current_music_name = None
music_popup_start_time = None
MUSIC_POPUP_DURATION = 5 # Durée d'affichage en secondes
def draw_music_popup(screen):
"""Affiche une popup discrète en bas à droite avec le nom de la musique en cours."""
global current_music_name, music_popup_start_time
if current_music_name is None or music_popup_start_time is None:
return
# Vérifier si la popup doit encore être affichée
current_time = pygame.time.get_ticks() / 1000 # Temps en secondes
if current_time - music_popup_start_time > MUSIC_POPUP_DURATION:
current_music_name = None
music_popup_start_time = None
return
# Paramètres de la popup
font = config.small_font
text = font.render(current_music_name, True, THEME_COLORS["text"])
text_width, text_height = font.size(current_music_name)
padding = 10
rect_width = text_width + 2 * padding
rect_height = text_height + 2 * padding
rect_x = config.screen_width - rect_width - 22 # 20 pixels de marge à droite
rect_y = config.screen_height - rect_height - 8 # 20 pixels de marge en bas
# Créer une surface semi-transparente
popup_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA)
pygame.draw.rect(popup_surface, THEME_COLORS["fond_image"] + (180,), (0, 0, rect_width, rect_height), border_radius=8)
pygame.draw.rect(popup_surface, THEME_COLORS["border"] + (200,), (0, 0, rect_width, rect_height), 1, border_radius=8)
# Ajouter le texte
text_rect = text.get_rect(center=(rect_width // 2, rect_height // 2))
popup_surface.blit(text, text_rect)
# Afficher la popup
screen.blit(popup_surface, (rect_x, rect_y))
+89 -339
View File
@@ -1,348 +1,123 @@
import requests import requests
import subprocess import subprocess
import re
import os import os
import threading import threading
import pygame # type: ignore import pygame # type: ignore
import zipfile import sys
import json
import asyncio import asyncio
import config import config
import sys
from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT
from utils import sanitize_filename from utils import sanitize_filename, extract_zip, extract_rar
from history import add_to_history, load_history from history import add_to_history, load_history
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
JSON_EXTENSIONS = "/userdata/roms/ports/RGSX/rom_extensions.json"
cache = {} cache = {}
CACHE_TTL = 3600 # 1 heure CACHE_TTL = 3600 # 1 heure
def test_internet(): def test_internet():
"""Teste la connexion Internet dans un thread séparé."""
logger.debug("Test de connexion Internet") logger.debug("Test de connexion Internet")
result = [False] try:
result = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
def ping_thread(): if result.returncode == 0:
try: logger.debug("Connexion Internet OK")
proc = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5) return True
result[0] = proc.returncode == 0 else:
logger.debug("Connexion Internet OK" if result[0] else "Échec ping 8.8.8.8") logger.debug("Échec ping 8.8.8.8")
except Exception as e: return False
logger.debug(f"Erreur test Internet: {str(e)}") except Exception as e:
result[0] = False logger.debug(f"Erreur test Internet: {str(e)}")
return False
thread = threading.Thread(target=ping_thread)
thread.start()
thread.join()
return result[0]
# Fonction pour vérifier et appliquer les mises à jour OTA
async def check_for_updates(): async def check_for_updates():
"""Vérifie et applique les mises à jour OTA dans un thread séparé.""" try:
result = [None, None] logger.debug("Vérification de la version disponible sur le serveur")
config.current_loading_system = "Mise à jour en cours... Patientez l'ecran reste figé..Puis relancer l'application"
def update_thread(): config.loading_progress = 5.0
try: config.needs_redraw = True
logger.debug("Vérification de la version disponible sur le serveur") response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
config.current_loading_system = "Mise à jour en cours... Patientez l'écran reste figé..Puis relancer l'application" response.raise_for_status()
config.loading_progress = 5.0 if response.headers.get("content-type") != "application/json":
raise ValueError(f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})")
version_data = response.json()
latest_version = version_data.get("version")
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_version}")
if latest_version != config.app_version:
config.current_loading_system = f"Mise à jour disponible : {latest_version}"
config.loading_progress = 10.0
config.needs_redraw = True config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5) logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}")
response.raise_for_status()
if response.headers.get("content-type") != "application/json":
raise ValueError(f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})")
version_data = response.json()
latest_version = version_data.get("version")
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_version}")
if latest_version != config.app_version: update_script_path = "/userdata/roms/ports/rgsx-update.sh"
config.current_loading_system = f"Mise à jour disponible : {latest_version}" logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}")
config.loading_progress = 10.0 with requests.get(OTA_UPDATE_SCRIPT, stream=True, timeout=10) as r:
config.needs_redraw = True r.raise_for_status()
logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}") with open(update_script_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
config.loading_progress = min(50.0, config.loading_progress + 5.0)
config.needs_redraw = True
await asyncio.sleep(0)
update_script_path = "/userdata/roms/ports/rgsx-update.sh" config.current_loading_system = "Préparation de la mise à jour..."
logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}") config.loading_progress = 60.0
with requests.get(OTA_UPDATE_SCRIPT, stream=True, timeout=10) as r:
r.raise_for_status()
with open(update_script_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
config.loading_progress = min(50.0, config.loading_progress + 5.0)
config.needs_redraw = True
# Pas de sleep ici, car on est dans un thread
config.current_loading_system = "Préparation de la mise à jour..."
config.loading_progress = 60.0
config.needs_redraw = True
logger.debug(f"Rendre {update_script_path} exécutable")
subprocess.run(["chmod", "+x", update_script_path], check=True)
logger.debug(f"Script {update_script_path} rendu exécutable")
logger.debug(f"Vérification de l'existence et des permissions de {update_script_path}")
if not os.path.isfile(update_script_path):
logger.error(f"Le script {update_script_path} n'existe pas")
result[0], result[1] = False, f"Erreur : le script {update_script_path} n'existe pas"
return
if not os.access(update_script_path, os.X_OK):
logger.error(f"Le script {update_script_path} n'est pas exécutable")
result[0], result[1] = False, f"Erreur : le script {update_script_path} n'est pas exécutable"
return
wrapper_script_path = "/userdata/roms/ports/RGSX/update/run.update"
logger.debug(f"Vérification de l'existence et des permissions de {wrapper_script_path}")
if not os.path.isfile(wrapper_script_path):
logger.error(f"Le script wrapper {wrapper_script_path} n'existe pas")
result[0], result[1] = False, f"Erreur : le script wrapper {wrapper_script_path} n'existe pas"
return
if not os.access(wrapper_script_path, os.X_OK):
logger.error(f"Le script wrapper {wrapper_script_path} n'est pas exécutable")
subprocess.run(["chmod", "+x", wrapper_script_path], check=True)
logger.debug(f"Script wrapper {wrapper_script_path} rendu exécutable")
logger.debug("Désactivation des événements Pygame QUIT")
pygame.event.set_blocked(pygame.QUIT)
config.current_loading_system = "Application de la mise à jour..."
config.loading_progress = 80.0
config.needs_redraw = True
logger.debug(f"Exécution du script wrapper : {wrapper_script_path}")
os_result = os.system(f"{wrapper_script_path} &")
logger.debug(f"Résultat de os.system : {os_result}")
if os_result != 0:
logger.error(f"Échec du lancement du script wrapper : code de retour {os_result}")
result[0], result[1] = False, f"Échec du lancement du script wrapper : code de retour {os_result}"
return
config.current_loading_system = "Mise à jour déclenchée, redémarrage..."
config.loading_progress = 100.0
config.needs_redraw = True
logger.debug("Mise à jour déclenchée, arrêt de l'application")
config.update_triggered = True
pygame.quit()
sys.exit(0)
else:
logger.debug("Aucune mise à jour logicielle disponible")
result[0], result[1] = True, "Aucune mise à jour disponible"
except Exception as e:
logger.error(f"Erreur OTA : {str(e)}")
result[0], result[1] = False, f"Erreur lors de la vérification des mises à jour : {str(e)}"
thread = threading.Thread(target=update_thread)
thread.start()
while thread.is_alive():
pygame.event.pump()
await asyncio.sleep(0.1)
thread.join()
return result[0], result[1]
def load_extensions_json():
"""Charge le fichier JSON contenant les extensions supportées."""
try:
with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}")
return []
def is_extension_supported(filename, platform, extensions_data):
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée."""
extension = os.path.splitext(filename)[1].lower()
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
dest_dir = platform_dict.get("folder")
break
if not dest_dir:
logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}")
dest_dir = os.path.join("/userdata/roms", platform)
for system in extensions_data:
if system["folder"] == dest_dir:
return extension in system["extensions"]
logger.warning(f"Aucun système trouvé pour le dossier {dest_dir}")
return False
def extract_zip(zip_path, dest_dir, url):
"""Extrait le contenu du fichier ZIP dans le dossier cible avec un suivi progressif de la progression."""
try:
lock = threading.Lock()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
total_size = sum(info.file_size for info in zip_ref.infolist() if not info.is_dir())
logger.info(f"Taille totale à extraire: {total_size} octets")
if total_size == 0:
logger.warning("ZIP vide ou ne contenant que des dossiers")
return True, "ZIP vide extrait avec succès"
extracted_size = 0
os.makedirs(dest_dir, exist_ok=True)
chunk_size = 8192
for info in zip_ref.infolist():
if info.is_dir():
continue
file_path = os.path.join(dest_dir, info.filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with zip_ref.open(info) as source, open(file_path, 'wb') as dest:
file_size = info.file_size
file_extracted = 0
while True:
chunk = source.read(chunk_size)
if not chunk:
break
dest.write(chunk)
file_extracted += len(chunk)
extracted_size += len(chunk)
with lock:
config.download_progress[url]["downloaded_size"] = extracted_size
config.download_progress[url]["total_size"] = total_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = (extracted_size / total_size * 100) if total_size > 0 else 0
config.needs_redraw = True # Forcer le redraw
logger.debug(f"Extraction {info.filename}, chunk: {len(chunk)}, file_extracted: {file_extracted}/{file_size}, total_extracted: {extracted_size}/{total_size}, progression: {(extracted_size/total_size*100):.1f}%")
os.chmod(file_path, 0o644)
for root, dirs, _ in os.walk(dest_dir):
for dir_name in dirs:
os.chmod(os.path.join(root, dir_name), 0o755)
os.remove(zip_path)
logger.info(f"Fichier ZIP {zip_path} extrait dans {dest_dir} et supprimé")
return True, "ZIP extrait avec succès"
except Exception as e:
logger.error(f"Erreur lors de l'extraction de {zip_path}: {e}")
return False, str(e)
def extract_rar(rar_path, dest_dir, url):
"""Extrait le contenu du fichier RAR dans le dossier cible, préservant la structure des dossiers."""
try:
lock = threading.Lock()
os.makedirs(dest_dir, exist_ok=True)
result = subprocess.run(['unrar'], capture_output=True, text=True)
if result.returncode not in [0, 1]:
logger.error("Commande unrar non disponible")
return False, "Commande unrar non disponible"
result = subprocess.run(['unrar', 'l', '-v', rar_path], capture_output=True, text=True)
if result.returncode != 0:
error_msg = result.stderr.strip()
logger.error(f"Erreur lors de la liste des fichiers RAR: {error_msg}")
return False, f"Échec de la liste des fichiers RAR: {error_msg}"
logger.debug(f"Sortie brute de 'unrar l -v {rar_path}':\n{result.stdout}")
total_size = 0
files_to_extract = []
root_dirs = set()
lines = result.stdout.splitlines()
in_file_list = False
for line in lines:
if line.startswith("----"):
in_file_list = not in_file_list
continue
if in_file_list:
match = re.match(r'^\s*(\S+)\s+(\d+)\s+\d*\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(.+)$', line)
if match:
attrs = match.group(1)
file_size = int(match.group(2))
file_date = match.group(3)
file_name = match.group(4).strip()
if 'D' not in attrs:
files_to_extract.append((file_name, file_size))
total_size += file_size
root_dir = file_name.split('/')[0] if '/' in file_name else ''
if root_dir:
root_dirs.add(root_dir)
logger.debug(f"Ligne parsée: {file_name}, taille: {file_size}, date: {file_date}")
else:
logger.debug(f"Dossier ignoré: {file_name}")
else:
logger.debug(f"Ligne ignorée (format inattendu): {line}")
logger.info(f"Taille totale à extraire (RAR): {total_size} octets")
logger.debug(f"Fichiers à extraire: {files_to_extract}")
logger.debug(f"Dossiers racines détectés: {root_dirs}")
if total_size == 0:
logger.warning("RAR vide, ne contenant que des dossiers, ou erreur de parsing")
return False, "RAR vide ou erreur lors de la liste des fichiers"
with lock:
config.download_progress[url]["downloaded_size"] = 0
config.download_progress[url]["total_size"] = total_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = 0
config.needs_redraw = True config.needs_redraw = True
logger.debug(f"Rendre {update_script_path} exécutable")
subprocess.run(["chmod", "+x", update_script_path], check=True)
logger.debug(f"Script {update_script_path} rendu exécutable")
escaped_rar_path = rar_path.replace(" ", "\\ ") logger.debug(f"Vérification de l'existence et des permissions de {update_script_path}")
escaped_dest_dir = dest_dir.replace(" ", "\\ ") if not os.path.isfile(update_script_path):
process = subprocess.Popen(['unrar', 'x', '-y', escaped_rar_path, escaped_dest_dir], logger.error(f"Le script {update_script_path} n'existe pas")
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) return False, f"Erreur : le script {update_script_path} n'existe pas"
stdout, stderr = process.communicate() if not os.access(update_script_path, os.X_OK):
logger.error(f"Le script {update_script_path} n'est pas exécutable")
return False, f"Erreur : le script {update_script_path} n'est pas exécutable"
if process.returncode != 0: wrapper_script_path = "/userdata/roms/ports/RGSX/update/run.update"
logger.error(f"Erreur lors de l'extraction de {rar_path}: {stderr}") logger.debug(f"Vérification de l'existence et des permissions de {wrapper_script_path}")
return False, f"Erreur lors de l'extraction: {stderr}" if not os.path.isfile(wrapper_script_path):
logger.error(f"Le script wrapper {wrapper_script_path} n'existe pas")
return False, f"Erreur : le script wrapper {wrapper_script_path} n'existe pas"
if not os.access(wrapper_script_path, os.X_OK):
logger.error(f"Le script wrapper {wrapper_script_path} n'est pas exécutable")
subprocess.run(["chmod", "+x", wrapper_script_path], check=True)
logger.debug(f"Script wrapper {wrapper_script_path} rendu exécutable")
extracted_size = 0 logger.debug("Désactivation des événements Pygame QUIT")
extracted_files = [] pygame.event.set_blocked(pygame.QUIT)
total_files = len(files_to_extract)
for i, (expected_file, file_size) in enumerate(files_to_extract):
file_path = os.path.join(dest_dir, expected_file)
if os.path.exists(file_path):
extracted_size += file_size
extracted_files.append(expected_file)
os.chmod(file_path, 0o644)
logger.debug(f"Fichier extrait: {expected_file}, taille: {file_size}, chemin: {file_path}")
with lock:
config.download_progress[url]["downloaded_size"] = extracted_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = ((i + 1) / total_files * 100) if total_files > 0 else 0
config.needs_redraw = True
else:
logger.warning(f"Fichier non trouvé après extraction: {expected_file}")
missing_files = [f for f, _ in files_to_extract if f not in extracted_files] config.current_loading_system = "Application de la mise à jour..."
if missing_files: config.loading_progress = 80.0
logger.warning(f"Fichiers non extraits: {', '.join(missing_files)}") config.needs_redraw = True
return False, f"Fichiers non extraits: {', '.join(missing_files)}" logger.debug(f"Exécution du script wrapper : {wrapper_script_path}")
result = os.system(f"{wrapper_script_path} &")
logger.debug(f"Résultat de os.system : {result}")
if result != 0:
logger.error(f"Échec du lancement du script wrapper : code de retour {result}")
return False, f"Échec du lancement du script wrapper : code de retour {result}"
if dest_dir == "/userdata/roms/ps3" and len(root_dirs) == 1: config.current_loading_system = "Mise à jour déclenchée, redémarrage..."
root_dir = root_dirs.pop() config.loading_progress = 100.0
old_path = os.path.join(dest_dir, root_dir) config.needs_redraw = True
new_path = os.path.join(dest_dir, f"{root_dir}.ps3") logger.debug("Mise à jour déclenchée, arrêt de l'application")
if os.path.isdir(old_path): config.update_triggered = True
try: pygame.quit()
os.rename(old_path, new_path) sys.exit(0)
logger.info(f"Dossier renommé: {old_path} -> {new_path}") else:
except Exception as e: logger.debug("Aucune mise à jour logicielle disponible")
logger.error(f"Erreur lors du renommage de {old_path} en {new_path}: {str(e)}") return True, "Aucune mise à jour disponible"
return False, f"Erreur lors du renommage du dossier: {str(e)}"
else:
logger.warning(f"Dossier racine {old_path} non trouvé après extraction")
elif dest_dir == "/userdata/roms/ps3" and len(root_dirs) > 1:
logger.warning(f"Plusieurs dossiers racines détectés dans l'archive: {root_dirs}. Aucun renommage effectué.")
for root, dirs, _ in os.walk(dest_dir):
for dir_name in dirs:
os.chmod(os.path.join(root, dir_name), 0o755)
os.remove(rar_path)
logger.info(f"Fichier RAR {rar_path} extrait dans {dest_dir} et supprimé")
return True, "RAR extrait avec succès"
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de l'extraction de {rar_path}: {str(e)}") logger.error(f"Erreur OTA : {str(e)}")
return False, str(e) return False, f"Erreur lors de la vérification des mises à jour : {str(e)}"
finally:
if os.path.exists(rar_path):
try:
os.remove(rar_path)
logger.info(f"Fichier RAR {rar_path} supprimé après échec de l'extraction")
except Exception as e:
logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}")
async def download_rom(url, platform, game_name, is_zip_non_supported=False): async def download_rom(url, platform, game_name, is_zip_non_supported=False):
logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}") logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}")
@@ -463,31 +238,6 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False):
return result[0], result[1] return result[0], result[1]
def check_extension_before_download(game_name, platform, url):
"""Vérifie l'extension avant de lancer le téléchargement et retourne un tuple de 4 éléments."""
try:
sanitized_name = sanitize_filename(game_name)
extensions_data = load_extensions_json()
if not extensions_data:
logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable")
return None
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
extension = os.path.splitext(sanitized_name)[1].lower()
is_archive = extension in (".zip", ".rar")
if is_supported:
logger.debug(f"L'extension de {sanitized_name} est supportée pour {platform}")
return (url, platform, game_name, False)
elif is_archive:
logger.debug(f"Archive {extension.upper()} détectée pour {sanitized_name}, extraction automatique prévue")
return (url, platform, game_name, True)
else:
logger.debug(f"Extension non supportée ({extension}) pour {sanitized_name}, avertissement affiché")
return (url, platform, game_name, False)
except Exception as e:
logger.error(f"Erreur vérification extension {url}: {str(e)}")
return None
def is_1fichier_url(url): def is_1fichier_url(url):
"""Détecte si l'URL est un lien 1fichier.""" """Détecte si l'URL est un lien 1fichier."""
+335 -120
View File
@@ -3,124 +3,26 @@ import re
import json import json
import os import os
import logging import logging
import threading
import requests import requests
import config
import random
import platform import platform
import subprocess import subprocess
import config
import threading
import zipfile
import random
from config import JSON_EXTENSIONS
from datetime import datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
# Liste globale pour stocker les systèmes avec une erreur 404
unavailable_systems = []
unavailable_systems = [] # Liste globale pour stocker les systèmes avec une erreur 404
def check_url(url, platform_id, unavailable_systems_lock=None, unavailable_systems=None):
"""Vérifie si une URL est accessible via une requête HEAD."""
try:
response = requests.head(url, timeout=5, allow_redirects=True)
if response.status_code == 404:
logger.error(f"URL non accessible pour {platform_id}: {url} (code 404)")
if unavailable_systems_lock and unavailable_systems is not None:
with unavailable_systems_lock:
unavailable_systems.append(platform_id)
elif unavailable_systems is not None:
unavailable_systems.append(platform_id)
except requests.RequestException as e:
logger.error(f"Erreur lors du test de l'URL pour {platform_id}: {url} ({str(e)})")
if unavailable_systems_lock and unavailable_systems is not None:
with unavailable_systems_lock:
unavailable_systems.append(platform_id)
elif unavailable_systems is not None:
unavailable_systems.append(platform_id)
def load_games(platform_id, unavailable_systems_lock=None, unavailable_systems=None):
"""Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL."""
games_path = f"/userdata/roms/ports/RGSX/games/{platform_id}.json"
try:
with open(games_path, 'r', encoding='utf-8') as f:
games = json.load(f)
# Tester la première URL si la liste n'est pas vide
if games and len(games) > 0 and len(games[0]) > 1:
first_url = games[0][1]
check_url(first_url, platform_id, unavailable_systems_lock, unavailable_systems)
else:
logger.debug(f"Aucune URL à tester pour {platform_id} (liste vide ou mal formée)")
logger.debug(f"Jeux chargés pour {platform_id}: {len(games)} jeux")
return games
except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {str(e)}")
return []
def write_unavailable_systems():
"""Écrit la liste des systèmes avec une erreur 404 dans un fichier texte."""
if not unavailable_systems:
logger.debug("Aucun système avec une erreur 404, aucun fichier écrit")
return
from datetime import datetime
current_time = datetime.now()
timestamp = current_time.strftime("%d-%m-%Y-%H-%M")
log_dir = "/userdata/roms/ports/logs/RGSX"
log_file = f"{log_dir}/systemes_unavailable_{timestamp}.txt"
try:
os.makedirs(log_dir, exist_ok=True)
with open(log_file, 'w', encoding='utf-8') as f:
f.write("Systèmes avec une erreur 404 :\n")
for system in unavailable_systems:
f.write(f"{system}\n")
logger.debug(f"Fichier écrit : {log_file} avec {len(unavailable_systems)} systèmes")
except Exception as e:
logger.error(f"Erreur lors de l'écriture du fichier {log_file}: {str(e)}")
def load_sources():
"""Charge sources.json et les jeux pour toutes les plateformes en parallèle."""
sources_path = "/userdata/roms/ports/RGSX/sources.json"
logger.debug(f"Chargement de {sources_path}")
try:
with open(sources_path, 'r', encoding='utf-8') as f:
sources = json.load(f)
sources = sorted(sources, key=lambda x: x.get("nom", x.get("platform", "")).lower())
config.platforms = [source["platform"] for source in sources]
config.platform_dicts = sources
config.platform_names = {source["platform"]: source["nom"] for source in sources}
config.games_count = {platform: 0 for platform in config.platforms}
# Créer un verrou pour unavailable_systems
unavailable_systems_lock = threading.Lock()
global unavailable_systems
unavailable_systems = []
# Lancer les chargements des jeux en parallèle avec threading
threads = []
results = [None] * len(config.platforms)
for i, platform in enumerate(config.platforms):
thread = threading.Thread(target=lambda idx=i, plat=platform: results.__setitem__(idx, load_games(plat, unavailable_systems_lock, unavailable_systems)))
threads.append(thread)
thread.start()
# Attendre que tous les threads se terminent
for thread in threads:
thread.join()
# Mettre à jour games_count avec les résultats
for platform, games in zip(config.platforms, results):
if games:
config.games_count[platform] = len(games)
logger.debug(f"Jeux chargés pour {platform}: {len(games)} jeux")
else:
config.games_count[platform] = 0
logger.error(f"Échec du chargement des jeux pour {platform}")
write_unavailable_systems()
return sources
except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json: {str(e)}")
return []
# Détection système non-PC # Détection système non-PC
def detect_non_pc(): def detect_non_pc():
@@ -137,6 +39,144 @@ def detect_non_pc():
logger.debug(f"Système détecté: {platform.system()}, architecture: {arch}, is_non_pc={is_non_pc}") logger.debug(f"Système détecté: {platform.system()}, architecture: {arch}, is_non_pc={is_non_pc}")
return is_non_pc return is_non_pc
# Fonction pour charger le fichier JSON des extensions supportées
def load_extensions_json():
"""Charge le fichier JSON contenant les extensions supportées."""
try:
with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}")
return []
def check_extension_before_download(url, platform, game_name):
"""Vérifie l'extension avant de lancer le téléchargement et retourne un tuple de 4 éléments."""
try:
sanitized_name = sanitize_filename(game_name)
extensions_data = load_extensions_json()
if not extensions_data:
logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable")
return None
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
extension = os.path.splitext(sanitized_name)[1].lower()
is_archive = extension in (".zip", ".rar")
if is_supported:
logger.debug(f"L'extension de {sanitized_name} est supportée pour {platform}")
return (url, platform, game_name, False)
elif is_archive:
logger.debug(f"Archive {extension.upper()} détectée pour {sanitized_name}, extraction automatique prévue")
return (url, platform, game_name, True)
else:
logger.debug(f"Extension non supportée ({extension}) pour {sanitized_name}, avertissement affiché")
return (url, platform, game_name, False)
except Exception as e:
logger.error(f"Erreur vérification extension {url}: {str(e)}")
return None
# Fonction pour vérifier si l'extension est supportée pour une plateforme donnée
def is_extension_supported(filename, platform, extensions_data):
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée."""
extension = os.path.splitext(filename)[1].lower()
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
dest_dir = platform_dict.get("folder")
break
if not dest_dir:
logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}")
dest_dir = os.path.join("/userdata/roms", platform)
for system in extensions_data:
if system["folder"] == dest_dir:
return extension in system["extensions"]
logger.warning(f"Aucun système trouvé pour le dossier {dest_dir}")
return False
# Fonction pour charger sources.json
def load_sources():
"""Charge les sources depuis sources.json et initialise les plateformes."""
sources_path = "/userdata/roms/ports/RGSX/sources.json"
logger.debug(f"Chargement de {sources_path}")
try:
with open(sources_path, 'r', encoding='utf-8') as f:
sources = json.load(f)
sources = sorted(sources, key=lambda x: x.get("nom", x.get("platform", "")).lower())
config.platforms = [source["platform"] for source in sources]
config.platform_dicts = sources
config.platform_names = {source["platform"]: source["nom"] for source in sources}
config.games_count = {platform: 0 for platform in config.platforms} # Initialiser à 0
# Charger les jeux pour chaque plateforme
loaded_platforms = set() # Pour suivre les plateformes déjà loguées
for platform in config.platforms:
games = load_games(platform)
config.games_count[platform] = len(games)
if platform not in loaded_platforms:
loaded_platforms.add(platform)
# Appeler write_unavailable_systems une seule fois après la boucle
write_unavailable_systems() # Assurez-vous que cette fonction est définie
return sources
except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json : {str(e)}")
return []
def load_games(platform_id):
"""Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL."""
games_path = f"/userdata/roms/ports/RGSX/games/{platform_id}.json"
try:
with open(games_path, 'r', encoding='utf-8') as f:
games = json.load(f)
# Tester la première URL si la liste n'est pas vide
if games and len(games) > 0 and len(games[0]) > 1:
first_url = games[0][1]
try:
response = requests.head(first_url, timeout=5, allow_redirects=True)
if response.status_code not in (200, 303): # Ne logger que les codes autres que 200 et 303
logger.debug(f"https://{first_url} \"HEAD {first_url} HTTP/1.1\" {response.status_code} 0")
if response.status_code == 404:
logger.error(f"URL non accessible pour {platform_id} : {first_url} (code 404)")
unavailable_systems.append(platform_id) # Assurez-vous que unavailable_systems est défini
except requests.RequestException as e:
logger.error(f"Erreur lors du test de l'URL pour {platform_id} : {first_url} ({str(e)})")
else:
logger.debug(f"Aucune URL à tester pour {platform_id} (liste vide ou mal formée)")
logger.debug(f"Jeux chargés pour {platform_id}: {len(games)} jeux")
return games
except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id} : {str(e)}")
return []
def write_unavailable_systems():
"""Écrit la liste des systèmes avec une erreur 404 dans un fichier texte."""
if not unavailable_systems:
logger.debug("Aucun système avec des liens HS, rien à écrire dans le fichier.")
return
# Formater la date et l'heure pour le nom du fichier
current_time = datetime.now()
timestamp = current_time.strftime("%d-%m-%Y-%H-%M")
log_dir = "/userdata/roms/ports/logs/RGSX"
log_file = f"{log_dir}/systemes_unavailable_{timestamp}.txt"
try:
# Créer le répertoire s'il n'existe pas
os.makedirs(log_dir, exist_ok=True)
# Écrire les systèmes dans le fichier
with open(log_file, 'w', encoding='utf-8') as f:
f.write("Systèmes avec une erreur 404 :\n")
for system in unavailable_systems:
f.write(f"{system}\n")
logger.debug(f"Fichier écrit : {log_file} avec {len(unavailable_systems)} systèmes")
except Exception as e:
logger.error(f"Erreur lors de l'écriture du fichier {log_file} : {str(e)}")
def truncate_text_middle(text, font, max_width): def truncate_text_middle(text, font, max_width):
"""Tronque le texte en insérant '...' au milieu, en préservant le début et la fin, sans extension de fichier.""" """Tronque le texte en insérant '...' au milieu, en préservant le début et la fin, sans extension de fichier."""
@@ -264,15 +304,190 @@ def load_system_image(platform_dict):
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}") logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
return None return None
# Dossier musique Batocera
music_folder = "/userdata/roms/ports/RGSX/assets/music"
music_files = [f for f in os.listdir(music_folder) if f.lower().endswith(('.ogg', '.mp3'))]
current_music = None # Suivre la musique en cours
loading_step = "none"
def play_random_music(): # Fonction pour extraire le contenu d'un fichier ZIP
def extract_zip(zip_path, dest_dir, url):
"""Extrait le contenu du fichier ZIP dans le dossier cible avec un suivi progressif de la progression."""
try:
lock = threading.Lock()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
total_size = sum(info.file_size for info in zip_ref.infolist() if not info.is_dir())
logger.info(f"Taille totale à extraire: {total_size} octets")
if total_size == 0:
logger.warning("ZIP vide ou ne contenant que des dossiers")
return True, "ZIP vide extrait avec succès"
extracted_size = 0
os.makedirs(dest_dir, exist_ok=True)
chunk_size = 8192
for info in zip_ref.infolist():
if info.is_dir():
continue
file_path = os.path.join(dest_dir, info.filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with zip_ref.open(info) as source, open(file_path, 'wb') as dest:
file_size = info.file_size
file_extracted = 0
while True:
chunk = source.read(chunk_size)
if not chunk:
break
dest.write(chunk)
file_extracted += len(chunk)
extracted_size += len(chunk)
with lock:
config.download_progress[url]["downloaded_size"] = extracted_size
config.download_progress[url]["total_size"] = total_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = (extracted_size / total_size * 100) if total_size > 0 else 0
config.needs_redraw = True # Forcer le redraw
# Logger une seule ligne à la fin de l'extraction du fichier
progress_percentage = (extracted_size / total_size * 100) if total_size > 0 else 0
logger.debug(f"Extraction terminée pour {info.filename}, file_extracted: {file_extracted}/{file_size}, total_extracted: {extracted_size}/{total_size}, progression: {progress_percentage:.1f}%")
os.chmod(file_path, 0o644)
for root, dirs, _ in os.walk(dest_dir):
for dir_name in dirs:
os.chmod(os.path.join(root, dir_name), 0o755)
os.remove(zip_path)
logger.info(f"Fichier ZIP {zip_path} extrait dans {dest_dir} et supprimé")
return True, "ZIP extrait avec succès"
except Exception as e:
logger.error(f"Erreur lors de l'extraction de {zip_path}: {e}")
return False, str(e)
# Fonction pour extraire le contenu d'un fichier RAR
def extract_rar(rar_path, dest_dir, url):
"""Extrait le contenu du fichier RAR dans le dossier cible, préservant la structure des dossiers."""
try:
lock = threading.Lock()
os.makedirs(dest_dir, exist_ok=True)
result = subprocess.run(['unrar'], capture_output=True, text=True)
if result.returncode not in [0, 1]:
logger.error("Commande unrar non disponible")
return False, "Commande unrar non disponible"
result = subprocess.run(['unrar', 'l', '-v', rar_path], capture_output=True, text=True)
if result.returncode != 0:
error_msg = result.stderr.strip()
logger.error(f"Erreur lors de la liste des fichiers RAR: {error_msg}")
return False, f"Échec de la liste des fichiers RAR: {error_msg}"
logger.debug(f"Sortie brute de 'unrar l -v {rar_path}':\n{result.stdout}")
total_size = 0
files_to_extract = []
root_dirs = set()
lines = result.stdout.splitlines()
in_file_list = False
for line in lines:
if line.startswith("----"):
in_file_list = not in_file_list
continue
if in_file_list:
match = re.match(r'^\s*(\S+)\s+(\d+)\s+\d*\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(.+)$', line)
if match:
attrs = match.group(1)
file_size = int(match.group(2))
file_date = match.group(3)
file_name = match.group(4).strip()
if 'D' not in attrs:
files_to_extract.append((file_name, file_size))
total_size += file_size
root_dir = file_name.split('/')[0] if '/' in file_name else ''
if root_dir:
root_dirs.add(root_dir)
logger.debug(f"Ligne parsée: {file_name}, taille: {file_size}, date: {file_date}")
else:
logger.debug(f"Dossier ignoré: {file_name}")
else:
logger.debug(f"Ligne ignorée (format inattendu): {line}")
logger.info(f"Taille totale à extraire (RAR): {total_size} octets")
logger.debug(f"Fichiers à extraire: {files_to_extract}")
logger.debug(f"Dossiers racines détectés: {root_dirs}")
if total_size == 0:
logger.warning("RAR vide, ne contenant que des dossiers, ou erreur de parsing")
return False, "RAR vide ou erreur lors de la liste des fichiers"
with lock:
config.download_progress[url]["downloaded_size"] = 0
config.download_progress[url]["total_size"] = total_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = 0
config.needs_redraw = True
escaped_rar_path = rar_path.replace(" ", "\\ ")
escaped_dest_dir = dest_dir.replace(" ", "\\ ")
process = subprocess.Popen(['unrar', 'x', '-y', escaped_rar_path, escaped_dest_dir],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = process.communicate()
if process.returncode != 0:
logger.error(f"Erreur lors de l'extraction de {rar_path}: {stderr}")
return False, f"Erreur lors de l'extraction: {stderr}"
extracted_size = 0
extracted_files = []
total_files = len(files_to_extract)
for i, (expected_file, file_size) in enumerate(files_to_extract):
file_path = os.path.join(dest_dir, expected_file)
if os.path.exists(file_path):
extracted_size += file_size
extracted_files.append(expected_file)
os.chmod(file_path, 0o644)
logger.debug(f"Fichier extrait: {expected_file}, taille: {file_size}, chemin: {file_path}")
with lock:
config.download_progress[url]["downloaded_size"] = extracted_size
config.download_progress[url]["status"] = "Extracting"
config.download_progress[url]["progress_percent"] = ((i + 1) / total_files * 100) if total_files > 0 else 0
config.needs_redraw = True
else:
logger.warning(f"Fichier non trouvé après extraction: {expected_file}")
missing_files = [f for f, _ in files_to_extract if f not in extracted_files]
if missing_files:
logger.warning(f"Fichiers non extraits: {', '.join(missing_files)}")
return False, f"Fichiers non extraits: {', '.join(missing_files)}"
if dest_dir == "/userdata/roms/ps3" and len(root_dirs) == 1:
root_dir = root_dirs.pop()
old_path = os.path.join(dest_dir, root_dir)
new_path = os.path.join(dest_dir, f"{root_dir}.ps3")
if os.path.isdir(old_path):
try:
os.rename(old_path, new_path)
logger.info(f"Dossier renommé: {old_path} -> {new_path}")
except Exception as e:
logger.error(f"Erreur lors du renommage de {old_path} en {new_path}: {str(e)}")
return False, f"Erreur lors du renommage du dossier: {str(e)}"
else:
logger.warning(f"Dossier racine {old_path} non trouvé après extraction")
elif dest_dir == "/userdata/roms/ps3" and len(root_dirs) > 1:
logger.warning(f"Plusieurs dossiers racines détectés dans l'archive: {root_dirs}. Aucun renommage effectué.")
for root, dirs, _ in os.walk(dest_dir):
for dir_name in dirs:
os.chmod(os.path.join(root, dir_name), 0o755)
os.remove(rar_path)
logger.info(f"Fichier RAR {rar_path} extrait dans {dest_dir} et supprimé")
return True, "RAR extrait avec succès"
except Exception as e:
logger.error(f"Erreur lors de l'extraction de {rar_path}: {str(e)}")
return False, str(e)
finally:
if os.path.exists(rar_path):
try:
os.remove(rar_path)
logger.info(f"Fichier RAR {rar_path} supprimé après échec de l'extraction")
except Exception as e:
logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}")
def play_random_music(music_files, music_folder, current_music=None):
"""Joue une musique aléatoire et configure l'événement de fin.""" """Joue une musique aléatoire et configure l'événement de fin."""
global current_music
if music_files: if music_files:
# Éviter de rejouer la même musique consécutivement # Éviter de rejouer la même musique consécutivement
available_music = [f for f in music_files if f != current_music] available_music = [f for f in music_files if f != current_music]
@@ -285,14 +500,14 @@ def play_random_music():
pygame.mixer.music.set_volume(0.5) pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(loops=0) # Jouer une seule fois pygame.mixer.music.play(loops=0) # Jouer une seule fois
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin
current_music = music_file # Mettre à jour la musique en cours
set_music_popup(music_file) # Afficher le nom de la musique dans la popup set_music_popup(music_file) # Afficher le nom de la musique dans la popup
return music_file # Retourner la nouvelle musique pour mise à jour
else: else:
logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/music") logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/music")
return current_music
def set_music_popup(music_name): def set_music_popup(music_name):
"""Définit le nom de la musique à afficher dans la popup.""" """Définit le nom de la musique à afficher dans la popup."""
global current_music_name, music_popup_start_time global current_music_name, music_popup_start_time
current_music_name = f"{os.path.splitext(music_name)[0]}" # Utilise l'emoji ♬ directement current_music_name = f"{os.path.splitext(music_name)[0]}" # Utilise l'emoji ♬ directement
music_popup_start_time = pygame.time.get_ticks() / 1000 # Temps actuel en secondes music_popup_start_time = pygame.time.get_ticks() / 1000 # Temps actuel en secondes