## v2.6.4.2 (2026.05.07)

- Add reset all settings functionality in the pause menu
- add seeding display on history
This commit is contained in:
skymike03
2026-05-07 22:55:11 +02:00
parent 14aa7a1c12
commit ea61d75f3d
13 changed files with 535 additions and 15 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -1,3 +1,3 @@
{
"version": "2.6.4.1"
"version": "2.6.4.2"
}