From 059c38d8d64dbcae5e03c33a070a9b2f2e28fd4f Mon Sep 17 00:00:00 2001
From: skymike03
Date: Mon, 20 Oct 2025 19:02:28 +0200
Subject: [PATCH] v2.3.0.1 - Add game info scraper test (does not save
information) : Push "confirm/download" button for 3sec to see picture/info of
a game - correct ps3 decrypt for some games with special characters inside
name
---
SYMLINK_FEATURE.md | 77 --------
ports/RGSX/__main__.py | 77 +++++++-
ports/RGSX/config.py | 18 +-
ports/RGSX/controls.py | 391 ++++++++++++++++++++++++++++++-----------
ports/RGSX/display.py | 201 +++++++++++++++++++--
ports/RGSX/scraper.py | 294 +++++++++++++++++++++++++++++++
ports/RGSX/utils.py | 25 ++-
7 files changed, 880 insertions(+), 203 deletions(-)
delete mode 100644 SYMLINK_FEATURE.md
create mode 100644 ports/RGSX/scraper.py
diff --git a/SYMLINK_FEATURE.md b/SYMLINK_FEATURE.md
deleted file mode 100644
index 396bb7f..0000000
--- a/SYMLINK_FEATURE.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# Symlink Option Feature
-
-## Overview
-
-This feature adds a simple toggle option to append the platform folder name to the download path, creating a symlink-friendly structure for external storage.
-
-## How It Works
-
-When the symlink option is **disabled** (default):
-- Super Nintendo ROMs download to: `../roms/snes/`
-- PlayStation 2 ROMs download to: `../roms/ps2/`
-
-When the symlink option is **enabled**:
-- Super Nintendo ROMs download to: `../roms/snes/snes/`
-- PlayStation 2 ROMs download to: `../roms/ps2/ps2/`
-
-This allows users to create symlinks from the platform folder to external storage locations.
-
-## Usage
-
-1. Open the pause menu (P key or Start button)
-2. Navigate to "Symlink Option" (second to last option, before Quit)
-3. Press Enter to toggle the option on/off
-4. The menu will show the current status: "Symlink option enabled" or "Symlink option disabled"
-
-## Implementation Details
-
-### Files Added
-- `symlink_settings.py` - Core functionality for managing the symlink option
-
-### Files Modified
-- `display.py` - Added symlink option to pause menu with dynamic status display
-- `controls.py` - Added handling for symlink option toggle
-- `network.py` - Modified download functions to use symlink paths when enabled
-- Language files - Added translation strings for all supported languages
-
-### Configuration
-
-The symlink setting is stored in `symlink_settings.json` in the save folder:
-
-```json
-{
- "use_symlink_path": false
-}
-```
-
-### API Functions
-
-- `get_symlink_option()` - Get current symlink option status
-- `set_symlink_option(enabled)` - Enable/disable the symlink option
-- `apply_symlink_path(base_path, platform_folder)` - Apply symlink path modification
-
-## Example Use Case
-
-1. Enable the symlink option
-2. **Optional**: Create a symlink: `ln -s /external/storage/snes ../roms/snes/snes`
- - If you don't create the symlink, the nested directories will be created automatically when you download ROMs
-3. Download ROMs - the nested directories (like `../roms/snes/snes/`) will be created automatically if they don't exist
-4. Now Super Nintendo ROMs will download to the external storage via the symlink (if created) or to the local nested directory
-
-## Features
-
-- **Simple Toggle**: Easy on/off switch in the pause menu
-- **Persistent Settings**: Option is remembered between sessions
-- **Multi-language Support**: Full internationalization
-- **Backward Compatible**: Disabled by default, doesn't affect existing setups
-- **Platform Agnostic**: Works with all platforms automatically
-- **Automatic Directory Creation**: Nested directories are created automatically if they don't exist
-
-## Technical Notes
-
-- The option is disabled by default
-- Settings are stored in JSON format
-- Path modification is applied at download time
-- Works with both regular downloads and 1fichier downloads
-- No impact on existing ROMs or folder structure
-- Missing directories are automatically created using `os.makedirs(dest_dir, exist_ok=True)`
diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py
index abea5f1..d11202e 100644
--- a/ports/RGSX/__main__.py
+++ b/ports/RGSX/__main__.py
@@ -478,6 +478,73 @@ async def main():
# Gestion de la répétition automatique des actions
process_key_repeats(sources, joystick, screen)
+ # Gestion de l'appui long sur confirm dans le menu game pour ouvrir le scraper
+ if (config.menu_state == "game" and
+ config.confirm_press_start_time > 0 and
+ not config.confirm_long_press_triggered):
+ press_duration = current_time - config.confirm_press_start_time
+ if press_duration >= config.confirm_long_press_threshold:
+ # Appui long détecté, ouvrir le scraper
+ games = config.filtered_games if config.filter_active or config.search_mode else config.games
+ if games:
+ game_name = games[config.current_game][0]
+ platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
+
+ config.previous_menu_state = "game"
+ config.menu_state = "scraper"
+ config.scraper_game_name = game_name
+ config.scraper_platform_name = platform
+ config.scraper_loading = True
+ config.scraper_error_message = ""
+ config.scraper_image_surface = None
+ config.scraper_image_url = ""
+ config.scraper_description = ""
+ config.scraper_genre = ""
+ config.scraper_release_date = ""
+ config.scraper_game_page_url = ""
+ config.needs_redraw = True
+ config.confirm_long_press_triggered = True # Éviter de déclencher plusieurs fois
+ logger.debug(f"Appui long détecté ({press_duration}ms), ouverture du scraper pour {game_name}")
+
+ # Lancer la recherche des métadonnées dans un thread séparé
+ def scrape_async():
+ from scraper import get_game_metadata, download_image_to_surface
+ logger.info(f"Scraping métadonnées pour {game_name} sur {platform}")
+ metadata = get_game_metadata(game_name, platform)
+
+ # Vérifier si on a une erreur
+ if "error" in metadata:
+ config.scraper_error_message = metadata["error"]
+ config.scraper_loading = False
+ config.needs_redraw = True
+ logger.error(f"Erreur de scraping: {metadata['error']}")
+ return
+
+ # Mettre à jour les métadonnées textuelles
+ config.scraper_description = metadata.get("description", "")
+ config.scraper_genre = metadata.get("genre", "")
+ config.scraper_release_date = metadata.get("release_date", "")
+ config.scraper_game_page_url = metadata.get("game_page_url", "")
+
+ # Télécharger l'image si disponible
+ image_url = metadata.get("image_url")
+ if image_url:
+ logger.info(f"Téléchargement de l'image: {image_url}")
+ image_surface = download_image_to_surface(image_url)
+ if image_surface:
+ config.scraper_image_surface = image_surface
+ config.scraper_image_url = image_url
+ else:
+ logger.warning("Échec du téléchargement de l'image")
+
+ config.scraper_loading = False
+ config.needs_redraw = True
+ logger.info("Scraping terminé")
+
+ import threading
+ thread = threading.Thread(target=scrape_async, daemon=True)
+ thread.start()
+
# Gestion des événements
events = pygame.event.get()
for event in events:
@@ -594,6 +661,7 @@ async def main():
"history_game_options",
"history_show_folder",
"history_scraper_info",
+ "scraper", # Ajout du scraper pour gérer les contrôles
"history_error_details",
"history_confirm_delete",
"history_extract_archive",
@@ -830,7 +898,7 @@ async def main():
# Gestion des téléchargements
if config.download_tasks:
for task_id, (task, url, game_name, platform_name) in list(config.download_tasks.items()):
- logger.debug(f"[DOWNLOAD_CHECK] Checking task {task_id}: done={task.done()}, game={game_name}")
+ #logger.debug(f"[DOWNLOAD_CHECK] Checking task {task_id}: done={task.done()}, game={game_name}")
if task.done():
logger.debug(f"[DOWNLOAD_COMPLETE] Task {task_id} is done, processing result for {game_name}")
try:
@@ -840,9 +908,9 @@ async def main():
message = message.split("https://")[0].strip()
logger.debug(f"[HISTORY_SEARCH] Searching in {len(config.history)} history entries for url={url[:50]}...")
for entry in config.history:
- logger.debug(f"[HISTORY_ENTRY] Checking: url_match={entry['url'] == url}, status={entry['status']}, game={entry.get('game_name')}")
+ #logger.debug(f"[HISTORY_ENTRY] Checking: url_match={entry['url'] == url}, status={entry['status']}, game={entry.get('game_name')}")
if entry["url"] == url and entry["status"] in ["downloading", "Téléchargement"]:
- logger.debug(f"[HISTORY_MATCH] Found matching entry for {game_name}, updating status")
+ #logger.debug(f"[HISTORY_MATCH] Found matching entry for {game_name}, updating status")
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
@@ -994,6 +1062,9 @@ async def main():
elif config.menu_state == "history_show_folder":
from display import draw_history_show_folder
draw_history_show_folder(screen)
+ elif config.menu_state == "scraper":
+ from display import draw_scraper_screen
+ draw_scraper_screen(screen)
elif config.menu_state == "history_scraper_info":
from display import draw_history_scraper_info
draw_history_scraper_info(screen)
diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py
index 22b521a..a53988d 100644
--- a/ports/RGSX/config.py
+++ b/ports/RGSX/config.py
@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
-app_version = "2.3.0.0"
+app_version = "2.3.0.1"
def get_application_root():
@@ -307,6 +307,18 @@ confirm_cancel_selection = 0 # confirmation annulation téléchargement
# Tracking des jeux téléchargés
downloaded_games = {} # Dict {platform_name: {game_name: {"timestamp": "...", "size": "..."}}}
+# Scraper de métadonnées
+scraper_image_surface = None # Surface Pygame contenant l'image scrapée
+scraper_image_url = "" # URL de l'image actuellement affichée
+scraper_game_name = "" # Nom du jeu en cours de scraping
+scraper_platform_name = "" # Nom de la plateforme en cours de scraping
+scraper_loading = False # Indicateur de chargement en cours
+scraper_error_message = "" # Message d'erreur du scraper
+scraper_description = "" # Description du jeu
+scraper_genre = "" # Genre(s) du jeu
+scraper_release_date = "" # Date de sortie du jeu
+scraper_game_page_url = "" # URL de la page du jeu sur TheGamesDB
+
# CLES API / PREMIUM HOSTS
API_KEY_1FICHIER = ""
API_KEY_ALLDEBRID = ""
@@ -328,6 +340,10 @@ current_music_name = None # Nom de la piste musicale actuelle
music_popup_start_time = 0 # Timestamp de début du popup musique
error_message = "" # Message d'erreur à afficher
+# Détection d'appui long sur confirm (menu game)
+confirm_press_start_time = 0 # Timestamp du début de l'appui sur confirm
+confirm_long_press_threshold = 2000 # Durée en ms pour déclencher l'appui long (2 secondes)
+confirm_long_press_triggered = False # Flag pour éviter de déclencher plusieurs fois
# Tenter la récupération de la famille de police sauvegardée
try:
diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py
index 291e0db..c2ee5c0 100644
--- a/ports/RGSX/controls.py
+++ b/ports/RGSX/controls.py
@@ -46,6 +46,7 @@ VALID_STATES = [
"history_game_options", # menu options pour un jeu de l'historique
"history_show_folder", # afficher le dossier de téléchargement
"history_scraper_info", # info scraper non implémenté
+ "scraper", # écran du scraper avec métadonnées
"history_error_details", # détails de l'erreur
"history_confirm_delete", # confirmation suppression jeu
"history_extract_archive" # extraction d'archive
@@ -706,100 +707,15 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "reload_games_data"
config.needs_redraw = True
logger.debug("Passage à reload_games_data depuis game")
- # Télécharger le jeu courant
+ # Télécharger le jeu courant (ou scraper si appui long)
elif is_input_matched(event, "confirm"):
- if games:
- url = games[config.current_game][1]
- game_name = games[config.current_game][0]
- platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
- logger.debug(f"Vérification pour {game_name}, URL: {url}")
- # Vérifier d'abord l'extension avant d'ajouter à l'historique
- if is_1fichier_url(url):
- from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
- ensure_download_provider_keys(False)
-
- # Avertissement si pas de clé (utilisation mode gratuit)
- if missing_all_provider_keys():
- logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
-
- config.pending_download = check_extension_before_download(url, platform, game_name)
- if config.pending_download:
- is_supported = is_extension_supported(
- sanitize_filename(game_name),
- platform,
- load_extensions_json()
- )
- zip_ok = bool(config.pending_download[3])
- allow_unknown = False
- try:
- from rgsx_settings import get_allow_unknown_extensions
- allow_unknown = get_allow_unknown_extensions()
- except Exception:
- allow_unknown = False
- if (not is_supported and not zip_ok) and not allow_unknown:
- config.previous_menu_state = config.menu_state
- config.menu_state = "extension_warning"
- config.extension_confirm_selection = 0
- config.needs_redraw = True
- logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
- else:
- task_id = str(pygame.time.get_ticks())
- task = asyncio.create_task(download_from_1fichier(url, platform, game_name, config.pending_download[3], task_id))
- config.download_tasks[task_id] = (task, url, game_name, platform)
- # Afficher un toast de notification
- show_toast(f"{_('download_started')}: {game_name}")
- config.needs_redraw = True
- logger.debug(f"Début du téléchargement 1fichier: {game_name} pour {platform} depuis {url}, task_id={task_id}")
- config.pending_download = None
- action = "download"
- else:
- config.menu_state = "error"
- config.error_message = "Extension non supportée ou erreur de téléchargement"
- config.pending_download = None
- config.needs_redraw = True
- logger.error(f"config.pending_download est None pour {game_name}")
- else:
- config.pending_download = check_extension_before_download(url, platform, game_name)
- if config.pending_download:
- extensions_data = load_extensions_json()
- logger.debug(f"Extensions chargées: {len(extensions_data)} systèmes")
- is_supported = is_extension_supported(
- sanitize_filename(game_name),
- platform,
- extensions_data
- )
- zip_ok = bool(config.pending_download[3])
- allow_unknown = False
- try:
- from rgsx_settings import get_allow_unknown_extensions
- allow_unknown = get_allow_unknown_extensions()
- except Exception:
- allow_unknown = False
- logger.debug(f"Extension check pour {game_name}: is_supported={is_supported}, zip_ok={zip_ok}, allow_unknown={allow_unknown}")
- if (not is_supported and not zip_ok) and not allow_unknown:
- config.previous_menu_state = config.menu_state
- config.menu_state = "extension_warning"
- config.extension_confirm_selection = 0
- config.needs_redraw = True
- logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
- else:
- task_id = str(pygame.time.get_ticks())
- task = asyncio.create_task(download_rom(url, platform, game_name, config.pending_download[3], task_id))
- config.download_tasks[task_id] = (task, url, game_name, platform)
- # Afficher un toast de notification
- show_toast(f"{_('download_started')}: {game_name}")
- config.needs_redraw = True
- config.pending_download = None
- action = "download"
- else:
- config.menu_state = "error"
- try:
- config.error_message = _("error_invalid_download_data")
- except Exception:
- config.error_message = "Invalid download data"
- config.pending_download = None
- config.needs_redraw = True
- logger.error(f"config.pending_download est None pour {game_name}")
+ # Détecter le début de l'appui
+ if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN):
+ config.confirm_press_start_time = current_time
+ config.confirm_long_press_triggered = False
+ logger.debug(f"Début appui confirm à {current_time}")
+ # NE PAS télécharger immédiatement, attendre le relâchement
+ # pour déterminer si c'est un appui long ou court
# Avertissement extension
elif config.menu_state == "extension_warning":
@@ -1024,8 +940,9 @@ def handle_controls(event, sources, joystick, screen):
# Déterminer les options disponibles selon le statut
options = []
- # Option commune: dossier de téléchargement
- options.append("download_folder")
+
+ # Option commune: scraper (toujours disponible)
+ options.append("scraper")
# Options selon statut
if status == "Download_OK" or status == "Completed":
@@ -1034,15 +951,15 @@ def handle_controls(event, sources, joystick, screen):
ext = os.path.splitext(actual_filename)[1].lower()
if ext in ['.zip', '.rar']:
options.append("extract_archive")
- # Scraper et suppression uniquement si le fichier existe
- if file_exists:
- options.append("scraper")
- options.append("delete_game")
elif status in ["Erreur", "Error", "Canceled"]:
options.append("error_info")
options.append("retry")
- options.append("delete_game")
+ # Options communes si le fichier existe
+ if file_exists:
+ options.append("download_folder")
+ options.append("delete_game")
+
# Option commune: retour
options.append("back")
@@ -1084,11 +1001,60 @@ def handle_controls(event, sources, joystick, screen):
logger.debug(f"Extraction de l'archive {game_name}")
elif selected_option == "scraper":
- # Scraper (non implémenté pour le moment)
+ # Lancer le scraper
config.previous_menu_state = "history_game_options"
- config.menu_state = "history_scraper_info"
+ config.menu_state = "scraper"
+ config.scraper_game_name = game_name
+ config.scraper_platform_name = platform
+ config.scraper_loading = True
+ config.scraper_error_message = ""
+ config.scraper_image_surface = None
+ config.scraper_image_url = ""
+ config.scraper_description = ""
+ config.scraper_genre = ""
+ config.scraper_release_date = ""
+ config.scraper_game_page_url = ""
config.needs_redraw = True
- logger.debug(f"Scraper pour {game_name} (non implémenté)")
+ logger.debug(f"Lancement du scraper pour {game_name}")
+
+ # Lancer la recherche des métadonnées dans un thread séparé
+
+ def scrape_async():
+ from scraper import get_game_metadata, download_image_to_surface
+ logger.info(f"Scraping métadonnées pour {game_name} sur {platform}")
+ metadata = get_game_metadata(game_name, platform)
+
+ # Vérifier si on a une erreur
+ if "error" in metadata:
+ config.scraper_error_message = metadata["error"]
+ config.scraper_loading = False
+ config.needs_redraw = True
+ logger.error(f"Erreur de scraping: {metadata['error']}")
+ return
+
+ # Mettre à jour les métadonnées textuelles
+ config.scraper_description = metadata.get("description", "")
+ config.scraper_genre = metadata.get("genre", "")
+ config.scraper_release_date = metadata.get("release_date", "")
+ config.scraper_game_page_url = metadata.get("game_page_url", "")
+
+ # Télécharger l'image si disponible
+ image_url = metadata.get("image_url")
+ if image_url:
+ logger.info(f"Téléchargement de l'image: {image_url}")
+ image_surface = download_image_to_surface(image_url)
+ if image_surface:
+ config.scraper_image_surface = image_surface
+ config.scraper_image_url = image_url
+ else:
+ logger.warning("Échec du téléchargement de l'image")
+
+ config.scraper_loading = False
+ config.needs_redraw = True
+ logger.info("Scraping terminé")
+
+ thread = threading.Thread(target=scrape_async, daemon=True)
+ thread.start()
elif selected_option == "delete_game":
# Demander confirmation avant suppression
@@ -1153,7 +1119,26 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
- # Information scraper (non implémenté)
+ # Scraper
+ elif config.menu_state == "scraper":
+ if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
+ logger.info(f"Scraper: fermeture demandée")
+ # Retour au menu précédent
+ config.menu_state = validate_menu_state(config.previous_menu_state)
+ # Nettoyer les variables du scraper
+ config.scraper_image_surface = None
+ config.scraper_image_url = ""
+ config.scraper_game_name = ""
+ config.scraper_platform_name = ""
+ config.scraper_loading = False
+ config.scraper_error_message = ""
+ config.scraper_description = ""
+ config.scraper_genre = ""
+ config.scraper_release_date = ""
+ config.scraper_game_page_url = ""
+ config.needs_redraw = True
+
+ # Information scraper (ancien, gardé pour compatibilité)
elif config.menu_state == "history_scraper_info":
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
config.menu_state = validate_menu_state(config.previous_menu_state)
@@ -1224,7 +1209,7 @@ def handle_controls(event, sources, joystick, screen):
entry = config.history[config.current_history_item]
platform = entry.get("platform", "")
- import threading
+ # threading est déjà importé en haut du fichier (ligne 8)
# Utiliser le chemin réel trouvé (avec ou sans extension)
file_path = getattr(config, 'history_actual_path', None)
@@ -1995,6 +1980,104 @@ def handle_controls(event, sources, joystick, screen):
if config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key:
update_key_state(action_name, False)
+
+ # Gestion spéciale pour confirm dans le menu game
+ if action_name == "confirm" and config.menu_state == "game":
+ press_duration = current_time - config.confirm_press_start_time
+ # Si appui court (< 2 secondes) et pas déjà traité par l'appui long
+ if press_duration < config.confirm_long_press_threshold and not config.confirm_long_press_triggered:
+ # Déclencher le téléchargement normal
+ games = config.filtered_games if config.filter_active or config.search_mode else config.games
+ if games:
+ url = games[config.current_game][1]
+ game_name = games[config.current_game][0]
+ platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
+ logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
+
+ # Vérifier d'abord l'extension avant d'ajouter à l'historique
+ if is_1fichier_url(url):
+ from utils import ensure_download_provider_keys, missing_all_provider_keys
+ ensure_download_provider_keys(False)
+
+ # Avertissement si pas de clé (utilisation mode gratuit)
+ if missing_all_provider_keys():
+ logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
+
+ config.pending_download = check_extension_before_download(url, platform, game_name)
+ if config.pending_download:
+ is_supported = is_extension_supported(
+ sanitize_filename(game_name),
+ platform,
+ load_extensions_json()
+ )
+ zip_ok = bool(config.pending_download[3])
+ allow_unknown = False
+ try:
+ from rgsx_settings import get_allow_unknown_extensions
+ allow_unknown = get_allow_unknown_extensions()
+ except Exception:
+ allow_unknown = False
+ if (not is_supported and not zip_ok) and not allow_unknown:
+ config.previous_menu_state = config.menu_state
+ config.menu_state = "extension_warning"
+ config.extension_confirm_selection = 0
+ config.needs_redraw = True
+ logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
+ else:
+ task_id = str(pygame.time.get_ticks())
+ task = asyncio.create_task(download_from_1fichier(url, platform, game_name, config.pending_download[3], task_id))
+ config.download_tasks[task_id] = (task, url, game_name, platform)
+ show_toast(f"{_('download_started')}: {game_name}")
+ config.needs_redraw = True
+ logger.debug(f"Début du téléchargement 1fichier: {game_name} pour {platform}, task_id={task_id}")
+ config.pending_download = None
+ else:
+ config.menu_state = "error"
+ config.error_message = "Extension non supportée ou erreur de téléchargement"
+ config.pending_download = None
+ config.needs_redraw = True
+ logger.error(f"config.pending_download est None pour {game_name}")
+ else:
+ config.pending_download = check_extension_before_download(url, platform, game_name)
+ if config.pending_download:
+ extensions_data = load_extensions_json()
+ is_supported = is_extension_supported(
+ sanitize_filename(game_name),
+ platform,
+ extensions_data
+ )
+ zip_ok = bool(config.pending_download[3])
+ allow_unknown = False
+ try:
+ from rgsx_settings import get_allow_unknown_extensions
+ allow_unknown = get_allow_unknown_extensions()
+ except Exception:
+ allow_unknown = False
+ if (not is_supported and not zip_ok) and not allow_unknown:
+ config.previous_menu_state = config.menu_state
+ config.menu_state = "extension_warning"
+ config.extension_confirm_selection = 0
+ config.needs_redraw = True
+ logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
+ else:
+ task_id = str(pygame.time.get_ticks())
+ task = asyncio.create_task(download_rom(url, platform, game_name, config.pending_download[3], task_id))
+ config.download_tasks[task_id] = (task, url, game_name, platform)
+ show_toast(f"{_('download_started')}: {game_name}")
+ config.needs_redraw = True
+ config.pending_download = None
+ else:
+ config.menu_state = "error"
+ try:
+ config.error_message = _("error_invalid_download_data")
+ except Exception:
+ config.error_message = "Invalid download data"
+ config.pending_download = None
+ config.needs_redraw = True
+ logger.error(f"config.pending_download est None pour {game_name}")
+ # Réinitialiser les flags
+ config.confirm_press_start_time = 0
+ config.confirm_long_press_triggered = False
elif event.type == pygame.JOYBUTTONUP:
# Vérifier quel bouton a été relâché
@@ -2002,6 +2085,104 @@ def handle_controls(event, sources, joystick, screen):
if config.controls_config.get(action_name, {}).get("type") == "button" and \
config.controls_config.get(action_name, {}).get("button") == event.button:
update_key_state(action_name, False)
+
+ # Gestion spéciale pour confirm dans le menu game
+ if action_name == "confirm" and config.menu_state == "game":
+ press_duration = current_time - config.confirm_press_start_time
+ # Si appui court (< 2 secondes) et pas déjà traité par l'appui long
+ if press_duration < config.confirm_long_press_threshold and not config.confirm_long_press_triggered:
+ # Déclencher le téléchargement normal (même code que pour KEYUP)
+ games = config.filtered_games if config.filter_active or config.search_mode else config.games
+ if games:
+ url = games[config.current_game][1]
+ game_name = games[config.current_game][0]
+ platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
+ logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
+
+ # Vérifier d'abord l'extension avant d'ajouter à l'historique
+ if is_1fichier_url(url):
+ from utils import ensure_download_provider_keys, missing_all_provider_keys
+ ensure_download_provider_keys(False)
+
+ # Avertissement si pas de clé (utilisation mode gratuit)
+ if missing_all_provider_keys():
+ logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
+
+ config.pending_download = check_extension_before_download(url, platform, game_name)
+ if config.pending_download:
+ is_supported = is_extension_supported(
+ sanitize_filename(game_name),
+ platform,
+ load_extensions_json()
+ )
+ zip_ok = bool(config.pending_download[3])
+ allow_unknown = False
+ try:
+ from rgsx_settings import get_allow_unknown_extensions
+ allow_unknown = get_allow_unknown_extensions()
+ except Exception:
+ allow_unknown = False
+ if (not is_supported and not zip_ok) and not allow_unknown:
+ config.previous_menu_state = config.menu_state
+ config.menu_state = "extension_warning"
+ config.extension_confirm_selection = 0
+ config.needs_redraw = True
+ logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
+ else:
+ task_id = str(pygame.time.get_ticks())
+ task = asyncio.create_task(download_from_1fichier(url, platform, game_name, config.pending_download[3], task_id))
+ config.download_tasks[task_id] = (task, url, game_name, platform)
+ show_toast(f"{_('download_started')}: {game_name}")
+ config.needs_redraw = True
+ logger.debug(f"Début du téléchargement 1fichier: {game_name} pour {platform}, task_id={task_id}")
+ config.pending_download = None
+ else:
+ config.menu_state = "error"
+ config.error_message = "Extension non supportée ou erreur de téléchargement"
+ config.pending_download = None
+ config.needs_redraw = True
+ logger.error(f"config.pending_download est None pour {game_name}")
+ else:
+ config.pending_download = check_extension_before_download(url, platform, game_name)
+ if config.pending_download:
+ extensions_data = load_extensions_json()
+ is_supported = is_extension_supported(
+ sanitize_filename(game_name),
+ platform,
+ extensions_data
+ )
+ zip_ok = bool(config.pending_download[3])
+ allow_unknown = False
+ try:
+ from rgsx_settings import get_allow_unknown_extensions
+ allow_unknown = get_allow_unknown_extensions()
+ except Exception:
+ allow_unknown = False
+ if (not is_supported and not zip_ok) and not allow_unknown:
+ config.previous_menu_state = config.menu_state
+ config.menu_state = "extension_warning"
+ config.extension_confirm_selection = 0
+ config.needs_redraw = True
+ logger.debug(f"Extension non supportée, passage à extension_warning pour {game_name}")
+ else:
+ task_id = str(pygame.time.get_ticks())
+ task = asyncio.create_task(download_rom(url, platform, game_name, config.pending_download[3], task_id))
+ config.download_tasks[task_id] = (task, url, game_name, platform)
+ show_toast(f"{_('download_started')}: {game_name}")
+ config.needs_redraw = True
+ config.pending_download = None
+ else:
+ config.menu_state = "error"
+ try:
+ config.error_message = _("error_invalid_download_data")
+ except Exception:
+ config.error_message = "Invalid download data"
+ config.pending_download = None
+ config.needs_redraw = True
+ logger.error(f"config.pending_download est None pour {game_name}")
+ # Réinitialiser les flags
+ config.confirm_press_start_time = 0
+ config.confirm_long_press_triggered = False
elif event.type == pygame.JOYAXISMOTION and abs(event.value) < 0.5:
# Vérifier quel axe a été relâché
diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py
index 2ebfb6a..d37163d 100644
--- a/ports/RGSX/display.py
+++ b/ports/RGSX/display.py
@@ -762,7 +762,7 @@ def draw_game_list(screen):
logger.debug("Aucune liste de jeux disponible")
message = _("game_no_games")
lines = wrap_text(message, config.font, config.screen_width - 80)
- line_height = config.font1.get_height() + 5
+ line_height = config.font.get_height() + 5
text_height = len(lines) * line_height
margin_top_bottom = 20
rect_height = text_height + 2 * margin_top_bottom
@@ -1464,6 +1464,10 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
("history", i18n("controls_action_close_history")),
("cancel", i18n("controls_cancel_back")),
],
+ "scraper": [
+ ("confirm", i18n("controls_confirm_select")),
+ ("cancel", i18n("controls_cancel_back")),
+ ],
"error": [
("confirm", i18n("controls_confirm_select")),
],
@@ -2770,10 +2774,11 @@ def draw_history_game_options(screen):
options = []
option_labels = []
- # Option commune: dossier de téléchargement
- options.append("download_folder")
- option_labels.append(_("history_option_download_folder"))
-
+ # Options communes
+
+ options.append("scraper")
+ option_labels.append(_("history_option_scraper"))
+
# Options selon statut
if status == "Download_OK" or status == "Completed":
# Vérifier si c'est une archive ET si le fichier existe
@@ -2782,21 +2787,18 @@ def draw_history_game_options(screen):
if ext in ['.zip', '.rar']:
options.append("extract_archive")
option_labels.append(_("history_option_extract_archive"))
- # Scraper et suppression uniquement si le fichier existe
- if file_exists:
- options.append("scraper")
- option_labels.append(_("history_option_scraper"))
- options.append("delete_game")
- option_labels.append(_("history_option_delete_game"))
elif status in ["Erreur", "Error", "Canceled"]:
options.append("error_info")
option_labels.append(_("history_option_error_info"))
options.append("retry")
option_labels.append(_("history_option_retry"))
+
+ # Options communes
+ if file_exists:
+ options.append("download_folder")
+ option_labels.append(_("history_option_download_folder"))
options.append("delete_game")
option_labels.append(_("history_option_delete_game"))
-
- # Option commune: retour
options.append("back")
option_labels.append(_("history_option_back"))
@@ -3060,3 +3062,176 @@ def draw_history_extract_archive(screen):
draw_stylized_button(screen, _("button_OK"), rect_x + (rect_width - button_width) // 2, rect_y + text_height + margin_top_bottom, button_width, button_height, selected=True)
+
+
+def draw_scraper_screen(screen):
+ screen.blit(OVERLAY, (0, 0))
+
+ # Dimensions de l'écran avec marge pour les contrôles en bas
+ margin = 40
+ # Calcul exact de la position des contrôles (même formule que draw_controls)
+ controls_y = config.screen_height - int(config.screen_height * 0.037)
+ bottom_margin = 10
+
+ rect_width = config.screen_width - 2 * margin
+ rect_height = controls_y - 2 * margin - bottom_margin
+ rect_x = margin
+ rect_y = margin
+
+ # Fond principal
+ 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)
+
+ # Titre
+ title_text = f"Scraper: {config.scraper_game_name}"
+ title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
+ title_rect = title_surface.get_rect(center=(config.screen_width // 2, rect_y + 40))
+ screen.blit(title_surface, title_rect)
+
+ # Sous-titre avec plateforme
+ subtitle_text = f"Platform: {config.scraper_platform_name}"
+ subtitle_surface = config.font.render(subtitle_text, True, THEME_COLORS["title_text"])
+ subtitle_rect = subtitle_surface.get_rect(center=(config.screen_width // 2, rect_y + 80))
+ screen.blit(subtitle_surface, subtitle_rect)
+
+ # Zone de contenu (après titre et sous-titre)
+ content_y = rect_y + 120
+ content_height = rect_height - 140 # Ajusté pour ne pas inclure les marges du bas
+
+ # Si chargement en cours
+ if config.scraper_loading:
+ loading_text = "Searching for metadata..."
+ loading_surface = config.font.render(loading_text, True, THEME_COLORS["text"])
+ loading_rect = loading_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
+ screen.blit(loading_surface, loading_rect)
+
+ # Si erreur
+ elif config.scraper_error_message:
+ error_lines = wrap_text(config.scraper_error_message, config.font, rect_width - 80)
+ line_height = config.font.get_height() + 10
+ start_y = config.screen_height // 2 - (len(error_lines) * line_height) // 2
+
+ for i, line in enumerate(error_lines):
+ error_surface = config.font.render(line, True, THEME_COLORS["error_text"])
+ error_rect = error_surface.get_rect(center=(config.screen_width // 2, start_y + i * line_height))
+ screen.blit(error_surface, error_rect)
+
+ # Si données disponibles
+ else:
+ # Division en deux colonnes: image à gauche, métadonnées à droite
+ left_width = int(rect_width * 0.4)
+ right_width = rect_width - left_width - 20
+ left_x = rect_x + 20
+ right_x = left_x + left_width + 20
+
+ # === COLONNE GAUCHE: IMAGE ===
+ if config.scraper_image_surface:
+ # Calculer la taille max pour l'image
+ max_image_width = left_width - 20
+ max_image_height = content_height - 20
+
+ # Redimensionner l'image en conservant le ratio
+ image = config.scraper_image_surface
+ img_width, img_height = image.get_size()
+
+ # Calculer le ratio de redimensionnement
+ width_ratio = max_image_width / img_width
+ height_ratio = max_image_height / img_height
+ scale_ratio = min(width_ratio, height_ratio, 1.0)
+
+ new_width = int(img_width * scale_ratio)
+ new_height = int(img_height * scale_ratio)
+
+ # Redimensionner l'image
+ scaled_image = pygame.transform.smoothscale(image, (new_width, new_height))
+
+ # Centrer l'image dans la colonne gauche
+ image_x = left_x + (left_width - new_width) // 2
+ image_y = content_y + (content_height - new_height) // 2
+
+ # Fond derrière l'image
+ padding = 10
+ bg_rect = pygame.Rect(image_x - padding, image_y - padding, new_width + 2 * padding, new_height + 2 * padding)
+ pygame.draw.rect(screen, THEME_COLORS["fond_image"], bg_rect, border_radius=8)
+ pygame.draw.rect(screen, THEME_COLORS["neon"], bg_rect, 2, border_radius=8)
+
+ # Afficher l'image
+ screen.blit(scaled_image, (image_x, image_y))
+ else:
+ # Pas d'image disponible
+ no_image_text = "No image available"
+ no_image_surface = config.font.render(no_image_text, True, THEME_COLORS["title_text"])
+ no_image_rect = no_image_surface.get_rect(center=(left_x + left_width // 2, content_y + content_height // 2))
+ screen.blit(no_image_surface, no_image_rect)
+
+ # === COLONNE DROITE: METADONNEES (centrées verticalement) ===
+ line_height = config.font.get_height() + 8
+ small_line_height = config.small_font.get_height() + 5
+
+ # Calculer la hauteur totale des métadonnées pour centrer verticalement
+ total_metadata_height = 0
+
+ # Compter les lignes de genre
+ if config.scraper_genre:
+ total_metadata_height += line_height * 2 + 10 # Label + valeur + espace
+
+ # Compter les lignes de date
+ if config.scraper_release_date:
+ total_metadata_height += line_height * 2 + 10 # Label + valeur + espace
+
+ # Compter les lignes de description
+ if config.scraper_description:
+ desc_lines = wrap_text(config.scraper_description, config.small_font, right_width - 100)
+ max_desc_lines = min(len(desc_lines), int((content_height - total_metadata_height - 100) / small_line_height))
+ total_metadata_height += line_height + 5 # Label + espace
+ total_metadata_height += max_desc_lines * small_line_height
+
+ # Calculer le Y de départ pour centrer verticalement
+ metadata_y = content_y + (content_height - total_metadata_height) // 2
+
+ # Genre
+ if config.scraper_genre:
+ genre_label = config.font.render("Genre:", True, THEME_COLORS["neon"])
+ screen.blit(genre_label, (right_x, metadata_y))
+ metadata_y += line_height
+
+ genre_value = config.font.render(config.scraper_genre, True, THEME_COLORS["text"])
+ screen.blit(genre_value, (right_x + 10, metadata_y))
+ metadata_y += line_height + 10
+
+ # Date de sortie
+ if config.scraper_release_date:
+ date_label = config.font.render("Release Date:", True, THEME_COLORS["neon"])
+ screen.blit(date_label, (right_x, metadata_y))
+ metadata_y += line_height
+
+ date_value = config.font.render(config.scraper_release_date, True, THEME_COLORS["text"])
+ screen.blit(date_value, (right_x + 10, metadata_y))
+ metadata_y += line_height + 10
+
+ # Description
+ if config.scraper_description:
+ desc_label = config.font.render("Description:", True, THEME_COLORS["neon"])
+ screen.blit(desc_label, (right_x, metadata_y))
+ metadata_y += line_height + 5
+
+ # Wrapper la description avec plus de padding à droite
+ desc_lines = wrap_text(config.scraper_description, config.small_font, right_width - 40)
+ max_desc_lines = min(len(desc_lines), int((content_height - (metadata_y - content_y)) / small_line_height))
+
+ for i, line in enumerate(desc_lines[:max_desc_lines]):
+ desc_surface = config.small_font.render(line, True, THEME_COLORS["text"])
+ screen.blit(desc_surface, (right_x + 10, metadata_y))
+ metadata_y += small_line_height
+
+ # Si trop de lignes, afficher "..."
+ if len(desc_lines) > max_desc_lines:
+ more_text = config.small_font.render("...", True, THEME_COLORS["title_text"])
+ screen.blit(more_text, (right_x + 10, metadata_y))
+
+ # URL de la source en bas (si disponible)
+ if config.scraper_game_page_url:
+ url_text = truncate_text_middle(config.scraper_game_page_url, config.small_font, rect_width - 80, is_filename=False)
+ url_surface = config.small_font.render(url_text, True, THEME_COLORS["title_text"])
+ url_rect = url_surface.get_rect(center=(config.screen_width // 2, rect_y + rect_height - 20))
+ screen.blit(url_surface, url_rect)
diff --git a/ports/RGSX/scraper.py b/ports/RGSX/scraper.py
new file mode 100644
index 0000000..c5ce5fe
--- /dev/null
+++ b/ports/RGSX/scraper.py
@@ -0,0 +1,294 @@
+"""
+Module de scraping pour récupérer les métadonnées des jeux depuis TheGamesDB.net
+"""
+import logging
+import requests
+import re
+from io import BytesIO
+import pygame
+
+logger = logging.getLogger(__name__)
+
+# Mapping des noms de plateformes vers leurs IDs sur TheGamesDB
+# Les noms correspondent exactement à ceux utilisés dans systems_list.json
+PLATFORM_MAPPING = {
+ # Noms exacts du systems_list.json
+ "3DO Interactive Multiplayer": "25",
+ "3DS": "4912",
+ "Adventure Vision": "4974",
+ "Amiga CD32": "4947",
+ "Amiga CDTV": "4947", # Même ID que CD32
+ "Amiga OCS ECS": "4911",
+ "Apple II": "4942",
+ "Apple IIGS": "4942", # Même famille
+ "Arcadia 2001": "4963",
+ "Archimedes": "4944",
+ "Astrocade": "4968",
+ "Atari 2600": "22",
+ "Atari 5200": "26",
+ "Atari 7800": "27",
+ "Atari Lynx": "4924",
+ "Atari ST": "4937",
+ "Atom": "5014",
+ "Channel-F": "4928",
+ "ColecoVision": "31",
+ "Commodore 64": "40",
+ "Commodore Plus4": "5007",
+ "Commodore VIC-20": "4945",
+ "CreatiVision": "5005",
+ "Dos (x86)": "1",
+ "Dreamcast": "16",
+ "Family Computer Disk System": "4936",
+ "Final Burn Neo": "23", # Arcade
+ "FM-TOWNS": "4932",
+ "Gamate": "5004",
+ "Game Boy": "4",
+ "Game Boy Advance": "5",
+ "Game Boy Color": "41",
+ "Game Cube": "2",
+ "Game Gear": "20",
+ "Game Master": "4948", # Mega Duck
+ "Game.com": "4940",
+ "Jaguar": "28",
+ "Macintosh": "37",
+ "Master System": "35",
+ "Mattel Intellivision": "32",
+ "Mega CD": "21",
+ "Mega Drive": "36",
+ "Mega Duck Cougar Boy": "4948",
+ "MSX1": "4929",
+ "MSX2+": "4929",
+ "Namco System 246 256": "23", # Arcade
+ "Naomi": "23", # Arcade
+ "Naomi 2": "23", # Arcade
+ "Neo-Geo CD": "4956",
+ "Neo-Geo Pocket": "4922",
+ "Neo-Geo Pocket Color": "4923",
+ "Neo-Geo": "24",
+ "Nintendo 64": "3",
+ "Nintendo 64 Disk Drive": "3",
+ "Nintendo DS": "8",
+ "Nintendo DSi": "8",
+ "Nintendo Entertainment System": "7",
+ "Odyssey2": "4927",
+ "PC Engine": "34",
+ "PC Engine CD": "4955",
+ "PC Engine SuperGrafx": "34",
+ "PC-9800": "4934",
+ "PlayStation": "10",
+ "PlayStation 2": "11",
+ "PlayStation 3": "12",
+ "PlayStation Portable": "13",
+ "PlayStation Vita": "39",
+ "Pokemon Mini": "4957",
+ "PV-1000": "4964",
+ "Satellaview": "6", # SNES addon
+ "Saturn": "17",
+ "ScummVM": "1", # PC
+ "Sega 32X": "33",
+ "Sega Chihiro": "23", # Arcade
+ "Sega Pico": "4958",
+ "SG-1000": "4949",
+ "Sharp X1": "4977",
+ "SuFami Turbo": "6", # SNES addon
+ "Super A'Can": "4918", # Pas d'ID exact, utilise Virtual Boy
+ "Super Cassette Vision": "4966",
+ "Super Nintendo Entertainment System": "6",
+ "Supervision": "4959",
+ "Switch (1Fichier)": "4971",
+ "TI-99": "4953",
+ "V.Smile": "4988",
+ "Vectrex": "4939",
+ "Virtual Boy": "4918",
+ "Wii": "9",
+ "Wii (Virtual Console)": "9",
+ "Wii U": "38",
+ "Windows (1Fichier)": "1",
+ "WonderSwan": "4925",
+ "WonderSwan Color": "4926",
+ "Xbox": "14",
+ "Xbox 360": "15",
+ "ZX Spectrum": "4913",
+ "Game and Watch": "4950",
+ "Nintendo Famicom Disk System": "4936",
+
+ # Aliases communs (pour compatibilité)
+ "3DO": "25",
+ "NES": "7",
+ "SNES": "6",
+ "GBA": "5",
+ "GBC": "41",
+ "GameCube": "2",
+ "N64": "3",
+ "NDS": "8",
+ "PSX": "10",
+ "PS1": "10",
+ "PS2": "11",
+ "PS3": "12",
+ "PSP": "13",
+ "PS Vita": "39",
+ "Genesis": "18",
+ "32X": "33",
+ "Game & Watch": "4950",
+ "PC-98": "4934",
+ "TurboGrafx 16": "34",
+ "TurboGrafx CD": "4955",
+ "Mega Duck": "4948",
+ "Amiga": "4911"
+}
+
+
+def get_game_metadata(game_name, platform_name):
+ """
+ Récupère les métadonnées complètes d'un jeu depuis TheGamesDB.net
+
+ Args:
+ game_name (str): Nom du jeu à rechercher
+ platform_name (str): Nom de la plateforme
+
+ Returns:
+ dict: Dictionnaire contenant les métadonnées ou message d'erreur
+ Keys: image_url, game_page_url, description, genre, release_date, error
+ """
+ # Nettoyer le nom du jeu
+ clean_game_name = game_name
+ for ext in ['.zip', '.7z', '.rar', '.iso', '.chd', '.cue', '.bin', '.gdi', '.cdi']:
+ if clean_game_name.lower().endswith(ext):
+ clean_game_name = clean_game_name[:-len(ext)]
+ clean_game_name = re.sub(r'\s*[\(\[].*?[\)\]]', '', clean_game_name)
+ clean_game_name = clean_game_name.strip()
+
+ logger.info(f"Recherche métadonnées pour: '{clean_game_name}' sur plateforme '{platform_name}'")
+
+ # Obtenir l'ID de la plateforme
+ platform_id = PLATFORM_MAPPING.get(platform_name)
+ if not platform_id:
+ return {"error": f"Plateforme '{platform_name}' non supportée"}
+
+ # Construire l'URL de recherche
+ base_url = "https://thegamesdb.net/search.php"
+ params = {
+ "name": clean_game_name,
+ "platform_id[]": platform_id
+ }
+
+ try:
+ # Envoyer la requête GET pour la recherche
+ logger.debug(f"Recherche sur TheGamesDB: {base_url} avec params={params}")
+ response = requests.get(base_url, params=params, timeout=10)
+
+ if response.status_code != 200:
+ return {"error": f"Erreur HTTP {response.status_code}"}
+
+ html_content = response.text
+
+ # Trouver la première carte avec class 'card border-primary'
+ card_start = html_content.find('div class="card border-primary"')
+ if card_start == -1:
+ return {"error": "Aucun résultat trouvé"}
+
+ # Extraire l'URL de la page du jeu
+ href_match = re.search(r'', html_content[card_start-100:card_start+500])
+ game_page_url = None
+ if href_match:
+ game_page_url = f"https://thegamesdb.net/{href_match.group(1)[2:]}" # Enlever le ./
+ logger.info(f"Page du jeu trouvée: {game_page_url}")
+
+ # Extraire l'URL de l'image
+ img_start = html_content.find('
(\d{4}-\d{2}-\d{2})
', html_content[card_footer_start:card_footer_start+300])
+ if date_match:
+ release_date = date_match.group(1)
+ logger.info(f"Date de sortie trouvée: {release_date}")
+
+ # Si on a l'URL de la page, récupérer la description et le genre
+ description = None
+ genre = None
+ if game_page_url:
+ try:
+ logger.debug(f"Récupération de la page du jeu: {game_page_url}")
+ game_response = requests.get(game_page_url, timeout=10)
+
+ if game_response.status_code == 200:
+ game_html = game_response.text
+
+ # Extraire la description
+ desc_match = re.search(r'(.*?)
', game_html, re.DOTALL)
+ if desc_match:
+ description = desc_match.group(1).strip()
+ # Nettoyer les entités HTML
+ description = description.replace(''', "'")
+ description = description.replace('"', '"')
+ description = description.replace('&', '&')
+ logger.info(f"Description trouvée ({len(description)} caractères)")
+
+ # Extraire le genre
+ genre_match = re.search(r'Genre\(s\): (.*?)
', game_html)
+ if genre_match:
+ genre = genre_match.group(1).strip()
+ logger.info(f"Genre trouvé: {genre}")
+
+ except Exception as e:
+ logger.warning(f"Erreur lors de la récupération de la page du jeu: {e}")
+
+ # Construire le résultat
+ result = {
+ "image_url": image_url,
+ "game_page_url": game_page_url,
+ "description": description,
+ "genre": genre,
+ "release_date": release_date
+ }
+
+ # Vérifier qu'on a au moins quelque chose
+ if not any([image_url, description, genre]):
+ result["error"] = "Métadonnées incomplètes"
+
+ return result
+
+ except requests.RequestException as e:
+ logger.error(f"Erreur lors de la requête: {str(e)}")
+ return {"error": f"Erreur réseau: {str(e)}"}
+
+
+def download_image_to_surface(image_url):
+ """
+ Télécharge une image depuis une URL et la convertit en surface Pygame
+
+ Args:
+ image_url (str): URL de l'image à télécharger
+
+ Returns:
+ pygame.Surface ou None: Surface Pygame contenant l'image, ou None en cas d'erreur
+ """
+ try:
+ logger.debug(f"Téléchargement de l'image: {image_url}")
+ response = requests.get(image_url, timeout=10)
+
+ if response.status_code != 200:
+ logger.error(f"Erreur HTTP {response.status_code} lors du téléchargement de l'image")
+ return None
+
+ # Charger l'image depuis les bytes
+ image_data = BytesIO(response.content)
+ image_surface = pygame.image.load(image_data)
+ logger.info("Image téléchargée et chargée avec succès")
+ return image_surface
+
+ except Exception as e:
+ logger.error(f"Erreur lors du téléchargement de l'image: {str(e)}")
+ return None
diff --git a/ports/RGSX/utils.py b/ports/RGSX/utils.py
index aa548a3..1e01e20 100644
--- a/ports/RGSX/utils.py
+++ b/ports/RGSX/utils.py
@@ -1338,16 +1338,33 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
# Continuer quand même, l'erreur sera capturée plus tard
if config.OPERATING_SYSTEM == "Windows":
+ # Utiliser des guillemets doubles et échapper correctement pour PowerShell
+ # Doubler les guillemets doubles internes pour l'échappement PowerShell
+ dkey_escaped = dkey_path.replace('"', '""')
+ ps3dec_escaped = config.PS3DEC_EXE.replace('"', '""')
+ iso_escaped = iso_path.replace('"', '""')
+ decrypted_escaped = decrypted_iso_path.replace('"', '""')
+
cmd = [
"powershell", "-Command",
- f"$key = (Get-Content '{dkey_path}' -Raw).Trim(); " +
- f"& '{config.PS3DEC_EXE}' d key $key '{iso_path}' '{decrypted_iso_path}'"
+ f'$key = (Get-Content "{dkey_escaped}" -Raw).Trim(); ' +
+ f'& "{ps3dec_escaped}" d key $key "{iso_escaped}" "{decrypted_escaped}"'
]
else: # Linux
+ # Utiliser des guillemets doubles avec échappement pour bash
+ # Échapper les caractères spéciaux: $, `, \, ", et !
+ def bash_escape(path):
+ return path.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$').replace('`', '\\`')
+
+ dkey_escaped = bash_escape(dkey_path)
+ ps3dec_escaped = bash_escape(config.PS3DEC_LINUX)
+ iso_escaped = bash_escape(iso_path)
+ decrypted_escaped = bash_escape(decrypted_iso_path)
+
cmd = [
"bash", "-c",
- f"key=$(cat '{dkey_path}' | tr -d ' \\n\\r\\t'); " +
- f"'{config.PS3DEC_LINUX}' d key \"$key\" '{iso_path}' '{decrypted_iso_path}'"
+ f'key=$(cat "{dkey_escaped}" | tr -d \' \\n\\r\\t\'); ' +
+ f'"{ps3dec_escaped}" d key "$key" "{iso_escaped}" "{decrypted_escaped}"'
]
logger.debug(f"Commande de décryptage: {' '.join(cmd)}")