mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 20:35:26 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb86d69895 | ||
|
|
b09b3da371 | ||
|
|
0915a90fbe | ||
|
|
3ae3c151eb | ||
|
|
7460b12d71 | ||
|
|
2f437c1aa4 | ||
|
|
054b174c18 | ||
|
|
fbb1a2aa68 | ||
|
|
1dbc741617 | ||
|
|
c4913a5fc2 | ||
|
|
bf9d3d2de5 | ||
|
|
9979949bdc | ||
|
|
9ed264544f | ||
|
|
779c060927 | ||
|
|
88400e538f | ||
|
|
cbab067dd6 | ||
|
|
b4ed0b355d | ||
|
|
51ad08ff33 | ||
|
|
d6a5c4b27e | ||
|
|
2c7c3414a5 | ||
|
|
059d3988ac |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -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"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ pygame/
|
||||
data/
|
||||
docker-compose.test.yml
|
||||
config/
|
||||
pyrightconfig.json
|
||||
|
||||
@@ -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
|
||||
@@ -526,7 +527,7 @@ async def main():
|
||||
# Appui long détecté, ouvrir le scraper
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
if games:
|
||||
game_name = games[config.current_game][0]
|
||||
game_name = games[config.current_game].name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
|
||||
config.previous_menu_state = "game"
|
||||
@@ -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")
|
||||
@@ -709,6 +733,7 @@ async def main():
|
||||
"pause_games_menu",
|
||||
"pause_settings_menu",
|
||||
"pause_api_keys_status",
|
||||
"pause_connection_status",
|
||||
"filter_platforms",
|
||||
"display_menu",
|
||||
"language_select",
|
||||
@@ -728,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)
|
||||
@@ -834,12 +860,9 @@ async def main():
|
||||
logger.debug("Action quit détectée, arrêt de l'application")
|
||||
elif action == "download" and config.menu_state == "game" and config.filtered_games:
|
||||
game = config.filtered_games[config.current_game]
|
||||
if isinstance(game, (list, tuple)):
|
||||
game_name = game[0]
|
||||
url = game[1] if len(game) > 1 else None
|
||||
else: # fallback str
|
||||
game_name = str(game)
|
||||
url = None
|
||||
game_name = game.name
|
||||
url = game.url
|
||||
|
||||
# Nouveau schéma: config.platforms contient déjà platform_name (string)
|
||||
platform_name = config.platforms[config.current_platform]
|
||||
if url:
|
||||
@@ -851,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
|
||||
@@ -1118,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é
|
||||
@@ -1149,6 +1181,9 @@ async def main():
|
||||
elif config.menu_state == "pause_api_keys_status":
|
||||
from display import draw_pause_api_keys_status
|
||||
draw_pause_api_keys_status(screen)
|
||||
elif config.menu_state == "pause_connection_status":
|
||||
from display import draw_pause_connection_status
|
||||
draw_pause_connection_status(screen)
|
||||
elif config.menu_state == "filter_platforms":
|
||||
from display import draw_filter_platforms_menu
|
||||
draw_filter_platforms_menu(screen)
|
||||
|
||||
1
ports/RGSX/assets/ArchiveOrgCookie.txt
Normal file
1
ports/RGSX/assets/ArchiveOrgCookie.txt
Normal file
@@ -0,0 +1 @@
|
||||
donation-identifier=39546f3b2d3f67a664818596d81a5bec; abtest-identifier=fee0e28eb6c8d0de147d19db4303ee84; logged-in-sig=1802098179%201770562179%20AKHN8aF4EsFeR%2FundhgQTu0j27ZdFZXmgyUiqnJvXq%2BwtDGVvapqhKUFhIlI9bXAMYLMHDRJoO76bsqXI662nrIsx58efihNrafdk285r8MAdotWx03usO30baYoNPoMMEaK8iuhtbfTEyfE7oTZwdO7wjxNUTm%2Bbjjm6kmUD3HSQRzPsc0oWrrnd8Wj2x3UiuZeRnBfC60OjJHcnKC2Xv7teS%2BBx3EdKAG1i739MxTzjtEfERWw83bnaV30827qaFhZ%2BDK3%2FwCGOUwtablPA%2B0EeLR9%2BoYeC6x5aaJMZHBMjBowSIEE4QAK9IG9haBsn7%2F1PCweYuLivMIZJeA7mA%3D%3D; logged-in-user=rgsx%40outlook.fr
|
||||
@@ -7,7 +7,7 @@
|
||||
"cancel": {
|
||||
"type": "key",
|
||||
"key": 27,
|
||||
"display": "\u00c9chap"
|
||||
"display": "Esc/Echap"
|
||||
},
|
||||
"up": {
|
||||
"type": "key",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/arcade-beat.mp3
Normal file
BIN
ports/RGSX/assets/music/arcade-beat.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/arthurhale.mp3
Normal file
BIN
ports/RGSX/assets/music/arthurhale.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/battle-time.mp3
Normal file
BIN
ports/RGSX/assets/music/battle-time.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/dream-in-keygen.mp3
Normal file
BIN
ports/RGSX/assets/music/dream-in-keygen.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/happy-videogame.mp3
Normal file
BIN
ports/RGSX/assets/music/happy-videogame.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/man-is-he-mega.mp3
Normal file
BIN
ports/RGSX/assets/music/man-is-he-mega.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/moodmode-8-bit.mp3
Normal file
BIN
ports/RGSX/assets/music/moodmode-8-bit.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/niknet_art.mp3
Normal file
BIN
ports/RGSX/assets/music/niknet_art.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/pixel-dreams.mp3
Normal file
BIN
ports/RGSX/assets/music/pixel-dreams.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/pixelated-dreams.mp3
Normal file
BIN
ports/RGSX/assets/music/pixelated-dreams.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/reflextunes-game-zone.mp3
Normal file
BIN
ports/RGSX/assets/music/reflextunes-game-zone.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/video-game-short.mp3
Normal file
BIN
ports/RGSX/assets/music/video-game-short.mp3
Normal file
Binary file not shown.
69
ports/RGSX/assets/progs/versionclean
Normal file
69
ports/RGSX/assets/progs/versionclean
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# BATOCERA SERVICE
|
||||
# name: Version Clean Service
|
||||
# description: Clean batocera-version output (hide extra services)
|
||||
# author: batocera-unofficial-addons
|
||||
# depends:
|
||||
# version: 1.0
|
||||
|
||||
SERVICE_NAME="versionclean"
|
||||
TARGET="/usr/bin/batocera-version"
|
||||
BACKUP="${TARGET}.bak"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
# If we've already backed up, assume it's already "cleaned"
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: already started (backup exists at ${BACKUP})."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${SERVICE_NAME}: backing up original ${TARGET} to ${BACKUP}..."
|
||||
cp "$TARGET" "$BACKUP"
|
||||
|
||||
echo "${SERVICE_NAME}: writing clean version script to ${TARGET}..."
|
||||
cat << 'EOF' > "$TARGET"
|
||||
#!/bin/bash
|
||||
|
||||
# Clean batocera-version
|
||||
# - "batocera-version --extra" -> "none"
|
||||
# - "batocera-version" -> contents of /usr/share/batocera/batocera.version
|
||||
|
||||
if test "$1" = "--extra"; then
|
||||
echo "none"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat /usr/share/batocera/batocera.version
|
||||
EOF
|
||||
|
||||
chmod +x "$TARGET"
|
||||
echo "${SERVICE_NAME}: clean version applied."
|
||||
;;
|
||||
|
||||
stop)
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: restoring original ${TARGET} from ${BACKUP}..."
|
||||
cp "$BACKUP" "$TARGET"
|
||||
rm "$BACKUP"
|
||||
echo "${SERVICE_NAME}: restore complete."
|
||||
else
|
||||
echo "${SERVICE_NAME}: no backup found, nothing to restore."
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: CLEAN VERSION ACTIVE (backup present)."
|
||||
else
|
||||
echo "${SERVICE_NAME}: ORIGINAL VERSION ACTIVE."
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -2,6 +2,19 @@
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
import socket
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Game:
|
||||
name: str
|
||||
url: str
|
||||
size: str
|
||||
display_name: str # name withou file extension or platform prefix
|
||||
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"
|
||||
@@ -14,10 +27,10 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.5.0.1"
|
||||
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():
|
||||
@@ -141,6 +154,12 @@ pending_download_is_queue = False # Indique si pending_download doit être ajou
|
||||
# Indique si un téléchargement est en cours
|
||||
download_active = False
|
||||
|
||||
# Cache status de connexion (menu pause > settings)
|
||||
connection_status = {}
|
||||
connection_status_timestamp = 0.0
|
||||
connection_status_in_progress = False
|
||||
connection_status_progress = {"done": 0, "total": 0}
|
||||
|
||||
# Log directory
|
||||
# Docker mode: /config/logs (persisted in config volume)
|
||||
# Traditional mode: /app/RGSX/logs (current behavior)
|
||||
@@ -179,7 +198,9 @@ 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")
|
||||
|
||||
|
||||
|
||||
@@ -230,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)
|
||||
@@ -285,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
|
||||
|
||||
@@ -347,6 +393,7 @@ platforms = [] # Liste des plateformes disponibles
|
||||
current_platform = 0 # Index de la plateforme actuelle sélectionnée
|
||||
platform_names = {} # {platform_id: platform_name}
|
||||
games_count = {} # Dictionnaire comptant le nombre de jeux par plateforme
|
||||
games_count_log_verbose = False # Log détaillé par fichier (sinon résumé compact)
|
||||
platform_dicts = [] # Liste des dictionnaires de plateformes
|
||||
|
||||
# Filtre plateformes
|
||||
@@ -356,7 +403,8 @@ filter_platforms_dirty = False # indique si modifications non sauvegardées
|
||||
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
|
||||
|
||||
# Affichage des jeux et sélection
|
||||
games = [] # Liste des jeux pour la plateforme actuelle
|
||||
games: list[Game] = [] # Liste des jeux pour la plateforme actuelle
|
||||
fbneo_games = {}
|
||||
current_game = 0 # Index du jeu actuellement sélectionné
|
||||
menu_state = "loading" # État actuel de l'interface menu
|
||||
scroll_offset = 0 # Offset de défilement pour la liste des jeux
|
||||
@@ -381,10 +429,16 @@ selected_language_index = 0 # Index de la langue sélectionnée dans la liste
|
||||
|
||||
|
||||
# Recherche et filtres
|
||||
filtered_games = [] # Liste des jeux filtrés par recherche ou filtre
|
||||
filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filtre
|
||||
search_mode = False # Indicateur si le mode recherche est actif
|
||||
search_query = "" # Chaîne de recherche saisie par l'utilisateur
|
||||
filter_active = False # Indicateur si un filtre est appliqué
|
||||
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é)
|
||||
@@ -430,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
|
||||
|
||||
|
||||
@@ -8,18 +8,20 @@ import datetime
|
||||
import threading
|
||||
import logging
|
||||
import config
|
||||
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH
|
||||
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH, Game
|
||||
from display import draw_validation_transition, show_toast
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel
|
||||
from utils import (
|
||||
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
|
||||
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
|
||||
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,
|
||||
@@ -30,6 +32,8 @@ from rgsx_settings import (
|
||||
from accessibility import save_accessibility_settings
|
||||
from scraper import get_game_metadata, download_image_to_surface
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Extensions d'archives pour lesquelles on ignore l'avertissement d'extension non supportée
|
||||
@@ -54,6 +58,7 @@ VALID_STATES = [
|
||||
"pause_games_menu", # sous-menu Games (source mode, update/redownload cache)
|
||||
"pause_settings_menu", # sous-menu Settings (music on/off, symlink toggle, api keys status)
|
||||
"pause_api_keys_status", # sous-menu API Keys (affichage statut des clés)
|
||||
"pause_connection_status", # sous-menu Connection status (statut accès sites)
|
||||
# Nouveaux menus historique
|
||||
"history_game_options", # menu options pour un jeu de l'historique
|
||||
"history_show_folder", # afficher le dossier de téléchargement
|
||||
@@ -68,12 +73,15 @@ 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
|
||||
]
|
||||
|
||||
def validate_menu_state(state):
|
||||
if not state:
|
||||
return "platform"
|
||||
if state not in VALID_STATES:
|
||||
logger.debug(f"État invalide {state}, retour à platform")
|
||||
return "platform"
|
||||
@@ -103,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
|
||||
@@ -111,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:
|
||||
@@ -254,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.
|
||||
@@ -322,6 +407,237 @@ def _launch_next_queued_download():
|
||||
if config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
|
||||
def filter_games_by_search_query() -> list[Game]:
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
|
||||
filtered_games = []
|
||||
for game in base_games:
|
||||
game_name = game.display_name
|
||||
if config.search_query.lower() in game_name.lower():
|
||||
filtered_games.append(game)
|
||||
|
||||
return filtered_games
|
||||
|
||||
|
||||
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):
|
||||
"""Gère un événement clavier/joystick/souris et la répétition automatique.
|
||||
Retourne 'quit', 'download', 'redownload', ou None."""
|
||||
@@ -484,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
|
||||
@@ -503,9 +821,120 @@ 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 = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
games: list[Game] = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
if config.search_mode and getattr(config, 'joystick', False):
|
||||
keyboard_layout = [
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
@@ -559,10 +988,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif is_input_matched(event, "confirm"):
|
||||
config.search_query += keyboard_layout[row][col]
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -571,10 +997,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.search_query:
|
||||
config.search_query = config.search_query[:-1]
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -582,10 +1005,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif is_input_matched(event, "space"):
|
||||
config.search_query += " "
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -638,10 +1058,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if event.unicode.isalnum() or event.unicode == ' ':
|
||||
config.search_query += event.unicode
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -651,10 +1068,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.search_query:
|
||||
config.search_query = config.search_query[:-1]
|
||||
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
|
||||
base_games = config.games
|
||||
if config.game_filter_obj and config.game_filter_obj.is_active():
|
||||
base_games = config.game_filter_obj.apply_filters(config.games)
|
||||
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
|
||||
config.filtered_games = filter_games_by_search_query()
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
@@ -714,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")
|
||||
@@ -722,8 +1137,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if games:
|
||||
idx = config.current_game
|
||||
game = games[idx]
|
||||
url = game[1]
|
||||
game_name = game[0]
|
||||
url = game.url
|
||||
game_name = game.name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
|
||||
pending_download = check_extension_before_download(url, platform, game_name)
|
||||
@@ -763,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,
|
||||
@@ -835,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,
|
||||
@@ -1075,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
|
||||
@@ -1334,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
|
||||
|
||||
@@ -1804,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
|
||||
@@ -1866,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()
|
||||
@@ -1878,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()
|
||||
@@ -1889,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)
|
||||
@@ -1899,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
|
||||
@@ -2001,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
|
||||
@@ -2014,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')
|
||||
@@ -2036,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)
|
||||
@@ -2046,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)
|
||||
@@ -2055,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
|
||||
@@ -2074,21 +2576,23 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif config.menu_state == "pause_settings_menu":
|
||||
sel = getattr(config, 'pause_settings_selection', 0)
|
||||
# Calculer le nombre total d'options selon le système
|
||||
# Liste des options : music, symlink, auto_extract, roms_folder, [web_service], [custom_dns], api keys, back
|
||||
total = 6 # music, symlink, auto_extract, roms_folder, api keys, back (Windows)
|
||||
# Liste des options : music, symlink, auto_extract, roms_folder, [web_service], [custom_dns], api keys, connection_status, back
|
||||
total = 7 # music, symlink, auto_extract, roms_folder, api keys, connection_status, back (Windows)
|
||||
auto_extract_index = 2
|
||||
roms_folder_index = 3
|
||||
web_service_index = -1
|
||||
custom_dns_index = -1
|
||||
api_keys_index = 4
|
||||
back_index = 5
|
||||
connection_status_index = 5
|
||||
back_index = 6
|
||||
|
||||
if config.OPERATING_SYSTEM == "Linux":
|
||||
total = 8 # music, symlink, auto_extract, roms_folder, web_service, custom_dns, api keys, back
|
||||
total = 9 # music, symlink, auto_extract, roms_folder, web_service, custom_dns, api keys, connection_status, back
|
||||
web_service_index = 4
|
||||
custom_dns_index = 5
|
||||
api_keys_index = 6
|
||||
back_index = 7
|
||||
connection_status_index = 7
|
||||
back_index = 8
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_settings_selection = (sel - 1) % total
|
||||
@@ -2208,6 +2712,11 @@ def handle_controls(event, sources, joystick, screen):
|
||||
elif sel == api_keys_index and is_input_matched(event, "confirm"):
|
||||
config.menu_state = "pause_api_keys_status"
|
||||
config.needs_redraw = True
|
||||
# Option Connection Status
|
||||
elif sel == connection_status_index and is_input_matched(event, "confirm"):
|
||||
start_connection_status_check(force=True)
|
||||
config.menu_state = "pause_connection_status"
|
||||
config.needs_redraw = True
|
||||
# Option Back (dernière option)
|
||||
elif sel == back_index and is_input_matched(event, "confirm"):
|
||||
config.menu_state = "pause_menu"
|
||||
@@ -2224,6 +2733,12 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
|
||||
elif config.menu_state == "pause_connection_status":
|
||||
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm") or is_input_matched(event, "start"):
|
||||
config.menu_state = "pause_settings_menu"
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
|
||||
# Aide contrôles
|
||||
elif config.menu_state == "controls_help":
|
||||
if is_input_matched(event, "cancel"):
|
||||
@@ -2537,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
|
||||
@@ -2552,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
|
||||
@@ -3167,8 +3712,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Déclencher le téléchargement normal
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
if games:
|
||||
url = games[config.current_game][1]
|
||||
game_name = games[config.current_game][0]
|
||||
url = games[config.current_game].url
|
||||
game_name = games[config.current_game].name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
|
||||
|
||||
@@ -3299,8 +3844,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Déclencher le téléchargement normal (même code que pour KEYUP)
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
if games:
|
||||
url = games[config.current_game][1]
|
||||
game_name = games[config.current_game][0]
|
||||
url = games[config.current_game].url
|
||||
game_name = games[config.current_game].name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
|
||||
|
||||
|
||||
@@ -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
@@ -9,6 +9,8 @@ import re
|
||||
import logging
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
from config import Game
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -156,9 +158,7 @@ class GameFilters:
|
||||
r'\([^\)]*PRERELEASE[^\)]*\)',
|
||||
r'\([^\)]*UNFINISHED[^\)]*\)',
|
||||
r'\([^\)]*WIP[^\)]*\)',
|
||||
r'\[[^\]]*BETA[^\]]*\]',
|
||||
r'\[[^\]]*DEMO[^\]]*\]',
|
||||
r'\[[^\]]*TEST[^\]]*\]'
|
||||
r'\([^\)]*BOOTLEG[^\)]*\)',
|
||||
]
|
||||
return any(re.search(pattern, name) for pattern in non_release_patterns)
|
||||
|
||||
@@ -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
|
||||
@@ -211,7 +231,7 @@ class GameFilters:
|
||||
|
||||
return best_priority
|
||||
|
||||
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
|
||||
def apply_filters(self, games: list[Game]) -> list[Game]:
|
||||
"""
|
||||
Applique les filtres à une liste de jeux
|
||||
games: Liste de tuples (game_name, game_url, size)
|
||||
@@ -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[0]
|
||||
|
||||
# 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)
|
||||
@@ -255,13 +274,12 @@ class GameFilters:
|
||||
|
||||
return filtered_games
|
||||
|
||||
def _apply_one_rom_per_game(self, games: List[Tuple]) -> List[Tuple]:
|
||||
def _apply_one_rom_per_game(self, games: List[Game]) -> List[Game]:
|
||||
"""Garde seulement une ROM par jeu selon la priorité de région"""
|
||||
games_by_base = {}
|
||||
|
||||
for game in games:
|
||||
game_name = game[0]
|
||||
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[0]))
|
||||
key=self.get_region_priority)
|
||||
result.append(sorted_games[0])
|
||||
|
||||
return result
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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}",
|
||||
@@ -189,7 +196,14 @@
|
||||
"status_present": "Vorhanden",
|
||||
"status_missing": "Fehlt",
|
||||
"menu_api_keys_status": "API-Schlüssel",
|
||||
"menu_connection_status": "Verbindungsstatus",
|
||||
"api_keys_status_title": "Status der API-Schlüssel",
|
||||
"connection_status_title": "Verbindungsstatus",
|
||||
"connection_status_category_updates": "App-/Gamelist-Update",
|
||||
"connection_status_category_sources": "Spielquellen",
|
||||
"connection_status_checking": "Prüfe...",
|
||||
"connection_status_progress": "Prüfe... {done}/{total}",
|
||||
"connection_status_last_check": "Letzte Prüfung: {time}",
|
||||
"menu_games": "Spiele",
|
||||
"api_keys_hint_manage": "Legen Sie Ihre Schlüssel in {path}",
|
||||
"api_key_empty_suffix": "leer",
|
||||
@@ -225,11 +239,13 @@
|
||||
"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",
|
||||
"instruction_settings_roms_folder": "Standard-Download-Verzeichnis für ROMs ändern",
|
||||
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
|
||||
"instruction_settings_connection_status": "Zugriff auf Update- und Quellen-Seiten prüfen",
|
||||
"instruction_settings_web_service": "Web-Dienst Autostart beim Booten aktivieren/deaktivieren",
|
||||
"instruction_settings_custom_dns": "Custom DNS (Cloudflare 1.1.1.1) beim Booten aktivieren/deaktivieren",
|
||||
"settings_auto_extract": "Auto-Extraktion Archive",
|
||||
@@ -250,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...",
|
||||
@@ -288,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",
|
||||
@@ -297,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",
|
||||
@@ -461,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",
|
||||
|
||||
@@ -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",
|
||||
@@ -188,7 +195,14 @@
|
||||
"status_present": "Present",
|
||||
"status_missing": "Missing",
|
||||
"menu_api_keys_status": "API Keys",
|
||||
"menu_connection_status": "Connection status",
|
||||
"api_keys_status_title": "API Keys Status",
|
||||
"connection_status_title": "Connection status",
|
||||
"connection_status_category_updates": "App/Gamelist update",
|
||||
"connection_status_category_sources": "Game sources",
|
||||
"connection_status_checking": "Checking...",
|
||||
"connection_status_progress": "Checking... {done}/{total}",
|
||||
"connection_status_last_check": "Last check: {time}",
|
||||
"menu_games": "Games",
|
||||
"api_keys_hint_manage": "Put your keys in {path}",
|
||||
"api_key_empty_suffix": "empty",
|
||||
@@ -227,11 +241,13 @@
|
||||
"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",
|
||||
"instruction_settings_roms_folder": "Change the default ROMs download directory",
|
||||
"instruction_settings_api_keys": "See detected premium provider API keys",
|
||||
"instruction_settings_connection_status": "Check access to update and source sites",
|
||||
"instruction_settings_web_service": "Enable/disable web service auto-start at boot",
|
||||
"instruction_settings_custom_dns": "Enable/disable custom DNS (Cloudflare 1.1.1.1) at boot",
|
||||
"settings_auto_extract": "Auto Extract Archives",
|
||||
@@ -290,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",
|
||||
@@ -299,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",
|
||||
@@ -461,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",
|
||||
|
||||
@@ -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}",
|
||||
@@ -189,7 +196,14 @@
|
||||
"status_present": "Presente",
|
||||
"status_missing": "Ausente",
|
||||
"menu_api_keys_status": "Claves API",
|
||||
"menu_connection_status": "Estado de conexión",
|
||||
"api_keys_status_title": "Estado de las claves API",
|
||||
"connection_status_title": "Estado de conexión",
|
||||
"connection_status_category_updates": "Actualización app/lista de juegos",
|
||||
"connection_status_category_sources": "Fuentes de juegos",
|
||||
"connection_status_checking": "Comprobando...",
|
||||
"connection_status_progress": "Comprobando... {done}/{total}",
|
||||
"connection_status_last_check": "Última comprobación: {time}",
|
||||
"menu_games": "Juegos",
|
||||
"api_keys_hint_manage": "Coloca tus claves en {path}",
|
||||
"api_key_empty_suffix": "vacío",
|
||||
@@ -225,11 +239,13 @@
|
||||
"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",
|
||||
"instruction_settings_roms_folder": "Cambiar el directorio de descarga de ROMs por defecto",
|
||||
"instruction_settings_api_keys": "Ver claves API premium detectadas",
|
||||
"instruction_settings_connection_status": "Comprobar acceso a sitios de actualizaciones y fuentes",
|
||||
"instruction_settings_web_service": "Activar/desactivar inicio automático del servicio web",
|
||||
"instruction_settings_custom_dns": "Activar/desactivar DNS personalizado (Cloudflare 1.1.1.1) al inicio",
|
||||
"settings_auto_extract": "Extracción auto de archivos",
|
||||
@@ -250,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...",
|
||||
@@ -288,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",
|
||||
@@ -297,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",
|
||||
@@ -459,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",
|
||||
|
||||
@@ -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}",
|
||||
@@ -185,7 +192,14 @@
|
||||
"status_present": "Présente",
|
||||
"status_missing": "Absente",
|
||||
"menu_api_keys_status": "Clés API",
|
||||
"menu_connection_status": "État de connexion",
|
||||
"api_keys_status_title": "Statut des clés API",
|
||||
"connection_status_title": "État de connexion",
|
||||
"connection_status_category_updates": "Mise à jour App/Liste de jeux",
|
||||
"connection_status_category_sources": "Sources de jeux",
|
||||
"connection_status_checking": "Vérification...",
|
||||
"connection_status_progress": "Vérification... {done}/{total}",
|
||||
"connection_status_last_check": "Dernière vérif : {time}",
|
||||
"menu_games": "Jeux",
|
||||
"api_keys_hint_manage": "Placez vos clés dans {path}",
|
||||
"api_key_empty_suffix": "vide",
|
||||
@@ -227,11 +241,13 @@
|
||||
"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",
|
||||
"instruction_settings_roms_folder": "Changer le répertoire de téléchargement des ROMs par défaut",
|
||||
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
|
||||
"instruction_settings_connection_status": "Vérifier l'accès aux sites d'update et de sources",
|
||||
"instruction_settings_web_service": "Activer/désactiver le démarrage automatique du service web",
|
||||
"instruction_settings_custom_dns": "Activer/désactiver les DNS personnalisés (Cloudflare 1.1.1.1) au démarrage",
|
||||
"settings_auto_extract": "Extraction auto des archives",
|
||||
@@ -290,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é",
|
||||
@@ -299,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",
|
||||
@@ -461,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",
|
||||
|
||||
@@ -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",
|
||||
@@ -184,7 +191,14 @@
|
||||
"status_present": "Presente",
|
||||
"status_missing": "Assente",
|
||||
"menu_api_keys_status": "Chiavi API",
|
||||
"menu_connection_status": "Stato connessione",
|
||||
"api_keys_status_title": "Stato delle chiavi API",
|
||||
"connection_status_title": "Stato connessione",
|
||||
"connection_status_category_updates": "Aggiornamento app/lista giochi",
|
||||
"connection_status_category_sources": "Sorgenti giochi",
|
||||
"connection_status_checking": "Verifica in corso...",
|
||||
"connection_status_progress": "Verifica in corso... {done}/{total}",
|
||||
"connection_status_last_check": "Ultimo controllo: {time}",
|
||||
"menu_games": "Giochi",
|
||||
"api_keys_hint_manage": "Metti le tue chiavi in {path}",
|
||||
"api_key_empty_suffix": "vuoto",
|
||||
@@ -220,11 +234,13 @@
|
||||
"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",
|
||||
"instruction_settings_roms_folder": "Cambiare la directory di download ROMs predefinita",
|
||||
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
|
||||
"instruction_settings_connection_status": "Verifica accesso ai siti di aggiornamento e sorgenti",
|
||||
"instruction_settings_web_service": "Attivare/disattivare avvio automatico servizio web all'avvio",
|
||||
"instruction_settings_custom_dns": "Attivare/disattivare DNS personalizzato (Cloudflare 1.1.1.1) all'avvio",
|
||||
"settings_auto_extract": "Estrazione auto archivi",
|
||||
@@ -250,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)",
|
||||
@@ -283,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",
|
||||
@@ -292,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",
|
||||
@@ -457,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",
|
||||
|
||||
@@ -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",
|
||||
@@ -190,7 +197,14 @@
|
||||
"status_present": "Presente",
|
||||
"status_missing": "Ausente",
|
||||
"menu_api_keys_status": "Chaves API",
|
||||
"menu_connection_status": "Estado da conexão",
|
||||
"api_keys_status_title": "Status das chaves API",
|
||||
"connection_status_title": "Estado da conexão",
|
||||
"connection_status_category_updates": "Atualização do app/lista de jogos",
|
||||
"connection_status_category_sources": "Fontes de jogos",
|
||||
"connection_status_checking": "Verificando...",
|
||||
"connection_status_progress": "Verificando... {done}/{total}",
|
||||
"connection_status_last_check": "Última verificação: {time}",
|
||||
"menu_games": "Jogos",
|
||||
"api_keys_hint_manage": "Coloque suas chaves em {path}",
|
||||
"api_key_empty_suffix": "vazio",
|
||||
@@ -226,11 +240,13 @@
|
||||
"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",
|
||||
"instruction_settings_roms_folder": "Alterar o diretório de download de ROMs padrão",
|
||||
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
|
||||
"instruction_settings_connection_status": "Verificar acesso a sites de atualização e fontes",
|
||||
"instruction_settings_web_service": "Ativar/desativar início automático do serviço web na inicialização",
|
||||
"instruction_settings_custom_dns": "Ativar/desativar DNS personalizado (Cloudflare 1.1.1.1) na inicialização",
|
||||
"settings_auto_extract": "Extração auto de arquivos",
|
||||
@@ -250,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",
|
||||
@@ -289,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",
|
||||
@@ -298,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",
|
||||
@@ -461,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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import requests
|
||||
import requests # type: ignore
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
@@ -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, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys
|
||||
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
|
||||
@@ -32,11 +32,45 @@ from language import _ # Import de la fonction de traduction
|
||||
import re
|
||||
import html as html_module
|
||||
from urllib.parse import urljoin, unquote
|
||||
import urllib.parse
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _redact_headers(headers: dict) -> dict:
|
||||
"""Return a copy of headers with sensitive fields redacted for logs."""
|
||||
if not isinstance(headers, dict):
|
||||
return {}
|
||||
safe = headers.copy()
|
||||
if 'Cookie' in safe and safe['Cookie']:
|
||||
safe['Cookie'] = '<redacted>'
|
||||
return safe
|
||||
|
||||
|
||||
def _split_archive_org_path(url: str):
|
||||
"""Parse archive.org download URL and return (identifier, archive_name, inner_path)."""
|
||||
try:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
parts = parsed.path.split('/download/', 1)
|
||||
if len(parts) != 2:
|
||||
return None, None, None
|
||||
after = parts[1]
|
||||
identifier = after.split('/', 1)[0]
|
||||
rest = after[len(identifier):]
|
||||
if rest.startswith('/'):
|
||||
rest = rest[1:]
|
||||
rest_decoded = urllib.parse.unquote(rest)
|
||||
if '/' not in rest_decoded:
|
||||
return identifier, None, None
|
||||
first_seg, remainder = rest_decoded.split('/', 1)
|
||||
if first_seg.lower().endswith(('.zip', '.rar', '.7z')):
|
||||
return identifier, first_seg, remainder
|
||||
return identifier, None, None
|
||||
except Exception:
|
||||
return None, None, None
|
||||
|
||||
# --- File d'attente de téléchargements (worker) ---
|
||||
def download_queue_worker():
|
||||
"""Worker qui surveille la file d'attente et lance le prochain téléchargement si aucun n'est actif."""
|
||||
@@ -821,6 +855,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
cancel_events[task_id] = threading.Event()
|
||||
|
||||
def download_thread():
|
||||
nonlocal url
|
||||
try:
|
||||
# IMPORTANT: Créer l'entrée dans config.history dès le début avec status "Downloading"
|
||||
# pour que l'interface web puisse afficher le téléchargement en cours
|
||||
@@ -839,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
|
||||
@@ -848,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,
|
||||
@@ -1059,14 +1096,67 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
download_headers = headers.copy()
|
||||
download_headers['Accept'] = 'application/octet-stream, */*'
|
||||
download_headers['Referer'] = 'https://myrient.erista.me/'
|
||||
archive_cookie = load_archive_org_cookie()
|
||||
archive_alt_urls = []
|
||||
meta_json = None
|
||||
|
||||
# Préparation spécifique archive.org : récupérer quelques pages pour obtenir cookies éventuels
|
||||
# Préparation spécifique archive.org : normaliser URL + récupérer cookies/metadata
|
||||
if 'archive.org/download/' in url:
|
||||
try:
|
||||
pre_id = url.split('/download/')[1].split('/')[0]
|
||||
session.get('https://archive.org/robots.txt', timeout=20)
|
||||
session.get(f'https://archive.org/metadata/{pre_id}', timeout=20)
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
parts = parsed.path.split('/download/', 1)
|
||||
pre_id = None
|
||||
rest_decoded = None
|
||||
if len(parts) == 2:
|
||||
after = parts[1]
|
||||
pre_id = after.split('/', 1)[0]
|
||||
rest = after[len(pre_id):]
|
||||
if rest.startswith('/'):
|
||||
rest = rest[1:]
|
||||
rest_decoded = urllib.parse.unquote(rest)
|
||||
rest_encoded = urllib.parse.quote(rest_decoded, safe='/') if rest_decoded else ''
|
||||
new_path = f"/download/{pre_id}/" + rest_encoded
|
||||
url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, new_path, parsed.query, parsed.fragment))
|
||||
logger.debug(f"URL archive.org normalisée: {url}")
|
||||
if not pre_id:
|
||||
pre_id = url.split('/download/')[1].split('/')[0]
|
||||
|
||||
download_headers['Referer'] = f"https://archive.org/details/{pre_id}"
|
||||
download_headers['Origin'] = 'https://archive.org'
|
||||
if archive_cookie:
|
||||
download_headers['Cookie'] = archive_cookie
|
||||
if archive_cookie:
|
||||
# Apply cookie to session for redirects to ia*.us.archive.org
|
||||
for pair in archive_cookie.split(';'):
|
||||
if '=' in pair:
|
||||
name, value = pair.split('=', 1)
|
||||
session.cookies.set(name.strip(), value.strip(), domain='.archive.org')
|
||||
|
||||
session.get('https://archive.org/robots.txt', timeout=20, headers={'Cookie': archive_cookie} if archive_cookie else None)
|
||||
meta_resp = session.get(f'https://archive.org/metadata/{pre_id}', timeout=20, headers={'Cookie': archive_cookie} if archive_cookie else None)
|
||||
if meta_resp.status_code == 200:
|
||||
try:
|
||||
meta_json = meta_resp.json()
|
||||
except Exception:
|
||||
meta_json = None
|
||||
logger.debug(f"Pré-chargement cookies/metadata archive.org pour {pre_id}")
|
||||
|
||||
# Construire des URLs alternatives pour archive interne
|
||||
identifier, archive_name, inner_path = _split_archive_org_path(url)
|
||||
if identifier and archive_name and inner_path:
|
||||
# Variante sans préfixe archive
|
||||
archive_alt_urls.append(f"https://archive.org/download/{identifier}/" + urllib.parse.quote(inner_path, safe='/'))
|
||||
# Variante filename
|
||||
archive_alt_urls.append(f"https://archive.org/download/{identifier}/{archive_name}?filename=" + urllib.parse.quote(inner_path, safe='/'))
|
||||
# Variante view_archive.php via serveur/dir metadata
|
||||
if meta_json:
|
||||
server = meta_json.get('server')
|
||||
directory = meta_json.get('dir')
|
||||
if server and directory:
|
||||
archive_path = f"{directory}/{archive_name}"
|
||||
view_url = f"https://{server}/view_archive.php?archive=" + urllib.parse.quote(archive_path, safe='/') + "&file=" + urllib.parse.quote(inner_path, safe='/')
|
||||
# Prioriser view_archive.php (cas valide observe dans le navigateur)
|
||||
archive_alt_urls.insert(0, view_url)
|
||||
except Exception as e:
|
||||
logger.debug(f"Pré-chargement archive.org ignoré: {e}")
|
||||
|
||||
@@ -1087,19 +1177,22 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
header_variants = [
|
||||
download_headers,
|
||||
{ # Variante sans Referer spécifique
|
||||
'User-Agent': headers['User-Agent'],
|
||||
'User-Agent': headers.get('User-Agent', download_headers.get('User-Agent', 'Mozilla/5.0')),
|
||||
'Accept': 'application/octet-stream,*/*;q=0.8',
|
||||
'Accept-Language': headers['Accept-Language'],
|
||||
'Connection': 'keep-alive'
|
||||
'Accept-Language': headers.get('Accept-Language', 'en-US,en;q=0.5'),
|
||||
'Connection': 'keep-alive',
|
||||
**({'Cookie': archive_cookie} if archive_cookie else {})
|
||||
},
|
||||
{ # Variante minimaliste type curl
|
||||
'User-Agent': 'curl/8.4.0',
|
||||
'Accept': '*/*'
|
||||
'Accept': '*/*',
|
||||
**({'Cookie': archive_cookie} if archive_cookie else {})
|
||||
},
|
||||
{ # Variante avec Referer archive.org
|
||||
'User-Agent': headers['User-Agent'],
|
||||
'User-Agent': headers.get('User-Agent', download_headers.get('User-Agent', 'Mozilla/5.0')),
|
||||
'Accept': '*/*',
|
||||
'Referer': 'https://archive.org/'
|
||||
'Referer': 'https://archive.org/',
|
||||
**({'Cookie': archive_cookie} if archive_cookie else {})
|
||||
}
|
||||
]
|
||||
response = None
|
||||
@@ -1117,7 +1210,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
# Mettre à jour le fichier web
|
||||
# Plus besoin de update_web_progress
|
||||
|
||||
logger.debug(f"Tentative téléchargement {attempt}/{len(header_variants)} avec headers: {hv}")
|
||||
logger.debug(f"Tentative téléchargement {attempt}/{len(header_variants)} avec headers: {_redact_headers(hv)}")
|
||||
# Timeout plus long pour archive.org, avec tuple (connect_timeout, read_timeout)
|
||||
timeout_val = (60, 90) if 'archive.org' in url else 30
|
||||
r = session.get(url, stream=True, timeout=timeout_val, allow_redirects=True, headers=hv)
|
||||
@@ -1161,13 +1254,36 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
time.sleep(2)
|
||||
|
||||
if response is None:
|
||||
if archive_alt_urls and (last_status in (401, 403) or last_error_type in ("timeout", "connection", "request")):
|
||||
for alt_url in archive_alt_urls:
|
||||
try:
|
||||
timeout_val = (45, 90)
|
||||
logger.debug(f"Tentative archive.org alt URL: {alt_url}")
|
||||
alt_headers = download_headers.copy()
|
||||
try:
|
||||
alt_host = urllib.parse.urlsplit(alt_url).netloc
|
||||
if alt_host.startswith("ia") and alt_host.endswith(".archive.org"):
|
||||
alt_headers["Referer"] = f"https://{alt_host}/"
|
||||
alt_headers["Origin"] = "https://archive.org"
|
||||
except Exception:
|
||||
pass
|
||||
r = session.get(alt_url, stream=True, timeout=timeout_val, allow_redirects=True, headers=alt_headers)
|
||||
if r.status_code not in (401, 403):
|
||||
r.raise_for_status()
|
||||
response = r
|
||||
url = alt_url
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Alt URL archive.org échec: {e}")
|
||||
# Fallback metadata archive.org pour message clair
|
||||
if 'archive.org/download/' in url:
|
||||
try:
|
||||
identifier = url.split('/download/')[1].split('/')[0]
|
||||
meta_resp = session.get(f'https://archive.org/metadata/{identifier}', timeout=30)
|
||||
if meta_resp.status_code == 200:
|
||||
meta_json = meta_resp.json()
|
||||
if meta_json is None:
|
||||
meta_resp = session.get(f'https://archive.org/metadata/{identifier}', timeout=30)
|
||||
if meta_resp.status_code == 200:
|
||||
meta_json = meta_resp.json()
|
||||
if meta_json:
|
||||
if meta_json.get('is_dark'):
|
||||
raise requests.HTTPError(f"Item archive.org restreint (is_dark=true): {identifier}")
|
||||
if not meta_json.get('files'):
|
||||
@@ -1176,7 +1292,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
available = [f.get('name') for f in meta_json.get('files', [])][:10]
|
||||
raise requests.HTTPError(f"Accès refusé (HTTP {last_status}). Fichiers disponibles exemples: {available}")
|
||||
else:
|
||||
raise requests.HTTPError(f"HTTP {last_status} & metadata {meta_resp.status_code} pour {identifier}")
|
||||
raise requests.HTTPError(f"HTTP {last_status} & metadata indisponible pour {identifier}")
|
||||
except requests.HTTPError:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -1365,6 +1481,21 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
|
||||
elif extension == ".7z":
|
||||
try:
|
||||
success, msg = extract_7z(dest_path, dest_dir, url)
|
||||
if success:
|
||||
logger.debug(f"Extraction 7z réussie: {msg}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction 7z: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction 7z: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction 7z {game_name}: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Type d'archive non supporté: {extension}")
|
||||
result[0] = True
|
||||
@@ -1415,7 +1546,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
|
||||
@@ -1487,7 +1618,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
|
||||
@@ -1530,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]
|
||||
@@ -1586,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:
|
||||
@@ -1633,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
|
||||
@@ -1642,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,
|
||||
@@ -1945,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)")
|
||||
@@ -2171,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
|
||||
@@ -2401,6 +2624,21 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
|
||||
elif extension == ".7z":
|
||||
try:
|
||||
success, msg = extract_7z(dest_path, dest_dir, url)
|
||||
logger.debug(f"Extraction 7z terminée: {msg}")
|
||||
if success:
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction 7z: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction 7z: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction 7z {game_name}: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Type d'archive non supporté: {extension}")
|
||||
result[0] = True
|
||||
@@ -2456,7 +2694,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
|
||||
@@ -2511,7 +2749,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.debug(f"[1F_DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
|
||||
@@ -17,7 +17,7 @@ import re
|
||||
import config # paths, settings, SAVE_FOLDER, etc.
|
||||
from utils import load_api_keys as _prime_api_keys # ensure API key files are created
|
||||
import network as network_mod # for progress_queues access
|
||||
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename, extract_zip_data
|
||||
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename
|
||||
from history import load_history, save_history, add_to_history
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url
|
||||
from rgsx_settings import get_sources_zip_url
|
||||
@@ -277,7 +277,7 @@ def cmd_games(args):
|
||||
suggestions = [] # (priority, score, game_obj)
|
||||
# 1) Substring match (full or sans extension) priority 0, score = position
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
t_lower = title.lower()
|
||||
@@ -303,7 +303,7 @@ def cmd_games(args):
|
||||
# 2) Ordered non-contiguous tokens (priority 1)
|
||||
if q_tokens:
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
tt = _tokens(title)
|
||||
@@ -313,7 +313,7 @@ def cmd_games(args):
|
||||
# 3) All tokens present, any order (priority 2), score = token set size
|
||||
if q_tokens:
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
t_tokens = set(_tokens(title))
|
||||
@@ -322,12 +322,12 @@ def cmd_games(args):
|
||||
# Deduplicate by title keeping best (lowest priority, then score)
|
||||
best = {}
|
||||
for prio, score, g in suggestions:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
|
||||
title = g.name
|
||||
key = title.lower()
|
||||
cur = best.get(key)
|
||||
if cur is None or (prio, score) < (cur[0], cur[1]):
|
||||
best[key] = (prio, score, g)
|
||||
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower()))
|
||||
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2].name if isinstance(x[2], (list, config.Game)) and x[2] else str(x[2])).lower()))
|
||||
games = [g for _, _, g in ranked]
|
||||
# Table: Name (60) | Size (12) to allow "xxxx.xx MiB"
|
||||
NAME_W = 60
|
||||
@@ -344,7 +344,7 @@ def cmd_games(args):
|
||||
print(header)
|
||||
print(border)
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
|
||||
title = g.name
|
||||
size_val = ''
|
||||
if isinstance(g, (list, tuple)) and len(g) >= 3:
|
||||
size_val = display_size(g[2])
|
||||
@@ -447,11 +447,11 @@ def cmd_download(args):
|
||||
def _tokens(s: str) -> list[str]:
|
||||
return re.findall(r"[a-z0-9]+", s.lower())
|
||||
|
||||
def _game_title(g) -> str | None:
|
||||
return g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
def _game_title(g: config.Game) -> str | None:
|
||||
return g.name
|
||||
|
||||
def _game_url(g) -> str | None:
|
||||
return g[1] if isinstance(g, (list, tuple)) and len(g) > 1 else None
|
||||
def _game_url(g: config.Game) -> str | None:
|
||||
return g.url
|
||||
|
||||
# 1) Exact match (case-insensitive), with and without extension
|
||||
match = None
|
||||
@@ -561,8 +561,8 @@ def cmd_download(args):
|
||||
size_val = ''
|
||||
size_raw = None
|
||||
for g in games:
|
||||
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
|
||||
size_raw = g[2]
|
||||
if g.name == title:
|
||||
size_raw = g.size
|
||||
break
|
||||
if size_raw is not None:
|
||||
size_val = display_size(size_raw)
|
||||
|
||||
@@ -21,10 +21,11 @@ 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
|
||||
from config import Game
|
||||
|
||||
try:
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
@@ -161,7 +162,7 @@ def get_cached_sources() -> tuple[list[dict], str, datetime]:
|
||||
return copy.deepcopy(platforms), etag, last_modified
|
||||
|
||||
|
||||
def get_cached_games(platform: str) -> tuple[list[tuple], str, datetime]:
|
||||
def get_cached_games(platform: str) -> tuple[list[Game], str, datetime]:
|
||||
"""Return cached games list for platform with metadata."""
|
||||
now = time.time()
|
||||
with cache_lock:
|
||||
@@ -696,14 +697,14 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
elif games_last_modified:
|
||||
latest_modified = games_last_modified
|
||||
for game in games:
|
||||
game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
|
||||
game_name = game.name
|
||||
game_name_lower = game_name.lower()
|
||||
if all(word in game_name_lower for word in search_words):
|
||||
matching_games.append({
|
||||
'game_name': game_name,
|
||||
'platform': platform_name,
|
||||
'url': game[1] if len(game) > 1 and isinstance(game, (list, tuple)) else None,
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None, self._get_language_from_cookies())
|
||||
'url': game.url,
|
||||
'size': normalize_size(game.size, self._get_language_from_cookies())
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de la recherche dans {platform_name}: {e}")
|
||||
@@ -750,9 +751,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
games, _, games_last_modified = get_cached_games(platform_name)
|
||||
games_formatted = [
|
||||
{
|
||||
'name': g[0],
|
||||
'url': g[1] if len(g) > 1 else None,
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None, lang)
|
||||
'name': g.name,
|
||||
'url': g.url,
|
||||
'size': normalize_size(g.size, lang)
|
||||
}
|
||||
for g in games
|
||||
]
|
||||
@@ -882,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', '')
|
||||
}
|
||||
|
||||
@@ -1134,7 +1136,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
if game_name_param and game_index is None:
|
||||
game_index = None
|
||||
for idx, game in enumerate(games):
|
||||
current_game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
|
||||
current_game_name = game.name
|
||||
if current_game_name == game_name_param:
|
||||
game_index = idx
|
||||
break
|
||||
@@ -1155,8 +1157,8 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
game = games[game_index]
|
||||
game_name = game[0]
|
||||
game_url = game[1] if len(game) > 1 else None
|
||||
game_name = game.name
|
||||
game_url = game.url
|
||||
|
||||
if not game_url:
|
||||
self._send_json({
|
||||
@@ -1241,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,
|
||||
@@ -1278,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,
|
||||
@@ -2068,18 +2072,47 @@ def run_server(host='0.0.0.0', port=5000):
|
||||
class ReuseAddrHTTPServer(HTTPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
# Tuer les processus existants utilisant le port
|
||||
# Tuer les processus existants utilisant le port (plateforme spécifique)
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True, timeout=2)
|
||||
pids = result.stdout.strip().split('\n')
|
||||
for pid in pids:
|
||||
if pid:
|
||||
try:
|
||||
subprocess.run(['kill', '-9', pid], timeout=2)
|
||||
logger.info(f"Processus {pid} tué (port {port} libéré)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
|
||||
# Windows: utiliser netstat + taskkill
|
||||
if os.name == 'nt' or getattr(config, 'OPERATING_SYSTEM', '').lower() == 'windows':
|
||||
try:
|
||||
netstat = subprocess.run(['netstat', '-ano'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=3)
|
||||
lines = netstat.stdout.splitlines()
|
||||
pids = set()
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) >= 5:
|
||||
local = parts[1]
|
||||
pid = parts[-1]
|
||||
if local.endswith(f':{port}'):
|
||||
pids.add(pid)
|
||||
for pid in pids:
|
||||
# Safer: ignore PID 0 and non-numeric entries (system / header lines)
|
||||
if not pid or not pid.isdigit():
|
||||
continue
|
||||
pid_int = int(pid)
|
||||
if pid_int <= 0:
|
||||
continue
|
||||
try:
|
||||
subprocess.run(['taskkill', '/PID', pid, '/F'], timeout=3)
|
||||
logger.info(f"Processus {pid} tué (port {port} libéré) [Windows]")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Windows port release check failed: {e}")
|
||||
else:
|
||||
# Unix-like: utiliser lsof + kill
|
||||
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=2)
|
||||
pids = result.stdout.strip().split('\n')
|
||||
for pid in pids:
|
||||
if pid:
|
||||
try:
|
||||
subprocess.run(['kill', '-9', pid], timeout=2)
|
||||
logger.info(f"Processus {pid} tué (port {port} libéré)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de libérer le port {port}: {e}")
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import requests # type: ignore
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
@@ -6,7 +8,7 @@ import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import config
|
||||
from config import HEADLESS
|
||||
from config import HEADLESS, Game
|
||||
try:
|
||||
if not HEADLESS:
|
||||
import pygame # type: ignore
|
||||
@@ -31,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)
|
||||
|
||||
@@ -171,6 +203,121 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
return (False, str(e), None)
|
||||
|
||||
|
||||
VERSIONCLEAN_SERVICE_NAME = "versionclean"
|
||||
VERSIONCLEAN_BACKUP_PATH = "/usr/bin/batocera-version.bak"
|
||||
|
||||
|
||||
def _get_enabled_services():
|
||||
"""Retourne la liste des services activés dans batocera-settings, ou None si indisponible."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["batocera-settings-get", "system.services"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"batocera-settings-get failed: {result.stderr}")
|
||||
return None
|
||||
return result.stdout.split()
|
||||
except FileNotFoundError:
|
||||
logger.warning("batocera-settings-get command not found")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read enabled services: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_versionclean_service():
|
||||
"""Installe et active versionclean si nécessaire.
|
||||
|
||||
- Installe uniquement si le service n'est pas déjà présent.
|
||||
- Active uniquement si le service n'est pas déjà activé.
|
||||
- Démarre uniquement si le nettoyage n'est pas déjà appliqué.
|
||||
"""
|
||||
try:
|
||||
if config.OPERATING_SYSTEM != "Linux":
|
||||
return (True, "Versionclean skipped (non-Linux)")
|
||||
|
||||
services_dir = "/userdata/system/services"
|
||||
service_file = os.path.join(services_dir, VERSIONCLEAN_SERVICE_NAME)
|
||||
source_file = os.path.join(config.APP_FOLDER, "assets", "progs", VERSIONCLEAN_SERVICE_NAME)
|
||||
|
||||
if not os.path.exists(service_file):
|
||||
try:
|
||||
os.makedirs(services_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
if not os.path.exists(source_file):
|
||||
error_msg = f"Source service file not found: {source_file}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
try:
|
||||
shutil.copy2(source_file, service_file)
|
||||
os.chmod(service_file, 0o755)
|
||||
logger.info(f"Versionclean service installed: {service_file}")
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to copy versionclean service file: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
else:
|
||||
logger.debug("Versionclean service already present, skipping install")
|
||||
|
||||
enabled_services = _get_enabled_services()
|
||||
if enabled_services is None or VERSIONCLEAN_SERVICE_NAME not in enabled_services:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["batocera-services", "enable", VERSIONCLEAN_SERVICE_NAME],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"batocera-services enable versionclean failed: {result.stderr}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
logger.debug(f"Versionclean enabled: {result.stdout}")
|
||||
except FileNotFoundError:
|
||||
error_msg = "batocera-services command not found"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to enable versionclean: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
else:
|
||||
logger.debug("Versionclean already enabled, skipping enable")
|
||||
|
||||
if os.path.exists(VERSIONCLEAN_BACKUP_PATH):
|
||||
logger.debug("Versionclean already active (backup present), skipping start")
|
||||
return (True, "Versionclean already active")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["batocera-services", "start", VERSIONCLEAN_SERVICE_NAME],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"batocera-services start versionclean warning: {result.stderr}")
|
||||
else:
|
||||
logger.debug(f"Versionclean started: {result.stdout}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to start versionclean (non-critical): {str(e)}")
|
||||
|
||||
return (True, "Versionclean ensured")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected versionclean error: {str(e)}"
|
||||
logger.exception(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
|
||||
def toggle_web_service_at_boot(enable: bool):
|
||||
"""Active ou désactive le service web au démarrage de Batocera.
|
||||
|
||||
@@ -203,6 +350,11 @@ def toggle_web_service_at_boot(enable: bool):
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 1b. Assurer versionclean (install/enable/start si nécessaire)
|
||||
ensure_ok, ensure_msg = _ensure_versionclean_service()
|
||||
if not ensure_ok:
|
||||
return (False, ensure_msg)
|
||||
|
||||
# 2. Copier le fichier rgsx_web
|
||||
try:
|
||||
@@ -339,6 +491,11 @@ def toggle_custom_dns_at_boot(enable: bool):
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 1b. Assurer versionclean (install/enable/start si nécessaire)
|
||||
ensure_ok, ensure_msg = _ensure_versionclean_service()
|
||||
if not ensure_ok:
|
||||
return (False, ensure_msg)
|
||||
|
||||
# 2. Copier le fichier custom_dns
|
||||
try:
|
||||
@@ -479,6 +636,149 @@ def check_custom_dns_status():
|
||||
return False
|
||||
|
||||
|
||||
CONNECTION_STATUS_TTL_SECONDS = 120
|
||||
|
||||
|
||||
def get_connection_status_targets():
|
||||
"""Retourne la liste des sites à vérifier pour le status de connexion."""
|
||||
return [
|
||||
{
|
||||
"key": "retrogamesets",
|
||||
"label": "Retrogamesets.fr",
|
||||
"url": "https://retrogamesets.fr",
|
||||
"category": "updates",
|
||||
},
|
||||
{
|
||||
"key": "github",
|
||||
"label": "GitHub.com",
|
||||
"url": "https://github.com",
|
||||
"category": "updates",
|
||||
},
|
||||
{
|
||||
"key": "myrient",
|
||||
"label": "Myrient.erista.me",
|
||||
"url": "https://myrient.erista.me",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "1fichier",
|
||||
"label": "1fichier.com",
|
||||
"url": "https://1fichier.com",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "archive",
|
||||
"label": "Archive.org",
|
||||
"url": "https://archive.org",
|
||||
"category": "sources",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _check_url_connectivity(url: str, timeout: int = 6) -> bool:
|
||||
"""Teste rapidement la connectivité à une URL (DNS + HTTPS)."""
|
||||
headers = {"User-Agent": "RGSX-Connectivity/1.0"}
|
||||
try:
|
||||
try:
|
||||
|
||||
|
||||
try:
|
||||
response = requests.head(url, timeout=timeout, allow_redirects=True, headers=headers)
|
||||
if response.status_code < 500:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
response = requests.get(url, timeout=timeout, allow_redirects=True, stream=True, headers=headers)
|
||||
return response.status_code < 500
|
||||
except Exception:
|
||||
return False
|
||||
except Exception:
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, method="HEAD", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return resp.status < 500
|
||||
except Exception:
|
||||
try:
|
||||
req = urllib.request.Request(url, method="GET", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return resp.status < 500
|
||||
except Exception:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def start_connection_status_check(force: bool = False) -> None:
|
||||
"""Lance un check asynchrone des sites (avec cache/TTL)."""
|
||||
try:
|
||||
now = time.time()
|
||||
if getattr(config, "connection_status_in_progress", False):
|
||||
return
|
||||
last_ts = getattr(config, "connection_status_timestamp", 0.0) or 0.0
|
||||
if not force and last_ts and now - last_ts < CONNECTION_STATUS_TTL_SECONDS:
|
||||
return
|
||||
|
||||
targets = get_connection_status_targets()
|
||||
status = getattr(config, "connection_status", {})
|
||||
if not isinstance(status, dict):
|
||||
status = {}
|
||||
if not status:
|
||||
for item in targets:
|
||||
status[item["key"]] = None
|
||||
config.connection_status = status
|
||||
config.connection_status_in_progress = True
|
||||
config.connection_status_progress = {"done": 0, "total": len(targets)}
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
results = {}
|
||||
done = 0
|
||||
total = len(targets)
|
||||
for item in targets:
|
||||
results[item["key"]] = _check_url_connectivity(item["url"])
|
||||
done += 1
|
||||
config.connection_status_progress = {"done": done, "total": total}
|
||||
try:
|
||||
config.needs_redraw = True
|
||||
except Exception:
|
||||
pass
|
||||
config.connection_status.update(results)
|
||||
config.connection_status_timestamp = time.time()
|
||||
try:
|
||||
summary = ", ".join([f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items()])
|
||||
logger.info(f"Connection status results: {summary}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
config.needs_redraw = True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"Connection status check failed: {e}")
|
||||
finally:
|
||||
config.connection_status_in_progress = False
|
||||
|
||||
threading.Thread(target=_worker, daemon=True).start()
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to start connection status check: {e}")
|
||||
|
||||
|
||||
def get_connection_status_snapshot():
|
||||
"""Retourne (status_dict, timestamp, in_progress, progress)."""
|
||||
status = getattr(config, "connection_status", {})
|
||||
if not isinstance(status, dict):
|
||||
status = {}
|
||||
ts = getattr(config, "connection_status_timestamp", 0.0) or 0.0
|
||||
in_progress = getattr(config, "connection_status_in_progress", False)
|
||||
progress = getattr(config, "connection_status_progress", {"done": 0, "total": 0})
|
||||
if not isinstance(progress, dict):
|
||||
progress = {"done": 0, "total": 0}
|
||||
return status, ts, in_progress, progress
|
||||
|
||||
|
||||
|
||||
_extensions_cache = None # type: ignore
|
||||
_extensions_json_regenerated = False
|
||||
@@ -659,7 +959,7 @@ def check_extension_before_download(url, platform, game_name):
|
||||
|
||||
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
|
||||
extension = os.path.splitext(sanitized_name)[1].lower()
|
||||
is_archive = extension in (".zip", ".rar")
|
||||
is_archive = extension in (".zip", ".rar", ".7z")
|
||||
|
||||
# Déterminer si le système (dossier) est connu dans extensions_data
|
||||
dest_folder_name = _get_dest_folder_name(platform)
|
||||
@@ -886,12 +1186,18 @@ def load_sources():
|
||||
for platform_name in config.platforms:
|
||||
games = load_games(platform_name)
|
||||
config.games_count[platform_name] = len(games)
|
||||
if config.games_count:
|
||||
try:
|
||||
summary = ", ".join([f"{name}: {count}" for name, count in config.games_count.items()])
|
||||
logger.debug(f"Nombre de jeux par système: {summary}")
|
||||
except Exception:
|
||||
pass
|
||||
return sources
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
|
||||
return []
|
||||
|
||||
def load_games(platform_id):
|
||||
def load_games(platform_id:str) -> list[Game]:
|
||||
try:
|
||||
# Retrouver l'objet plateforme pour accéder éventuellement à 'folder'
|
||||
platform_dict = None
|
||||
@@ -919,9 +1225,15 @@ def load_games(platform_id):
|
||||
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)
|
||||
|
||||
@@ -958,9 +1270,22 @@ def load_games(platform_id):
|
||||
else:
|
||||
logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}")
|
||||
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
return normalized
|
||||
if getattr(config, "games_count_log_verbose", False):
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
|
||||
games_list: list[Game] = []
|
||||
for name, url, size in normalized:
|
||||
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 []
|
||||
|
||||
@@ -1544,7 +1869,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
|
||||
|
||||
@@ -1563,6 +1888,95 @@ def extract_rar(rar_path, dest_dir, url):
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}")
|
||||
|
||||
|
||||
def extract_7z(archive_path, dest_dir, url):
|
||||
"""Extrait le contenu d'un fichier 7z dans le dossier cible."""
|
||||
try:
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
if config.OPERATING_SYSTEM == "Windows":
|
||||
seven_z_cmd = config.SEVEN_Z_EXE
|
||||
else:
|
||||
seven_z_cmd = config.SEVEN_Z_LINUX
|
||||
try:
|
||||
if os.path.exists(seven_z_cmd) and not os.access(seven_z_cmd, os.X_OK):
|
||||
logger.warning("7zz n'est pas exécutable, correction des permissions...")
|
||||
os.chmod(seven_z_cmd, 0o755)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification des permissions de 7zz: {e}")
|
||||
|
||||
if not os.path.exists(seven_z_cmd):
|
||||
return False, "7z non trouvé - vérifiez que 7z.exe (Windows) ou 7zz (Linux) est présent dans assets/progs"
|
||||
|
||||
# Capture état initial
|
||||
before_dirs = _capture_directories_before_extraction(dest_dir)
|
||||
before_items = _capture_all_items_before_extraction(dest_dir)
|
||||
iso_before = set()
|
||||
for root, dirs, files in os.walk(dest_dir):
|
||||
for file in files:
|
||||
if file.lower().endswith('.iso'):
|
||||
iso_before.add(os.path.abspath(os.path.join(root, file)))
|
||||
|
||||
# Calcul taille totale via 7z l -slt (best effort)
|
||||
total_size = 0
|
||||
try:
|
||||
list_cmd = [seven_z_cmd, "l", "-slt", archive_path]
|
||||
result = subprocess.run(list_cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
current_size = None
|
||||
is_dir = False
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if current_size is not None and not is_dir:
|
||||
total_size += current_size
|
||||
current_size = None
|
||||
is_dir = False
|
||||
continue
|
||||
if line.startswith("Attributes ="):
|
||||
attrs = line.split("=", 1)[1].strip()
|
||||
if "D" in attrs:
|
||||
is_dir = True
|
||||
elif line.startswith("Size ="):
|
||||
try:
|
||||
current_size = int(line.split("=", 1)[1].strip())
|
||||
except Exception:
|
||||
current_size = None
|
||||
if current_size is not None and not is_dir:
|
||||
total_size += current_size
|
||||
except Exception as e:
|
||||
logger.debug(f"Impossible de calculer la taille 7z: {e}")
|
||||
|
||||
if url not in getattr(config, 'download_progress', {}):
|
||||
config.download_progress[url] = {}
|
||||
config.download_progress[url].update({
|
||||
"downloaded_size": 0,
|
||||
"total_size": total_size,
|
||||
"status": "Extracting",
|
||||
"progress_percent": 0
|
||||
})
|
||||
config.needs_redraw = True
|
||||
|
||||
extract_cmd = [seven_z_cmd, "x", archive_path, f"-o{dest_dir}", "-y"]
|
||||
logger.debug(f"Commande d'extraction 7z: {' '.join(extract_cmd)}")
|
||||
result = subprocess.run(extract_cmd, capture_output=True, text=True)
|
||||
if result.returncode > 2:
|
||||
error_msg = result.stderr.strip() or f"Erreur extraction 7z (code {result.returncode})"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"7z a retourné un avertissement (code {result.returncode}): {result.stderr}")
|
||||
|
||||
# Gestion plateformes spéciales
|
||||
success, error_msg = _handle_special_platforms(dest_dir, archive_path, before_dirs, iso_before, url, before_items)
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
return _finalize_extraction(archive_path, dest_dir, url)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'extraction 7z: {str(e)}")
|
||||
return False, _("utils_extraction_failed").format(str(e))
|
||||
|
||||
def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archive_name=None):
|
||||
"""Gère le traitement spécifique des jeux PS3.
|
||||
PS3 Redump (ps3): Décryptage ISO + extraction dans dossier .ps3
|
||||
@@ -1589,18 +2003,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')]
|
||||
@@ -1611,42 +2038,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:
|
||||
import requests
|
||||
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:
|
||||
@@ -2344,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
|
||||
|
||||
@@ -2395,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
|
||||
@@ -2402,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
|
||||
}
|
||||
@@ -2410,16 +2850,68 @@ 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
|
||||
}
|
||||
|
||||
|
||||
def load_archive_org_cookie(force: bool = False) -> str:
|
||||
"""Charge le cookie Archive.org depuis un fichier texte.
|
||||
|
||||
- Fichier: config.ARCHIVE_ORG_COOKIE_PATH
|
||||
- Accepte soit une ligne brute de cookie, soit une ligne "Cookie: ..."
|
||||
- Utilise un cache mtime pour éviter les relectures
|
||||
"""
|
||||
try:
|
||||
path = getattr(config, 'ARCHIVE_ORG_COOKIE_PATH', '')
|
||||
if not path:
|
||||
return ""
|
||||
cache_attr = '_archive_cookie_cache'
|
||||
if not hasattr(config, cache_attr):
|
||||
setattr(config, cache_attr, {'mtime': None, 'value': ''})
|
||||
cache_data = getattr(config, cache_attr)
|
||||
|
||||
# Créer le fichier vide si absent
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write("")
|
||||
except Exception as ce:
|
||||
logger.error(f"Impossible de préparer le fichier cookie archive.org: {ce}")
|
||||
return ""
|
||||
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
except Exception:
|
||||
mtime = None
|
||||
|
||||
if force or (mtime is not None and mtime != cache_data.get('mtime')):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
value = f.read().strip()
|
||||
except Exception as re:
|
||||
logger.error(f"Erreur lecture cookie archive.org: {re}")
|
||||
value = ""
|
||||
|
||||
if value.lower().startswith("cookie:"):
|
||||
value = value.split(":", 1)[1].strip()
|
||||
|
||||
cache_data['mtime'] = mtime
|
||||
cache_data['value'] = value
|
||||
|
||||
return cache_data.get('value', '') or ""
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur load_archive_org_cookie: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -2429,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', ''),
|
||||
}
|
||||
|
||||
@@ -2456,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()
|
||||
|
||||
@@ -2481,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', '')
|
||||
|
||||
@@ -2493,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."""
|
||||
@@ -2513,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')),
|
||||
}
|
||||
|
||||
@@ -2563,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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.5.0.0"
|
||||
"version": "2.6.1.1"
|
||||
}
|
||||
@@ -301,6 +301,7 @@ set PYGAME_HIDE_SUPPORT_PROMPT=1
|
||||
set SDL_VIDEODRIVER=windows
|
||||
set SDL_AUDIODRIVER=directsound
|
||||
set PYTHONWARNINGS=ignore::UserWarning:pygame.pkgdata
|
||||
set PYTHONIOENCODING=utf-8
|
||||
|
||||
:: =============================================================================
|
||||
:: Configuration multi-ecran
|
||||
@@ -331,6 +332,7 @@ echo [%DATE% %TIME%] Environment variables set: >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_ROOT=%RGSX_ROOT% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] SDL_VIDEODRIVER=%SDL_VIDEODRIVER% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] SDL_AUDIODRIVER=%SDL_AUDIODRIVER% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] PYTHONIOENCODING=%PYTHONIOENCODING% >> "%LOG_FILE%"
|
||||
|
||||
echo.
|
||||
if defined DISPLAY_NUM (
|
||||
|
||||
Reference in New Issue
Block a user