mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-26 21:54:46 +01:00
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
This commit is contained in:
@@ -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)`
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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é
|
||||
|
||||
@@ -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)
|
||||
|
||||
294
ports/RGSX/scraper.py
Normal file
294
ports/RGSX/scraper.py
Normal file
@@ -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'<a href="(\.\/game\.php\?id=\d+)">', 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('<img class="card-img-top"', card_start)
|
||||
image_url = None
|
||||
if img_start != -1:
|
||||
src_match = re.search(r'src="([^"]+)"', html_content[img_start:img_start+200])
|
||||
if src_match:
|
||||
image_url = src_match.group(1)
|
||||
if not image_url.startswith("https://"):
|
||||
image_url = f"https://thegamesdb.net{image_url}"
|
||||
logger.info(f"Image trouvée: {image_url}")
|
||||
|
||||
# Extraire la date de sortie depuis les résultats de recherche
|
||||
release_date = None
|
||||
card_footer_start = html_content.find('class="card-footer', card_start)
|
||||
if card_footer_start != -1:
|
||||
# Chercher une date au format YYYY-MM-DD
|
||||
date_match = re.search(r'<p>(\d{4}-\d{2}-\d{2})</p>', 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'<p class="game-overview">(.*?)</p>', 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'<p>Genre\(s\): (.*?)</p>', 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
|
||||
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user