diff --git a/__main__.py b/__main__.py index f269aef..c251b38 100644 --- a/__main__.py +++ b/__main__.py @@ -5,12 +5,14 @@ import asyncio import platform import logging import requests +import queue +import datetime 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 detect_non_pc, load_sources, check_extension_before_download, extract_zip, play_random_music -from history import load_history +from history import load_history, save_history import config from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT, OTA_data_ZIP @@ -132,7 +134,7 @@ async def main(): clock = pygame.time.Clock() while running: - clock.tick(60) # Limite à 60 FPS + clock.tick(30) # Limite à 60 FPS if config.update_triggered: logger.debug("Mise à jour déclenchée, arrêt de la boucle principale") break @@ -143,6 +145,12 @@ async def main(): if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100: config.needs_redraw = True last_redraw_time = current_time + # Forcer redraw toutes les 100 ms dans history avec téléchargement actif + if config.menu_state == "history" and any(entry["status"] == "Téléchargement" for entry in config.history): + if current_time - last_redraw_time >= 100: + config.needs_redraw = True + last_redraw_time = current_time + # logger.debug("Forcing redraw in history state due to active download") # Gestion de la fin du popup if config.menu_state == "restart_popup" and config.popup_timer > 0: @@ -218,7 +226,40 @@ async def main(): logger.debug(f"Événement transmis à handle_controls dans redownload_game_cache: {event.type}") continue - if config.menu_state in ["platform", "game", "error", "confirm_exit", "download_progress", "download_result", "extension_warning", "history"]: + if config.menu_state == "extension_warning": + action = handle_controls(event, sources, joystick, screen) + config.needs_redraw = True + if action == "confirm": + if config.pending_download and config.extension_confirm_selection == 0: # Oui + url, platform, game_name, is_zip_non_supported = config.pending_download + logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}") + task_id = str(pygame.time.get_ticks()) + config.history.append({ + "platform": platform, + "game_name": game_name, + "status": "downloading", + "progress": 0, + "url": url, + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }) + config.current_history_item = len(config.history) - 1 + save_history(config.history) + config.download_tasks[task_id] = ( + asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id)), + url, game_name, platform + ) + config.menu_state = "history" + config.pending_download = None + config.needs_redraw = True + logger.debug(f"Téléchargement démarré pour {game_name}, task_id={task_id}") + elif config.extension_confirm_selection == 1: # Non + config.menu_state = config.previous_menu_state + config.pending_download = None + config.needs_redraw = True + logger.debug("Téléchargement annulé, retour à l'état précédent") + continue + + if config.menu_state in ["platform", "game", "error", "confirm_exit", "download_progress", "download_result", "history"]: action = handle_controls(event, sources, joystick, screen) config.needs_redraw = True if action == "quit": @@ -227,10 +268,20 @@ async def main(): elif action == "download" and config.menu_state == "game" and config.filtered_games: game = config.filtered_games[config.current_game] game_name = game[0] if isinstance(game, (list, tuple)) else game - platform = config.platforms[config.current_platform] + platform = config.platforms[config.current_platform]["name"] # Utiliser le nom de la plateforme url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None if url: logger.debug(f"Vérification pour {game_name}, URL: {url}") + # Ajouter une entrée temporaire à l'historique + config.history.append({ + "platform": platform, + "game_name": game_name, + "status": "downloading", + "progress": 0, + "url": url, + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }) + config.current_history_item = len(config.history) - 1 # Sélectionner l'entrée en cours if is_1fichier_url(url): if not config.API_KEY_1FICHIER: config.previous_menu_state = config.menu_state @@ -238,6 +289,11 @@ async def main(): config.error_message = ( "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt" ) + # Mettre à jour l'entrée temporaire avec l'erreur + config.history[-1]["status"] = "Erreur" + config.history[-1]["progress"] = 0 + config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente" + save_history(config.history) config.needs_redraw = True logger.error("Clé API 1fichier absente") config.pending_download = None @@ -249,18 +305,20 @@ async def main(): config.extension_confirm_selection = 0 config.needs_redraw = True logger.debug(f"Extension non reconnue pour lien 1fichier, passage à extension_warning pour {game_name}") + # Supprimer l'entrée temporaire si erreur + config.history.pop() 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 + # Lancer le téléchargement dans une tâche asynchrone + task_id = str(pygame.time.get_ticks()) + config.download_tasks[task_id] = ( + asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported)), + url, game_name, platform + ) + config.menu_state = "history" # Passer à l'historique config.needs_redraw = True - logger.debug(f"Téléchargement 1fichier terminé pour {game_name}, succès={success}, message={message}") + logger.debug(f"Téléchargement 1fichier démarré pour {game_name}, passage à l'historique") else: is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name) if not is_supported: @@ -269,18 +327,20 @@ async def main(): config.extension_confirm_selection = 0 config.needs_redraw = True logger.debug(f"Extension non reconnue, passage à extension_warning pour {game_name}") + # Supprimer l'entrée temporaire si erreur + config.history.pop() 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 + # Lancer le téléchargement dans une tâche asynchrone + task_id = str(pygame.time.get_ticks()) + config.download_tasks[task_id] = ( + asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)), + url, game_name, platform + ) + config.menu_state = "history" # Passer à l'historique config.needs_redraw = True - logger.debug(f"Téléchargement terminé pour {game_name}, succès={success}, message={message}") + logger.debug(f"Téléchargement démarré pour {game_name}, passage à l'historique") elif action == "redownload" and config.menu_state == "history" and config.history: entry = config.history[config.current_history_item] platform = entry["platform"] @@ -340,13 +400,27 @@ async def main(): 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 if config.download_tasks: for task_id, (task, url, game_name, platform) in list(config.download_tasks.items()): if task.done(): try: success, message = await task + if "http" in message: + message = message.split("https://")[0].strip() + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["status"] = "Download_OK" if success else "Erreur" + entry["progress"] = 100 if success else 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}, task_id={task_id}") + break config.download_result_message = message config.download_result_error = not success config.download_result_start_time = pygame.time.get_ticks() @@ -355,9 +429,59 @@ async def main(): config.pending_download = None config.needs_redraw = True del config.download_tasks[task_id] - logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}") except Exception as e: - config.download_result_message = f"Erreur lors du téléchargement : {str(e)}" + message = f"Erreur lors du téléchargement: {str(e)}" + if "http" in message: + message = message.split("https://")[0].strip() + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["status"] = "Erreur" + entry["progress"] = 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Erreur téléchargement: {game_name}, message={message}, task_id={task_id}") + break + config.download_result_message = message + config.download_result_error = True + 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 + del config.download_tasks[task_id] + else: + # Traiter les mises à jour de progression + + progress_queue = queue.Queue() + while not progress_queue.empty(): + data = progress_queue.get() + # logger.debug(f"Progress queue data received: {data}, task_id={task_id}") + if len(data) != 3 or data[0] != task_id: # Ignorer les données d'une autre tâche + logger.debug(f"Ignoring queue data for task_id={data[0]}, expected={task_id}") + continue + if isinstance(data[1], bool): # Fin du téléchargement + success, message = data[1], data[2] + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["status"] = "Download_OK" if success else "Erreur" + entry["progress"] = 100 if success else 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") + break + else: + downloaded, total_size = data[1], data[2] + progress = (downloaded / total_size * 100) if total_size > 0 else 0 + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["progress"] = progress + entry["status"] = "Téléchargement" + config.needs_redraw = True + # logger.debug(f"Progress updated in history: {progress:.1f}% for {game_name}, task_id={task_id}") + break + config.download_result_message = message config.download_result_error = True config.download_result_start_time = pygame.time.get_ticks() config.menu_state = "download_result" @@ -365,19 +489,20 @@ async def main(): config.pending_download = None config.needs_redraw = True del config.download_tasks[task_id] - logger.error(f"Erreur dans tâche de téléchargement: {str(e)}") # Gestion de la fin du popup download_result if config.menu_state == "download_result" and current_time - config.download_result_start_time > 3000: - config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "history"] else "game" + config.menu_state = "history" # Rester dans l'historique après le popup config.download_progress.clear() config.pending_download = None config.needs_redraw = True - logger.debug(f"Fin popup download_result, retour à {config.menu_state}") + logger.debug(f"Fin popup download_result, retour à history") # Affichage if config.needs_redraw: draw_gradient(screen, THEME_COLORS["background_top"], THEME_COLORS["background_bottom"]) + + if config.menu_state == "controls_mapping": draw_controls_mapping(screen, ACTIONS[0], None, False, 0.0) elif config.menu_state == "loading": @@ -406,7 +531,8 @@ async def main(): elif config.menu_state == "controls_help": draw_controls_help(screen, config.previous_menu_state) elif config.menu_state == "history": - draw_history_list(screen) + draw_history_list(screen) + # logger.debug("Screen updated with draw_history_list") elif config.menu_state == "confirm_clear_history": draw_clear_history_dialog(screen) elif config.menu_state == "redownload_game_cache": @@ -420,7 +546,9 @@ async def main(): logger.error(f"État de menu non valide détecté: {config.menu_state}, retour à platform") draw_controls(screen, config.menu_state) pygame.display.flip() + config.needs_redraw = False + # logger.debug("Screen flipped with pygame.display.flip()") # Gestion de l'état controls_mapping if config.menu_state == "controls_mapping": diff --git a/assets/music/aquatic_ambience.mp3 b/assets/music/aquatic_ambience.mp3 new file mode 100644 index 0000000..d5578ae Binary files /dev/null and b/assets/music/aquatic_ambience.mp3 differ diff --git a/config.py b/config.py index f02b4f0..c0d307c 100644 --- a/config.py +++ b/config.py @@ -5,7 +5,7 @@ import logging logger = logging.getLogger(__name__) # Version actuelle de l'application -app_version = "1.9.5" +app_version = "1.9.6" # URL du serveur OTA @@ -37,11 +37,7 @@ transition_state = "idle" transition_progress = 0.0 transition_duration = 18 games_count = {} -download_tasks = {} -download_progress = {} -download_result_message = "" -download_result_error = False -download_result_start_time = 0 + loading_progress = 0.0 current_loading_system = "" error_message = "" @@ -58,8 +54,16 @@ pending_download = None controls_config = {} selected_option = 0 previous_menu_state = None -history = [] # Liste des entrées de l'historique -current_history_item = 0 # Index de l'élément sélectionné dans l'historique +history = [] # Liste des entrées d'historique avec platform, game_name, status, url, progress, message, timestamp +download_progress = {} +download_tasks = {} # Dictionnaire pour les tâches de téléchargement +download_result_message = "" +download_result_error = False +download_result_start_time = 0 +pending_download = None +needs_redraw = False +current_history_item = 0 +history_scroll_offset = 0 history_scroll_offset = 0 # Offset pour le défilement de l'historique visible_history_items = 15 # Nombre d'éléments d'historique visibles (ajusté dynamiquement) confirm_clear_selection = 0 # confirmation clear historique diff --git a/controls.py b/controls.py index 036cb2c..63ae696 100644 --- a/controls.py +++ b/controls.py @@ -8,7 +8,7 @@ import os from display import draw_validation_transition 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 +from history import load_history, clear_history, add_to_history, save_history import logging logger = logging.getLogger(__name__) @@ -177,7 +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}") + # 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 @@ -186,7 +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}") + # logger.debug(f"Plateforme sélectionnée: {config.selected_platform}") elif is_input_matched(event, "left"): if col > 0: config.selected_platform -= 1 @@ -195,7 +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}") + # 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) @@ -206,7 +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}") + # 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 @@ -215,7 +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}") + # 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 @@ -226,7 +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}") + # 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 @@ -238,7 +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(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: @@ -251,7 +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(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: @@ -264,7 +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(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: @@ -411,7 +411,6 @@ def handle_controls(event, sources, joystick, screen): logger.debug("Sortie du mode recherche") else: - if is_input_matched(event, "up"): if config.current_game > 0: config.current_game -= 1 @@ -435,7 +434,6 @@ def handle_controls(event, sources, joystick, screen): config.repeat_start_time = 0 config.repeat_last_action = current_time config.needs_redraw = True - #logger.debug("Page précédente dans la liste des jeux") elif is_input_matched(event, "page_down"): config.current_game = min(len(games) - 1, config.current_game + config.visible_games) config.repeat_action = None @@ -443,7 +441,6 @@ def handle_controls(event, sources, joystick, screen): config.repeat_start_time = 0 config.repeat_last_action = current_time config.needs_redraw = True - #logger.debug("Page suivante dans la liste des jeux") elif is_input_matched(event, "filter"): config.search_mode = True config.search_query = "" @@ -462,8 +459,7 @@ def handle_controls(event, sources, joystick, screen): elif is_input_matched(event, "history"): config.menu_state = "history" config.needs_redraw = True - logger.debug("Ouverture history depuis game") - + logger.debug("Ouverture history depuis game") elif is_input_matched(event, "cancel"): config.menu_state = "platform" config.current_game = 0 @@ -475,27 +471,39 @@ def handle_controls(event, sources, joystick, screen): config.menu_state = "redownload_game_cache" config.needs_redraw = True logger.debug("Passage à redownload_game_cache depuis game") - - # Sélectionner un jeu , evenent confirm + # Sélectionner un jeu, événement 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] + platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform] logger.debug(f"Vérification pour {game_name}, URL: {url}") + # Ajouter une entrée temporaire à l'historique + config.history.append(add_to_history( + platform=platform, + game_name=game_name, + status="downloading", + url=url, + progress=0, + message="Téléchargement en cours" + )) + config.current_history_item = len(config.history) - 1 # 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" + "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un éditeur de texte et coller la clé API" ) + config.history[-1]["status"] = "Erreur" + config.history[-1]["progress"] = 0 + config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente" + save_history(config.history) 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( @@ -509,14 +517,15 @@ def handle_controls(event, sources, joystick, screen): config.extension_confirm_selection = 0 config.needs_redraw = True logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}") + config.history.pop() # Supprimer l'entrée temporaire 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) + task_id = str(pygame.time.get_ticks()) + task = asyncio.create_task(download_from_1fichier(url, platform, game_name, config.pending_download[3], task_id)) + config.download_tasks[task_id] = (task, url, game_name, platform) config.previous_menu_state = config.menu_state - config.menu_state = "download_progress" + config.menu_state = "history" # Passer à l'historique config.needs_redraw = True - logger.debug(f"Début du téléchargement 1fichier: {game_name} pour {platform} depuis {url}") + logger.debug(f"Début du téléchargement 1fichier: {game_name} pour {platform} depuis {url}, task_id={task_id}") config.pending_download = None action = "download" else: @@ -525,8 +534,8 @@ def handle_controls(event, sources, joystick, screen): config.pending_download = None config.needs_redraw = True logger.error(f"config.pending_download est None pour {game_name}") + config.history.pop() # Supprimer l'entrée temporaire 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( @@ -540,13 +549,15 @@ def handle_controls(event, sources, joystick, screen): config.extension_confirm_selection = 0 config.needs_redraw = True logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}") + config.history.pop() # Supprimer l'entrée temporaire else: - task = asyncio.create_task(download_rom(url, platform, game_name, config.pending_download[3])) - config.download_tasks[task] = (task, url, game_name, platform) + task_id = str(pygame.time.get_ticks()) + task = asyncio.create_task(download_rom(url, platform, game_name, config.pending_download[3], task_id)) + config.download_tasks[task_id] = (task, url, game_name, platform) config.previous_menu_state = config.menu_state - config.menu_state = "download_progress" + config.menu_state = "history" # Passer à l'historique config.needs_redraw = True - logger.debug(f"Début du téléchargement: {game_name} pour {platform} depuis {url}") + logger.debug(f"Début du téléchargement: {game_name} pour {platform} depuis {url}, task_id={task_id}") config.pending_download = None action = "download" else: @@ -555,6 +566,7 @@ def handle_controls(event, sources, joystick, screen): config.pending_download = None config.needs_redraw = True logger.error(f"config.pending_download est None pour {game_name}") + config.history.pop() # Supprimer l'entrée temporaire # Avertissement extension elif config.menu_state == "extension_warning": @@ -562,6 +574,16 @@ def handle_controls(event, sources, joystick, screen): 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 + # Ajouter une entrée temporaire à l'historique + config.history.append(add_to_history( + platform=platform, + game_name=game_name, + status="downloading", + url=url, + progress=0, + message="Téléchargement en cours" + )) + config.current_history_item = len(config.history) - 1 if is_1fichier_url(url): if not config.API_KEY_1FICHIER: config.previous_menu_state = config.menu_state @@ -569,19 +591,24 @@ def handle_controls(event, sources, joystick, screen): config.error_message = ( "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt" ) + config.history[-1]["status"] = "Erreur" + config.history[-1]["progress"] = 0 + config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente" + save_history(config.history) 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) + task_id = str(pygame.time.get_ticks()) + task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id)) else: - task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported)) - config.download_tasks[task] = (task, url, game_name, platform) + task_id = str(pygame.time.get_ticks()) + task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id)) + config.download_tasks[task_id] = (task, url, game_name, platform) config.previous_menu_state = validate_menu_state(config.previous_menu_state) - config.menu_state = "download_progress" + config.menu_state = "history" # Passer à l'historique config.needs_redraw = True - logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}") + logger.debug(f"Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}, task_id={task_id}") config.pending_download = None action = "download" else: @@ -590,6 +617,7 @@ def handle_controls(event, sources, joystick, screen): config.pending_download = None config.needs_redraw = True logger.error("config.pending_download invalide") + config.history.pop() # Supprimer l'entrée temporaire else: config.pending_download = None config.menu_state = validate_menu_state(config.previous_menu_state) @@ -598,7 +626,6 @@ def handle_controls(event, sources, joystick, screen): 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) @@ -653,22 +680,40 @@ def handle_controls(event, sources, joystick, screen): game_name = entry["game_name"] for game in config.games: if game[0] == game_name and config.platforms[config.current_platform] == platform: - config.pending_download = check_extension_before_download(game_name, platform, game[1]) + config.pending_download = check_extension_before_download(game[1], platform, game_name) if config.pending_download: url, platform, game_name, is_zip_non_supported = config.pending_download if is_zip_non_supported: - config.previous_menu_state = config.menu_state # Remplacer cette ligne + 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 pour retéléchargement, passage à extension_warning pour {game_name}") 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 # Remplacer cette ligne - config.menu_state = "download_progress" + task_id = str(pygame.time.get_ticks()) + 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.history[-1]["status"] = "Erreur" + config.history[-1]["progress"] = 0 + config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente" + save_history(config.history) + config.needs_redraw = True + logger.error("Clé API 1fichier absente, retéléchargement impossible.") + config.pending_download = None + return action + task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id)) + else: + task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id)) + config.download_tasks[task_id] = (task, url, game_name, platform) + config.previous_menu_state = config.menu_state + config.menu_state = "history" config.needs_redraw = True - logger.debug(f"Retéléchargement: {game_name} pour {platform} depuis {url}") + logger.debug(f"Retéléchargement: {game_name} pour {platform} depuis {url}, task_id={task_id}") config.pending_download = None action = "redownload" else: diff --git a/display.py b/display.py index 4951bf4..959157b 100644 --- a/display.py +++ b/display.py @@ -1,6 +1,6 @@ import pygame # type: ignore import config -from utils import truncate_text_middle, wrap_text, load_system_image +from utils import truncate_text_middle, wrap_text, load_system_image, truncate_text_end import logging import math from history import load_history # Ajout de l'import @@ -437,13 +437,20 @@ def draw_game_scrollbar(screen, scroll_offset, total_items, visible_items, x, y, pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (x, scrollbar_y, 15, scrollbar_height), border_radius=4) def draw_history_list(screen): - """Affiche l'historique des téléchargements avec un style moderne.""" + # logger.debug(f"Dessin historique, history={config.history}, needs_redraw={config.needs_redraw}") history = config.history if hasattr(config, 'history') else load_history() history_count = len(history) - col_platform_width = int((0.95 * config.screen_width - 60) * 0.33) - col_game_width = int((0.95 * config.screen_width - 60) * 0.50) - col_status_width = int((0.95 * config.screen_width - 60) * 0.17) + # Define column widths as percentages of available space + column_width_percentages = { + "platform": 0.25, # platform column + "game_name": 0.50, # game name column + "status": 0.25 # status column + } + available_width = int(0.95 * config.screen_width - 60) # Total available width for columns + col_platform_width = int(available_width * column_width_percentages["platform"]) + col_game_width = int(available_width * column_width_percentages["game_name"]) + col_status_width = int(available_width * column_width_percentages["status"]) rect_width = int(0.95 * config.screen_width) line_height = config.small_font.get_height() + 10 @@ -514,7 +521,6 @@ def draw_history_list(screen): text_rect = text_surface.get_rect(center=(x_pos, header_y)) screen.blit(text_surface, text_rect) - # Ajouter un séparateur sous les en-têtes separator_y = rect_y + margin_top_bottom + header_height pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, separator_y), (rect_x + rect_width - 20, separator_y), 2) @@ -523,10 +529,28 @@ def draw_history_list(screen): platform = entry.get("platform", "Inconnu") game_name = entry.get("game_name", "Inconnu") status = entry.get("status", "Inconnu") + progress = entry.get("progress", 0) + # Personnaliser l'affichage du statut + if status in ["Téléchargement", "downloading"]: + status_text = f"Téléchargement : {progress:.1f}%" + # logger.debug(f"Affichage progression: {progress:.1f}% pour {game_name}, status={status_text}") + elif status == "Extracting": + status_text = f"Extraction : {progress:.1f}%" + # logger.debug(f"Affichage extraction: {progress:.1f}% pour {game_name}, status={status_text}") + elif status == "Download_OK": + status_text = "Terminé" + # logger.debug(f"Affichage terminé: {game_name}, status={status_text}") + elif status == "Erreur": + status_text = f"Erreur : {entry.get('message', 'Échec')}" + logger.debug(f"Affichage erreur: {game_name}, status={status_text}") + else: + status_text = status + logger.debug(f"Affichage statut inconnu: {game_name}, status={status_text}") + color = THEME_COLORS["fond_lignes"] if i == config.current_history_item else THEME_COLORS["text"] - platform_text = truncate_text_middle(platform, config.small_font, col_platform_width - 10) - game_text = truncate_text_middle(game_name, config.small_font, col_game_width - 10) - status_text = truncate_text_middle(status, config.small_font, col_status_width - 10) + platform_text = truncate_text_end(platform, config.small_font, col_platform_width - 10) + game_text = truncate_text_end(game_name, config.small_font, col_game_width - 10) + status_text = truncate_text_middle(status_text, config.small_font, col_status_width - 10, is_filename=False) y_pos = rect_y + margin_top_bottom + header_height + idx * line_height + line_height // 2 platform_surface = config.small_font.render(platform_text, True, color) @@ -697,31 +721,41 @@ def draw_progress_screen(screen): # Écran popup résultat téléchargement def draw_popup_result_download(screen, message, is_error): - """Affiche une popup avec un message de résultat.""" - screen.blit(OVERLAY, (0, 0)) + """Affiche une popup flottante centrée avec un message de résultat.""" if message is None: 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) - # Débogage pour vérifier les lignes wrappées - logger.debug(f"Lignes wrappées : {wrapped_message}") + max_popup_width = config.screen_width // 3 # Popup prend 1/3 de la largeur de l'écran + wrapped_message = wrap_text(message, config.small_font, max_popup_width - 40) # 40 pixels de marge interne line_height = config.small_font.get_height() + 5 text_height = len(wrapped_message) * line_height - margin_top_bottom = 20 + margin_top_bottom = 15 + margin_sides = 20 rect_height = text_height + 2 * margin_top_bottom - max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message], default=300) - rect_width = max_text_width + 100 # Augmenter la marge + max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message], default=200) + rect_width = min(max_text_width + 2 * margin_sides, max_popup_width) + + # Positionner la popup au centre de l'écran rect_x = (config.screen_width - rect_width) // 2 rect_y = (config.screen_height - rect_height) // 2 - pygame.draw.rect(screen, THEME_COLORS["button_idle"], (rect_x, rect_y, rect_width, rect_height), border_radius=12) - pygame.draw.rect(screen, THEME_COLORS["border"], (rect_x, rect_y, rect_width, rect_height), 2, border_radius=12) + # Fond semi-transparent pour un effet flottant + popup_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA) + # Utiliser button_idle sans modifier son alpha (déjà à 150) + pygame.draw.rect(popup_surface, THEME_COLORS["button_idle"], (0, 0, rect_width, rect_height), border_radius=10) + # Bordure sans alpha modifié + pygame.draw.rect(popup_surface, THEME_COLORS["border"], (0, 0, rect_width, rect_height), 2, border_radius=10) + # Afficher le texte for i, line in enumerate(wrapped_message): - text = config.small_font.render(line, True, THEME_COLORS["error_text"] if is_error else THEME_COLORS["fond_lignes"]) - text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2)) - screen.blit(text, text_rect) + text = config.small_font.render(line, True, THEME_COLORS["error_text"] if is_error else THEME_COLORS["text"]) + text_rect = text.get_rect(center=(rect_width // 2, margin_top_bottom + i * line_height + line_height // 2)) + popup_surface.blit(text, text_rect) + + # Appliquer la surface de la popup sur l'écran + screen.blit(popup_surface, (rect_x, rect_y)) # Écran avertissement extension non supportée téléchargement def draw_extension_warning(screen): @@ -795,7 +829,9 @@ def draw_extension_warning(screen): def draw_controls(screen, menu_state): """Affiche les contrôles sur une seule ligne en bas de l’écran.""" start_button = get_control_display('start', 'START') - control_text = f"RGSX v{config.app_version} - {start_button} : Options - History - Help" + history_button = get_control_display('history', 'H') + filter_button = get_control_display('filter', 'F') + control_text = f"RGSX v{config.app_version} - {start_button} : Options - {history_button}: Historique - {filter_button} : Filtrer (bug)" max_width = config.screen_width - 40 wrapped_controls = wrap_text(control_text, config.small_font, max_width) line_height = config.small_font.get_height() + 5 diff --git a/history.py b/history.py index 459c8bd..b25ac0d 100644 --- a/history.py +++ b/history.py @@ -2,6 +2,7 @@ import json import os import logging import config +from datetime import datetime logger = logging.getLogger(__name__) @@ -15,26 +16,30 @@ def init_history(): if not os.path.exists(history_path): try: os.makedirs(os.path.dirname(history_path), exist_ok=True) - with open(history_path, "w") as f: + with open(history_path, "w", encoding='utf-8') as f: json.dump([], f) # Initialise avec une liste vide logger.info(f"Fichier d'historique créé : {history_path}") except OSError as e: logger.error(f"Erreur lors de la création du fichier d'historique : {e}") else: logger.info(f"Fichier d'historique trouvé : {history_path}") - return history_path + return history_path def load_history(): """Charge l'historique depuis history.json.""" history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) try: - with open(history_path, "r") as f: + if not os.path.exists(history_path): + logger.debug(f"Aucun fichier d'historique trouvé à {history_path}") + return [] + with open(history_path, "r", encoding='utf-8') as f: history = json.load(f) # Valider la structure : liste de dictionnaires avec 'platform', 'game_name', 'status' for entry in history: if not all(key in entry for key in ['platform', 'game_name', 'status']): logger.warning(f"Entrée d'historique invalide : {entry}") return [] + logger.debug(f"Historique chargé depuis {history_path}, {len(history)} entrées") return history except (FileNotFoundError, json.JSONDecodeError) as e: logger.error(f"Erreur lors de la lecture de {history_path} : {e}") @@ -44,28 +49,36 @@ def save_history(history): """Sauvegarde l'historique dans history.json.""" history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) try: - with open(history_path, "w") as f: - json.dump(history, f, indent=2) + os.makedirs(os.path.dirname(history_path), exist_ok=True) + with open(history_path, "w", encoding='utf-8') as f: + json.dump(history, f, indent=2, ensure_ascii=False) logger.debug(f"Historique sauvegardé dans {history_path}") except Exception as e: logger.error(f"Erreur lors de l'écriture de {history_path} : {e}") -def add_to_history(platform, game_name, status): +def add_to_history(platform, game_name, status, url=None, progress=0, message=None, timestamp=None): """Ajoute une entrée à l'historique.""" history = load_history() - history.append({ + entry = { "platform": platform, "game_name": game_name, - "status": status - }) + "status": status, + "url": url, + "progress": progress, + "timestamp": timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + if message: + entry["message"] = message + history.append(entry) save_history(history) - logger.info(f"Ajout à l'historique : platform={platform}, game_name={game_name}, status={status}") + logger.info(f"Ajout à l'historique : platform={platform}, game_name={game_name}, status={status}, progress={progress}") + return entry def clear_history(): """Vide l'historique.""" history_path = getattr(config, 'HISTORY_PATH', DEFAULT_HISTORY_PATH) try: - with open(history_path, "w") as f: + with open(history_path, "w", encoding='utf-8') as f: json.dump([], f) logger.info(f"Historique vidé : {history_path}") except Exception as e: diff --git a/network.py b/network.py index a1d7df3..365473c 100644 --- a/network.py +++ b/network.py @@ -8,8 +8,10 @@ import asyncio import config from config import OTA_VERSION_ENDPOINT, OTA_UPDATE_SCRIPT from utils import sanitize_filename, extract_zip, extract_rar -from history import add_to_history, load_history +from history import save_history import logging +import queue +import time logger = logging.getLogger(__name__) @@ -118,13 +120,26 @@ async def check_for_updates(): +# File d'attente pour la progression +import queue +progress_queue = queue.Queue() -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}") + + +async def download_rom(url, platform, game_name, is_zip_non_supported=False, task_id=None): + logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}") result = [None, None] + # Vider la file d'attente avant de commencer + while not progress_queue.empty(): + try: + progress_queue.get_nowait() + logger.debug(f"File progress_queue vidée pour {game_name}") + except queue.Empty: + break + def download_thread(): - logger.debug(f"Thread téléchargement démarré pour {url}") + logger.debug(f"Thread téléchargement démarré pour {url}, task_id={task_id}") try: dest_dir = None for platform_dict in config.platform_dicts: @@ -132,10 +147,8 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False): 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) + dest_dir = os.path.join("/userdata/roms", platform.lower().replace(" ", "")) - logger.debug(f"Vérification répertoire destination: {dest_dir}") os.makedirs(dest_dir, exist_ok=True) if not os.access(dest_dir, os.W_OK): raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}") @@ -144,118 +157,106 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False): dest_path = os.path.join(dest_dir, f"{sanitized_name}") logger.debug(f"Chemin destination: {dest_path}") - lock = threading.Lock() - with lock: - config.download_progress[url] = { - "downloaded_size": 0, - "total_size": 0, - "status": "Téléchargement", - "progress_percent": 0, - "game_name": game_name - } - config.needs_redraw = True # Forcer le redraw - logger.debug(f"Progression initialisée pour {url}") - headers = {'User-Agent': 'Mozilla/5.0'} - logger.debug(f"Envoi requête GET à {url}") response = requests.get(url, stream=True, headers=headers, timeout=30) - logger.debug(f"Réponse reçue, status: {response.status_code}") response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) logger.debug(f"Taille totale: {total_size} octets") - with lock: - config.download_progress[url]["total_size"] = total_size - config.needs_redraw = True # Forcer le redraw + + # Initialiser la progression avec task_id + progress_queue.put((task_id, 0, total_size)) + logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}") downloaded = 0 + chunk_size = 4096 + last_update_time = time.time() + update_interval = 0.5 # Mettre à jour toutes les 0,5 secondes with open(dest_path, 'wb') as f: - logger.debug(f"Ouverture fichier: {dest_path}") - for chunk in response.iter_content(chunk_size=8192): + for chunk in response.iter_content(chunk_size=chunk_size): if chunk: + size_received = len(chunk) f.write(chunk) - downloaded += len(chunk) - with lock: - config.download_progress[url]["downloaded_size"] = downloaded - config.download_progress[url]["status"] = "Téléchargement" - config.download_progress[url]["progress_percent"] = (downloaded / total_size * 100) if total_size > 0 else 0 - config.needs_redraw = True # Forcer le redraw - #logger.debug(f"Progression: {downloaded}/{total_size} octets, {config.download_progress[url]['progress_percent']:.1f}%") + downloaded += size_received + current_time = time.time() + if current_time - last_update_time >= update_interval: + progress = (downloaded / total_size * 100) if total_size > 0 else 0 + progress_queue.put((task_id, downloaded, total_size)) + # logger.debug(f"Progress update sent: {progress:.1f}% for {game_name}, task_id={task_id}") + last_update_time = current_time + else: + logger.debug("Chunk vide reçu") - if is_zip_non_supported: - with lock: - config.download_progress[url]["downloaded_size"] = 0 - config.download_progress[url]["total_size"] = 0 - config.download_progress[url]["status"] = "Extracting" - config.download_progress[url]["progress_percent"] = 0 - config.needs_redraw = True # Forcer le redraw - extension = os.path.splitext(dest_path)[1].lower() - if extension == ".zip": - success, msg = extract_zip(dest_path, dest_dir, url) - elif extension == ".rar": - success, msg = extract_rar(dest_path, dest_dir, url) - else: - raise Exception(f"Type d'archive non supporté: {extension}") - if not success: - raise Exception(f"Échec de l'extraction de l'archive: {msg}") - result[0] = True - result[1] = f"Downloaded / extracted : {game_name}" - else: - os.chmod(dest_path, 0o644) - logger.debug(f"Téléchargement terminé: {dest_path}") - result[0] = True - result[1] = f"Download_OK : {game_name}" + os.chmod(dest_path, 0o644) + logger.debug(f"Téléchargement terminé: {dest_path}") + result[0] = True + result[1] = f"Download_OK: {game_name}" except Exception as e: logger.error(f"Erreur téléchargement {url}: {str(e)}") - if url in config.download_progress: - with lock: - del config.download_progress[url] - if os.path.exists(dest_path): - os.remove(dest_path) result[0] = False - result[1] = f"Erreur téléchargement {game_name}" + result[1] = f"Erreur téléchargement {game_name}: {str(e)}" finally: - logger.debug(f"Thread téléchargement terminé pour {url}") - with lock: - config.download_result_message = result[1] - config.download_result_error = not result[0] - config.download_result_start_time = pygame.time.get_ticks() - config.menu_state = "download_result" - config.needs_redraw = True # Forcer le redraw - # Enregistrement dans l'historique - add_to_history(platform, game_name, "OK" if result[0] else "Error") - config.history = load_history() # Recharger l'historique - logger.debug(f"Enregistrement dans l'historique: platform={platform}, game_name={game_name}, status={'Download_OK' if result[0] else 'Erreur'}") + logger.debug(f"Thread téléchargement terminé pour {url}, task_id={task_id}") + progress_queue.put((task_id, result[0], result[1])) + logger.debug(f"Final result sent to queue: success={result[0]}, message={result[1]}, task_id={task_id}") thread = threading.Thread(target=download_thread) - logger.debug(f"Démarrage thread pour {url}") thread.start() - while thread.is_alive(): - pygame.event.pump() - await asyncio.sleep(0.1) - thread.join() - logger.debug(f"Thread rejoint pour {url}") + # Boucle principale pour mettre à jour la progression + while thread.is_alive(): + try: + while not progress_queue.empty(): + data = progress_queue.get() + logger.debug(f"Progress queue data received: {data}") + if len(data) != 3 or data[0] != task_id: # Ignorer les données d'une autre tâche + logger.debug(f"Ignoring queue data for task_id={data[0]}, expected={task_id}") + continue + if isinstance(data[1], bool): # Fin du téléchargement + success, message = data[1], data[2] + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["status"] = "Download_OK" if success else "Erreur" + entry["progress"] = 100 if success else 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") + break + else: + downloaded, total_size = data[1], data[2] + progress = (downloaded / total_size * 100) if total_size > 0 else 0 + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["progress"] = progress + entry["status"] = "Téléchargement" + config.needs_redraw = True + # logger.debug(f"Progress updated in history: {progress:.1f}% for {game_name}, task_id={task_id}") + break + await asyncio.sleep(0.2) + except Exception as e: + logger.error(f"Erreur mise à jour progression: {str(e)}") + + thread.join() + logger.debug(f"Thread joined for {url}, task_id={task_id}") return result[0], result[1] - -def is_1fichier_url(url): - """Détecte si l'URL est un lien 1fichier.""" - return "1fichier.com" in url - - -def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False): - """Télécharge un fichier depuis 1fichier en utilisant l'API officielle.""" - logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}") +async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None): + logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}") result = [None, None] - def download_thread(): - logger.debug(f"Thread téléchargement 1fichier démarré pour {url}") + # Vider la file d'attente avant de commencer + while not progress_queue.empty(): try: - # Nettoyer l'URL - link = url.split('&af=')[0] + progress_queue.get_nowait() + logger.debug(f"File progress_queue vidée pour {game_name}") + except queue.Empty: + break - # Déterminer le répertoire de destination + def download_thread(): + logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}") + try: + link = url.split('&af=')[0] dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: @@ -270,7 +271,6 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) if not os.access(dest_dir, os.W_OK): raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}") - # Préparer les en-têtes et le payload headers = { "Authorization": f"Bearer {config.API_KEY_1FICHIER}", "Content-Type": "application/json" @@ -280,7 +280,6 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) "pretty": 1 } - # Étape 1 : Obtenir les informations du fichier logger.debug(f"Envoi requête POST à https://api.1fichier.com/v1/file/info.cgi pour {url}") response = requests.post("https://api.1fichier.com/v1/file/info.cgi", headers=headers, json=payload, timeout=30) logger.debug(f"Réponse reçue, status: {response.status_code}") @@ -304,7 +303,6 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) dest_path = os.path.join(dest_dir, sanitized_filename) logger.debug(f"Chemin destination: {dest_path}") - # Étape 2 : Obtenir le jeton de téléchargement logger.debug(f"Envoi requête POST à https://api.1fichier.com/v1/download/get_token.cgi pour {url}") response = requests.post("https://api.1fichier.com/v1/download/get_token.cgi", headers=headers, json=payload, timeout=30) logger.debug(f"Réponse reçue, status: {response.status_code}") @@ -318,22 +316,12 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) result[1] = "Impossible de récupérer l'URL de téléchargement" return - # Étape 3 : Initialiser la progression lock = threading.Lock() - with lock: - config.download_progress[url] = { - "downloaded_size": 0, - "total_size": 0, - "status": "Téléchargement", - "progress_percent": 0, - "game_name": game_name - } - config.needs_redraw = True - logger.debug(f"Progression initialisée pour {url}") - - # Étape 4 : Télécharger le fichier retries = 10 retry_delay = 10 + # Initialiser la progression avec task_id + progress_queue.put((task_id, 0, 0)) # Taille initiale inconnue + logger.debug(f"Progression initiale envoyée: 0% pour {game_name}, task_id={task_id}") for attempt in range(retries): try: logger.debug(f"Tentative {attempt + 1} : Envoi requête GET à {final_url}") @@ -343,31 +331,44 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) total_size = int(response.headers.get('content-length', 0)) logger.debug(f"Taille totale: {total_size} octets") with lock: - config.download_progress[url]["total_size"] = total_size - config.needs_redraw = True + for entry in config.history: + if entry["url"] == url and entry["status"] == "downloading": + entry["total_size"] = total_size + config.needs_redraw = True + break + progress_queue.put((task_id, 0, total_size)) # Mettre à jour la taille totale downloaded = 0 + chunk_size = 8192 + last_update_time = time.time() + update_interval = 0.5 # Mettre à jour toutes les 0,5 secondes with open(dest_path, 'wb') as f: logger.debug(f"Ouverture fichier: {dest_path}") - for chunk in response.iter_content(chunk_size=8192): + for chunk in response.iter_content(chunk_size=chunk_size): if chunk: f.write(chunk) downloaded += len(chunk) - with lock: - config.download_progress[url]["downloaded_size"] = downloaded - config.download_progress[url]["status"] = "Téléchargement" - config.download_progress[url]["progress_percent"] = (downloaded / total_size * 100) if total_size > 0 else 0 - config.needs_redraw = True - #logger.debug(f"Progression: {downloaded}/{total_size} octets, {config.download_progress[url]['progress_percent']:.1f}%") + current_time = time.time() + if current_time - last_update_time >= update_interval: + with lock: + for entry in config.history: + if entry["url"] == url and entry["status"] == "downloading": + entry["progress"] = (downloaded / total_size * 100) if total_size > 0 else 0 + entry["status"] = "Téléchargement" + config.needs_redraw = True + logger.debug(f"Progression mise à jour: {entry['progress']:.1f}% pour {game_name}") + break + progress_queue.put((task_id, downloaded, total_size)) + last_update_time = current_time - # Étape 5 : Extraire si nécessaire if is_zip_non_supported: with lock: - config.download_progress[url]["downloaded_size"] = 0 - config.download_progress[url]["total_size"] = 0 - config.download_progress[url]["status"] = "Extracting" - config.download_progress[url]["progress_percent"] = 0 - config.needs_redraw = True + for entry in config.history: + if entry["url"] == url and entry["status"] == "Téléchargement": + entry["progress"] = 0 + entry["status"] = "Extracting" + config.needs_redraw = True + break extension = os.path.splitext(dest_path)[1].lower() if extension == ".zip": success, msg = extract_zip(dest_path, dest_dir, url) @@ -389,7 +390,6 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) except requests.exceptions.RequestException as e: logger.error(f"Tentative {attempt + 1} échouée : {e}") if attempt < retries - 1: - import time time.sleep(retry_delay) else: logger.error("Nombre maximum de tentatives atteint") @@ -400,25 +400,57 @@ def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False) except requests.exceptions.RequestException as e: logger.error(f"Erreur API 1fichier : {e}") result[0] = False - result[1] = f"Erreur lors de la requête API, la clé est peut etre incorrecte: {str(e)}" + result[1] = f"Erreur lors de la requête API, la clé est peut-être incorrecte: {str(e)}" finally: - logger.debug(f"Thread téléchargement 1fichier terminé pour {url}") - with lock: - config.download_result_message = result[1] - config.download_result_error = not result[0] - config.download_result_start_time = pygame.time.get_ticks() - config.menu_state = "download_result" - config.needs_redraw = True - # Enregistrement dans l'historique - add_to_history(platform, game_name, "Download_OK" if result[0] else "Erreur") - config.history = load_history() - logger.debug(f"Enregistrement dans l'historique: platform={platform}, game_name={game_name}, status={'Download_OK' if result[0] else 'Erreur'}") + logger.debug(f"Thread téléchargement 1fichier terminé pour {url}, task_id={task_id}") + progress_queue.put((task_id, result[0], result[1])) + logger.debug(f"Final result sent to queue: success={result[0]}, message={result[1]}, task_id={task_id}") thread = threading.Thread(target=download_thread) - logger.debug(f"Démarrage thread pour {url}") + logger.debug(f"Démarrage thread pour {url}, task_id={task_id}") thread.start() - thread.join() - logger.debug(f"Thread rejoint pour {url}") - return result[0], result[1] \ No newline at end of file + # Boucle principale pour mettre à jour la progression + while thread.is_alive(): + try: + while not progress_queue.empty(): + data = progress_queue.get() + logger.debug(f"Progress queue data received: {data}") + if len(data) != 3 or data[0] != task_id: # Ignorer les données d'une autre tâche + logger.debug(f"Ignoring queue data for task_id={data[0]}, expected={task_id}") + continue + if isinstance(data[1], bool): # Fin du téléchargement + success, message = data[1], data[2] + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["status"] = "Download_OK" if success else "Erreur" + entry["progress"] = 100 if success else 0 + entry["message"] = message + save_history(config.history) + config.needs_redraw = True + logger.debug(f"Final update in history: status={entry['status']}, progress={entry['progress']}%, message={message}, task_id={task_id}") + break + else: + downloaded, total_size = data[1], data[2] + progress = (downloaded / total_size * 100) if total_size > 0 else 0 + for entry in config.history: + if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]: + entry["progress"] = progress + entry["status"] = "Téléchargement" + config.needs_redraw = True + # logger.debug(f"Progress updated in history: {progress:.1f}% for {game_name}, task_id={task_id}") + break + await asyncio.sleep(0.2) + except Exception as e: + logger.error(f"Erreur mise à jour progression: {str(e)}") + + thread.join() + logger.debug(f"Thread joined for {url}, task_id={task_id}") + return result[0], result[1] + + +def is_1fichier_url(url): + """Détecte si l'URL est un lien 1fichier.""" + return "1fichier.com" in url + diff --git a/utils.py b/utils.py index 54c9d9d..325fae9 100644 --- a/utils.py +++ b/utils.py @@ -9,8 +9,10 @@ import subprocess import config import threading import zipfile +import time import random from config import JSON_EXTENSIONS +from history import save_history from datetime import datetime @@ -177,11 +179,12 @@ def write_unavailable_systems(): 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.""" - # Supprimer l'extension de fichier - text = text.rsplit('.', 1)[0] if '.' in text else text +def truncate_text_middle(text, font, max_width, is_filename=True): + """Tronque le texte en insérant '...' au milieu, en préservant le début et la fin. + Si is_filename=False, ne supprime pas l'extension.""" + # Supprimer l'extension uniquement si is_filename est True + if is_filename: + text = text.rsplit('.', 1)[0] if '.' in text else text text_width = font.size(text)[0] if text_width <= max_width: return text @@ -191,7 +194,7 @@ def truncate_text_middle(text, font, max_width): if max_text_width <= 0: return ellipsis - # Diviser la largeur disponible entre début et fin + # Diviser la largeur disponible entre début et fin, en priorisant la fin chars = list(text) left = [] right = [] @@ -200,14 +203,9 @@ def truncate_text_middle(text, font, max_width): left_idx = 0 right_idx = len(chars) - 1 + # Préserver plus de caractères à droite pour garder le '%' while left_idx <= right_idx and (left_width + right_width) < max_text_width: - if left_idx < right_idx: - left.append(chars[left_idx]) - left_width = font.size(''.join(left))[0] - if left_width + right_width > max_text_width: - left.pop() - break - left_idx += 1 + # Ajouter à droite en priorité if left_idx <= right_idx: right.insert(0, chars[right_idx]) right_width = font.size(''.join(right))[0] @@ -215,6 +213,14 @@ def truncate_text_middle(text, font, max_width): right.pop(0) break right_idx -= 1 + # Ajouter à gauche seulement si nécessaire + if left_idx < right_idx: + left.append(chars[left_idx]) + left_width = font.size(''.join(left))[0] + if left_width + right_width > max_text_width: + left.pop() + break + left_idx += 1 # Reculer jusqu'à un espace pour éviter de couper un mot while left and left[-1] != ' ' and left_width + right_width > max_text_width: @@ -305,12 +311,13 @@ def load_system_image(platform_dict): return None -# 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.""" + logger.debug(f"Extraction de {zip_path} dans {dest_dir}") try: lock = threading.Lock() with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.testzip() # Vérifier l'intégrité de l'archive 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: @@ -319,7 +326,9 @@ def extract_zip(zip_path, dest_dir, url): extracted_size = 0 os.makedirs(dest_dir, exist_ok=True) - chunk_size = 8192 + chunk_size = 2048 # Réduire pour plus de mises à jour + last_save_time = time.time() + save_interval = 0.5 # Sauvegarder toutes les 0.5 secondes for info in zip_ref.infolist(): if info.is_dir(): continue @@ -335,15 +344,19 @@ def extract_zip(zip_path, dest_dir, url): dest.write(chunk) file_extracted += len(chunk) extracted_size += len(chunk) + current_time = time.time() 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}%") + for entry in config.history: + if entry["url"] == url and entry["status"] in ["Téléchargement", "Extracting"]: + entry["status"] = "Extracting" + entry["progress"] = (extracted_size / total_size * 100) if total_size > 0 else 0 + entry["message"] = "Extraction en cours" + if current_time - last_save_time >= save_interval: + save_history(config.history) + last_save_time = current_time + logger.debug(f"Extraction en cours: {info.filename}, file_extracted={file_extracted}/{file_size}, total_extracted={extracted_size}/{total_size}, progression={entry['progress']:.1f}%") + config.needs_redraw = True + break os.chmod(file_path, 0o644) for root, dirs, _ in os.walk(dest_dir): @@ -352,10 +365,17 @@ def extract_zip(zip_path, dest_dir, url): os.remove(zip_path) logger.info(f"Fichier ZIP {zip_path} extrait dans {dest_dir} et supprimé") - return True, "ZIP extrait avec succès" + return True, f"Extracted: {os.path.basename(zip_path)}" + except zipfile.BadZipFile as e: + logger.error(f"Erreur: Archive ZIP corrompue: {str(e)}") + return False, f"Archive ZIP corrompue: {str(e)}" + except PermissionError as e: + logger.error(f"Erreur: Permission refusée lors de l'extraction: {str(e)}") + return False, f"Permission refusée lors de l'extraction: {str(e)}" except Exception as e: - logger.error(f"Erreur lors de l'extraction de {zip_path}: {e}") - return False, str(e) + logger.error(f"Erreur lors de l'extraction de {zip_path}: {str(e)}") + return False, f"Échec de l'extraction: {str(e)}" + # Fonction pour extraire le contenu d'un fichier RAR def extract_rar(rar_path, dest_dir, url):