mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 12:53:35 +02:00
## 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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.4.1"
|
||||
"version": "2.6.4.2"
|
||||
}
|
||||
Reference in New Issue
Block a user