mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 20:55:24 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce39722351 | ||
|
|
a7dad84108 | ||
|
|
c9f48d20dd | ||
|
|
cd7795f70e | ||
|
|
6813a0bc3d | ||
|
|
21b39c66b9 | ||
|
|
42b2204aeb | ||
|
|
67a38c45aa | ||
|
|
893b73ecc5 | ||
|
|
5e1a684275 | ||
|
|
9226a818f3 | ||
|
|
2fd1bcaf01 | ||
|
|
875bf8fa23 | ||
|
|
f9cbf0196e | ||
|
|
eb86d69895 | ||
|
|
b09b3da371 | ||
|
|
0915a90fbe | ||
|
|
3ae3c151eb | ||
|
|
7460b12d71 | ||
|
|
2f437c1aa4 | ||
|
|
054b174c18 | ||
|
|
fbb1a2aa68 |
23
.github/workflows/release.yml
vendored
23
.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"
|
||||
@@ -138,3 +148,16 @@ jobs:
|
||||
dist/RGSX_full_latest.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Send changelog to Discord
|
||||
run: |
|
||||
CHANGELOG=$(git log -1 --format=%B ${{ github.ref_name }} | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||
|
||||
curl -H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "{
|
||||
\"username\": \"RGSX Releases Bot\",
|
||||
\"avatar_url\": \"https://retrogamesets.fr/assets/images/avatar.png\",
|
||||
\"content\": \"📦 **RGSX ${{ github.ref_name }}**\n\n📝 **Changelog :**\n${CHANGELOG}\"
|
||||
}" \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -28,13 +28,14 @@ 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
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, apply_pending_update, cancel_all_downloads, download_queue_worker
|
||||
from controls import handle_controls, validate_menu_state, process_key_repeats, get_emergency_controls
|
||||
from controls_mapper import map_controls, draw_controls_mapping, get_actions
|
||||
from controls import load_controls_config
|
||||
@@ -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:
|
||||
@@ -98,6 +99,9 @@ _run_windows_gamelist_update()
|
||||
try:
|
||||
config.update_checked = False
|
||||
config.gamelist_update_prompted = False # Flag pour ne pas redemander la mise à jour plusieurs fois
|
||||
config.pending_update_version = ""
|
||||
config.startup_update_confirmed = False
|
||||
config.text_file_mode = ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -429,7 +433,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
|
||||
@@ -456,6 +460,7 @@ async def main():
|
||||
|
||||
running = True
|
||||
loading_step = "none"
|
||||
ota_update_task = None
|
||||
sources = []
|
||||
config.last_state_change_time = 0
|
||||
config.debounce_delay = 50
|
||||
@@ -488,6 +493,9 @@ async def main():
|
||||
if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100:
|
||||
config.needs_redraw = True
|
||||
last_redraw_time = current_time
|
||||
if config.menu_state == "loading" and current_time - last_redraw_time >= 100:
|
||||
config.needs_redraw = True
|
||||
last_redraw_time = current_time
|
||||
# Forcer redraw toutes les 100 ms dans history avec téléchargement actif
|
||||
if config.menu_state == "history" and any(entry["status"] in ["Downloading", "Téléchargement"] for entry in config.history):
|
||||
if current_time - last_redraw_time >= 100:
|
||||
@@ -609,6 +617,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 +690,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 +699,7 @@ async def main():
|
||||
# Basculer sur les contrôles clavier
|
||||
config.joystick = False
|
||||
config.keyboard = True
|
||||
config.controller_device_name = ""
|
||||
# Recharger la configuration des contrôles pour le clavier
|
||||
config.controls_config = load_controls_config()
|
||||
logger.info("Contrôles clavier chargés")
|
||||
@@ -729,6 +760,7 @@ async def main():
|
||||
"filter_menu_choice",
|
||||
"filter_advanced",
|
||||
"filter_priority_config",
|
||||
"platform_search",
|
||||
}
|
||||
if config.menu_state in SIMPLE_HANDLE_STATES:
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
@@ -849,7 +881,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
|
||||
@@ -1013,7 +1050,8 @@ async def main():
|
||||
if success:
|
||||
toast_msg = f"[OK] {game_name}\n{_('download_completed') if _ else 'Download completed'}"
|
||||
else:
|
||||
toast_msg = f"[ERROR] {game_name}\n{_('download_failed') if _ else 'Download failed'}"
|
||||
toast_body = message or (_('download_failed') if _ else 'Download failed')
|
||||
toast_msg = f"[ERROR] {game_name}\n{toast_body}"
|
||||
show_toast(toast_msg, 3000)
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
@@ -1035,7 +1073,8 @@ async def main():
|
||||
config.download_progress.clear()
|
||||
config.pending_download = None
|
||||
# Afficher un toast au lieu de changer de page
|
||||
toast_msg = f"[ERROR] {game_name}\n{_('download_failed') if _ else 'Download failed'}"
|
||||
toast_body = message or (_('download_failed') if _ else 'Download failed')
|
||||
toast_msg = f"[ERROR] {game_name}\n{toast_body}"
|
||||
show_toast(toast_msg, 3000)
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
@@ -1074,7 +1113,8 @@ async def main():
|
||||
if success:
|
||||
toast_msg = f"[OK] {game_name}\n{_('download_completed') if _ else 'Download completed'}"
|
||||
else:
|
||||
toast_msg = f"[ERROR] {game_name}\n{_('download_failed') if _ else 'Download failed'}"
|
||||
toast_body = message or (_('download_failed') if _ else 'Download failed')
|
||||
toast_msg = f"[ERROR] {game_name}\n{toast_body}"
|
||||
show_toast(toast_msg, 3000)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"[DOWNLOAD_TASK] Toast displayed after completion, task_id={task_id}")
|
||||
@@ -1116,6 +1156,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é
|
||||
@@ -1323,6 +1367,10 @@ async def main():
|
||||
config.error_message = message or _("error_check_updates_failed")
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Erreur OTA : {message}")
|
||||
elif getattr(config, "pending_update_version", ""):
|
||||
loading_step = "await_ota_confirmation"
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
else:
|
||||
loading_step = "check_data"
|
||||
config.current_loading_system = _("loading_downloading_games_images")
|
||||
@@ -1330,6 +1378,38 @@ async def main():
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
continue # Passer immédiatement à check_data
|
||||
elif loading_step == "await_ota_confirmation":
|
||||
if not getattr(config, "startup_update_confirmed", False):
|
||||
await asyncio.sleep(0.01)
|
||||
continue
|
||||
|
||||
latest_version = getattr(config, "pending_update_version", "")
|
||||
config.startup_update_confirmed = False
|
||||
ota_update_task = asyncio.create_task(apply_pending_update(latest_version))
|
||||
loading_step = "apply_ota_update"
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
elif loading_step == "apply_ota_update":
|
||||
if ota_update_task is None:
|
||||
loading_step = "check_data"
|
||||
continue
|
||||
if not ota_update_task.done():
|
||||
await asyncio.sleep(0.01)
|
||||
continue
|
||||
|
||||
success, message = await ota_update_task
|
||||
ota_update_task = None
|
||||
if not success:
|
||||
config.menu_state = "error"
|
||||
config.error_message = message or _("error_check_updates_failed")
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
config.pending_update_version = ""
|
||||
config.text_file_mode = ""
|
||||
config.text_file_content = ""
|
||||
config.loading_detail_lines = []
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
elif loading_step == "check_data":
|
||||
is_data_empty = not os.path.exists(config.GAMES_FOLDER) or not any(os.scandir(config.GAMES_FOLDER))
|
||||
if is_data_empty:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"cancel": {
|
||||
"type": "key",
|
||||
"key": 27,
|
||||
"display": "\u00c9chap"
|
||||
"display": "Esc/Echap"
|
||||
},
|
||||
"up": {
|
||||
"type": "key",
|
||||
|
||||
BIN
ports/RGSX/assets/progs/aria2c.exe
Normal file
BIN
ports/RGSX/assets/progs/aria2c.exe
Normal file
Binary file not shown.
@@ -2,15 +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
|
||||
url: Optional[str]
|
||||
size: Optional[str]
|
||||
display_name: str # name withou file extension or platform prefix
|
||||
regions: Optional[list[str]] = None
|
||||
is_non_release: Optional[bool] = None
|
||||
base_name: Optional[str] = None
|
||||
|
||||
# Headless mode for CLI: set env RGSX_HEADLESS=1 to avoid pygame and noisy prints
|
||||
HEADLESS = os.environ.get("RGSX_HEADLESS") == "1"
|
||||
@@ -23,10 +27,10 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.5.0.6"
|
||||
app_version = "2.6.1.7"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 7
|
||||
GAMELIST_UPDATE_DAYS = 1
|
||||
|
||||
|
||||
def get_application_root():
|
||||
@@ -194,6 +198,7 @@ DOWNLOADED_GAMES_PATH = os.path.join(SAVE_FOLDER, "downloaded_games.json")
|
||||
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
|
||||
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
|
||||
API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.txt")
|
||||
API_KEY_DEBRIDLINK_PATH = os.path.join(SAVE_FOLDER, "DebridLinkAPI.txt")
|
||||
API_KEY_REALDEBRID_PATH = os.path.join(SAVE_FOLDER, "RealDebridAPI.txt")
|
||||
ARCHIVE_ORG_COOKIE_PATH = os.path.join(APP_FOLDER, "assets", "ArchiveOrgCookie.txt")
|
||||
|
||||
@@ -220,6 +225,8 @@ PS3DEC_EXE = os.path.join(APP_FOLDER,"assets", "progs", "ps3dec_win.exe")
|
||||
PS3DEC_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "ps3dec_linux")
|
||||
SEVEN_Z_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "7zz")
|
||||
SEVEN_Z_EXE = os.path.join(APP_FOLDER,"assets", "progs", "7z.exe")
|
||||
ARIA2C_EXE = os.path.join(APP_FOLDER,"assets", "progs", "aria2c.exe")
|
||||
ARIA2C_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "aria2c")
|
||||
|
||||
# Détection du système d'exploitation (une seule fois au démarrage)
|
||||
OPERATING_SYSTEM = platform.system()
|
||||
@@ -246,6 +253,30 @@ SYSTEM_INFO = {
|
||||
def get_batocera_system_info():
|
||||
"""Récupère les informations système via la commande batocera-info."""
|
||||
global SYSTEM_INFO
|
||||
|
||||
def get_local_network_ip():
|
||||
try:
|
||||
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
udp_socket.connect(("8.8.8.8", 80))
|
||||
local_ip = udp_socket.getsockname()[0]
|
||||
if local_ip and not local_ip.startswith("127."):
|
||||
return local_ip
|
||||
finally:
|
||||
udp_socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
local_ip = socket.gethostbyname(hostname)
|
||||
if local_ip and not local_ip.startswith("127."):
|
||||
return local_ip
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ""
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['batocera-info'], capture_output=True, text=True, timeout=5)
|
||||
@@ -301,6 +332,7 @@ def get_batocera_system_info():
|
||||
SYSTEM_INFO["system"] = f"{platform.system()} {platform.release()}"
|
||||
SYSTEM_INFO["architecture"] = platform.machine()
|
||||
SYSTEM_INFO["cpu_model"] = platform.processor() or "Unknown"
|
||||
SYSTEM_INFO["network_ip"] = get_local_network_ip()
|
||||
|
||||
return False
|
||||
|
||||
@@ -403,6 +435,12 @@ filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filt
|
||||
search_mode = False # Indicateur si le mode recherche est actif
|
||||
search_query = "" # Chaîne de recherche saisie par l'utilisateur
|
||||
filter_active = False # Indicateur si un filtre est appliqué
|
||||
global_search_index = [] # Index de recherche global {platform, jeu} construit a l'ouverture
|
||||
global_search_results = [] # Resultats de la recherche globale inter-plateformes
|
||||
global_search_query = "" # Texte saisi pour la recherche globale
|
||||
global_search_selected = 0 # Index du resultat global selectionne
|
||||
global_search_scroll_offset = 0 # Offset de defilement des resultats globaux
|
||||
global_search_editing = False # True si le clavier virtuel est actif pour la recherche globale
|
||||
|
||||
# Variables pour le filtrage avancé
|
||||
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
|
||||
@@ -448,14 +486,20 @@ 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
|
||||
|
||||
# Variables diverses
|
||||
update_checked = False
|
||||
pending_update_version = ""
|
||||
startup_update_confirmed = False
|
||||
text_file_mode = ""
|
||||
loading_detail_lines = []
|
||||
extension_confirm_selection = 0 # Index de sélection pour confirmation d'extension
|
||||
controls_config = {} # Configuration des contrôles personnalisés
|
||||
selected_key = (0, 0) # Position du curseur dans le clavier virtuel
|
||||
|
||||
@@ -15,12 +15,13 @@ from utils import (
|
||||
load_games, check_extension_before_download, is_extension_supported,
|
||||
load_extensions_json, play_random_music, sanitize_filename,
|
||||
save_music_config, load_api_keys, _get_dest_folder_name,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, toggle_web_service_at_boot, check_web_service_status,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
|
||||
restart_application, generate_support_zip, load_sources,
|
||||
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
|
||||
start_connection_status_check
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
|
||||
move_files_to_directory, parse_torrent_download_url
|
||||
)
|
||||
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,
|
||||
@@ -39,6 +40,35 @@ logger = logging.getLogger(__name__)
|
||||
ARCHIVE_EXTENSIONS = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
|
||||
|
||||
|
||||
def _notify_torrent_in_maintenance(game_name: str | None = None) -> None:
|
||||
try:
|
||||
message = _("popup_torrent_in_maintenance")
|
||||
except Exception:
|
||||
message = "torrent in maintence"
|
||||
|
||||
show_toast(message, 3000)
|
||||
logger.info(f"Source torrent non telechargeable pour le moment: {game_name or 'unknown game'}")
|
||||
|
||||
|
||||
def _has_download_url(url, game_name: str | None = None) -> bool:
|
||||
if isinstance(url, str) and url.strip():
|
||||
if parse_torrent_download_url(url) is not None:
|
||||
_notify_torrent_in_maintenance(game_name)
|
||||
config.needs_redraw = True
|
||||
return False
|
||||
return True
|
||||
|
||||
_notify_torrent_in_maintenance(game_name)
|
||||
config.needs_redraw = True
|
||||
return False
|
||||
|
||||
|
||||
def _wrap_index(current_index: int, delta: int, item_count: int) -> int:
|
||||
if item_count <= 0:
|
||||
return 0
|
||||
return (current_index + delta) % item_count
|
||||
|
||||
|
||||
# Variables globales pour la répétition
|
||||
key_states = {} # Dictionnaire pour suivre l'état des touches
|
||||
|
||||
@@ -72,6 +102,7 @@ VALID_STATES = [
|
||||
"filter_search", # recherche par nom (existant, mais renommé)
|
||||
"filter_advanced", # filtrage avancé par région, etc.
|
||||
"filter_priority_config", # configuration priorité régions pour one-rom-per-game
|
||||
"platform_search", # recherche globale inter-plateformes
|
||||
"platform_folder_config", # configuration du dossier personnalisé pour une plateforme
|
||||
"folder_browser", # navigateur de dossiers intégré
|
||||
"folder_browser_new_folder", # création d'un nouveau dossier
|
||||
@@ -109,6 +140,18 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
|
||||
"delete": {"type": "key", "key": pygame.K_BACKSPACE},
|
||||
"space": {"type": "key", "key": pygame.K_SPACE}
|
||||
}
|
||||
|
||||
def _is_keyboard_only_config(data):
|
||||
if not isinstance(data, dict) or not data:
|
||||
return False
|
||||
for action_name, mapping in data.items():
|
||||
if action_name == "device":
|
||||
continue
|
||||
if not isinstance(mapping, dict):
|
||||
return False
|
||||
if mapping.get("type") != "key":
|
||||
return False
|
||||
return True
|
||||
|
||||
try:
|
||||
# 1) Fichier utilisateur
|
||||
@@ -117,21 +160,25 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
keyboard_mode = (not getattr(config, 'joystick', False)) or getattr(config, 'keyboard', False)
|
||||
if keyboard_mode and not _is_keyboard_only_config(data):
|
||||
logging.getLogger(__name__).info("Configuration utilisateur manette ignorée en mode clavier")
|
||||
else:
|
||||
# Compléter les actions manquantes, et sauve seulement si le fichier utilisateur existe
|
||||
changed = False
|
||||
for k, v in default_config.items():
|
||||
if k not in data:
|
||||
data[k] = v
|
||||
changed = True
|
||||
if changed:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
logging.getLogger(__name__).debug(f"controls.json complété avec les actions manquantes: {path}")
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).warning(f"Impossible d'écrire les actions manquantes dans {path}: {e}")
|
||||
return data
|
||||
changed = False
|
||||
for k, v in default_config.items():
|
||||
if k not in data:
|
||||
data[k] = v
|
||||
changed = True
|
||||
if changed:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
logging.getLogger(__name__).debug(f"controls.json complété avec les actions manquantes: {path}")
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).warning(f"Impossible d'écrire les actions manquantes dans {path}: {e}")
|
||||
return data
|
||||
|
||||
# 2) Préréglages sans copie si aucun fichier utilisateur
|
||||
try:
|
||||
@@ -260,6 +307,67 @@ def is_input_matched(event, action_name):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_global_search_input_matched(event, action_name):
|
||||
"""Fallback robuste pour la recherche globale, independant du preset courant."""
|
||||
if is_input_matched(event, action_name):
|
||||
return True
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
keyboard_fallback = {
|
||||
"up": pygame.K_UP,
|
||||
"down": pygame.K_DOWN,
|
||||
"left": pygame.K_LEFT,
|
||||
"right": pygame.K_RIGHT,
|
||||
"confirm": pygame.K_RETURN,
|
||||
"cancel": pygame.K_ESCAPE,
|
||||
"filter": pygame.K_f,
|
||||
"delete": pygame.K_BACKSPACE,
|
||||
"space": pygame.K_SPACE,
|
||||
"page_up": pygame.K_PAGEUP,
|
||||
"page_down": pygame.K_PAGEDOWN,
|
||||
}
|
||||
if action_name in keyboard_fallback and event.key == keyboard_fallback[action_name]:
|
||||
return True
|
||||
|
||||
if event.type == pygame.JOYBUTTONDOWN:
|
||||
common_button_fallback = {
|
||||
"confirm": {0},
|
||||
"cancel": {1},
|
||||
"filter": {6},
|
||||
"start": {7},
|
||||
"delete": {2},
|
||||
"space": {5},
|
||||
"page_up": {4},
|
||||
"page_down": {5},
|
||||
}
|
||||
if action_name in common_button_fallback and event.button in common_button_fallback[action_name]:
|
||||
return True
|
||||
|
||||
if event.type == pygame.JOYHATMOTION:
|
||||
hat_fallback = {
|
||||
"up": (0, 1),
|
||||
"down": (0, -1),
|
||||
"left": (-1, 0),
|
||||
"right": (1, 0),
|
||||
}
|
||||
if action_name in hat_fallback and event.value == hat_fallback[action_name]:
|
||||
return True
|
||||
|
||||
if event.type == pygame.JOYAXISMOTION:
|
||||
axis_fallback = {
|
||||
"left": (0, -1),
|
||||
"right": (0, 1),
|
||||
"up": (1, -1),
|
||||
"down": (1, 1),
|
||||
}
|
||||
if action_name in axis_fallback:
|
||||
axis_id, direction = axis_fallback[action_name]
|
||||
if event.axis == axis_id and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _launch_next_queued_download():
|
||||
"""Lance le prochain téléchargement de la queue si aucun n'est actif.
|
||||
Gère la liaison entre le système Desktop et le système de download_rom/download_from_1fichier.
|
||||
@@ -340,6 +448,225 @@ def filter_games_by_search_query() -> list[Game]:
|
||||
filtered_games.append(game)
|
||||
|
||||
return filtered_games
|
||||
|
||||
|
||||
GLOBAL_SEARCH_KEYBOARD_LAYOUT = [
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
|
||||
['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'],
|
||||
['W', 'X', 'C', 'V', 'B', 'N']
|
||||
]
|
||||
|
||||
|
||||
def _get_platform_id(platform) -> str:
|
||||
return platform.get("name") if isinstance(platform, dict) else str(platform)
|
||||
|
||||
|
||||
def _get_platform_label(platform_id: str) -> str:
|
||||
return config.platform_names.get(platform_id, platform_id)
|
||||
|
||||
|
||||
def build_global_search_index() -> list[dict]:
|
||||
indexed_games = []
|
||||
for platform_index, platform in enumerate(config.platforms):
|
||||
platform_id = _get_platform_id(platform)
|
||||
platform_label = _get_platform_label(platform_id)
|
||||
for game in load_games(platform_id):
|
||||
display_name = game.display_name or Path(game.name).stem
|
||||
indexed_games.append({
|
||||
"platform_id": platform_id,
|
||||
"platform_label": platform_label,
|
||||
"platform_index": platform_index,
|
||||
"game_name": game.name,
|
||||
"display_name": display_name,
|
||||
"search_name": display_name.lower(),
|
||||
"url": game.url,
|
||||
"size": game.size,
|
||||
})
|
||||
|
||||
indexed_games.sort(key=lambda item: (item["platform_label"].lower(), item["display_name"].lower()))
|
||||
return indexed_games
|
||||
|
||||
|
||||
def refresh_global_search_results(reset_selection: bool = True) -> None:
|
||||
query = (config.global_search_query or "").strip().lower()
|
||||
if not query:
|
||||
config.global_search_results = []
|
||||
else:
|
||||
config.global_search_results = [
|
||||
item for item in config.global_search_index
|
||||
if query in item.get("search_name", item["display_name"].lower())
|
||||
]
|
||||
|
||||
if reset_selection:
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
else:
|
||||
max_index = max(0, len(config.global_search_results) - 1)
|
||||
config.global_search_selected = max(0, min(config.global_search_selected, max_index))
|
||||
config.global_search_scroll_offset = max(0, min(config.global_search_scroll_offset, config.global_search_selected))
|
||||
|
||||
|
||||
def enter_global_search() -> None:
|
||||
index_signature = tuple(config.platforms)
|
||||
if not getattr(config, 'global_search_index', None) or getattr(config, 'global_search_index_signature', None) != index_signature:
|
||||
config.global_search_index = build_global_search_index()
|
||||
config.global_search_index_signature = index_signature
|
||||
config.global_search_query = ""
|
||||
config.global_search_results = []
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
config.global_search_editing = bool(getattr(config, 'joystick', False))
|
||||
config.selected_key = (0, 0)
|
||||
config.previous_menu_state = "platform"
|
||||
config.menu_state = "platform_search"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entree en recherche globale inter-plateformes")
|
||||
|
||||
|
||||
def exit_global_search() -> None:
|
||||
config.global_search_query = ""
|
||||
config.global_search_results = []
|
||||
config.global_search_selected = 0
|
||||
config.global_search_scroll_offset = 0
|
||||
config.global_search_editing = False
|
||||
config.selected_key = (0, 0)
|
||||
config.menu_state = "platform"
|
||||
config.needs_redraw = True
|
||||
|
||||
|
||||
def open_global_search_result(screen) -> None:
|
||||
if not config.global_search_results:
|
||||
return
|
||||
|
||||
result = config.global_search_results[config.global_search_selected]
|
||||
platform_index = result.get("platform_index", 0)
|
||||
if platform_index < 0 or platform_index >= len(config.platforms):
|
||||
return
|
||||
|
||||
config.current_platform = platform_index
|
||||
config.selected_platform = platform_index
|
||||
config.current_page = platform_index // max(1, config.GRID_COLS * config.GRID_ROWS)
|
||||
|
||||
platform_id = result["platform_id"]
|
||||
config.games = load_games(platform_id)
|
||||
config.filtered_games = config.games
|
||||
config.search_mode = False
|
||||
config.search_query = ""
|
||||
config.filter_active = False
|
||||
|
||||
target_name = result["game_name"]
|
||||
target_display_name = result["display_name"]
|
||||
target_index = 0
|
||||
for index, game in enumerate(config.games):
|
||||
if game.name == target_name:
|
||||
target_index = index
|
||||
break
|
||||
if game.display_name == target_display_name:
|
||||
target_index = index
|
||||
|
||||
config.current_game = target_index
|
||||
config.scroll_offset = 0
|
||||
config.global_search_editing = False
|
||||
|
||||
from rgsx_settings import get_light_mode
|
||||
if not get_light_mode():
|
||||
draw_validation_transition(screen, config.current_platform)
|
||||
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Ouverture du resultat global {target_display_name} sur {platform_id}")
|
||||
|
||||
|
||||
def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
if not config.global_search_results:
|
||||
return
|
||||
|
||||
result = config.global_search_results[config.global_search_selected]
|
||||
url = result.get("url")
|
||||
platform = result.get("platform_id")
|
||||
game_name = result.get("game_name")
|
||||
display_name = result.get("display_name") or get_clean_display_name(game_name, platform)
|
||||
|
||||
if not platform or not game_name:
|
||||
logger.error(f"Resultat de recherche globale invalide: {result}")
|
||||
return
|
||||
if not _has_download_url(url, game_name):
|
||||
return
|
||||
|
||||
pending_download = check_extension_before_download(url, platform, game_name)
|
||||
if not pending_download:
|
||||
logger.error(f"config.pending_download est None pour {game_name}")
|
||||
config.needs_redraw = True
|
||||
return
|
||||
|
||||
is_supported = is_extension_supported(
|
||||
sanitize_filename(game_name),
|
||||
platform,
|
||||
load_extensions_json()
|
||||
)
|
||||
zip_ok = bool(pending_download[3])
|
||||
allow_unknown = get_allow_unknown_extensions()
|
||||
|
||||
if (not is_supported and not zip_ok) and not allow_unknown:
|
||||
config.pending_download = pending_download
|
||||
config.pending_download_is_queue = queue_only
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "extension_warning"
|
||||
config.extension_confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Extension non supportee, passage a extension_warning pour {game_name}")
|
||||
return
|
||||
|
||||
if queue_only:
|
||||
task_id = str(pygame.time.get_ticks())
|
||||
queue_item = {
|
||||
'url': url,
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'is_zip_non_supported': pending_download[3],
|
||||
'is_1fichier': is_1fichier_url(url),
|
||||
'task_id': task_id,
|
||||
'status': 'Queued'
|
||||
}
|
||||
config.download_queue.append(queue_item)
|
||||
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': display_name,
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
'message': _("download_queued"),
|
||||
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'downloaded_size': 0,
|
||||
'total_size': 0,
|
||||
'task_id': task_id
|
||||
})
|
||||
save_history(config.history)
|
||||
show_toast(f"{display_name}\n{_('download_queued')}")
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"{game_name} ajoute a la file d'attente depuis la recherche globale. Queue size: {len(config.download_queue)}")
|
||||
|
||||
if not config.download_active and config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
return
|
||||
|
||||
if is_1fichier_url(url):
|
||||
ensure_download_provider_keys(False)
|
||||
if missing_all_provider_keys():
|
||||
logger.warning("Aucune cle API - Mode gratuit 1fichier sera utilise (attente requise)")
|
||||
task_id = str(pygame.time.get_ticks())
|
||||
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, pending_download[3], task_id))
|
||||
else:
|
||||
task_id = str(pygame.time.get_ticks())
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, pending_download[3], task_id))
|
||||
|
||||
config.download_tasks[task_id] = (task, url, game_name, platform)
|
||||
show_toast(f"{_('download_started')}: {display_name}")
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Telechargement demarre depuis la recherche globale: {game_name} pour {platform}, task_id={task_id}")
|
||||
...
|
||||
|
||||
def handle_controls(event, sources, joystick, screen):
|
||||
@@ -504,6 +831,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Ouverture history depuis platform")
|
||||
elif is_input_matched(event, "filter"):
|
||||
enter_global_search()
|
||||
elif is_input_matched(event, "confirm"):
|
||||
# Démarrer le chronomètre pour l'appui long - ne pas exécuter immédiatement
|
||||
# L'action sera exécutée au relâchement si appui court, ou config dossier si appui long
|
||||
@@ -523,6 +852,117 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
|
||||
elif config.menu_state == "platform_search":
|
||||
if getattr(config, 'joystick', False) and getattr(config, 'global_search_editing', False):
|
||||
row, col = config.selected_key
|
||||
max_row = len(GLOBAL_SEARCH_KEYBOARD_LAYOUT) - 1
|
||||
max_col = len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row]) - 1
|
||||
if is_global_search_input_matched(event, "up"):
|
||||
if row == 0:
|
||||
row = max_row + (1 if col <= 5 else 0)
|
||||
if row > 0:
|
||||
config.selected_key = (row - 1, min(col, len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row - 1]) - 1))
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "down"):
|
||||
if (col <= 5 and row == max_row) or (col > 5 and row == max_row - 1):
|
||||
row = -1
|
||||
if row < max_row:
|
||||
config.selected_key = (row + 1, min(col, len(GLOBAL_SEARCH_KEYBOARD_LAYOUT[row + 1]) - 1))
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "left"):
|
||||
if col == 0:
|
||||
col = max_col + 1
|
||||
if col > 0:
|
||||
config.selected_key = (row, col - 1)
|
||||
config.repeat_action = "left"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "right"):
|
||||
if col == max_col:
|
||||
col = -1
|
||||
if col < max_col:
|
||||
config.selected_key = (row, col + 1)
|
||||
config.repeat_action = "right"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "confirm"):
|
||||
config.global_search_query += GLOBAL_SEARCH_KEYBOARD_LAYOUT[row][col]
|
||||
refresh_global_search_results()
|
||||
logger.debug(f"Recherche globale mise a jour: query={config.global_search_query}, resultats={len(config.global_search_results)}")
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "delete"):
|
||||
if config.global_search_query:
|
||||
config.global_search_query = config.global_search_query[:-1]
|
||||
refresh_global_search_results()
|
||||
logger.debug(f"Recherche globale suppression: query={config.global_search_query}, resultats={len(config.global_search_results)}")
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "space"):
|
||||
config.global_search_query += " "
|
||||
refresh_global_search_results()
|
||||
logger.debug(f"Recherche globale espace: query={config.global_search_query}, resultats={len(config.global_search_results)}")
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "filter"):
|
||||
config.global_search_editing = False
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "cancel"):
|
||||
exit_global_search()
|
||||
else:
|
||||
results = config.global_search_results
|
||||
if is_global_search_input_matched(event, "up"):
|
||||
if config.global_search_selected > 0:
|
||||
config.global_search_selected -= 1
|
||||
update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "down"):
|
||||
if config.global_search_selected < len(results) - 1:
|
||||
config.global_search_selected += 1
|
||||
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "page_up") or is_global_search_input_matched(event, "left"):
|
||||
config.global_search_selected = max(0, config.global_search_selected - 10)
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "page_down") or is_global_search_input_matched(event, "right"):
|
||||
config.global_search_selected = min(max(0, len(results) - 1), config.global_search_selected + 10)
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "confirm"):
|
||||
trigger_global_search_download(queue_only=False)
|
||||
elif is_global_search_input_matched(event, "clear_history"):
|
||||
trigger_global_search_download(queue_only=True)
|
||||
elif is_global_search_input_matched(event, "filter") and getattr(config, 'joystick', False):
|
||||
config.global_search_editing = True
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "cancel"):
|
||||
exit_global_search()
|
||||
elif not getattr(config, 'joystick', False) and event.type == pygame.KEYDOWN:
|
||||
if event.unicode.isalnum() or event.unicode == ' ':
|
||||
config.global_search_query += event.unicode
|
||||
refresh_global_search_results()
|
||||
logger.debug(f"Recherche globale clavier: query={config.global_search_query}, resultats={len(config.global_search_results)}")
|
||||
config.needs_redraw = True
|
||||
elif is_global_search_input_matched(event, "delete"):
|
||||
if config.global_search_query:
|
||||
config.global_search_query = config.global_search_query[:-1]
|
||||
refresh_global_search_results()
|
||||
logger.debug(f"Recherche globale clavier suppression: query={config.global_search_query}, resultats={len(config.global_search_results)}")
|
||||
config.needs_redraw = True
|
||||
|
||||
if config.global_search_results:
|
||||
config.global_search_selected = max(0, min(config.global_search_selected, len(config.global_search_results) - 1))
|
||||
else:
|
||||
config.global_search_selected = 0
|
||||
|
||||
# Jeux
|
||||
elif config.menu_state == "game":
|
||||
games: list[Game] = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
@@ -668,16 +1108,16 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
else:
|
||||
if is_input_matched(event, "up"):
|
||||
if config.current_game > 0:
|
||||
config.current_game -= 1
|
||||
if games:
|
||||
config.current_game = _wrap_index(config.current_game, -1, len(games))
|
||||
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_input_matched(event, "down"):
|
||||
if config.current_game < len(games) - 1:
|
||||
config.current_game += 1
|
||||
if games:
|
||||
config.current_game = _wrap_index(config.current_game, 1, len(games))
|
||||
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
|
||||
@@ -719,6 +1159,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")
|
||||
@@ -730,6 +1171,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
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]
|
||||
if not _has_download_url(url, game_name):
|
||||
return action
|
||||
|
||||
pending_download = check_extension_before_download(url, platform, game_name)
|
||||
if pending_download:
|
||||
@@ -768,6 +1211,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -840,6 +1284,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,
|
||||
@@ -908,8 +1353,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
history = config.history
|
||||
if is_input_matched(event, "up"):
|
||||
# L'historique est inversé à l'affichage, donc UP descend dans l'index (incrément)
|
||||
if config.current_history_item < len(history) - 1:
|
||||
config.current_history_item += 1
|
||||
if history:
|
||||
config.current_history_item = _wrap_index(config.current_history_item, 1, len(history))
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
@@ -917,8 +1362,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
# L'historique est inversé à l'affichage, donc DOWN monte dans l'index (décrement)
|
||||
if config.current_history_item > 0:
|
||||
config.current_history_item -= 1
|
||||
if history:
|
||||
config.current_history_item = _wrap_index(config.current_history_item, -1, len(history))
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
@@ -1080,6 +1525,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
|
||||
@@ -1300,7 +1752,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "history"
|
||||
# Réinitialiser l'entrée et relancer
|
||||
url = entry.get("url")
|
||||
if url:
|
||||
if _has_download_url(url, game_name):
|
||||
# Mettre à jour le statut
|
||||
entry["status"] = "Downloading"
|
||||
entry["progress"] = 0
|
||||
@@ -1314,6 +1766,9 @@ def handle_controls(event, sources, joystick, screen):
|
||||
is_zip_non_supported = pending_download[3] if len(pending_download) > 3 else False
|
||||
|
||||
if is_1fichier_url(url):
|
||||
ensure_download_provider_keys(False)
|
||||
if missing_all_provider_keys():
|
||||
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
|
||||
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
|
||||
else:
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
|
||||
@@ -1339,7 +1794,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
|
||||
|
||||
@@ -1369,60 +1864,6 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
|
||||
# Affichage détails erreur
|
||||
# Visualiseur de fichiers texte
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
if content:
|
||||
lines = content.split('\n')
|
||||
line_height = config.small_font.get_height() + 2
|
||||
|
||||
# Calculer le nombre de lignes visibles (approximation)
|
||||
controls_y = config.screen_height - int(config.screen_height * 0.037)
|
||||
margin = 40
|
||||
header_height = 60
|
||||
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
|
||||
visible_lines = int(content_area_height / line_height)
|
||||
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
max_scroll = max(0, len(lines) - visible_lines)
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - 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_input_matched(event, "down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 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_input_matched(event, "page_up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
|
||||
update_key_state("page_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_input_matched(event, "page_down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
|
||||
update_key_state("page_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_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Si pas de contenu, retourner au menu précédent
|
||||
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
|
||||
# Visualiseur de fichiers texte
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
@@ -1453,6 +1894,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
max_scroll = max(0, len(wrapped_lines) - visible_lines)
|
||||
viewer_mode = getattr(config, 'text_file_mode', '')
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - 1)
|
||||
@@ -1482,7 +1924,11 @@ def handle_controls(event, sources, joystick, screen):
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
elif viewer_mode == "ota_update" and is_input_matched(event, "confirm"):
|
||||
config.startup_update_confirmed = True
|
||||
config.menu_state = "loading"
|
||||
config.needs_redraw = True
|
||||
elif viewer_mode != "ota_update" and (is_input_matched(event, "cancel") or is_input_matched(event, "confirm")):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
@@ -1809,11 +2255,27 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Sous-menu Display
|
||||
elif config.menu_state == "pause_display_menu":
|
||||
sel = getattr(config, 'pause_display_selection', 0)
|
||||
# layout, font submenu, family, [monitor if multi], light, unknown, back
|
||||
# layout, font submenu, family, [monitor if multi], [display mode on Windows], light, unknown, back
|
||||
from rgsx_settings import get_available_monitors
|
||||
monitors = get_available_monitors()
|
||||
show_monitor = len(monitors) > 1
|
||||
total = 7 if show_monitor else 6 # dynamic total based on monitor count
|
||||
show_display_mode = getattr(config, 'OPERATING_SYSTEM', '') == "Windows"
|
||||
|
||||
monitor_index = 3 if show_monitor else None
|
||||
display_mode_index = 4 if show_monitor else 3
|
||||
if not show_display_mode:
|
||||
display_mode_index = None
|
||||
|
||||
next_index = 3
|
||||
if show_monitor:
|
||||
next_index += 1
|
||||
if show_display_mode:
|
||||
next_index += 1
|
||||
|
||||
light_index = next_index
|
||||
unknown_index = light_index + 1
|
||||
back_index = unknown_index + 1
|
||||
total = back_index + 1
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_display_selection = (sel - 1) % total
|
||||
config.needs_redraw = True
|
||||
@@ -1871,8 +2333,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement font family: {e}")
|
||||
# 3 monitor selection (only if multiple monitors)
|
||||
elif sel == 3 and show_monitor and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# Monitor selection (only if multiple monitors)
|
||||
elif monitor_index is not None and sel == monitor_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_display_monitor, set_display_monitor
|
||||
current = get_display_monitor()
|
||||
@@ -1883,8 +2345,19 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement moniteur: {e}")
|
||||
# light mode toggle (index 4 if show_monitor, else 3)
|
||||
elif ((sel == 4 and show_monitor) or (sel == 3 and not show_monitor)) and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# Display mode toggle (Windows only)
|
||||
elif display_mode_index is not None and sel == display_mode_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_display_fullscreen, set_display_fullscreen
|
||||
current = get_display_fullscreen()
|
||||
set_display_fullscreen(not current)
|
||||
config.popup_message = _("display_mode_restart_required") if _ else "Restart required to apply screen mode"
|
||||
config.popup_timer = 3000
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle fullscreen/windowed: {e}")
|
||||
# Light mode toggle
|
||||
elif sel == light_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_light_mode, set_light_mode
|
||||
current = get_light_mode()
|
||||
@@ -1894,8 +2367,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle light mode: {e}")
|
||||
# allow unknown extensions (index 5 if show_monitor, else 4)
|
||||
elif ((sel == 5 and show_monitor) or (sel == 4 and not show_monitor)) and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# Allow unknown extensions
|
||||
elif sel == unknown_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
current = get_allow_unknown_extensions()
|
||||
new_val = set_allow_unknown_extensions(not current)
|
||||
@@ -1904,8 +2377,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
|
||||
# back (index 6 if show_monitor, else 5)
|
||||
elif ((sel == 6 and show_monitor) or (sel == 5 and not show_monitor)) and is_input_matched(event, "confirm"):
|
||||
# Back
|
||||
elif sel == back_index and is_input_matched(event, "confirm"):
|
||||
config.menu_state = "pause_menu"
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
@@ -2006,7 +2479,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Sous-menu Games
|
||||
elif config.menu_state == "pause_games_menu":
|
||||
sel = getattr(config, 'pause_games_selection', 0)
|
||||
total = 7 # update cache, history, source, unsupported, hide premium, filter, back
|
||||
total = 8 # update cache, scan roms, history, source, unsupported, hide premium, filter, back
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_games_selection = (sel - 1) % total
|
||||
config.needs_redraw = True
|
||||
@@ -2019,14 +2492,25 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "reload_games_data"
|
||||
config.redownload_confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
elif sel == 1 and is_input_matched(event, "confirm"): # history
|
||||
elif sel == 1 and is_input_matched(event, "confirm"): # scan local roms
|
||||
try:
|
||||
added_games, scanned_platforms = scan_roms_for_downloaded_games()
|
||||
config.popup_message = _("popup_scan_owned_roms_done").format(added_games, scanned_platforms) if _ else f"ROM scan complete: {added_games} games added across {scanned_platforms} platforms"
|
||||
config.popup_timer = 4000
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur scan ROMs locaux: {e}")
|
||||
config.popup_message = _("popup_scan_owned_roms_error").format(str(e)) if _ else f"ROM scan error: {e}"
|
||||
config.popup_timer = 5000
|
||||
config.needs_redraw = True
|
||||
elif sel == 2 and is_input_matched(event, "confirm"): # history
|
||||
config.history = load_history()
|
||||
config.current_history_item = 0
|
||||
config.history_scroll_offset = 0
|
||||
config.previous_menu_state = "pause_games_menu"
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
elif sel == 2 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
|
||||
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
|
||||
try:
|
||||
current_mode = get_sources_mode()
|
||||
new_mode = set_sources_mode('custom' if current_mode == 'rgsx' else 'rgsx')
|
||||
@@ -2041,7 +2525,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
logger.info(f"Changement du mode des sources vers {new_mode}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement mode sources: {e}")
|
||||
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
|
||||
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
|
||||
try:
|
||||
current = get_show_unsupported_platforms()
|
||||
new_val = set_show_unsupported_platforms(not current)
|
||||
@@ -2051,7 +2535,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle unsupported: {e}")
|
||||
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
|
||||
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
|
||||
try:
|
||||
cur = get_hide_premium_systems()
|
||||
new_val = set_hide_premium_systems(not cur)
|
||||
@@ -2060,13 +2544,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
|
||||
@@ -2502,6 +2986,25 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.folder_browser_selection >= config.folder_browser_scroll_offset + config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
jump_size = 10
|
||||
if config.folder_browser_selection > 0:
|
||||
config.folder_browser_selection = max(0, config.folder_browser_selection - jump_size)
|
||||
config.folder_browser_scroll_offset = min(
|
||||
config.folder_browser_scroll_offset,
|
||||
config.folder_browser_selection
|
||||
)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
jump_size = 10
|
||||
if config.folder_browser_selection < len(config.folder_browser_items) - 1:
|
||||
config.folder_browser_selection = min(
|
||||
len(config.folder_browser_items) - 1,
|
||||
config.folder_browser_selection + jump_size
|
||||
)
|
||||
if config.folder_browser_selection >= config.folder_browser_scroll_offset + config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.folder_browser_items:
|
||||
selected_item = config.folder_browser_items[config.folder_browser_selection]
|
||||
@@ -2555,6 +3058,34 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Informer qu'un redémarrage est nécessaire
|
||||
config.popup_message = _("roms_folder_set_restart").format(selected_path) if _ else f"ROMs folder set: {selected_path}\nRestart required!"
|
||||
config.menu_state = "pause_settings_menu"
|
||||
elif browser_mode == "history_move":
|
||||
entry = config.history[config.current_history_item] if config.history and config.current_history_item < len(config.history) else None
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
if not actual_matches and entry:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
|
||||
source_paths = [match_path for _, match_path in actual_matches]
|
||||
if not source_paths:
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
if actual_path:
|
||||
source_paths = [actual_path]
|
||||
|
||||
success, moved_matches, error_message = move_files_to_directory(source_paths, selected_path)
|
||||
if success:
|
||||
config.history_actual_matches = moved_matches
|
||||
if moved_matches:
|
||||
config.history_actual_filename, config.history_actual_path = moved_matches[0]
|
||||
if entry is not None:
|
||||
entry["moved_paths"] = [path for _, path in moved_matches]
|
||||
save_history(config.history)
|
||||
config.popup_message = _("history_move_success").format(len(moved_matches), selected_path) if _ else f"Moved {len(moved_matches)} file(s) to {selected_path}"
|
||||
config.popup_timer = 3000
|
||||
logger.info(f"Déplacement historique terminé vers {selected_path}: {len(moved_matches)} fichier(s)")
|
||||
else:
|
||||
config.popup_message = _("history_move_error").format(error_message) if _ else f"Move error: {error_message}"
|
||||
config.popup_timer = 4000
|
||||
logger.error(f"Erreur déplacement historique vers {selected_path}: {error_message}")
|
||||
config.menu_state = "history_show_folder"
|
||||
else:
|
||||
# Mode dossier plateforme
|
||||
from rgsx_settings import set_platform_custom_path
|
||||
@@ -2570,6 +3101,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
|
||||
@@ -3189,6 +3722,10 @@ def handle_controls(event, sources, joystick, screen):
|
||||
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}")
|
||||
if not _has_download_url(url, game_name):
|
||||
config.confirm_press_start_time = 0
|
||||
config.confirm_long_press_triggered = False
|
||||
return action
|
||||
|
||||
# Vérifier d'abord l'extension avant d'ajouter à l'historique
|
||||
if is_1fichier_url(url):
|
||||
@@ -3321,6 +3858,10 @@ def handle_controls(event, sources, joystick, screen):
|
||||
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}")
|
||||
if not _has_download_url(url, game_name):
|
||||
config.confirm_press_start_time = 0
|
||||
config.confirm_long_press_triggered = False
|
||||
return action
|
||||
|
||||
# Vérifier d'abord l'extension avant d'ajouter à l'historique
|
||||
if is_1fichier_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
@@ -191,11 +191,31 @@ class GameFilters:
|
||||
base = base + disc_info
|
||||
|
||||
return base
|
||||
|
||||
@staticmethod
|
||||
def get_cached_regions(game: Game) -> List[str]:
|
||||
"""Retourne les régions en les calculant une seule fois par jeu."""
|
||||
if game.regions is None:
|
||||
game.regions = GameFilters.get_game_regions(game.display_name)
|
||||
return game.regions
|
||||
|
||||
@staticmethod
|
||||
def get_cached_non_release(game: Game) -> bool:
|
||||
"""Retourne le flag non-release en le calculant à la demande."""
|
||||
if game.is_non_release is None:
|
||||
game.is_non_release = GameFilters.is_non_release_game(game.display_name)
|
||||
return game.is_non_release
|
||||
|
||||
@staticmethod
|
||||
def get_cached_base_name(game: Game) -> str:
|
||||
"""Retourne le nom de base en le calculant une seule fois par jeu."""
|
||||
if game.base_name is None:
|
||||
game.base_name = GameFilters.get_base_game_name(game.display_name)
|
||||
return game.base_name
|
||||
|
||||
def get_region_priority(self, game_name: str) -> int:
|
||||
def get_region_priority(self, game: Game) -> int:
|
||||
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
|
||||
# Utiliser la fonction de détection de régions pour être cohérent
|
||||
game_regions = self.get_game_regions(game_name)
|
||||
game_regions = self.get_cached_regions(game)
|
||||
|
||||
# Trouver la meilleure priorité parmi toutes les régions détectées
|
||||
best_priority = len(self.region_priority) # Par défaut: priorité la plus basse
|
||||
@@ -221,14 +241,13 @@ class GameFilters:
|
||||
return games
|
||||
|
||||
filtered_games = []
|
||||
has_region_excludes = any(state == 'exclude' for state in self.region_filters.values())
|
||||
|
||||
# Filtrage par région
|
||||
for game in games:
|
||||
game_name = game.display_name
|
||||
|
||||
# Vérifier les filtres de région
|
||||
if self.region_filters:
|
||||
game_regions = self.get_game_regions(game_name)
|
||||
if has_region_excludes:
|
||||
game_regions = self.get_cached_regions(game)
|
||||
|
||||
# Vérifier si le jeu a au moins une région incluse
|
||||
has_included_region = False
|
||||
@@ -244,7 +263,7 @@ class GameFilters:
|
||||
continue
|
||||
|
||||
# Filtrer les non-release
|
||||
if self.hide_non_release and self.is_non_release_game(game_name):
|
||||
if self.hide_non_release and self.get_cached_non_release(game):
|
||||
continue
|
||||
|
||||
filtered_games.append(game)
|
||||
@@ -260,8 +279,7 @@ class GameFilters:
|
||||
games_by_base = {}
|
||||
|
||||
for game in games:
|
||||
game_name = game.display_name
|
||||
base_name = self.get_base_game_name(game_name)
|
||||
base_name = self.get_cached_base_name(game)
|
||||
|
||||
if base_name not in games_by_base:
|
||||
games_by_base[base_name] = []
|
||||
@@ -276,7 +294,7 @@ class GameFilters:
|
||||
else:
|
||||
# Trier par priorité de région
|
||||
sorted_games = sorted(game_list,
|
||||
key=lambda g: self.get_region_priority(g.display_name))
|
||||
key=self.get_region_priority)
|
||||
result.append(sorted_games[0])
|
||||
|
||||
return result
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Keine Internetverbindung. Überprüfe dein Netzwerk.",
|
||||
"error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben",
|
||||
"error_invalid_download_data": "Ungültige Downloaddaten",
|
||||
"popup_torrent_in_maintenance": "Torrent in Wartung, bitte warten",
|
||||
"error_delete_sources": "Fehler beim Löschen der Datei systems_list.json oder Ordner",
|
||||
"platform_no_platform": "Keine Plattform",
|
||||
"platform_page": "Seite {0}/{1}",
|
||||
@@ -24,7 +25,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",
|
||||
@@ -42,6 +47,9 @@
|
||||
"free_mode_submitting": "[Kostenloser Modus] Formular wird gesendet...",
|
||||
"free_mode_link_found": "[Kostenloser Modus] Link gefunden: {0}...",
|
||||
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: Der kostenlose Gast-Download ist vorübergehend nicht verfügbar (alle Slots sind belegt). Bitte versuchen Sie es später erneut.",
|
||||
"free_mode_unavailable_in_app": "1fichier: Dieser Download ist derzeit in der Anwendung nicht verfügbar. Bitte versuchen Sie es später erneut.",
|
||||
"free_mode_premium_advice": "Für unbegrenzte Downloads jederzeit und mit voller Geschwindigkeit benötigen Sie ein Premium-Konto oder einen Debrid-Dienst und müssen dessen API-Schlüssel in RGSX eintragen.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download vom Benutzer abgebrochen.",
|
||||
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
|
||||
@@ -111,6 +119,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 +153,8 @@
|
||||
"controls_confirm_select": "Bestätigen/Auswählen",
|
||||
"controls_cancel_back": "Abbrechen/Zurück",
|
||||
"controls_filter_search": "Filtern/Suchen",
|
||||
"controls_action_edit_search": "Suche bearbeiten",
|
||||
"controls_action_show_results": "Ergebnisse zeigen",
|
||||
"network_download_failed": "Download nach {0} Versuchen fehlgeschlagen",
|
||||
"network_api_error": "Fehler bei der API-Anfrage, der Schlüssel könnte falsch sein: {0}",
|
||||
"network_download_error": "Downloadfehler {0}: {1}",
|
||||
@@ -232,6 +243,7 @@
|
||||
"instruction_games_history": "Vergangene Downloads und Status anzeigen",
|
||||
"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln",
|
||||
"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren",
|
||||
"instruction_games_scan_owned": "ROM-Ordner scannen und bereits vorhandene Spiele markieren",
|
||||
"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren",
|
||||
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
|
||||
"instruction_settings_auto_extract": "Automatische Archivextraktion nach Download aktivieren/deaktivieren",
|
||||
@@ -258,6 +270,9 @@
|
||||
"settings_web_service_success_disabled": "Web-Dienst beim Booten deaktiviert",
|
||||
"settings_web_service_error": "Fehler: {0}",
|
||||
"settings_custom_dns": "Custom DNS beim Booten",
|
||||
"menu_scan_owned_roms": "Vorhandene ROMs scannen",
|
||||
"popup_scan_owned_roms_done": "ROM-Scan abgeschlossen: {0} Spiele auf {1} Plattformen hinzugefügt",
|
||||
"popup_scan_owned_roms_error": "ROM-Scan-Fehler: {0}",
|
||||
"settings_custom_dns_enabled": "Aktiviert",
|
||||
"settings_custom_dns_disabled": "Deaktiviert",
|
||||
"settings_custom_dns_enabling": "Custom DNS wird aktiviert...",
|
||||
@@ -296,6 +311,7 @@
|
||||
"history_option_delete_game": "Spiel löschen",
|
||||
"history_option_error_info": "Fehlerdetails",
|
||||
"history_option_retry": "Download wiederholen",
|
||||
"history_move_action": "Verschieben",
|
||||
"history_option_back": "Zurück",
|
||||
"history_folder_path_label": "Zielpfad:",
|
||||
"history_scraper_not_implemented": "Scraper noch nicht implementiert",
|
||||
@@ -305,6 +321,8 @@
|
||||
"history_extracted": "Extrahiert",
|
||||
"history_delete_success": "Spiel erfolgreich gelöscht",
|
||||
"history_delete_error": "Fehler beim Löschen des Spiels: {0}",
|
||||
"history_move_success": "{0} Datei(en) verschoben nach: {1}",
|
||||
"history_move_error": "Fehler beim Verschieben: {0}",
|
||||
"history_error_details_title": "Fehlerdetails",
|
||||
"history_no_error_message": "Keine Fehlermeldung verfügbar",
|
||||
"web_title": "RGSX Web-Oberfläche",
|
||||
@@ -469,6 +487,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",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "No Internet connection. Check your network.",
|
||||
"error_api_key": "Please enter your API key (premium only) in the file {0}",
|
||||
"error_invalid_download_data": "Invalid download data",
|
||||
"popup_torrent_in_maintenance": "Torrent under maintenance, please wait",
|
||||
"error_delete_sources": "Error deleting systems_list.json file or folders",
|
||||
"platform_no_platform": "No platform",
|
||||
"platform_page": "Page {0}/{1}",
|
||||
@@ -24,7 +25,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",
|
||||
@@ -42,6 +47,9 @@
|
||||
"free_mode_submitting": "[Free mode] Submitting form...",
|
||||
"free_mode_link_found": "[Free mode] Link found: {0}...",
|
||||
"free_mode_completed": "[Free mode] Completed: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: free guest download is temporarily unavailable (all slots are currently in use). Please try again later.",
|
||||
"free_mode_unavailable_in_app": "1fichier: this download is not available in the application right now. Please try again later.",
|
||||
"free_mode_premium_advice": "For unlimited, on-demand, full-speed downloads, you need a premium account or debrid service and must enter its API key in RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download canceled by user.",
|
||||
"download_removed_from_queue": "Removed from download queue",
|
||||
@@ -112,6 +120,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 +174,8 @@
|
||||
"controls_confirm_select": "Confirm/Select",
|
||||
"controls_cancel_back": "Cancel/Back",
|
||||
"controls_filter_search": "Filter/Search",
|
||||
"controls_action_edit_search": "Edit search",
|
||||
"controls_action_show_results": "Show results",
|
||||
"symlink_option_enabled": "Symlink option enabled",
|
||||
"symlink_option_disabled": "Symlink option disabled",
|
||||
"menu_games_source_prefix": "Game source",
|
||||
@@ -234,6 +245,7 @@
|
||||
"instruction_games_history": "List past downloads and statuses",
|
||||
"instruction_games_source_mode": "Switch between RGSX or your own custom list source",
|
||||
"instruction_games_update_cache": "Redownload & refresh current games list",
|
||||
"instruction_games_scan_owned": "Scan your ROM folders and mark matching games as already owned",
|
||||
"instruction_settings_music": "Enable or disable background music playback",
|
||||
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
|
||||
"instruction_settings_auto_extract": "Toggle automatic archive extraction after download",
|
||||
@@ -298,6 +310,10 @@
|
||||
"history_option_delete_game": "Delete game",
|
||||
"history_option_error_info": "Error details",
|
||||
"history_option_retry": "Retry download",
|
||||
"history_move_action": "Move",
|
||||
"menu_scan_owned_roms": "Scan owned ROMs",
|
||||
"popup_scan_owned_roms_done": "ROM scan complete: {0} games added across {1} platforms",
|
||||
"popup_scan_owned_roms_error": "ROM scan error: {0}",
|
||||
"history_option_back": "Back",
|
||||
"history_folder_path_label": "Destination path:",
|
||||
"history_scraper_not_implemented": "Scraper not yet implemented",
|
||||
@@ -307,6 +323,8 @@
|
||||
"history_extracted": "Extracted",
|
||||
"history_delete_success": "Game deleted successfully",
|
||||
"history_delete_error": "Error deleting game: {0}",
|
||||
"history_move_success": "Moved {0} file(s) to: {1}",
|
||||
"history_move_error": "Error while moving files: {0}",
|
||||
"history_error_details_title": "Error Details",
|
||||
"history_no_error_message": "No error message available",
|
||||
"web_title": "RGSX Web Interface",
|
||||
@@ -469,6 +487,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",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Sin conexión a Internet. Verifica tu red.",
|
||||
"error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}",
|
||||
"error_invalid_download_data": "Datos de descarga no válidos",
|
||||
"popup_torrent_in_maintenance": "Torrent en mantenimiento, por favor espere",
|
||||
"error_delete_sources": "Error al eliminar el archivo systems_list.json o carpetas",
|
||||
"platform_no_platform": "Ninguna plataforma",
|
||||
"platform_page": "Página {0}/{1}",
|
||||
@@ -24,7 +25,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",
|
||||
@@ -42,6 +47,9 @@
|
||||
"free_mode_submitting": "[Modo gratuito] Enviando formulario...",
|
||||
"free_mode_link_found": "[Modo gratuito] Enlace encontrado: {0}...",
|
||||
"free_mode_completed": "[Modo gratuito] Completado: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: la descarga gratuita como invitado no está disponible temporalmente (todos los cupos están ocupados). Inténtelo de nuevo más tarde.",
|
||||
"free_mode_unavailable_in_app": "1fichier: esta descarga no está disponible en la aplicación en este momento. Inténtelo de nuevo más tarde.",
|
||||
"free_mode_premium_advice": "Para descargar de forma ilimitada, cuando quiera y a máxima velocidad, necesita una cuenta premium o un desbridizador y debe introducir su clave API en RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Descarga cancelada por el usuario.",
|
||||
"download_removed_from_queue": "Eliminado de la cola de descarga",
|
||||
@@ -109,6 +117,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 +151,8 @@
|
||||
"controls_confirm_select": "Confirmar/Seleccionar",
|
||||
"controls_cancel_back": "Cancelar/Volver",
|
||||
"controls_filter_search": "Filtrar/Buscar",
|
||||
"controls_action_edit_search": "Editar busqueda",
|
||||
"controls_action_show_results": "Ver resultados",
|
||||
"network_download_failed": "Error en la descarga tras {0} intentos",
|
||||
"network_api_error": "Error en la solicitud de API, la clave puede ser incorrecta: {0}",
|
||||
"network_download_error": "Error en la descarga {0}: {1}",
|
||||
@@ -232,6 +243,7 @@
|
||||
"instruction_games_history": "Ver descargas pasadas y su estado",
|
||||
"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada",
|
||||
"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos",
|
||||
"instruction_games_scan_owned": "Escanear las carpetas ROMs y marcar los juegos que ya posees",
|
||||
"instruction_settings_music": "Activar o desactivar música de fondo",
|
||||
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
|
||||
"instruction_settings_auto_extract": "Activar/desactivar extracción automática de archivos después de descargar",
|
||||
@@ -258,6 +270,9 @@
|
||||
"settings_web_service_success_disabled": "Servicio web desactivado al inicio",
|
||||
"settings_web_service_error": "Error: {0}",
|
||||
"settings_custom_dns": "DNS Personalizado al Inicio",
|
||||
"menu_scan_owned_roms": "Escanear ROMs disponibles",
|
||||
"popup_scan_owned_roms_done": "Escaneo ROM completado: {0} juegos añadidos en {1} plataformas",
|
||||
"popup_scan_owned_roms_error": "Error al escanear ROMs: {0}",
|
||||
"settings_custom_dns_enabled": "Activado",
|
||||
"settings_custom_dns_disabled": "Desactivado",
|
||||
"settings_custom_dns_enabling": "Activando DNS personalizado...",
|
||||
@@ -296,6 +311,7 @@
|
||||
"history_option_delete_game": "Eliminar juego",
|
||||
"history_option_error_info": "Detalles del error",
|
||||
"history_option_retry": "Reintentar descarga",
|
||||
"history_move_action": "Mover",
|
||||
"history_option_back": "Volver",
|
||||
"history_folder_path_label": "Ruta de destino:",
|
||||
"history_scraper_not_implemented": "Scraper aún no implementado",
|
||||
@@ -305,6 +321,8 @@
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Juego eliminado con éxito",
|
||||
"history_delete_error": "Error al eliminar juego: {0}",
|
||||
"history_move_success": "{0} archivo(s) movido(s) a: {1}",
|
||||
"history_move_error": "Error al mover los archivos: {0}",
|
||||
"history_error_details_title": "Detalles del error",
|
||||
"history_no_error_message": "No hay mensaje de error disponible",
|
||||
"web_title": "Interfaz Web RGSX",
|
||||
@@ -467,6 +485,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",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Pas de connexion Internet. Vérifiez votre réseau.",
|
||||
"error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}",
|
||||
"error_invalid_download_data": "Données de téléchargement invalides",
|
||||
"popup_torrent_in_maintenance": "Torrent en maintenance, veuillez patienter",
|
||||
"error_delete_sources": "Erreur lors de la suppression du fichier systems_list.json ou dossiers",
|
||||
"platform_no_platform": "Aucune plateforme",
|
||||
"platform_page": "Page {0}/{1}",
|
||||
@@ -24,7 +25,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",
|
||||
@@ -42,6 +47,9 @@
|
||||
"free_mode_submitting": "[Mode gratuit] Soumission formulaire...",
|
||||
"free_mode_link_found": "[Mode gratuit] Lien trouvé: {0}...",
|
||||
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier : le téléchargement gratuit invité est temporairement indisponible (tous les créneaux sont occupés). Réessayez plus tard.",
|
||||
"free_mode_unavailable_in_app": "1fichier : ce téléchargement n'est pas disponible dans l'application pour le moment. Réessayez plus tard.",
|
||||
"free_mode_premium_advice": "Pour télécharger de manière illimitée, quand vous voulez et à pleine vitesse, vous devez obtenir un compte premium ou un débrideur et entrer votre clé API dans RGSX.",
|
||||
"download_status": "{0} : {1}",
|
||||
"download_canceled": "Téléchargement annulé par l'utilisateur.",
|
||||
"download_removed_from_queue": "Retiré de la file de téléchargement",
|
||||
@@ -106,6 +114,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 +147,8 @@
|
||||
"controls_confirm_select": "Confirmer/Sélectionner",
|
||||
"controls_cancel_back": "Annuler/Retour",
|
||||
"controls_filter_search": "Filtrer/Rechercher",
|
||||
"controls_action_edit_search": "Modifier recherche",
|
||||
"controls_action_show_results": "Voir resultats",
|
||||
"network_download_failed": "Échec du téléchargement après {0} tentatives",
|
||||
"network_api_error": "Erreur lors de la requête API, la clé est peut-être incorrecte: {0}",
|
||||
"network_download_error": "Erreur téléchargement {0}: {1}",
|
||||
@@ -234,6 +245,7 @@
|
||||
"instruction_games_history": "Lister les téléchargements passés et leur statut",
|
||||
"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée",
|
||||
"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux",
|
||||
"instruction_games_scan_owned": "Scanner les dossiers ROMs et marquer les jeux déjà possédés",
|
||||
"instruction_settings_music": "Activer ou désactiver la lecture musicale",
|
||||
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
|
||||
"instruction_settings_auto_extract": "Activer/désactiver l'extraction automatique des archives après téléchargement",
|
||||
@@ -298,6 +310,10 @@
|
||||
"history_option_delete_game": "Supprimer le jeu",
|
||||
"history_option_error_info": "Détails de l'erreur",
|
||||
"history_option_retry": "Retenter le téléchargement",
|
||||
"history_move_action": "Déplacer",
|
||||
"menu_scan_owned_roms": "Scanner les ROMs présentes",
|
||||
"popup_scan_owned_roms_done": "Scan ROMs terminé : {0} jeux ajoutés sur {1} plateformes",
|
||||
"popup_scan_owned_roms_error": "Erreur scan ROMs : {0}",
|
||||
"history_option_back": "Retour",
|
||||
"history_folder_path_label": "Chemin de destination :",
|
||||
"history_scraper_not_implemented": "Scraper pas encore implémenté",
|
||||
@@ -307,6 +323,8 @@
|
||||
"history_extracted": "Extrait",
|
||||
"history_delete_success": "Jeu supprimé avec succès",
|
||||
"history_delete_error": "Erreur lors de la suppression du jeu : {0}",
|
||||
"history_move_success": "{0} fichier(s) déplacé(s) vers : {1}",
|
||||
"history_move_error": "Erreur lors du déplacement : {0}",
|
||||
"history_error_details_title": "Détails de l'erreur",
|
||||
"history_no_error_message": "Aucun message d'erreur disponible",
|
||||
"web_title": "Interface Web RGSX",
|
||||
@@ -469,6 +487,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",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Nessuna connessione Internet. Controlla la rete.",
|
||||
"error_api_key": "Inserisci la tua API key (solo premium) nel file {0}",
|
||||
"error_invalid_download_data": "Dati di download non validi",
|
||||
"popup_torrent_in_maintenance": "Torrent in manutenzione, attendere prego",
|
||||
"error_delete_sources": "Errore nell'eliminazione del file systems_list.json o delle cartelle",
|
||||
"platform_no_platform": "Nessuna piattaforma",
|
||||
"platform_page": "Pagina {0}/{1}",
|
||||
@@ -24,7 +25,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",
|
||||
@@ -42,6 +47,9 @@
|
||||
"free_mode_submitting": "[Modalità gratuita] Invio modulo...",
|
||||
"free_mode_link_found": "[Modalità gratuita] Link trovato: {0}...",
|
||||
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: il download gratuito come ospite non è temporaneamente disponibile (tutti gli slot sono occupati). Riprova più tardi.",
|
||||
"free_mode_unavailable_in_app": "1fichier: questo download non è disponibile nell'applicazione in questo momento. Riprova più tardi.",
|
||||
"free_mode_premium_advice": "Per scaricare senza limiti, quando vuoi e alla massima velocità, hai bisogno di un account premium o di un servizio debrid e devi inserire la sua chiave API in RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download annullato dall'utente.",
|
||||
"download_removed_from_queue": "Rimosso dalla coda di download",
|
||||
@@ -107,6 +115,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 +173,8 @@
|
||||
"controls_confirm_select": "Conferma/Seleziona",
|
||||
"controls_cancel_back": "Annulla/Indietro",
|
||||
"controls_filter_search": "Filtro/Ricerca",
|
||||
"controls_action_edit_search": "Modifica ricerca",
|
||||
"controls_action_show_results": "Mostra risultati",
|
||||
"games_source_rgsx": "RGSX",
|
||||
"sources_mode_rgsx_select_info": "RGSX: aggiorna l'elenco dei giochi",
|
||||
"games_source_custom": "Personalizzato",
|
||||
@@ -227,6 +238,7 @@
|
||||
"instruction_games_history": "Elencare download passati e stato",
|
||||
"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata",
|
||||
"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi",
|
||||
"instruction_games_scan_owned": "Scansiona le cartelle ROMs e segna i giochi gia posseduti",
|
||||
"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo",
|
||||
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
|
||||
"instruction_settings_auto_extract": "Attivare/disattivare estrazione automatica archivi dopo il download",
|
||||
@@ -258,6 +270,9 @@
|
||||
"settings_custom_dns_enabling": "Abilitazione DNS personalizzato...",
|
||||
"settings_custom_dns_disabling": "Disabilitazione DNS personalizzato...",
|
||||
"settings_custom_dns_success_enabled": "DNS personalizzato abilitato all'avvio (1.1.1.1)",
|
||||
"menu_scan_owned_roms": "Scansiona ROM presenti",
|
||||
"popup_scan_owned_roms_done": "Scansione ROM completata: {0} giochi aggiunti su {1} piattaforme",
|
||||
"popup_scan_owned_roms_error": "Errore scansione ROM: {0}",
|
||||
"settings_custom_dns_success_disabled": "DNS personalizzato disabilitato all'avvio",
|
||||
"controls_desc_confirm": "Confermare (es. A/Croce)",
|
||||
"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)",
|
||||
@@ -291,6 +306,7 @@
|
||||
"history_option_delete_game": "Elimina gioco",
|
||||
"history_option_error_info": "Dettagli errore",
|
||||
"history_option_retry": "Riprova download",
|
||||
"history_move_action": "Sposta",
|
||||
"history_option_back": "Indietro",
|
||||
"history_folder_path_label": "Percorso destinazione:",
|
||||
"history_scraper_not_implemented": "Scraper non ancora implementato",
|
||||
@@ -300,6 +316,8 @@
|
||||
"history_extracted": "Estratto",
|
||||
"history_delete_success": "Gioco eliminato con successo",
|
||||
"history_delete_error": "Errore durante l'eliminazione del gioco: {0}",
|
||||
"history_move_success": "{0} file spostato/i in: {1}",
|
||||
"history_move_error": "Errore durante lo spostamento: {0}",
|
||||
"history_error_details_title": "Dettagli errore",
|
||||
"history_no_error_message": "Nessun messaggio di errore disponibile",
|
||||
"web_title": "Interfaccia Web RGSX",
|
||||
@@ -465,6 +483,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",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Sem conexão com a Internet. Verifique sua rede.",
|
||||
"error_api_key": "Insira sua chave API (somente premium) no arquivo {0}",
|
||||
"error_invalid_download_data": "Dados de download inválidos",
|
||||
"popup_torrent_in_maintenance": "Torrent em manutenção, aguarde",
|
||||
"error_delete_sources": "Erro ao deletar arquivo sources.json ou pastas",
|
||||
"platform_no_platform": "Sem plataforma",
|
||||
"platform_page": "Página {0}/{1}",
|
||||
@@ -24,7 +25,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",
|
||||
@@ -42,6 +47,9 @@
|
||||
"free_mode_submitting": "[Modo gratuito] Enviando formulário...",
|
||||
"free_mode_link_found": "[Modo gratuito] Link encontrado: {0}...",
|
||||
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: o download gratuito como convidado está temporariamente indisponível (todos os slots estão ocupados). Tente novamente mais tarde.",
|
||||
"free_mode_unavailable_in_app": "1fichier: este download não está disponível no aplicativo no momento. Tente novamente mais tarde.",
|
||||
"free_mode_premium_advice": "Para baixar sem limites, quando quiser e em velocidade máxima, você precisa de uma conta premium ou de um serviço debrid e deve inserir a chave API no RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download cancelado pelo usuário.",
|
||||
"download_removed_from_queue": "Removido da fila de download",
|
||||
@@ -111,6 +119,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 +176,8 @@
|
||||
"controls_confirm_select": "Confirmar/Selecionar",
|
||||
"controls_cancel_back": "Cancelar/Voltar",
|
||||
"controls_filter_search": "Filtrar/Buscar",
|
||||
"controls_action_edit_search": "Editar busca",
|
||||
"controls_action_show_results": "Ver resultados",
|
||||
"symlink_option_enabled": "Opção de symlink ativada",
|
||||
"symlink_option_disabled": "Opção de symlink desativada",
|
||||
"menu_games_source_prefix": "Fonte de jogos",
|
||||
@@ -233,6 +244,7 @@
|
||||
"instruction_games_history": "Listar downloads anteriores e status",
|
||||
"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada",
|
||||
"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos",
|
||||
"instruction_games_scan_owned": "Verificar as pastas ROMs e marcar os jogos ja existentes",
|
||||
"instruction_settings_music": "Ativar ou desativar música de fundo",
|
||||
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
|
||||
"instruction_settings_auto_extract": "Ativar/desativar extração automática de arquivos após download",
|
||||
@@ -258,6 +270,9 @@
|
||||
"settings_web_service_success_enabled": "Serviço web ativado na inicialização",
|
||||
"settings_web_service_success_disabled": "Serviço web desativado na inicialização",
|
||||
"settings_web_service_error": "Erro: {0}",
|
||||
"menu_scan_owned_roms": "Verificar ROMs existentes",
|
||||
"popup_scan_owned_roms_done": "Verificacao de ROMs concluida: {0} jogos adicionados em {1} plataformas",
|
||||
"popup_scan_owned_roms_error": "Erro ao verificar ROMs: {0}",
|
||||
"settings_custom_dns": "DNS Personalizado na Inicialização",
|
||||
"settings_custom_dns_enabled": "Ativado",
|
||||
"settings_custom_dns_disabled": "Desativado",
|
||||
@@ -297,6 +312,7 @@
|
||||
"history_option_delete_game": "Excluir jogo",
|
||||
"history_option_error_info": "Detalhes do erro",
|
||||
"history_option_retry": "Tentar novamente",
|
||||
"history_move_action": "Mover",
|
||||
"history_option_back": "Voltar",
|
||||
"history_folder_path_label": "Caminho de destino:",
|
||||
"history_scraper_not_implemented": "Scraper ainda não implementado",
|
||||
@@ -306,6 +322,8 @@
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Jogo excluído com sucesso",
|
||||
"history_delete_error": "Erro ao excluir jogo: {0}",
|
||||
"history_move_success": "{0} arquivo(s) movido(s) para: {1}",
|
||||
"history_move_error": "Erro ao mover os arquivos: {0}",
|
||||
"history_error_details_title": "Detalhes do erro",
|
||||
"history_no_error_message": "Nenhuma mensagem de erro disponível",
|
||||
"web_title": "Interface Web RGSX",
|
||||
@@ -469,6 +487,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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
import config
|
||||
from history import load_history, save_history
|
||||
from utils import load_sources, load_games, extract_data
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
@@ -883,6 +883,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
settings['api_keys'] = {
|
||||
'1fichier': api_keys_data.get('1fichier', ''),
|
||||
'alldebrid': api_keys_data.get('alldebrid', ''),
|
||||
'debridlink': api_keys_data.get('debridlink', ''),
|
||||
'realdebrid': api_keys_data.get('realdebrid', '')
|
||||
}
|
||||
|
||||
@@ -1160,9 +1161,18 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
game_url = game.url
|
||||
|
||||
if not game_url:
|
||||
torrent_message = TRANSLATIONS.get('popup_torrent_in_maintenance', 'torrent in maintence')
|
||||
self._send_json({
|
||||
'success': False,
|
||||
'error': 'URL de téléchargement non disponible'
|
||||
'error': torrent_message
|
||||
}, status=400)
|
||||
return
|
||||
|
||||
if parse_torrent_download_url(game_url) is not None:
|
||||
torrent_message = TRANSLATIONS.get('popup_torrent_in_maintenance', 'torrent in maintence')
|
||||
self._send_json({
|
||||
'success': False,
|
||||
'error': torrent_message
|
||||
}, status=400)
|
||||
return
|
||||
|
||||
@@ -1242,6 +1252,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
queue_history_entry = {
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': game_url,
|
||||
'progress': 0,
|
||||
@@ -1279,6 +1290,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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import config
|
||||
from config import HEADLESS, Game
|
||||
try:
|
||||
@@ -33,6 +34,38 @@ 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 = {}
|
||||
_torrent_manifest_cache = {}
|
||||
_TORRENT_DOWNLOAD_SCHEME = "rgsx+torrent"
|
||||
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
|
||||
@@ -49,6 +82,238 @@ unavailable_systems = []
|
||||
|
||||
# Cache/process flags for extensions generation/loading
|
||||
|
||||
|
||||
def _format_size_bytes(size_bytes: int) -> str:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
|
||||
value = float(size_bytes)
|
||||
for unit in ["KB", "MB", "GB", "TB", "PB"]:
|
||||
value /= 1024.0
|
||||
if value < 1024.0 or unit == "PB":
|
||||
return f"{value:.2f} {unit}"
|
||||
|
||||
return f"{size_bytes} B"
|
||||
|
||||
|
||||
def _decode_bencode_text(value) -> str:
|
||||
if isinstance(value, bytes):
|
||||
for encoding in ("utf-8", "utf-8-sig", "latin-1"):
|
||||
try:
|
||||
return value.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return value.decode("utf-8", errors="replace")
|
||||
return str(value or "")
|
||||
|
||||
|
||||
def _bdecode(data: bytes, index: int = 0):
|
||||
token = data[index:index + 1]
|
||||
if token == b"i":
|
||||
end = data.index(b"e", index)
|
||||
return int(data[index + 1:end]), end + 1
|
||||
if token == b"l":
|
||||
items = []
|
||||
index += 1
|
||||
while data[index:index + 1] != b"e":
|
||||
value, index = _bdecode(data, index)
|
||||
items.append(value)
|
||||
return items, index + 1
|
||||
if token == b"d":
|
||||
values = {}
|
||||
index += 1
|
||||
while data[index:index + 1] != b"e":
|
||||
key, index = _bdecode(data, index)
|
||||
value, index = _bdecode(data, index)
|
||||
values[key] = value
|
||||
return values, index + 1
|
||||
if token.isdigit():
|
||||
sep = data.index(b":", index)
|
||||
length = int(data[index:sep])
|
||||
start = sep + 1
|
||||
end = start + length
|
||||
return data[start:end], end
|
||||
raise ValueError(f"Invalid bencode token at offset {index}: {token!r}")
|
||||
|
||||
|
||||
def is_torrent_manifest_url(url: str | None) -> bool:
|
||||
if not url or not isinstance(url, str):
|
||||
return False
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(url.strip())
|
||||
except Exception:
|
||||
return False
|
||||
return (parsed.path or "").lower().endswith(".torrent")
|
||||
|
||||
|
||||
def build_torrent_download_url(source_url: str, file_index: int, relative_path: str, size_bytes: int | None = None) -> str:
|
||||
params = {
|
||||
"source": source_url,
|
||||
"index": str(max(1, int(file_index))),
|
||||
"path": relative_path,
|
||||
}
|
||||
if isinstance(size_bytes, int) and size_bytes > 0:
|
||||
params["size"] = str(size_bytes)
|
||||
return f"{_TORRENT_DOWNLOAD_SCHEME}://download?{urllib.parse.urlencode(params, quote_via=urllib.parse.quote)}"
|
||||
|
||||
|
||||
def is_torrent_download_url(url: str | None) -> bool:
|
||||
if not url or not isinstance(url, str):
|
||||
return False
|
||||
try:
|
||||
return urllib.parse.urlparse(url).scheme == _TORRENT_DOWNLOAD_SCHEME
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def parse_torrent_download_url(url: str | None) -> dict[str, str | int] | None:
|
||||
if not is_torrent_download_url(url):
|
||||
return None
|
||||
parsed = urllib.parse.urlparse(str(url))
|
||||
query = urllib.parse.parse_qs(parsed.query)
|
||||
source_url = (query.get("source") or [""])[0].strip()
|
||||
relative_path = (query.get("path") or [""])[0].strip()
|
||||
try:
|
||||
file_index = int((query.get("index") or ["1"])[0])
|
||||
except (TypeError, ValueError):
|
||||
file_index = 1
|
||||
try:
|
||||
size_bytes = int((query.get("size") or ["0"])[0])
|
||||
except (TypeError, ValueError):
|
||||
size_bytes = 0
|
||||
if not source_url or not relative_path:
|
||||
return None
|
||||
return {
|
||||
"source_url": source_url,
|
||||
"file_index": max(1, file_index),
|
||||
"relative_path": relative_path,
|
||||
"size_bytes": max(0, size_bytes),
|
||||
}
|
||||
|
||||
|
||||
def _extract_torrent_entries_from_bytes(payload: bytes, source_url: str) -> list[dict[str, str | int]]:
|
||||
torrent_data, _ = _bdecode(payload)
|
||||
if not isinstance(torrent_data, dict):
|
||||
raise ValueError("Torrent root metadata is not a dictionary")
|
||||
|
||||
info = torrent_data.get(b"info")
|
||||
if not isinstance(info, dict):
|
||||
raise ValueError("Torrent metadata does not contain an info dictionary")
|
||||
|
||||
entries: list[dict[str, str | int]] = []
|
||||
files = info.get(b"files")
|
||||
root_name = _decode_bencode_text(info.get(b"name.utf-8") or info.get(b"name") or "").strip()
|
||||
if isinstance(files, list):
|
||||
for file_index, file_entry in enumerate(files, start=1):
|
||||
if not isinstance(file_entry, dict):
|
||||
continue
|
||||
path_parts = file_entry.get(b"path.utf-8") or file_entry.get(b"path") or []
|
||||
if not isinstance(path_parts, list):
|
||||
continue
|
||||
parts = [_decode_bencode_text(part).strip() for part in path_parts]
|
||||
parts = [part for part in parts if part]
|
||||
if not parts:
|
||||
continue
|
||||
full_path = "/".join(parts)
|
||||
download_path = "/".join([p for p in [root_name, full_path] if p])
|
||||
entries.append({
|
||||
"name": parts[-1],
|
||||
"path": full_path,
|
||||
"download_path": download_path or full_path,
|
||||
"index": file_index,
|
||||
"size_bytes": int(file_entry.get(b"length") or 0),
|
||||
"source_url": source_url,
|
||||
})
|
||||
else:
|
||||
if root_name:
|
||||
entries.append({
|
||||
"name": root_name,
|
||||
"path": root_name,
|
||||
"download_path": root_name,
|
||||
"index": 1,
|
||||
"size_bytes": int(info.get(b"length") or 0),
|
||||
"source_url": source_url,
|
||||
})
|
||||
|
||||
duplicate_names = {}
|
||||
for entry in entries:
|
||||
name = str(entry["name"])
|
||||
duplicate_names[name] = duplicate_names.get(name, 0) + 1
|
||||
|
||||
for entry in entries:
|
||||
if duplicate_names.get(str(entry["name"]), 0) > 1:
|
||||
entry["name"] = str(entry["path"])
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _get_torrent_entries(source_url: str) -> list[dict[str, str | int]]:
|
||||
cached = _torrent_manifest_cache.get(source_url)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
||||
"Accept": "*/*",
|
||||
}
|
||||
response = requests.get(source_url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
entries = _extract_torrent_entries_from_bytes(response.content, source_url)
|
||||
_torrent_manifest_cache[source_url] = entries
|
||||
return entries
|
||||
|
||||
|
||||
def _extract_torrent_source(item) -> tuple[str, str] | None:
|
||||
if isinstance(item, (list, tuple)):
|
||||
if len(item) < 2:
|
||||
return None
|
||||
source_name = str(item[0] or "").strip()
|
||||
source_url = item[1] if isinstance(item[1], str) else None
|
||||
if source_url and is_torrent_manifest_url(source_url):
|
||||
return source_name, source_url.strip()
|
||||
return None
|
||||
|
||||
if isinstance(item, dict):
|
||||
source_url = item.get("torrent_url") or item.get("url") or item.get("download") or item.get("link")
|
||||
if not isinstance(source_url, str) or not source_url.strip():
|
||||
return None
|
||||
source_type = str(item.get("type") or item.get("source_type") or item.get("source") or "").strip().lower()
|
||||
if source_type == "torrent" or is_torrent_manifest_url(source_url):
|
||||
source_name = item.get("game_name") or item.get("name") or item.get("title") or item.get("game") or item.get("label")
|
||||
if not source_name:
|
||||
parsed = urllib.parse.urlparse(source_url)
|
||||
source_name = urllib.parse.unquote(Path(parsed.path).name)
|
||||
return str(source_name or "").strip(), source_url.strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _expand_torrent_source(item, platform_id: str) -> list[tuple[str, None, str | None]] | None:
|
||||
source = _extract_torrent_source(item)
|
||||
if not source:
|
||||
return None
|
||||
|
||||
source_name, source_url = source
|
||||
try:
|
||||
entries = _get_torrent_entries(source_url)
|
||||
except Exception as exc:
|
||||
label = source_name or source_url
|
||||
logger.error(f"Erreur chargement torrent pour {platform_id} ({label}): {exc}")
|
||||
return []
|
||||
|
||||
expanded: list[tuple[str, None, str | None]] = []
|
||||
for entry in entries:
|
||||
game_name = str(entry.get("name") or "").strip()
|
||||
if not game_name:
|
||||
continue
|
||||
size_bytes = int(entry.get("size_bytes") or 0)
|
||||
file_index = int(entry.get("index") or 1)
|
||||
relative_path = str(entry.get("download_path") or entry.get("path") or game_name)
|
||||
download_url = build_torrent_download_url(source_url, file_index, relative_path, size_bytes)
|
||||
expanded.append((game_name, download_url, _format_size_bytes(size_bytes) if size_bytes > 0 else None))
|
||||
return expanded
|
||||
|
||||
|
||||
def restart_application(delay_ms: int = 2000):
|
||||
"""Schedule a restart with a visible popup; actual restart happens in the main loop.
|
||||
@@ -1195,9 +1460,15 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
game_file = c
|
||||
break
|
||||
if not game_file:
|
||||
_games_cache.pop(platform_id, None)
|
||||
logger.warning(f"Aucun fichier de jeux trouvé pour {platform_id} (candidats: {candidates})")
|
||||
return []
|
||||
|
||||
game_mtime_ns = os.stat(game_file).st_mtime_ns
|
||||
cached_entry = _games_cache.get(platform_id)
|
||||
if cached_entry and cached_entry.get("path") == game_file and cached_entry.get("mtime_ns") == game_mtime_ns:
|
||||
return cached_entry["games"]
|
||||
|
||||
with open(game_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
@@ -1208,6 +1479,10 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
normalized = [] # (name, url, size)
|
||||
|
||||
def extract_from_dict(d):
|
||||
torrent_rows = _expand_torrent_source(d, platform_id)
|
||||
if torrent_rows is not None:
|
||||
normalized.extend(torrent_rows)
|
||||
return
|
||||
name = d.get('game_name') or d.get('name') or d.get('title') or d.get('game')
|
||||
url = d.get('url') or d.get('download') or d.get('link') or d.get('href')
|
||||
size = d.get('size') or d.get('filesize') or d.get('length')
|
||||
@@ -1217,6 +1492,10 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
if isinstance(item, (list, tuple)):
|
||||
torrent_rows = _expand_torrent_source(item, platform_id)
|
||||
if torrent_rows is not None:
|
||||
normalized.extend(torrent_rows)
|
||||
continue
|
||||
if len(item) == 0:
|
||||
continue
|
||||
name = str(item[0])
|
||||
@@ -1239,11 +1518,17 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
|
||||
games_list: list[Game] = []
|
||||
for name, url, size in normalized:
|
||||
display_name = Path(name).stem
|
||||
display_name = display_name.replace(platform_id, "")
|
||||
display_name = get_clean_display_name(name, platform_id)
|
||||
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
|
||||
|
||||
_games_cache[platform_id] = {
|
||||
"path": game_file,
|
||||
"mtime_ns": game_mtime_ns,
|
||||
"games": games_list,
|
||||
}
|
||||
return games_list
|
||||
except Exception as e:
|
||||
_games_cache.pop(platform_id, None)
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
|
||||
return []
|
||||
|
||||
@@ -2738,24 +3023,25 @@ def set_music_popup(music_name):
|
||||
config.needs_redraw = True # Forcer le redraw pour afficher le nom de la musique
|
||||
|
||||
def load_api_keys(force: bool = False):
|
||||
"""Charge les clés API (1fichier, AllDebrid, RealDebrid) en une seule passe.
|
||||
"""Charge les clés API (1fichier, AllDebrid, Debrid-Link, RealDebrid) en une seule passe.
|
||||
|
||||
- Crée les fichiers vides s'ils n'existent pas
|
||||
- Met à jour config.API_KEY_1FICHIER, config.API_KEY_ALLDEBRID, config.API_KEY_REALDEBRID
|
||||
- Met à jour config.API_KEY_1FICHIER, config.API_KEY_ALLDEBRID, config.API_KEY_DEBRIDLINK, config.API_KEY_REALDEBRID
|
||||
- Utilise un cache basé sur le mtime pour éviter des relectures
|
||||
- force=True ignore le cache et relit systématiquement
|
||||
|
||||
Retourne: { '1fichier': str, 'alldebrid': str, 'realdebrid': str, 'reloaded': bool }
|
||||
Retourne: { '1fichier': str, 'alldebrid': str, 'debridlink': str, 'realdebrid': str, 'reloaded': bool }
|
||||
"""
|
||||
try:
|
||||
paths = {
|
||||
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
|
||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
|
||||
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK_PATH', ''),
|
||||
'realdebrid': getattr(config, 'API_KEY_REALDEBRID_PATH', ''),
|
||||
}
|
||||
cache_attr = '_api_keys_cache'
|
||||
if not hasattr(config, cache_attr):
|
||||
setattr(config, cache_attr, {'1fichier_mtime': None, 'alldebrid_mtime': None, 'realdebrid_mtime': None})
|
||||
setattr(config, cache_attr, {'1fichier_mtime': None, 'alldebrid_mtime': None, 'debridlink_mtime': None, 'realdebrid_mtime': None})
|
||||
cache_data = getattr(config, cache_attr)
|
||||
reloaded = False
|
||||
|
||||
@@ -2789,6 +3075,8 @@ def load_api_keys(force: bool = False):
|
||||
config.API_KEY_1FICHIER = value
|
||||
elif key_name == 'alldebrid':
|
||||
config.API_KEY_ALLDEBRID = value
|
||||
elif key_name == 'debridlink':
|
||||
config.API_KEY_DEBRIDLINK = value
|
||||
elif key_name == 'realdebrid':
|
||||
config.API_KEY_REALDEBRID = value
|
||||
cache_data[cache_key] = mtime
|
||||
@@ -2796,6 +3084,7 @@ def load_api_keys(force: bool = False):
|
||||
return {
|
||||
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
||||
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK', ''),
|
||||
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
|
||||
'reloaded': reloaded
|
||||
}
|
||||
@@ -2804,6 +3093,7 @@ def load_api_keys(force: bool = False):
|
||||
return {
|
||||
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
||||
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK', ''),
|
||||
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
|
||||
'reloaded': False
|
||||
}
|
||||
@@ -2861,10 +3151,10 @@ def load_archive_org_cookie(force: bool = False) -> str:
|
||||
|
||||
|
||||
def save_api_keys(api_keys: dict):
|
||||
"""Sauvegarde les clés API (1fichier, AllDebrid, RealDebrid) dans leurs fichiers respectifs.
|
||||
"""Sauvegarde les clés API (1fichier, AllDebrid, Debrid-Link, RealDebrid) dans leurs fichiers respectifs.
|
||||
|
||||
Args:
|
||||
api_keys: dict avec les clés '1fichier', 'alldebrid', 'realdebrid'
|
||||
api_keys: dict avec les clés '1fichier', 'alldebrid', 'debridlink', 'realdebrid'
|
||||
|
||||
Retourne: True si au moins une clé a été sauvegardée avec succès
|
||||
"""
|
||||
@@ -2874,6 +3164,7 @@ def save_api_keys(api_keys: dict):
|
||||
paths = {
|
||||
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
|
||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
|
||||
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK_PATH', ''),
|
||||
'realdebrid': getattr(config, 'API_KEY_REALDEBRID_PATH', ''),
|
||||
}
|
||||
|
||||
@@ -2901,6 +3192,8 @@ def save_api_keys(api_keys: dict):
|
||||
config.API_KEY_1FICHIER = value.strip()
|
||||
elif key_name == 'alldebrid':
|
||||
config.API_KEY_ALLDEBRID = value.strip()
|
||||
elif key_name == 'debridlink':
|
||||
config.API_KEY_DEBRIDLINK = value.strip()
|
||||
elif key_name == 'realdebrid':
|
||||
config.API_KEY_REALDEBRID = value.strip()
|
||||
|
||||
@@ -2926,6 +3219,9 @@ def load_api_key_1fichier(force: bool = False): # pragma: no cover
|
||||
def load_api_key_alldebrid(force: bool = False): # pragma: no cover
|
||||
return load_api_keys(force).get('alldebrid', '')
|
||||
|
||||
def load_api_key_debridlink(force: bool = False): # pragma: no cover
|
||||
return load_api_keys(force).get('debridlink', '')
|
||||
|
||||
def load_api_key_realdebrid(force: bool = False): # pragma: no cover
|
||||
return load_api_keys(force).get('realdebrid', '')
|
||||
|
||||
@@ -2938,19 +3234,19 @@ def ensure_api_keys_loaded(force: bool = False): # pragma: no cover
|
||||
# ------------------------------
|
||||
def build_provider_paths_string():
|
||||
"""Retourne une chaîne listant les chemins des fichiers de clés pour affichage/erreurs."""
|
||||
return f"{getattr(config, 'API_KEY_1FICHIER_PATH', '')} or {getattr(config, 'API_KEY_ALLDEBRID_PATH', '')} or {getattr(config, 'API_KEY_REALDEBRID_PATH', '')}"
|
||||
return f"{getattr(config, 'API_KEY_1FICHIER_PATH', '')} or {getattr(config, 'API_KEY_ALLDEBRID_PATH', '')} or {getattr(config, 'API_KEY_DEBRIDLINK_PATH', '')} or {getattr(config, 'API_KEY_REALDEBRID_PATH', '')}"
|
||||
|
||||
def ensure_download_provider_keys(force: bool = False): # pragma: no cover
|
||||
"""S'assure que les clés 1fichier/AllDebrid/RealDebrid sont chargées et retourne le dict.
|
||||
"""S'assure que les clés 1fichier/AllDebrid/Debrid-Link/RealDebrid sont chargées et retourne le dict.
|
||||
|
||||
Utilise load_api_keys (cache mtime). force=True invalide le cache.
|
||||
"""
|
||||
return load_api_keys(force)
|
||||
|
||||
def missing_all_provider_keys(): # pragma: no cover
|
||||
"""True si aucune des trois clés n'est définie."""
|
||||
"""True si aucune des clés premium n'est définie."""
|
||||
keys = load_api_keys(False)
|
||||
return not keys.get('1fichier') and not keys.get('alldebrid') and not keys.get('realdebrid')
|
||||
return not keys.get('1fichier') and not keys.get('alldebrid') and not keys.get('debridlink') and not keys.get('realdebrid')
|
||||
|
||||
def provider_keys_status(): # pragma: no cover
|
||||
"""Retourne un dict de présence pour debug/log."""
|
||||
@@ -2958,6 +3254,7 @@ def provider_keys_status(): # pragma: no cover
|
||||
return {
|
||||
'1fichier': bool(keys.get('1fichier')),
|
||||
'alldebrid': bool(keys.get('alldebrid')),
|
||||
'debridlink': bool(keys.get('debridlink')),
|
||||
'realdebrid': bool(keys.get('realdebrid')),
|
||||
}
|
||||
|
||||
@@ -3008,26 +3305,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.6"
|
||||
"version": "2.6.1.7"
|
||||
}
|
||||
Reference in New Issue
Block a user