Compare commits

..

8 Commits

Author SHA1 Message Date
skymike03
eb86d69895 v2.6.1.1 (2026.21.03)
- Improved History/Downloads table readability by giving more space to game titles and using middle truncation for long names
- Cleaned displayed game names to remove platform/path prefixes from titles
- Improved file matching for downloaded and extracted games, including support for filename variants and tag differences
- Updated Locate file to show all matching files instead of only one path
- Added a Move action from the locate screen, using the existing folder browser to move all listed files to a selected destination
- Added collision-safe file moves and persisted moved paths in history
- Added localized labels/messages for the new move flow
- Fixed a startup crash caused by a translation function name conflict
- Fixed navigation after move so OK and Back work correctly from the locate screen
2026-03-21 17:29:39 +01:00
skymike03
b09b3da371 v2.6.1.0 (2026.03.20)
- Added the IP address to the top-right info badge.
- Added disk usage to the left badge using a used/total(percentage) format.
- Added screen resolution below disk usage on the platforms page.
- Improved entry speed for global cross-platform search.
- Fixed virtual keyboard positioning in the search screen.
- Adjusted the platform grid to avoid overlapping the footer on small resolutions.
- Made the header badges responsive to prevent overlap on smaller screens.
- Made the footer responsive with automatic scaling of text, icons, and spacing.
- Asking to update gamelist once a day
2026-03-20 19:14:27 +01:00
skymike03
0915a90fbe Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2026-03-17 23:24:33 +01:00
skymike03
3ae3c151eb v2.6.0.3 (2025.03.17)
- Add support and donation information to release notes
- Add normalize game name for roms scanning (ie. Game (USA).ext will be shown owned for a rom named only "Game.ext"
- Add fulscreen/windowed mode in Settings > Display with auto resize window
2026-03-17 23:24:31 +01:00
RGS
7460b12d71 Delete snes directory error 2026-03-17 23:16:49 +01:00
skymike03
2f437c1aa4 v2.6.0.2 (2025.03.17)
- Add support and donation information to release notes
- Add normalize game name for roms scanning (ie. Game (USA).ext will be shown owned for a rom named only "Game.ext"
2026-03-17 23:12:50 +01:00
skymike03
054b174c18 v2.6.0.1 (2025.03.16)
• add Debrid-Link API key support in desktop and web settings
• add Debrid-Link fallback for premium link generation in addition to AllDebrid and RealDebrid
2026-03-16 20:28:19 +01:00
skymike03
fbb1a2aa68 v2.5.0.7 (2026.03.16)
• improve filters/search performance with lazy cache without slowing unfiltered game list access
• fix fbneo logging traceback and avoid re-downloading/parsing fbneo gamelist when cache is already available
• add rom scan to rebuild downloaded games database from local files with extension-insensitive matching and backward compatibility
• fix clear history to remove stale converting entries and keep only real active transfers
• fix keyboard mode controls display to ignore joystick-only mappings and restore proper keyboard labels
• update displayed keyboard labels with ascii-safe names (Esc/Echap, Debut, Verr Def)
• move version/controller info to a top-right header badge and add page number badge on top-left
• simplify platform footer and loading footer layout
• add global search from platform menu across all available systems
• fix global search input handling and routing
• update global search confirm action to download directly and allow queue action from results
• add size and ext columns to global search results
• add ext column to game list and history
• add folder column to history to show download destination folder
• fix return from history to restore current game list instead of jumping back to platform list
• fix history crash caused by ext column text truncation
2026-03-16 19:09:47 +01:00
21 changed files with 2298 additions and 273 deletions

View File

@@ -119,6 +119,16 @@ jobs:
### 📖 Documentation
[README.md](https://github.com/${{ github.repository }}/blob/main/README.md)
## SUPPORT US
Donate , Buy me a beer or a coffee :
if you want to support my project you can look here 🙂 https://bit.ly/donate-to-rgsx
Affiliate links :
hi all if you want to buy a premium account, you can use affiliated links here to support dev of RGSX without donate anything :
DEBRID-LINK.FR : https://debrid-link.fr/id/ow1DD
1FICHIER.COM : https://1fichier.com/?af=3186111
REAL-DEBRID.FR : http://real-debrid.com/?id=8441
RELEASE_EOF
echo "✓ Release notes generated"

View File

@@ -28,10 +28,11 @@ from display import (
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid,
draw_progress_screen, draw_controls, draw_virtual_keyboard,
draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list,
draw_global_search_list,
draw_display_menu, draw_filter_menu_choice, draw_filter_advanced, draw_filter_priority_config,
draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog,
draw_confirm_dialog, draw_reload_games_data_dialog, draw_popup, draw_gradient,
draw_toast, show_toast, THEME_COLORS
draw_toast, show_toast, THEME_COLORS, sync_display_metrics
)
from language import _
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads, download_queue_worker
@@ -44,7 +45,7 @@ from utils import (
)
from history import load_history, save_history, load_downloaded_games
from config import OTA_data_ZIP
from rgsx_settings import get_sources_mode, get_custom_sources_url, get_sources_zip_url
from rgsx_settings import get_sources_mode, get_custom_sources_url, get_sources_zip_url, get_display_fullscreen
from accessibility import load_accessibility_settings
# Configuration du logging
@@ -64,7 +65,7 @@ except Exception as e:
logger = logging.getLogger(__name__)
# Ensure API key files (1Fichier, AllDebrid, RealDebrid) exist at startup so user can fill them before any download
# Ensure API key files (1Fichier, AllDebrid, Debrid-Link, RealDebrid) exist at startup so user can fill them before any download
try: # pragma: no cover
load_api_keys(False)
except Exception as _e:
@@ -429,7 +430,7 @@ def stop_web_server():
# Boucle principale
async def main():
global current_music, music_files, music_folder, joystick
global current_music, music_files, music_folder, joystick, screen
logger.debug("Début main")
# Charger les filtres de jeux sauvegardés
@@ -609,6 +610,27 @@ async def main():
current_music = play_random_music(music_files, music_folder, current_music)
continue
resize_events = {
getattr(pygame, 'VIDEORESIZE', -1),
getattr(pygame, 'WINDOWSIZECHANGED', -2),
getattr(pygame, 'WINDOWRESIZED', -3),
}
if event.type in resize_events and not get_display_fullscreen():
try:
if event.type == getattr(pygame, 'VIDEORESIZE', -1):
new_width = max(640, int(getattr(event, 'w', config.screen_width)))
new_height = max(360, int(getattr(event, 'h', config.screen_height)))
screen = pygame.display.set_mode((new_width, new_height), pygame.RESIZABLE)
else:
screen = pygame.display.get_surface() or screen
sync_display_metrics(screen)
config.needs_redraw = True
logger.debug(f"Fenêtre redimensionnée: {config.screen_width}x{config.screen_height}")
except Exception as e:
logger.error(f"Erreur lors du redimensionnement de la fenêtre: {e}")
continue
if event.type == pygame.QUIT:
config.menu_state = "confirm_exit"
config.confirm_selection = 0
@@ -661,6 +683,7 @@ async def main():
# Basculer sur les contrôles clavier
config.joystick = False
config.keyboard = True
config.controller_device_name = ""
# Recharger la configuration des contrôles pour le clavier
config.controls_config = load_controls_config()
logger.info("Contrôles clavier chargés")
@@ -669,6 +692,7 @@ async def main():
# Basculer sur les contrôles clavier
config.joystick = False
config.keyboard = True
config.controller_device_name = ""
# Recharger la configuration des contrôles pour le clavier
config.controls_config = load_controls_config()
logger.info("Contrôles clavier chargés")
@@ -729,6 +753,7 @@ async def main():
"filter_menu_choice",
"filter_advanced",
"filter_priority_config",
"platform_search",
}
if config.menu_state in SIMPLE_HANDLE_STATES:
action = handle_controls(event, sources, joystick, screen)
@@ -849,7 +874,12 @@ async def main():
keys_info = ensure_download_provider_keys(False)
except Exception as e:
logger.error(f"Impossible de charger les clés via helpers: {e}")
keys_info = {'1fichier': getattr(config,'API_KEY_1FICHIER',''), 'alldebrid': getattr(config,'API_KEY_ALLDEBRID',''), 'realdebrid': getattr(config,'API_KEY_REALDEBRID','')}
keys_info = {
'1fichier': getattr(config,'API_KEY_1FICHIER',''),
'alldebrid': getattr(config,'API_KEY_ALLDEBRID',''),
'debridlink': getattr(config,'API_KEY_DEBRIDLINK',''),
'realdebrid': getattr(config,'API_KEY_REALDEBRID','')
}
# SUPPRIMÉ: Vérification clés API obligatoires
# Maintenant on a le mode gratuit en fallback automatique
@@ -1116,6 +1146,10 @@ async def main():
draw_game_list(screen)
if getattr(config, 'joystick', False):
draw_virtual_keyboard(screen)
elif config.menu_state == "platform_search":
draw_global_search_list(screen)
if getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False):
draw_virtual_keyboard(screen)
elif config.menu_state == "download_progress":
draw_progress_screen(screen)
# État download_result supprimé

View File

@@ -7,7 +7,7 @@
"cancel": {
"type": "key",
"key": 27,
"display": "\u00c9chap"
"display": "Esc/Echap"
},
"up": {
"type": "key",

View File

@@ -2,6 +2,7 @@
import os
import logging
import platform
import socket
from typing import Optional
from dataclasses import dataclass
@@ -11,6 +12,9 @@ class Game:
url: str
size: str
display_name: str # name withou file extension or platform prefix
regions: Optional[list[str]] = None
is_non_release: Optional[bool] = None
base_name: Optional[str] = None
# Headless mode for CLI: set env RGSX_HEADLESS=1 to avoid pygame and noisy prints
HEADLESS = os.environ.get("RGSX_HEADLESS") == "1"
@@ -23,10 +27,10 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.5.0.6"
app_version = "2.6.1.1"
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
GAMELIST_UPDATE_DAYS = 7
GAMELIST_UPDATE_DAYS = 1
def get_application_root():
@@ -194,6 +198,7 @@ DOWNLOADED_GAMES_PATH = os.path.join(SAVE_FOLDER, "downloaded_games.json")
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.txt")
API_KEY_DEBRIDLINK_PATH = os.path.join(SAVE_FOLDER, "DebridLinkAPI.txt")
API_KEY_REALDEBRID_PATH = os.path.join(SAVE_FOLDER, "RealDebridAPI.txt")
ARCHIVE_ORG_COOKIE_PATH = os.path.join(APP_FOLDER, "assets", "ArchiveOrgCookie.txt")
@@ -246,6 +251,30 @@ SYSTEM_INFO = {
def get_batocera_system_info():
"""Récupère les informations système via la commande batocera-info."""
global SYSTEM_INFO
def get_local_network_ip():
try:
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
udp_socket.connect(("8.8.8.8", 80))
local_ip = udp_socket.getsockname()[0]
if local_ip and not local_ip.startswith("127."):
return local_ip
finally:
udp_socket.close()
except Exception:
pass
try:
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
if local_ip and not local_ip.startswith("127."):
return local_ip
except Exception:
pass
return ""
try:
import subprocess
result = subprocess.run(['batocera-info'], capture_output=True, text=True, timeout=5)
@@ -301,6 +330,7 @@ def get_batocera_system_info():
SYSTEM_INFO["system"] = f"{platform.system()} {platform.release()}"
SYSTEM_INFO["architecture"] = platform.machine()
SYSTEM_INFO["cpu_model"] = platform.processor() or "Unknown"
SYSTEM_INFO["network_ip"] = get_local_network_ip()
return False
@@ -403,6 +433,12 @@ filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filt
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é
global_search_index = [] # Index de recherche global {platform, jeu} construit a l'ouverture
global_search_results = [] # Resultats de la recherche globale inter-plateformes
global_search_query = "" # Texte saisi pour la recherche globale
global_search_selected = 0 # Index du resultat global selectionne
global_search_scroll_offset = 0 # Offset de defilement des resultats globaux
global_search_editing = False # True si le clavier virtuel est actif pour la recherche globale
# Variables pour le filtrage avancé
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
@@ -448,9 +484,11 @@ scraper_game_page_url = "" # URL de la page du jeu sur TheGamesDB
# CLES API / PREMIUM HOSTS
API_KEY_1FICHIER = ""
API_KEY_ALLDEBRID = ""
API_KEY_DEBRIDLINK = ""
API_KEY_REALDEBRID = ""
PREMIUM_HOST_MARKERS = [
"1Fichier",
"Debrid-Link",
]
hide_premium_systems = False # Indicateur pour masquer les systèmes premium

View File

@@ -15,12 +15,13 @@ from utils import (
load_games, check_extension_before_download, is_extension_supported,
load_extensions_json, play_random_music, sanitize_filename,
save_music_config, load_api_keys, _get_dest_folder_name,
extract_zip, extract_rar, find_file_with_or_without_extension, toggle_web_service_at_boot, check_web_service_status,
extract_zip, extract_rar, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
restart_application, generate_support_zip, load_sources,
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
start_connection_status_check
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
move_files_to_directory
)
from history import load_history, clear_history, add_to_history, save_history
from history import load_history, clear_history, add_to_history, save_history, scan_roms_for_downloaded_games
from language import _, get_available_languages, set_language
from rgsx_settings import (
get_allow_unknown_extensions, set_display_grid, get_font_family, set_font_family,
@@ -72,6 +73,7 @@ VALID_STATES = [
"filter_search", # recherche par nom (existant, mais renommé)
"filter_advanced", # filtrage avancé par région, etc.
"filter_priority_config", # configuration priorité régions pour one-rom-per-game
"platform_search", # recherche globale inter-plateformes
"platform_folder_config", # configuration du dossier personnalisé pour une plateforme
"folder_browser", # navigateur de dossiers intégré
"folder_browser_new_folder", # création d'un nouveau dossier
@@ -109,6 +111,18 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
"delete": {"type": "key", "key": pygame.K_BACKSPACE},
"space": {"type": "key", "key": pygame.K_SPACE}
}
def _is_keyboard_only_config(data):
if not isinstance(data, dict) or not data:
return False
for action_name, mapping in data.items():
if action_name == "device":
continue
if not isinstance(mapping, dict):
return False
if mapping.get("type") != "key":
return False
return True
try:
# 1) Fichier utilisateur
@@ -117,21 +131,25 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
data = json.load(f)
if not isinstance(data, dict):
data = {}
keyboard_mode = (not getattr(config, 'joystick', False)) or getattr(config, 'keyboard', False)
if keyboard_mode and not _is_keyboard_only_config(data):
logging.getLogger(__name__).info("Configuration utilisateur manette ignorée en mode clavier")
else:
# Compléter les actions manquantes, et sauve seulement si le fichier utilisateur existe
changed = False
for k, v in default_config.items():
if k not in data:
data[k] = v
changed = True
if changed:
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
logging.getLogger(__name__).debug(f"controls.json complété avec les actions manquantes: {path}")
except Exception as e:
logging.getLogger(__name__).warning(f"Impossible d'écrire les actions manquantes dans {path}: {e}")
return data
changed = False
for k, v in default_config.items():
if k not in data:
data[k] = v
changed = True
if changed:
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
logging.getLogger(__name__).debug(f"controls.json complété avec les actions manquantes: {path}")
except Exception as e:
logging.getLogger(__name__).warning(f"Impossible d'écrire les actions manquantes dans {path}: {e}")
return data
# 2) Préréglages sans copie si aucun fichier utilisateur
try:
@@ -260,6 +278,67 @@ def is_input_matched(event, action_name):
return False
def is_global_search_input_matched(event, action_name):
"""Fallback robuste pour la recherche globale, independant du preset courant."""
if is_input_matched(event, action_name):
return True
if event.type == pygame.KEYDOWN:
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"filter": pygame.K_f,
"delete": pygame.K_BACKSPACE,
"space": pygame.K_SPACE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
if action_name in keyboard_fallback and event.key == keyboard_fallback[action_name]:
return True
if event.type == pygame.JOYBUTTONDOWN:
common_button_fallback = {
"confirm": {0},
"cancel": {1},
"filter": {6},
"start": {7},
"delete": {2},
"space": {5},
"page_up": {4},
"page_down": {5},
}
if action_name in common_button_fallback and event.button in common_button_fallback[action_name]:
return True
if event.type == pygame.JOYHATMOTION:
hat_fallback = {
"up": (0, 1),
"down": (0, -1),
"left": (-1, 0),
"right": (1, 0),
}
if action_name in hat_fallback and event.value == hat_fallback[action_name]:
return True
if event.type == pygame.JOYAXISMOTION:
axis_fallback = {
"left": (0, -1),
"right": (0, 1),
"up": (1, -1),
"down": (1, 1),
}
if action_name in axis_fallback:
axis_id, direction = axis_fallback[action_name]
if event.axis == axis_id and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction:
return True
return False
def _launch_next_queued_download():
"""Lance le prochain téléchargement de la queue si aucun n'est actif.
Gère la liaison entre le système Desktop et le système de download_rom/download_from_1fichier.
@@ -340,6 +419,223 @@ def filter_games_by_search_query() -> list[Game]:
filtered_games.append(game)
return filtered_games
GLOBAL_SEARCH_KEYBOARD_LAYOUT = [
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'],
['W', 'X', 'C', 'V', 'B', 'N']
]
def _get_platform_id(platform) -> str:
return platform.get("name") if isinstance(platform, dict) else str(platform)
def _get_platform_label(platform_id: str) -> str:
return config.platform_names.get(platform_id, platform_id)
def build_global_search_index() -> list[dict]:
indexed_games = []
for platform_index, platform in enumerate(config.platforms):
platform_id = _get_platform_id(platform)
platform_label = _get_platform_label(platform_id)
for game in load_games(platform_id):
display_name = game.display_name or Path(game.name).stem
indexed_games.append({
"platform_id": platform_id,
"platform_label": platform_label,
"platform_index": platform_index,
"game_name": game.name,
"display_name": display_name,
"search_name": display_name.lower(),
"url": game.url,
"size": game.size,
})
indexed_games.sort(key=lambda item: (item["platform_label"].lower(), item["display_name"].lower()))
return indexed_games
def refresh_global_search_results(reset_selection: bool = True) -> None:
query = (config.global_search_query or "").strip().lower()
if not query:
config.global_search_results = []
else:
config.global_search_results = [
item for item in config.global_search_index
if query in item.get("search_name", item["display_name"].lower())
]
if reset_selection:
config.global_search_selected = 0
config.global_search_scroll_offset = 0
else:
max_index = max(0, len(config.global_search_results) - 1)
config.global_search_selected = max(0, min(config.global_search_selected, max_index))
config.global_search_scroll_offset = max(0, min(config.global_search_scroll_offset, config.global_search_selected))
def enter_global_search() -> None:
index_signature = tuple(config.platforms)
if not getattr(config, 'global_search_index', None) or getattr(config, 'global_search_index_signature', None) != index_signature:
config.global_search_index = build_global_search_index()
config.global_search_index_signature = index_signature
config.global_search_query = ""
config.global_search_results = []
config.global_search_selected = 0
config.global_search_scroll_offset = 0
config.global_search_editing = bool(getattr(config, 'joystick', False))
config.selected_key = (0, 0)
config.previous_menu_state = "platform"
config.menu_state = "platform_search"
config.needs_redraw = True
logger.debug("Entree en recherche globale inter-plateformes")
def exit_global_search() -> None:
config.global_search_query = ""
config.global_search_results = []
config.global_search_selected = 0
config.global_search_scroll_offset = 0
config.global_search_editing = False
config.selected_key = (0, 0)
config.menu_state = "platform"
config.needs_redraw = True
def open_global_search_result(screen) -> None:
if not config.global_search_results:
return
result = config.global_search_results[config.global_search_selected]
platform_index = result.get("platform_index", 0)
if platform_index < 0 or platform_index >= len(config.platforms):
return
config.current_platform = platform_index
config.selected_platform = platform_index
config.current_page = platform_index // max(1, config.GRID_COLS * config.GRID_ROWS)
platform_id = result["platform_id"]
config.games = load_games(platform_id)
config.filtered_games = config.games
config.search_mode = False
config.search_query = ""
config.filter_active = False
target_name = result["game_name"]
target_display_name = result["display_name"]
target_index = 0
for index, game in enumerate(config.games):
if game.name == target_name:
target_index = index
break
if game.display_name == target_display_name:
target_index = index
config.current_game = target_index
config.scroll_offset = 0
config.global_search_editing = False
from rgsx_settings import get_light_mode
if not get_light_mode():
draw_validation_transition(screen, config.current_platform)
config.menu_state = "game"
config.needs_redraw = True
logger.debug(f"Ouverture du resultat global {target_display_name} sur {platform_id}")
def trigger_global_search_download(queue_only: bool = False) -> None:
if not config.global_search_results:
return
result = config.global_search_results[config.global_search_selected]
url = result.get("url")
platform = result.get("platform_id")
game_name = result.get("game_name")
display_name = result.get("display_name") or get_clean_display_name(game_name, platform)
if not url or not platform or not game_name:
logger.error(f"Resultat de recherche globale invalide: {result}")
return
pending_download = check_extension_before_download(url, platform, game_name)
if not pending_download:
logger.error(f"config.pending_download est None pour {game_name}")
config.needs_redraw = True
return
is_supported = is_extension_supported(
sanitize_filename(game_name),
platform,
load_extensions_json()
)
zip_ok = bool(pending_download[3])
allow_unknown = get_allow_unknown_extensions()
if (not is_supported and not zip_ok) and not allow_unknown:
config.pending_download = pending_download
config.pending_download_is_queue = queue_only
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 supportee, passage a extension_warning pour {game_name}")
return
if queue_only:
task_id = str(pygame.time.get_ticks())
queue_item = {
'url': url,
'platform': platform,
'game_name': game_name,
'is_zip_non_supported': pending_download[3],
'is_1fichier': is_1fichier_url(url),
'task_id': task_id,
'status': 'Queued'
}
config.download_queue.append(queue_item)
config.history.append({
'platform': platform,
'game_name': game_name,
'display_name': display_name,
'status': 'Queued',
'url': url,
'progress': 0,
'message': _("download_queued"),
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'downloaded_size': 0,
'total_size': 0,
'task_id': task_id
})
save_history(config.history)
show_toast(f"{display_name}\n{_('download_queued')}")
config.needs_redraw = True
logger.debug(f"{game_name} ajoute a la file d'attente depuis la recherche globale. Queue size: {len(config.download_queue)}")
if not config.download_active and config.download_queue:
_launch_next_queued_download()
return
if is_1fichier_url(url):
ensure_download_provider_keys(False)
if missing_all_provider_keys():
logger.warning("Aucune cle API - Mode gratuit 1fichier sera utilise (attente requise)")
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, pending_download[3], task_id))
else:
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_rom(url, platform, game_name, pending_download[3], task_id))
config.download_tasks[task_id] = (task, url, game_name, platform)
show_toast(f"{_('download_started')}: {display_name}")
config.needs_redraw = True
logger.debug(f"Telechargement demarre depuis la recherche globale: {game_name} pour {platform}, task_id={task_id}")
...
def handle_controls(event, sources, joystick, screen):
@@ -504,6 +800,8 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "history"
config.needs_redraw = True
logger.debug("Ouverture history depuis platform")
elif is_input_matched(event, "filter"):
enter_global_search()
elif is_input_matched(event, "confirm"):
# Démarrer le chronomètre pour l'appui long - ne pas exécuter immédiatement
# L'action sera exécutée au relâchement si appui court, ou config dossier si appui long
@@ -523,6 +821,117 @@ def handle_controls(event, sources, joystick, screen):
config.confirm_selection = 0
config.needs_redraw = True
elif config.menu_state == "platform_search":
if getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False):
row, col = config.selected_key
max_row = len(GLOBAL_SEARCH_KEYBOARD_LAYOUT) - 1
max_col = len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row]) - 1
if is_global_search_input_matched(event, "up"):
if row == 0:
row = max_row + (1 if col <= 5 else 0)
if row > 0:
config.selected_key = (row - 1, min(col, len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row - 1]) - 1))
config.repeat_action = "up"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "down"):
if (col <= 5 and row == max_row) or (col > 5 and row == max_row - 1):
row = -1
if row < max_row:
config.selected_key = (row + 1, min(col, len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row + 1]) - 1))
config.repeat_action = "down"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "left"):
if col == 0:
col = max_col + 1
if col > 0:
config.selected_key = (row, col - 1)
config.repeat_action = "left"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "right"):
if col == max_col:
col = -1
if col < max_col:
config.selected_key = (row, col + 1)
config.repeat_action = "right"
config.repeat_start_time = current_time + REPEAT_DELAY
config.repeat_last_action = current_time
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
config.needs_redraw = True
elif is_global_search_input_matched(event, "confirm"):
config.global_search_query += GLOBAL_SEARCH_KEYBOARD_LAYOUT[row][col]
refresh_global_search_results()
logger.debug(f"Recherche globale mise a jour: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "delete"):
if config.global_search_query:
config.global_search_query = config.global_search_query[:-1]
refresh_global_search_results()
logger.debug(f"Recherche globale suppression: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "space"):
config.global_search_query += " "
refresh_global_search_results()
logger.debug(f"Recherche globale espace: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "filter"):
config.global_search_editing = False
config.needs_redraw = True
elif is_global_search_input_matched(event, "cancel"):
exit_global_search()
else:
results = config.global_search_results
if is_global_search_input_matched(event, "up"):
if config.global_search_selected > 0:
config.global_search_selected -= 1
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else event.value)
config.needs_redraw = True
elif is_global_search_input_matched(event, "down"):
if config.global_search_selected < len(results) - 1:
config.global_search_selected += 1
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else event.value)
config.needs_redraw = True
elif is_global_search_input_matched(event, "page_up") or is_global_search_input_matched(event, "left"):
config.global_search_selected = max(0, config.global_search_selected - 10)
config.needs_redraw = True
elif is_global_search_input_matched(event, "page_down") or is_global_search_input_matched(event, "right"):
config.global_search_selected = min(max(0, len(results) - 1), config.global_search_selected + 10)
config.needs_redraw = True
elif is_global_search_input_matched(event, "confirm"):
trigger_global_search_download(queue_only=False)
elif is_global_search_input_matched(event, "clear_history"):
trigger_global_search_download(queue_only=True)
elif is_global_search_input_matched(event, "filter") and getattr(config, 'joystick', False):
config.global_search_editing = True
config.needs_redraw = True
elif is_global_search_input_matched(event, "cancel"):
exit_global_search()
elif not getattr(config, 'joystick', False) and event.type == pygame.KEYDOWN:
if event.unicode.isalnum() or event.unicode == ' ':
config.global_search_query += event.unicode
refresh_global_search_results()
logger.debug(f"Recherche globale clavier: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
elif is_global_search_input_matched(event, "delete"):
if config.global_search_query:
config.global_search_query = config.global_search_query[:-1]
refresh_global_search_results()
logger.debug(f"Recherche globale clavier suppression: query={config.global_search_query}, resultats={len(config.global_search_results)}")
config.needs_redraw = True
if config.global_search_results:
config.global_search_selected = max(0, min(config.global_search_selected, len(config.global_search_results) - 1))
else:
config.global_search_selected = 0
# Jeux
elif config.menu_state == "game":
games: list[Game] = config.filtered_games if config.filter_active or config.search_mode else config.games
@@ -719,6 +1128,7 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
logger.debug("Ouverture du menu de filtrage")
elif is_input_matched(event, "history"):
config.history_origin = "game"
config.menu_state = "history"
config.needs_redraw = True
logger.debug("Ouverture history depuis game")
@@ -768,6 +1178,7 @@ def handle_controls(event, sources, joystick, screen):
config.history.append({
'platform': platform,
'game_name': game_name,
'display_name': get_clean_display_name(game_name, platform),
'status': 'Queued',
'url': url,
'progress': 0,
@@ -840,6 +1251,7 @@ def handle_controls(event, sources, joystick, screen):
config.history.append({
'platform': platform,
'game_name': game_name,
'display_name': get_clean_display_name(game_name, platform),
'status': 'Queued',
'url': url,
'progress': 0,
@@ -1080,6 +1492,13 @@ def handle_controls(event, sources, joystick, screen):
dest_folder = _get_dest_folder_name(platform)
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
actual_matches = find_matching_files(base_path, game_name)
if not actual_matches:
actual_matches = get_existing_history_matches(entry)
if actual_matches:
actual_filename, actual_path = actual_matches[0]
file_exists = True
config.history_actual_matches = actual_matches
# Stocker les informations pour les autres handlers
config.history_actual_filename = actual_filename
@@ -1339,7 +1758,47 @@ def handle_controls(event, sources, joystick, screen):
# Affichage du dossier de téléchargement
elif config.menu_state == "history_show_folder":
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
if is_input_matched(event, "clear_history"):
if not config.history or config.current_history_item >= len(config.history):
config.menu_state = "history"
config.needs_redraw = True
else:
entry = config.history[config.current_history_item]
actual_matches = getattr(config, 'history_actual_matches', None) or []
if not actual_matches:
actual_matches = get_existing_history_matches(entry)
start_path = None
if actual_matches:
start_path = os.path.dirname(actual_matches[0][1])
else:
actual_path = getattr(config, 'history_actual_path', None)
if actual_path and os.path.exists(actual_path):
start_path = os.path.dirname(actual_path)
if not start_path or not os.path.isdir(start_path):
start_path = config.ROMS_FOLDER
config.folder_browser_path = start_path
config.folder_browser_selection = 0
config.folder_browser_scroll_offset = 0
config.folder_browser_mode = "history_move"
config.platform_config_name = entry.get("display_name") or get_clean_display_name(entry.get("game_name", ""), entry.get("platform", ""))
try:
items = [".."]
for item in sorted(os.listdir(start_path)):
full_path = os.path.join(start_path, item)
if os.path.isdir(full_path):
items.append(item)
config.folder_browser_items = items
except Exception as e:
logger.error(f"Erreur lecture dossier {start_path}: {e}")
config.folder_browser_items = [".."]
config.menu_state = "folder_browser"
config.needs_redraw = True
elif is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
config.menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
@@ -1809,11 +2268,27 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Display
elif config.menu_state == "pause_display_menu":
sel = getattr(config, 'pause_display_selection', 0)
# layout, font submenu, family, [monitor if multi], light, unknown, back
# layout, font submenu, family, [monitor if multi], [display mode on Windows], light, unknown, back
from rgsx_settings import get_available_monitors
monitors = get_available_monitors()
show_monitor = len(monitors) > 1
total = 7 if show_monitor else 6 # dynamic total based on monitor count
show_display_mode = getattr(config, 'OPERATING_SYSTEM', '') == "Windows"
monitor_index = 3 if show_monitor else None
display_mode_index = 4 if show_monitor else 3
if not show_display_mode:
display_mode_index = None
next_index = 3
if show_monitor:
next_index += 1
if show_display_mode:
next_index += 1
light_index = next_index
unknown_index = light_index + 1
back_index = unknown_index + 1
total = back_index + 1
if is_input_matched(event, "up"):
config.pause_display_selection = (sel - 1) % total
config.needs_redraw = True
@@ -1871,8 +2346,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement font family: {e}")
# 3 monitor selection (only if multiple monitors)
elif sel == 3 and show_monitor and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# Monitor selection (only if multiple monitors)
elif monitor_index is not None and sel == monitor_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
from rgsx_settings import get_display_monitor, set_display_monitor
current = get_display_monitor()
@@ -1883,8 +2358,19 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur changement moniteur: {e}")
# light mode toggle (index 4 if show_monitor, else 3)
elif ((sel == 4 and show_monitor) or (sel == 3 and not show_monitor)) and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# Display mode toggle (Windows only)
elif display_mode_index is not None and sel == display_mode_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
from rgsx_settings import get_display_fullscreen, set_display_fullscreen
current = get_display_fullscreen()
set_display_fullscreen(not current)
config.popup_message = _("display_mode_restart_required") if _ else "Restart required to apply screen mode"
config.popup_timer = 3000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle fullscreen/windowed: {e}")
# Light mode toggle
elif sel == light_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
from rgsx_settings import get_light_mode, set_light_mode
current = get_light_mode()
@@ -1894,8 +2380,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle light mode: {e}")
# allow unknown extensions (index 5 if show_monitor, else 4)
elif ((sel == 5 and show_monitor) or (sel == 4 and not show_monitor)) and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
# Allow unknown extensions
elif sel == unknown_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
try:
current = get_allow_unknown_extensions()
new_val = set_allow_unknown_extensions(not current)
@@ -1904,8 +2390,8 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
# back (index 6 if show_monitor, else 5)
elif ((sel == 6 and show_monitor) or (sel == 5 and not show_monitor)) and is_input_matched(event, "confirm"):
# Back
elif sel == back_index and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -2006,7 +2492,7 @@ def handle_controls(event, sources, joystick, screen):
# Sous-menu Games
elif config.menu_state == "pause_games_menu":
sel = getattr(config, 'pause_games_selection', 0)
total = 7 # update cache, history, source, unsupported, hide premium, filter, back
total = 8 # update cache, scan roms, history, source, unsupported, hide premium, filter, back
if is_input_matched(event, "up"):
config.pause_games_selection = (sel - 1) % total
config.needs_redraw = True
@@ -2019,14 +2505,25 @@ def handle_controls(event, sources, joystick, screen):
config.menu_state = "reload_games_data"
config.redownload_confirm_selection = 0
config.needs_redraw = True
elif sel == 1 and is_input_matched(event, "confirm"): # history
elif sel == 1 and is_input_matched(event, "confirm"): # scan local roms
try:
added_games, scanned_platforms = scan_roms_for_downloaded_games()
config.popup_message = _("popup_scan_owned_roms_done").format(added_games, scanned_platforms) if _ else f"ROM scan complete: {added_games} games added across {scanned_platforms} platforms"
config.popup_timer = 4000
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur scan ROMs locaux: {e}")
config.popup_message = _("popup_scan_owned_roms_error").format(str(e)) if _ else f"ROM scan error: {e}"
config.popup_timer = 5000
config.needs_redraw = True
elif sel == 2 and is_input_matched(event, "confirm"): # history
config.history = load_history()
config.current_history_item = 0
config.history_scroll_offset = 0
config.previous_menu_state = "pause_games_menu"
config.menu_state = "history"
config.needs_redraw = True
elif sel == 2 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
try:
current_mode = get_sources_mode()
new_mode = set_sources_mode('custom' if current_mode == 'rgsx' else 'rgsx')
@@ -2041,7 +2538,7 @@ def handle_controls(event, sources, joystick, screen):
logger.info(f"Changement du mode des sources vers {new_mode}")
except Exception as e:
logger.error(f"Erreur changement mode sources: {e}")
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
try:
current = get_show_unsupported_platforms()
new_val = set_show_unsupported_platforms(not current)
@@ -2051,7 +2548,7 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle unsupported: {e}")
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
try:
cur = get_hide_premium_systems()
new_val = set_hide_premium_systems(not cur)
@@ -2060,13 +2557,13 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
except Exception as e:
logger.error(f"Erreur toggle hide_premium_systems: {e}")
elif sel == 5 and is_input_matched(event, "confirm"): # filter platforms
elif sel == 6 and is_input_matched(event, "confirm"): # filter platforms
config.filter_return_to = "pause_games_menu"
config.menu_state = "filter_platforms"
config.selected_filter_index = 0
config.filter_platforms_scroll_offset = 0
config.needs_redraw = True
elif sel == 6 and is_input_matched(event, "confirm"): # back
elif sel == 7 and is_input_matched(event, "confirm"): # back
config.menu_state = "pause_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
@@ -2555,6 +3052,34 @@ def handle_controls(event, sources, joystick, screen):
# Informer qu'un redémarrage est nécessaire
config.popup_message = _("roms_folder_set_restart").format(selected_path) if _ else f"ROMs folder set: {selected_path}\nRestart required!"
config.menu_state = "pause_settings_menu"
elif browser_mode == "history_move":
entry = config.history[config.current_history_item] if config.history and config.current_history_item < len(config.history) else None
actual_matches = getattr(config, 'history_actual_matches', None) or []
if not actual_matches and entry:
actual_matches = get_existing_history_matches(entry)
source_paths = [match_path for _, match_path in actual_matches]
if not source_paths:
actual_path = getattr(config, 'history_actual_path', None)
if actual_path:
source_paths = [actual_path]
success, moved_matches, error_message = move_files_to_directory(source_paths, selected_path)
if success:
config.history_actual_matches = moved_matches
if moved_matches:
config.history_actual_filename, config.history_actual_path = moved_matches[0]
if entry is not None:
entry["moved_paths"] = [path for _, path in moved_matches]
save_history(config.history)
config.popup_message = _("history_move_success").format(len(moved_matches), selected_path) if _ else f"Moved {len(moved_matches)} file(s) to {selected_path}"
config.popup_timer = 3000
logger.info(f"Déplacement historique terminé vers {selected_path}: {len(moved_matches)} fichier(s)")
else:
config.popup_message = _("history_move_error").format(error_message) if _ else f"Move error: {error_message}"
config.popup_timer = 4000
logger.error(f"Erreur déplacement historique vers {selected_path}: {error_message}")
config.menu_state = "history_show_folder"
else:
# Mode dossier plateforme
from rgsx_settings import set_platform_custom_path
@@ -2570,6 +3095,8 @@ def handle_controls(event, sources, joystick, screen):
browser_mode = getattr(config, 'folder_browser_mode', 'platform')
if browser_mode == "roms_root":
config.menu_state = "pause_settings_menu"
elif browser_mode == "history_move":
config.menu_state = "history_show_folder"
else:
config.menu_state = "platform_folder_config"
config.needs_redraw = True

View File

@@ -69,7 +69,7 @@ SDL_TO_PYGAME_KEY = {
# Noms lisibles pour les touches clavier
KEY_NAMES = {
pygame.K_RETURN: "Enter",
pygame.K_ESCAPE: "Échap",
pygame.K_ESCAPE: "Esc/Echap",
pygame.K_SPACE: "Espace",
pygame.K_UP: "",
pygame.K_DOWN: "",
@@ -87,7 +87,7 @@ KEY_NAMES = {
pygame.K_RMETA: "RMeta",
pygame.K_CAPSLOCK: "Verr Maj",
pygame.K_NUMLOCK: "Verr Num",
pygame.K_SCROLLOCK: "Verr Déf",
pygame.K_SCROLLOCK: "Verr Def",
pygame.K_a: "A",
pygame.K_b: "B",
pygame.K_c: "C",
@@ -158,7 +158,7 @@ KEY_NAMES = {
pygame.K_F15: "F15",
pygame.K_INSERT: "Inser",
pygame.K_DELETE: "Suppr",
pygame.K_HOME: "Début",
pygame.K_HOME: "Debut",
pygame.K_END: "Fin",
pygame.K_PAGEUP: "Page+",
pygame.K_PAGEDOWN: "Page-",

File diff suppressed because it is too large Load Diff

View File

@@ -191,11 +191,31 @@ class GameFilters:
base = base + disc_info
return base
@staticmethod
def get_cached_regions(game: Game) -> List[str]:
"""Retourne les régions en les calculant une seule fois par jeu."""
if game.regions is None:
game.regions = GameFilters.get_game_regions(game.display_name)
return game.regions
@staticmethod
def get_cached_non_release(game: Game) -> bool:
"""Retourne le flag non-release en le calculant à la demande."""
if game.is_non_release is None:
game.is_non_release = GameFilters.is_non_release_game(game.display_name)
return game.is_non_release
@staticmethod
def get_cached_base_name(game: Game) -> str:
"""Retourne le nom de base en le calculant une seule fois par jeu."""
if game.base_name is None:
game.base_name = GameFilters.get_base_game_name(game.display_name)
return game.base_name
def get_region_priority(self, game_name: str) -> int:
def get_region_priority(self, game: Game) -> int:
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
# Utiliser la fonction de détection de régions pour être cohérent
game_regions = self.get_game_regions(game_name)
game_regions = self.get_cached_regions(game)
# Trouver la meilleure priorité parmi toutes les régions détectées
best_priority = len(self.region_priority) # Par défaut: priorité la plus basse
@@ -221,14 +241,13 @@ class GameFilters:
return games
filtered_games = []
has_region_excludes = any(state == 'exclude' for state in self.region_filters.values())
# Filtrage par région
for game in games:
game_name = game.display_name
# Vérifier les filtres de région
if self.region_filters:
game_regions = self.get_game_regions(game_name)
if has_region_excludes:
game_regions = self.get_cached_regions(game)
# Vérifier si le jeu a au moins une région incluse
has_included_region = False
@@ -244,7 +263,7 @@ class GameFilters:
continue
# Filtrer les non-release
if self.hide_non_release and self.is_non_release_game(game_name):
if self.hide_non_release and self.get_cached_non_release(game):
continue
filtered_games.append(game)
@@ -260,8 +279,7 @@ class GameFilters:
games_by_base = {}
for game in games:
game_name = game.display_name
base_name = self.get_base_game_name(game_name)
base_name = self.get_cached_base_name(game)
if base_name not in games_by_base:
games_by_base[base_name] = []
@@ -276,7 +294,7 @@ class GameFilters:
else:
# Trier par priorité de région
sorted_games = sorted(game_list,
key=lambda g: self.get_region_priority(g.display_name))
key=self.get_region_priority)
result.append(sorted_games[0])
return result

View File

@@ -1,6 +1,7 @@
import json
import os
import logging
import re
import config
from datetime import datetime
@@ -119,18 +120,39 @@ def clear_history():
try:
# Charger l'historique actuel
current_history = load_history()
# Conserver uniquement les entrées avec statut actif (téléchargement, extraction ou conversion en cours)
# Supporter les deux variantes de statut (anglais et français)
active_statuses = {"Downloading", "Téléchargement", "downloading", "Extracting", "Converting", "Queued"}
preserved_entries = [
entry for entry in current_history
if entry.get("status") in active_statuses
]
# Sauvegarder l'historique filtré
with open(history_path, "w", encoding='utf-8') as f:
json.dump(preserved_entries, f, indent=2, ensure_ascii=False)
active_task_ids = set(getattr(config, 'download_tasks', {}).keys())
active_progress_urls = set(getattr(config, 'download_progress', {}).keys())
queued_urls = {
item.get("url") for item in getattr(config, 'download_queue', [])
if isinstance(item, dict) and item.get("url")
}
queued_task_ids = {
item.get("task_id") for item in getattr(config, 'download_queue', [])
if isinstance(item, dict) and item.get("task_id")
}
def is_truly_active(entry):
if not isinstance(entry, dict):
return False
status = entry.get("status")
if status not in active_statuses:
return False
task_id = entry.get("task_id")
url = entry.get("url")
if status == "Queued":
return task_id in queued_task_ids or url in queued_urls
return task_id in active_task_ids or url in active_progress_urls
preserved_entries = [entry for entry in current_history if is_truly_active(entry)]
save_history(preserved_entries)
removed_count = len(current_history) - len(preserved_entries)
logger.info(f"Historique vidé : {history_path} ({removed_count} entrées supprimées, {len(preserved_entries)} conservées)")
@@ -140,6 +162,118 @@ def clear_history():
# ==================== GESTION DES JEUX TÉLÉCHARGÉS ====================
IGNORED_ROM_SCAN_EXTENSIONS = {
'.bak', '.bmp', '.db', '.gif', '.ini', '.jpeg', '.jpg', '.json', '.log', '.mp4',
'.nfo', '.pdf', '.png', '.srm', '.sav', '.state', '.svg', '.txt', '.webp', '.xml'
}
def normalize_downloaded_game_name(game_name):
"""Normalise un nom de jeu pour les comparaisons en ignorant extension et tags."""
if not isinstance(game_name, str):
return ""
normalized = os.path.basename(game_name.strip())
if not normalized:
return ""
normalized = os.path.splitext(normalized)[0]
normalized = re.sub(r'\s*[\[(][^\])]*[\])]', '', normalized)
normalized = re.sub(r'\s+', ' ', normalized)
return normalized.strip().lower()
def _normalize_downloaded_games_dict(downloaded):
"""Normalise la structure de downloaded_games.json en restant rétrocompatible."""
normalized_downloaded = {}
if not isinstance(downloaded, dict):
return normalized_downloaded
for platform_name, games in downloaded.items():
if not isinstance(platform_name, str):
continue
if not isinstance(games, dict):
continue
normalized_games = {}
for game_name, metadata in games.items():
normalized_name = normalize_downloaded_game_name(game_name)
if not normalized_name:
continue
normalized_games[normalized_name] = metadata if isinstance(metadata, dict) else {}
if normalized_games:
normalized_downloaded[platform_name] = normalized_games
return normalized_downloaded
def _count_downloaded_games(downloaded_games_dict):
return sum(len(games) for games in downloaded_games_dict.values() if isinstance(games, dict))
def scan_roms_for_downloaded_games():
"""Scanne les dossiers ROMs et ajoute les jeux trouvés à downloaded_games.json."""
from utils import load_games
downloaded = _normalize_downloaded_games_dict(getattr(config, 'downloaded_games', {}))
platform_dicts = list(getattr(config, 'platform_dicts', []) or [])
if not platform_dicts:
return 0, 0
scanned_platforms = 0
added_games = 0
for platform_entry in platform_dicts:
if not isinstance(platform_entry, dict):
continue
platform_name = (platform_entry.get('platform_name') or '').strip()
folder_name = (platform_entry.get('folder') or '').strip()
if not platform_name or not folder_name:
continue
roms_path = os.path.join(config.ROMS_FOLDER, folder_name)
if not os.path.isdir(roms_path):
continue
available_games = load_games(platform_name)
available_names = {
normalize_downloaded_game_name(game.name)
for game in available_games
if normalize_downloaded_game_name(game.name)
}
if not available_names:
continue
platform_games = downloaded.setdefault(platform_name, {})
scanned_platforms += 1
for root, _, filenames in os.walk(roms_path):
for filename in filenames:
file_ext = os.path.splitext(filename)[1].lower()
if file_ext in IGNORED_ROM_SCAN_EXTENSIONS:
continue
normalized_name = normalize_downloaded_game_name(filename)
if not normalized_name or normalized_name not in available_names:
continue
if normalized_name not in platform_games:
platform_games[normalized_name] = {}
added_games += 1
config.downloaded_games = downloaded
save_downloaded_games(downloaded)
logger.info(
"Scan ROMs terminé : %s jeux ajoutés sur %s plateformes",
added_games,
scanned_platforms,
)
return added_games, scanned_platforms
def load_downloaded_games():
"""Charge la liste des jeux déjà téléchargés depuis downloaded_games.json."""
downloaded_path = getattr(config, 'DOWNLOADED_GAMES_PATH')
@@ -162,9 +296,10 @@ def load_downloaded_games():
if not isinstance(downloaded, dict):
logger.warning(f"Format downloaded_games.json invalide (pas un dict)")
return {}
logger.debug(f"Jeux téléchargés chargés : {sum(len(v) for v in downloaded.values())} jeux")
return downloaded
normalized_downloaded = _normalize_downloaded_games_dict(downloaded)
logger.debug(f"Jeux téléchargés chargés : {_count_downloaded_games(normalized_downloaded)} jeux")
return normalized_downloaded
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.error(f"Erreur lors de la lecture de {downloaded_path} : {e}")
return {}
@@ -177,17 +312,18 @@ def save_downloaded_games(downloaded_games_dict):
"""Sauvegarde la liste des jeux téléchargés dans downloaded_games.json."""
downloaded_path = getattr(config, 'DOWNLOADED_GAMES_PATH')
try:
normalized_downloaded = _normalize_downloaded_games_dict(downloaded_games_dict)
os.makedirs(os.path.dirname(downloaded_path), exist_ok=True)
# Écriture atomique
temp_path = downloaded_path + '.tmp'
with open(temp_path, "w", encoding='utf-8') as f:
json.dump(downloaded_games_dict, f, indent=2, ensure_ascii=False)
json.dump(normalized_downloaded, f, indent=2, ensure_ascii=False)
f.flush()
os.fsync(f.fileno())
os.replace(temp_path, downloaded_path)
logger.debug(f"Jeux téléchargés sauvegardés : {sum(len(v) for v in downloaded_games_dict.values())} jeux")
logger.debug(f"Jeux téléchargés sauvegardés : {_count_downloaded_games(normalized_downloaded)} jeux")
except Exception as e:
logger.error(f"Erreur lors de l'écriture de {downloaded_path} : {e}")
try:
@@ -200,21 +336,22 @@ def save_downloaded_games(downloaded_games_dict):
def mark_game_as_downloaded(platform_name, game_name, file_size=None):
"""Marque un jeu comme téléchargé."""
downloaded = config.downloaded_games
normalized_name = normalize_downloaded_game_name(game_name)
if not normalized_name:
return
if platform_name not in downloaded:
downloaded[platform_name] = {}
downloaded[platform_name][game_name] = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"size": file_size or "N/A"
}
downloaded[platform_name][normalized_name] = {}
# Sauvegarder immédiatement
save_downloaded_games(downloaded)
logger.info(f"Jeu marqué comme téléchargé : {platform_name} / {game_name}")
logger.info(f"Jeu marqué comme téléchargé : {platform_name} / {normalized_name}")
def is_game_downloaded(platform_name, game_name):
"""Vérifie si un jeu a déjà été téléchargé."""
downloaded = config.downloaded_games
return platform_name in downloaded and game_name in downloaded.get(platform_name, {})
normalized_name = normalize_downloaded_game_name(game_name)
return bool(normalized_name) and platform_name in downloaded and normalized_name in downloaded.get(platform_name, {})

View File

@@ -114,14 +114,14 @@ def get_text(key, default=None):
pass
return str(default) if default is not None else str(key)
def get_available_languages():
def get_available_languages() -> list[str]:
"""Récupère la liste des langues disponibles."""
if not os.path.exists(config.LANGUAGES_FOLDER):
logger.warning(f"Dossier des langues {config.LANGUAGES_FOLDER} non trouvé")
return []
languages = []
languages: list[str] = []
for file in os.listdir(config.LANGUAGES_FOLDER):
if file.endswith(".json"):
lang_code = os.path.splitext(file)[0]

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} Spiele)",
"game_filter": "Aktiver Filter: {0}",
"game_search": "Filtern: {0}",
"global_search_title": "Globale Suche: {0}",
"global_search_empty_query": "Geben Sie einen Namen ein, um alle Systeme zu durchsuchen",
"global_search_no_results": "Keine Ergebnisse fur: {0}",
"game_header_name": "Name",
"game_header_ext": "Ext",
"game_header_size": "Größe",
"history_title": "Downloads ({0})",
"history_empty": "Keine Downloads im Verlauf",
@@ -111,6 +115,7 @@
"controls_action_clear_history": "Verlauf leeren",
"controls_action_history": "Verlauf / Downloads",
"controls_action_close_history": "Verlauf schließen",
"history_column_folder": "Ordner",
"controls_action_queue": "Warteschlange",
"controls_action_delete": "Löschen",
"controls_action_space": "Leerzeichen",
@@ -144,6 +149,8 @@
"controls_confirm_select": "Bestätigen/Auswählen",
"controls_cancel_back": "Abbrechen/Zurück",
"controls_filter_search": "Filtern/Suchen",
"controls_action_edit_search": "Suche bearbeiten",
"controls_action_show_results": "Ergebnisse zeigen",
"network_download_failed": "Download nach {0} Versuchen fehlgeschlagen",
"network_api_error": "Fehler bei der API-Anfrage, der Schlüssel könnte falsch sein: {0}",
"network_download_error": "Downloadfehler {0}: {1}",
@@ -232,6 +239,7 @@
"instruction_games_history": "Vergangene Downloads und Status anzeigen",
"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln",
"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren",
"instruction_games_scan_owned": "ROM-Ordner scannen und bereits vorhandene Spiele markieren",
"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren",
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
"instruction_settings_auto_extract": "Automatische Archivextraktion nach Download aktivieren/deaktivieren",
@@ -258,6 +266,9 @@
"settings_web_service_success_disabled": "Web-Dienst beim Booten deaktiviert",
"settings_web_service_error": "Fehler: {0}",
"settings_custom_dns": "Custom DNS beim Booten",
"menu_scan_owned_roms": "Vorhandene ROMs scannen",
"popup_scan_owned_roms_done": "ROM-Scan abgeschlossen: {0} Spiele auf {1} Plattformen hinzugefügt",
"popup_scan_owned_roms_error": "ROM-Scan-Fehler: {0}",
"settings_custom_dns_enabled": "Aktiviert",
"settings_custom_dns_disabled": "Deaktiviert",
"settings_custom_dns_enabling": "Custom DNS wird aktiviert...",
@@ -296,6 +307,7 @@
"history_option_delete_game": "Spiel löschen",
"history_option_error_info": "Fehlerdetails",
"history_option_retry": "Download wiederholen",
"history_move_action": "Verschieben",
"history_option_back": "Zurück",
"history_folder_path_label": "Zielpfad:",
"history_scraper_not_implemented": "Scraper noch nicht implementiert",
@@ -305,6 +317,8 @@
"history_extracted": "Extrahiert",
"history_delete_success": "Spiel erfolgreich gelöscht",
"history_delete_error": "Fehler beim Löschen des Spiels: {0}",
"history_move_success": "{0} Datei(en) verschoben nach: {1}",
"history_move_error": "Fehler beim Verschieben: {0}",
"history_error_details_title": "Fehlerdetails",
"history_no_error_message": "Keine Fehlermeldung verfügbar",
"web_title": "RGSX Web-Oberfläche",
@@ -469,6 +483,7 @@
"platform_folder_set": "Ordner für {0} festgelegt: {1}",
"platform_folder_default_path": "Standard: {0}",
"folder_browser_title": "Ordner für {0} auswählen",
"folder_browser_title_history_move": "Zielordner auswählen",
"folder_browser_parent": "Übergeordneter Ordner",
"folder_browser_enter": "Öffnen",
"folder_browser_select": "Auswählen",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} games)",
"game_filter": "Active filter: {0}",
"game_search": "Filter: {0}",
"global_search_title": "Global search: {0}",
"global_search_empty_query": "Type a game name to search across all systems",
"global_search_no_results": "No results for: {0}",
"game_header_name": "Name",
"game_header_ext": "Ext",
"game_header_size": "Size",
"history_title": "Downloads ({0})",
"history_empty": "No downloads in history",
@@ -112,6 +116,7 @@
"support_dialog_error": "Error generating support file:\n{0}\n\nPress {1} to return to the menu.",
"controls_action_history": "History / Downloads",
"controls_action_close_history": "Close History",
"history_column_folder": "Folder",
"network_checking_updates": "Update in progress please wait...",
"network_update_available": "Update available: {0}",
"network_extracting_update": "Extracting update...",
@@ -165,6 +170,8 @@
"controls_confirm_select": "Confirm/Select",
"controls_cancel_back": "Cancel/Back",
"controls_filter_search": "Filter/Search",
"controls_action_edit_search": "Edit search",
"controls_action_show_results": "Show results",
"symlink_option_enabled": "Symlink option enabled",
"symlink_option_disabled": "Symlink option disabled",
"menu_games_source_prefix": "Game source",
@@ -234,6 +241,7 @@
"instruction_games_history": "List past downloads and statuses",
"instruction_games_source_mode": "Switch between RGSX or your own custom list source",
"instruction_games_update_cache": "Redownload & refresh current games list",
"instruction_games_scan_owned": "Scan your ROM folders and mark matching games as already owned",
"instruction_settings_music": "Enable or disable background music playback",
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
"instruction_settings_auto_extract": "Toggle automatic archive extraction after download",
@@ -298,6 +306,10 @@
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",
"history_move_action": "Move",
"menu_scan_owned_roms": "Scan owned ROMs",
"popup_scan_owned_roms_done": "ROM scan complete: {0} games added across {1} platforms",
"popup_scan_owned_roms_error": "ROM scan error: {0}",
"history_option_back": "Back",
"history_folder_path_label": "Destination path:",
"history_scraper_not_implemented": "Scraper not yet implemented",
@@ -307,6 +319,8 @@
"history_extracted": "Extracted",
"history_delete_success": "Game deleted successfully",
"history_delete_error": "Error deleting game: {0}",
"history_move_success": "Moved {0} file(s) to: {1}",
"history_move_error": "Error while moving files: {0}",
"history_error_details_title": "Error Details",
"history_no_error_message": "No error message available",
"web_title": "RGSX Web Interface",
@@ -469,6 +483,7 @@
"platform_folder_set": "Folder set for {0}: {1}",
"platform_folder_default_path": "Default: {0}",
"folder_browser_title": "Select folder for {0}",
"folder_browser_title_history_move": "Select destination folder",
"folder_browser_parent": "Parent folder",
"folder_browser_enter": "Enter",
"folder_browser_select": "Select",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} juegos)",
"game_filter": "Filtro activo: {0}",
"game_search": "Filtrar: {0}",
"global_search_title": "Busqueda global: {0}",
"global_search_empty_query": "Escribe un nombre para buscar en todas las consolas",
"global_search_no_results": "Sin resultados para: {0}",
"game_header_name": "Nombre",
"game_header_ext": "Ext",
"game_header_size": "Tamaño",
"history_title": "Descargas ({0})",
"history_empty": "No hay descargas en el historial",
@@ -109,6 +113,7 @@
"controls_action_clear_history": "Vaciar historial",
"controls_action_history": "Historial / Descargas",
"controls_action_close_history": "Cerrar Historial",
"history_column_folder": "Carpeta",
"controls_action_delete": "Eliminar",
"controls_action_space": "Espacio",
"controls_action_start": "Ayuda / Configuración",
@@ -142,6 +147,8 @@
"controls_confirm_select": "Confirmar/Seleccionar",
"controls_cancel_back": "Cancelar/Volver",
"controls_filter_search": "Filtrar/Buscar",
"controls_action_edit_search": "Editar busqueda",
"controls_action_show_results": "Ver resultados",
"network_download_failed": "Error en la descarga tras {0} intentos",
"network_api_error": "Error en la solicitud de API, la clave puede ser incorrecta: {0}",
"network_download_error": "Error en la descarga {0}: {1}",
@@ -232,6 +239,7 @@
"instruction_games_history": "Ver descargas pasadas y su estado",
"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada",
"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos",
"instruction_games_scan_owned": "Escanear las carpetas ROMs y marcar los juegos que ya posees",
"instruction_settings_music": "Activar o desactivar música de fondo",
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
"instruction_settings_auto_extract": "Activar/desactivar extracción automática de archivos después de descargar",
@@ -258,6 +266,9 @@
"settings_web_service_success_disabled": "Servicio web desactivado al inicio",
"settings_web_service_error": "Error: {0}",
"settings_custom_dns": "DNS Personalizado al Inicio",
"menu_scan_owned_roms": "Escanear ROMs disponibles",
"popup_scan_owned_roms_done": "Escaneo ROM completado: {0} juegos añadidos en {1} plataformas",
"popup_scan_owned_roms_error": "Error al escanear ROMs: {0}",
"settings_custom_dns_enabled": "Activado",
"settings_custom_dns_disabled": "Desactivado",
"settings_custom_dns_enabling": "Activando DNS personalizado...",
@@ -296,6 +307,7 @@
"history_option_delete_game": "Eliminar juego",
"history_option_error_info": "Detalles del error",
"history_option_retry": "Reintentar descarga",
"history_move_action": "Mover",
"history_option_back": "Volver",
"history_folder_path_label": "Ruta de destino:",
"history_scraper_not_implemented": "Scraper aún no implementado",
@@ -305,6 +317,8 @@
"history_extracted": "Extraído",
"history_delete_success": "Juego eliminado con éxito",
"history_delete_error": "Error al eliminar juego: {0}",
"history_move_success": "{0} archivo(s) movido(s) a: {1}",
"history_move_error": "Error al mover los archivos: {0}",
"history_error_details_title": "Detalles del error",
"history_no_error_message": "No hay mensaje de error disponible",
"web_title": "Interfaz Web RGSX",
@@ -467,6 +481,7 @@
"platform_folder_set": "Carpeta establecida para {0}: {1}",
"platform_folder_default_path": "Por defecto: {0}",
"folder_browser_title": "Seleccionar carpeta para {0}",
"folder_browser_title_history_move": "Seleccionar carpeta de destino",
"folder_browser_parent": "Carpeta superior",
"folder_browser_enter": "Entrar",
"folder_browser_select": "Seleccionar",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} jeux)",
"game_filter": "Filtre actif : {0}",
"game_search": "Filtrer : {0}",
"global_search_title": "Recherche globale : {0}",
"global_search_empty_query": "Saisissez un nom pour rechercher dans toutes les consoles",
"global_search_no_results": "Aucun resultat pour : {0}",
"game_header_name": "Nom",
"game_header_ext": "Ext",
"game_header_size": "Taille",
"history_title": "Téléchargements ({0})",
"history_empty": "Aucun téléchargement dans l'historique",
@@ -106,6 +110,7 @@
"controls_action_queue": "Mettre en file d'attente",
"controls_action_history": "Historique / Téléchargements",
"controls_action_close_history": "Fermer l'historique",
"history_column_folder": "Dossier",
"controls_action_delete": "Supprimer",
"controls_action_space": "Espace",
"controls_action_start": "Aide / Réglages",
@@ -138,6 +143,8 @@
"controls_confirm_select": "Confirmer/Sélectionner",
"controls_cancel_back": "Annuler/Retour",
"controls_filter_search": "Filtrer/Rechercher",
"controls_action_edit_search": "Modifier recherche",
"controls_action_show_results": "Voir resultats",
"network_download_failed": "Échec du téléchargement après {0} tentatives",
"network_api_error": "Erreur lors de la requête API, la clé est peut-être incorrecte: {0}",
"network_download_error": "Erreur téléchargement {0}: {1}",
@@ -234,6 +241,7 @@
"instruction_games_history": "Lister les téléchargements passés et leur statut",
"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée",
"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux",
"instruction_games_scan_owned": "Scanner les dossiers ROMs et marquer les jeux déjà possédés",
"instruction_settings_music": "Activer ou désactiver la lecture musicale",
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
"instruction_settings_auto_extract": "Activer/désactiver l'extraction automatique des archives après téléchargement",
@@ -298,6 +306,10 @@
"history_option_delete_game": "Supprimer le jeu",
"history_option_error_info": "Détails de l'erreur",
"history_option_retry": "Retenter le téléchargement",
"history_move_action": "Déplacer",
"menu_scan_owned_roms": "Scanner les ROMs présentes",
"popup_scan_owned_roms_done": "Scan ROMs terminé : {0} jeux ajoutés sur {1} plateformes",
"popup_scan_owned_roms_error": "Erreur scan ROMs : {0}",
"history_option_back": "Retour",
"history_folder_path_label": "Chemin de destination :",
"history_scraper_not_implemented": "Scraper pas encore implémenté",
@@ -307,6 +319,8 @@
"history_extracted": "Extrait",
"history_delete_success": "Jeu supprimé avec succès",
"history_delete_error": "Erreur lors de la suppression du jeu : {0}",
"history_move_success": "{0} fichier(s) déplacé(s) vers : {1}",
"history_move_error": "Erreur lors du déplacement : {0}",
"history_error_details_title": "Détails de l'erreur",
"history_no_error_message": "Aucun message d'erreur disponible",
"web_title": "Interface Web RGSX",
@@ -469,6 +483,7 @@
"platform_folder_set": "Dossier défini pour {0}: {1}",
"platform_folder_default_path": "Par défaut: {0}",
"folder_browser_title": "Sélectionner le dossier pour {0}",
"folder_browser_title_history_move": "Sélectionner le dossier de destination",
"folder_browser_parent": "Dossier parent",
"folder_browser_enter": "Entrer",
"folder_browser_select": "Valider",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} giochi)",
"game_filter": "Filtro attivo: {0}",
"game_search": "Filtro: {0}",
"global_search_title": "Ricerca globale: {0}",
"global_search_empty_query": "Digita un nome per cercare in tutte le console",
"global_search_no_results": "Nessun risultato per: {0}",
"game_header_name": "Nome",
"game_header_ext": "Ext",
"game_header_size": "Dimensione",
"history_title": "Download ({0})",
"history_empty": "Nessun download nella cronologia",
@@ -107,6 +111,7 @@
"controls_action_clear_history": "Cancella cronologia",
"controls_action_history": "Cronologia / Downloads",
"controls_action_close_history": "Chiudi Cronologia",
"history_column_folder": "Cartella",
"controls_action_delete": "Elimina",
"controls_action_space": "Spazio",
"controls_action_start": "Aiuto / Impostazioni",
@@ -164,6 +169,8 @@
"controls_confirm_select": "Conferma/Seleziona",
"controls_cancel_back": "Annulla/Indietro",
"controls_filter_search": "Filtro/Ricerca",
"controls_action_edit_search": "Modifica ricerca",
"controls_action_show_results": "Mostra risultati",
"games_source_rgsx": "RGSX",
"sources_mode_rgsx_select_info": "RGSX: aggiorna l'elenco dei giochi",
"games_source_custom": "Personalizzato",
@@ -227,6 +234,7 @@
"instruction_games_history": "Elencare download passati e stato",
"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata",
"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi",
"instruction_games_scan_owned": "Scansiona le cartelle ROMs e segna i giochi gia posseduti",
"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo",
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
"instruction_settings_auto_extract": "Attivare/disattivare estrazione automatica archivi dopo il download",
@@ -258,6 +266,9 @@
"settings_custom_dns_enabling": "Abilitazione DNS personalizzato...",
"settings_custom_dns_disabling": "Disabilitazione DNS personalizzato...",
"settings_custom_dns_success_enabled": "DNS personalizzato abilitato all'avvio (1.1.1.1)",
"menu_scan_owned_roms": "Scansiona ROM presenti",
"popup_scan_owned_roms_done": "Scansione ROM completata: {0} giochi aggiunti su {1} piattaforme",
"popup_scan_owned_roms_error": "Errore scansione ROM: {0}",
"settings_custom_dns_success_disabled": "DNS personalizzato disabilitato all'avvio",
"controls_desc_confirm": "Confermare (es. A/Croce)",
"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)",
@@ -291,6 +302,7 @@
"history_option_delete_game": "Elimina gioco",
"history_option_error_info": "Dettagli errore",
"history_option_retry": "Riprova download",
"history_move_action": "Sposta",
"history_option_back": "Indietro",
"history_folder_path_label": "Percorso destinazione:",
"history_scraper_not_implemented": "Scraper non ancora implementato",
@@ -300,6 +312,8 @@
"history_extracted": "Estratto",
"history_delete_success": "Gioco eliminato con successo",
"history_delete_error": "Errore durante l'eliminazione del gioco: {0}",
"history_move_success": "{0} file spostato/i in: {1}",
"history_move_error": "Errore durante lo spostamento: {0}",
"history_error_details_title": "Dettagli errore",
"history_no_error_message": "Nessun messaggio di errore disponibile",
"web_title": "Interfaccia Web RGSX",
@@ -465,6 +479,7 @@
"platform_folder_set": "Cartella impostata per {0}: {1}",
"platform_folder_default_path": "Predefinito: {0}",
"folder_browser_title": "Seleziona cartella per {0}",
"folder_browser_title_history_move": "Seleziona cartella di destinazione",
"folder_browser_parent": "Cartella superiore",
"folder_browser_enter": "Entra",
"folder_browser_select": "Seleziona",

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} jogos)",
"game_filter": "Filtro ativo: {0}",
"game_search": "Filtro: {0}",
"global_search_title": "Busca global: {0}",
"global_search_empty_query": "Digite um nome para buscar em todos os consoles",
"global_search_no_results": "Nenhum resultado para: {0}",
"game_header_name": "Nome",
"game_header_ext": "Ext",
"game_header_size": "Tamanho",
"history_title": "Downloads ({0})",
"history_empty": "Nenhum download no histórico",
@@ -111,6 +115,7 @@
"controls_action_clear_history": "Limpar histórico",
"controls_action_history": "Histórico / Downloads",
"controls_action_close_history": "Fechar Histórico",
"history_column_folder": "Pasta",
"controls_action_delete": "Deletar",
"controls_action_space": "Espaço",
"controls_action_start": "Ajuda / Configurações",
@@ -167,6 +172,8 @@
"controls_confirm_select": "Confirmar/Selecionar",
"controls_cancel_back": "Cancelar/Voltar",
"controls_filter_search": "Filtrar/Buscar",
"controls_action_edit_search": "Editar busca",
"controls_action_show_results": "Ver resultados",
"symlink_option_enabled": "Opção de symlink ativada",
"symlink_option_disabled": "Opção de symlink desativada",
"menu_games_source_prefix": "Fonte de jogos",
@@ -233,6 +240,7 @@
"instruction_games_history": "Listar downloads anteriores e status",
"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada",
"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos",
"instruction_games_scan_owned": "Verificar as pastas ROMs e marcar os jogos ja existentes",
"instruction_settings_music": "Ativar ou desativar música de fundo",
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
"instruction_settings_auto_extract": "Ativar/desativar extração automática de arquivos após download",
@@ -258,6 +266,9 @@
"settings_web_service_success_enabled": "Serviço web ativado na inicialização",
"settings_web_service_success_disabled": "Serviço web desativado na inicialização",
"settings_web_service_error": "Erro: {0}",
"menu_scan_owned_roms": "Verificar ROMs existentes",
"popup_scan_owned_roms_done": "Verificacao de ROMs concluida: {0} jogos adicionados em {1} plataformas",
"popup_scan_owned_roms_error": "Erro ao verificar ROMs: {0}",
"settings_custom_dns": "DNS Personalizado na Inicialização",
"settings_custom_dns_enabled": "Ativado",
"settings_custom_dns_disabled": "Desativado",
@@ -297,6 +308,7 @@
"history_option_delete_game": "Excluir jogo",
"history_option_error_info": "Detalhes do erro",
"history_option_retry": "Tentar novamente",
"history_move_action": "Mover",
"history_option_back": "Voltar",
"history_folder_path_label": "Caminho de destino:",
"history_scraper_not_implemented": "Scraper ainda não implementado",
@@ -306,6 +318,8 @@
"history_extracted": "Extraído",
"history_delete_success": "Jogo excluído com sucesso",
"history_delete_error": "Erro ao excluir jogo: {0}",
"history_move_success": "{0} arquivo(s) movido(s) para: {1}",
"history_move_error": "Erro ao mover os arquivos: {0}",
"history_error_details_title": "Detalhes do erro",
"history_no_error_message": "Nenhuma mensagem de erro disponível",
"web_title": "Interface Web RGSX",
@@ -469,6 +483,7 @@
"platform_folder_set": "Pasta definida para {0}: {1}",
"platform_folder_default_path": "Padrão: {0}",
"folder_browser_title": "Selecionar pasta para {0}",
"folder_browser_title_history_move": "Selecionar pasta de destino",
"folder_browser_parent": "Pasta superior",
"folder_browser_enter": "Entrar",
"folder_browser_select": "Selecionar",

View File

@@ -15,7 +15,7 @@ try:
except Exception:
pygame = None # type: ignore
from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZIP
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie, get_clean_display_name
from history import save_history
from display import show_toast
import logging
@@ -874,6 +874,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
entry["downloaded_size"] = 0
entry["platform"] = platform
entry["game_name"] = game_name
entry["display_name"] = get_clean_display_name(game_name, platform)
entry["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry["task_id"] = task_id
break
@@ -883,6 +884,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
config.history.append({
"platform": platform,
"game_name": game_name,
"display_name": get_clean_display_name(game_name, platform),
"url": url,
"status": "Downloading",
"progress": 0,
@@ -1659,17 +1661,21 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
keys_info = load_api_keys()
config.API_KEY_1FICHIER = keys_info.get('1fichier', '')
config.API_KEY_ALLDEBRID = keys_info.get('alldebrid', '')
config.API_KEY_DEBRIDLINK = keys_info.get('debridlink', '')
config.API_KEY_REALDEBRID = keys_info.get('realdebrid', '')
if not config.API_KEY_1FICHIER and config.API_KEY_ALLDEBRID:
logger.debug("Clé 1fichier absente, utilisation fallback AllDebrid")
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and config.API_KEY_REALDEBRID:
logger.debug("Clé 1fichier & AllDebrid absentes, utilisation fallback RealDebrid")
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_REALDEBRID:
logger.debug("Aucune clé API disponible (1fichier, AllDebrid, RealDebrid)")
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and config.API_KEY_DEBRIDLINK:
logger.debug("Clé 1fichier & AllDebrid absentes, utilisation fallback Debrid-Link")
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_DEBRIDLINK and config.API_KEY_REALDEBRID:
logger.debug("Clé 1fichier, AllDebrid & Debrid-Link absentes, utilisation fallback RealDebrid")
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_DEBRIDLINK and not config.API_KEY_REALDEBRID:
logger.debug("Aucune clé API disponible (1fichier, AllDebrid, Debrid-Link, RealDebrid)")
logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
logger.debug(
f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'} / "
f"AllDebrid: {'présente' if config.API_KEY_ALLDEBRID else 'absente'} / "
f"Debrid-Link: {'présente' if config.API_KEY_DEBRIDLINK else 'absente'} / "
f"RealDebrid: {'présente' if config.API_KEY_REALDEBRID else 'absente'} (reloaded={keys_info.get('reloaded')})"
)
result = [None, None]
@@ -1715,7 +1721,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
if task_id not in cancel_events:
cancel_events[task_id] = threading.Event()
provider_used = None # '1F', 'AD', 'RD'
provider_used = None # '1F', 'AD', 'DL', 'RD'
def _set_provider_in_history(pfx: str):
try:
@@ -1762,6 +1768,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
entry["downloaded_size"] = 0
entry["platform"] = platform
entry["game_name"] = game_name
entry["display_name"] = get_clean_display_name(game_name, platform)
entry["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry["task_id"] = task_id
break
@@ -1771,6 +1778,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
config.history.append({
"platform": platform,
"game_name": game_name,
"display_name": get_clean_display_name(game_name, platform),
"url": url,
"status": "Downloading",
"progress": 0,
@@ -2074,6 +2082,92 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
logger.warning(f"AllDebrid status != success: {ad_json}")
except Exception as e:
logger.error(f"Erreur AllDebrid fallback: {e}")
# Tentative Debrid-Link si pas de final_url
if not final_url and getattr(config, 'API_KEY_DEBRIDLINK', ''):
logger.debug("Tentative fallback Debrid-Link (downloader/add)")
try:
dl_key = config.API_KEY_DEBRIDLINK
headers_dl = {
"Authorization": f"Bearer {dl_key}",
"Content-Type": "application/json",
}
payload_dl = {"url": link}
dl_resp = requests.post(
"https://debrid-link.com/api/v2/downloader/add",
json=payload_dl,
headers=headers_dl,
timeout=30
)
dl_status = dl_resp.status_code
raw_text_dl = None
dl_json = None
try:
raw_text_dl = dl_resp.text
except Exception:
pass
try:
dl_json = dl_resp.json()
except Exception:
dl_json = None
logger.debug(f"Réponse Debrid-Link code={dl_status} body_snippet={(raw_text_dl[:120] + '...') if raw_text_dl and len(raw_text_dl) > 120 else raw_text_dl}")
DEBRIDLINK_ERROR_MAP = {
"badToken": "DL: Invalid API key",
"notDebrid": "DL: Host unavailable",
"hostNotValid": "DL: Unsupported host",
"fileNotFound": "DL: File not found",
"fileNotAvailable": "DL: File temporarily unavailable",
"badFileUrl": "DL: Invalid link",
"badFilePassword": "DL: Invalid file password",
"notFreeHost": "DL: Premium account only",
"maintenanceHost": "DL: Host in maintenance",
"noServerHost": "DL: No server available",
"maxLink": "DL: Daily link limit reached",
"maxLinkHost": "DL: Daily host limit reached",
"maxData": "DL: Daily data limit reached",
"maxDataHost": "DL: Daily host data limit reached",
"disabledServerHost": "DL: Server or VPN not allowed",
"floodDetected": "DL: Rate limit reached",
}
error_message = None
error_message_raw = None
if dl_json and isinstance(dl_json, dict):
if dl_json.get('success') is True:
value = dl_json.get('value') or {}
if isinstance(value, dict):
final_url = value.get('downloadUrl') or value.get('downloadURL') or value.get('link') or value.get('url')
filename = value.get('name') or value.get('filename') or filename or game_name
else:
error_code = dl_json.get('error')
if error_code:
error_message = DEBRIDLINK_ERROR_MAP.get(error_code, f"DL: {error_code}")
error_message_raw = str(error_code)
if dl_status in (200, 201) and final_url:
logger.debug("Débridage réussi via Debrid-Link")
provider_used = 'DL'
_set_provider_in_history(provider_used)
elif not final_url:
if not error_message:
if dl_status == 401:
error_message = "DL: Unauthorized (401)"
elif dl_status == 429:
error_message = "DL: Rate limited (429)"
elif dl_status >= 500:
error_message = f"DL: Server error ({dl_status})"
else:
error_message = f"DL: Unexpected status ({dl_status})"
error_message_raw = raw_text_dl or error_message
logger.warning(f"Debrid-Link fallback échec: {error_message}")
result[0] = False
result[1] = error_message
try:
if isinstance(result, list):
result.append({"raw_error_debridlink": error_message_raw})
except Exception:
pass
except Exception as e:
logger.error(f"Exception Debrid-Link fallback: {e}")
# Tentative RealDebrid si pas de final_url
if not final_url and getattr(config, 'API_KEY_REALDEBRID', ''):
logger.debug("Tentative fallback RealDebrid (unlock)")
@@ -2300,9 +2394,9 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
content_length = head_response.headers.get('content-length')
if content_length:
remote_size = int(content_length)
logger.debug(f"Taille du fichier serveur (AllDebrid/RealDebrid): {remote_size} octets")
logger.debug(f"Taille du fichier serveur (AllDebrid/Debrid-Link/RealDebrid): {remote_size} octets")
except Exception as e:
logger.debug(f"Impossible de vérifier la taille serveur (AllDebrid/RealDebrid): {e}")
logger.debug(f"Impossible de vérifier la taille serveur (AllDebrid/Debrid-Link/RealDebrid): {e}")
# Vérifier si le fichier existe déjà (exact ou avec autre extension)
file_found = False

View File

@@ -21,7 +21,7 @@ from datetime import datetime, timezone
from email.utils import formatdate, parsedate_to_datetime
import config
from history import load_history, save_history
from utils import load_sources, load_games, extract_data
from utils import load_sources, load_games, extract_data, get_clean_display_name
from network import download_rom, download_from_1fichier
from pathlib import Path
from rgsx_settings import get_language
@@ -883,6 +883,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
settings['api_keys'] = {
'1fichier': api_keys_data.get('1fichier', ''),
'alldebrid': api_keys_data.get('alldebrid', ''),
'debridlink': api_keys_data.get('debridlink', ''),
'realdebrid': api_keys_data.get('realdebrid', '')
}
@@ -1242,6 +1243,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
queue_history_entry = {
'platform': platform,
'game_name': game_name,
'display_name': get_clean_display_name(game_name, platform),
'status': 'Queued',
'url': game_url,
'progress': 0,
@@ -1279,6 +1281,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
download_history_entry = {
'platform': platform,
'game_name': game_name,
'display_name': get_clean_display_name(game_name, platform),
'status': 'Downloading',
'url': game_url,
'progress': 0,

View File

@@ -2115,6 +2115,12 @@
<input type="password" id="setting-api-alldebrid" value="${settings.api_keys?.alldebrid || ''}"
placeholder="Enter AllDebrid API key">
</div>
<div style="margin-bottom: 15px;">
<label>Debrid-Link API Key</label>
<input type="password" id="setting-api-debridlink" value="${settings.api_keys?.debridlink || ''}"
placeholder="Enter Debrid-Link API key">
</div>
<div style="margin-bottom: 20px;">
<label>RealDebrid API Key</label>
@@ -2187,6 +2193,7 @@
api_keys: {
'1fichier': document.getElementById('setting-api-1fichier')?.value.trim() || '',
'alldebrid': document.getElementById('setting-api-alldebrid')?.value.trim() || '',
'debridlink': document.getElementById('setting-api-debridlink')?.value.trim() || '',
'realdebrid': document.getElementById('setting-api-realdebrid')?.value.trim() || ''
},
game_filters: {

View File

@@ -33,6 +33,36 @@ import tempfile
logger = logging.getLogger(__name__)
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
def get_clean_display_name(raw_name, platform_id=None):
"""Return a user-facing game title from a raw file/path entry."""
text = str(raw_name or "").strip()
if not text:
return ""
normalized = text.replace("\\", "/")
leaf_name = normalized.rsplit("/", 1)[-1]
display_name = Path(leaf_name).stem.strip()
prefixes = []
if platform_id:
prefixes.append(str(platform_id).strip())
platform_label = getattr(config, "platform_names", {}).get(platform_id)
if platform_label:
prefixes.append(str(platform_label).strip())
for prefix in prefixes:
if not prefix:
continue
pattern = rf"^{re.escape(prefix)}[\s\-_:]+"
updated_name = re.sub(pattern, "", display_name, flags=re.IGNORECASE).strip()
if updated_name:
display_name = updated_name
return display_name.strip(" -_/")
_games_cache = {}
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
@@ -1195,9 +1225,15 @@ def load_games(platform_id:str) -> list[Game]:
game_file = c
break
if not game_file:
_games_cache.pop(platform_id, None)
logger.warning(f"Aucun fichier de jeux trouvé pour {platform_id} (candidats: {candidates})")
return []
game_mtime_ns = os.stat(game_file).st_mtime_ns
cached_entry = _games_cache.get(platform_id)
if cached_entry and cached_entry.get("path") == game_file and cached_entry.get("mtime_ns") == game_mtime_ns:
return cached_entry["games"]
with open(game_file, 'r', encoding='utf-8') as f:
data = json.load(f)
@@ -1239,11 +1275,17 @@ def load_games(platform_id:str) -> list[Game]:
games_list: list[Game] = []
for name, url, size in normalized:
display_name = Path(name).stem
display_name = display_name.replace(platform_id, "")
display_name = get_clean_display_name(name, platform_id)
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
_games_cache[platform_id] = {
"path": game_file,
"mtime_ns": game_mtime_ns,
"games": games_list,
}
return games_list
except Exception as e:
_games_cache.pop(platform_id, None)
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
return []
@@ -2738,24 +2780,25 @@ def set_music_popup(music_name):
config.needs_redraw = True # Forcer le redraw pour afficher le nom de la musique
def load_api_keys(force: bool = False):
"""Charge les clés API (1fichier, AllDebrid, RealDebrid) en une seule passe.
"""Charge les clés API (1fichier, AllDebrid, Debrid-Link, RealDebrid) en une seule passe.
- Crée les fichiers vides s'ils n'existent pas
- Met à jour config.API_KEY_1FICHIER, config.API_KEY_ALLDEBRID, config.API_KEY_REALDEBRID
- Met à jour config.API_KEY_1FICHIER, config.API_KEY_ALLDEBRID, config.API_KEY_DEBRIDLINK, config.API_KEY_REALDEBRID
- Utilise un cache basé sur le mtime pour éviter des relectures
- force=True ignore le cache et relit systématiquement
Retourne: { '1fichier': str, 'alldebrid': str, 'realdebrid': str, 'reloaded': bool }
Retourne: { '1fichier': str, 'alldebrid': str, 'debridlink': str, 'realdebrid': str, 'reloaded': bool }
"""
try:
paths = {
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK_PATH', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID_PATH', ''),
}
cache_attr = '_api_keys_cache'
if not hasattr(config, cache_attr):
setattr(config, cache_attr, {'1fichier_mtime': None, 'alldebrid_mtime': None, 'realdebrid_mtime': None})
setattr(config, cache_attr, {'1fichier_mtime': None, 'alldebrid_mtime': None, 'debridlink_mtime': None, 'realdebrid_mtime': None})
cache_data = getattr(config, cache_attr)
reloaded = False
@@ -2789,6 +2832,8 @@ def load_api_keys(force: bool = False):
config.API_KEY_1FICHIER = value
elif key_name == 'alldebrid':
config.API_KEY_ALLDEBRID = value
elif key_name == 'debridlink':
config.API_KEY_DEBRIDLINK = value
elif key_name == 'realdebrid':
config.API_KEY_REALDEBRID = value
cache_data[cache_key] = mtime
@@ -2796,6 +2841,7 @@ def load_api_keys(force: bool = False):
return {
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
'reloaded': reloaded
}
@@ -2804,6 +2850,7 @@ def load_api_keys(force: bool = False):
return {
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
'reloaded': False
}
@@ -2861,10 +2908,10 @@ def load_archive_org_cookie(force: bool = False) -> str:
def save_api_keys(api_keys: dict):
"""Sauvegarde les clés API (1fichier, AllDebrid, RealDebrid) dans leurs fichiers respectifs.
"""Sauvegarde les clés API (1fichier, AllDebrid, Debrid-Link, RealDebrid) dans leurs fichiers respectifs.
Args:
api_keys: dict avec les clés '1fichier', 'alldebrid', 'realdebrid'
api_keys: dict avec les clés '1fichier', 'alldebrid', 'debridlink', 'realdebrid'
Retourne: True si au moins une clé a été sauvegardée avec succès
"""
@@ -2874,6 +2921,7 @@ def save_api_keys(api_keys: dict):
paths = {
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK_PATH', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID_PATH', ''),
}
@@ -2901,6 +2949,8 @@ def save_api_keys(api_keys: dict):
config.API_KEY_1FICHIER = value.strip()
elif key_name == 'alldebrid':
config.API_KEY_ALLDEBRID = value.strip()
elif key_name == 'debridlink':
config.API_KEY_DEBRIDLINK = value.strip()
elif key_name == 'realdebrid':
config.API_KEY_REALDEBRID = value.strip()
@@ -2926,6 +2976,9 @@ def load_api_key_1fichier(force: bool = False): # pragma: no cover
def load_api_key_alldebrid(force: bool = False): # pragma: no cover
return load_api_keys(force).get('alldebrid', '')
def load_api_key_debridlink(force: bool = False): # pragma: no cover
return load_api_keys(force).get('debridlink', '')
def load_api_key_realdebrid(force: bool = False): # pragma: no cover
return load_api_keys(force).get('realdebrid', '')
@@ -2938,19 +2991,19 @@ def ensure_api_keys_loaded(force: bool = False): # pragma: no cover
# ------------------------------
def build_provider_paths_string():
"""Retourne une chaîne listant les chemins des fichiers de clés pour affichage/erreurs."""
return f"{getattr(config, 'API_KEY_1FICHIER_PATH', '')} or {getattr(config, 'API_KEY_ALLDEBRID_PATH', '')} or {getattr(config, 'API_KEY_REALDEBRID_PATH', '')}"
return f"{getattr(config, 'API_KEY_1FICHIER_PATH', '')} or {getattr(config, 'API_KEY_ALLDEBRID_PATH', '')} or {getattr(config, 'API_KEY_DEBRIDLINK_PATH', '')} or {getattr(config, 'API_KEY_REALDEBRID_PATH', '')}"
def ensure_download_provider_keys(force: bool = False): # pragma: no cover
"""S'assure que les clés 1fichier/AllDebrid/RealDebrid sont chargées et retourne le dict.
"""S'assure que les clés 1fichier/AllDebrid/Debrid-Link/RealDebrid sont chargées et retourne le dict.
Utilise load_api_keys (cache mtime). force=True invalide le cache.
"""
return load_api_keys(force)
def missing_all_provider_keys(): # pragma: no cover
"""True si aucune des trois clés n'est définie."""
"""True si aucune des clés premium n'est définie."""
keys = load_api_keys(False)
return not keys.get('1fichier') and not keys.get('alldebrid') and not keys.get('realdebrid')
return not keys.get('1fichier') and not keys.get('alldebrid') and not keys.get('debridlink') and not keys.get('realdebrid')
def provider_keys_status(): # pragma: no cover
"""Retourne un dict de présence pour debug/log."""
@@ -2958,6 +3011,7 @@ def provider_keys_status(): # pragma: no cover
return {
'1fichier': bool(keys.get('1fichier')),
'alldebrid': bool(keys.get('alldebrid')),
'debridlink': bool(keys.get('debridlink')),
'realdebrid': bool(keys.get('realdebrid')),
}
@@ -3008,26 +3062,150 @@ def normalize_platform_name(platform):
return platform.lower().replace(" ", "")
def find_matching_files(base_path, filename):
"""Return all matching files for a requested download name within a ROM folder."""
if not base_path or not os.path.exists(base_path):
return []
candidate_name = Path(str(filename or "")).name
requested_stem, requested_ext = os.path.splitext(candidate_name)
requested_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', requested_stem)).strip().lower()
archive_exts = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
matches = []
seen_paths = set()
full_path = os.path.join(base_path, candidate_name)
if os.path.exists(full_path) and os.path.isfile(full_path):
seen_paths.add(os.path.normcase(full_path))
matches.append((1000, candidate_name, full_path))
for existing_file in os.listdir(base_path):
existing_path = os.path.join(base_path, existing_file)
if not os.path.isfile(existing_path):
continue
normalized_path = os.path.normcase(existing_path)
if normalized_path in seen_paths:
continue
existing_stem, existing_ext = os.path.splitext(existing_file)
score = None
if requested_stem and existing_stem == requested_stem:
score = 900
else:
existing_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', existing_stem)).strip().lower()
if requested_normalized and existing_normalized and existing_normalized == requested_normalized:
score = 0
if requested_ext and existing_ext.lower() == requested_ext.lower():
score += 4
if existing_ext.lower() not in archive_exts:
score += 3
score -= abs(len(existing_stem) - len(requested_stem))
if score is not None:
seen_paths.add(normalized_path)
matches.append((score, existing_file, existing_path))
matches.sort(key=lambda item: item[0], reverse=True)
return [(actual_filename, actual_path) for _, actual_filename, actual_path in matches]
def get_existing_history_matches(entry):
"""Return persisted moved paths that still exist for a history entry."""
if not isinstance(entry, dict):
return []
moved_paths = entry.get("moved_paths", []) or []
matches = []
seen_paths = set()
for raw_path in moved_paths:
if not raw_path:
continue
actual_path = os.path.abspath(str(raw_path))
normalized_path = os.path.normcase(actual_path)
if normalized_path in seen_paths or not os.path.isfile(actual_path):
continue
seen_paths.add(normalized_path)
matches.append((os.path.basename(actual_path), actual_path))
return matches
def move_files_to_directory(file_paths, destination_dir):
"""Move files to a destination directory, avoiding name collisions."""
if not destination_dir:
return False, [], "Destination directory is empty"
if not any(file_paths or []):
return False, [], "No files to move"
try:
os.makedirs(destination_dir, exist_ok=True)
except Exception as e:
logger.error(f"Impossible de créer le dossier de destination {destination_dir}: {e}")
return False, [], str(e)
moved_matches = []
seen_sources = set()
reserved_targets = set()
for raw_source in file_paths:
if not raw_source:
continue
source_path = os.path.abspath(str(raw_source))
normalized_source = os.path.normcase(source_path)
if normalized_source in seen_sources:
continue
seen_sources.add(normalized_source)
if not os.path.isfile(source_path):
error_message = f"File not found: {source_path}"
logger.warning(error_message)
return False, moved_matches, error_message
source_name = os.path.basename(source_path)
target_path = os.path.join(destination_dir, source_name)
target_root, target_ext = os.path.splitext(target_path)
suffix = 1
while os.path.normcase(target_path) in reserved_targets or (
os.path.exists(target_path)
and os.path.normcase(target_path) != os.path.normcase(source_path)
):
target_path = f"{target_root} ({suffix}){target_ext}"
suffix += 1
reserved_targets.add(os.path.normcase(target_path))
try:
if os.path.normcase(source_path) != os.path.normcase(target_path):
shutil.move(source_path, target_path)
logger.info(f"Fichier déplacé: {source_path} -> {target_path}")
else:
logger.debug(f"Déplacement ignoré, même chemin source/destination: {source_path}")
moved_matches.append((os.path.basename(target_path), target_path))
except Exception as e:
logger.error(f"Erreur lors du déplacement de {source_path} vers {target_path}: {e}")
return False, moved_matches, str(e)
return True, moved_matches, None
def find_file_with_or_without_extension(base_path, filename):
"""
Cherche un fichier, avec son extension ou sans (cherche jeuxxx.* si jeuxxx.zip n'existe pas).
Retourne (file_exists, actual_filename, actual_path).
"""
# 1. Tester d'abord le fichier tel quel
full_path = os.path.join(base_path, filename)
if os.path.exists(full_path):
return True, filename, full_path
# 2. Si pas trouvé et que le fichier a une extension, chercher sans extension
name_without_ext, ext = os.path.splitext(filename)
if ext: # Si le fichier a une extension
# Chercher tous les fichiers commençant par le nom sans extension
if os.path.exists(base_path):
for existing_file in os.listdir(base_path):
existing_name, _ = os.path.splitext(existing_file)
if existing_name == name_without_ext:
found_path = os.path.join(base_path, existing_file)
return True, existing_file, found_path
# 3. Fichier non trouvé
return False, filename, full_path
candidate_name = Path(str(filename or "")).name
full_path = os.path.join(base_path, candidate_name)
matches = find_matching_files(base_path, candidate_name)
if matches:
actual_filename, actual_path = matches[0]
return True, actual_filename, actual_path
return False, candidate_name, full_path

View File

@@ -1,3 +1,3 @@
{
"version": "2.5.0.6"
"version": "2.6.1.1"
}