- add new instructions on menus to describe each function
- upgrade controller_debug.pygame file to create a controller support
- update command-line interface to be more effiscient and readable
This commit is contained in:
skymike03
2025-09-12 17:00:51 +02:00
parent 3c36dd2e02
commit 45f5d8bf7b
14 changed files with 925 additions and 162 deletions
+10 -10
View File
@@ -215,38 +215,38 @@ else:
# Détection spécifique Elite AVANT la détection générique Xbox
if ("microsoft xbox controller" in lname):
config.xbox_elite_controller = True
logger.debug(f"Controller detected (Xbox Elite): {name}")
print(f"Controller detected (Xbox Elite): {name}")
logger.debug(f"Controller detected: {name}")
print(f"Controller detected: {name}")
break
if ("xbox" in lname) or ("x-box" in lname) or ("xinput" in lname) or ("microsoft x-box" in lname) or ("x-box 360" in lname) or ("360" in lname):
config.xbox_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Xbox Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "playstation" in lname:
elif "playstation" in lname or "ps3" in lname or "sony" in lname:
config.playstation_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Playstation Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "nintendo" in lname:
config.nintendo_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Nintendo Controller detected : {name}")
print(f"Controller detected : {name}")
elif "trimui" in lname:
config.trimui_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Trimui Controller detected : {name}")
print(f"Controller detected : {name}")
elif "logitech" in lname:
config.logitech_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Logitech Controller detected : {name}")
print(f"Controller detected : {name}")
elif "8bitdo" in lname or "8-bitdo" in lname:
config.eightbitdo_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"8bitdoController detected : {name}")
print(f"Controller detected : {name}")
elif "steam" in lname:
config.steam_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Steam Controller detected : {name}")
print(f"Controller detected : {name}")
# Note: virtual keyboard display now depends on controller presence (config.joystick)
logger.debug(f"Flags contrôleur: xbox={config.xbox_controller}, ps={config.playstation_controller}, nintendo={config.nintendo_controller}, eightbitdo={config.eightbitdo_controller}, steam={config.steam_controller}, trimui={config.trimui_controller}, logitech={config.logitech_controller}, generic={config.generic_controller}")
+1 -6
View File
@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.2.2.1"
app_version = "2.2.2.2"
def get_application_root():
@@ -173,14 +173,9 @@ batch_download_indices = [] # File d'attente des indices de jeux à traiter en
batch_in_progress = False # Indique qu'un lot est en cours
batch_pending_game = None # Données du jeu en attente de confirmation d'extension
# --- Premium systems filtering ---
# Liste des marqueurs (substrings) indiquant qu'un système/plateforme requiert un compte premium ou une clé API.
# On teste la présence (case-insensitive) de ces marqueurs dans le nom du système (ex: "Microsoft Windows (1Fichier)").
# Ajoutez librement d'autres valeurs (ex: 'RealDebrid', 'AllDebrid') si de futurs systèmes nécessitent un compte.
PREMIUM_HOST_MARKERS = [
"1Fichier",
]
# Flag runtime contrôlant le masquage des systèmes premium dans le menu pause > games.
hide_premium_systems = False
# Indicateurs d'entrée (détectés au démarrage)
+151
View File
@@ -1389,6 +1389,28 @@ def draw_language_menu(screen):
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y))
screen.blit(instruction_surface, instruction_rect)
def draw_menu_instruction(screen, instruction_text, last_button_bottom=None):
"""Dessine une ligne d'instruction centrée au-dessus du footer.
- Réserve une zone footer (72px) + marge bas.
- Si last_button_bottom est fourni, s'assure d'un écart minimal (16px).
- Utilise la petite police et couleurs du thème.
"""
if not instruction_text:
return
try:
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
footer_reserved = 72
bottom_margin = 12
instruction_y = config.screen_height - footer_reserved - bottom_margin
min_gap = 16
if last_button_bottom is not None and instruction_y - last_button_bottom < min_gap:
instruction_y = last_button_bottom + min_gap
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y))
screen.blit(instruction_surface, instruction_rect)
except Exception as e:
logger.error(f"Erreur draw_menu_instruction: {e}")
def draw_display_menu(screen):
"""Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés)."""
screen.blit(OVERLAY, (0, 0))
@@ -1494,6 +1516,28 @@ def draw_pause_menu(screen, selected_option):
)
config.pause_menu_total_options = len(options)
# Instruction contextuelle pour l'option sélectionnée
# Mapping des clés i18n parallèles à la liste options (même ordre)
instruction_keys = [
"instruction_pause_language",
"instruction_pause_controls",
"instruction_pause_display",
"instruction_pause_games",
"instruction_pause_settings",
"instruction_pause_restart",
"instruction_pause_quit",
]
try:
key = instruction_keys[selected_option]
instruction_text = _(key)
except Exception:
instruction_text = "" # Sécurité si index hors borne
if instruction_text:
# Calcul de la position du dernier bouton pour éviter chevauchement
last_button_bottom = menu_y + margin_top_bottom + (len(options) - 1) * (button_height + 12) + button_height
draw_menu_instruction(screen, instruction_text, last_button_bottom)
def _draw_submenu_generic(screen, title, options, selected_index):
"""Helper générique pour dessiner un sous-menu hiérarchique."""
screen.blit(OVERLAY, (0, 0))
@@ -1529,6 +1573,57 @@ def draw_pause_controls_menu(screen, selected_index):
_("menu_back") if _ else "Back"
]
_draw_submenu_generic(screen, _("menu_controls") if _ else "Controls", options, selected_index)
# Instructions contextuelles
instruction_keys = [
"instruction_controls_help", # pour menu_controls (afficher l'aide)
"instruction_controls_remap", # remap
"instruction_generic_back", # retour
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
last_button_bottom = None # recalculer via géométrie si nécessaire; ici on réutilise calcul simple
# Reconstituer la position du dernier bouton comme dans _draw_submenu_generic
menu_width = int(config.screen_width * 0.72)
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
# Title height approximatif
title_surface = config.font.render("X", True, THEME_COLORS["text"]) # hauteur représentative
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 # approx: title center adjust + bottom spacing
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
text = _(key)
if key == "instruction_display_hide_premium":
# Inject dynamic list of premium providers from config.PREMIUM_HOST_MARKERS
try:
from config import PREMIUM_HOST_MARKERS
# Clean, preserve order, remove duplicates (case-insensitive)
seen = set()
providers_clean = []
for p in PREMIUM_HOST_MARKERS:
if not p: continue
norm = p.strip()
if not norm: continue
low = norm.lower()
if low in seen: continue
seen.add(low)
providers_clean.append(norm)
providers_str = ", ".join(providers_clean)
if not providers_str:
providers_str = "-"
if "{providers}" in text:
try:
text = text.format(providers=providers_str)
except Exception:
# Fallback if formatting fails
text = f"{text.replace('{providers}','').strip()} {providers_str}".strip()
else:
# Append providers if placeholder missing (backward compatibility)
text = f"{text} : {providers_str}" if providers_str else text
except Exception:
pass
draw_menu_instruction(screen, text, last_button_bottom)
def draw_pause_display_menu(screen, selected_index):
from rgsx_settings import (
@@ -1583,6 +1678,28 @@ def draw_pause_display_menu(screen, selected_index):
back_txt = _("menu_back") if _ else "Back"
options = [layout_txt, font_txt, font_family_txt, unsupported_txt, unknown_txt, hide_premium_txt, filter_txt, back_txt]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
instruction_keys = [
"instruction_display_layout",
"instruction_display_font_size",
"instruction_display_font_family",
"instruction_display_show_unsupported",
"instruction_display_unknown_ext",
"instruction_display_hide_premium",
"instruction_display_filter_platforms",
"instruction_generic_back",
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
menu_width = int(config.screen_width * 0.72)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_pause_games_menu(screen, selected_index):
from rgsx_settings import get_sources_mode
@@ -1594,6 +1711,23 @@ def draw_pause_games_menu(screen, selected_index):
back_txt = _("menu_back") if _ else "Back"
options = [history_txt, source_txt, update_txt, back_txt]
_draw_submenu_generic(screen, _("menu_games") if _ else "Games", options, selected_index)
instruction_keys = [
"instruction_games_history",
"instruction_games_source_mode",
"instruction_games_update_cache",
"instruction_generic_back",
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_pause_settings_menu(screen, selected_index):
from rgsx_settings import get_symlink_option
@@ -1618,6 +1752,23 @@ def draw_pause_settings_menu(screen, selected_index):
back_txt = _("menu_back") if _ else "Back"
options = [music_option, symlink_option, api_keys_txt, back_txt]
_draw_submenu_generic(screen, _("menu_settings_category") if _ else "Settings", options, selected_index)
instruction_keys = [
"instruction_settings_music",
"instruction_settings_symlink",
"instruction_settings_api_keys",
"instruction_generic_back",
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_pause_api_keys_status(screen):
screen.blit(OVERLAY, (0,0))
+23
View File
@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Premium-Systeme sichtbar"
,"submenu_display_font_family": "Schrift"
,"popup_font_family_changed": "Schrift geändert: {0}"
,"instruction_pause_language": "Sprache der Oberfläche ändern"
,"instruction_pause_controls": "Steuerungsübersicht ansehen oder neu zuordnen"
,"instruction_pause_display": "Layout, Schriften und Systemsichtbarkeit konfigurieren"
,"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_quit": "RGSX Anwendung beenden"
,"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen"
,"instruction_controls_remap": "Tasten / Buttons neu zuordnen"
,"instruction_generic_back": "Zum vorherigen Menü zurückkehren"
,"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten"
,"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen"
,"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln"
,"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden"
,"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten"
,"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}"
,"instruction_display_filter_platforms": "Manuell wählen welche Systeme sichtbar sind"
,"instruction_games_history": "Vergangene Downloads und Status anzeigen"
,"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln"
,"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren"
,"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren"
,"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten"
,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen"
}
+24 -1
View File
@@ -154,5 +154,28 @@
,"popup_hide_premium_on": "Premium systems hidden"
,"popup_hide_premium_off": "Premium systems visible"
,"submenu_display_font_family": "Font"
,"popup_font_family_changed": "Font changed: {0}"
,"popup_font_family_changed": "Font changed: {0}",
"instruction_pause_language": "Change the interface language",
"instruction_pause_controls": "View control layout or start remapping",
"instruction_pause_display": "Configure layout, fonts and system visibility",
"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_quit": "Exit the RGSX application"
,"instruction_controls_help": "Show full controller & keyboard reference"
,"instruction_controls_remap": "Change button / key bindings"
,"instruction_generic_back": "Return to the previous menu"
,"instruction_display_layout": "Cycle grid dimensions (columns × rows)"
,"instruction_display_font_size": "Adjust text scale for readability"
,"instruction_display_font_family": "Switch between available font families"
,"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg"
,"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg"
,"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}"
,"instruction_display_filter_platforms": "Manually choose which systems are visible"
,"instruction_games_history": "List past downloads and statuses"
,"instruction_games_source_mode": "Switch between RGSX or your own custom list source"
,"instruction_games_update_cache": "Redownload & refresh current games list"
,"instruction_settings_music": "Enable or disable background music playback"
,"instruction_settings_symlink": "Toggle using filesystem symlinks for installs"
,"instruction_settings_api_keys": "See detected premium provider API keys"
}
+23
View File
@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Sistemas Premium visibles"
,"submenu_display_font_family": "Fuente"
,"popup_font_family_changed": "Fuente cambiada: {0}"
,"instruction_pause_language": "Cambiar el idioma de la interfaz"
,"instruction_pause_controls": "Ver esquema de controles o remapear"
,"instruction_pause_display": "Configurar distribución, fuentes y visibilidad de sistemas"
,"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_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"
,"instruction_generic_back": "Volver al menú anterior"
,"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)"
,"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad"
,"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles"
,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg"
,"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg"
,"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}"
,"instruction_display_filter_platforms": "Elegir manualmente qué sistemas son visibles"
,"instruction_games_history": "Ver descargas pasadas y su estado"
,"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada"
,"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos"
,"instruction_settings_music": "Activar o desactivar música de fondo"
,"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones"
,"instruction_settings_api_keys": "Ver claves API premium detectadas"
}
+23
View File
@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Systèmes Premium visibles"
,"submenu_display_font_family": "Police"
,"popup_font_family_changed": "Police changée : {0}"
,"instruction_pause_language": "Changer la langue de l'interface"
,"instruction_pause_controls": "Afficher la configuration ou remapper"
,"instruction_pause_display": "Agencer l'affichage, polices et systèmes visibles"
,"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_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"
,"instruction_generic_back": "Revenir au menu précédent"
,"instruction_display_layout": "Changer les dimensions de la grille"
,"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité"
,"instruction_display_font_family": "Basculer entre les polices disponibles"
,"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg"
,"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg"
,"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}"
,"instruction_display_filter_platforms": "Choisir manuellement les systèmes visibles"
,"instruction_games_history": "Lister les téléchargements passés et leur statut"
,"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée"
,"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux"
,"instruction_settings_music": "Activer ou désactiver la lecture musicale"
,"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation"
,"instruction_settings_api_keys": "Voir les clés API détectées des services premium"
}
+23
View File
@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Sistemi Premium visibili"
,"submenu_display_font_family": "Font"
,"popup_font_family_changed": "Font cambiato: {0}"
,"instruction_pause_language": "Cambiare la lingua dell'interfaccia"
,"instruction_pause_controls": "Vedere schema controlli o avviare rimappatura"
,"instruction_pause_display": "Configurare layout, font e visibilità sistemi"
,"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_quit": "Uscire dall'applicazione RGSX"
,"instruction_controls_help": "Mostrare riferimento completo controller & tastiera"
,"instruction_controls_remap": "Modificare associazione pulsanti / tasti"
,"instruction_generic_back": "Tornare al menu precedente"
,"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)"
,"instruction_display_font_size": "Regolare dimensione testo per leggibilità"
,"instruction_display_font_family": "Cambiare famiglia di font disponibile"
,"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg"
,"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg"
,"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}"
,"instruction_display_filter_platforms": "Scegliere manualmente quali sistemi sono visibili"
,"instruction_games_history": "Elencare download passati e stato"
,"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata"
,"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi"
,"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo"
,"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni"
,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate"
}
+23
View File
@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Sistemas Premium visíveis"
,"submenu_display_font_family": "Fonte"
,"popup_font_family_changed": "Fonte alterada: {0}"
,"instruction_pause_language": "Alterar o idioma da interface"
,"instruction_pause_controls": "Ver esquema de controles ou iniciar remapeamento"
,"instruction_pause_display": "Configurar layout, fontes e visibilidade de sistemas"
,"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_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"
,"instruction_generic_back": "Voltar ao menu anterior"
,"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)"
,"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade"
,"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis"
,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg"
,"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg"
,"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}"
,"instruction_display_filter_platforms": "Escolher manualmente quais sistemas são visíveis"
,"instruction_games_history": "Listar downloads anteriores e status"
,"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada"
,"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos"
,"instruction_settings_music": "Ativar ou desativar música de fundo"
,"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações"
,"instruction_settings_api_keys": "Ver chaves API premium detectadas"
}
+1
View File
@@ -1233,6 +1233,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
cancel_events.pop(task_id, None)
logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}")
return result[0], result[1]
def is_1fichier_url(url):
"""Détecte si l'URL est un lien 1fichier."""
return "1fichier.com" in url
+310 -26
View File
@@ -24,6 +24,53 @@ from rgsx_settings import get_sources_zip_url
logger = logging.getLogger("rgsx.cli")
# Unified size display helper: preserve pre-formatted locale strings (MiB, GiB, Go, Mo, Ko, MB, KB, bytes).
# If numeric (int/float or pure digit string), convert to binary units with suffix B, KiB, MiB, GiB.
# Otherwise return original string.
def display_size(val):
try:
if val is None:
return ''
if isinstance(val, (list, tuple)):
return ''
s = str(val).strip()
if not s:
return ''
lower = s.lower()
# Already human formatted (English or French common units or contains a space + unit token)
known_tokens = ("mib", "gib", "kib", "kb", "mb", "gb", "bytes", " b", " mo", " go", " ko", "mb ", "gb ")
if any(tok in lower for tok in known_tokens):
return s
# Pure numeric => treat as bytes
import re as _re
if _re.fullmatch(r"\d+", s):
b = float(s)
else:
# Leading numeric? if not, return original
m = _re.match(r"^([0-9]+(?:\.[0-9]+)?)", s)
if not m:
return s
# If trailing unit unknown, assume already human string
if len(s) > len(m.group(0)):
return s
b = float(m.group(1))
if b < 1024:
return f"{int(b)} B"
kib = b / 1024
if kib < 1024:
return f"{kib:.2f} KiB"
mib = kib / 1024
if mib < 1024:
return f"{mib:.2f} MiB"
gib = mib / 1024
return f"{gib:.2f} GiB"
except Exception:
try:
return str(val)
except Exception:
return ''
def setup_logging(verbose: bool):
level = logging.DEBUG if verbose else logging.WARNING
logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
@@ -153,9 +200,26 @@ def cmd_platforms(args):
if getattr(args, 'json', False):
print(json.dumps(items, ensure_ascii=False, indent=2))
else:
# Hint before table
print("hint: you can use either the exact platform name or folder in --platform (e.g. 'SNK Neo Geo' or 'neogeo')")
# ASCII table with fixed widths: name=35, folder=15
NAME_W = 35
FOLDER_W = 15
def fmt_cell(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
border = "+" + "-" * (NAME_W + 2) + "+" + "-" * (FOLDER_W + 2) + "+"
header = f"| {'Platform Name'.ljust(NAME_W)} | {'Folder'.ljust(FOLDER_W)} |"
print(border)
print(header)
print(border)
for it in items:
# name TAB folder (folder may be empty for BIOS/virtual)
print(f"{it['name']}\t{it['folder']}")
row = f"| {fmt_cell(it['name'], NAME_W)} | {fmt_cell(it['folder'], FOLDER_W)} |"
print(row)
print(border)
def _resolve_platform(sources, platform_name: str):
@@ -188,13 +252,100 @@ def cmd_games(args):
or args.platform
)
games = load_games(platform_id)
# Fuzzy ranking similar to download suggestions when --search provided
if args.search:
q = args.search.lower()
games = [g for g in games if q in (g[0] or '').lower()]
query_raw = args.search.strip()
def _strip_ext(name: str) -> str:
try:
base, _ = os.path.splitext(name)
return base
except Exception:
return name
def _tokens(s: str) -> list[str]:
return re.findall(r"[a-z0-9]+", s.lower())
q_lower = query_raw.lower()
q_no_ext = _strip_ext(query_raw).lower()
q_tokens = _tokens(query_raw)
suggestions = [] # (priority, score, game_obj)
# 1) Substring match (full or sans extension) priority 0, score = position
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
if not title:
continue
t_lower = title.lower()
t_no_ext = _strip_ext(t_lower)
pos_full = t_lower.find(q_lower) if q_lower else -1
pos_noext = t_no_ext.find(q_no_ext) if q_no_ext else -1
if pos_full != -1 or pos_noext != -1:
pos = pos_full if pos_full != -1 else pos_noext
suggestions.append((0, max(0, pos), g))
# Helper for ordered gap score
def ordered_gap_score(qt: list[str], tt: list[str]):
pos = []
start = 0
for tok in qt:
try:
i = next(i for i in range(start, len(tt)) if tt[i] == tok)
except StopIteration:
return None
pos.append(i)
start = i + 1
gap = (pos[-1] - pos[0]) - (len(qt) - 1)
return max(0, gap)
# 2) Ordered non-contiguous tokens (priority 1)
if q_tokens:
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
if not title:
continue
tt = _tokens(title)
score = ordered_gap_score(q_tokens, tt)
if score is not None:
suggestions.append((1, score, g))
# 3) All tokens present, any order (priority 2), score = token set size
if q_tokens:
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
if not title:
continue
t_tokens = set(_tokens(title))
if all(tok in t_tokens for tok in q_tokens):
suggestions.append((2, len(t_tokens), g))
# Deduplicate by title keeping best (lowest priority, then score)
best = {}
for prio, score, g in suggestions:
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
key = title.lower()
cur = best.get(key)
if cur is None or (prio, score) < (cur[0], cur[1]):
best[key] = (prio, score, g)
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower()))
games = [g for _, _, g in ranked]
# Table: Name (60) | Size (12) to allow "xxxx.xx MiB"
NAME_W = 60
SIZE_W = 12
def trunc(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
border = "+" + "-" * (NAME_W + 2) + "+" + "-" * (SIZE_W + 2) + "+"
header = f"| {'Game Title'.ljust(NAME_W)} | {'Size'.ljust(SIZE_W)} |"
print(border)
print(header)
print(border)
for g in games:
# games items can be (name, url) or (name, url, size)
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
print(title)
size_val = ''
if isinstance(g, (list, tuple)) and len(g) >= 3:
size_val = display_size(g[2])
row = f"| {trunc(title, NAME_W)} | {trunc(size_val, SIZE_W)} |"
print(row)
print(border)
if args.search and not games:
print("No results for search.")
def cmd_history(args):
@@ -382,10 +533,37 @@ def cmd_download(args):
interactive = bool(getattr(args, 'interactive', False))
if interactive:
print("Select a match to download:")
# Tableau formaté: # (4) | Title (60) | Size (12)
NUM_W = 4
TITLE_W = 60
SIZE_W = 12
def trunc(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
# Use shared display_size
border = "+" + "-" * (NUM_W + 2) + "+" + "-" * (TITLE_W + 2) + "+" + "-" * (SIZE_W + 2) + "+"
header = f"| {'#'.ljust(NUM_W)} | {'Title'.ljust(TITLE_W)} | {'Size'.ljust(SIZE_W)} |"
print(border)
print(header)
print(border)
for i, s in enumerate(shown, start=1):
print(f" {i}. {s[2]}")
title = s[2]
size_val = ''
size_raw = None
for g in games:
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
size_raw = g[2]
break
if size_raw is not None:
size_val = display_size(size_raw)
row = f"| {str(i).ljust(NUM_W)} | {trunc(title, TITLE_W)} | {trunc(size_val, SIZE_W)} |"
print(row)
print(border)
if len(suggestions) > limit:
print(f" ... and {len(suggestions) - limit} more not shown")
print(f"... {len(suggestions) - limit} more not shown")
try:
choice = input("Enter number (or press Enter to cancel): ").strip()
except EOFError:
@@ -400,15 +578,41 @@ def cmd_download(args):
pass
if not match:
print("Here are potential matches (use the exact title with --game):")
NUM_W = 4
TITLE_W = 60
SIZE_W = 12
def trunc(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
# Use shared display_size
border = "+" + "-" * (NUM_W + 2) + "+" + "-" * (TITLE_W + 2) + "+" + "-" * (SIZE_W + 2) + "+"
header = f"| {'#'.ljust(NUM_W)} | {'Title'.ljust(TITLE_W)} | {'Size'.ljust(SIZE_W)} |"
print(border)
print(header)
print(border)
for i, s in enumerate(shown, start=1):
print(f" {i}. {s[2]}")
title = s[2]
size_val = ''
size_raw = None
for g in games:
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
size_raw = g[2]
break
if size_raw is not None:
size_val = display_size(size_raw)
row = f"| {str(i).ljust(NUM_W)} | {trunc(title, TITLE_W)} | {trunc(size_val, SIZE_W)} |"
print(row)
print(border)
if len(suggestions) > limit:
print(f" ... and {len(suggestions) - limit} more")
print("Tip: list games with: python rgsx_cli.py games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
print(f"... {len(suggestions) - limit} more")
print("Tip: list games with: games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
sys.exit(3)
else:
print("No similar titles found.")
print("Tip: list games with: python rgsx_cli.py games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
print("Tip: list games with: games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
sys.exit(3)
title, url = match
@@ -454,33 +658,109 @@ def cmd_download(args):
sys.exit(exit_code)
def interactive_loop(parser):
"""Simple REPL so user can run multiple subcommands without retyping python rgsx_cli.py.
Rules:
- Empty line: ignore
- help / ?: show help
- exit / quit: leave loop
- Global flags like --verbose can be set per command; verbose persists for session once set.
"""
persistent_verbose = False
print("RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.")
while True:
try:
line = input("rgsx> ").strip()
except EOFError:
print()
break
except KeyboardInterrupt:
print()
break
if not line:
continue
if line in {"exit", "quit"}:
break
if line in {"help", "?"}:
parser.print_help()
continue
# Tokenize respecting simple quotes
try:
import shlex
argv = shlex.split(line)
except Exception:
argv = line.split()
# Inject persistent verbose if previously enabled and not explicitly disabled
if persistent_verbose and "--verbose" not in argv:
argv.insert(0, "--verbose")
try:
args = parser.parse_args(argv)
except SystemExit as se:
# argparse already printed error; continue loop
continue
# Update persistent verbose state
if getattr(args, 'verbose', False):
persistent_verbose = True
# Dispatch
if not getattr(args, 'cmd', None):
# If user typed e.g. just global flags
print("No command provided. Type 'help' to list commands.")
continue
setup_logging(getattr(args, 'verbose', False))
# Global force-update handling (duplicate minimal logic to avoid leaving loop early)
if getattr(args, 'force_update', False):
try:
if os.path.exists(config.SOURCES_FILE):
os.remove(config.SOURCES_FILE)
except Exception:
pass
try:
shutil.rmtree(config.GAMES_FOLDER, ignore_errors=True)
except Exception:
pass
try:
shutil.rmtree(config.IMAGES_FOLDER, ignore_errors=True)
except Exception:
pass
ok = ensure_data_present(verbose=True)
if not ok:
print("force-update failed; aborting command.")
continue
try:
args.func(args)
except SystemExit:
# Subcommand may sys.exit on errors; swallow in REPL
continue
except Exception as e:
print(f"Error: {e}")
continue
def build_parser():
p = argparse.ArgumentParser(prog="rgsx-cli", description="RGSX headless CLI")
p.add_argument("--verbose", action="store_true", help="Verbose logging")
p.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sub = p.add_subparsers(dest="cmd")
sp = sub.add_parser("platforms", help="List available platforms")
sp = sub.add_parser("platforms", aliases=["p"], help="List available platforms")
sp.add_argument("--json", action="store_true", help="Output JSON with name and folder")
# Also accept global flags after the subcommand
sp.add_argument("--verbose", action="store_true", help="Verbose logging")
sp.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sp.set_defaults(func=cmd_platforms)
sg = sub.add_parser("games", help="List games for a platform")
sg.add_argument("--platform", required=True, help="Platform name or key")
sg.add_argument("--search", help="Filter by name contains")
# Also accept global flags after the subcommand
sg = sub.add_parser("games", aliases=["g"], help="List games for a platform")
sg.add_argument("--platform", "--p", "-p", required=True, help="Platform name or key")
sg.add_argument("--search", "--s", "-s", help="Filter by name contains")
sg.add_argument("--verbose", action="store_true", help="Verbose logging")
sg.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sg.set_defaults(func=cmd_games)
sd = sub.add_parser("download", help="Download a game by title")
sd.add_argument("--platform", required=True)
sd.add_argument("--game", required=True)
sd.add_argument("--force", action="store_true", help="Override unsupported extension warning")
sd = sub.add_parser("download", aliases=["dl"], help="Download a game by title")
sd.add_argument("--platform", "--p", "-p", required=True)
sd.add_argument("--game", "--g", "-g", required=True)
sd.add_argument("--force", "-f", action="store_true", help="Override unsupported extension warning")
sd.add_argument("--interactive", "-i", action="store_true", help="Prompt to choose from matches when no exact title is found")
# Also accept global flags after the subcommand
sd.add_argument("--verbose", action="store_true", help="Verbose logging")
sd.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sd.set_defaults(func=cmd_download)
@@ -488,13 +768,11 @@ def build_parser():
sh = sub.add_parser("history", help="Show recent history")
sh.add_argument("--tail", type=int, default=50, help="Last N entries")
sh.add_argument("--json", action="store_true")
# Also accept global flags after the subcommand
sh.add_argument("--verbose", action="store_true", help="Verbose logging")
sh.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sh.set_defaults(func=cmd_history)
sc = sub.add_parser("clear-history", help="Clear history")
# Also accept global flags after the subcommand
sc = sub.add_parser("clear-history", aliases=["clear"], help="Clear history")
sc.add_argument("--verbose", action="store_true", help="Verbose logging")
sc.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sc.set_defaults(func=cmd_clear_history)
@@ -507,6 +785,12 @@ def main(argv=None):
# Force headless mode for CLI
os.environ.setdefault("RGSX_HEADLESS", "1")
parser = build_parser()
if not argv:
# Start interactive mode
try:
interactive_loop(parser)
finally:
return
args = parser.parse_args(argv)
setup_logging(args.verbose)