diff --git a/__main__.py b/__main__.py index 4070b03..f269aef 100644 --- a/__main__.py +++ b/__main__.py @@ -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) diff --git a/config.py b/config.py index bfd35e8..f02b4f0 100644 --- a/config.py +++ b/config.py @@ -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().""" diff --git a/controls.py b/controls.py index 11470b8..036cb2c 100644 --- a/controls.py +++ b/controls.py @@ -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)}") diff --git a/display.py b/display.py index fd5005d..4951bf4 100644 --- a/display.py +++ b/display.py @@ -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) \ No newline at end of file diff --git a/network.py b/network.py index cef4b6e..a1d7df3 100644 --- a/network.py +++ b/network.py @@ -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.""" diff --git a/utils.py b/utils.py index b559506..54c9d9d 100644 --- a/utils.py +++ b/utils.py @@ -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 \ No newline at end of file