v2.0.0.0 - important update to game list structure and reading . you can now add a json and it will be automatically added to gamelist. Added menu to filter systems showing in main screen. Add size showing if available on game list view.

add custom game list source (advanced users only
This commit is contained in:
skymike03
2025-09-05 01:49:06 +02:00
parent 1c923086cf
commit 62d9bba85d
14 changed files with 685 additions and 238 deletions

View File

@@ -265,6 +265,12 @@ async def main():
#logger.debug(f"Événement transmis à handle_controls dans pause_menu: {event.type}")
continue
# Gestion des événements pour le menu de filtrage des plateformes
if config.menu_state == "filter_platforms":
action = handle_controls(event, sources, joystick, screen)
config.needs_redraw = True
continue
if config.menu_state == "accessibility_menu":
from accessibility import handle_accessibility_events
if handle_accessibility_events(event):
@@ -342,9 +348,14 @@ async def main():
logger.debug("Action quit détectée, arrêt de l'application")
elif action == "download" and config.menu_state == "game" and config.filtered_games:
game = config.filtered_games[config.current_game]
game_name = game[0] if isinstance(game, (list, tuple)) else game
platform = config.platforms[config.current_platform]["name"] # Utiliser le nom de la plateforme
url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None
if isinstance(game, (list, tuple)):
game_name = game[0]
url = game[1] if len(game) > 1 else None
else: # fallback str
game_name = str(game)
url = None
# Nouveau schéma: config.platforms contient déjà platform_name (string)
platform = config.platforms[config.current_platform]
if url:
logger.debug(f"Vérification pour {game_name}, URL: {url}")
# Ajouter une entrée temporaire à l'historique
@@ -422,8 +433,12 @@ async def main():
platform = entry["platform"]
game_name = entry["game_name"]
for game in config.games:
if game[0] == game_name and config.platforms[config.current_platform] == platform:
url = game[1]
if isinstance(game, (list, tuple)) and game and game[0] == game_name and config.platforms[config.current_platform] == platform:
url = game[1] if len(game) > 1 else None
else:
continue
if not url:
continue
logger.debug(f"Vérification pour retéléchargement de {game_name}, URL: {url}")
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
@@ -604,6 +619,9 @@ async def main():
elif config.menu_state == "pause_menu":
draw_pause_menu(screen, config.selected_option)
#logger.debug("Rendu de draw_pause_menu")
elif config.menu_state == "filter_platforms":
from display import draw_filter_platforms_menu
draw_filter_platforms_menu(screen)
elif config.menu_state == "controls_help":
draw_controls_help(screen, config.previous_menu_state)
elif config.menu_state == "history":

BIN
ports/RGSX/assets/unrar.exe Normal file

Binary file not shown.

BIN
ports/RGSX/assets/xdvdfs Normal file

Binary file not shown.

View File

@@ -2,10 +2,10 @@ import pygame # type: ignore
import os
import logging
import platform
from rgsx_settings import load_rgsx_settings, save_rgsx_settings, migrate_old_settings
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
# Version actuelle de l'application
app_version = "1.9.9.4"
app_version = "2.0.0.0"
def get_operating_system():
"""Renvoie le nom du système d'exploitation."""
@@ -85,28 +85,22 @@ GAMELISTXML = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_F
UPDATE_FOLDER = os.path.join(APP_FOLDER, "update")
LANGUAGES_FOLDER = os.path.join(APP_FOLDER, "languages")
JSON_EXTENSIONS = os.path.join(APP_FOLDER, "rom_extensions.json")
MUSIC_FOLDER = os.path.join(APP_FOLDER, "assets", "music")
#Dossier /saves/ports/rgsx
IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images", "systemes")
IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images")
GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games")
SOURCES_FILE = os.path.join(SAVE_FOLDER, "sources.json")
SOURCES_FILE = os.path.join(SAVE_FOLDER, "systems_list.json")
CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json")
HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json")
# Nouveau fichier unifié pour les paramètres RGSX
API_KEY_1FICHIER = os.path.join(SAVE_FOLDER, "1fichierAPI.txt")
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
# Anciens chemins des fichiers de config (conservés temporairement pour la migration)
ACCESSIBILITY_FOLDER = os.path.join(SAVE_FOLDER, "accessibility.json")
LANGUAGE_CONFIG_PATH = os.path.join(SAVE_FOLDER, "language.json")
MUSIC_CONFIG_PATH = os.path.join(SAVE_FOLDER, "music_config.json")
SYMLINK_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "symlink_settings.json")
# URL
OTA_SERVER_URL = "https://retrogamesets.fr/softs/"
OTA_VERSION_ENDPOINT = os.path.join(OTA_SERVER_URL, "version.json")
OTA_UPDATE_ZIP = os.path.join(OTA_SERVER_URL, "RGSX.zip")
OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "rgsx-data.zip")
OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "test.zip")
#CHEMINS DES EXECUTABLES
UNRAR_EXE = os.path.join(APP_FOLDER,"assets", "unrar.exe")
@@ -197,6 +191,12 @@ 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
# --- Filtre plateformes (UI) ---
selected_filter_index = 0 # index dans la liste visible triée
filter_platforms_scroll_offset = 0 # défilement si liste longue
filter_platforms_dirty = False # indique si modifications non sauvegardées
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
GRID_COLS = 3 # Number of columns in the platform grid
GRID_ROWS = 4 # Number of rows in the platform grid

View File

