From ea61d75f3d9986e7785eb8926e839fb779119d2f Mon Sep 17 00:00:00 2001 From: skymike03 Date: Thu, 7 May 2026 22:55:11 +0200 Subject: [PATCH] ## v2.6.4.2 (2026.05.07) - Add reset all settings functionality in the pause menu - add seeding display on history --- ports/RGSX/__main__.py | 6 +- ports/RGSX/config.py | 2 +- ports/RGSX/controls.py | 58 +++++- ports/RGSX/display.py | 92 +++++++++- ports/RGSX/history.py | 5 +- ports/RGSX/languages/de.json | 8 + ports/RGSX/languages/en.json | 8 + ports/RGSX/languages/es.json | 8 + ports/RGSX/languages/fr.json | 8 + ports/RGSX/languages/it.json | 8 + ports/RGSX/languages/pt.json | 8 + ports/RGSX/network.py | 337 ++++++++++++++++++++++++++++++++++- version.json | 2 +- 13 files changed, 535 insertions(+), 15 deletions(-) diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index e91a66f..dd3c4c4 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -827,7 +827,7 @@ async def main(): (event.type == pygame.JOYHATMOTION and start_config.get("type") == "hat" and event.value == tuple(start_config.get("value") if isinstance(start_config.get("value"), list) else start_config.get("value"))) or (event.type == pygame.MOUSEBUTTONDOWN and start_config.get("type") == "mouse" and event.button == start_config.get("button")) ): - if config.menu_state not in ["pause_menu", "controls_help", "controls_mapping", "history", "confirm_clear_history"]: + if config.menu_state not in ["pause_menu", "controls_help", "controls_mapping", "history", "confirm_clear_history", "reset_settings_confirm"]: config.previous_menu_state = config.menu_state # Capturer l'état d'origine pour une sortie fiable du menu pause config.pause_origin_state = config.menu_state @@ -858,6 +858,7 @@ async def main(): "controls_help", "confirm_cancel_download", "reload_games_data", + "reset_settings_confirm", # Menus historique "history_game_options", "history_show_folder", @@ -1355,6 +1356,9 @@ async def main(): draw_cancel_download_dialog(screen) elif config.menu_state == "reload_games_data": draw_reload_games_data_dialog(screen) + elif config.menu_state == "reset_settings_confirm": + from display import draw_reset_settings_confirm_dialog + draw_reset_settings_confirm_dialog(screen) elif config.menu_state == "gamelist_update_prompt": from display import draw_gamelist_update_prompt draw_gamelist_update_prompt(screen) diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 5e02601..c1853cb 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -27,7 +27,7 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.6.4.1" +app_version = "2.6.4.2" # Nombre de jours avant de proposer la mise à jour de la liste des jeux GAMELIST_UPDATE_DAYS = 1 diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 0a20d65..8a44e66 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -254,6 +254,7 @@ VALID_STATES = [ "platform", "game", "confirm_exit", "extension_warning", "pause_menu", "controls_help", "history", "controls_mapping", "reload_games_data", "restart_popup", "error", "loading", "confirm_clear_history", + "reset_settings_confirm", "language_select", "filter_platforms", "display_menu", "confirm_cancel_download", "gamelist_update_prompt", "platform_folder_config", # Nouveaux sous-menus hiérarchiques (refonte pause menu) @@ -2555,12 +2556,12 @@ def handle_controls(event, sources, joystick, screen): logger.debug(f"Start: retour à {config.menu_state} depuis pause_menu") elif is_input_matched(event, "up"): # Menu racine hiérarchique: nombre dynamique (langue + catégories) - total = getattr(config, 'pause_menu_total_options', 7) + total = getattr(config, 'pause_menu_total_options', 8) config.selected_option = (config.selected_option - 1) % total config.needs_redraw = True elif is_input_matched(event, "down"): # Menu racine hiérarchique: nombre dynamique (langue + catégories) - total = getattr(config, 'pause_menu_total_options', 7) + total = getattr(config, 'pause_menu_total_options', 8) config.selected_option = (config.selected_option + 1) % total config.needs_redraw = True elif is_input_matched(event, "confirm"): @@ -2604,7 +2605,13 @@ def handle_controls(event, sources, joystick, screen): config.menu_state = "support_dialog" config.last_state_change_time = pygame.time.get_ticks() config.needs_redraw = True - elif config.selected_option == 6: # Quit submenu + elif config.selected_option == 6: # Reset default settings (delete file + restart) + config.previous_menu_state = "pause_menu" + config.menu_state = "reset_settings_confirm" + config.reset_settings_confirm_selection = 0 # 0=No, 1=Yes + config.last_state_change_time = pygame.time.get_ticks() + config.needs_redraw = True + elif config.selected_option == 7: # Quit submenu # Capturer l'origine pause_menu pour retour si annulation config.confirm_exit_origin = "pause_menu" config.previous_menu_state = validate_menu_state(config.previous_menu_state) @@ -3654,6 +3661,51 @@ def handle_controls(event, sources, joystick, screen): config.menu_state = validate_menu_state(config.previous_menu_state) config.needs_redraw = True logger.debug(f"Retour à {config.menu_state} depuis reload_games_data") + + # Confirmation reset settings (warning + yes/no) + elif config.menu_state == "reset_settings_confirm": + if is_input_matched(event, "left") or is_input_matched(event, "right"): + config.reset_settings_confirm_selection = 1 - int(getattr(config, 'reset_settings_confirm_selection', 0)) + config.needs_redraw = True + elif is_input_matched(event, "confirm"): + if int(getattr(config, 'reset_settings_confirm_selection', 0)) == 1: # Yes + try: + settings_path = getattr(config, 'RGSX_SETTINGS_PATH', '') + if settings_path and os.path.exists(settings_path): + os.remove(settings_path) + logger.info(f"Paramètres supprimés: {settings_path}") + else: + logger.info(f"Aucun fichier paramètres à supprimer: {settings_path}") + + restart_msg = _("popup_settings_reset_restarting") if _ else "Default settings reset. Restarting..." + if not restart_msg or restart_msg == "popup_settings_reset_restarting": + restart_msg = "Default settings reset. Restarting..." + + config.menu_state = "restart_popup" + config.popup_message = restart_msg + config.popup_timer = 2000 + config.last_state_change_time = pygame.time.get_ticks() + config.needs_redraw = True + restart_application(2000) + except Exception as e: + logger.error(f"Erreur reset paramètres par défaut: {e}") + err_tpl = _("popup_settings_reset_error") if _ else "Unable to reset settings: {0}" + if not err_tpl or err_tpl == "popup_settings_reset_error": + config.popup_message = f"Unable to reset settings: {e}" + else: + try: + config.popup_message = err_tpl.format(str(e)) + except Exception: + config.popup_message = f"Unable to reset settings: {e}" + config.popup_timer = 5000 + config.menu_state = "pause_menu" + config.needs_redraw = True + else: # No + config.menu_state = "pause_menu" + config.needs_redraw = True + elif is_input_matched(event, "cancel") or is_input_matched(event, "start"): + config.menu_state = "pause_menu" + config.needs_redraw = True # Popup de redémarrage diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index c09db30..6dbd1fe 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -2525,6 +2525,11 @@ def draw_history_list(screen): _phase_label = _phase_labels.get(_aria2_phase, "") if _phase_label: title_text = f"{title_text} [{_phase_label}]" + elif selected_entry and selected_status == "Seeding": + _cn = int(selected_entry.get("seeds", 0) or 0) + _ul = float(selected_entry.get("ul_speed", 0.0) or 0.0) + _ul_text = format_speed_adaptive(_ul) + title_text = f"Seeding - {_ul_text} - [{_cn}p]" elif selected_entry and selected_status in completed_statuses: completed_count = sum(1 for item in history if str(item.get("status") or "") in completed_statuses) title_text = _("history_title_completed_count").format(completed_count) @@ -2699,6 +2704,10 @@ def draw_history_list(screen): # Completed: no provider prefix (per requirement) status_text = _("history_status_completed") status_text = str(status_text or "") + elif status == "Seeding": + _cn = int(entry.get("seeds", 0) or 0) + status_text = _("history_status_seeding").format(_cn) + status_text = str(status_text or "") elif status == "Erreur": # Prefer friendly mapped message now stored in 'message' status_text = entry.get('message') @@ -2728,6 +2737,9 @@ def draw_history_list(screen): elif status == "Download_OK" or status == "Completed": # Use green OK color status_color = THEME_COLORS.get("success_text", (0, 255, 0)) + elif status == "Seeding": + # Seeding : couleur verte légèrement différente + status_color = THEME_COLORS.get("success_text", (0, 220, 120)) elif status in ("Downloading", "Téléchargement", "downloading", "Extracting", "Converting", "Queued", "Connecting"): # En cours - couleur bleue/cyan pour différencier des autres status_color = THEME_COLORS.get("text_selected", (100, 180, 255)) @@ -3529,7 +3541,11 @@ def draw_display_menu(screen): def draw_pause_menu(screen, selected_option): """Dessine le menu pause racine (catégories).""" screen.blit(OVERLAY, (0, 0)) - # Nouvel ordre: Games / Language / Controls / Display / Settings / Support / Quit + # Nouvel ordre: Games / Language / Controls / Display / Settings / Support / Reset / Quit + reset_label = _("menu_reset_default_settings") if _ else "Reset default settings" + if not reset_label or reset_label == "menu_reset_default_settings": + reset_label = "Reset default settings" + options = [ _("menu_games") if _ else "Games", # 0 -> sous-menu games (history + sources + update) _("menu_language") if _ else "Language", # 1 -> sélecteur de langue direct @@ -3537,7 +3553,8 @@ def draw_pause_menu(screen, selected_option): _("menu_display"), # 3 -> sous-menu display _("menu_settings_category") if _ else "Settings", # 4 -> sous-menu settings _("menu_support"), # 5 -> support - _("menu_quit") # 6 -> sous-menu quit (quit + restart) + reset_label, # 6 -> reset settings (delete + restart) + _("menu_quit") # 7 -> sous-menu quit (quit + restart) ] # Instruction contextuelle pour l'option sélectionnée @@ -3548,11 +3565,14 @@ def draw_pause_menu(screen, selected_option): "instruction_pause_display", "instruction_pause_settings", "instruction_pause_support", + "instruction_pause_reset_settings", "instruction_pause_quit", ] try: key = instruction_keys[selected_option] instruction_text = _(key) + if instruction_text == key: + instruction_text = "" except Exception: instruction_text = "" @@ -4807,6 +4827,72 @@ def draw_reload_games_data_dialog(screen): draw_stylized_button(screen, _("button_no"), no_x, buttons_y, button_width, button_height, selected=config.redownload_confirm_selection == 0) +def draw_reset_settings_confirm_dialog(screen): + """Affiche un avertissement avant reset des paramètres (oui/non).""" + global OVERLAY + if OVERLAY is None or OVERLAY.get_size() != (config.screen_width, config.screen_height): + OVERLAY = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA) + OVERLAY.fill((0, 0, 0, 150)) + + screen.blit(OVERLAY, (0, 0)) + + title = _("menu_reset_default_settings") if _ else "Reset default settings" + if not title or title == "menu_reset_default_settings": + title = "Reset default settings" + + message = _("confirm_reset_settings_warning") if _ else ( + "Warning: no file, history or game will be deleted.\n" + "Only settings will be reset (platform filtering, sort order, custom ROM paths).\n" + "Continue?" + ) + if not message or message == "confirm_reset_settings_warning": + message = ( + "Warning: no file, history or game will be deleted.\n" + "Only settings will be reset (platform filtering, sort order, custom ROM paths).\n" + "Continue?" + ) + + wrapped_message = [] + for paragraph in str(message).split("\n"): + lines = wrap_text(paragraph, config.small_font, config.screen_width - 120) if paragraph else [""] + wrapped_message.extend(lines) + + line_height = config.small_font.get_height() + 5 + title_height = config.font.get_height() + 10 + text_height = len(wrapped_message) * line_height + sample_text = config.small_font.render("Sample", True, THEME_COLORS["text"]) + font_height = sample_text.get_height() + button_height = max(int(config.screen_height * 0.0463), font_height + 15) + margin_top_bottom = 20 + rect_height = title_height + text_height + button_height + 2 * margin_top_bottom + 8 + max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message], default=420) + title_width = config.font.size(title)[0] + rect_width = max(max_text_width + 80, title_width + 80) + 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) + + title_surface = config.font.render(title, True, THEME_COLORS["text"]) + title_rect = title_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + title_height // 2)) + screen.blit(title_surface, title_rect) + + text_top = rect_y + margin_top_bottom + title_height + for i, line in enumerate(wrapped_message): + text = config.small_font.render(line, True, THEME_COLORS["text"]) + text_rect = text.get_rect(center=(config.screen_width // 2, text_top + i * line_height + line_height // 2)) + screen.blit(text, text_rect) + + button_width = min(170, (rect_width - 60) // 2) + yes_x = rect_x + rect_width // 2 - button_width - 10 + no_x = rect_x + rect_width // 2 + 10 + buttons_y = rect_y + margin_top_bottom + title_height + text_height + 8 + sel = int(getattr(config, 'reset_settings_confirm_selection', 0)) + draw_stylized_button(screen, _("button_yes"), yes_x, buttons_y, button_width, button_height, selected=sel == 1) + draw_stylized_button(screen, _("button_no"), no_x, buttons_y, button_width, button_height, selected=sel == 0) + + def draw_gamelist_update_prompt(screen): """Affiche la boîte de dialogue pour proposer la mise à jour de la liste des jeux.""" global OVERLAY @@ -5373,7 +5459,7 @@ def draw_history_game_options(screen): option_labels.append(_("history_option_pause_download")) options.append("cancel_download") option_labels.append(_("history_option_cancel_download")) - elif status == "Download_OK" or status == "Completed": + elif status == "Download_OK" or status == "Completed" or status == "Seeding": # Vérifier si c'est une archive ET si le fichier existe if actual_filename and file_exists: ext = os.path.splitext(actual_filename)[1].lower() diff --git a/ports/RGSX/history.py b/ports/RGSX/history.py index 98d8988..f1fbc7c 100644 --- a/ports/RGSX/history.py +++ b/ports/RGSX/history.py @@ -275,7 +275,7 @@ def clear_history(): # Charger l'historique actuel current_history = load_history() - active_statuses = {"Downloading", "Téléchargement", "downloading", "Extracting", "Converting", "Queued"} + active_statuses = {"Downloading", "Téléchargement", "downloading", "Extracting", "Converting", "Queued", "Seeding"} active_task_ids = set(getattr(config, 'download_tasks', {}).keys()) active_progress_urls = set(getattr(config, 'download_progress', {}).keys()) @@ -299,6 +299,9 @@ def clear_history(): task_id = entry.get("task_id") url = entry.get("url") + if status == "Seeding": + return True + if status == "Queued": return task_id in queued_task_ids or url in queued_urls diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index d164fa7..25dbf00 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -41,11 +41,13 @@ "history_title": "Downloads ({0})", "history_title_downloading_active": "Download - {0} - {1}", "history_title_completed_count": "Abgeschlossene Downloads ({0})", + "history_title_seeding": "Fertig··Seeden an {0} Peers", "history_title_error_count": "Fehlgeschlagene Downloads ({0})", "history_title_canceled_count": "Abgebrochene Downloads ({0})", "aria2_phase_connecting": "Verbinden...", "aria2_phase_verifying": "Überprüfen...", "aria2_phase_waiting": "Warten...", + "aria2_phase_seeding": "Seeden...", "history_empty": "Keine Downloads im Verlauf", "history_column_system": "System", "history_column_game": "Spielname", @@ -54,6 +56,7 @@ "history_status_downloading": "Download: {0}%", "history_status_extracting": "Extrahieren: {0}%", "history_status_completed": "Abgeschlossen", + "history_status_seeding": "Seeden ({0}p)", "history_status_error": "Fehler: {0}", "history_status_canceled": "Abgebrochen", "free_mode_waiting": "[Kostenloser Modus] Warten: {0}/{1}s", @@ -112,6 +115,7 @@ "menu_music_disabled": "Musik deaktiviert", "menu_restart": "RGSX neu starten", "menu_support": "Unterstützung", + "menu_reset_default_settings": "Standardeinstellungen zurücksetzen", "menu_filter_platforms": "Systeme filtern", "filter_platforms_title": "Systemsichtbarkeit", "filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}", @@ -239,7 +243,11 @@ "instruction_pause_settings": "Musik, Symlink-Option & API-Schlüsselstatus", "instruction_pause_restart": "RGSX neu starten um Konfiguration neu zu laden", "instruction_pause_support": "Eine Diagnose-ZIP-Datei für den Support erstellen", + "instruction_pause_reset_settings": "Nur Einstellungen zurücksetzen (Dateien/Verlauf/Spiele bleiben erhalten)", "instruction_pause_quit": "Menü für Beenden oder Neustart aufrufen", + "confirm_reset_settings_warning": "Warnung: Es werden keine Dateien, kein Verlauf und keine Spiele gelöscht.\nNur Einstellungen werden zurückgesetzt (Systemfilter, Sortierreihenfolge, benutzerdefinierte ROM-Pfade).\nFortfahren?", + "popup_settings_reset_restarting": "Einstellungen zurückgesetzt. Neustart...", + "popup_settings_reset_error": "Einstellungen konnten nicht zurückgesetzt werden: {0}", "instruction_quit_app": "RGSX Anwendung beenden", "instruction_quit_restart": "RGSX Anwendung neu starten", "instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen", diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index 9bfa867..0230708 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -41,11 +41,13 @@ "history_title": "Downloads ({0})", "history_title_downloading_active": "Downloading - {0} - {1}", "history_title_completed_count": "Completed downloads ({0})", + "history_title_seeding": "Complete..Seeding to {0} peers", "history_title_error_count": "Failed downloads ({0})", "history_title_canceled_count": "Canceled downloads ({0})", "aria2_phase_connecting": "Connecting...", "aria2_phase_verifying": "Verifying...", "aria2_phase_waiting": "Waiting...", + "aria2_phase_seeding": "Seeding...", "history_empty": "No downloads in history", "history_column_system": "System", "history_column_game": "Game name", @@ -54,6 +56,7 @@ "history_status_downloading": "Downloading: {0}%", "history_status_extracting": "Extracting: {0}%", "history_status_completed": "Completed", + "history_status_seeding": "Seeding ({0}p)", "history_status_error": "Error: {0}", "history_status_canceled": "Canceled", "free_mode_waiting": "[Free mode] Waiting: {0}/{1}s", @@ -120,6 +123,7 @@ "menu_allow_unknown_ext_enabled": "Hide unknown extension warning enabled", "menu_allow_unknown_ext_disabled": "Hide unknown extension warning disabled", "menu_support": "Support", + "menu_reset_default_settings": "Reset default settings", "menu_quit": "Quit", "menu_quit_app": "Quit RGSX", "button_yes": "Yes", @@ -238,7 +242,11 @@ "instruction_pause_settings": "Music, symlink option & API keys status", "instruction_pause_restart": "Restart RGSX to reload configuration", "instruction_pause_support": "Generate a diagnostic ZIP file for support", + "instruction_pause_reset_settings": "Reset settings only (your files/history/games are kept)", "instruction_pause_quit": "Access menu to quit or restart", + "confirm_reset_settings_warning": "Warning: no file, history or game will be deleted.\nOnly settings will be reset (platform filtering, sort order, custom ROM paths).\nContinue?", + "popup_settings_reset_restarting": "Settings reset. Restarting...", + "popup_settings_reset_error": "Unable to reset settings: {0}", "instruction_quit_app": "Exit the RGSX application", "instruction_quit_restart": "Restart the RGSX application", "instruction_controls_help": "Show full controller & keyboard reference", diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index da93ea1..7ef8314 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -41,11 +41,13 @@ "history_title": "Descargas ({0})", "history_title_downloading_active": "Descargando - {0} - {1}", "history_title_completed_count": "Descargas completadas ({0})", + "history_title_seeding": "Completo··Sembrando a {0} peers", "history_title_error_count": "Descargas con error ({0})", "history_title_canceled_count": "Descargas canceladas ({0})", "aria2_phase_connecting": "Conectando...", "aria2_phase_verifying": "Verificando...", "aria2_phase_waiting": "Esperando...", + "aria2_phase_seeding": "Sembrando...", "history_empty": "No hay descargas en el historial", "history_column_system": "Sistema", "history_column_game": "Nombre del juego", @@ -54,6 +56,7 @@ "history_status_downloading": "Descargando: {0}%", "history_status_extracting": "Extrayendo: {0}%", "history_status_completed": "Completado", + "history_status_seeding": "Sembrando ({0}p)", "history_status_error": "Error: {0}", "history_status_canceled": "Cancelado", "free_mode_waiting": "[Modo gratuito] Esperando: {0}/{1}s", @@ -110,6 +113,7 @@ "menu_music_disabled": "Música desactivada", "menu_restart": "Reiniciar RGSX", "menu_support": "Soporte", + "menu_reset_default_settings": "Restablecer parámetros", "menu_filter_platforms": "Filtrar sistemas", "filter_platforms_title": "Visibilidad de sistemas", "filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}", @@ -239,7 +243,11 @@ "instruction_pause_settings": "Música, opción symlink y estado de claves API", "instruction_pause_restart": "Reiniciar RGSX para recargar configuración", "instruction_pause_support": "Generar un archivo ZIP de diagnóstico para soporte", + "instruction_pause_reset_settings": "Restablecer solo parámetros (se conservan archivos/historial/juegos)", "instruction_pause_quit": "Acceder al menú para salir o reiniciar", + "confirm_reset_settings_warning": "Aviso: no se eliminará ningún archivo, historial ni juego.\nSolo se restablecerán parámetros (filtrado de plataformas, orden de clasificación, rutas ROM personalizadas).\n¿Continuar?", + "popup_settings_reset_restarting": "Parámetros restablecidos. Reiniciando...", + "popup_settings_reset_error": "No se pudieron restablecer los parámetros: {0}", "instruction_quit_app": "Salir de la aplicación RGSX", "instruction_quit_restart": "Reiniciar la aplicación RGSX", "instruction_controls_help": "Mostrar referencia completa de mando y teclado", diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index aa407ff..917c474 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -41,11 +41,13 @@ "history_title": "Téléchargements ({0})", "history_title_downloading_active": "Téléchargement - {0} - {1}", "history_title_completed_count": "Téléchargements terminés ({0})", + "history_title_seeding": "Terminé · Seed en cours → {0} peers", "history_title_error_count": "Téléchargements en erreur ({0})", "history_title_canceled_count": "Téléchargements annulés ({0})", "aria2_phase_connecting": "Connexion...", "aria2_phase_verifying": "Vérification...", "aria2_phase_waiting": "En attente...", + "aria2_phase_seeding": "Seed en cours...", "history_empty": "Aucun téléchargement dans l'historique", "history_column_system": "Système", "history_column_game": "Nom du jeu", @@ -54,6 +56,7 @@ "history_status_downloading": "Téléchargement : {0}%", "history_status_extracting": "Extraction : {0}%", "history_status_completed": "Terminé", + "history_status_seeding": "Seed ({0}p)", "history_status_error": "Erreur : {0}", "history_status_canceled": "Annulé", "free_mode_waiting": "[Mode gratuit] Attente: {0}/{1}s", @@ -103,6 +106,7 @@ "display_light_mode_disabled": "Mode performance désactivé - effets activés", "menu_redownload_cache": "Mettre à jour la liste des jeux", "menu_support": "Support", + "menu_reset_default_settings": "Réinitialiser paramètres", "menu_quit": "Quitter", "menu_quit_app": "Quitter RGSX", "menu_music_enabled": "Musique activée : {0}", @@ -235,7 +239,11 @@ "instruction_pause_settings": "Musique, option symlink & statut des clés API", "instruction_pause_restart": "Redémarrer RGSX pour recharger la configuration", "instruction_pause_support": "Générer un fichier ZIP de diagnostic pour l'assistance", + "instruction_pause_reset_settings": "Réinitialiser les paramètres (sans supprimer vos jeux/fichiers)", "instruction_pause_quit": "Accéder au menu pour quitter ou redémarrer", + "confirm_reset_settings_warning": "Attention: aucun fichier, historique ou jeu ne sera supprimé.\nSeuls les paramètres seront réinitialisés (filtrage plateformes, ordre de tri, chemins ROMs personnalisés).\nContinuer ?", + "popup_settings_reset_restarting": "Paramètres réinitialisés. Redémarrage...", + "popup_settings_reset_error": "Impossible de réinitialiser les paramètres: {0}", "instruction_quit_app": "Quitter l'application RGSX", "instruction_quit_restart": "Redémarrer l'application RGSX", "instruction_controls_help": "Afficher la référence complète manette & clavier", diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json index 44bb058..83a1949 100644 --- a/ports/RGSX/languages/it.json +++ b/ports/RGSX/languages/it.json @@ -41,11 +41,13 @@ "history_title": "Download ({0})", "history_title_downloading_active": "Download in corso - {0} - {1}", "history_title_completed_count": "Download completati ({0})", + "history_title_seeding": "Completato··Seeding a {0} peers", "history_title_error_count": "Download con errore ({0})", "history_title_canceled_count": "Download annullati ({0})", "aria2_phase_connecting": "Connessione...", "aria2_phase_verifying": "Verifica...", "aria2_phase_waiting": "In attesa...", + "aria2_phase_seeding": "Seeding...", "history_empty": "Nessun download nella cronologia", "history_column_system": "Sistema", "history_column_game": "Nome del gioco", @@ -54,6 +56,7 @@ "history_status_downloading": "Download: {0}%", "history_status_extracting": "Estrazione: {0}%", "history_status_completed": "Completato", + "history_status_seeding": "Seeding ({0}p)", "history_status_error": "Errore: {0}", "history_status_canceled": "Annullato", "free_mode_waiting": "[Modalità gratuita] Attesa: {0}/{1}s", @@ -108,6 +111,7 @@ "menu_music_disabled": "Musica disattivata", "menu_restart": "Riavvia RGSX", "menu_support": "Supporto", + "menu_reset_default_settings": "Ripristina impostazioni", "menu_filter_platforms": "Filtra sistemi", "filter_platforms_title": "Visibilità sistemi", "filter_platforms_info": "Visibili: {0} | Nascosti: {1} / Totale: {2}", @@ -234,7 +238,11 @@ "instruction_pause_settings": "Musica, opzione symlink e stato chiavi API", "instruction_pause_restart": "Riavvia RGSX per ricaricare la configurazione", "instruction_pause_support": "Genera un file ZIP diagnostico per il supporto", + "instruction_pause_reset_settings": "Ripristina solo impostazioni (file/cronologia/giochi conservati)", "instruction_pause_quit": "Accedere al menu per uscire o riavviare", + "confirm_reset_settings_warning": "Avviso: nessun file, cronologia o gioco verrà eliminato.\nVerranno ripristinate solo le impostazioni (filtro piattaforme, ordine di ordinamento, percorsi ROM personalizzati).\nContinuare?", + "popup_settings_reset_restarting": "Impostazioni ripristinate. Riavvio...", + "popup_settings_reset_error": "Impossibile ripristinare le impostazioni: {0}", "instruction_quit_app": "Uscire dall'applicazione RGSX", "instruction_quit_restart": "Riavviare l'applicazione RGSX", "instruction_controls_help": "Mostrare riferimento completo controller & tastiera", diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index a83690b..5fd59ee 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -41,11 +41,13 @@ "history_title": "Downloads ({0})", "history_title_downloading_active": "Baixando - {0} - {1}", "history_title_completed_count": "Downloads concluídos ({0})", + "history_title_seeding": "Completo··Semeando para {0} peers", "history_title_error_count": "Downloads com erro ({0})", "history_title_canceled_count": "Downloads cancelados ({0})", "aria2_phase_connecting": "Conectando...", "aria2_phase_verifying": "Verificando...", "aria2_phase_waiting": "Aguardando...", + "aria2_phase_seeding": "Semeando...", "history_empty": "Nenhum download no histórico", "history_column_system": "Sistema", "history_column_game": "Nome do jogo", @@ -54,6 +56,7 @@ "history_status_downloading": "Baixando: {0}%", "history_status_extracting": "Extraindo: {0}%", "history_status_completed": "Concluído", + "history_status_seeding": "Semeando ({0}p)", "history_status_error": "Erro: {0}", "history_status_canceled": "Cancelado", "free_mode_waiting": "[Modo gratuito] Aguardando: {0}/{1}s", @@ -112,6 +115,7 @@ "menu_music_disabled": "Música desativada", "menu_restart": "Reiniciar RGSX", "menu_support": "Suporte", + "menu_reset_default_settings": "Redefinir parâmetros", "menu_filter_platforms": "Filtrar sistemas", "filter_platforms_title": "Visibilidade dos sistemas", "filter_platforms_info": "Visíveis: {0} | Ocultos: {1} / Total: {2}", @@ -240,7 +244,11 @@ "instruction_pause_settings": "Música, opção symlink e status das chaves API", "instruction_pause_restart": "Reiniciar RGSX para recarregar configuração", "instruction_pause_support": "Gerar um arquivo ZIP de diagnóstico para suporte", + "instruction_pause_reset_settings": "Redefinir apenas parâmetros (arquivos/histórico/jogos serão mantidos)", "instruction_pause_quit": "Acessar menu para sair ou reiniciar", + "confirm_reset_settings_warning": "Aviso: nenhum arquivo, histórico ou jogo será excluído.\nApenas parâmetros serão redefinidos (filtro de plataformas, ordem de classificação, caminhos ROM personalizados).\nContinuar?", + "popup_settings_reset_restarting": "Parâmetros redefinidos. Reiniciando...", + "popup_settings_reset_error": "Não foi possível redefinir os parâmetros: {0}", "instruction_quit_app": "Sair da aplicação RGSX", "instruction_quit_restart": "Reiniciar a aplicação RGSX", "instruction_controls_help": "Mostrar referência completa de controle e teclado", diff --git a/ports/RGSX/network.py b/ports/RGSX/network.py index 871e589..7503979 100644 --- a/ports/RGSX/network.py +++ b/ports/RGSX/network.py @@ -254,7 +254,7 @@ def _strip_ansi_escape_codes(text: str) -> str: def _parse_aria2_progress_line(line: str, total_size: int) -> dict[str, float | int] | None: - if not line or "[#" not in line: + if not line or ("[#" not in line and "[SEEDING#" not in line): return None line = _strip_ansi_escape_codes(line) @@ -279,6 +279,12 @@ def _parse_aria2_progress_line(line: str, total_size: int) -> dict[str, float | if speed_bytes is not None: result["speed_mib_s"] = speed_bytes / (1024 * 1024) + ul_match = re.search(r"UL:([0-9]+(?:\.[0-9]+)?(?:KiB|MiB|GiB|TiB|B))", line) + if ul_match: + ul_bytes = _parse_aria2_size_to_bytes(ul_match.group(1)) + if ul_bytes is not None: + result["ul_speed_mib_s"] = ul_bytes / (1024 * 1024) + cn_match = re.search(r'\bCN:(\d+)', line) if cn_match: result["connections"] = int(cn_match.group(1)) @@ -494,6 +500,7 @@ def _download_torrent_with_aria2( task_id: str, cancel_ev, progress_queue, + original_history_url: str = "", ) -> tuple[bool, str]: source_url = str(torrent_meta.get("source_url") or "") relative_path = str(torrent_meta.get("relative_path") or "").strip() or os.path.basename(dest_path) @@ -830,10 +837,6 @@ def _download_torrent_with_aria2( shutil.rmtree(temp_root, ignore_errors=True) except Exception: pass - try: - os.remove(temp_manifest) - except Exception: - pass _upnp_close_port(upnp_handle, bt_listen_port) final_size = os.path.getsize(dest_path) if os.path.exists(dest_path) else total_size @@ -841,6 +844,22 @@ def _download_torrent_with_aria2( progress_queue.put((task_id, final_size, max(total_size, final_size), 0.0)) torrent_temp_roots.pop(task_id, None) _aria2c_processes.pop(task_id, None) + # Démarrer le seed en arrière-plan (temp_manifest sera supprimé par le seeder une fois terminé). + if original_history_url: + _start_background_seeder( + task_id=task_id, + source_url=source_url, + temp_manifest=temp_manifest, + dest_path=dest_path, + relative_path=relative_path, + file_index=file_index, + original_history_url=original_history_url, + ) + else: + try: + os.remove(temp_manifest) + except Exception: + pass return True, _("network_download_ok").format(os.path.basename(dest_path)) except Exception: try: @@ -867,6 +886,296 @@ def _download_torrent_with_aria2( raise +def _update_seeding_status(original_history_url: str, peers: int, ul_speed: float = 0.0) -> None: + """Met à jour l'entrée historique avec le statut Seeding, le nombre de peers et la vitesse UL.""" + if not isinstance(config.history, list): + return + for entry in config.history: + if entry.get("url") == original_history_url: + entry["status"] = "Seeding" + entry["seeds"] = peers + entry["ul_speed"] = ul_speed + config.needs_redraw = True + break + + +def _stop_seeding_status(original_history_url: str) -> None: + """Restaure le statut Download_OK une fois le seed terminé.""" + if not isinstance(config.history, list): + return + for entry in config.history: + if entry.get("url") == original_history_url: + if entry.get("status") == "Seeding": + entry["status"] = "Download_OK" + entry["seeds"] = 0 + config.needs_redraw = True + _save_history_with_feedback("seeder:done") + break + + +def _start_background_seeder( + task_id: str, + source_url: str, + temp_manifest: str, + dest_path: str, + relative_path: str, + file_index: int, + original_history_url: str, +) -> None: + """Lance aria2c en mode seed pur dans un thread daemon après un téléchargement torrent réussi. + + aria2c vérifie l'intégrité du fichier (hash-check) puis seed indéfiniment. + Le seed s'arrête automatiquement si le fichier est supprimé ou si l'app se ferme. + Le fichier temp_manifest (.torrent) est supprimé à la fin du seed. + """ + def _seeder_worker() -> None: + import hashlib as _hashlib + import random as _random + dest_dir = os.path.dirname(dest_path) + dest_basename = os.path.basename(dest_path) + torrent_basename = os.path.basename(relative_path) or dest_basename + + # Si le nom du fichier dans le torrent diffère du nom final (dest_path), + # créer un hard-link portant le nom attendu par aria2c dans un sous-dossier dédié. + # aria2c identifie les fichiers par leur nom (chemin interne du torrent) : + # sans ce lien, il recréerait le fichier depuis 0 au lieu de seeder. + _seed_key = _hashlib.md5(f"seed|{source_url}|{file_index}".encode()).hexdigest()[:12] + seed_work_dir = os.path.join(dest_dir, ".rgsx_seed", _seed_key) + link_created = False + seed_dir: str + + if torrent_basename != dest_basename: + try: + os.makedirs(seed_work_dir, exist_ok=True) + link_path = os.path.join(seed_work_dir, torrent_basename) + if os.path.exists(link_path): + os.remove(link_path) + os.link(dest_path, link_path) # hard-link (même volume) + seed_dir = seed_work_dir + link_created = True + except OSError: + # Hard-link impossible (volumes différents, etc.) → seed dans dest_dir + # avec le nom réel ; aria2c risque de ne pas trouver le fichier et + # de le re-télécharger, mais c'est mieux que de ne pas essayer. + logger.warning( + "[seeder] impossible de créer un hard-link pour %s ; " + "le seed peut échouer si le nom ne correspond pas au torrent", + dest_path, + ) + seed_dir = dest_dir + else: + seed_dir = dest_dir + + try: + aria2c_cmd = _resolve_aria2c_command() + except Exception as exc: + logger.warning("[seeder] aria2c indisponible, seed annulé : %s", exc) + try: + os.remove(temp_manifest) + except Exception: + pass + if link_created: + try: + shutil.rmtree(seed_work_dir, ignore_errors=True) + except Exception: + pass + return + + bt_listen_port = _random.randint(40000, 50000) + bt_listen_port_range = f"{bt_listen_port}-{bt_listen_port + 10}" + dht_state_file = os.path.join(config.CONFIG_FOLDER, "dht.dat") + upnp_handle = _upnp_open_port(bt_listen_port, description="RGSX-BT-SEED") + + dht_bootstrap_nodes = [ + "router.bittorrent.com:6881", + "router.utorrent.com:6881", + "dht.transmissionbt.com:6881", + ] + public_trackers = [ + "http://tracker.opentrackr.org:1337/announce", + "http://tracker.openbittorrent.com:80/announce", + "http://open.acgnxtracker.com:80/announce", + "http://tracker.bt4g.com:2095/announce", + "http://tracker.files.fm:6969/announce", + "http://tracker.gbitt.info:80/announce", + "http://vps02.net.orel.ru:80/announce", + "http://t.nyaatracker.com:80/announce", + "https://1337.abcvg.info:443/announce", + "https://opentracker.i2p.rocks:443/announce", + "https://tracker.gbitt.info:443/announce", + "https://tracker.loligirl.cn:443/announce", + "https://tracker.nanoha.org:443/announce", + "https://tracker.sloppyta.co:443/announce", + "https://tracker1.ctix.cn:443/announce", + "udp://tracker.opentrackr.org:1337/announce", + "udp://tracker.openbittorrent.com:6969/announce", + "udp://open.stealth.si:80/announce", + "udp://open.demonii.com:1337/announce", + "udp://opentracker.i2p.rocks:6969/announce", + "udp://opentracker.io:6969/announce", + "udp://exodus.desync.com:6969/announce", + "udp://explodie.org:6969/announce", + "udp://tracker.torrent.eu.org:451/announce", + "udp://tracker.moeking.me:6969/announce", + "udp://tracker.dler.org:6969/announce", + "udp://tracker.tiny-vps.com:6969/announce", + "udp://tracker.filemail.com:6969/announce", + "udp://tracker.therarbg.com:6969/announce", + "udp://tracker.therarbg.to:6969/announce", + "udp://tracker-udp.gbitt.info:80/announce", + "udp://tracker.0x.tf:6969/announce", + "udp://p4p.arenabg.com:1337/announce", + "udp://movies.zsw.ca:6969/announce", + "udp://new-line.net:6969/announce", + "udp://moonburrow.club:6969/announce", + "udp://epider.me:6969/announce", + "udp://bt1.archive.org:6969/announce", + "udp://bt2.archive.org:6969/announce", + "udp://bt.ktrackers.com:6666/announce", + "udp://fe.dealclub.de:6969/announce", + "udp://ipv4.tracker.harry.lu:80/announce", + "udp://public.tracker.vraphim.com:6969/announce", + ] + + cmd = [ + aria2c_cmd, + "--no-conf=true", + # Seed indéfiniment : seed-time=525600 = 365 jours en minutes. + # aria2c interprète --seed-time=-1 comme ≤ 0 (même effet que 0 = pas de seed). + # Notre boucle de monitoring arrête le processus dès que le fichier est supprimé. + "--seed-time=525600", + "--seed-ratio=0.0", + "--file-allocation=none", + "--allow-overwrite=false", + "--auto-file-renaming=false", + "--bt-remove-unselected-file=false", + "--enable-peer-exchange=true", + "--bt-enable-lpd=true", + "--bt-max-peers=0", + "--bt-min-crypto-level=plain", + "--bt-require-crypto=false", + "--enable-dht=true", + "--enable-dht6=false", + f"--dht-file-path={dht_state_file}", + f"--select-file={file_index}", + f"--dir={seed_dir}", + "--summary-interval=1", + "--download-result=hide", + f"--listen-port={bt_listen_port_range}", + f"--dht-listen-port={bt_listen_port_range}", + "--bt-tracker-timeout=60", + "--bt-tracker-connect-timeout=30", + # Vérifier que le fichier est intact avant de seeder. + "--check-integrity=true", + "--console-log-level=notice", + "--enable-color=false", + temp_manifest, + ] + for node in dht_bootstrap_nodes: + cmd.insert(1, f"--dht-entry-point={node}") + cmd.insert(1, f"--bt-tracker={','.join(public_trackers)}") + cmd.insert(1, "--bt-tracker-interval=60") + if config.OPERATING_SYSTEM == "Windows": + cmd.insert(1, "--disable-ipv6=true") + + logger.info("[seeder] démarrage seed pour %s, port=%s", dest_path, bt_listen_port_range) + + try: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + ) + except Exception as exc: + logger.error("[seeder] impossible de démarrer aria2c pour le seed : %s", exc) + _upnp_close_port(upnp_handle, bt_listen_port) + try: + os.remove(temp_manifest) + except Exception: + pass + if link_created: + try: + shutil.rmtree(seed_work_dir, ignore_errors=True) + except Exception: + pass + return + + _active_seeders[task_id] = { + "process": process, + "dest_path": dest_path, + "peers": 0, + "original_history_url": original_history_url, + } + _update_seeding_status(original_history_url, peers=0) + + def _consume_seed_stdout() -> None: + if not process.stdout: + return + for raw_line in iter(process.stdout.readline, ""): + line = _strip_ansi_escape_codes(raw_line).strip() + if not line: + continue + parsed = _parse_aria2_progress_line(line, 0) + if parsed is not None: + cn = int(parsed.get("connections") or 0) + ul = float(parsed.get("ul_speed_mib_s") or 0.0) + if task_id in _active_seeders: + _active_seeders[task_id]["peers"] = cn + _active_seeders[task_id]["ul_speed"] = ul + _update_seeding_status(original_history_url, peers=cn, ul_speed=ul) + if any(kw in line for kw in ("NOTICE", "WARN", "ERROR", "tracker", "Tracker")): + logger.debug("[seeder/aria2c] %s", line) + + stdout_thread = threading.Thread(target=_consume_seed_stdout, daemon=True) + stdout_thread.start() + + try: + while process.poll() is None: + if _app_shutting_down: + logger.info("[seeder] arrêt app détecté, seed terminé pour %s", dest_path) + process.terminate() + break + if not os.path.exists(dest_path): + logger.info("[seeder] fichier supprimé, seed terminé : %s", dest_path) + process.terminate() + break + time.sleep(2.0) + try: + process.wait(timeout=5) + except Exception: + try: + process.kill() + process.wait(timeout=5) + except Exception: + pass + finally: + stdout_thread.join(timeout=2) + _upnp_close_port(upnp_handle, bt_listen_port) + _active_seeders.pop(task_id, None) + _stop_seeding_status(original_history_url) + try: + os.remove(temp_manifest) + except Exception: + pass + if link_created: + try: + shutil.rmtree(seed_work_dir, ignore_errors=True) + except Exception: + pass + logger.info("[seeder] seed terminé pour %s", dest_path) + + seeder_thread = threading.Thread( + target=_seeder_worker, + name=f"seeder-{task_id}", + daemon=True, + ) + seeder_thread.start() + + def _build_browser_download_headers(referer: str | None = None, accept: str = 'application/octet-stream,*/*;q=0.8') -> dict: """Build browser-like headers for file downloads that reject minimal clients.""" headers = { @@ -2748,6 +3057,8 @@ download_threads = {} torrent_temp_roots: dict[str, str] = {} # Process aria2c actifs indexés par task_id : permet de les tuer au shutdown. _aria2c_processes: dict[str, "subprocess.Popen"] = {} +# Process aria2c de seed post-téléchargement, indexés par task_id. +_active_seeders: dict[str, dict] = {} # Flag global : True quand l'application est en cours d'arrêt propre. # Permet d'ignorer les signaux d'annulation déclenchés par le shutdown asyncio # et de préserver l'état "Téléchargement" en historique pour la reprise. @@ -2873,6 +3184,21 @@ def shutdown_downloads(): pass logger.debug(f"shutdown_downloads: aria2c tué pour task_id={_tid}") _aria2c_processes.clear() + # Tuer tous les seeders actifs + for _tid, _info in list(_active_seeders.items()): + _proc = _info.get("process") + if _proc is not None: + try: + if _proc.poll() is None: + _proc.terminate() + _proc.wait(timeout=3) + except Exception: + try: + _proc.kill() + except Exception: + pass + logger.debug(f"shutdown_downloads: seeder tué pour task_id={_tid}") + _active_seeders.clear() logger.debug("shutdown_downloads: _app_shutting_down=True, aria2c terminés, file d'attente vidée.") @@ -3066,6 +3392,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas task_id, cancel_ev, progress_queues[task_id], + original_history_url, ) result[0] = success result[1] = message diff --git a/version.json b/version.json index a59d3fe..d215285 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2.6.4.1" + "version": "2.6.4.2" } \ No newline at end of file