1
0
forked from Mirrors/RGSX

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

View File

@@ -1,19 +1,18 @@
import pygame# type: ignore
import os
os.environ["SDL_FBDEV"] = "/dev/fb0"
import pygame # type: ignore
import asyncio
import platform
import logging
import requests
import config
from config import logger
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 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 network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates
from controls import handle_controls, validate_menu_state
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 config import OTA_data_ZIP
import config
from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT, OTA_data_ZIP
# Configuration du logging
log_dir = "/userdata/roms/ports/RGSX/logs"
@@ -34,44 +33,35 @@ except Exception as e:
logger = logging.getLogger(__name__)
# Initialisation de Pygame
# Initialisation de Pygame et des polices
pygame.init()
config.init_font()
pygame.joystick.init()
pygame.mouse.set_visible(True)
# Détection du système
# Détection du système 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
screen = init_display()
pygame.display.set_caption("RGSX")
clock = pygame.time.Clock()
# Afficher un écran de chargement initial
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
loading_text = config.font.render("Initialisation...", True, (255, 255, 255))
text_rect = loading_text.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
screen.blit(loading_text, text_rect)
pygame.display.flip()
logger.debug("Écran de chargement initial affiché")
# Initialisation des polices
try:
config.font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36) # Police principale
config.title_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48) # Police pour les titres
config.search_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48) # Police pour la recherche
config.progress_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 36) # Police pour l'affichage de la progression
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
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.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
config.history = load_history()
logger.debug(f"Historique chargé: {len(config.history)} entrées")
@@ -101,14 +110,14 @@ if pygame.joystick.get_count() > 0:
joystick.init()
logger.debug("Gamepad initialisé")
# Initialisation de pygame.mixer
# Initialisation du mixer Pygame
pygame.mixer.pre_init(44100, -16, 2, 4096)
pygame.mixer.init()
# Jouer la première musique au démarrage
play_random_music()
# Boucle principale
async def main():
global current_music, music_files, music_folder
logger.debug("Début main")
running = True
loading_step = "none"
@@ -117,33 +126,27 @@ async def main():
config.debounce_delay = 50
config.update_triggered = False
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()
# 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:
clock.tick(60)
clock.tick(60) # Limite à 60 FPS
if config.update_triggered:
logger.debug("Mise à jour déclenchée, arrêt de la boucle principale")
break
current_time = pygame.time.get_ticks()
current_time_sec = current_time / 1000.0
# Forcer redraw toutes les 100 ms dans download_progress
if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100:
config.needs_redraw = True
last_redraw_time = current_time
# Gestion du popup timer
delta_time = current_time - config.last_frame_time
config.last_frame_time = current_time
# Gestion de la fin du popup
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
if config.popup_timer <= 0:
config.menu_state = validate_menu_state(config.previous_menu_state)
@@ -155,15 +158,18 @@ async def main():
# Gestion des événements
events = pygame.event.get()
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:
config.menu_state = "confirm_exit"
config.confirm_selection = 0
config.needs_redraw = True
logger.debug("Événement QUIT détecté, passage à confirm_exit")
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", {})
if start_config and (
(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
logger.debug(f"Ouverture menu pause depuis {config.previous_menu_state}")
continue
if config.menu_state == "pause_menu":
handle_controls(event, sources, joystick, screen)
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}")
continue
@@ -201,12 +207,13 @@ async def main():
continue
if config.menu_state == "confirm_clear_history":
handle_controls(event, sources, joystick, screen)
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans confirm_clear_history: {event.type}")
continue
if config.menu_state == "redownload_game_cache":
handle_controls(event, sources, joystick, screen)
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}")
continue
@@ -223,21 +230,57 @@ async def main():
platform = config.platforms[config.current_platform]
url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None
if url:
logger.debug(f"Vérification de l'extension pour {game_name}, URL: {url}")
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, passage à extension_warning pour {game_name}")
logger.debug(f"Vérification pour {game_name}, URL: {url}")
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")
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:
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform)
config.menu_state = "download_progress"
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Téléchargement démarré pour {game_name}, passage à download_progress")
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, 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:
entry = config.history[config.current_history_item]
platform = entry["platform"]
@@ -245,20 +288,57 @@ async def main():
for game in config.games:
if game[0] == game_name and config.platforms[config.current_platform] == platform:
url = game[1]
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 retéléchargement, passage à extension_warning pour {game_name}")
logger.debug(f"Vérification pour retéléchargement de {game_name}, URL: {url}")
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")
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:
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
config.download_tasks[task] = (task, url, game_name, platform)
config.menu_state = "download_progress"
config.pending_download = None
config.needs_redraw = True
logger.debug(f"Retéléchargement démarré pour {game_name}, passage à download_progress")
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 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
# Gestion des téléchargements
@@ -295,197 +375,6 @@ async def main():
config.needs_redraw = True
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
if config.needs_redraw:
draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"])
@@ -513,6 +402,7 @@ async def main():
draw_extension_warning(screen)
elif config.menu_state == "pause_menu":
draw_pause_menu(screen, config.selected_option)
logger.debug("Rendu de draw_pause_menu")
elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history":
@@ -529,12 +419,12 @@ async def main():
config.needs_redraw = True
logger.error(f"État de menu non valide détecté: {config.menu_state}, retour à platform")
draw_controls(screen, config.menu_state)
draw_music_popup(screen)
pygame.display.flip()
config.needs_redraw = False
# Gestion de l'état controls_mapping
if config.menu_state == "controls_mapping":
logger.debug("Avant appel de map_controls")
try:
success = map_controls(screen)
logger.debug(f"map_controls terminé, succès={success}")
@@ -542,6 +432,7 @@ async def main():
config.controls_config = load_controls_config()
config.menu_state = "loading"
config.needs_redraw = True
logger.debug("Passage à l'état loading après mappage")
else:
config.menu_state = "error"
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.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)
await asyncio.sleep(0.01)