@@ -32,7 +32,7 @@ VALID_STATES = [
"platform", "game", "confirm_exit",
"extension_warning", "pause_menu", "controls_help", "history", "controls_mapping",
"redownload_game_cache", "restart_popup", "error", "loading", "confirm_clear_history",
"language_select"
"language_select", "filter_platforms"
]
def validate_menu_state(state):
@@ -1036,7 +1036,13 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "accessibility_menu"
config.needs_redraw = True
logger.debug("Passage au menu accessibilité")
elif config.selected_option == 5: # Source toggle
elif config.selected_option == 5: # Filter platforms
# Ne pas écraser previous_menu_state; il référence l'état avant l'ouverture du pause menu
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
elif config.selected_option == 6: # Source toggle (index shifted by new option)
try:
from rgsx_settings import get_sources_mode, set_sources_mode
current_mode = get_sources_mode()
@@ -1052,13 +1058,13 @@ def handle_controls(event, sources, joystick, screen):
logger.info(f"Changement du mode des sources vers {new_mode}")
except Exception as e:
logger.error(f"Erreur changement mode sources: {e}")
elif config.selected_option == 6: # Redownload game cache
elif config.selected_option == 7: # Redownload game cache
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.menu_state = "redownload_game_cache"
config.redownload_confirm_selection = 0
config.needs_redraw = True
logger.debug(f"Passage à redownload_game_cache depuis pause_menu")
elif config.selected_option == 7: # Music toggle
elif config.selected_option == 8: # Music toggle
config.music_enabled = not config.music_enabled
save_music_config()
if config.music_enabled:
@@ -1070,7 +1076,7 @@ def handle_controls(event, sources, joystick, screen):
pygame.mixer.music.stop()
config.needs_redraw = True
logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via menu pause")
elif config.selected_option == 8: # Symlink option
elif config.selected_option == 9: # Symlink option
from rgsx_settings import set_symlink_option, get_symlink_option
current_status = get_symlink_option()
success, message = set_symlink_option(not current_status)
@@ -1078,7 +1084,7 @@ def handle_controls(event, sources, joystick, screen):
config.popup_timer = 3000 if success else 5000
config.needs_redraw = True
logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via menu pause")
elif config.selected_option == 9: # Quit
elif config.selected_option == 10: # Quit
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.menu_state = "confirm_exit"
config.confirm_selection = 0
@@ -1117,8 +1123,12 @@ def handle_controls(event, sources, joystick, screen):
config.pending_download = None
if os.path.exists(config.SOURCES_FILE):
try:
os.remove(config.SOURCES_FILE)
logger.debug("Fichier sources.json supprimé avec succès")
if os.path.exists(config.SOURCES_FILE):
os.remove(config.SOURCES_FILE)
logger.debug("Fichier system_list.json supprimé avec succès")
if os.path.exists(config.SAVE_FOLDER + "/sources.json"):
os.remove(config.SAVE_FOLDER + "/sources.json")
logger.debug("Fichier sources.json supprimé avec succès")
if os.path.exists(config.GAMES_FOLDER):
shutil.rmtree(config.GAMES_FOLDER)
logger.debug("Dossier games supprimé avec succès")
@@ -1203,6 +1213,71 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug("Annulation de la sélection de langue, retour au menu pause")
# Menu filtre plateformes
elif config.menu_state == "filter_platforms":
total_items = len(config.filter_platforms_selection)
action_buttons = 4
extended_max = total_items + action_buttons - 1
if is_input_matched(event, "up"):
if config.selected_filter_index > 0:
config.selected_filter_index -= 1
config.needs_redraw = True
else:
# Wrap vers les boutons (premier bouton) depuis le haut
if total_items > 0:
config.selected_filter_index = total_items
config.needs_redraw = True
elif is_input_matched(event, "down"):
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
else:
# Wrap retour en haut de la liste
config.selected_filter_index = 0
config.needs_redraw = True
elif is_input_matched(event, "left"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index > total_items:
config.selected_filter_index -= 1
config.needs_redraw = True
# sinon ignorer
elif is_input_matched(event, "right"):
if config.selected_filter_index >= total_items:
if config.selected_filter_index < extended_max:
config.selected_filter_index += 1
config.needs_redraw = True
# sinon ignorer
elif is_input_matched(event, "confirm"):
if config.selected_filter_index < total_items:
name, hidden = config.filter_platforms_selection[config.selected_filter_index]
config.filter_platforms_selection[config.selected_filter_index] = (name, not hidden)
config.filter_platforms_dirty = True
config.needs_redraw = True
else:
btn_idx = config.selected_filter_index - total_items
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
from utils import load_sources
settings = load_rgsx_settings()
if btn_idx == 0: # all visible
config.filter_platforms_selection = [(n, False) for n, _ in config.filter_platforms_selection]
config.filter_platforms_dirty = True
elif btn_idx == 1: # none visible
config.filter_platforms_selection = [(n, True) for n, _ in config.filter_platforms_selection]
config.filter_platforms_dirty = True
elif btn_idx == 2: # apply
hidden_list = [n for n, h in config.filter_platforms_selection if h]
settings["hidden_platforms"] = hidden_list
save_rgsx_settings(settings)
load_sources()
config.filter_platforms_dirty = False
config.menu_state = "pause_menu"
elif btn_idx == 3: # back
config.menu_state = "pause_menu"
config.needs_redraw = True
elif is_input_matched(event, "cancel"):
config.menu_state = "pause_menu"
config.needs_redraw = True
# Gestion des relâchements de touches
if event.type == pygame.KEYUP:

View File

@@ -83,8 +83,20 @@ def draw_stylized_button(screen, text, x, y, width, height, selected=False):
# Transition d'image lors de la sélection d'un système
def draw_validation_transition(screen, platform_index):
"""Affiche une animation de transition fluide pour la sélection dune plateforme."""
platform_dict = config.platform_dicts[platform_index]
"""Affiche une animation de transition fluide pour la sélection dune plateforme.
Utilise le mapping par nom pour éviter les décalages d'image si l'ordre d'affichage
diffère de l'ordre de stockage."""
# Récupérer le nom affiché correspondant à l'index trié
if platform_index < 0 or platform_index >= len(config.platforms):
return
platform_name = config.platforms[platform_index]
platform_dict = getattr(config, 'platform_dict_by_name', {}).get(platform_name)
if not platform_dict:
# Fallback index direct si mapping absent
try:
platform_dict = config.platform_dicts[platform_index]
except Exception:
return
image = load_system_image(platform_dict)
if not image:
return
@@ -468,8 +480,16 @@ def draw_platform_grid(screen):
scale_base = 1.5 if is_selected else 1.0
scale = scale_base + pulse if is_selected else scale_base
platform_dict = config.platform_dicts[idx]
platform_id = platform_dict.get("platform", str(idx))
# Récupération robuste du dict via nom
display_name = config.platforms[idx]
platform_dict = getattr(config, 'platform_dict_by_name', {}).get(display_name)
if not platform_dict:
# Fallback index brut
if idx < len(config.platform_dicts):
platform_dict = config.platform_dicts[idx]
else:
continue
platform_id = platform_dict.get("platform_name") or platform_dict.get("platform") or display_name
# Utiliser le cache d'images pour éviter de recharger/redimensionner à chaque frame
cache_key = f"{platform_id}_{scale:.2f}"
@@ -570,15 +590,17 @@ def draw_game_list(screen):
return
line_height = config.small_font.get_height() + 10
header_height = line_height # hauteur de l'en-tête identique à une ligne
margin_top_bottom = 20
extra_margin_top = 20
extra_margin_bottom = 60
title_height = config.title_font.get_height() + 20
available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom
items_per_page = available_height // line_height
# Réserver de l'espace pour l'en-tête (header_height)
available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom - header_height
items_per_page = max(1, available_height // line_height)
rect_height = items_per_page * line_height + 2 * margin_top_bottom
rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom
rect_width = int(0.95 * config.screen_width)
rect_x = (config.screen_width - rect_width) // 2
rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2
@@ -622,21 +644,60 @@ def draw_game_list(screen):
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)
# Largeur colonne taille (15%) mini 120px, reste pour nom
size_col_width = max(120, int(rect_width * 0.15))
name_col_width = rect_width - 40 - size_col_width # padding horizontal 40
# ---- En-tête ----
header_name = _("game_header_name")
header_size = _("game_header_size")
header_y_center = rect_y + margin_top_bottom + header_height // 2
# Nom aligné gauche
header_name_surface = config.small_font.render(header_name, True, THEME_COLORS["text"])
header_name_rect = header_name_surface.get_rect()
header_name_rect.midleft = (rect_x + 20, header_y_center)
# Taille alignée droite
header_size_surface = config.small_font.render(header_size, True, THEME_COLORS["text"])
header_size_rect = header_size_surface.get_rect()
header_size_rect.midright = (rect_x + rect_width - 20, header_y_center)
screen.blit(header_name_surface, header_name_rect)
screen.blit(header_size_surface, header_size_rect)
# Ligne de séparation sous l'en-tête
separator_y = rect_y + margin_top_bottom + header_height
pygame.draw.line(screen, THEME_COLORS["border"], (rect_x + 20, separator_y), (rect_x + rect_width - 20, separator_y), 2)
# Position de départ des lignes après l'en-tête
list_start_y = rect_y + margin_top_bottom + header_height
for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))):
game_name = games[i][0] if isinstance(games[i], (list, tuple)) else games[i]
item = games[i]
if isinstance(item, (list, tuple)) and item:
game_name = item[0]
size_val = item[2] if len(item) > 2 else None
else:
game_name = str(item)
size_val = None
size_text = size_val if (isinstance(size_val, str) and size_val.strip()) else "N/A"
is_marked = i in getattr(config, 'selected_games', set())
# Couleur verte si jeu sous curseur OU marqué en multi-sélection
color = THEME_COLORS["fond_lignes"] if (i == config.current_game or is_marked) else THEME_COLORS["text"]
# Préfixe ASCII pour distinguer les jeux marqués (éviter collision glyphes)
prefix = "[X] " if is_marked else " "
game_text = truncate_text_middle(prefix + game_name, config.small_font, rect_width - 40, is_filename=False)
text_surface = config.small_font.render(game_text, True, color)
text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + (i - config.scroll_offset) * line_height + line_height // 2))
truncated_name = truncate_text_middle(prefix + game_name, config.small_font, name_col_width, is_filename=False)
name_surface = config.small_font.render(truncated_name, True, color)
size_surface = config.small_font.render(size_text, True, THEME_COLORS["text"])
row_center_y = list_start_y + (i - config.scroll_offset) * line_height + line_height // 2
# Position nom (aligné à gauche dans la boite)
name_rect = name_surface.get_rect()
name_rect.midleft = (rect_x + 20, row_center_y)
size_rect = size_surface.get_rect()
size_rect.midright = (rect_x + rect_width - 20, row_center_y)
if i == config.current_game:
glow_surface = pygame.Surface((text_rect.width + 20, text_rect.height + 10), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (10, 5, text_rect.width, text_rect.height), border_radius=8)
screen.blit(glow_surface, (text_rect.left - 10, text_rect.top - 5))
screen.blit(text_surface, text_rect)
glow_width = rect_width - 40
glow_height = name_rect.height + 10
glow_surface = pygame.Surface((glow_width, glow_height), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, glow_width, glow_height), border_radius=8)
screen.blit(glow_surface, (rect_x + 20, row_center_y - glow_height // 2))
screen.blit(name_surface, name_rect)
screen.blit(size_surface, size_rect)
if len(games) > items_per_page:
try:
@@ -1202,6 +1263,7 @@ def draw_pause_menu(screen, selected_option):
_("menu_history"), # 2
_("menu_language"), # 3
_("menu_accessibility"), # 4
_("menu_filter_platforms"), # 5 new filter option
f"{_('menu_games_source_prefix')}: {source_label}", # 5
_("menu_redownload_cache"), # 6
music_option, # 7
@@ -1232,6 +1294,116 @@ def draw_pause_menu(screen, selected_option):
# Stocker le nombre total d'options pour la navigation dynamique
config.pause_menu_total_options = len(options)
def draw_filter_platforms_menu(screen):
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""
from rgsx_settings import load_rgsx_settings
screen.blit(OVERLAY, (0, 0))
settings = load_rgsx_settings()
hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set()
# Initialiser la copie de travail si vide ou taille différente
if not config.filter_platforms_selection or len(config.filter_platforms_selection) != len(config.platform_dicts):
# Liste alphabétique complète (sans filtrer hidden existant)
all_names = sorted([d.get("platform_name", "") for d in config.platform_dicts if d.get("platform_name")])
config.filter_platforms_selection = [(name, name in hidden) for name in all_names]
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.filter_platforms_dirty = False
title_text = _("filter_platforms_title")
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
title_rect_inflated = title_rect.inflate(80, 40)
title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10)
pygame.draw.rect(screen, THEME_COLORS["button_idle"], title_rect_inflated, border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
# Zone liste
list_width = int(config.screen_width * 0.7)
list_height = int(config.screen_height * 0.6)
list_x = (config.screen_width - list_width) // 2
list_y = title_rect_inflated.bottom + 20
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (list_x, list_y, list_width, list_height), border_radius=12)
pygame.draw.rect(screen, THEME_COLORS["border"], (list_x, list_y, list_width, list_height), 2, border_radius=12)
line_height = config.small_font.get_height() + 8
visible_items = list_height // line_height - 1 # laisser un peu d'espace bas
total_items = len(config.filter_platforms_selection)
if config.selected_filter_index < 0:
config.selected_filter_index = 0
# Ne pas forcer la réduction si on est sur les boutons (indices >= total_items)
# Laisser controls.py gérer la borne max étendue
# Ajuster scroll
if config.selected_filter_index < config.filter_platforms_scroll_offset:
config.filter_platforms_scroll_offset = config.selected_filter_index
elif config.selected_filter_index >= config.filter_platforms_scroll_offset + visible_items:
config.filter_platforms_scroll_offset = config.selected_filter_index - visible_items + 1
# Dessiner items
for i in range(config.filter_platforms_scroll_offset, min(config.filter_platforms_scroll_offset + visible_items, total_items)):
name, is_hidden = config.filter_platforms_selection[i]
idx_on_screen = i - config.filter_platforms_scroll_offset
y_center = list_y + 10 + idx_on_screen * line_height + line_height // 2
selected = (i == config.selected_filter_index)
checkbox = "[ ]" if is_hidden else "[X]" # inversé: coché signifie visible
# Correction: on veut [X] si visible => is_hidden False
checkbox = "[X]" if not is_hidden else "[ ]"
display_text = f"{checkbox} {name}"
color = THEME_COLORS["fond_lignes"] if selected else THEME_COLORS["text"]
text_surface = config.small_font.render(display_text, True, color)
text_rect = text_surface.get_rect(midleft=(list_x + 20, y_center))
if selected:
glow_surface = pygame.Surface((list_width - 40, line_height), pygame.SRCALPHA)
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, list_width - 40, line_height), border_radius=8)
screen.blit(glow_surface, (list_x + 20, y_center - line_height // 2))
screen.blit(text_surface, text_rect)
# Scrollbar
if total_items > visible_items:
scroll_height = int((visible_items / total_items) * (list_height - 20))
scroll_y = int((config.filter_platforms_scroll_offset / max(1, total_items - visible_items)) * (list_height - 20 - scroll_height))
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 25, list_y + 10 + scroll_y, 10, scroll_height), border_radius=4)
# Boutons d'action
btn_width = 220
btn_height = int(config.screen_height * 0.0463)
spacing = 30
buttons_y = list_y + list_height + 20
center_x = config.screen_width // 2
actions = [
("filter_all", -2),
("filter_none", -3),
("filter_apply", -4),
("filter_back", -5)
]
# Indice spécial sélection boutons quand selected_filter_index >= total_items
extra_index_base = total_items
# Ajuster selected_filter_index max pour inclure boutons
extended_max = total_items + len(actions) - 1
if config.selected_filter_index > extended_max:
config.selected_filter_index = extended_max
for idx, (key, offset) in enumerate(actions):
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
is_selected = (config.selected_filter_index == total_items + idx)
label = _(key)
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
# Infos bas
hidden_count = sum(1 for _, h in config.filter_platforms_selection if h)
visible_count = total_items - hidden_count
info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_items)
info_surface = config.small_font.render(info_text, True, THEME_COLORS["text"])
info_rect = info_surface.get_rect(center=(config.screen_width // 2, buttons_y + btn_height + 30))
screen.blit(info_surface, info_rect)
if config.filter_platforms_dirty:
dirty_text = _("filter_unsaved_warning")
dirty_surface = config.small_font.render(dirty_text, True, THEME_COLORS["warning_text"])
dirty_rect = dirty_surface.get_rect(center=(config.screen_width // 2, info_rect.bottom + 25))
screen.blit(dirty_surface, dirty_rect)
# Menu aide contrôles
def draw_controls_help(screen, previous_state):
"""Affiche la liste des contrôles (aide) avec mise en page adaptative."""

View File

@@ -26,7 +26,7 @@
"error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben",
"error_api_key_extended": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei /userdata/saves/ports/rgsx/1fichierAPI.txt einfügen. Öffne die Datei in einem Texteditor und füge den API-Schlüssel ein",
"error_invalid_download_data": "Ungültige Downloaddaten",
"error_delete_sources": "Fehler beim Löschen der Datei sources.json oder Ordner",
"error_delete_sources": "Fehler beim Löschen der Datei systems_list.json oder Ordner",
"error_extension": "Nicht unterstützte Erweiterung oder Downloadfehler",
"error_no_download": "Keine Downloads ausstehend.",
@@ -37,6 +37,8 @@
"game_count": "{0} ({1} Spiele)",
"game_filter": "Aktiver Filter: {0}",
"game_search": "Filtern: {0}",
"game_header_name": "Name",
"game_header_size": "Größe",
"history_title": "Downloads ({0})",
"history_empty": "Keine Downloads im Verlauf",
@@ -78,6 +80,14 @@
"menu_music_toggle": "Musik ein/aus",
"menu_music_enabled": "Musik aktiviert: {0}",
"menu_music_disabled": "Musik deaktiviert",
"menu_filter_platforms": "Systeme filtern",
"filter_platforms_title": "Systemsichtbarkeit",
"filter_all": "Alle anzeigen",
"filter_none": "Alle ausblenden",
"filter_apply": "Anwenden",
"filter_back": "Zurück",
"filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}",
"filter_unsaved_warning": "Ungespeicherte Änderungen",
"menu_quit": "Beenden",
"button_yes": "Ja",

View File

@@ -26,7 +26,7 @@
"error_api_key": "Please enter your API key (premium only) in the file {0}",
"error_api_key_extended": "Please enter your API key (premium only) in the file /userdata/saves/ports/rgsx/1fichierAPI.txt by opening it in a text editor and pasting your API key",
"error_invalid_download_data": "Invalid download data",
"error_delete_sources": "Error deleting sources.json file or folders",
"error_delete_sources": "Error deleting systems_list.json file or folders",
"error_extension": "Unsupported extension or download error",
"error_no_download": "No pending download.",
@@ -37,6 +37,8 @@
"game_count": "{0} ({1} games)",
"game_filter": "Active filter: {0}",
"game_search": "Filter: {0}",
"game_header_name": "Name",
"game_header_size": "Size",
"history_title": "Downloads ({0})",
"history_empty": "No downloads in history",
@@ -78,6 +80,14 @@
"menu_music_toggle": "Toggle music",
"menu_music_enabled": "Music enabled: {0}",
"menu_music_disabled": "Music disabled",
"menu_filter_platforms": "Filter systems",
"filter_platforms_title": "Systems visibility",
"filter_all": "Show all",
"filter_none": "Hide all",
"filter_apply": "Apply",
"filter_back": "Back",
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
"filter_unsaved_warning": "Unsaved changes",
"menu_quit": "Quit",
"button_yes": "Yes",

View File

@@ -27,7 +27,7 @@
"error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}",
"error_api_key_extended": "Atención, debes ingresar tu clave API (solo premium) en el archivo /userdata/saves/ports/rgsx/1fichierAPI.txt, abrirlo en un editor de texto y pegar la clave API",
"error_invalid_download_data": "Datos de descarga no válidos",
"error_delete_sources": "Error al eliminar el archivo sources.json o carpetas",
"error_delete_sources": "Error al eliminar el archivo systems_list.json o carpetas",
"error_extension": "Extensión no soportada o error de descarga",
"error_no_download": "No hay descargas pendientes.",
@@ -38,6 +38,8 @@
"game_count": "{0} ({1} juegos)",
"game_filter": "Filtro activo: {0}",
"game_search": "Filtrar: {0}",
"game_header_name": "Nombre",
"game_header_size": "Tamaño",
"history_title": "Descargas ({0})",
"history_empty": "No hay descargas en el historial",
@@ -79,6 +81,14 @@
"menu_music_toggle": "Activar/Desactivar música",
"menu_music_enabled": "Música activada: {0}",
"menu_music_disabled": "Música desactivada",
"menu_filter_platforms": "Filtrar sistemas",
"filter_platforms_title": "Visibilidad de sistemas",
"filter_all": "Mostrar todo",
"filter_none": "Ocultar todo",
"filter_apply": "Aplicar",
"filter_back": "Volver",
"filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}",
"filter_unsaved_warning": "Cambios no guardados",
"menu_quit": "Salir",
"button_yes": "Sí",

View File

@@ -23,7 +23,7 @@
"error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}",
"error_api_key_extended": "Attention il faut renseigner sa clé API (premium only) dans le fichier /userdata/saves/ports/rgsx/1fichierAPI.txt à ouvrir dans un éditeur de texte et coller la clé API",
"error_invalid_download_data": "Données de téléchargement invalides",
"error_delete_sources": "Erreur lors de la suppression du fichier sources.json ou dossiers",
"error_delete_sources": "Erreur lors de la suppression du fichier systems_list.json ou dossiers",
"error_extension": "Extension non supportée ou erreur de téléchargement",
"error_no_download": "Aucun téléchargement en attente.",
@@ -34,6 +34,8 @@
"game_count": "{0} ({1} jeux)",
"game_filter": "Filtre actif : {0}",
"game_search": "Filtrer : {0}",
"game_header_name": "Nom",
"game_header_size": "Taille",
"history_title": "Téléchargements ({0})",
"history_empty": "Aucun téléchargement dans l'historique",
@@ -76,6 +78,14 @@
"menu_music_toggle": "Activer/Désactiver la musique",
"menu_music_enabled": "Musique activée : {0}",
"menu_music_disabled": "Musique désactivée",
"menu_filter_platforms": "Filtrer les systèmes",
"filter_platforms_title": "Affichage des systèmes",
"filter_all": "Tout afficher",
"filter_none": "Tout masquer",
"filter_apply": "Appliquer",
"filter_back": "Retour",
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
"filter_unsaved_warning": "Modifications non sauvegardées",
"button_yes": "Oui",
"button_no": "Non",

View File

@@ -260,7 +260,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
if platform_dict.get("platform_name") == platform:
platform_folder = platform_dict.get("folder", normalize_platform_name(platform))
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}")
@@ -298,9 +298,82 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
download_headers = headers.copy()
download_headers['Accept'] = 'application/octet-stream, */*'
download_headers['Referer'] = 'https://myrient.erista.me/'
response = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=download_headers)
logger.debug(f"Status code: {response.status_code}")
response.raise_for_status()
# Préparation spécifique archive.org : récupérer quelques pages pour obtenir cookies éventuels
if 'archive.org/download/' in url:
try:
pre_id = url.split('/download/')[1].split('/')[0]
session.get('https://archive.org/robots.txt', timeout=10)
session.get(f'https://archive.org/metadata/{pre_id}', timeout=10)
logger.debug(f"Pré-chargement cookies/metadata archive.org pour {pre_id}")
except Exception as e:
logger.debug(f"Pré-chargement archive.org ignoré: {e}")
# Tentatives multiples avec variations d'en-têtes pour contourner certains 401/403 (archive.org / hotlink protection)
header_variants = [
download_headers,
{ # Variante sans Referer spécifique
'User-Agent': headers['User-Agent'],
'Accept': 'application/octet-stream,*/*;q=0.8',
'Accept-Language': headers['Accept-Language'],
'Connection': 'keep-alive'
},
{ # Variante minimaliste type curl
'User-Agent': 'curl/8.4.0',
'Accept': '*/*'
},
{ # Variante avec Referer archive.org
'User-Agent': headers['User-Agent'],
'Accept': '*/*',
'Referer': 'https://archive.org/'
}
]
response = None
last_status = None
for attempt, hv in enumerate(header_variants, start=1):
try:
logger.debug(f"Tentative téléchargement {attempt}/{len(header_variants)} avec headers: {hv}")
r = session.get(url, stream=True, timeout=30, allow_redirects=True, headers=hv)
last_status = r.status_code
logger.debug(f"Status code tentative {attempt}: {r.status_code}")
if r.status_code in (401, 403):
# Lire un petit bout pour voir si message utile
try:
snippet = r.text[:200]
logger.debug(f"Réponse {r.status_code} snippet: {snippet}")
except Exception:
pass
continue # Essayer variante suivante
r.raise_for_status()
response = r
break
except requests.RequestException as e:
logger.debug(f"Erreur tentative {attempt}: {e}")
# Si ce n'est pas une erreur auth explicite et qu'on a un code => on sort
if isinstance(e, requests.HTTPError) and last_status not in (401, 403):
break
if response is None:
# Fallback metadata archive.org pour message clair
if 'archive.org/download/' in url:
try:
identifier = url.split('/download/')[1].split('/')[0]
meta_resp = session.get(f'https://archive.org/metadata/{identifier}', timeout=15)
if meta_resp.status_code == 200:
meta_json = meta_resp.json()
if meta_json.get('is_dark'):
raise requests.HTTPError(f"Item archive.org restreint (is_dark=true): {identifier}")
if not meta_json.get('files'):
raise requests.HTTPError(f"Item archive.org sans fichiers listés: {identifier}")
# Fichier peut avoir un nom différent : informer
available = [f.get('name') for f in meta_json.get('files', [])][:10]
raise requests.HTTPError(f"Accès refusé (HTTP {last_status}). Fichiers disponibles exemples: {available}")
else:
raise requests.HTTPError(f"HTTP {last_status} & metadata {meta_resp.status_code} pour {identifier}")
except requests.HTTPError:
raise
except Exception as e:
raise requests.HTTPError(f"HTTP {last_status} après variations; metadata échec: {e}")
auth_msg = f"HTTP {last_status} après variations d'en-têtes" if last_status else "Aucune réponse valide"
raise requests.HTTPError(auth_msg)
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale: {total_size} octets")
@@ -471,7 +544,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
if platform_dict.get("platform_name") == platform:
platform_folder = platform_dict.get("folder", normalize_platform_name(platform))
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
break

View File

@@ -29,9 +29,9 @@ def load_rgsx_settings():
"enabled": False,
"target_directory": ""
},
"sources": { # Nouvelle section pour la source des jeux
"mode": "rgsx", # "rgsx" ou "custom"
"custom_url": "" # URL personnalisée pour le ZIP des sources
"sources": {
"mode": "rgsx",
"custom_url": ""
}
}
@@ -44,12 +44,6 @@ def load_rgsx_settings():
if key not in settings:
settings[key] = value
return settings
else:
# Tenter de migrer depuis les anciens fichiers
migrated_settings = migrate_old_settings()
if migrated_settings:
save_rgsx_settings(migrated_settings)
return migrated_settings
except Exception as e:
print(f"Erreur lors du chargement de rgsx_settings.json: {str(e)}")
@@ -68,96 +62,6 @@ def save_rgsx_settings(settings):
print(f"Erreur lors de la sauvegarde de rgsx_settings.json: {str(e)}")
def migrate_old_settings():
"""Migre les anciens fichiers de configuration vers le nouveau format."""
from config import LANGUAGE_CONFIG_PATH, MUSIC_CONFIG_PATH, ACCESSIBILITY_FOLDER, SYMLINK_SETTINGS_PATH
migrated_settings = {
"language": "en",
"music_enabled": True,
"accessibility": {
"font_scale": 1.0
},
"symlink": {
"enabled": False,
"target_directory": ""
}
}
files_to_remove = [] # Liste des fichiers à supprimer après migration réussie
# Migrer language.json
if os.path.exists(LANGUAGE_CONFIG_PATH):
try:
with open(LANGUAGE_CONFIG_PATH, 'r', encoding='utf-8') as f:
content = f.read().strip()
# Gérer le cas où le fichier contient juste une chaîne (pas de JSON)
if content.startswith('"') and content.endswith('"'):
migrated_settings["language"] = content.strip('"')
elif not content.startswith('{'):
# Fichier texte simple sans guillemets
migrated_settings["language"] = content
else:
# Fichier JSON normal
lang_data = json.loads(content)
migrated_settings["language"] = lang_data.get("language", "en")
files_to_remove.append(LANGUAGE_CONFIG_PATH)
except:
pass
# Migrer music_config.json
if os.path.exists(MUSIC_CONFIG_PATH):
try:
with open(MUSIC_CONFIG_PATH, 'r', encoding='utf-8') as f:
content = f.read().strip()
# Gérer le cas où le fichier contient juste un booléen
if content.lower() in ['true', 'false']:
migrated_settings["music_enabled"] = content.lower() == 'true'
else:
# Fichier JSON normal
music_data = json.loads(content)
migrated_settings["music_enabled"] = music_data.get("music_enabled", True)
files_to_remove.append(MUSIC_CONFIG_PATH)
except:
pass
# Migrer accessibility.json
if os.path.exists(ACCESSIBILITY_FOLDER):
try:
with open(ACCESSIBILITY_FOLDER, 'r', encoding='utf-8') as f:
acc_data = json.load(f)
migrated_settings["accessibility"] = {
"font_scale": acc_data.get("font_scale", 1.0)
}
files_to_remove.append(ACCESSIBILITY_FOLDER)
except:
pass
# Migrer symlink_settings.json
if os.path.exists(SYMLINK_SETTINGS_PATH):
try:
with open(SYMLINK_SETTINGS_PATH, 'r', encoding='utf-8') as f:
symlink_data = json.load(f)
migrated_settings["symlink"] = {
"enabled": symlink_data.get("use_symlink_path", False),
"target_directory": symlink_data.get("target_directory", "")
}
files_to_remove.append(SYMLINK_SETTINGS_PATH)
except:
pass
# Supprimer les anciens fichiers après migration réussie
if files_to_remove:
print(f"Migration réussie. Suppression des anciens fichiers de configuration...")
for file_path in files_to_remove:
try:
os.remove(file_path)
print(f" - Supprimé: {os.path.basename(file_path)}")
except Exception as e:
print(f" - Erreur lors de la suppression de {os.path.basename(file_path)}: {e}")
return migrated_settings
def load_symlink_settings():
"""Load symlink settings from rgsx_settings.json."""

View File

@@ -12,9 +12,9 @@ from rgsx_settings import load_rgsx_settings, save_rgsx_settings
import zipfile
import time
import random
from config import JSON_EXTENSIONS, SAVE_FOLDER
import config
from history import save_history
from language import _ # Import de la fonction de traduction
from language import _
from datetime import datetime
@@ -47,10 +47,10 @@ def detect_non_pc():
def load_extensions_json():
"""Charge le fichier JSON contenant les extensions supportées."""
try:
with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
with open(config.JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}")
logger.error(f"Erreur lors de la lecture de {config.JSON_EXTENSIONS}: {e}")
return []
def check_extension_before_download(url, platform, game_name):
@@ -59,7 +59,7 @@ def check_extension_before_download(url, platform, game_name):
sanitized_name = sanitize_filename(game_name)
extensions_data = load_extensions_json()
if not extensions_data:
logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable")
logger.error(f"Fichier {config.JSON_EXTENSIONS} vide ou introuvable")
return None
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
@@ -80,13 +80,15 @@ def check_extension_before_download(url, platform, game_name):
return None
# Fonction pour vérifier si l'extension est supportée pour une plateforme donnée
def is_extension_supported(filename, platform, extensions_data):
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée."""
def is_extension_supported(filename, platform_key, extensions_data):
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée.
platform_key correspond maintenant à l'identifiant utilisé dans config.platforms (platform_name)."""
extension = os.path.splitext(filename)[1].lower()
dest_dir = None
for platform_dict in config.platform_dicts:
if platform_dict["platform"] == platform:
# Nouveau schéma: platform_name
if platform_dict.get("platform_name") == platform_key:
dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder"))
break
@@ -108,43 +110,225 @@ def is_extension_supported(filename, platform, extensions_data):
# Fonction pour charger sources.json
def load_sources():
"""Charge les sources depuis sources.json et initialise les plateformes."""
sources_path = os.path.join(config.SOURCES_FILE)
logger.debug(f"Chargement de {sources_path}")
"""Charge la liste de base depuis systems_list.json puis ajoute les plateformes
implicites (fichiers de jeux *.json non listés) à la fin sans réordonner.
Schéma attendu dans systems_list.json:
[ {"platform_name": str, "folder": str, (optionnel) "platform_image"|"system_image": str }, ... ]
Règles d'ajout automatique:
- Chaque fichier <nom>.json dans config.GAMES_FOLDER est candidat.
- Si <nom> ne correspond à aucune entrée existante (case sensitive simple),
on ajoute {"platform_name": <nom>, "folder": <nom>} en fin de liste.
- Aucun tri n'est appliqué pour préserver l'ordre original défini par l'utilisateur.
"""
try:
with open(sources_path, 'r', encoding='utf-8') as f:
sources = json.load(f)
sources = sorted(sources, key=lambda x: x.get("nom", x.get("platform", "")).lower())
config.platforms = [source["platform"] for source in sources]
config.platform_dicts = sources
config.platform_names = {source["platform"]: source["nom"] for source in sources}
config.games_count = {platform: 0 for platform in config.platforms} # Initialiser à 0
# Charger les jeux pour chaque plateforme
loaded_platforms = set() # Pour suivre les plateformes déjà loguées
for platform in config.platforms:
games = load_games(platform)
config.games_count[platform] = len(games)
if platform not in loaded_platforms:
loaded_platforms.add(platform)
# Appeler write_unavailable_systems une seule fois après la boucle
write_unavailable_systems() # Assurez-vous que cette fonction est définie
# Détection legacy: si sources.json (ancien format) existe encore, déclencher redownload automatique
legacy_path = os.path.join(config.SAVE_FOLDER, "sources.json")
if os.path.exists(legacy_path):
logger.warning("Ancien fichier sources.json détecté: déclenchement redownload cache jeux")
try:
# Supprimer ancien cache et forcer redémarrage logique comme dans l'option de menu
if os.path.exists(config.SOURCES_FILE):
try:
os.remove(config.SOURCES_FILE)
except Exception:
pass
if os.path.exists(config.GAMES_FOLDER):
shutil.rmtree(config.GAMES_FOLDER, ignore_errors=True)
if os.path.exists(config.IMAGES_FOLDER):
shutil.rmtree(config.IMAGES_FOLDER, ignore_errors=True)
# Renommer legacy pour éviter boucle
try:
os.replace(legacy_path, legacy_path + ".bak")
except Exception:
pass
# Préparer popup redémarrage si contexte graphique chargé
config.popup_message = _("popup_redownload_success") if hasattr(config, 'popup_message') else "Cache jeux réinitialisé"
config.popup_timer = 5000 if hasattr(config, 'popup_timer') else 0
config.menu_state = "restart_popup" if hasattr(config, 'menu_state') else getattr(config, 'menu_state', 'platform')
config.needs_redraw = True
logger.info("Redownload cache déclenché automatiquement (legacy sources.json)")
return [] # On sort pour laisser le processus de redémarrage gérer le rechargement
except Exception as e:
logger.error(f"Échec redownload automatique depuis legacy sources.json: {e}")
sources = []
if os.path.exists(config.SOURCES_FILE):
with open(config.SOURCES_FILE, 'r', encoding='utf-8') as f:
sources = json.load(f)
if not isinstance(sources, list):
logger.error("systems_list.json n'est pas une liste JSON valide")
sources = []
else:
logger.warning(f"Fichier systems_list absent: {config.SOURCES_FILE}")
# S'assurer que chaque entrée possède la clé platform_image (vide si absente)
for s in sources:
if "platform_image" not in s:
# Supporter ancienne clé system_image -> platform_image si présente
legacy = s.pop("system_image", "") if isinstance(s, dict) else ""
s["platform_image"] = legacy or ""
existing_names = {s.get("platform_name", "") for s in sources}
added = []
if os.path.isdir(config.GAMES_FOLDER):
for fname in sorted(os.listdir(config.GAMES_FOLDER)):
if not fname.lower().endswith('.json'):
continue
pname = os.path.splitext(fname)[0]
if not pname or pname in existing_names:
continue
new_entry = {"platform_name": pname, "folder": pname, "platform_image": ""}
sources.append(new_entry)
added.append(pname)
existing_names.add(pname)
# Déterminer les plateformes orphelines (fichier manquant)
existing_files = set()
if os.path.isdir(config.GAMES_FOLDER):
existing_files = {os.path.splitext(f)[0] for f in os.listdir(config.GAMES_FOLDER) if f.lower().endswith('.json')}
removed = []
filtered_sources = []
for entry in sources:
pname = entry.get("platform_name", "")
# Garder seulement si un fichier existe
if pname in existing_files:
filtered_sources.append(entry)
else:
# Ne retirer que si ce n'est pas un nom vide
if pname:
removed.append(pname)
sources = filtered_sources
if added:
logger.info(f"Plateformes ajoutées automatiquement: {', '.join(added)}")
if removed:
logger.info(f"Plateformes supprimées (fichiers absents): {', '.join(removed)}")
# Persister si modifications (ajouts ou suppressions)
if added or removed:
try:
# Pas de tri avant persistance: conserver ordre d'origine + ajouts fins
os.makedirs(os.path.dirname(config.SOURCES_FILE), exist_ok=True)
with open(config.SOURCES_FILE, 'w', encoding='utf-8') as f:
json.dump(sources, f, ensure_ascii=False, indent=2)
logger.info("systems_list.json mis à jour (ajouts/suppressions, ordre conservé)")
except Exception as e:
logger.error(f"Échec écriture systems_list.json après maj auto: {e}")
# Pour l'affichage on veut un tri alphabétique sans toucher l'ordre de persistance
sorted_for_display = sorted(sources, key=lambda x: x.get("platform_name", "").lower())
# Construire structures config: platform_dicts = ordre fichier, platforms = tri (avec filtre masqués)
config.platform_dicts = sources # ordre brut fichier
settings = load_rgsx_settings()
hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set()
all_sorted_names = [s.get("platform_name", "") for s in sorted_for_display]
visible_names = [n for n in all_sorted_names if n and n not in hidden]
config.platforms = visible_names
config.platform_names = {p: p for p in config.platforms}
# Nouveau mapping par nom pour éviter décalages index après tri d'affichage
try:
config.platform_dict_by_name = {d.get("platform_name", ""): d for d in config.platform_dicts}
except Exception:
config.platform_dict_by_name = {}
config.games_count = {}
for platform_name in config.platforms:
games = load_games(platform_name)
config.games_count[platform_name] = len(games)
write_unavailable_systems()
return sources
except Exception as e:
logger.error(f"Erreur lors du chargement de sources.json : {str(e)}")
logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
return []
def load_games(platform_id):
"""Charge les jeux pour une plateforme donnée en utilisant platform_id et teste la première URL."""
games_path = os.path.join(config.GAMES_FOLDER, f"{platform_id}.json")
#logger.debug(f"Chargement des jeux pour {platform_id} depuis {games_path}")
"""Charge la liste des jeux pour une plateforme.
Recherche des fichiers dans config.GAMES_FOLDER selon l'ordre:
1. <platform_id>.json (nom exact)
2. normalisé: normalize_platform_name(platform_id).json
3. <folder>.json (valeur 'folder' du dictionnaire de plateforme)
Format accepté du fichier JSON:
- liste de listes/tuples: [ [name, url], [name, url, size], ... ]
- liste de chaînes: [ name1, name2, ... ]
- liste de dicts: [ {"game_name"|"name"|"title": str, "url"|"download"|"link"|"href": str?, "size"|"filesize"|"length": str?}, ... ]
- dict contenant une clé 'games' avec un des formats ci-dessus
Retourne une liste normalisée de tuples (name, url_or_None, size_or_None).
"""
try:
with open(games_path, 'r', encoding='utf-8') as f:
games = json.load(f)
logger.debug(f"Jeux chargés pour {platform_id}: {len(games)} jeux")
return games
# Retrouver l'objet plateforme pour accéder éventuellement à 'folder'
platform_dict = None
for pd in config.platform_dicts:
if pd.get("platform_name") == platform_id or pd.get("platform") == platform_id:
platform_dict = pd
break
candidates = []
# 1. Nom exact
candidates.append(os.path.join(config.GAMES_FOLDER, f"{platform_id}.json"))
# 2. Nom normalisé
norm = normalize_platform_name(platform_id)
if norm and norm != platform_id:
candidates.append(os.path.join(config.GAMES_FOLDER, f"{norm}.json"))
# 3. Folder déclaré
if platform_dict:
folder_name = platform_dict.get("folder")
if folder_name:
candidates.append(os.path.join(config.GAMES_FOLDER, f"{folder_name}.json"))
game_file = None
for c in candidates:
if os.path.exists(c):
game_file = c
break
if not game_file:
logger.warning(f"Aucun fichier de jeux trouvé pour {platform_id} (candidats: {candidates})")
return []
with open(game_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Si dict avec clé 'games'
if isinstance(data, dict) and 'games' in data:
data = data['games']
normalized = [] # (name, url, size)
def extract_from_dict(d):
name = d.get('game_name') or d.get('name') or d.get('title') or d.get('game')
url = d.get('url') or d.get('download') or d.get('link') or d.get('href')
size = d.get('size') or d.get('filesize') or d.get('length')
if name:
normalized.append((str(name), url if isinstance(url, str) and url.strip() else None, str(size) if size else None))
if isinstance(data, list):
for item in data:
if isinstance(item, (list, tuple)):
if len(item) == 0:
continue
name = str(item[0])
url = item[1] if len(item) > 1 and isinstance(item[1], str) and item[1].strip() else None
size = item[2] if len(item) > 2 and isinstance(item[2], str) and item[2].strip() else None
normalized.append((name, url, size))
elif isinstance(item, dict):
extract_from_dict(item)
elif isinstance(item, str):
normalized.append((item, None, None))
else:
normalized.append((str(item), None, None))
elif isinstance(data, dict): # dict sans 'games'
extract_from_dict(data)
else:
logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}")
logger.debug(f"Jeux chargés pour {platform_id} depuis {os.path.basename(game_file)}: {len(normalized)} entrées")
return normalized
except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id} : {str(e)}")
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
return []
def write_unavailable_systems():
@@ -156,12 +340,11 @@ def write_unavailable_systems():
# Formater la date et l'heure pour le nom du fichier
current_time = datetime.now()
timestamp = current_time.strftime("%d-%m-%Y-%H-%M")
log_dir = os.path.join(os.path.dirname(config.APP_FOLDER), "logs", "RGSX")
log_file = os.path.join(log_dir, f"systemes_unavailable_{timestamp}.txt")
log_file = os.path.join(config.log_dir, f"systemes_unavailable_{timestamp}.txt")
try:
# Créer le répertoire s'il n'existe pas
os.makedirs(log_dir, exist_ok=True)
os.makedirs(config.log_dir, exist_ok=True)
# Écrire les systèmes dans le fichier
with open(log_file, 'w', encoding='utf-8') as f:
@@ -290,15 +473,28 @@ def wrap_text(text, font, max_width):
return lines
def load_system_image(platform_dict):
"""Charge une image système depuis le chemin spécifié dans system_image."""
image_path = os.path.join(config.IMAGES_FOLDER, platform_dict.get("system_image", "default.png"))
platform_name = platform_dict.get("platform", "unknown")
#logger.debug(f"Chargement de l'image système pour {platform_name} depuis {image_path}")
"""Charge une image système avec priorité:
1. Fichier nommé exactement <platform_name>.png
2. Champ platform_image si non vide
3. Fallback default.png"""
platform_name = platform_dict.get("platform_name", "unknown")
preferred_filename = f"{platform_name}.png"
preferred_path = os.path.join(config.IMAGES_FOLDER, preferred_filename)
# Normaliser platform_image pouvant être vide
platform_image_field = platform_dict.get("platform_image") or ""
explicit_image_path = os.path.join(config.IMAGES_FOLDER, platform_image_field) if platform_image_field else None
default_path = os.path.join(config.IMAGES_FOLDER, "default.png")
try:
if not os.path.exists(image_path):
logger.error(f"Image introuvable pour {platform_name} à {image_path}")
return None
return pygame.image.load(image_path).convert_alpha()
if os.path.exists(preferred_path):
return pygame.image.load(preferred_path).convert_alpha()
if explicit_image_path and os.path.exists(explicit_image_path):
return pygame.image.load(explicit_image_path).convert_alpha()
if os.path.exists(default_path):
return pygame.image.load(default_path).convert_alpha()
logger.error(f"Aucune image trouvée pour {platform_name} (cherché: {preferred_path}, {explicit_image_path}, default.png)")
return None
except Exception as e:
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
return None
@@ -447,16 +643,6 @@ def extract_rar(rar_path, dest_dir, url):
if system_type == "Windows":
# Sur Windows, utiliser directement config.UNRAR_EXE
unrar_exe = config.UNRAR_EXE
if not os.path.exists(unrar_exe):
logger.warning("unrar.exe absent, téléchargement en cours...")
try:
import urllib.request
os.makedirs(os.path.dirname(unrar_exe), exist_ok=True)
urllib.request.urlretrieve(config.unrar_download_exe, unrar_exe)
logger.info(f"unrar.exe téléchargé dans {unrar_exe}")
except Exception as e:
logger.error(f"Impossible de télécharger unrar.exe: {str(e)}")
return False, _("utils_unrar_unavailable")
unrar_cmd = [unrar_exe]
else:
# Linux/Batocera: utiliser 'unrar' du système
@@ -657,32 +843,11 @@ def handle_xbox(dest_dir, iso_files):
if system_type == "Windows":
# Sur Windows; telecharger le fichier exe
XDVDFS_EXE = config.XDVDFS_EXE
if not os.path.exists(XDVDFS_EXE):
logger.warning("xdvdfs.exe absent, téléchargement en cours...")
try:
import urllib.request
os.makedirs(os.path.dirname(XDVDFS_EXE), exist_ok=True)
urllib.request.urlretrieve(config.xdvdfs_download_exe, XDVDFS_EXE)
logger.info(f"xdvdfs.exe téléchargé dans {XDVDFS_EXE}")
except Exception as e:
logger.error(f"Impossible de télécharger xdvdfs.exe: {str(e)}")
return False, _("utils_xdvdfs_unavailable")
xdvdfs_cmd = [XDVDFS_EXE, "pack"] # Liste avec 2 éléments
else:
# Linux/Batocera : télécharger le fichier xdvdfs
XDVDFS_LINUX = config.XDVDFS_LINUX
if not os.path.exists(XDVDFS_LINUX):
logger.warning("xdvdfs non trouvé, téléchargement en cours...")
try:
import urllib.request
os.makedirs(os.path.dirname(XDVDFS_LINUX), exist_ok=True)
urllib.request.urlretrieve(config.xdvdfs_download_linux, XDVDFS_LINUX)
os.chmod(XDVDFS_LINUX, 0o755) # Rendre exécutable
logger.info(f"xdvdfs téléchargé dans {XDVDFS_LINUX}")
except Exception as e:
logger.error(f"Impossible de télécharger xdvdfs: {str(e)}")
return False, _("utils_xdvdfs_unavailable") # Vérifier les permissions après le téléchargement
try:
stat_info = os.stat(XDVDFS_LINUX)
mode = stat_info.st_mode
@@ -792,24 +957,24 @@ def set_music_popup(music_name):
def load_api_key_1fichier():
"""Charge la clé API 1fichier depuis le dossier de sauvegarde, crée le fichier si absent."""
api_path = os.path.join(SAVE_FOLDER, "1fichierAPI.txt")
logger.debug(f"Tentative de chargement de la clé API depuis: {api_path}")
logger.debug(f"Tentative de chargement de la clé API depuis: {config.API_KEY_1FICHIER}")
try:
# Vérifie si le fichier existe déjà
if not os.path.exists(api_path):
if not os.path.exists(config.API_KEY_1FICHIER):
# Crée le dossier parent si nécessaire
os.makedirs(SAVE_FOLDER, exist_ok=True)
os.makedirs(config.SAVE_FOLDER, exist_ok=True)
# Crée le fichier vide si absent
with open(api_path, "w") as f:
with open(config.API_KEY_1FICHIER, "w") as f:
f.write("")
logger.info(f"Fichier de clé API créé : {api_path}")
logger.info(f"Fichier de clé API créé : {config.API_KEY_1FICHIER}")
return ""
except OSError as e:
logger.error(f"Erreur lors de la création du fichier de clé API : {e}")
return ""
# Lit la clé API depuis le fichier
try:
with open(api_path, "r", encoding="utf-8") as f:
with open(config.API_KEY_1FICHIER, "r", encoding="utf-8") as f:
api_key = f.read().strip()
logger.debug(f"Clé API 1fichier lue: '{api_key}' (longueur: {len(api_key)})")
if not api_key: