From 31c8f6a63ee0a6a045c08ae4bbda0e98d21edb5f Mon Sep 17 00:00:00 2001 From: skymike03 Date: Sun, 12 Oct 2025 00:24:45 +0200 Subject: [PATCH] v2.2.4.2 - add 1fichier free mode handling (test) with wait time, like you download on 1fichier website. Don't need an api key anymore if you don"t have a subscription. If you have any api key it will use in priority --- ports/RGSX/__main__.py | 41 +++-- ports/RGSX/config.py | 2 +- ports/RGSX/controls.py | 101 ++++++++---- ports/RGSX/display.py | 94 ++++++++++- ports/RGSX/languages/de.json | 10 ++ ports/RGSX/languages/en.json | 12 +- ports/RGSX/languages/es.json | 10 ++ ports/RGSX/languages/fr.json | 10 ++ ports/RGSX/languages/it.json | 10 ++ ports/RGSX/languages/pt.json | 10 ++ ports/RGSX/network.py | 293 ++++++++++++++++++++++++++++++++++- ports/RGSX/utils.py | 85 ++++++++++ 12 files changed, 623 insertions(+), 55 deletions(-) diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index d82546b..6590303 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -697,22 +697,30 @@ async def main(): except Exception as e: logger.error(f"Impossible de charger les clés via helpers: {e}") keys_info = {'1fichier': getattr(config,'API_KEY_1FICHIER',''), 'alldebrid': getattr(config,'API_KEY_ALLDEBRID',''), 'realdebrid': getattr(config,'API_KEY_REALDEBRID','')} + + # SUPPRIMÉ: Vérification clés API obligatoires + # Maintenant on a le mode gratuit en fallback automatique + # if missing_all_provider_keys(): + # config.previous_menu_state = config.menu_state + # config.menu_state = "error" + # try: + # config.error_message = _("error_api_key").format(build_provider_paths_string()) + # except Exception: + # config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)" + # # Mise à jour historique + # config.history[-1]["status"] = "Erreur" + # config.history[-1]["progress"] = 0 + # config.history[-1]["message"] = "API NOT FOUND" + # save_history(config.history) + # config.needs_redraw = True + # logger.error("Aucune clé fournisseur (1fichier/AllDebrid/RealDebrid) disponible") + # config.pending_download = None + # continue + + # Avertissement si pas de clé (utilisation mode gratuit) if missing_all_provider_keys(): - config.previous_menu_state = config.menu_state - config.menu_state = "error" - try: - config.error_message = _("error_api_key").format(build_provider_paths_string()) - except Exception: - config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)" - # Mise à jour historique - config.history[-1]["status"] = "Erreur" - config.history[-1]["progress"] = 0 - config.history[-1]["message"] = "API NOT FOUND" - save_history(config.history) - config.needs_redraw = True - logger.error("Aucune clé fournisseur (1fichier/AllDebrid/RealDebrid) disponible") - config.pending_download = None - continue + logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)") + pending = check_extension_before_download(url, platform_name, game_name) if not pending: config.menu_state = "error" @@ -957,6 +965,9 @@ async def main(): draw_history_extract_archive(screen) elif config.menu_state == "confirm_clear_history": draw_clear_history_dialog(screen) + elif config.menu_state == "support_dialog": + from display import draw_support_dialog + draw_support_dialog(screen) elif config.menu_state == "confirm_cancel_download": draw_cancel_download_dialog(screen) elif config.menu_state == "reload_games_data": diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 0d60d6f..041c614 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -13,7 +13,7 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.2.4.1" +app_version = "2.2.4.2" def get_application_root(): diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 67c091e..db7bc63 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -660,22 +660,30 @@ def handle_controls(event, sources, joystick, screen): if is_1fichier_url(url): from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string ensure_download_provider_keys(False) + + # SUPPRIMÉ: Vérification clés API obligatoires + # Maintenant on a le mode gratuit en fallback automatique + # if missing_all_provider_keys(): + # config.previous_menu_state = config.menu_state + # config.menu_state = "error" + # try: + # config.error_message = _("error_api_key").format(build_provider_paths_string()) + # except Exception as e: + # logger.error(f"Erreur lors de la traduction de error_api_key: {str(e)}") + # config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)" + # config.history[-1]["status"] = "Erreur" + # config.history[-1]["progress"] = 0 + # config.history[-1]["message"] = "API NOT FOUND" + # save_history(config.history) + # config.needs_redraw = True + # logger.error("Clés API manquantes pour tous les fournisseurs (1fichier/AllDebrid/RealDebrid).") + # config.pending_download = None + # return action + + # Avertissement si pas de clé (utilisation mode gratuit) if missing_all_provider_keys(): - config.previous_menu_state = config.menu_state - config.menu_state = "error" - try: - config.error_message = _("error_api_key").format(build_provider_paths_string()) - except Exception as e: - logger.error(f"Erreur lors de la traduction de error_api_key: {str(e)}") - config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)" - config.history[-1]["status"] = "Erreur" - config.history[-1]["progress"] = 0 - config.history[-1]["message"] = "API NOT FOUND" - save_history(config.history) - config.needs_redraw = True - logger.error("Clés API manquantes pour tous les fournisseurs (1fichier/AllDebrid/RealDebrid).") - config.pending_download = None - return action + logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)") + config.pending_download = check_extension_before_download(url, platform, game_name) if config.pending_download: is_supported = is_extension_supported( @@ -778,21 +786,29 @@ def handle_controls(event, sources, joystick, screen): if is_1fichier_url(url): from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string ensure_download_provider_keys(False) + + # SUPPRIMÉ: Vérification clés API obligatoires + # Maintenant on a le mode gratuit en fallback automatique + # if missing_all_provider_keys(): + # config.previous_menu_state = config.menu_state + # config.menu_state = "error" + # try: + # config.error_message = _("error_api_key").format(build_provider_paths_string()) + # except Exception: + # config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)" + # config.history[-1]["status"] = "Erreur" + # config.history[-1]["progress"] = 0 + # config.history[-1]["message"] = "API NOT FOUND" + # save_history(config.history) + # config.needs_redraw = True + # logger.error("Clés API manquantes pour tous les fournisseurs (1fichier/AllDebrid/RealDebrid).") + # config.pending_download = None + # return action + + # Avertissement si pas de clé (utilisation mode gratuit) if missing_all_provider_keys(): - config.previous_menu_state = config.menu_state - config.menu_state = "error" - try: - config.error_message = _("error_api_key").format(build_provider_paths_string()) - except Exception: - config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)" - config.history[-1]["status"] = "Erreur" - config.history[-1]["progress"] = 0 - config.history[-1]["message"] = "API NOT FOUND" - save_history(config.history) - config.needs_redraw = True - logger.error("Clés API manquantes pour tous les fournisseurs (1fichier/AllDebrid/RealDebrid).") - config.pending_download = None - return action + logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)") + task_id = str(pygame.time.get_ticks()) task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id)) else: @@ -1071,6 +1087,19 @@ def handle_controls(event, sources, joystick, screen): config.needs_redraw = True logger.debug("Annulation du vidage de l'historique, retour à history") + # Dialogue fichier de support + elif config.menu_state == "support_dialog": + if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"): + # Retour au menu pause + config.menu_state = "pause_menu" + config.needs_redraw = True + # Nettoyage des variables temporaires + if hasattr(config, 'support_zip_path'): + delattr(config, 'support_zip_path') + if hasattr(config, 'support_zip_error'): + delattr(config, 'support_zip_error') + logger.debug("Retour au menu pause depuis support_dialog") + # Menu options du jeu dans l'historique elif config.menu_state == "history_game_options": if not config.history or config.current_history_item >= len(config.history): @@ -1438,7 +1467,19 @@ def handle_controls(event, sources, joystick, screen): elif config.selected_option == 5: # Restart from utils import restart_application restart_application(2000) - elif config.selected_option == 6: # Quit + elif config.selected_option == 6: # Support + from utils import generate_support_zip + success, message, zip_path = generate_support_zip() + if success: + config.support_zip_path = zip_path + config.support_zip_error = None + else: + config.support_zip_path = None + config.support_zip_error = message + config.menu_state = "support_dialog" + config.last_state_change_time = pygame.time.get_ticks() + config.needs_redraw = True + elif config.selected_option == 7: # Quit # 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) diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index 22b7b4f..f23a593 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -1069,13 +1069,22 @@ def draw_history_list(screen): # Precompute provider prefix once provider_prefix = entry.get("provider_prefix") or (entry.get("provider") + ":" if entry.get("provider") else "") + # Compute status text (optimized version without redundant prefix for errors) if status in ["Téléchargement", "downloading"]: - status_text = _("history_status_downloading").format(progress) - # Coerce to string and prefix provider when relevant - status_text = str(status_text or "") - if provider_prefix and not status_text.startswith(provider_prefix): - status_text = f"{provider_prefix} {status_text}" + # Vérifier si un message personnalisé existe (ex: mode gratuit avec attente) + custom_message = entry.get('message', '') + # Détecter les messages du mode gratuit (commencent par '[' dans toutes les langues) + if custom_message and custom_message.strip().startswith('['): + # Utiliser le message personnalisé pour le mode gratuit + status_text = custom_message + else: + # Comportement normal: afficher le pourcentage + status_text = _("history_status_downloading").format(progress) + # Coerce to string and prefix provider when relevant + status_text = str(status_text or "") + if provider_prefix and not status_text.startswith(provider_prefix): + status_text = f"{provider_prefix} {status_text}" elif status == "Extracting": status_text = _("history_status_extracting").format(progress) status_text = str(status_text or "") @@ -1653,7 +1662,7 @@ 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: Language / Controls / Display / Games / Settings / Restart / Quit + # Nouvel ordre: Language / Controls / Display / Games / Settings / Restart / Support / Quit options = [ _("menu_language") if _ else "Language", # 0 -> sélecteur de langue direct _("menu_controls"), # 1 -> sous-menu controls @@ -1661,7 +1670,8 @@ def draw_pause_menu(screen, selected_option): _("menu_games") if _ else "Games", # 3 -> sous-menu games (history + sources + update) _("menu_settings_category") if _ else "Settings", # 4 -> sous-menu settings _("menu_restart"), # 5 -> reboot - _("menu_quit") # 6 -> quit + _("menu_support"), # 6 -> support + _("menu_quit") # 7 -> quit ] menu_width = int(config.screen_width * 0.6) button_height = int(config.screen_height * 0.048) @@ -1692,6 +1702,7 @@ def draw_pause_menu(screen, selected_option): "instruction_pause_games", "instruction_pause_settings", "instruction_pause_restart", + "instruction_pause_support", "instruction_pause_quit", ] try: @@ -2415,6 +2426,75 @@ def draw_reload_games_data_dialog(screen): draw_stylized_button(screen, _("button_yes"), yes_x, buttons_y, button_width, button_height, selected=config.redownload_confirm_selection == 1) draw_stylized_button(screen, _("button_no"), no_x, buttons_y, button_width, button_height, selected=config.redownload_confirm_selection == 0) + +def draw_support_dialog(screen): + """Affiche la boîte de dialogue du fichier de support généré.""" + 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)) + logger.debug("OVERLAY recréé dans draw_support_dialog") + + screen.blit(OVERLAY, (0, 0)) + + # Récupérer le nom du bouton "cancel/back" depuis la configuration des contrôles + cancel_key = "SELECT" + try: + from controls_mapper import get_mapped_button + cancel_key = get_mapped_button("cancel") or "SELECT" + except Exception: + pass + + # Déterminer le message à afficher (succès ou erreur) + if hasattr(config, 'support_zip_error') and config.support_zip_error: + title = _("support_dialog_title") + message = _("support_dialog_error").format(config.support_zip_error, cancel_key) + else: + title = _("support_dialog_title") + zip_path = getattr(config, 'support_zip_path', 'rgsx_support.zip') + message = _("support_dialog_message").format(zip_path, cancel_key) + + # Diviser le message par les retours à la ligne puis wrapper chaque segment + raw_segments = message.split('\n') if message else [] + wrapped_message = [] + for seg in raw_segments: + if seg.strip() == "": + wrapped_message.append("") # Ligne vide pour espacement + else: + wrapped_message.extend(wrap_text(seg, config.small_font, config.screen_width - 100)) + + line_height = config.small_font.get_height() + 5 + text_height = len(wrapped_message) * line_height + + # Calculer la hauteur du titre + title_height = config.font.get_height() + 10 + + # Calculer les dimensions de la boîte + margin_top_bottom = 20 + rect_height = title_height + text_height + 2 * margin_top_bottom + max_text_width = max([config.small_font.size(line)[0] for line in wrapped_message if line], default=300) + title_width = config.font.size(title)[0] + rect_width = max(max_text_width, title_width) + 100 + rect_x = (config.screen_width - rect_width) // 2 + rect_y = (config.screen_height - rect_height) // 2 + + # Dessiner la boîte + 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) + + # Afficher le titre + title_surf = config.font.render(title, True, THEME_COLORS["text"]) + title_rect = title_surf.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + title_height // 2)) + screen.blit(title_surf, title_rect) + + # Afficher le message + for i, line in enumerate(wrapped_message): + if line: # Ne pas rendre les lignes vides + text = config.small_font.render(line, True, THEME_COLORS["text"]) + text_rect = text.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + title_height + i * line_height + line_height // 2)) + screen.blit(text, text_rect) + + # Popup avec compte à rebours def draw_popup(screen): """Dessine un popup avec un message (adapté en largeur) et un compte à rebours.""" diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index 60cebbd..6a5d2ff 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -37,6 +37,11 @@ "history_status_completed": "Abgeschlossen", "history_status_error": "Fehler: {0}", "history_status_canceled": "Abgebrochen", + "free_mode_waiting": "[Kostenloser Modus] Warten: {0}/{1}s", + "free_mode_download": "[Kostenloser Modus] Download: {0}", + "free_mode_submitting": "[Kostenloser Modus] Formular wird gesendet...", + "free_mode_link_found": "[Kostenloser Modus] Link gefunden: {0}...", + "free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}", "download_status": "{0}: {1}", "download_canceled": "Download vom Benutzer abgebrochen.", "extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?", @@ -63,6 +68,7 @@ "menu_music_enabled": "Musik aktiviert: {0}", "menu_music_disabled": "Musik deaktiviert", "menu_restart": "Neustart", + "menu_support": "Unterstützung", "menu_filter_platforms": "Systeme filtern", "filter_platforms_title": "Systemsichtbarkeit", "filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}", @@ -74,6 +80,9 @@ "menu_allow_unknown_ext_enabled": "Ausblenden der Warnung bei unbekannter Erweiterung aktiviert", "menu_allow_unknown_ext_disabled": "Ausblenden der Warnung bei unbekannter Erweiterung deaktiviert", "menu_quit": "Beenden", + "support_dialog_title": "Support-Datei", + "support_dialog_message": "Eine Support-Datei wurde mit allen Ihren Konfigurations- und Protokolldateien erstellt.\n\nDatei: {0}\n\nUm Hilfe zu erhalten:\n1. Treten Sie dem RGSX Discord-Server bei\n2. Beschreiben Sie Ihr Problem\n3. Teilen Sie diese ZIP-Datei\n\nDrücken Sie {1}, um zum Menü zurückzukehren.", + "support_dialog_error": "Fehler beim Erstellen der Support-Datei:\n{0}\n\nDrücken Sie {1}, um zum Menü zurückzukehren.", "button_yes": "Ja", "button_no": "Nein", "button_OK": "OK", @@ -171,6 +180,7 @@ ,"instruction_pause_games": "Verlauf öffnen, Quelle wechseln oder Liste aktualisieren" ,"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_quit": "RGSX Anwendung beenden" ,"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen" ,"instruction_controls_remap": "Tasten / Buttons neu zuordnen" diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index 9775c72..a37e245 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -37,6 +37,11 @@ "history_status_completed": "Completed", "history_status_error": "Error: {0}", "history_status_canceled": "Canceled", + "free_mode_waiting": "[Free mode] Waiting: {0}/{1}s", + "free_mode_download": "[Free mode] Downloading: {0}", + "free_mode_submitting": "[Free mode] Submitting form...", + "free_mode_link_found": "[Free mode] Link found: {0}...", + "free_mode_completed": "[Free mode] Completed: {0}", "download_status": "{0}: {1}", "download_canceled": "Download canceled by user.", "extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?", @@ -73,16 +78,20 @@ "menu_allow_unknown_ext_off": "Hide unknown extension warning: No", "menu_allow_unknown_ext_enabled": "Hide unknown extension warning enabled", "menu_allow_unknown_ext_disabled": "Hide unknown extension warning disabled", + "menu_support": "Support", "menu_quit": "Quit", "button_yes": "Yes", "button_no": "No", "button_OK": "OK", "popup_restarting": "Restarting...", "controls_action_clear_history": "Multi-select / Clear History", - "controls_action_history": "History", "controls_action_delete": "Delete", "controls_action_space": "Space", "controls_action_start": "Help / Settings", + "support_dialog_title": "Support File", + "support_dialog_message": "A support file has been created with all your configuration and log files.\n\nFile: {0}\n\nTo get help:\n1. Join the RGSX Discord server\n2. Describe your issue\n3. Share this ZIP file\n\nPress {1} to return to the menu.", + "support_dialog_error": "Error generating support file:\n{0}\n\nPress {1} to return to the menu.", + "controls_action_history": "History", "network_checking_updates": "Checking for updates...", "network_update_available": "Update available: {0}", "network_extracting_update": "Extracting update...", @@ -171,6 +180,7 @@ "instruction_pause_games": "Open history, switch source or refresh list", "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_quit": "Exit the RGSX application" ,"instruction_controls_help": "Show full controller & keyboard reference" ,"instruction_controls_remap": "Change button / key bindings" diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index c2636ca..1de0a5a 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -37,6 +37,11 @@ "history_status_completed": "Completado", "history_status_error": "Error: {0}", "history_status_canceled": "Cancelado", + "free_mode_waiting": "[Modo gratuito] Esperando: {0}/{1}s", + "free_mode_download": "[Modo gratuito] Descargando: {0}", + "free_mode_submitting": "[Modo gratuito] Enviando formulario...", + "free_mode_link_found": "[Modo gratuito] Enlace encontrado: {0}...", + "free_mode_completed": "[Modo gratuito] Completado: {0}", "download_status": "{0}: {1}", "download_canceled": "Descarga cancelada por el usuario.", "extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?", @@ -63,6 +68,7 @@ "menu_music_enabled": "Música activada: {0}", "menu_music_disabled": "Música desactivada", "menu_restart": "Reiniciar", + "menu_support": "Soporte", "menu_filter_platforms": "Filtrar sistemas", "filter_platforms_title": "Visibilidad de sistemas", "filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}", @@ -74,6 +80,9 @@ "menu_allow_unknown_ext_enabled": "Aviso de extensión desconocida oculto (activado)", "menu_allow_unknown_ext_disabled": "Aviso de extensión desconocida visible (desactivado)", "menu_quit": "Salir", + "support_dialog_title": "Archivo de soporte", + "support_dialog_message": "Se ha creado un archivo de soporte con todos sus archivos de configuración y registros.\n\nArchivo: {0}\n\nPara obtener ayuda:\n1. Únete al servidor Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nPresiona {1} para volver al menú.", + "support_dialog_error": "Error al generar el archivo de soporte:\n{0}\n\nPresiona {1} para volver al menú.", "button_yes": "Sí", "button_no": "No", "button_OK": "OK", @@ -171,6 +180,7 @@ ,"instruction_pause_games": "Abrir historial, cambiar fuente o refrescar lista" ,"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_quit": "Salir de la aplicación RGSX" ,"instruction_controls_help": "Mostrar referencia completa de mando y teclado" ,"instruction_controls_remap": "Cambiar asignación de botones / teclas" diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index ca25ff2..5ff8c21 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -36,6 +36,11 @@ "history_status_completed": "Terminé", "history_status_error": "Erreur : {0}", "history_status_canceled": "Annulé", + "free_mode_waiting": "[Mode gratuit] Attente: {0}/{1}s", + "free_mode_download": "[Mode gratuit] Téléchargement: {0}", + "free_mode_submitting": "[Mode gratuit] Soumission formulaire...", + "free_mode_link_found": "[Mode gratuit] Lien trouvé: {0}...", + "free_mode_completed": "[Mode gratuit] Terminé: {0}", "download_status": "{0} : {1}", "download_canceled": "Téléchargement annulé par l'utilisateur.", "extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?", @@ -59,6 +64,7 @@ "menu_display": "Affichage", "display_layout": "Disposition", "menu_redownload_cache": "Mettre à jour la liste des jeux", + "menu_support": "Support", "menu_quit": "Quitter", "menu_music_enabled": "Musique activée : {0}", "menu_music_disabled": "Musique désactivée", @@ -68,6 +74,9 @@ "filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}", "filter_unsaved_warning": "Modifications non sauvegardées", "menu_show_unsupported_enabled": "Affichage systèmes non supportés activé", + "support_dialog_title": "Fichier de support", + "support_dialog_message": "Un fichier de support a été créé avec tous vos fichiers de configuration et logs.\n\nFichier: {0}\n\nPour obtenir de l'aide :\n1. Rejoignez le Discord RGSX\n2. Décrivez votre problème\n3. Partagez ce fichier ZIP\n\nAppuyez sur {1} pour revenir au menu.", + "support_dialog_error": "Erreur lors de la génération du fichier de support :\n{0}\n\nAppuyez sur {1} pour revenir au menu.", "menu_show_unsupported_disabled": "Affichage systèmes non supportés désactivé", "menu_allow_unknown_ext_on": "Masquer avertissement extension inconnue : Oui", "menu_allow_unknown_ext_off": "Masquer avertissement extension inconnue : Non", @@ -170,6 +179,7 @@ ,"instruction_pause_games": "Historique, source de liste ou rafraîchissement" ,"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_quit": "Quitter l'application RGSX" ,"instruction_controls_help": "Afficher la référence complète manette & clavier" ,"instruction_controls_remap": "Modifier l'association boutons / touches" diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json index e1f5092..95f9802 100644 --- a/ports/RGSX/languages/it.json +++ b/ports/RGSX/languages/it.json @@ -37,6 +37,11 @@ "history_status_completed": "Completato", "history_status_error": "Errore: {0}", "history_status_canceled": "Annullato", + "free_mode_waiting": "[Modalità gratuita] Attesa: {0}/{1}s", + "free_mode_download": "[Modalità gratuita] Download: {0}", + "free_mode_submitting": "[Modalità gratuita] Invio modulo...", + "free_mode_link_found": "[Modalità gratuita] Link trovato: {0}...", + "free_mode_completed": "[Modalità gratuita] Completato: {0}", "download_status": "{0}: {1}", "download_canceled": "Download annullato dall'utente.", "extension_warning_zip": "Il file '{0}' è un archivio e Batocera non supporta archivi per questo sistema. L'estrazione automatica avverrà dopo il download, continuare?", @@ -63,6 +68,7 @@ "menu_music_enabled": "Musica attivata: {0}", "menu_music_disabled": "Musica disattivata", "menu_restart": "Riavvia", + "menu_support": "Supporto", "menu_filter_platforms": "Filtra sistemi", "filter_platforms_title": "Visibilità sistemi", "filter_platforms_info": "Visibili: {0} | Nascosti: {1} / Totale: {2}", @@ -74,6 +80,9 @@ "menu_allow_unknown_ext_enabled": "Nascondi avviso estensione sconosciuta abilitato", "menu_allow_unknown_ext_disabled": "Nascondi avviso estensione sconosciuta disabilitato", "menu_quit": "Esci", + "support_dialog_title": "File di supporto", + "support_dialog_message": "È stato creato un file di supporto con tutti i file di configurazione e di registro.\n\nFile: {0}\n\nPer ottenere aiuto:\n1. Unisciti al server Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nPremi {1} per tornare al menu.", + "support_dialog_error": "Errore durante la generazione del file di supporto:\n{0}\n\nPremi {1} per tornare al menu.", "button_yes": "Sì", "button_no": "No", "button_OK": "OK", @@ -171,6 +180,7 @@ ,"instruction_pause_games": "Aprire cronologia, cambiare sorgente o aggiornare elenco" ,"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_quit": "Uscire dall'applicazione RGSX" ,"instruction_controls_help": "Mostrare riferimento completo controller & tastiera" ,"instruction_controls_remap": "Modificare associazione pulsanti / tasti" diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index 80751f0..af47270 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -37,6 +37,11 @@ "history_status_completed": "Concluído", "history_status_error": "Erro: {0}", "history_status_canceled": "Cancelado", + "free_mode_waiting": "[Modo gratuito] Aguardando: {0}/{1}s", + "free_mode_download": "[Modo gratuito] Baixando: {0}", + "free_mode_submitting": "[Modo gratuito] Enviando formulário...", + "free_mode_link_found": "[Modo gratuito] Link encontrado: {0}...", + "free_mode_completed": "[Modo gratuito] Concluído: {0}", "download_status": "{0}: {1}", "download_canceled": "Download cancelado pelo usuário.", "extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?", @@ -63,6 +68,7 @@ "menu_music_enabled": "Música ativada: {0}", "menu_music_disabled": "Música desativada", "menu_restart": "Reiniciar", + "menu_support": "Suporte", "menu_filter_platforms": "Filtrar sistemas", "filter_platforms_title": "Visibilidade dos sistemas", "filter_platforms_info": "Visíveis: {0} | Ocultos: {1} / Total: {2}", @@ -74,6 +80,9 @@ "menu_allow_unknown_ext_enabled": "Aviso de extensão desconhecida oculto (ativado)", "menu_allow_unknown_ext_disabled": "Aviso de extensão desconhecida visível (desativado)", "menu_quit": "Sair", + "support_dialog_title": "Arquivo de suporte", + "support_dialog_message": "Foi criado um arquivo de suporte com todos os seus arquivos de configuração e logs.\n\nArquivo: {0}\n\nPara obter ajuda:\n1. Junte-se ao servidor Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nPressione {1} para voltar ao menu.", + "support_dialog_error": "Erro ao gerar o arquivo de suporte:\n{0}\n\nPressione {1} para voltar ao menu.", "button_yes": "Sim", "button_no": "Não", "button_OK": "OK", @@ -171,6 +180,7 @@ ,"instruction_pause_games": "Abrir histórico, mudar fonte ou atualizar lista" ,"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_quit": "Sair da aplicação RGSX" ,"instruction_controls_help": "Mostrar referência completa de controle e teclado" ,"instruction_controls_remap": "Modificar associação de botões / teclas" diff --git a/ports/RGSX/network.py b/ports/RGSX/network.py index 267d9ba..a5f7d26 100644 --- a/ports/RGSX/network.py +++ b/ports/RGSX/network.py @@ -28,10 +28,198 @@ import os import json from pathlib import Path from language import _ # Import de la fonction de traduction +import re +import html as html_module +from urllib.parse import urljoin, unquote logger = logging.getLogger(__name__) +# ================== TÉLÉCHARGEMENT 1FICHIER GRATUIT ================== +# Fonction pour télécharger depuis 1fichier sans API key (mode gratuit) +# Compatible RGSX - Sans BeautifulSoup ni httpx + +# Regex pour détecter le compte à rebours +WAIT_REGEXES_1F = [ + r'(?:veuillez\s+)?patiente[rz]\s*(\d+)\s*(?:sec|secondes?|s)\b', + r'please\s+wait\s*(\d+)\s*(?:sec|seconds?)\b', + r'var\s+ct\s*=\s*(\d+)\s*;', + r'var\s+ct\s*=\s*(\d+)\s*\*\s*60\s*;', +] + +def extract_wait_seconds_1f(html_text): + """Extrait le temps d'attente depuis le HTML 1fichier""" + for pattern in WAIT_REGEXES_1F: + match = re.search(pattern, html_text, re.IGNORECASE) + if match: + seconds = int(match.group(1)) + # Si c'est en minutes (pattern avec *60) + if '*60' in pattern or r'*\s*60' in pattern: + seconds = seconds * 60 + return seconds + return 0 + +def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progress_callback=None, wait_callback=None, cancel_event=None): + """ + Télécharge un fichier depuis 1fichier.com en mode gratuit (sans API key). + Compatible RGSX - Sans BeautifulSoup ni httpx. + + Args: + url: URL 1fichier + dest_dir: Dossier de destination + session: Session requests + log_callback: Fonction appelée avec les messages de log + progress_callback: Fonction appelée avec (filename, downloaded, total, percent) + wait_callback: Fonction appelée avec (remaining_seconds, total_seconds) + cancel_event: threading.Event pour annuler le téléchargement + + Returns: + (success: bool, filepath: str|None, error_message: str|None) + """ + + def _log(msg): + if log_callback: + try: + log_callback(msg) + except Exception: + pass + logger.info(msg) + + def _progress(filename, downloaded, total, pct): + if progress_callback: + try: + progress_callback(filename, downloaded, total, pct) + except Exception: + pass + + def _wait(remaining, total_wait): + if wait_callback: + try: + wait_callback(remaining, total_wait) + except Exception: + pass + + try: + os.makedirs(dest_dir, exist_ok=True) + _log(_("free_mode_download").format(url)) + + # 1. GET page initiale + if cancel_event and cancel_event.is_set(): + return (False, None, "Annulé") + + r = session.get(url, allow_redirects=True, timeout=30) + r.raise_for_status() + html = r.text + + # 2. Détection compte à rebours + wait_s = extract_wait_seconds_1f(html) + + if wait_s > 0: + _log(f"{wait_s}s...") + for remaining in range(wait_s, 0, -1): + if cancel_event and cancel_event.is_set(): + return (False, None, "Annulé") + _wait(remaining, wait_s) + time.sleep(1) + + # 3. Chercher formulaire et soumettre + if cancel_event and cancel_event.is_set(): + return (False, None, "Annulé") + + form_match = re.search(r']*id=[\"\']f1[\"\'][^>]*>(.*?)', html, re.DOTALL | re.IGNORECASE) + + if form_match: + form_html = form_match.group(1) + + # Extraire les champs + data = {} + for inp_match in re.finditer(r']+>', form_html, re.IGNORECASE): + inp = inp_match.group(0) + + name_m = re.search(r'name=[\"\']([^\"\']+)', inp) + value_m = re.search(r'value=[\"\']([^\"\']*)', inp) + + if name_m: + name = name_m.group(1) + value = value_m.group(1) if value_m else '' + data[name] = html_module.unescape(value) + + # POST formulaire + _log(_("free_mode_submitting")) + r2 = session.post(str(r.url), data=data, allow_redirects=True, timeout=30) + r2.raise_for_status() + html = r2.text + + # 4. Chercher lien de téléchargement + if cancel_event and cancel_event.is_set(): + return (False, None, "Annulé") + + patterns = [ + r'href=[\"\']([^\"\']+)[\"\'][^>]*>(?:cliquer|click|télécharger|download)', + r'href=[\"\']([^\"\']*/dl/[^\"\']+)', + r'https?://[a-z0-9.-]*1fichier\.com/[A-Za-z0-9]{8,}' + ] + + direct_link = None + for pattern in patterns: + match = re.search(pattern, html, re.IGNORECASE) + if match: + direct_link = match.group(1) if '//' in match.group(0) else urljoin(str(r.url), match.group(1)) + break + + if not direct_link: + return (False, None, "Lien de téléchargement introuvable") + + _log(_("free_mode_link_found").format(direct_link[:60])) + + # 5. HEAD pour infos fichier + if cancel_event and cancel_event.is_set(): + return (False, None, "Annulé") + + head = session.head(direct_link, allow_redirects=True, timeout=30) + + # Nom fichier + filename = 'downloaded_file' + cd = head.headers.get('content-disposition', '') + if cd: + fn_match = re.search(r'filename\*?=[\"\']?([^\"\';]+)', cd, re.IGNORECASE) + if fn_match: + filename = unquote(fn_match.group(1)) + + filename = sanitize_filename(filename) + filepath = os.path.join(dest_dir, filename) + + # 6. Téléchargement + _log(_("free_mode_download").format(filename)) + + with session.get(direct_link, stream=True, allow_redirects=True, timeout=30) as resp: + resp.raise_for_status() + total = int(resp.headers.get('content-length', 0)) + + with open(filepath, 'wb') as f: + downloaded = 0 + for chunk in resp.iter_content(chunk_size=128*1024): + if cancel_event and cancel_event.is_set(): + return (False, None, "Annulé") + + f.write(chunk) + downloaded += len(chunk) + + if total: + pct = downloaded / total * 100 + _progress(filename, downloaded, total, pct) + + _log(_("free_mode_completed").format(filepath)) + return (True, filepath, None) + + except Exception as e: + error_msg = f"Erreur mode gratuit: {str(e)}" + _log(error_msg) + logger.error(error_msg, exc_info=True) + return (False, None, error_msg) + +# ==================== FIN TÉLÉCHARGEMENT GRATUIT ==================== + # Plus besoin de web_progress.json - l'interface web lit directement history.json # Les fonctions update_web_progress() et remove_web_progress() sont supprimées @@ -1303,7 +1491,110 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= except Exception as e: logger.error(f"Exception RealDebrid fallback: {e}") if not final_url: - logger.error("Aucune URL directe obtenue (AllDebrid & RealDebrid échoués ou absents)") + # NOUVEAU: Fallback mode gratuit 1fichier si aucune clé API disponible + logger.warning("Aucune URL directe obtenue via API - Tentative mode gratuit 1fichier") + + # Créer un lock pour ce téléchargement + free_lock = threading.Lock() + + try: + # Créer une session requests pour le mode gratuit + free_session = requests.Session() + free_session.headers.update({'User-Agent': 'Mozilla/5.0'}) + + # Callbacks pour le mode gratuit + def log_cb(msg): + logger.info(msg) + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url: + entry["message"] = msg + config.needs_redraw = True + break + + def progress_cb(filename, downloaded, total, pct): + with free_lock: + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url and entry["status"] == "downloading": + entry["progress"] = int(pct) if pct else 0 + entry["downloaded_size"] = downloaded + entry["total_size"] = total + # Effacer le message personnalisé pour afficher le pourcentage + entry["message"] = "" + config.needs_redraw = True + save_history(config.history) + break + progress_queues[task_id].put((task_id, downloaded, total)) + + def wait_cb(remaining, total_wait): + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url: + entry["message"] = _("free_mode_waiting").format(remaining, total_wait) + config.needs_redraw = True + save_history(config.history) + break + + # Lancer le téléchargement gratuit + success, filepath, error_msg = download_1fichier_free_mode( + url=link, + dest_dir=dest_dir, + session=free_session, + log_callback=log_cb, + progress_callback=progress_cb, + wait_callback=wait_cb, + cancel_event=cancel_ev + ) + + if success: + logger.info(f"Téléchargement gratuit réussi: {filepath}") + result[0] = True + result[1] = _("network_download_ok").format(game_name) if _ else f"Download successful: {game_name}" + provider_used = 'FREE' + _set_provider_in_history(provider_used) + + # Mettre à jour l'historique + if isinstance(config.history, list): + for entry in config.history: + if "url" in entry and entry["url"] == url: + entry["status"] = "Completed" + entry["progress"] = 100 + entry["message"] = result[1] + entry["provider"] = "FREE" + entry["provider_prefix"] = "FREE:" + save_history(config.history) + config.needs_redraw = True + break + + # Traiter le fichier (extraction si nécessaire) + if not is_zip_non_supported: + try: + if filepath.lower().endswith('.zip'): + logger.info(f"Extraction ZIP: {filepath}") + extract_zip(filepath, dest_dir) + os.remove(filepath) + logger.info("ZIP extrait et supprimé") + elif filepath.lower().endswith('.rar'): + logger.info(f"Extraction RAR: {filepath}") + extract_rar(filepath, dest_dir) + os.remove(filepath) + logger.info("RAR extrait et supprimé") + except Exception as e: + logger.error(f"Erreur extraction: {e}") + + return + else: + logger.error(f"Échec téléchargement gratuit: {error_msg}") + result[0] = False + result[1] = f"Erreur mode gratuit: {error_msg}" + return + + except Exception as e: + logger.error(f"Exception mode gratuit: {e}", exc_info=True) + + # Si le mode gratuit a échoué aussi + logger.error("Échec de tous les providers (API + mode gratuit)") result[0] = False if result[1] is None: result[1] = _("network_api_error").format("No provider available") if _ else "No provider available" diff --git a/ports/RGSX/utils.py b/ports/RGSX/utils.py index 0832a2b..ca2f7e6 100644 --- a/ports/RGSX/utils.py +++ b/ports/RGSX/utils.py @@ -77,6 +77,91 @@ def restart_application(delay_ms: int = 2000): logger.exception(f"Failed to restart immediately: {e}") except Exception as e: logger.exception(f"Failed to schedule restart: {e}") + + +def generate_support_zip(): + """Génère un fichier ZIP contenant tous les fichiers de support pour le diagnostic. + + Returns: + tuple: (success: bool, message: str, zip_path: str ou None) + """ + import zipfile + import tempfile + from datetime import datetime + + try: + # Créer un fichier ZIP temporaire + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + zip_filename = f"rgsx_support_{timestamp}.zip" + zip_path = os.path.join(config.SAVE_FOLDER, zip_filename) + + # Liste des fichiers à inclure + files_to_include = [] + + # Ajouter les fichiers de configuration + if hasattr(config, 'CONTROLS_CONFIG_PATH') and os.path.exists(config.CONTROLS_CONFIG_PATH): + files_to_include.append(('controls.json', config.CONTROLS_CONFIG_PATH)) + + if hasattr(config, 'HISTORY_PATH') and os.path.exists(config.HISTORY_PATH): + files_to_include.append(('history.json', config.HISTORY_PATH)) + + if hasattr(config, 'RGSX_SETTINGS_PATH') and os.path.exists(config.RGSX_SETTINGS_PATH): + files_to_include.append(('rgsx_settings.json', config.RGSX_SETTINGS_PATH)) + + # Ajouter les fichiers de log + if hasattr(config, 'log_file') and os.path.exists(config.log_file): + files_to_include.append(('RGSX.log', config.log_file)) + + # Log du serveur web + if hasattr(config, 'log_dir'): + web_log = os.path.join(config.log_dir, 'rgsx_web.log') + if os.path.exists(web_log): + files_to_include.append(('rgsx_web.log', web_log)) + + web_startup_log = os.path.join(config.log_dir, 'rgsx_web_startup.log') + if os.path.exists(web_startup_log): + files_to_include.append(('rgsx_web_startup.log', web_startup_log)) + + # Créer le fichier ZIP + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for archive_name, file_path in files_to_include: + try: + zipf.write(file_path, archive_name) + logger.debug(f"Ajouté au ZIP: {archive_name}") + except Exception as e: + logger.warning(f"Impossible d'ajouter {archive_name}: {e}") + + # Ajouter un fichier README avec des informations système + readme_content = f"""RGSX Support Package +Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +System Information: +- OS: {config.OPERATING_SYSTEM} +- Python: {sys.version} +- Platform: {sys.platform} + +Included Files: +""" + for archive_name, _ in files_to_include: + readme_content += f"- {archive_name}\n" + + readme_content += """ +Instructions: +1. Join RGSX Discord server +2. Describe your issue in the support channel +3. Upload this ZIP file to help the team diagnose your problem + +DO NOT share this file publicly as it may contain sensitive information. +""" + zipf.writestr('README.txt', readme_content) + + logger.info(f"Fichier de support généré: {zip_path}") + return (True, f"Support file created: {zip_filename}", zip_path) + + except Exception as e: + logger.error(f"Erreur lors de la génération du fichier de support: {e}") + return (False, str(e), None) + _extensions_cache = None # type: ignore _extensions_json_regenerated = False