View File

@@ -5,7 +5,8 @@ import logging
logger = logging.getLogger(__name__)
# Version actuelle de l'application
app_version = "1.9.4"
app_version = "1.9.5"
# URL du serveur OTA
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_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
platforms = [] # Liste des plateformes chargées depuis sources.json
current_platform = 0 # Index de la plateforme actuellement sélectionnée
platforms = []
current_platform = 0
platform_names = {} # {platform_id: platform_name}
games = [] # Liste des jeux chargés pour la plateforme actuelle
current_game = 0 # Index du jeu actuellement sélectionné
menu_state = "" # État actuel du menu (par exemple, "main_menu", "game_list", "settings", etc.)
games = []
current_game = 0
menu_state = "popup"
confirm_choice = False
scroll_offset = 0
visible_games = 15
visible_games = 15
popup_start_time = 0
last_progress_update = 0
needs_redraw = True
@@ -39,6 +45,10 @@ download_result_start_time = 0
loading_progress = 0.0
current_loading_system = ""
error_message = ""
repeat_action = None
repeat_start_time = 0
repeat_last_action = 0
repeat_key = None
filtered_games = []
search_mode = False
search_query = ""
@@ -90,6 +100,9 @@ CONTROLS_CONFIG_PATH = "/userdata/saves/ports/rgsx/controls.json"
"""Chemin du fichier de configuration des contrôles."""
HISTORY_PATH = "/userdata/saves/ports/rgsx/history.json"
"""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():
"""Initialise les polices après pygame.init()."""

View File

@@ -6,19 +6,19 @@ import asyncio
import json
import os
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 utils import load_games
from network import download_rom, download_from_1fichier, is_1fichier_url
from utils import load_games, check_extension_before_download, is_extension_supported, load_extensions_json, sanitize_filename
from history import load_history, clear_history
import logging
logger = logging.getLogger(__name__)
# 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)
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)
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
VALID_STATES = [
@@ -177,6 +177,7 @@ def handle_controls(event, sources, joystick, screen):
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.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "up"):
if current_grid_index - GRID_COLS >= 0:
config.selected_platform -= GRID_COLS
@@ -185,6 +186,7 @@ def handle_controls(event, sources, joystick, screen):
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.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "left"):
if col > 0:
config.selected_platform -= 1
@@ -193,6 +195,7 @@ def handle_controls(event, sources, joystick, screen):
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.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif config.current_page > 0:
config.current_page -= 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_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
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "right"):
if col < GRID_COLS - 1 and current_grid_index < max_index:
config.selected_platform += 1
@@ -211,6 +215,7 @@ def handle_controls(event, sources, joystick, screen):
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.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif (config.current_page + 1) * systems_per_page < len(config.platforms):
config.current_page += 1
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_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
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
elif is_input_matched(event, "page_down"):
if (config.current_page + 1) * systems_per_page < len(config.platforms):
config.current_page += 1
@@ -232,6 +238,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_start_time = 0
config.repeat_last_action = current_time
config.needs_redraw = True
logger.debug(f"Plateforme sélectionnée: {config.selected_platform}")
#logger.debug("Page suivante, répétition réinitialisée")
elif is_input_matched(event, "page_up"):
if config.current_page > 0:
@@ -244,6 +251,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_start_time = 0
config.repeat_last_action = current_time
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")
elif is_input_matched(event, "page_up"):
if config.current_page > 0:
@@ -256,6 +264,7 @@ def handle_controls(event, sources, joystick, screen):
config.repeat_start_time = 0
config.repeat_last_action = current_time
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")
elif is_input_matched(event, "progress"):
if config.download_tasks:
@@ -454,55 +463,7 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "history"
config.needs_redraw = True
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"):
config.menu_state = "platform"
config.current_game = 0
@@ -515,6 +476,135 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
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
elif config.menu_state == "history":
history = config.history
@@ -667,41 +757,6 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
#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
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)}")

View File

@@ -1,6 +1,5 @@
import pygame # type: ignore
import config
import os
from utils import truncate_text_middle, wrap_text, load_system_image
import logging
import math
@@ -152,7 +151,9 @@ def draw_loading_screen(screen):
"Bienvenue dans RGSX",
"It's dangerous to go alone, take all you need!",
"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)
@@ -699,7 +700,7 @@ def draw_popup_result_download(screen, message, is_error):
"""Affiche une popup avec un message de résultat."""
screen.blit(OVERLAY, (0, 0))
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}")
# Réduire la largeur maximale pour le wrapping
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):
"""Affiche la liste des contrôles avec un style moderne."""
common_controls = {
"confirm": lambda action: f"{get_control_display('confirm', 'Entrée/A/Croix')} : {action}",
"cancel": lambda action: f"{get_control_display('cancel', 'Échap/B/Rond')} : {action}",
"start": lambda: f"{get_control_display('start', 'Start/')} : Menu",
"progress": lambda action: f"{get_control_display('progress', 'X/Carré')} : {action}",
"confirm": lambda action: f"{get_control_display('confirm', 'Entrée/A')} : {action}",
"cancel": lambda action: f"{get_control_display('cancel', 'Échap/B')} : {action}",
"start": lambda: f"{get_control_display('start', 'Start')} : Menu",
"progress": lambda action: f"{get_control_display('progress', 'X')} : {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}",
"page_up": lambda action: f"{get_control_display('page_up', 'Q/LB/L1')} : {action}",
"page_down": lambda action: f"{get_control_display('page_down', 'E/RB/R1')} : {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')} : {action}",
"filter": lambda action: f"{get_control_display('filter', 'Select')} : {action}",
"history": lambda action: f"{get_control_display('history', 'H/Y/Triangle')} : {action}",
"delete": lambda: f"{get_control_display('delete', 'Backspace/LT/L2')} : Supprimer",
"space": lambda: f"{get_control_display('space', 'Espace/RT/R2')} : Espace"
"history": lambda action: f"{get_control_display('history', 'H')} : {action}",
"delete": lambda: f"{get_control_display('delete', 'Retour Arrière')} : Supprimer",
"space": lambda: f"{get_control_display('space', 'Espace')} : Espace"
}
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_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))
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))
screen.blit(countdown_surface, countdown_rect)

View File

@@ -1,348 +1,123 @@
import requests
import subprocess
import re
import os
import threading
import pygame # type: ignore
import zipfile
import json
import sys
import asyncio
import config
import sys
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
import logging
logger = logging.getLogger(__name__)
JSON_EXTENSIONS = "/userdata/roms/ports/RGSX/rom_extensions.json"
cache = {}
CACHE_TTL = 3600 # 1 heure
def test_internet():
"""Teste la connexion Internet dans un thread séparé."""
logger.debug("Test de connexion Internet")
result = [False]
def ping_thread():
try:
proc = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
result[0] = proc.returncode == 0
logger.debug("Connexion Internet OK" if result[0] else "Échec ping 8.8.8.8")
except Exception as e:
logger.debug(f"Erreur test Internet: {str(e)}")
result[0] = False
thread = threading.Thread(target=ping_thread)
thread.start()
thread.join()
return result[0]
try:
result = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
logger.debug("Connexion Internet OK")
return True
else:
logger.debug("Échec ping 8.8.8.8")
return False
except Exception as e:
logger.debug(f"Erreur test Internet: {str(e)}")
return False
# Fonction pour vérifier et appliquer les mises à jour OTA
async def check_for_updates():
"""Vérifie et applique les mises à jour OTA dans un thread séparé."""
result = [None, None]
def update_thread():
try:
logger.debug("Vérification de la version disponible sur le serveur")
config.current_loading_system = "Mise à jour en cours... Patientez l'écran reste figé..Puis relancer l'application"
config.loading_progress = 5.0
try:
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"
config.loading_progress = 5.0
config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
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:
config.current_loading_system = f"Mise à jour disponible : {latest_version}"
config.loading_progress = 10.0
config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
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}")
logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}")
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
logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}")
update_script_path = "/userdata/roms/ports/rgsx-update.sh"
logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}")
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
await asyncio.sleep(0)
update_script_path = "/userdata/roms/ports/rgsx-update.sh"
logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}")
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.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")
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()
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")
return False, f"Erreur : le script {update_script_path} n'existe pas"
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:
logger.error(f"Erreur lors de l'extraction de {rar_path}: {stderr}")
return False, f"Erreur lors de l'extraction: {stderr}"
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")
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
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}")
logger.debug("Désactivation des événements Pygame QUIT")
pygame.event.set_blocked(pygame.QUIT)
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)}"
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}")
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:
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é.")
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")
return True, "Aucune mise à jour disponible"
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)}")
logger.error(f"Erreur OTA : {str(e)}")
return False, f"Erreur lors de la vérification des mises à jour : {str(e)}"
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}")
@@ -463,31 +238,6 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False):
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):
"""Détecte si l'URL est un lien 1fichier."""

455
utils.py
View File

@@ -3,124 +3,26 @@ import re
import json
import os
import logging
import threading
import requests
import config
import random
import platform
import subprocess
import config
import threading
import zipfile
import random
from config import JSON_EXTENSIONS
from datetime import datetime
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
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}")
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):
"""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)}")
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."""
global current_music
if music_files:
# Éviter de rejouer la même musique consécutivement
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.play(loops=0) # Jouer une seule fois
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
return music_file # Retourner la nouvelle musique pour mise à jour
else:
logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/music")
return current_music
def set_music_popup(music_name):
"""Définit le nom de la musique à afficher dans la popup."""
global current_music_name, music_popup_start_time
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