Compare commits

...

8 Commits

Author SHA1 Message Date
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
skymike03
1dbc741617 v2.5.0.6 (2026.03.15)
update download status handling for converting state and ps3 dec function
2026-03-15 23:47:32 +01:00
21 changed files with 2043 additions and 283 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.5"
app_version = "2.6.1.0"
# 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

@@ -20,7 +20,7 @@ from utils import (
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
start_connection_status_check
)
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 +72,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 +110,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 +130,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 +277,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 +418,222 @@ 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 game_name
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,
'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 +798,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 +819,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 +1126,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")
@@ -1809,11 +2217,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 +2295,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 +2307,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 +2329,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 +2339,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 +2441,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 +2454,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 +2487,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 +2497,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 +2506,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

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...",

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,9 @@
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",
"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",

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...",

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,9 @@
"history_option_delete_game": "Supprimer le jeu",
"history_option_error_info": "Détails de l'erreur",
"history_option_retry": "Retenter le téléchargement",
"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é",

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)",

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",

View File

@@ -1544,7 +1544,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
@@ -1616,7 +1616,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.debug(f"[DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
@@ -1659,17 +1659,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 +1719,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:
@@ -2074,6 +2078,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 +2390,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
@@ -2600,7 +2690,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
success, message = data[1], data[2]
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message
@@ -2655,7 +2745,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
logger.debug(f"[1F_DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
if isinstance(config.history, list):
for entry in config.history:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
entry["status"] = "Download_OK" if success else "Erreur"
entry["progress"] = 100 if success else 0
entry["message"] = message

View File

@@ -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', '')
}

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,8 @@ import tempfile
logger = logging.getLogger(__name__)
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
_games_cache = {}
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
@@ -1195,9 +1197,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)
@@ -1242,8 +1250,15 @@ def load_games(platform_id:str) -> list[Game]:
display_name = Path(name).stem
display_name = display_name.replace(platform_id, "")
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
_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 []
@@ -1827,7 +1842,7 @@ def extract_rar(rar_path, dest_dir, url):
os.chmod(os.path.join(root, dir_name), 0o755)
# Gestion plateformes spéciales (uniquement PS3 pour RAR)
success, error_msg = _handle_special_platforms(dest_dir, rar_path, before_dirs)
success, error_msg = _handle_special_platforms(dest_dir, rar_path, before_dirs, url=url)
if not success:
return False, error_msg
@@ -1961,18 +1976,31 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
# MODE PS3 : Décryptage et extraction
# ============================================
logger.info(f"Mode PS3 détecté pour: {archive_name}")
# L'extraction de l'archive est terminée; basculer l'UI en mode conversion/décryptage.
try:
if url:
if url not in getattr(config, 'download_progress', {}):
config.download_progress[url] = {}
config.download_progress[url]["status"] = "Converting"
config.download_progress[url]["progress_percent"] = 0
config.needs_redraw = True
if isinstance(config.history, list):
for entry in config.history:
if entry.get("url") == url and entry.get("status") in ("Extracting", "Téléchargement", "Downloading"):
entry["status"] = "Converting"
entry["progress"] = 0
entry["message"] = "PS3 conversion in progress"
save_history(config.history)
break
except Exception as e:
logger.debug(f"MAJ statut conversion PS3 ignorée: {e}")
try:
# Construire l'URL de la clé en remplaçant le dossier
if url and ("Sony%20-%20PlayStation%203/" in url or "Sony - PlayStation 3/" in url):
key_url = url.replace("Sony%20-%20PlayStation%203/", "Sony%20-%20PlayStation%203%20-%20Disc%20Keys%20TXT/")
key_url = key_url.replace("Sony - PlayStation 3/", "Sony - PlayStation 3 - Disc Keys TXT/")
else:
logger.warning("URL PS3 invalide ou manquante, tentative sans clé distante")
key_url = None
ps3_keys_base_url = "https://retrogamesets.fr/softs/ps3/"
logger.debug(f"URL jeu: {url}")
logger.debug(f"URL clé: {key_url}")
logger.debug(f"Base URL des clés PS3: {ps3_keys_base_url}")
# Chercher le fichier .iso déjà extrait
iso_files = [f for f in os.listdir(dest_dir) if f.endswith('.iso') and not f.endswith('_decrypted.iso')]
@@ -1983,41 +2011,51 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
iso_path = os.path.join(dest_dir, iso_file)
logger.info(f"Fichier ISO trouvé: {iso_path}")
# Étape 1: Télécharger et extraire la clé si URL disponible
# Étape 1: Télécharger directement la clé .dkey depuis la nouvelle source
dkey_path = None
if key_url:
logger.info("Téléchargement de la clé de décryption...")
key_zip_name = os.path.basename(archive_name) if archive_name else "key.zip"
key_zip_path = os.path.join(dest_dir, f"_temp_key_{key_zip_name}")
logger.info("Téléchargement de la clé de décryption (.dkey)...")
candidate_bases = []
def _add_candidate_base(base_name):
if not base_name:
return
cleaned = str(base_name).strip()
if not cleaned:
return
if cleaned.lower().endswith('.dkey'):
cleaned = cleaned[:-5]
if cleaned not in candidate_bases:
candidate_bases.append(cleaned)
if archive_name:
_add_candidate_base(os.path.splitext(os.path.basename(archive_name))[0])
if extracted_basename:
_add_candidate_base(extracted_basename)
_add_candidate_base(os.path.splitext(os.path.basename(iso_file))[0])
for base_name in candidate_bases:
remote_name = f"{base_name}.dkey"
encoded_name = remote_name.replace(" ", "%20")
key_url = f"{ps3_keys_base_url}{encoded_name}"
logger.debug(f"Tentative clé distante: {key_url}")
try:
response = requests.get(key_url, stream=True, timeout=30)
response.raise_for_status()
with open(key_zip_path, 'wb') as f:
if response.status_code != 200:
logger.debug(f"Clé distante introuvable ({response.status_code}): {remote_name}")
continue
local_dkey_path = os.path.join(dest_dir, remote_name)
with open(local_dkey_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
logger.info(f"Clé téléchargée: {key_zip_path}")
# Extraire la clé
logger.info("Extraction de la clé...")
with zipfile.ZipFile(key_zip_path, 'r') as zf:
dkey_files = [f for f in zf.namelist() if f.endswith('.dkey')]
if not dkey_files:
logger.warning("Aucun fichier .dkey trouvé dans l'archive de clé")
else:
dkey_file = dkey_files[0]
zf.extract(dkey_file, dest_dir)
dkey_path = os.path.join(dest_dir, dkey_file)
logger.info(f"Clé extraite: {dkey_path}")
# Supprimer le ZIP de la clé
os.remove(key_zip_path)
dkey_path = local_dkey_path
logger.info(f"Clé téléchargée: {dkey_path}")
break
except Exception as e:
logger.error(f"Erreur lors du téléchargement/extraction de la clé: {e}")
logger.warning(f"Échec téléchargement clé {remote_name}: {e}")
# Chercher une clé .dkey si pas téléchargée
if not dkey_path:
@@ -2715,24 +2753,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
@@ -2766,6 +2805,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
@@ -2773,6 +2814,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
}
@@ -2781,6 +2823,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
}
@@ -2838,10 +2881,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
"""
@@ -2851,6 +2894,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', ''),
}
@@ -2878,6 +2922,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()
@@ -2903,6 +2949,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', '')
@@ -2915,19 +2964,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."""
@@ -2935,6 +2984,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')),
}

View File

@@ -1,3 +1,3 @@
{
"version": "2.5.0.5"
"version": "2.6.1.0"
}