- 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
This commit is contained in:
skymike03
2025-10-12 00:24:45 +02:00
parent 2caf0f80c7
commit 31c8f6a63e
12 changed files with 623 additions and 55 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'<form[^>]*id=[\"\']f1[\"\'][^>]*>(.*?)</form>', 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'<input[^>]+>', 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"

View File

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