mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-20 16:55:39 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbc741617 | ||
|
|
c4913a5fc2 | ||
|
|
bf9d3d2de5 | ||
|
|
9979949bdc | ||
|
|
9ed264544f | ||
|
|
779c060927 | ||
|
|
88400e538f | ||
|
|
cbab067dd6 | ||
|
|
b4ed0b355d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ pygame/
|
||||
data/
|
||||
docker-compose.test.yml
|
||||
config/
|
||||
pyrightconfig.json
|
||||
|
||||
@@ -526,7 +526,7 @@ async def main():
|
||||
# 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]
|
||||
game_name = games[config.current_game].name
|
||||
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"
|
||||
@@ -835,12 +835,9 @@ async def main():
|
||||
logger.debug("Action quit détectée, arrêt de l'application")
|
||||
elif action == "download" and config.menu_state == "game" and config.filtered_games:
|
||||
game = config.filtered_games[config.current_game]
|
||||
if isinstance(game, (list, tuple)):
|
||||
game_name = game[0]
|
||||
url = game[1] if len(game) > 1 else None
|
||||
else: # fallback str
|
||||
game_name = str(game)
|
||||
url = None
|
||||
game_name = game.name
|
||||
url = game.url
|
||||
|
||||
# Nouveau schéma: config.platforms contient déjà platform_name (string)
|
||||
platform_name = config.platforms[config.current_platform]
|
||||
if url:
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Game:
|
||||
name: str
|
||||
url: str
|
||||
size: str
|
||||
display_name: str # name withou file extension or platform prefix
|
||||
|
||||
# Headless mode for CLI: set env RGSX_HEADLESS=1 to avoid pygame and noisy prints
|
||||
HEADLESS = os.environ.get("RGSX_HEADLESS") == "1"
|
||||
@@ -14,7 +23,7 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.5.0.4"
|
||||
app_version = "2.5.0.6"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 7
|
||||
@@ -364,7 +373,8 @@ filter_platforms_dirty = False # indique si modifications non sauvegardées
|
||||
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
|
||||
|
||||
# Affichage des jeux et sélection
|
||||
games = [] # Liste des jeux pour la plateforme actuelle
|
||||
games: list[Game] = [] # Liste des jeux pour la plateforme actuelle
|
||||
fbneo_games = {}
|
||||
current_game = 0 # Index du jeu actuellement sélectionné
|
||||
menu_state = "loading" # État actuel de l'interface menu
|
||||
scroll_offset = 0 # Offset de défilement pour la liste des jeux
|
||||
@@ -389,7 +399,7 @@ selected_language_index = 0 # Index de la langue sélectionnée dans la liste
|
||||
|
||||
|
||||
# Recherche et filtres
|
||||
filtered_games = [] # Liste des jeux filtrés par recherche ou filtre
|
||||
filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filtre
|
||||
search_mode = False # Indicateur si le mode recherche est actif
|
||||
search_query = "" # Chaîne de recherche saisie par l'utilisateur
|
||||
filter_active = False # Indicateur si un filtre est appliqué
|
||||
|
||||
@@ -8,7 +8,7 @@ import datetime
|
||||
import threading
|
||||
import logging
|
||||
import config
|
||||
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH
|
||||
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH, Game
|
||||
from display import draw_validation_transition, show_toast
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel
|
||||
from utils import (
|
||||
@@ -31,6 +31,8 @@ from rgsx_settings import (
|
||||
from accessibility import save_accessibility_settings
|
||||
from scraper import get_game_metadata, download_image_to_surface
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Extensions d'archives pour lesquelles on ignore l'avertissement d'extension non supportée
|
||||
@@ -326,6 +328,20 @@ def _launch_next_queued_download():
|
||||
if config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
|
||||
def filter_games_by_search_query() -> list[Game]:
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
|
||||
filtered_games = []
|
||||
for game in base_games:
|
||||
game_name = game.display_name
|
||||
if config.search_query.lower() in game_name.lower():
|
||||
filtered_games.append(game)
|
||||
|
||||
return filtered_games
|
||||
...
|
||||
|
||||
def handle_controls(event, sources, joystick, screen):
|
||||
"""Gère un événement clavier/joystick/souris et la répétition automatique.
|
||||
Retourne 'quit', 'download', 'redownload', ou None."""
|
||||
@@ -509,7 +525,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Jeux
|
||||
elif config.menu_state == "game":
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
games: list[Game] = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
if config.search_mode and getattr(config, 'joystick', False):
|
||||
keyboard_layout = [
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
@@ -563,10 +579,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif is_input_matched(event, "confirm"):
|
||||
config.search_query += keyboard_layout[row][col]
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -575,10 +588,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.search_query:
|
||||
config.search_query = config.search_query[:-1]
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -586,10 +596,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif is_input_matched(event, "space"):
|
||||
config.search_query += " "
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -642,10 +649,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if event.unicode.isalnum() or event.unicode == ' ':
|
||||
config.search_query += event.unicode
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -655,10 +659,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.search_query:
|
||||
config.search_query = config.search_query[:-1]
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -726,8 +727,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if games:
|
||||
idx = config.current_game
|
||||
game = games[idx]
|
||||
url = game[1]
|
||||
game_name = game[0]
|
||||
url = game.url
|
||||
game_name = game.name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
|
||||
pending_download = check_extension_before_download(url, platform, game_name)
|
||||
@@ -3184,8 +3185,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# 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]
|
||||
url = games[config.current_game].url
|
||||
game_name = games[config.current_game].name
|
||||
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}")
|
||||
|
||||
@@ -3316,8 +3317,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# 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]
|
||||
url = games[config.current_game].url
|
||||
game_name = games[config.current_game].name
|
||||
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}")
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ from rgsx_settings import (load_rgsx_settings, get_light_mode, get_show_unsuppor
|
||||
get_hide_premium_systems, get_symlink_option)
|
||||
from game_filters import GameFilters
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import urllib.request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OVERLAY = None # Initialisé dans init_display()
|
||||
@@ -1154,12 +1159,70 @@ def draw_platform_grid(screen):
|
||||
for key in keys_to_remove:
|
||||
del platform_images_cache[key]
|
||||
|
||||
|
||||
|
||||
FBNEO_GAME_LIST = "fbneo_gamelist.txt"
|
||||
|
||||
def download_fbneo_list(path_to_save: str) -> None:
|
||||
url = "https://raw.githubusercontent.com/libretro/FBNeo/master/gamelist.txt"
|
||||
path = Path(path_to_save)
|
||||
|
||||
if not path.exists():
|
||||
logger.debug("Downloading fbneo gamelist.txt from github ...")
|
||||
urllib.request.urlretrieve(url, path)
|
||||
|
||||
logger.debug("Download finished:", path)
|
||||
...
|
||||
|
||||
def parse_fbneo_list(path: str) -> Dict[str, Any]:
|
||||
games : Dict[str, Any] = {}
|
||||
headers = None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.rstrip()
|
||||
|
||||
if line.startswith("+"):
|
||||
continue
|
||||
|
||||
if "|" not in line:
|
||||
continue
|
||||
|
||||
parts = [p.strip() for p in line.split("|")[1:-1]]
|
||||
|
||||
if headers is None:
|
||||
headers = parts
|
||||
continue
|
||||
|
||||
row = dict(zip(headers, parts))
|
||||
|
||||
name = row["name"]
|
||||
games[name] = row
|
||||
|
||||
return games
|
||||
|
||||
# Liste des jeux
|
||||
def draw_game_list(screen):
|
||||
"""Affiche la liste des jeux avec un style moderne."""
|
||||
#logger.debug(f"[DRAW_GAME_LIST] Called - platform={config.current_platform}, search_mode={config.search_mode}, filter_active={config.filter_active}")
|
||||
platform = config.platforms[config.current_platform]
|
||||
platform_name = config.platform_names.get(platform, platform)
|
||||
|
||||
fbneo_selected = platform_name == 'Final Burn Neo'
|
||||
if fbneo_selected:
|
||||
fbneo_game_list_path = os.path.join(config.SAVE_FOLDER, FBNEO_GAME_LIST)
|
||||
download_fbneo_list(fbneo_game_list_path) # download the fbneo game list if necessary - 10 MB file
|
||||
config.fbneo_games = parse_fbneo_list(fbneo_game_list_path)
|
||||
for game in config.games:
|
||||
clean_name = game.display_name
|
||||
if clean_name in config.fbneo_games:
|
||||
fbneo_game = config.fbneo_games[clean_name]
|
||||
game.display_name = fbneo_game["full name"]
|
||||
...
|
||||
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active() and not config.search_query:
|
||||
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
|
||||
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
game_count = len(games)
|
||||
#logger.debug(f"[DRAW_GAME_LIST] Games count={game_count}, current_game={config.current_game}, filtered_games={len(config.filtered_games) if config.filtered_games else 0}, config.games={len(config.games) if config.games else 0}")
|
||||
@@ -1310,13 +1373,9 @@ def draw_game_list(screen):
|
||||
|
||||
for i in range(config.scroll_offset, min(config.scroll_offset + items_per_page, len(games))):
|
||||
item = games[i]
|
||||
if isinstance(item, (list, tuple)) and item:
|
||||
game_name = item[0]
|
||||
size_val = item[2] if len(item) > 2 else None
|
||||
else:
|
||||
game_name = str(item)
|
||||
size_val = None
|
||||
|
||||
game_name = item.display_name
|
||||
size_val = item.size
|
||||
|
||||
# Vérifier si le jeu est déjà téléchargé
|
||||
is_downloaded = is_game_downloaded(platform_name, game_name)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import re
|
||||
import logging
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
from config import Game
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -156,9 +158,7 @@ class GameFilters:
|
||||
r'\([^\)]*PRERELEASE[^\)]*\)',
|
||||
r'\([^\)]*UNFINISHED[^\)]*\)',
|
||||
r'\([^\)]*WIP[^\)]*\)',
|
||||
r'\[[^\]]*BETA[^\]]*\]',
|
||||
r'\[[^\]]*DEMO[^\]]*\]',
|
||||
r'\[[^\]]*TEST[^\]]*\]'
|
||||
r'\([^\)]*BOOTLEG[^\)]*\)',
|
||||
]
|
||||
return any(re.search(pattern, name) for pattern in non_release_patterns)
|
||||
|
||||
@@ -211,7 +211,7 @@ class GameFilters:
|
||||
|
||||
return best_priority
|
||||
|
||||
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
|
||||
def apply_filters(self, games: list[Game]) -> list[Game]:
|
||||
"""
|
||||
Applique les filtres à une liste de jeux
|
||||
games: Liste de tuples (game_name, game_url, size)
|
||||
@@ -224,7 +224,7 @@ class GameFilters:
|
||||
|
||||
# Filtrage par région
|
||||
for game in games:
|
||||
game_name = game[0]
|
||||
game_name = game.display_name
|
||||
|
||||
# Vérifier les filtres de région
|
||||
if self.region_filters:
|
||||
@@ -255,12 +255,12 @@ class GameFilters:
|
||||
|
||||
return filtered_games
|
||||
|
||||
def _apply_one_rom_per_game(self, games: List[Tuple]) -> List[Tuple]:
|
||||
def _apply_one_rom_per_game(self, games: List[Game]) -> List[Game]:
|
||||
"""Garde seulement une ROM par jeu selon la priorité de région"""
|
||||
games_by_base = {}
|
||||
|
||||
for game in games:
|
||||
game_name = game[0]
|
||||
game_name = game.display_name
|
||||
base_name = self.get_base_game_name(game_name)
|
||||
|
||||
if base_name not in games_by_base:
|
||||
@@ -276,7 +276,7 @@ class GameFilters:
|
||||
else:
|
||||
# Trier par priorité de région
|
||||
sorted_games = sorted(game_list,
|
||||
key=lambda g: self.get_region_priority(g[0]))
|
||||
key=lambda g: self.get_region_priority(g.display_name))
|
||||
result.append(sorted_games[0])
|
||||
|
||||
return result
|
||||
|
||||
@@ -1544,7 +1544,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
@@ -1616,7 +1616,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.debug(f"[DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
@@ -2600,7 +2600,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
success, message = data[1], data[2]
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
@@ -2655,7 +2655,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.debug(f"[1F_DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
|
||||
@@ -17,7 +17,7 @@ import re
|
||||
import config # paths, settings, SAVE_FOLDER, etc.
|
||||
from utils import load_api_keys as _prime_api_keys # ensure API key files are created
|
||||
import network as network_mod # for progress_queues access
|
||||
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename, extract_zip_data
|
||||
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename
|
||||
from history import load_history, save_history, add_to_history
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url
|
||||
from rgsx_settings import get_sources_zip_url
|
||||
@@ -277,7 +277,7 @@ def cmd_games(args):
|
||||
suggestions = [] # (priority, score, game_obj)
|
||||
# 1) Substring match (full or sans extension) priority 0, score = position
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
t_lower = title.lower()
|
||||
@@ -303,7 +303,7 @@ def cmd_games(args):
|
||||
# 2) Ordered non-contiguous tokens (priority 1)
|
||||
if q_tokens:
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
tt = _tokens(title)
|
||||
@@ -313,7 +313,7 @@ def cmd_games(args):
|
||||
# 3) All tokens present, any order (priority 2), score = token set size
|
||||
if q_tokens:
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
t_tokens = set(_tokens(title))
|
||||
@@ -322,12 +322,12 @@ def cmd_games(args):
|
||||
# Deduplicate by title keeping best (lowest priority, then score)
|
||||
best = {}
|
||||
for prio, score, g in suggestions:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
|
||||
title = g.name
|
||||
key = title.lower()
|
||||
cur = best.get(key)
|
||||
if cur is None or (prio, score) < (cur[0], cur[1]):
|
||||
best[key] = (prio, score, g)
|
||||
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower()))
|
||||
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2].name if isinstance(x[2], (list, config.Game)) and x[2] else str(x[2])).lower()))
|
||||
games = [g for _, _, g in ranked]
|
||||
# Table: Name (60) | Size (12) to allow "xxxx.xx MiB"
|
||||
NAME_W = 60
|
||||
@@ -344,7 +344,7 @@ def cmd_games(args):
|
||||
print(header)
|
||||
print(border)
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
|
||||
title = g.name
|
||||
size_val = ''
|
||||
if isinstance(g, (list, tuple)) and len(g) >= 3:
|
||||
size_val = display_size(g[2])
|
||||
@@ -447,11 +447,11 @@ def cmd_download(args):
|
||||
def _tokens(s: str) -> list[str]:
|
||||
return re.findall(r"[a-z0-9]+", s.lower())
|
||||
|
||||
def _game_title(g) -> str | None:
|
||||
return g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
def _game_title(g: config.Game) -> str | None:
|
||||
return g.name
|
||||
|
||||
def _game_url(g) -> str | None:
|
||||
return g[1] if isinstance(g, (list, tuple)) and len(g) > 1 else None
|
||||
def _game_url(g: config.Game) -> str | None:
|
||||
return g.url
|
||||
|
||||
# 1) Exact match (case-insensitive), with and without extension
|
||||
match = None
|
||||
@@ -561,8 +561,8 @@ def cmd_download(args):
|
||||
size_val = ''
|
||||
size_raw = None
|
||||
for g in games:
|
||||
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
|
||||
size_raw = g[2]
|
||||
if g.name == title:
|
||||
size_raw = g.size
|
||||
break
|
||||
if size_raw is not None:
|
||||
size_val = display_size(size_raw)
|
||||
|
||||
@@ -25,6 +25,7 @@ from utils import load_sources, load_games, extract_data
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
from config import Game
|
||||
|
||||
try:
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
@@ -161,7 +162,7 @@ def get_cached_sources() -> tuple[list[dict], str, datetime]:
|
||||
return copy.deepcopy(platforms), etag, last_modified
|
||||
|
||||
|
||||
def get_cached_games(platform: str) -> tuple[list[tuple], str, datetime]:
|
||||
def get_cached_games(platform: str) -> tuple[list[Game], str, datetime]:
|
||||
"""Return cached games list for platform with metadata."""
|
||||
now = time.time()
|
||||
with cache_lock:
|
||||
@@ -696,14 +697,14 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
elif games_last_modified:
|
||||
latest_modified = games_last_modified
|
||||
for game in games:
|
||||
game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
|
||||
game_name = game.name
|
||||
game_name_lower = game_name.lower()
|
||||
if all(word in game_name_lower for word in search_words):
|
||||
matching_games.append({
|
||||
'game_name': game_name,
|
||||
'platform': platform_name,
|
||||
'url': game[1] if len(game) > 1 and isinstance(game, (list, tuple)) else None,
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None, self._get_language_from_cookies())
|
||||
'url': game.url,
|
||||
'size': normalize_size(game.size, self._get_language_from_cookies())
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de la recherche dans {platform_name}: {e}")
|
||||
@@ -750,9 +751,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
games, _, games_last_modified = get_cached_games(platform_name)
|
||||
games_formatted = [
|
||||
{
|
||||
'name': g[0],
|
||||
'url': g[1] if len(g) > 1 else None,
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None, lang)
|
||||
'name': g.name,
|
||||
'url': g.url,
|
||||
'size': normalize_size(g.size, lang)
|
||||
}
|
||||
for g in games
|
||||
]
|
||||
@@ -1134,7 +1135,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
if game_name_param and game_index is None:
|
||||
game_index = None
|
||||
for idx, game in enumerate(games):
|
||||
current_game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
|
||||
current_game_name = game.name
|
||||
if current_game_name == game_name_param:
|
||||
game_index = idx
|
||||
break
|
||||
@@ -1155,8 +1156,8 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
game = games[game_index]
|
||||
game_name = game[0]
|
||||
game_url = game[1] if len(game) > 1 else None
|
||||
game_name = game.name
|
||||
game_url = game.url
|
||||
|
||||
if not game_url:
|
||||
self._send_json({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import requests # type: ignore
|
||||
import re
|
||||
@@ -7,7 +8,7 @@ import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import config
|
||||
from config import HEADLESS
|
||||
from config import HEADLESS, Game
|
||||
try:
|
||||
if not HEADLESS:
|
||||
import pygame # type: ignore
|
||||
@@ -1166,7 +1167,7 @@ def load_sources():
|
||||
logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
|
||||
return []
|
||||
|
||||
def load_games(platform_id):
|
||||
def load_games(platform_id:str) -> list[Game]:
|
||||
try:
|
||||
# Retrouver l'objet plateforme pour accéder éventuellement à 'folder'
|
||||
platform_dict = None
|
||||
@@ -1235,7 +1236,13 @@ def load_games(platform_id):
|
||||
|
||||
if getattr(config, "games_count_log_verbose", False):
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
return normalized
|
||||
|
||||
games_list: list[Game] = []
|
||||
for name, url, size in normalized:
|
||||
display_name = Path(name).stem
|
||||
display_name = display_name.replace(platform_id, "")
|
||||
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
|
||||
return games_list
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
|
||||
return []
|
||||
@@ -1820,7 +1827,7 @@ def extract_rar(rar_path, dest_dir, url):
|
||||
os.chmod(os.path.join(root, dir_name), 0o755)
|
||||
|
||||
# Gestion plateformes spéciales (uniquement PS3 pour RAR)
|
||||
success, error_msg = _handle_special_platforms(dest_dir, rar_path, before_dirs)
|
||||
success, error_msg = _handle_special_platforms(dest_dir, rar_path, before_dirs, url=url)
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
@@ -1954,18 +1961,31 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
# MODE PS3 : Décryptage et extraction
|
||||
# ============================================
|
||||
logger.info(f"Mode PS3 détecté pour: {archive_name}")
|
||||
|
||||
# L'extraction de l'archive est terminée; basculer l'UI en mode conversion/décryptage.
|
||||
try:
|
||||
if url:
|
||||
if url not in getattr(config, 'download_progress', {}):
|
||||
config.download_progress[url] = {}
|
||||
config.download_progress[url]["status"] = "Converting"
|
||||
config.download_progress[url]["progress_percent"] = 0
|
||||
config.needs_redraw = True
|
||||
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if entry.get("url") == url and entry.get("status") in ("Extracting", "Téléchargement", "Downloading"):
|
||||
entry["status"] = "Converting"
|
||||
entry["progress"] = 0
|
||||
entry["message"] = "PS3 conversion in progress"
|
||||
save_history(config.history)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"MAJ statut conversion PS3 ignorée: {e}")
|
||||
|
||||
try:
|
||||
# Construire l'URL de la clé en remplaçant le dossier
|
||||
if url and ("Sony%20-%20PlayStation%203/" in url or "Sony - PlayStation 3/" in url):
|
||||
key_url = url.replace("Sony%20-%20PlayStation%203/", "Sony%20-%20PlayStation%203%20-%20Disc%20Keys%20TXT/")
|
||||
key_url = key_url.replace("Sony - PlayStation 3/", "Sony - PlayStation 3 - Disc Keys TXT/")
|
||||
else:
|
||||
logger.warning("URL PS3 invalide ou manquante, tentative sans clé distante")
|
||||
key_url = None
|
||||
|
||||
ps3_keys_base_url = "https://retrogamesets.fr/softs/ps3/"
|
||||
logger.debug(f"URL jeu: {url}")
|
||||
logger.debug(f"URL clé: {key_url}")
|
||||
logger.debug(f"Base URL des clés PS3: {ps3_keys_base_url}")
|
||||
|
||||
# Chercher le fichier .iso déjà extrait
|
||||
iso_files = [f for f in os.listdir(dest_dir) if f.endswith('.iso') and not f.endswith('_decrypted.iso')]
|
||||
@@ -1976,41 +1996,51 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
iso_path = os.path.join(dest_dir, iso_file)
|
||||
logger.info(f"Fichier ISO trouvé: {iso_path}")
|
||||
|
||||
# Étape 1: Télécharger et extraire la clé si URL disponible
|
||||
# Étape 1: Télécharger directement la clé .dkey depuis la nouvelle source
|
||||
dkey_path = None
|
||||
if key_url:
|
||||
logger.info("Téléchargement de la clé de décryption...")
|
||||
key_zip_name = os.path.basename(archive_name) if archive_name else "key.zip"
|
||||
key_zip_path = os.path.join(dest_dir, f"_temp_key_{key_zip_name}")
|
||||
|
||||
logger.info("Téléchargement de la clé de décryption (.dkey)...")
|
||||
|
||||
candidate_bases = []
|
||||
|
||||
def _add_candidate_base(base_name):
|
||||
if not base_name:
|
||||
return
|
||||
cleaned = str(base_name).strip()
|
||||
if not cleaned:
|
||||
return
|
||||
if cleaned.lower().endswith('.dkey'):
|
||||
cleaned = cleaned[:-5]
|
||||
if cleaned not in candidate_bases:
|
||||
candidate_bases.append(cleaned)
|
||||
|
||||
if archive_name:
|
||||
_add_candidate_base(os.path.splitext(os.path.basename(archive_name))[0])
|
||||
if extracted_basename:
|
||||
_add_candidate_base(extracted_basename)
|
||||
_add_candidate_base(os.path.splitext(os.path.basename(iso_file))[0])
|
||||
|
||||
for base_name in candidate_bases:
|
||||
remote_name = f"{base_name}.dkey"
|
||||
encoded_name = remote_name.replace(" ", "%20")
|
||||
key_url = f"{ps3_keys_base_url}{encoded_name}"
|
||||
logger.debug(f"Tentative clé distante: {key_url}")
|
||||
try:
|
||||
response = requests.get(key_url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(key_zip_path, 'wb') as f:
|
||||
if response.status_code != 200:
|
||||
logger.debug(f"Clé distante introuvable ({response.status_code}): {remote_name}")
|
||||
continue
|
||||
|
||||
local_dkey_path = os.path.join(dest_dir, remote_name)
|
||||
with open(local_dkey_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
logger.info(f"Clé téléchargée: {key_zip_path}")
|
||||
|
||||
# Extraire la clé
|
||||
logger.info("Extraction de la clé...")
|
||||
with zipfile.ZipFile(key_zip_path, 'r') as zf:
|
||||
dkey_files = [f for f in zf.namelist() if f.endswith('.dkey')]
|
||||
if not dkey_files:
|
||||
logger.warning("Aucun fichier .dkey trouvé dans l'archive de clé")
|
||||
else:
|
||||
dkey_file = dkey_files[0]
|
||||
zf.extract(dkey_file, dest_dir)
|
||||
dkey_path = os.path.join(dest_dir, dkey_file)
|
||||
logger.info(f"Clé extraite: {dkey_path}")
|
||||
|
||||
# Supprimer le ZIP de la clé
|
||||
os.remove(key_zip_path)
|
||||
|
||||
|
||||
dkey_path = local_dkey_path
|
||||
logger.info(f"Clé téléchargée: {dkey_path}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du téléchargement/extraction de la clé: {e}")
|
||||
logger.warning(f"Échec téléchargement clé {remote_name}: {e}")
|
||||
|
||||
# Chercher une clé .dkey si pas téléchargée
|
||||
if not dkey_path:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.5.0.4"
|
||||
"version": "2.5.0.6"
|
||||
}
|
||||
Reference in New Issue
Block a user