mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-20 08:45:41 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f437c1aa4 | ||
|
|
054b174c18 | ||
|
|
fbb1a2aa68 | ||
|
|
1dbc741617 | ||
|
|
c4913a5fc2 | ||
|
|
bf9d3d2de5 | ||
|
|
9979949bdc | ||
|
|
9ed264544f | ||
|
|
779c060927 | ||
|
|
88400e538f | ||
|
|
cbab067dd6 | ||
|
|
b4ed0b355d | ||
|
|
51ad08ff33 | ||
|
|
d6a5c4b27e | ||
|
|
2c7c3414a5 | ||
|
|
059d3988ac | ||
|
|
50c9b9caad | ||
|
|
7d2d55fe5f | ||
|
|
14a5416d2d | ||
|
|
3193dc90f6 | ||
|
|
b437f31854 | ||
|
|
08f3e64d2a | ||
|
|
4968af2da9 | ||
|
|
920914b374 | ||
|
|
a326cb0b67 | ||
|
|
c9fdf93221 | ||
|
|
184a8c64fe | ||
|
|
9a2e4ce0db | ||
|
|
73eceeb777 | ||
|
|
2fcc4ca6df | ||
|
|
2ed889540b | ||
|
|
e9a610b5dd | ||
|
|
bd3b885736 | ||
|
|
1592671ddc | ||
|
|
4e029aabf1 | ||
|
|
970fcaf197 | ||
|
|
ff30e6d297 | ||
|
|
5c7fa0484f | ||
|
|
814861e9ee | ||
|
|
56c87ab05f | ||
|
|
b12d645fbf | ||
|
|
04e68adef0 | ||
|
|
52f2b960c2 | ||
|
|
1ea604840e | ||
|
|
802696e78f | ||
|
|
6f17173a8c |
133
.github/workflows/release.yml
vendored
133
.github/workflows/release.yml
vendored
@@ -30,16 +30,9 @@ jobs:
|
||||
zip -r "../../dist/RGSX_update_latest.zip" . \
|
||||
-x "logs/*" \
|
||||
"logs/**" \
|
||||
"images/*" \
|
||||
"images/**" \
|
||||
"games/*" \
|
||||
"games/**" \
|
||||
"scripts/*" \
|
||||
"scripts/**" \
|
||||
"__pycache__/*" \
|
||||
"__pycache__/**" \
|
||||
"*.pyc" \
|
||||
"sources.json" \
|
||||
"*.log"
|
||||
|
||||
cd ../..
|
||||
@@ -52,16 +45,9 @@ jobs:
|
||||
zip -r "dist/RGSX_full_latest.zip" ports windows \
|
||||
-x "ports/RGSX/logs/*" \
|
||||
"ports/RGSX/logs/**" \
|
||||
"ports/RGSX/images/*" \
|
||||
"ports/RGSX/images/**" \
|
||||
"ports/RGSX/games/*" \
|
||||
"ports/RGSX/games/**" \
|
||||
"ports/RGSX/scripts/*" \
|
||||
"ports/RGSX/scripts/**" \
|
||||
"ports/RGSX/__pycache__/*" \
|
||||
"ports/RGSX/__pycache__/**" \
|
||||
"ports/RGSX/*.pyc" \
|
||||
"ports/RGSX/sources.json" \
|
||||
"ports/RGSX/*.log" \
|
||||
"windows/logs/*" \
|
||||
"windows/*.xml" \
|
||||
@@ -75,57 +61,88 @@ jobs:
|
||||
echo "✓ All packages created successfully"
|
||||
ls -lh dist/
|
||||
|
||||
- name: Generate release notes with commit message
|
||||
shell: bash
|
||||
run: |
|
||||
# Récupérer le message de commit associé au tag
|
||||
COMMIT_MSG=$(git log -1 --format=%B ${{ github.ref_name }})
|
||||
echo "Commit message:"
|
||||
echo "$COMMIT_MSG"
|
||||
|
||||
# Créer le fichier de release notes
|
||||
cat > dist/RELEASE_NOTES.md << 'RELEASE_EOF'
|
||||
# 📦 RGSX Release ${{ github.ref_name }}
|
||||
|
||||
## 📝 Changelog
|
||||
RELEASE_EOF
|
||||
|
||||
# Ajouter le message de commit
|
||||
echo "$COMMIT_MSG" >> dist/RELEASE_NOTES.md
|
||||
|
||||
# Ajouter le reste des instructions
|
||||
cat >> dist/RELEASE_NOTES.md << 'RELEASE_EOF'
|
||||
|
||||
---
|
||||
|
||||
## 📥 Automatic Installation (Only for Batocera/Knulli)
|
||||
|
||||
### ON PC, NUC, SteamDeck or any x86_64 based:
|
||||
1. Open File Manager (F1) then "Applications" and launch xterm
|
||||
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
|
||||
3. Launch RGSX from "Ports" menu
|
||||
|
||||
### ON RASPBERRY/ ARM based SBC / HANDHELD :
|
||||
1. Connect your device with SSH on a computer/smartphone connected to same network (ssh root@IPADDRESS , pass:linux)
|
||||
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
|
||||
3. Launch RGSX from "Ports" menu
|
||||
|
||||
## 📥 Manual Installation
|
||||
|
||||
### Batocera/Knulli
|
||||
1. Download latest release : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip
|
||||
2. Extract only PORTS folder in `/userdata/roms/`
|
||||
3. Launch RGSX from the Ports menu
|
||||
|
||||
### Retrobat/Full Installation on Windows
|
||||
1. Download latest release : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip
|
||||
2. Extract all folders in your Retrobat\roms folder
|
||||
3. Launch RGSX from system "Windows"
|
||||
|
||||
|
||||
## 📥 Manual Update (only if automatic doesn't work for some obscure reason)
|
||||
|
||||
#### Batocera/Knulli/Retrobat
|
||||
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
|
||||
2. Extract zip content in `/userdata/roms/ports/RGSX`
|
||||
3. Launch RGSX
|
||||
|
||||
|
||||
### 📖 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"
|
||||
cat dist/RELEASE_NOTES.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: RGSX ${{ github.ref_name }}
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
draft: false
|
||||
prerelease: false
|
||||
body: |
|
||||
# 📦 RGSX Release ${{ github.ref_name }}
|
||||
|
||||
## 📥 Automatic Installation (Only for batocera Knulli)
|
||||
|
||||
### ON PC :
|
||||
1. Open File Manager (F1) then "Applications" and launch xterm
|
||||
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
|
||||
3. Launch RGSX from "Ports" menu
|
||||
|
||||
### ON RASPBERRY/ARM SBC / HANDHELD :
|
||||
1. Connect your device with SSH on a computer/smartphone connected to same network (ssh root@IPADDRESS , pass:linux)
|
||||
2. Use the command line `curl -L bit.ly/rgsx-install | sh`
|
||||
3. Launch RGSX from "Ports" menu
|
||||
|
||||
## 📥 Manual Installation
|
||||
|
||||
### Batocera/Knulli
|
||||
1. Download latest release : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip
|
||||
2. Extract only PORTS folder in `/userdata/roms/`
|
||||
3. Launch RGSX from the Ports menu
|
||||
|
||||
### Retrobat/Full Installation on Windows
|
||||
1. Download latest release : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip
|
||||
2. Extract all folders in your Retrobat\roms folder
|
||||
3. Launch RGSX from system "Windows"
|
||||
|
||||
|
||||
## 📥 Manual Update (you shouldn't need to do this as RGSX updates automatically on each start)
|
||||
|
||||
#### Batocera/Knulli
|
||||
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
|
||||
2. Extract only PORTS folder in `/userdata/roms/`
|
||||
3. Launch RGSX from the Ports menu
|
||||
|
||||
#### Retrobat
|
||||
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
|
||||
2. Extract all folders in your Retrobat\roms folder
|
||||
3. Launch RGSX from system "Windows"
|
||||
|
||||
|
||||
### 📖 Documentation
|
||||
[README.md](https://github.com/${{ github.repository }}/blob/main/README.md)
|
||||
body_path: dist/RELEASE_NOTES.md
|
||||
files: |
|
||||
dist/RGSX_update_latest.zip
|
||||
dist/RGSX_full_latest.zip
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ pygame/
|
||||
data/
|
||||
docker-compose.test.yml
|
||||
config/
|
||||
pyrightconfig.json
|
||||
|
||||
13
README.md
13
README.md
@@ -1,11 +1,11 @@
|
||||
# 🎮 Retro Game Sets Xtra (RGSX)
|
||||
|
||||
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)**
|
||||
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)** • **[Troubleshoot / Common Errors](https://github.com/RetroGameSets/RGSX#%EF%B8%8F-troubleshooting)** •
|
||||
|
||||
A free, user-friendly ROM downloader for Batocera, Knulli, and RetroBat with multi-source support.
|
||||
|
||||
<p align="center">
|
||||
<img width="69%" alt="platform menu" src="https://github.com/user-attachments/assets/4464b57b-06a8-45e9-a411-cc12b421545a" />
|
||||
<img width="69%" alt="main" src="https://github.com/user-attachments/assets/a98f1189-9a50-4cc3-b588-3f85245640d8" />
|
||||
<img width="30%" alt="controls help" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
|
||||
</p>
|
||||
<p align="center">
|
||||
@@ -201,11 +201,12 @@ RGSX includes a web interface that launched automatically when using RGSX for re
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart |
|
||||
| Games not showing | Pause Menu > Games > Update Game Cache |
|
||||
| Download stuck | Check API keys in `/saves/ports/rgsx/` |
|
||||
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` |
|
||||
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart app, you can try delete /roms/ports/RGSX/assets/controls/xx.json too |
|
||||
| No games ? | Pause Menu > Games > Update Game Cache |
|
||||
| Missing systems on the list? | RGSX read es_systems.cfg to show only supported systems, if you want all systems : Pause Menu > Games > Show unsupported systems |
|
||||
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` or `/roms/windows/logs/Retrobat_RGSX_log.txt` |
|
||||
| Layout change not applied | Restart RGSX after changing layout |
|
||||
| Downloading BIOS file is ok but you can't download any games? | Activate custom DNS on Pause Menu> Settings and reboot , server can be blocked by your ISP. check any threat/website protection on your router too, especially on ASUS one|
|
||||
|
||||
**Need help?** Share logs from `/roms/ports/RGSX/logs/` on [Discord](https://discord.gg/Vph9jwg3VV).
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import os
|
||||
import platform
|
||||
import warnings
|
||||
|
||||
# Ignorer le warning de deprecation de pkg_resources dans pygame
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="pygame.pkgdata")
|
||||
warnings.filterwarnings("ignore", message="pkg_resources is deprecated")
|
||||
|
||||
# Ne pas forcer SDL_FBDEV ici; si déjà défini par l'environnement, on le garde
|
||||
try:
|
||||
if "SDL_FBDEV" in os.environ:
|
||||
@@ -22,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_display_menu,
|
||||
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
|
||||
)
|
||||
from language import _
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, 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
|
||||
@@ -58,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:
|
||||
@@ -91,6 +98,7 @@ _run_windows_gamelist_update()
|
||||
|
||||
try:
|
||||
config.update_checked = False
|
||||
config.gamelist_update_prompted = False # Flag pour ne pas redemander la mise à jour plusieurs fois
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -140,6 +148,14 @@ initialize_language()
|
||||
config.sources_mode = get_sources_mode()
|
||||
config.custom_sources_url = get_custom_sources_url()
|
||||
logger.debug(f"Mode sources initial: {config.sources_mode}, URL custom: {config.custom_sources_url}")
|
||||
# Charger l'option nintendo_layout depuis les settings
|
||||
try:
|
||||
from rgsx_settings import get_nintendo_layout
|
||||
config.nintendo_layout = get_nintendo_layout()
|
||||
logger.debug(f"nintendo_layout initial: {config.nintendo_layout}")
|
||||
except Exception:
|
||||
# fallback: si l'import ou la lecture échoue, conserver la valeur par défaut dans config
|
||||
logger.debug("Impossible de charger nintendo_layout depuis rgsx_settings")
|
||||
|
||||
# Détection du système grace a une commande windows / linux (on oublie is non-pc c'est juste pour connaitre le materiel et le systeme d'exploitation)
|
||||
def detect_system_info():
|
||||
@@ -170,7 +186,6 @@ config.init_footer_font()
|
||||
|
||||
# Mise à jour de la résolution dans config
|
||||
config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
|
||||
logger.debug(f"Resolution d'ecran : {config.screen_width}x{config.screen_height}")
|
||||
print(f"Resolution ecran validee: {config.screen_width}x{config.screen_height}")
|
||||
|
||||
# Afficher un premier écran de chargement immédiatement pour éviter un écran noir
|
||||
@@ -213,7 +228,7 @@ except Exception:
|
||||
normalized_names = [n.lower() for n in joystick_names]
|
||||
if not joystick_names:
|
||||
joystick_names = ["Clavier"]
|
||||
print("Aucun joystick détecté, utilisation du clavier par défaut")
|
||||
print("Aucun joystick detecte, utilisation du clavier par defaut")
|
||||
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
|
||||
config.joystick = False
|
||||
config.keyboard = True
|
||||
@@ -270,7 +285,6 @@ logger.debug(f"Historique de téléchargement : {len(config.history)} entrées")
|
||||
|
||||
# Chargement des jeux téléchargés
|
||||
config.downloaded_games = load_downloaded_games()
|
||||
logger.debug(f"Jeux téléchargés : {sum(len(v) for v in config.downloaded_games.values())} jeux")
|
||||
|
||||
# Vérification et chargement de la configuration des contrôles (après mises à jour et détection manette)
|
||||
config.controls_config = load_controls_config()
|
||||
@@ -296,6 +310,9 @@ try:
|
||||
if config.controls_config:
|
||||
summary = {}
|
||||
for action, mapping in config.controls_config.items():
|
||||
# Vérifier que mapping est bien un dictionnaire
|
||||
if not isinstance(mapping, dict):
|
||||
continue
|
||||
mtype = mapping.get("type")
|
||||
val = None
|
||||
if mtype == "key":
|
||||
@@ -335,9 +352,6 @@ def start_web_server():
|
||||
global web_server_process
|
||||
try:
|
||||
web_server_script = os.path.join(config.APP_FOLDER, "rgsx_web.py")
|
||||
logger.info(f"Tentative de démarrage du serveur web...")
|
||||
logger.info(f"Script: {web_server_script}")
|
||||
logger.info(f"Fichier existe: {os.path.exists(web_server_script)}")
|
||||
|
||||
if not os.path.exists(web_server_script):
|
||||
logger.warning(f"Script serveur web introuvable: {web_server_script}")
|
||||
@@ -378,7 +392,6 @@ def start_web_server():
|
||||
|
||||
logger.info(f"✅ Serveur web démarré (PID: {web_server_process.pid})")
|
||||
logger.info(f"🌐 Serveur accessible sur http://localhost:5000")
|
||||
logger.info(f"📝 Logs de démarrage: {web_server_log}")
|
||||
|
||||
# Attendre un peu pour voir si le processus crash immédiatement
|
||||
import time
|
||||
@@ -420,9 +433,28 @@ async def main():
|
||||
global current_music, music_files, music_folder, joystick
|
||||
logger.debug("Début main")
|
||||
|
||||
# Charger les filtres de jeux sauvegardés
|
||||
try:
|
||||
from game_filters import GameFilters
|
||||
from rgsx_settings import load_game_filters
|
||||
config.game_filter_obj = GameFilters()
|
||||
filter_dict = load_game_filters()
|
||||
if filter_dict:
|
||||
config.game_filter_obj.load_from_dict(filter_dict)
|
||||
if config.game_filter_obj.is_active():
|
||||
config.filter_active = True
|
||||
logger.info("Filtres de jeux chargés et actifs")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des filtres: {e}")
|
||||
config.game_filter_obj = None
|
||||
|
||||
# Démarrer le serveur web en arrière-plan
|
||||
start_web_server()
|
||||
|
||||
# Démarrer le worker de la queue de téléchargement
|
||||
queue_worker_thread = threading.Thread(target=download_queue_worker, daemon=True)
|
||||
queue_worker_thread.start()
|
||||
|
||||
running = True
|
||||
loading_step = "none"
|
||||
sources = []
|
||||
@@ -458,7 +490,7 @@ async def main():
|
||||
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"] == "Téléchargement" for entry in config.history):
|
||||
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:
|
||||
config.needs_redraw = True
|
||||
last_redraw_time = current_time
|
||||
@@ -495,7 +527,7 @@ async def main():
|
||||
# Appui long détecté, ouvrir le scraper
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
if games:
|
||||
game_name = games[config.current_game][0]
|
||||
game_name = games[config.current_game].name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
|
||||
config.previous_menu_state = "game"
|
||||
@@ -552,6 +584,24 @@ async def main():
|
||||
thread = threading.Thread(target=scrape_async, daemon=True)
|
||||
thread.start()
|
||||
|
||||
# Gestion de l'appui long sur confirm dans le menu platform pour configurer le dossier de destination
|
||||
if (config.menu_state == "platform" and
|
||||
getattr(config, 'platform_confirm_press_start_time', 0) > 0 and
|
||||
not getattr(config, 'platform_confirm_long_press_triggered', False)):
|
||||
press_duration = current_time - config.platform_confirm_press_start_time
|
||||
if press_duration >= config.confirm_long_press_threshold:
|
||||
# Appui long détecté, ouvrir le dialogue de configuration du dossier
|
||||
if config.platforms:
|
||||
platform = config.platforms[config.selected_platform]
|
||||
platform_name = platform["name"] if isinstance(platform, dict) else platform
|
||||
config.platform_config_name = platform_name
|
||||
config.previous_menu_state = "platform"
|
||||
config.menu_state = "platform_folder_config"
|
||||
config.platform_folder_selection = 0 # 0=Current, 1=Browse, 2=Reset, 3=Cancel
|
||||
config.needs_redraw = True
|
||||
config.platform_confirm_long_press_triggered = True
|
||||
logger.debug(f"Appui long détecté ({press_duration}ms), ouverture config dossier pour {platform_name}")
|
||||
|
||||
# Gestion des événements
|
||||
events = pygame.event.get()
|
||||
for event in events:
|
||||
@@ -612,6 +662,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")
|
||||
@@ -620,6 +671,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")
|
||||
@@ -655,9 +707,12 @@ async def main():
|
||||
"pause_menu",
|
||||
"pause_controls_menu",
|
||||
"pause_display_menu",
|
||||
"pause_display_layout_menu",
|
||||
"pause_display_font_menu",
|
||||
"pause_games_menu",
|
||||
"pause_settings_menu",
|
||||
"pause_api_keys_status",
|
||||
"pause_connection_status",
|
||||
"filter_platforms",
|
||||
"display_menu",
|
||||
"language_select",
|
||||
@@ -668,10 +723,16 @@ async def main():
|
||||
"history_game_options",
|
||||
"history_show_folder",
|
||||
"history_scraper_info",
|
||||
"scraper", # Ajout du scraper pour gérer les contrôles
|
||||
"scraper",
|
||||
"history_error_details",
|
||||
"history_confirm_delete",
|
||||
"history_extract_archive",
|
||||
"text_file_viewer",
|
||||
# Menus filtrage avancé
|
||||
"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)
|
||||
@@ -706,6 +767,26 @@ async def main():
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
|
||||
if config.menu_state == "gamelist_update_prompt":
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
|
||||
if config.menu_state == "platform_folder_config":
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
|
||||
if config.menu_state == "folder_browser":
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
|
||||
if config.menu_state == "folder_browser_new_folder":
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
|
||||
if config.menu_state == "extension_warning":
|
||||
logger.debug(f"[EXTENSION_WARNING] Processing extension_warning, previous_menu_state={config.previous_menu_state}, pending_download={bool(config.pending_download)}")
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
@@ -758,12 +839,9 @@ async def main():
|
||||
logger.debug("Action quit détectée, arrêt de l'application")
|
||||
elif action == "download" and config.menu_state == "game" and config.filtered_games:
|
||||
game = config.filtered_games[config.current_game]
|
||||
if isinstance(game, (list, tuple)):
|
||||
game_name = game[0]
|
||||
url = game[1] if len(game) > 1 else None
|
||||
else: # fallback str
|
||||
game_name = str(game)
|
||||
url = None
|
||||
game_name = game.name
|
||||
url = game.url
|
||||
|
||||
# Nouveau schéma: config.platforms contient déjà platform_name (string)
|
||||
platform_name = config.platforms[config.current_platform]
|
||||
if url:
|
||||
@@ -775,7 +853,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
|
||||
@@ -1042,6 +1125,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é
|
||||
@@ -1058,6 +1145,12 @@ async def main():
|
||||
elif config.menu_state == "pause_display_menu":
|
||||
from display import draw_pause_display_menu
|
||||
draw_pause_display_menu(screen, getattr(config, 'pause_display_selection', 0))
|
||||
elif config.menu_state == "pause_display_layout_menu":
|
||||
from display import draw_pause_display_layout_menu
|
||||
draw_pause_display_layout_menu(screen, getattr(config, 'pause_display_layout_selection', 0))
|
||||
elif config.menu_state == "pause_display_font_menu":
|
||||
from display import draw_pause_display_font_menu
|
||||
draw_pause_display_font_menu(screen, getattr(config, 'pause_display_font_selection', 0))
|
||||
elif config.menu_state == "pause_games_menu":
|
||||
from display import draw_pause_games_menu
|
||||
draw_pause_games_menu(screen, getattr(config, 'pause_games_selection', 0))
|
||||
@@ -1067,9 +1160,18 @@ async def main():
|
||||
elif config.menu_state == "pause_api_keys_status":
|
||||
from display import draw_pause_api_keys_status
|
||||
draw_pause_api_keys_status(screen)
|
||||
elif config.menu_state == "pause_connection_status":
|
||||
from display import draw_pause_connection_status
|
||||
draw_pause_connection_status(screen)
|
||||
elif config.menu_state == "filter_platforms":
|
||||
from display import draw_filter_platforms_menu
|
||||
draw_filter_platforms_menu(screen)
|
||||
elif config.menu_state == "filter_menu_choice":
|
||||
draw_filter_menu_choice(screen)
|
||||
elif config.menu_state == "filter_advanced":
|
||||
draw_filter_advanced(screen)
|
||||
elif config.menu_state == "filter_priority_config":
|
||||
draw_filter_priority_config(screen)
|
||||
elif config.menu_state == "controls_help":
|
||||
draw_controls_help(screen, config.previous_menu_state)
|
||||
elif config.menu_state == "history":
|
||||
@@ -1090,6 +1192,9 @@ async def main():
|
||||
elif config.menu_state == "history_error_details":
|
||||
from display import draw_history_error_details
|
||||
draw_history_error_details(screen)
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
from display import draw_text_file_viewer
|
||||
draw_text_file_viewer(screen)
|
||||
elif config.menu_state == "history_confirm_delete":
|
||||
from display import draw_history_confirm_delete
|
||||
draw_history_confirm_delete(screen)
|
||||
@@ -1105,6 +1210,18 @@ async def main():
|
||||
draw_cancel_download_dialog(screen)
|
||||
elif config.menu_state == "reload_games_data":
|
||||
draw_reload_games_data_dialog(screen)
|
||||
elif config.menu_state == "gamelist_update_prompt":
|
||||
from display import draw_gamelist_update_prompt
|
||||
draw_gamelist_update_prompt(screen)
|
||||
elif config.menu_state == "platform_folder_config":
|
||||
from display import draw_platform_folder_config_dialog
|
||||
draw_platform_folder_config_dialog(screen)
|
||||
elif config.menu_state == "folder_browser":
|
||||
from display import draw_folder_browser
|
||||
draw_folder_browser(screen)
|
||||
elif config.menu_state == "folder_browser_new_folder":
|
||||
from display import draw_folder_browser_new_folder
|
||||
draw_folder_browser_new_folder(screen)
|
||||
elif config.menu_state == "restart_popup":
|
||||
draw_popup(screen)
|
||||
elif config.menu_state == "accessibility_menu":
|
||||
@@ -1195,6 +1312,7 @@ async def main():
|
||||
config.loading_progress = 20.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
continue # Passer immédiatement à check_ota
|
||||
else:
|
||||
config.menu_state = "error"
|
||||
config.error_message = _("error_no_internet")
|
||||
@@ -1224,6 +1342,7 @@ async def main():
|
||||
config.loading_progress = 50.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
continue # Passer immédiatement à check_data
|
||||
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:
|
||||
@@ -1231,6 +1350,7 @@ async def main():
|
||||
config.loading_progress = 30.0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Dossier Data vide, début du téléchargement du ZIP")
|
||||
sources_zip_url = None # Initialiser pour éviter les erreurs
|
||||
try:
|
||||
zip_path = os.path.join(config.SAVE_FOLDER, "data_download.zip")
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
@@ -1336,13 +1456,51 @@ async def main():
|
||||
config.loading_progress = 80.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Dossier Data non vide, passage à {loading_step}")
|
||||
continue # Passer immédiatement à load_sources
|
||||
elif loading_step == "load_sources":
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
sources = load_sources()
|
||||
config.menu_state = "platform"
|
||||
config.loading_progress = 100.0
|
||||
config.current_loading_system = ""
|
||||
|
||||
# Vérifier si une mise à jour de la liste des jeux est nécessaire (seulement si pas déjà demandé)
|
||||
if not config.gamelist_update_prompted:
|
||||
from rgsx_settings import get_last_gamelist_update
|
||||
from config import GAMELIST_UPDATE_DAYS
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
last_update = get_last_gamelist_update()
|
||||
should_prompt_update = False
|
||||
|
||||
if last_update is None:
|
||||
# Première utilisation, proposer la mise à jour
|
||||
logger.info("Première utilisation détectée, proposition de mise à jour de la liste des jeux")
|
||||
should_prompt_update = True
|
||||
else:
|
||||
try:
|
||||
last_update_date = datetime.strptime(last_update, "%Y-%m-%d")
|
||||
days_since_update = (datetime.now() - last_update_date).days
|
||||
logger.info(f"Dernière mise à jour de la liste des jeux: {last_update} ({days_since_update} jours)")
|
||||
|
||||
if days_since_update >= GAMELIST_UPDATE_DAYS:
|
||||
logger.info(f"Mise à jour de la liste des jeux recommandée (>{GAMELIST_UPDATE_DAYS} jours)")
|
||||
should_prompt_update = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification de la date de mise à jour: {e}")
|
||||
|
||||
if should_prompt_update:
|
||||
config.menu_state = "gamelist_update_prompt"
|
||||
config.gamelist_update_selection = 1 # 0=Non, 1=Oui (par défaut)
|
||||
config.gamelist_update_prompted = True # Marquer comme déjà demandé
|
||||
logger.debug("Affichage du prompt de mise à jour de la liste des jeux")
|
||||
else:
|
||||
config.menu_state = "platform"
|
||||
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
|
||||
else:
|
||||
config.menu_state = "platform"
|
||||
logger.debug(f"Prompt déjà affiché, passage à platform, progress={config.loading_progress}")
|
||||
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
|
||||
|
||||
# Gestion de l'état de transition
|
||||
if config.transition_state == "to_game":
|
||||
|
||||
1
ports/RGSX/assets/ArchiveOrgCookie.txt
Normal file
1
ports/RGSX/assets/ArchiveOrgCookie.txt
Normal file
@@ -0,0 +1 @@
|
||||
donation-identifier=39546f3b2d3f67a664818596d81a5bec; abtest-identifier=fee0e28eb6c8d0de147d19db4303ee84; logged-in-sig=1802098179%201770562179%20AKHN8aF4EsFeR%2FundhgQTu0j27ZdFZXmgyUiqnJvXq%2BwtDGVvapqhKUFhIlI9bXAMYLMHDRJoO76bsqXI662nrIsx58efihNrafdk285r8MAdotWx03usO30baYoNPoMMEaK8iuhtbfTEyfE7oTZwdO7wjxNUTm%2Bbjjm6kmUD3HSQRzPsc0oWrrnd8Wj2x3UiuZeRnBfC60OjJHcnKC2Xv7teS%2BBx3EdKAG1i739MxTzjtEfERWw83bnaV30827qaFhZ%2BDK3%2FwCGOUwtablPA%2B0EeLR9%2BoYeC6x5aaJMZHBMjBowSIEE4QAK9IG9haBsn7%2F1PCweYuLivMIZJeA7mA%3D%3D; logged-in-user=rgsx%40outlook.fr
|
||||
@@ -7,7 +7,7 @@
|
||||
"cancel": {
|
||||
"type": "key",
|
||||
"key": 27,
|
||||
"display": "\u00c9chap"
|
||||
"display": "Esc/Echap"
|
||||
},
|
||||
"up": {
|
||||
"type": "key",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/arcade-beat.mp3
Normal file
BIN
ports/RGSX/assets/music/arcade-beat.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/arthurhale.mp3
Normal file
BIN
ports/RGSX/assets/music/arthurhale.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/battle-time.mp3
Normal file
BIN
ports/RGSX/assets/music/battle-time.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/dream-in-keygen.mp3
Normal file
BIN
ports/RGSX/assets/music/dream-in-keygen.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/happy-videogame.mp3
Normal file
BIN
ports/RGSX/assets/music/happy-videogame.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/man-is-he-mega.mp3
Normal file
BIN
ports/RGSX/assets/music/man-is-he-mega.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/moodmode-8-bit.mp3
Normal file
BIN
ports/RGSX/assets/music/moodmode-8-bit.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/niknet_art.mp3
Normal file
BIN
ports/RGSX/assets/music/niknet_art.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/pixel-dreams.mp3
Normal file
BIN
ports/RGSX/assets/music/pixel-dreams.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/pixelated-dreams.mp3
Normal file
BIN
ports/RGSX/assets/music/pixelated-dreams.mp3
Normal file
Binary file not shown.
BIN
ports/RGSX/assets/music/reflextunes-game-zone.mp3
Normal file
BIN
ports/RGSX/assets/music/reflextunes-game-zone.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ports/RGSX/assets/music/video-game-short.mp3
Normal file
BIN
ports/RGSX/assets/music/video-game-short.mp3
Normal file
Binary file not shown.
69
ports/RGSX/assets/progs/versionclean
Normal file
69
ports/RGSX/assets/progs/versionclean
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# BATOCERA SERVICE
|
||||
# name: Version Clean Service
|
||||
# description: Clean batocera-version output (hide extra services)
|
||||
# author: batocera-unofficial-addons
|
||||
# depends:
|
||||
# version: 1.0
|
||||
|
||||
SERVICE_NAME="versionclean"
|
||||
TARGET="/usr/bin/batocera-version"
|
||||
BACKUP="${TARGET}.bak"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
# If we've already backed up, assume it's already "cleaned"
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: already started (backup exists at ${BACKUP})."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${SERVICE_NAME}: backing up original ${TARGET} to ${BACKUP}..."
|
||||
cp "$TARGET" "$BACKUP"
|
||||
|
||||
echo "${SERVICE_NAME}: writing clean version script to ${TARGET}..."
|
||||
cat << 'EOF' > "$TARGET"
|
||||
#!/bin/bash
|
||||
|
||||
# Clean batocera-version
|
||||
# - "batocera-version --extra" -> "none"
|
||||
# - "batocera-version" -> contents of /usr/share/batocera/batocera.version
|
||||
|
||||
if test "$1" = "--extra"; then
|
||||
echo "none"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat /usr/share/batocera/batocera.version
|
||||
EOF
|
||||
|
||||
chmod +x "$TARGET"
|
||||
echo "${SERVICE_NAME}: clean version applied."
|
||||
;;
|
||||
|
||||
stop)
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: restoring original ${TARGET} from ${BACKUP}..."
|
||||
cp "$BACKUP" "$TARGET"
|
||||
rm "$BACKUP"
|
||||
echo "${SERVICE_NAME}: restore complete."
|
||||
else
|
||||
echo "${SERVICE_NAME}: no backup found, nothing to restore."
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: CLEAN VERSION ACTIVE (backup present)."
|
||||
else
|
||||
echo "${SERVICE_NAME}: ORIGINAL VERSION ACTIVE."
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -1,6 +1,19 @@
|
||||
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Game:
|
||||
name: str
|
||||
url: str
|
||||
size: str
|
||||
display_name: str # name withou file extension or platform prefix
|
||||
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"
|
||||
@@ -13,7 +26,10 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.3.2.6"
|
||||
app_version = "2.6.0.1"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 7
|
||||
|
||||
|
||||
def get_application_root():
|
||||
@@ -133,9 +149,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# File d'attente de téléchargements (jobs en attente)
|
||||
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
|
||||
pending_download_is_queue = False # Indique si pending_download doit être ajouté à la queue
|
||||
# Indique si un téléchargement est en cours
|
||||
download_active = False
|
||||
|
||||
# Cache status de connexion (menu pause > settings)
|
||||
connection_status = {}
|
||||
connection_status_timestamp = 0.0
|
||||
connection_status_in_progress = False
|
||||
connection_status_progress = {"done": 0, "total": 0}
|
||||
|
||||
# Log directory
|
||||
# Docker mode: /config/logs (persisted in config volume)
|
||||
# Traditional mode: /app/RGSX/logs (current behavior)
|
||||
@@ -174,7 +197,9 @@ DOWNLOADED_GAMES_PATH = os.path.join(SAVE_FOLDER, "downloaded_games.json")
|
||||
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
|
||||
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
|
||||
API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.txt")
|
||||
API_KEY_DEBRIDLINK_PATH = os.path.join(SAVE_FOLDER, "DebridLinkAPI.txt")
|
||||
API_KEY_REALDEBRID_PATH = os.path.join(SAVE_FOLDER, "RealDebridAPI.txt")
|
||||
ARCHIVE_ORG_COOKIE_PATH = os.path.join(APP_FOLDER, "assets", "ArchiveOrgCookie.txt")
|
||||
|
||||
|
||||
|
||||
@@ -342,6 +367,7 @@ platforms = [] # Liste des plateformes disponibles
|
||||
current_platform = 0 # Index de la plateforme actuelle sélectionnée
|
||||
platform_names = {} # {platform_id: platform_name}
|
||||
games_count = {} # Dictionnaire comptant le nombre de jeux par plateforme
|
||||
games_count_log_verbose = False # Log détaillé par fichier (sinon résumé compact)
|
||||
platform_dicts = [] # Liste des dictionnaires de plateformes
|
||||
|
||||
# Filtre plateformes
|
||||
@@ -351,7 +377,8 @@ filter_platforms_dirty = False # indique si modifications non sauvegardées
|
||||
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
|
||||
|
||||
# Affichage des jeux et sélection
|
||||
games = [] # Liste des jeux pour la plateforme actuelle
|
||||
games: list[Game] = [] # Liste des jeux pour la plateforme actuelle
|
||||
fbneo_games = {}
|
||||
current_game = 0 # Index du jeu actuellement sélectionné
|
||||
menu_state = "loading" # État actuel de l'interface menu
|
||||
scroll_offset = 0 # Offset de défilement pour la liste des jeux
|
||||
@@ -374,11 +401,23 @@ sources_mode = "rgsx" # Mode des sources de jeux (rgsx/custom)
|
||||
custom_sources_url = {OTA_data_ZIP} # URL personnalisée si mode custom
|
||||
selected_language_index = 0 # Index de la langue sélectionnée dans la liste
|
||||
|
||||
|
||||
# Recherche et filtres
|
||||
filtered_games = [] # Liste des jeux filtrés par recherche ou filtre
|
||||
filtered_games: list[Game] = [] # Liste des jeux filtrés par recherche ou filtre
|
||||
search_mode = False # Indicateur si le mode recherche est actif
|
||||
search_query = "" # Chaîne de recherche saisie par l'utilisateur
|
||||
filter_active = False # Indicateur si un filtre est appliqué
|
||||
global_search_index = [] # Index de recherche global {platform, jeu} construit a l'ouverture
|
||||
global_search_results = [] # Resultats de la recherche globale inter-plateformes
|
||||
global_search_query = "" # Texte saisi pour la recherche globale
|
||||
global_search_selected = 0 # Index du resultat global selectionne
|
||||
global_search_scroll_offset = 0 # Offset de defilement des resultats globaux
|
||||
global_search_editing = False # True si le clavier virtuel est actif pour la recherche globale
|
||||
|
||||
# Variables pour le filtrage avancé
|
||||
selected_filter_choice = 0 # Index dans le menu de choix de filtrage (recherche / avancé)
|
||||
selected_filter_option = 0 # Index dans le menu de filtrage avancé
|
||||
game_filter_obj = None # Objet GameFilters pour le filtrage avancé
|
||||
|
||||
# Gestion des états du menu
|
||||
needs_redraw = False # Indicateur si l'écran doit être redessiné
|
||||
@@ -419,9 +458,11 @@ scraper_game_page_url = "" # URL de la page du jeu sur TheGamesDB
|
||||
# CLES API / PREMIUM HOSTS
|
||||
API_KEY_1FICHIER = ""
|
||||
API_KEY_ALLDEBRID = ""
|
||||
API_KEY_DEBRIDLINK = ""
|
||||
API_KEY_REALDEBRID = ""
|
||||
PREMIUM_HOST_MARKERS = [
|
||||
"1Fichier",
|
||||
"Debrid-Link",
|
||||
]
|
||||
hide_premium_systems = False # Indicateur pour masquer les systèmes premium
|
||||
|
||||
@@ -442,6 +483,22 @@ confirm_press_start_time = 0 # Timestamp du début de l'appui sur confirm
|
||||
confirm_long_press_threshold = 2000 # Durée en ms pour déclencher l'appui long (2 secondes)
|
||||
confirm_long_press_triggered = False # Flag pour éviter de déclencher plusieurs fois
|
||||
|
||||
# Détection d'appui long sur confirm (menu platform - pour config dossier)
|
||||
platform_confirm_press_start_time = 0 # Timestamp du début de l'appui sur confirm dans le menu platform
|
||||
platform_confirm_long_press_triggered = False # Flag pour éviter de déclencher plusieurs fois
|
||||
|
||||
# Configuration dossier personnalisé par plateforme
|
||||
platform_config_name = "" # Nom de la plateforme en cours de configuration
|
||||
platform_folder_selection = 0 # Index de sélection dans le menu de config dossier (0=Current, 1=Browse, 2=Reset, 3=Cancel)
|
||||
|
||||
# Navigateur de dossiers intégré (folder browser)
|
||||
folder_browser_path = "" # Chemin actuel dans le navigateur
|
||||
folder_browser_items = [] # Liste des éléments (dossiers) dans le répertoire actuel
|
||||
folder_browser_selection = 0 # Index de l'élément sélectionné
|
||||
folder_browser_scroll_offset = 0 # Offset de défilement
|
||||
folder_browser_visible_items = 10 # Nombre d'éléments visibles
|
||||
folder_browser_mode = "platform" # "platform" pour dossier plateforme, "roms_root" pour dossier ROMs principal
|
||||
|
||||
# Tenter la récupération de la famille de police sauvegardée
|
||||
try:
|
||||
from rgsx_settings import get_font_family # import tardif pour éviter dépendances circulaires lors de l'exécution initiale
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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-",
|
||||
@@ -303,22 +303,43 @@ def _images_base_dir() -> str:
|
||||
|
||||
def _action_icon_filename(action_name: str) -> Optional[str]:
|
||||
# Map actions to icon filenames present in assets/images
|
||||
mapping = {
|
||||
"up": "dpad_up.svg",
|
||||
"down": "dpad_down.svg",
|
||||
"left": "dpad_left.svg",
|
||||
"right": "dpad_right.svg",
|
||||
"confirm": "buttons_south.svg", # A (south)
|
||||
"cancel": "buttons_east.svg", # B (east)
|
||||
"clear_history": "buttons_west.svg", # X (west)
|
||||
"history": "buttons_north.svg", # Y (north)
|
||||
"start": "button_start.svg",
|
||||
"filter": "button_select.svg",
|
||||
"delete": "button_l.svg", # LB
|
||||
"space": "button_r.svg", # RB
|
||||
"page_up": "button_lt.svg",
|
||||
"page_down": "button_rt.svg",
|
||||
}
|
||||
# Option d'inversion ABXY (A/B <-> X/Y) via config.nintendo_layout
|
||||
|
||||
is_nintendo = getattr(config, 'nintendo_layout', False)
|
||||
if is_nintendo:
|
||||
mapping = {
|
||||
"up": "dpad_up.svg",
|
||||
"down": "dpad_down.svg",
|
||||
"left": "dpad_left.svg",
|
||||
"right": "dpad_right.svg",
|
||||
"confirm": "buttons_east.svg",
|
||||
"cancel": "buttons_south.svg",
|
||||
"clear_history": "buttons_west.svg",
|
||||
"history": "buttons_north.svg",
|
||||
"start": "button_start.svg",
|
||||
"filter": "button_select.svg",
|
||||
"delete": "button_l.svg",
|
||||
"space": "button_r.svg",
|
||||
"page_up": "button_lt.svg",
|
||||
"page_down": "button_rt.svg",
|
||||
}
|
||||
else:
|
||||
mapping = {
|
||||
"up": "dpad_up.svg",
|
||||
"down": "dpad_down.svg",
|
||||
"left": "dpad_left.svg",
|
||||
"right": "dpad_right.svg",
|
||||
"confirm": "buttons_south.svg",
|
||||
"cancel": "buttons_east.svg",
|
||||
"clear_history": "buttons_north.svg",
|
||||
"history": "buttons_west.svg",
|
||||
"start": "button_start.svg",
|
||||
"filter": "button_select.svg",
|
||||
"delete": "button_l.svg",
|
||||
"space": "button_r.svg",
|
||||
"page_up": "button_lt.svg",
|
||||
"page_down": "button_rt.svg",
|
||||
}
|
||||
return mapping.get(action_name)
|
||||
|
||||
def _load_svg_icon_surface(svg_path: str, size: int) -> Optional[pygame.Surface]:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
300
ports/RGSX/game_filters.py
Normal file
300
ports/RGSX/game_filters.py
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Module de filtrage des jeux pour RGSX
|
||||
Partagé entre l'interface graphique et l'interface web
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
from config import Game
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GameFilters:
|
||||
"""Classe pour gérer les filtres de jeux"""
|
||||
|
||||
# Régions disponibles
|
||||
REGIONS = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other']
|
||||
|
||||
def __init__(self):
|
||||
# Initialiser toutes les régions en mode 'include' par défaut
|
||||
self.region_filters = {region: 'include' for region in self.REGIONS}
|
||||
self.hide_non_release = False
|
||||
self.one_rom_per_game = False
|
||||
self.regex_mode = False
|
||||
self.region_priority = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other']
|
||||
|
||||
def load_from_dict(self, filter_dict: Dict[str, Any]):
|
||||
"""Charge les filtres depuis un dictionnaire (depuis settings)"""
|
||||
loaded_region_filters = filter_dict.get('region_filters', {})
|
||||
# Initialiser toutes les régions en 'include' par défaut, puis appliquer celles chargées
|
||||
self.region_filters = {region: 'include' for region in self.REGIONS}
|
||||
self.region_filters.update(loaded_region_filters)
|
||||
|
||||
self.hide_non_release = filter_dict.get('hide_non_release', False)
|
||||
self.one_rom_per_game = filter_dict.get('one_rom_per_game', False)
|
||||
self.regex_mode = filter_dict.get('regex_mode', False)
|
||||
self.region_priority = filter_dict.get('region_priority',
|
||||
['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convertit les filtres en dictionnaire (pour sauvegarder dans settings)"""
|
||||
return {
|
||||
'region_filters': self.region_filters,
|
||||
'hide_non_release': self.hide_non_release,
|
||||
'one_rom_per_game': self.one_rom_per_game,
|
||||
'regex_mode': self.regex_mode,
|
||||
'region_priority': self.region_priority
|
||||
}
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Vérifie si des filtres sont actifs (au moins une région en exclude ou options activées)"""
|
||||
has_exclude = any(state == 'exclude' for state in self.region_filters.values())
|
||||
return (has_exclude or
|
||||
self.hide_non_release or
|
||||
self.one_rom_per_game)
|
||||
|
||||
def reset(self):
|
||||
"""Réinitialise tous les filtres (toutes les régions en include)"""
|
||||
self.region_filters = {region: 'include' for region in self.REGIONS}
|
||||
self.hide_non_release = False
|
||||
self.one_rom_per_game = False
|
||||
self.regex_mode = False
|
||||
|
||||
@staticmethod
|
||||
def get_game_regions(game_name: str) -> List[str]:
|
||||
"""Extrait les régions d'un nom de jeu"""
|
||||
name = game_name.upper()
|
||||
regions = []
|
||||
|
||||
# Patterns de région communs - chercher les codes entre parenthèses d'abord
|
||||
# Codes de région/langue dans les parenthèses (Ex: (Fr,De) ou (En,Nl))
|
||||
paren_content = re.findall(r'\(([^)]+)\)', name)
|
||||
for content in paren_content:
|
||||
# Codes de langue/région séparés par virgules
|
||||
codes = [c.strip() for c in content.split(',')]
|
||||
for code in codes:
|
||||
if code in ['FR', 'FRA']:
|
||||
if 'France' not in regions:
|
||||
regions.append('France')
|
||||
elif code in ['DE', 'GER', 'DEU']:
|
||||
if 'Germany' not in regions:
|
||||
regions.append('Germany')
|
||||
elif code in ['EN', 'ENG'] or code.startswith('EN-'):
|
||||
# EN peut être USA, Europe ou autre - on vérifie le contexte
|
||||
if 'EU' in codes or 'EUR' in codes:
|
||||
if 'Europe' not in regions:
|
||||
regions.append('Europe')
|
||||
elif code in ['ES', 'ESP', 'SPA']:
|
||||
if 'Other' not in regions:
|
||||
regions.append('Other')
|
||||
elif code in ['IT', 'ITA']:
|
||||
if 'Other' not in regions:
|
||||
regions.append('Other')
|
||||
elif code in ['NL', 'NLD', 'DU', 'DUT']:
|
||||
if 'Europe' not in regions:
|
||||
regions.append('Europe')
|
||||
elif code in ['PT', 'POR']:
|
||||
if 'Other' not in regions:
|
||||
regions.append('Other')
|
||||
|
||||
# Patterns de région complets (mots entiers)
|
||||
if 'USA' in name or 'US)' in name or re.search(r'\bUS\b', name):
|
||||
if 'USA' not in regions:
|
||||
regions.append('USA')
|
||||
if 'CANADA' in name or 'CA)' in name:
|
||||
if 'Canada' not in regions:
|
||||
regions.append('Canada')
|
||||
if 'EUROPE' in name or 'EU)' in name or re.search(r'\bEU\b', name):
|
||||
if 'Europe' not in regions:
|
||||
regions.append('Europe')
|
||||
if 'FRANCE' in name or 'FR)' in name:
|
||||
if 'France' not in regions:
|
||||
regions.append('France')
|
||||
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
|
||||
if 'Germany' not in regions:
|
||||
regions.append('Germany')
|
||||
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name or re.search(r'\bJP\b', name):
|
||||
if 'Japan' not in regions:
|
||||
regions.append('Japan')
|
||||
if 'KOREA' in name or 'KR)' in name or 'KOR)' in name:
|
||||
if 'Korea' not in regions:
|
||||
regions.append('Korea')
|
||||
if 'WORLD' in name:
|
||||
if 'World' not in regions:
|
||||
regions.append('World')
|
||||
|
||||
# Autres régions
|
||||
if re.search(r'\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
|
||||
r'SPAIN|ITALY)\b', name):
|
||||
if 'Other' not in regions:
|
||||
regions.append('Other')
|
||||
|
||||
# Si aucune région trouvée
|
||||
if not regions:
|
||||
regions.append('Other')
|
||||
|
||||
return regions
|
||||
|
||||
@staticmethod
|
||||
def is_non_release_game(game_name: str) -> bool:
|
||||
"""Vérifie si un jeu est une version non-release (demo, beta, proto)"""
|
||||
name = game_name.upper()
|
||||
non_release_patterns = [
|
||||
r'\([^\)]*BETA[^\)]*\)',
|
||||
r'\([^\)]*DEMO[^\)]*\)',
|
||||
r'\([^\)]*PROTO[^\)]*\)',
|
||||
r'\([^\)]*SAMPLE[^\)]*\)',
|
||||
r'\([^\)]*KIOSK[^\)]*\)',
|
||||
r'\([^\)]*PREVIEW[^\)]*\)',
|
||||
r'\([^\)]*TEST[^\)]*\)',
|
||||
r'\([^\)]*DEBUG[^\)]*\)',
|
||||
r'\([^\)]*ALPHA[^\)]*\)',
|
||||
r'\([^\)]*PRE-RELEASE[^\)]*\)',
|
||||
r'\([^\)]*PRERELEASE[^\)]*\)',
|
||||
r'\([^\)]*UNFINISHED[^\)]*\)',
|
||||
r'\([^\)]*WIP[^\)]*\)',
|
||||
r'\([^\)]*BOOTLEG[^\)]*\)',
|
||||
]
|
||||
return any(re.search(pattern, name) for pattern in non_release_patterns)
|
||||
|
||||
@staticmethod
|
||||
def get_base_game_name(game_name: str) -> str:
|
||||
"""Obtient le nom de base du jeu (sans régions, versions, etc.)"""
|
||||
base = game_name
|
||||
|
||||
# Supprimer extensions
|
||||
base = re.sub(r'\.(zip|7z|rar|gz|iso)$', '', base, flags=re.IGNORECASE)
|
||||
|
||||
# Extraire info disque si présent
|
||||
disc_info = ''
|
||||
disc_match = (re.search(r'\(Dis[ck]\s*(\d+)\)', base, re.IGNORECASE) or
|
||||
re.search(r'\[Dis[ck]\s*(\d+)\]', base, re.IGNORECASE) or
|
||||
re.search(r'Dis[ck]\s*(\d+)', base, re.IGNORECASE) or
|
||||
re.search(r'\(CD\s*(\d+)\)', base, re.IGNORECASE) or
|
||||
re.search(r'CD\s*(\d+)', base, re.IGNORECASE))
|
||||
if disc_match:
|
||||
disc_info = f' (Disc {disc_match.group(1)})'
|
||||
|
||||
# Supprimer contenu entre parenthèses et crochets
|
||||
base = re.sub(r'\([^)]*\)', '', base)
|
||||
base = re.sub(r'\[[^\]]*\]', '', base)
|
||||
|
||||
# Normaliser espaces
|
||||
base = re.sub(r'\s+', ' ', base).strip()
|
||||
|
||||
# Rajouter info disque
|
||||
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: Game) -> int:
|
||||
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
|
||||
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
|
||||
|
||||
for region in game_regions:
|
||||
try:
|
||||
priority = self.region_priority.index(region)
|
||||
if priority < best_priority:
|
||||
best_priority = priority
|
||||
except ValueError:
|
||||
# La région n'est pas dans la liste de priorité
|
||||
continue
|
||||
|
||||
return best_priority
|
||||
|
||||
def apply_filters(self, games: list[Game]) -> list[Game]:
|
||||
"""
|
||||
Applique les filtres à une liste de jeux
|
||||
games: Liste de tuples (game_name, game_url, size)
|
||||
Retourne: Liste filtrée de tuples
|
||||
"""
|
||||
if not self.is_active():
|
||||
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:
|
||||
# Vérifier les filtres de région
|
||||
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
|
||||
|
||||
for region in game_regions:
|
||||
filter_state = self.region_filters.get(region, 'include')
|
||||
if filter_state == 'include':
|
||||
has_included_region = True
|
||||
break # Si on trouve une région incluse, c'est bon
|
||||
|
||||
# Le jeu est affiché seulement s'il a au moins une région incluse
|
||||
if not has_included_region:
|
||||
continue
|
||||
|
||||
# Filtrer les non-release
|
||||
if self.hide_non_release and self.get_cached_non_release(game):
|
||||
continue
|
||||
|
||||
filtered_games.append(game)
|
||||
|
||||
# Appliquer "one rom per game"
|
||||
if self.one_rom_per_game:
|
||||
filtered_games = self._apply_one_rom_per_game(filtered_games)
|
||||
|
||||
return filtered_games
|
||||
|
||||
def _apply_one_rom_per_game(self, games: List[Game]) -> List[Game]:
|
||||
"""Garde seulement une ROM par jeu selon la priorité de région"""
|
||||
games_by_base = {}
|
||||
|
||||
for game in games:
|
||||
base_name = self.get_cached_base_name(game)
|
||||
|
||||
if base_name not in games_by_base:
|
||||
games_by_base[base_name] = []
|
||||
|
||||
games_by_base[base_name].append(game)
|
||||
|
||||
# Pour chaque jeu de base, garder celui avec la meilleure priorité
|
||||
result = []
|
||||
for base_name, game_list in games_by_base.items():
|
||||
if len(game_list) == 1:
|
||||
result.append(game_list[0])
|
||||
else:
|
||||
# Trier par priorité de région
|
||||
sorted_games = sorted(game_list,
|
||||
key=self.get_region_priority)
|
||||
result.append(sorted_games[0])
|
||||
|
||||
return result
|
||||
@@ -119,18 +119,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 +161,115 @@ 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 l'extension."""
|
||||
if not isinstance(game_name, str):
|
||||
return ""
|
||||
|
||||
normalized = os.path.basename(game_name.strip())
|
||||
if not normalized:
|
||||
return ""
|
||||
|
||||
return os.path.splitext(normalized)[0].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 +292,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 +308,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 +332,22 @@ def save_downloaded_games(downloaded_games_dict):
|
||||
def mark_game_as_downloaded(platform_name, game_name, file_size=None):
|
||||
"""Marque un jeu comme téléchargé."""
|
||||
downloaded = config.downloaded_games
|
||||
normalized_name = normalize_downloaded_game_name(game_name)
|
||||
if not normalized_name:
|
||||
return
|
||||
|
||||
if platform_name not in downloaded:
|
||||
downloaded[platform_name] = {}
|
||||
|
||||
downloaded[platform_name][game_name] = {
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"size": file_size or "N/A"
|
||||
}
|
||||
downloaded[platform_name][normalized_name] = {}
|
||||
|
||||
# Sauvegarder immédiatement
|
||||
save_downloaded_games(downloaded)
|
||||
logger.info(f"Jeu marqué comme téléchargé : {platform_name} / {game_name}")
|
||||
logger.info(f"Jeu marqué comme téléchargé : {platform_name} / {normalized_name}")
|
||||
|
||||
|
||||
def is_game_downloaded(platform_name, game_name):
|
||||
"""Vérifie si un jeu a déjà été téléchargé."""
|
||||
downloaded = config.downloaded_games
|
||||
return platform_name in downloaded and game_name in downloaded.get(platform_name, {})
|
||||
normalized_name = normalize_downloaded_game_name(game_name)
|
||||
return bool(normalized_name) and platform_name in downloaded and normalized_name in downloaded.get(platform_name, {})
|
||||
|
||||
@@ -114,14 +114,14 @@ def get_text(key, default=None):
|
||||
pass
|
||||
return str(default) if default is not None else str(key)
|
||||
|
||||
def get_available_languages():
|
||||
def get_available_languages() -> list[str]:
|
||||
"""Récupère la liste des langues disponibles."""
|
||||
|
||||
if not os.path.exists(config.LANGUAGES_FOLDER):
|
||||
logger.warning(f"Dossier des langues {config.LANGUAGES_FOLDER} non trouvé")
|
||||
return []
|
||||
|
||||
languages = []
|
||||
languages: list[str] = []
|
||||
for file in os.listdir(config.LANGUAGES_FOLDER):
|
||||
if file.endswith(".json"):
|
||||
lang_code = os.path.splitext(file)[0]
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
"game_count": "{0} ({1} Spiele)",
|
||||
"game_filter": "Aktiver Filter: {0}",
|
||||
"game_search": "Filtern: {0}",
|
||||
"global_search_title": "Globale Suche: {0}",
|
||||
"global_search_empty_query": "Geben Sie einen Namen ein, um alle Systeme zu durchsuchen",
|
||||
"global_search_no_results": "Keine Ergebnisse fur: {0}",
|
||||
"game_header_name": "Name",
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Größe",
|
||||
"history_title": "Downloads ({0})",
|
||||
"history_empty": "Keine Downloads im Verlauf",
|
||||
@@ -44,6 +48,7 @@
|
||||
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download vom Benutzer abgebrochen.",
|
||||
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
|
||||
"extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?",
|
||||
"extension_warning_unsupported": "Die Dateierweiterung für '{0}' wird laut der Konfiguration es_systems.cfg von Batocera nicht unterstützt. Möchtest du fortfahren?",
|
||||
"extension_warning_enable_unknown_hint": "\nUm diese Meldung auszublenden: \"Warnung bei unbekannter Erweiterung ausblenden\" in Pausenmenü > Anzeige aktivieren",
|
||||
@@ -51,6 +56,8 @@
|
||||
"confirm_exit_with_downloads": "Achtung: {0} Download(s) laufen. Trotzdem beenden?",
|
||||
"confirm_clear_history": "Verlauf löschen?",
|
||||
"confirm_redownload_cache": "Spieleliste aktualisieren?",
|
||||
"gamelist_update_prompt_with_date": "Die Spieleliste wurde seit mehr als {0} Tagen nicht aktualisiert (letzte Aktualisierung: {1}). Die neueste Version herunterladen?",
|
||||
"gamelist_update_prompt_first_time": "Möchten Sie die neueste Spieleliste herunterladen?",
|
||||
"popup_redownload_success": "Cache gelöscht, bitte die Anwendung neu starten",
|
||||
"popup_no_cache": "Kein Cache gefunden.\nBitte starte die Anwendung neu, um die Spiele zu laden.",
|
||||
"popup_countdown": "Diese Nachricht schließt in {0} Sekunde{1}",
|
||||
@@ -59,15 +66,32 @@
|
||||
"language_changed": "Sprache geändert zu {0}",
|
||||
"menu_controls": "Steuerung",
|
||||
"menu_remap_controls": "Steuerung neu zuordnen",
|
||||
"menu_nintendo_layout_on": "Nintendo-Controller-Layout",
|
||||
"menu_nintendo_layout_off": "Xbox-Controller-Layout",
|
||||
"instruction_nintendo_layout": "Invertiert die angezeigten Steuerungen, um das Layout anzupassen",
|
||||
"controller_style_label": "Controller-Stil :",
|
||||
"controller_style_nintendo": "Nintendo",
|
||||
"controller_style_xbox": "Xbox",
|
||||
"menu_history": "Verlauf",
|
||||
"menu_language": "Sprache",
|
||||
"menu_accessibility": "Barrierefreiheit",
|
||||
"menu_display": "Anzeige",
|
||||
"display_layout": "Anzeigelayout",
|
||||
"display_monitor": "Monitor",
|
||||
"display_monitor_single": "Einzelner Monitor",
|
||||
"display_monitor_single_only": "Nur ein Monitor erkannt",
|
||||
"display_monitor_restart_required": "Neustart erforderlich um Monitor zu ändern",
|
||||
"display_mode": "Anzeigemodus",
|
||||
"display_fullscreen": "Vollbild",
|
||||
"display_windowed": "Fenster",
|
||||
"display_mode_restart_required": "Neustart erforderlich für Modusänderung",
|
||||
"display_light_mode": "Performance-Modus",
|
||||
"display_light_mode_enabled": "Performance-Modus aktiviert - Effekte deaktiviert",
|
||||
"display_light_mode_disabled": "Performance-Modus deaktiviert - Effekte aktiviert",
|
||||
"menu_redownload_cache": "Spieleliste aktualisieren",
|
||||
"menu_music_enabled": "Musik aktiviert: {0}",
|
||||
"menu_music_disabled": "Musik deaktiviert",
|
||||
"menu_restart": "Neustart",
|
||||
"menu_restart": "RGSX neu starten",
|
||||
"menu_support": "Unterstützung",
|
||||
"menu_filter_platforms": "Systeme filtern",
|
||||
"filter_platforms_title": "Systemsichtbarkeit",
|
||||
@@ -80,6 +104,7 @@
|
||||
"menu_allow_unknown_ext_enabled": "Ausblenden der Warnung bei unbekannter Erweiterung aktiviert",
|
||||
"menu_allow_unknown_ext_disabled": "Ausblenden der Warnung bei unbekannter Erweiterung deaktiviert",
|
||||
"menu_quit": "Beenden",
|
||||
"menu_quit_app": "RGSX beenden",
|
||||
"support_dialog_title": "Support-Datei",
|
||||
"support_dialog_message": "Eine Support-Datei wurde mit allen Ihren Konfigurations- und Protokolldateien erstellt.\n\nDatei: {0}\n\nUm Hilfe zu erhalten:\n1. Treten Sie dem RGSX Discord-Server bei\n2. Beschreiben Sie Ihr Problem\n3. Teilen Sie diese ZIP-Datei\n\nDrücken Sie {1}, um zum Menü zurückzukehren.",
|
||||
"support_dialog_error": "Fehler beim Erstellen der Support-Datei:\n{0}\n\nDrücken Sie {1}, um zum Menü zurückzukehren.",
|
||||
@@ -90,6 +115,7 @@
|
||||
"controls_action_clear_history": "Verlauf leeren",
|
||||
"controls_action_history": "Verlauf / Downloads",
|
||||
"controls_action_close_history": "Verlauf schließen",
|
||||
"history_column_folder": "Ordner",
|
||||
"controls_action_queue": "Warteschlange",
|
||||
"controls_action_delete": "Löschen",
|
||||
"controls_action_space": "Leerzeichen",
|
||||
@@ -123,6 +149,8 @@
|
||||
"controls_confirm_select": "Bestätigen/Auswählen",
|
||||
"controls_cancel_back": "Abbrechen/Zurück",
|
||||
"controls_filter_search": "Filtern/Suchen",
|
||||
"controls_action_edit_search": "Suche bearbeiten",
|
||||
"controls_action_show_results": "Ergebnisse zeigen",
|
||||
"network_download_failed": "Download nach {0} Versuchen fehlgeschlagen",
|
||||
"network_api_error": "Fehler bei der API-Anfrage, der Schlüssel könnte falsch sein: {0}",
|
||||
"network_download_error": "Downloadfehler {0}: {1}",
|
||||
@@ -168,7 +196,14 @@
|
||||
"status_present": "Vorhanden",
|
||||
"status_missing": "Fehlt",
|
||||
"menu_api_keys_status": "API-Schlüssel",
|
||||
"menu_connection_status": "Verbindungsstatus",
|
||||
"api_keys_status_title": "Status der API-Schlüssel",
|
||||
"connection_status_title": "Verbindungsstatus",
|
||||
"connection_status_category_updates": "App-/Gamelist-Update",
|
||||
"connection_status_category_sources": "Spielquellen",
|
||||
"connection_status_checking": "Prüfe...",
|
||||
"connection_status_progress": "Prüfe... {done}/{total}",
|
||||
"connection_status_last_check": "Letzte Prüfung: {time}",
|
||||
"menu_games": "Spiele",
|
||||
"api_keys_hint_manage": "Legen Sie Ihre Schlüssel in {path}",
|
||||
"api_key_empty_suffix": "leer",
|
||||
@@ -184,7 +219,9 @@
|
||||
"instruction_pause_settings": "Musik, Symlink-Option & API-Schlüsselstatus",
|
||||
"instruction_pause_restart": "RGSX neu starten um Konfiguration neu zu laden",
|
||||
"instruction_pause_support": "Eine Diagnose-ZIP-Datei für den Support erstellen",
|
||||
"instruction_pause_quit": "RGSX Anwendung beenden",
|
||||
"instruction_pause_quit": "Menü für Beenden oder Neustart aufrufen",
|
||||
"instruction_quit_app": "RGSX Anwendung beenden",
|
||||
"instruction_quit_restart": "RGSX Anwendung neu starten",
|
||||
"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen",
|
||||
"instruction_controls_remap": "Tasten / Buttons neu zuordnen",
|
||||
"instruction_generic_back": "Zum vorherigen Menü zurückkehren",
|
||||
@@ -192,6 +229,9 @@
|
||||
"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen",
|
||||
"instruction_display_footer_font_size": "Fußzeilen-Textgröße anpassen (Version & Steuerelemente)",
|
||||
"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln",
|
||||
"instruction_display_monitor": "Monitor für RGSX-Anzeige auswählen",
|
||||
"instruction_display_mode": "Zwischen Vollbild und Fenstermodus wechseln",
|
||||
"instruction_display_light_mode": "Performance-Modus für bessere FPS aktivieren",
|
||||
"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden",
|
||||
"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten",
|
||||
"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}",
|
||||
@@ -199,11 +239,24 @@
|
||||
"instruction_games_history": "Vergangene Downloads und Status anzeigen",
|
||||
"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln",
|
||||
"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren",
|
||||
"instruction_games_scan_owned": "ROM-Ordner scannen und bereits vorhandene Spiele markieren",
|
||||
"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren",
|
||||
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
|
||||
"instruction_settings_auto_extract": "Automatische Archivextraktion nach Download aktivieren/deaktivieren",
|
||||
"instruction_settings_roms_folder": "Standard-Download-Verzeichnis für ROMs ändern",
|
||||
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
|
||||
"instruction_settings_connection_status": "Zugriff auf Update- und Quellen-Seiten prüfen",
|
||||
"instruction_settings_web_service": "Web-Dienst Autostart beim Booten aktivieren/deaktivieren",
|
||||
"instruction_settings_custom_dns": "Custom DNS (Cloudflare 1.1.1.1) beim Booten aktivieren/deaktivieren",
|
||||
"settings_auto_extract": "Auto-Extraktion Archive",
|
||||
"settings_auto_extract_enabled": "Aktiviert",
|
||||
"settings_auto_extract_disabled": "Deaktiviert",
|
||||
"settings_roms_folder": "ROMs-Ordner",
|
||||
"settings_roms_folder_default": "Standard",
|
||||
"roms_folder_set": "ROMs-Ordner festgelegt: {0}",
|
||||
"roms_folder_set_restart": "ROMs-Ordner festgelegt: {0}\nNeustart erforderlich!",
|
||||
"roms_folder_reset": "ROMs-Ordner auf Standard zurückgesetzt\nNeustart erforderlich!",
|
||||
"folder_browser_title_roms_root": "Standard-ROMs-Ordner auswählen",
|
||||
"settings_web_service": "Web-Dienst beim Booten",
|
||||
"settings_web_service_enabled": "Aktiviert",
|
||||
"settings_web_service_disabled": "Deaktiviert",
|
||||
@@ -213,6 +266,9 @@
|
||||
"settings_web_service_success_disabled": "Web-Dienst beim Booten deaktiviert",
|
||||
"settings_web_service_error": "Fehler: {0}",
|
||||
"settings_custom_dns": "Custom DNS beim Booten",
|
||||
"menu_scan_owned_roms": "Vorhandene ROMs scannen",
|
||||
"popup_scan_owned_roms_done": "ROM-Scan abgeschlossen: {0} Spiele auf {1} Plattformen hinzugefügt",
|
||||
"popup_scan_owned_roms_error": "ROM-Scan-Fehler: {0}",
|
||||
"settings_custom_dns_enabled": "Aktiviert",
|
||||
"settings_custom_dns_disabled": "Deaktiviert",
|
||||
"settings_custom_dns_enabling": "Custom DNS wird aktiviert...",
|
||||
@@ -242,7 +298,12 @@
|
||||
"history_game_options_title": "Spiel Optionen",
|
||||
"history_option_download_folder": "Datei lokalisieren",
|
||||
"history_option_extract_archive": "Archiv extrahieren",
|
||||
"history_option_scraper": "Metadaten scrapen",
|
||||
"history_option_open_file": "Datei öffnen",
|
||||
"history_option_scraper": "Metadaten abrufen",
|
||||
"history_option_remove_from_queue": "Aus Warteschlange entfernen",
|
||||
"history_option_cancel_download": "Download abbrechen",
|
||||
"history_option_pause_download": "Download pausieren",
|
||||
"history_option_resume_download": "Download fortsetzen",
|
||||
"history_option_delete_game": "Spiel löschen",
|
||||
"history_option_error_info": "Fehlerdetails",
|
||||
"history_option_retry": "Download wiederholen",
|
||||
@@ -305,6 +366,9 @@
|
||||
"web_settings_source_mode": "Spielequelle",
|
||||
"web_settings_custom_url": "Benutzerdefinierte URL",
|
||||
"web_settings_custom_url_placeholder": "https://beispiel.com/spiele.zip",
|
||||
"web_settings_auto_extract": "Archive nach dem Download automatisch entpacken",
|
||||
"web_settings_web_service": "Webdienst beim Booten starten",
|
||||
"web_settings_custom_dns": "Benutzerdefinierten DNS beim Booten aktivieren",
|
||||
"web_settings_save": "Einstellungen speichern",
|
||||
"web_settings_saved": "Einstellungen erfolgreich gespeichert!",
|
||||
"web_settings_saved_restart": "Einstellungen erfolgreich gespeichert!\\n\\n⚠️ Einige Einstellungen erfordern einen Serverneustart:\\n- Benutzerdefinierter ROMs-Ordner\\n- Sprache\\n\\nBitte starten Sie den Webserver neu, um diese Änderungen anzuwenden.",
|
||||
@@ -328,6 +392,7 @@
|
||||
"web_history_status_completed": "Abgeschlossen",
|
||||
"web_history_status_error": "Fehler",
|
||||
"web_settings_os": "Betriebssystem",
|
||||
"web_system_info_title": "Systeminformationen",
|
||||
"web_settings_platforms_count": "Anzahl der Plattformen",
|
||||
"web_settings_show_unsupported": "Nicht unterstützte Plattformen anzeigen (System fehlt in es_systems.cfg)",
|
||||
"web_settings_allow_unknown": "Unbekannte Erweiterungen erlauben (keine Warnungen anzeigen)",
|
||||
@@ -339,7 +404,7 @@
|
||||
"web_restart_error": "Fehler beim Neustart: {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Support-Datei erstellt",
|
||||
"web_support_message": "Support-Datei erfolgreich erstellt!\\n\\n📁 Inhalt:\\n• Steuerungskonfiguration\\n• Download-Verlauf\\n• RGSX-Einstellungen\\n• Anwendungsprotokolle\\n• Webserver-Protokolle\\n\\n💬 Um Hilfe zu erhalten:\\n1. Trete dem RGSX Discord bei\\n2. Beschreibe dein Problem\\n3. Teile diese ZIP-Datei\\n\\nDownload startet...",
|
||||
"web_support_message": "Support-Datei erfolgreich erstellt!\n\n📁 Inhalt:\n• Steuerungskonfiguration\n• Download-Verlauf\n• RGSX-Einstellungen\n• Anwendungsprotokolle\n• Webserver-Protokolle\n\n💬 Um Hilfe zu erhalten:\n1. Trete dem RGSX Discord bei\n2. Beschreibe dein Problem\n3. Teile diese ZIP-Datei\n\nDownload startet...",
|
||||
"web_support_generating": "Support-Datei wird generiert...",
|
||||
"web_support_download": "Support-Datei herunterladen",
|
||||
"web_support_error": "Fehler beim Erstellen der Support-Datei: {0}",
|
||||
@@ -367,10 +432,9 @@
|
||||
"web_filter_regex_mode": "Regex-Suche aktivieren",
|
||||
"web_filter_one_rom_per_game": "Eine ROM pro Spiel",
|
||||
"web_filter_configure_priority": "Regions-Prioritätsreihenfolge konfigurieren",
|
||||
"filter_all": "Alles auswählen",
|
||||
"filter_none": "Alles abwählen",
|
||||
"filter_all": "Alle auswählen",
|
||||
"filter_none": "Alle abwählen",
|
||||
"filter_apply": "Filter anwenden",
|
||||
"filter_back": "Zurück",
|
||||
"accessibility_footer_font_size": "Fußzeilenschriftgröße: {0}",
|
||||
"popup_layout_changed_restart": "Layout geändert auf {0}x{1}. Bitte starten Sie die App neu.",
|
||||
"web_started": "Gestartet",
|
||||
@@ -379,5 +443,51 @@
|
||||
"web_added_to_queue": "zur Warteschlange hinzugefügt",
|
||||
"web_download_success": "erfolgreich heruntergeladen!",
|
||||
"web_download_error_for": "Fehler beim Herunterladen von",
|
||||
"web_already_present": "war bereits vorhanden"
|
||||
"web_already_present": "war bereits vorhanden",
|
||||
"filter_menu_title": "Filtermenü",
|
||||
"filter_search_by_name": "Nach Namen suchen",
|
||||
"filter_advanced": "Erweiterte Filterung",
|
||||
"filter_advanced_title": "Erweiterte Spielfilterung",
|
||||
"filter_region_title": "Nach Region filtern",
|
||||
"filter_region_include": "Einschließen",
|
||||
"filter_region_exclude": "Ausschließen",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Kanada",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "Frankreich",
|
||||
"filter_region_germany": "Deutschland",
|
||||
"filter_region_japan": "Japan",
|
||||
"filter_region_korea": "Korea",
|
||||
"filter_region_world": "Welt",
|
||||
"filter_region_other": "Andere",
|
||||
"filter_other_options": "Weitere Optionen",
|
||||
"filter_hide_non_release": "Demos/Betas/Protos ausblenden",
|
||||
"filter_one_rom_per_game": "Eine ROM pro Spiel",
|
||||
"filter_priority_order": "Prioritätsreihenfolge",
|
||||
"filter_priority_title": "Regionsprioritätskonfiguration",
|
||||
"filter_priority_desc": "Prioritätsreihenfolge für \"Eine ROM pro Spiel\" festlegen",
|
||||
"filter_regex_mode": "Regex-Modus",
|
||||
"filter_apply_filters": "Anwenden",
|
||||
"filter_reset_filters": "Zurücksetzen",
|
||||
"filter_back": "Zurück",
|
||||
"filter_active": "Filter aktiv",
|
||||
"filter_games_shown": "{0} Spiel(e) angezeigt",
|
||||
"platform_folder_config_current": "Download-Ordner für {0} konfigurieren\nAktuell: {1}",
|
||||
"platform_folder_config_default": "Download-Ordner für {0} konfigurieren\nStandardordner wird verwendet",
|
||||
"platform_folder_show_current": "Aktuellen Pfad anzeigen",
|
||||
"platform_folder_browse": "Durchsuchen",
|
||||
"platform_folder_reset": "Auf Standard zurücksetzen",
|
||||
"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_parent": "Übergeordneter Ordner",
|
||||
"folder_browser_enter": "Öffnen",
|
||||
"folder_browser_select": "Auswählen",
|
||||
"folder_new_folder": "Neuer Ordner",
|
||||
"folder_new_title": "Neuen Ordner erstellen",
|
||||
"folder_new_confirm": "Erstellen",
|
||||
"folder_created": "Ordner erstellt: {0}",
|
||||
"folder_create_error": "Fehler beim Erstellen: {0}",
|
||||
"controls_action_select_char": "Zeichen",
|
||||
"folder_browser_browse": "Durchsuchen"
|
||||
}
|
||||
@@ -24,7 +24,11 @@
|
||||
"game_count": "{0} ({1} games)",
|
||||
"game_filter": "Active filter: {0}",
|
||||
"game_search": "Filter: {0}",
|
||||
"global_search_title": "Global search: {0}",
|
||||
"global_search_empty_query": "Type a game name to search across all systems",
|
||||
"global_search_no_results": "No results for: {0}",
|
||||
"game_header_name": "Name",
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Size",
|
||||
"history_title": "Downloads ({0})",
|
||||
"history_empty": "No downloads in history",
|
||||
@@ -44,6 +48,7 @@
|
||||
"free_mode_completed": "[Free mode] Completed: {0}",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download canceled by user.",
|
||||
"download_removed_from_queue": "Removed from download queue",
|
||||
"extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?",
|
||||
"extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the es_systems.cfg configuration. Do you want to continue?",
|
||||
"extension_warning_enable_unknown_hint": "\nTo hide this message: enable \"Hide unknown extension warning\" in Pause Menu > Display",
|
||||
@@ -51,6 +56,8 @@
|
||||
"confirm_exit_with_downloads": "Attention: {0} download(s) in progress. Quit anyway?",
|
||||
"confirm_clear_history": "Clear history?",
|
||||
"confirm_redownload_cache": "Update games list?",
|
||||
"gamelist_update_prompt_with_date": "Game list hasn't been updated for more than {0} days (last update: {1}). Download the latest version?",
|
||||
"gamelist_update_prompt_first_time": "Would you like to download the latest game list?",
|
||||
"popup_redownload_success": "Cache cleared, please restart the application",
|
||||
"popup_no_cache": "No cache found.\nPlease restart the application to load games.",
|
||||
"popup_countdown": "This message will close in {0} second{1}",
|
||||
@@ -59,15 +66,29 @@
|
||||
"language_changed": "Language changed to {0}",
|
||||
"menu_controls": "Controls",
|
||||
"menu_remap_controls": "Remap controls",
|
||||
"menu_nintendo_layout_on": "Nintendo Controller layout",
|
||||
"menu_nintendo_layout_off": "Xbox Controller layout",
|
||||
"instruction_nintendo_layout": "Inverts the displayed controls to match layout",
|
||||
"menu_history": "History",
|
||||
"menu_language": "Language",
|
||||
"menu_accessibility": "Accessibility",
|
||||
"menu_display": "Display",
|
||||
"display_layout": "Display layout",
|
||||
"display_monitor": "Monitor",
|
||||
"display_monitor_single": "Single monitor",
|
||||
"display_monitor_single_only": "Only one monitor detected",
|
||||
"display_monitor_restart_required": "Restart required to apply monitor change",
|
||||
"display_mode": "Screen mode",
|
||||
"display_fullscreen": "Fullscreen",
|
||||
"display_windowed": "Windowed",
|
||||
"display_mode_restart_required": "Restart required to apply screen mode",
|
||||
"display_light_mode": "Performance mode",
|
||||
"display_light_mode_enabled": "Performance mode enabled - effects disabled",
|
||||
"display_light_mode_disabled": "Performance mode disabled - effects enabled",
|
||||
"menu_redownload_cache": "Update games list",
|
||||
"menu_music_enabled": "Music enabled: {0}",
|
||||
"menu_music_disabled": "Music disabled",
|
||||
"menu_restart": "Restart",
|
||||
"menu_restart": "Restart RGSX",
|
||||
"menu_filter_platforms": "Filter systems",
|
||||
"filter_platforms_title": "Systems visibility",
|
||||
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
|
||||
@@ -80,6 +101,7 @@
|
||||
"menu_allow_unknown_ext_disabled": "Hide unknown extension warning disabled",
|
||||
"menu_support": "Support",
|
||||
"menu_quit": "Quit",
|
||||
"menu_quit_app": "Quit RGSX",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"button_OK": "OK",
|
||||
@@ -94,6 +116,7 @@
|
||||
"support_dialog_error": "Error generating support file:\n{0}\n\nPress {1} to return to the menu.",
|
||||
"controls_action_history": "History / Downloads",
|
||||
"controls_action_close_history": "Close History",
|
||||
"history_column_folder": "Folder",
|
||||
"network_checking_updates": "Update in progress please wait...",
|
||||
"network_update_available": "Update available: {0}",
|
||||
"network_extracting_update": "Extracting update...",
|
||||
@@ -147,6 +170,8 @@
|
||||
"controls_confirm_select": "Confirm/Select",
|
||||
"controls_cancel_back": "Cancel/Back",
|
||||
"controls_filter_search": "Filter/Search",
|
||||
"controls_action_edit_search": "Edit search",
|
||||
"controls_action_show_results": "Show results",
|
||||
"symlink_option_enabled": "Symlink option enabled",
|
||||
"symlink_option_disabled": "Symlink option disabled",
|
||||
"menu_games_source_prefix": "Game source",
|
||||
@@ -170,7 +195,14 @@
|
||||
"status_present": "Present",
|
||||
"status_missing": "Missing",
|
||||
"menu_api_keys_status": "API Keys",
|
||||
"menu_connection_status": "Connection status",
|
||||
"api_keys_status_title": "API Keys Status",
|
||||
"connection_status_title": "Connection status",
|
||||
"connection_status_category_updates": "App/Gamelist update",
|
||||
"connection_status_category_sources": "Game sources",
|
||||
"connection_status_checking": "Checking...",
|
||||
"connection_status_progress": "Checking... {done}/{total}",
|
||||
"connection_status_last_check": "Last check: {time}",
|
||||
"menu_games": "Games",
|
||||
"api_keys_hint_manage": "Put your keys in {path}",
|
||||
"api_key_empty_suffix": "empty",
|
||||
@@ -186,14 +218,22 @@
|
||||
"instruction_pause_settings": "Music, symlink option & API keys status",
|
||||
"instruction_pause_restart": "Restart RGSX to reload configuration",
|
||||
"instruction_pause_support": "Generate a diagnostic ZIP file for support",
|
||||
"instruction_pause_quit": "Exit the RGSX application",
|
||||
"instruction_pause_quit": "Access menu to quit or restart",
|
||||
"instruction_quit_app": "Exit the RGSX application",
|
||||
"instruction_quit_restart": "Restart the RGSX application",
|
||||
"instruction_controls_help": "Show full controller & keyboard reference",
|
||||
"controller_style_label": "Controller Style :",
|
||||
"controller_style_nintendo": "Nintendo",
|
||||
"controller_style_xbox": "Xbox",
|
||||
"instruction_controls_remap": "Change button / key bindings",
|
||||
"instruction_generic_back": "Return to the previous menu",
|
||||
"instruction_display_layout": "Cycle grid dimensions (columns × rows)",
|
||||
"instruction_display_font_size": "Adjust text scale for readability",
|
||||
"instruction_display_footer_font_size": "Adjust footer text scale (version & controls display)",
|
||||
"instruction_display_font_family": "Switch between available font families",
|
||||
"instruction_display_monitor": "Select which monitor to display RGSX on",
|
||||
"instruction_display_mode": "Toggle between fullscreen and windowed mode",
|
||||
"instruction_display_light_mode": "Enable performance mode for better FPS on low-end devices",
|
||||
"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg",
|
||||
"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}",
|
||||
@@ -201,11 +241,24 @@
|
||||
"instruction_games_history": "List past downloads and statuses",
|
||||
"instruction_games_source_mode": "Switch between RGSX or your own custom list source",
|
||||
"instruction_games_update_cache": "Redownload & refresh current games list",
|
||||
"instruction_games_scan_owned": "Scan your ROM folders and mark matching games as already owned",
|
||||
"instruction_settings_music": "Enable or disable background music playback",
|
||||
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
|
||||
"instruction_settings_auto_extract": "Toggle automatic archive extraction after download",
|
||||
"instruction_settings_roms_folder": "Change the default ROMs download directory",
|
||||
"instruction_settings_api_keys": "See detected premium provider API keys",
|
||||
"instruction_settings_connection_status": "Check access to update and source sites",
|
||||
"instruction_settings_web_service": "Enable/disable web service auto-start at boot",
|
||||
"instruction_settings_custom_dns": "Enable/disable custom DNS (Cloudflare 1.1.1.1) at boot",
|
||||
"settings_auto_extract": "Auto Extract Archives",
|
||||
"settings_auto_extract_enabled": "Enabled",
|
||||
"settings_auto_extract_disabled": "Disabled",
|
||||
"settings_roms_folder": "ROMs Folder",
|
||||
"settings_roms_folder_default": "Default",
|
||||
"roms_folder_set": "ROMs folder set: {0}",
|
||||
"roms_folder_set_restart": "ROMs folder set: {0}\nRestart required to apply!",
|
||||
"roms_folder_reset": "ROMs folder reset to default\nRestart required to apply!",
|
||||
"folder_browser_title_roms_root": "Select default ROMs folder",
|
||||
"settings_web_service": "Web Service at Boot",
|
||||
"settings_web_service_enabled": "Enabled",
|
||||
"settings_web_service_disabled": "Disabled",
|
||||
@@ -244,10 +297,18 @@
|
||||
"history_game_options_title": "Game Options",
|
||||
"history_option_download_folder": "Locate file",
|
||||
"history_option_extract_archive": "Extract archive",
|
||||
"history_option_open_file": "Open file",
|
||||
"history_option_scraper": "Scrape metadata",
|
||||
"history_option_remove_from_queue": "Remove from queue",
|
||||
"history_option_cancel_download": "Cancel download",
|
||||
"history_option_pause_download": "Pause download",
|
||||
"history_option_resume_download": "Resume download",
|
||||
"history_option_delete_game": "Delete game",
|
||||
"history_option_error_info": "Error details",
|
||||
"history_option_retry": "Retry download",
|
||||
"menu_scan_owned_roms": "Scan owned ROMs",
|
||||
"popup_scan_owned_roms_done": "ROM scan complete: {0} games added across {1} platforms",
|
||||
"popup_scan_owned_roms_error": "ROM scan error: {0}",
|
||||
"history_option_back": "Back",
|
||||
"history_folder_path_label": "Destination path:",
|
||||
"history_scraper_not_implemented": "Scraper not yet implemented",
|
||||
@@ -307,6 +368,9 @@
|
||||
"web_settings_source_mode": "Games source",
|
||||
"web_settings_custom_url": "Custom URL",
|
||||
"web_settings_custom_url_placeholder": "Let empty for local /saves/ports/rgsx/games.zip or use a direct URL like https://example.com/games.zip",
|
||||
"web_settings_auto_extract": "Auto-extract archives after download",
|
||||
"web_settings_web_service": "Start web service at boot",
|
||||
"web_settings_custom_dns": "Enable custom DNS at boot",
|
||||
"web_settings_save": "Save Settings",
|
||||
"web_settings_saved": "Settings saved successfully!",
|
||||
"web_settings_saved_restart": "Settings saved successfully!\\n\\n⚠️ Some settings require a server restart:\\n- Custom ROMs folder\\n- Language\\n\\nPlease restart the web server to apply these changes.",
|
||||
@@ -330,6 +394,7 @@
|
||||
"web_history_status_completed": "Completed",
|
||||
"web_history_status_error": "Error",
|
||||
"web_settings_os": "Operating System",
|
||||
"web_system_info_title": "System Information",
|
||||
"web_settings_platforms_count": "Number of platforms",
|
||||
"web_settings_show_unsupported": "Show unsupported platforms (system not found in es_systems.cfg)",
|
||||
"web_settings_allow_unknown": "Allow unknown extensions (don't show warnings)",
|
||||
@@ -341,7 +406,7 @@
|
||||
"web_restart_error": "Restart error: {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Support File Generated",
|
||||
"web_support_message": "Support file created successfully!\\n\\n📁 Contents:\\n• Controls configuration\\n• Download history\\n• RGSX settings\\n• Application logs\\n• Web server logs\\n\\n💬 To get help:\\n1. Join RGSX Discord\\n2. Describe your issue\\n3. Share this ZIP file\\n\\nDownload will start...",
|
||||
"web_support_message": "Support file created successfully!\n\n📁 Contents:\n• Controls configuration\n• Download history\n• RGSX settings\n• Application logs\n• Web server logs\n\n💬 To get help:\n1. Join RGSX Discord\n2. Describe your issue\n3. Share this ZIP file\n\nDownload will start...",
|
||||
"web_support_generating": "Generating support file...",
|
||||
"web_support_download": "Download support file",
|
||||
"web_support_error": "Error generating support file: {0}",
|
||||
@@ -367,10 +432,9 @@
|
||||
"web_filter_regex_mode": "Enable Regex Search",
|
||||
"web_filter_one_rom_per_game": "One ROM Per Game",
|
||||
"web_filter_configure_priority": "Configure region priority order",
|
||||
"filter_all": "Check All",
|
||||
"filter_none": "Uncheck All",
|
||||
"filter_apply": "Apply Filter",
|
||||
"filter_back": "Back",
|
||||
"filter_all": "Check all",
|
||||
"filter_none": "Uncheck all",
|
||||
"filter_apply": "Apply filter",
|
||||
"accessibility_footer_font_size": "Footer font size: {0}",
|
||||
"popup_layout_changed_restart": "Layout changed to {0}x{1}. Please restart the app to apply.",
|
||||
"web_started": "Started",
|
||||
@@ -379,5 +443,51 @@
|
||||
"web_added_to_queue": "added to queue",
|
||||
"web_download_success": "downloaded successfully!",
|
||||
"web_download_error_for": "Error downloading",
|
||||
"web_already_present": "was already present"
|
||||
"web_already_present": "was already present",
|
||||
"filter_menu_title": "Filter Menu",
|
||||
"filter_search_by_name": "Search by name",
|
||||
"filter_advanced": "Advanced filtering",
|
||||
"filter_advanced_title": "Advanced Game Filtering",
|
||||
"filter_region_title": "Filter by region",
|
||||
"filter_region_include": "Include",
|
||||
"filter_region_exclude": "Exclude",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Canada",
|
||||
"filter_region_europe": "Europe",
|
||||
"filter_region_france": "France",
|
||||
"filter_region_germany": "Germany",
|
||||
"filter_region_japan": "Japan",
|
||||
"filter_region_korea": "Korea",
|
||||
"filter_region_world": "World",
|
||||
"filter_region_other": "Other",
|
||||
"filter_other_options": "Other options",
|
||||
"filter_hide_non_release": "Hide Demos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "One ROM per game",
|
||||
"filter_priority_order": "Priority order",
|
||||
"filter_priority_title": "Region Priority Configuration",
|
||||
"filter_priority_desc": "Set preference order for \"One ROM per game\"",
|
||||
"filter_regex_mode": "Regex Mode",
|
||||
"filter_apply_filters": "Apply",
|
||||
"filter_reset_filters": "Reset",
|
||||
"filter_back": "Back",
|
||||
"filter_active": "Filter active",
|
||||
"filter_games_shown": "{0} game(s) shown",
|
||||
"platform_folder_config_current": "Configure download folder for {0}\nCurrent: {1}",
|
||||
"platform_folder_config_default": "Configure download folder for {0}\nUsing default location",
|
||||
"platform_folder_show_current": "Show current path",
|
||||
"platform_folder_browse": "Browse",
|
||||
"platform_folder_reset": "Reset to default",
|
||||
"platform_folder_set": "Folder set for {0}: {1}",
|
||||
"platform_folder_default_path": "Default: {0}",
|
||||
"folder_browser_title": "Select folder for {0}",
|
||||
"folder_browser_parent": "Parent folder",
|
||||
"folder_browser_enter": "Enter",
|
||||
"folder_browser_select": "Select",
|
||||
"folder_new_folder": "New folder",
|
||||
"folder_new_title": "Create New Folder",
|
||||
"folder_new_confirm": "Create",
|
||||
"folder_created": "Folder created: {0}",
|
||||
"folder_create_error": "Error creating folder: {0}",
|
||||
"controls_action_select_char": "Add char",
|
||||
"folder_browser_browse": "Browse"
|
||||
}
|
||||
@@ -24,7 +24,11 @@
|
||||
"game_count": "{0} ({1} juegos)",
|
||||
"game_filter": "Filtro activo: {0}",
|
||||
"game_search": "Filtrar: {0}",
|
||||
"global_search_title": "Busqueda global: {0}",
|
||||
"global_search_empty_query": "Escribe un nombre para buscar en todas las consolas",
|
||||
"global_search_no_results": "Sin resultados para: {0}",
|
||||
"game_header_name": "Nombre",
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Tamaño",
|
||||
"history_title": "Descargas ({0})",
|
||||
"history_empty": "No hay descargas en el historial",
|
||||
@@ -44,14 +48,15 @@
|
||||
"free_mode_completed": "[Modo gratuito] Completado: {0}",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Descarga cancelada por el usuario.",
|
||||
"download_removed_from_queue": "Eliminado de la cola de descarga",
|
||||
"extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?",
|
||||
"extension_warning_unsupported": "La extensión del archivo '{0}' no está soportada por Batocera según la configuración es_systems.cfg. ¿Deseas continuar?",
|
||||
"extension_warning_enable_unknown_hint": "\nPara no mostrar este mensaje: activa \"Ocultar aviso de extensión desconocida\" en Menú de pausa > Pantalla",
|
||||
"confirm_exit": "¿Salir de la aplicación?",
|
||||
"confirm_exit_with_downloads": "Atención: {0} descarga(s) en curso. ¿Salir de todas formas?",
|
||||
"confirm_clear_history": "¿Vaciar el historial?",
|
||||
"confirm_redownload_cache": "¿Actualizar la lista de juegos?",
|
||||
"popup_redownload_success": "Caché borrada, por favor reinicia la aplicación",
|
||||
"confirm_redownload_cache": "¿Actualizar la lista de juegos?", "gamelist_update_prompt_with_date": "La lista de juegos no se ha actualizado durante más de {0} días (última actualización: {1}). ¿Descargar la última versión?",
|
||||
"gamelist_update_prompt_first_time": "¿Desea descargar la última lista de juegos?", "popup_redownload_success": "Caché borrada, por favor reinicia la aplicación",
|
||||
"popup_no_cache": "No se encontró caché.\nPor favor, reinicia la aplicación para cargar los juegos.",
|
||||
"popup_countdown": "Este mensaje se cerrará en {0} segundo{1}",
|
||||
"language_select_title": "Selección de idioma",
|
||||
@@ -59,15 +64,32 @@
|
||||
"language_changed": "Idioma cambiado a {0}",
|
||||
"menu_controls": "Controles",
|
||||
"menu_remap_controls": "Remapear controles",
|
||||
"menu_nintendo_layout_on": "Diseño de controlador Nintendo",
|
||||
"menu_nintendo_layout_off": "Diseño de controlador Xbox",
|
||||
"instruction_nintendo_layout": "Invierte los controles mostrados para coincidir con el diseño",
|
||||
"controller_style_label": "Estilo de controlador :",
|
||||
"controller_style_nintendo": "Nintendo",
|
||||
"controller_style_xbox": "Xbox",
|
||||
"menu_history": "Historial",
|
||||
"menu_language": "Idioma",
|
||||
"menu_accessibility": "Accesibilidad",
|
||||
"menu_display": "Pantalla",
|
||||
"display_layout": "Distribución",
|
||||
"display_monitor": "Monitor",
|
||||
"display_monitor_single": "Monitor único",
|
||||
"display_monitor_single_only": "Solo un monitor detectado",
|
||||
"display_monitor_restart_required": "Reinicio necesario para cambiar de monitor",
|
||||
"display_mode": "Modo de pantalla",
|
||||
"display_fullscreen": "Pantalla completa",
|
||||
"display_windowed": "Ventana",
|
||||
"display_mode_restart_required": "Reinicio necesario para cambiar el modo",
|
||||
"display_light_mode": "Modo rendimiento",
|
||||
"display_light_mode_enabled": "Modo rendimiento activado - efectos desactivados",
|
||||
"display_light_mode_disabled": "Modo rendimiento desactivado - efectos activados",
|
||||
"menu_redownload_cache": "Actualizar lista de juegos",
|
||||
"menu_music_enabled": "Música activada: {0}",
|
||||
"menu_music_disabled": "Música desactivada",
|
||||
"menu_restart": "Reiniciar",
|
||||
"menu_restart": "Reiniciar RGSX",
|
||||
"menu_support": "Soporte",
|
||||
"menu_filter_platforms": "Filtrar sistemas",
|
||||
"filter_platforms_title": "Visibilidad de sistemas",
|
||||
@@ -80,6 +102,7 @@
|
||||
"menu_allow_unknown_ext_enabled": "Aviso de extensión desconocida oculto (activado)",
|
||||
"menu_allow_unknown_ext_disabled": "Aviso de extensión desconocida visible (desactivado)",
|
||||
"menu_quit": "Salir",
|
||||
"menu_quit_app": "Salir de RGSX",
|
||||
"support_dialog_title": "Archivo de soporte",
|
||||
"support_dialog_message": "Se ha creado un archivo de soporte con todos sus archivos de configuración y registros.\n\nArchivo: {0}\n\nPara obtener ayuda:\n1. Únete al servidor Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nPresiona {1} para volver al menú.",
|
||||
"support_dialog_error": "Error al generar el archivo de soporte:\n{0}\n\nPresiona {1} para volver al menú.",
|
||||
@@ -90,6 +113,7 @@
|
||||
"controls_action_clear_history": "Vaciar historial",
|
||||
"controls_action_history": "Historial / Descargas",
|
||||
"controls_action_close_history": "Cerrar Historial",
|
||||
"history_column_folder": "Carpeta",
|
||||
"controls_action_delete": "Eliminar",
|
||||
"controls_action_space": "Espacio",
|
||||
"controls_action_start": "Ayuda / Configuración",
|
||||
@@ -123,6 +147,8 @@
|
||||
"controls_confirm_select": "Confirmar/Seleccionar",
|
||||
"controls_cancel_back": "Cancelar/Volver",
|
||||
"controls_filter_search": "Filtrar/Buscar",
|
||||
"controls_action_edit_search": "Editar busqueda",
|
||||
"controls_action_show_results": "Ver resultados",
|
||||
"network_download_failed": "Error en la descarga tras {0} intentos",
|
||||
"network_api_error": "Error en la solicitud de API, la clave puede ser incorrecta: {0}",
|
||||
"network_download_error": "Error en la descarga {0}: {1}",
|
||||
@@ -170,7 +196,14 @@
|
||||
"status_present": "Presente",
|
||||
"status_missing": "Ausente",
|
||||
"menu_api_keys_status": "Claves API",
|
||||
"menu_connection_status": "Estado de conexión",
|
||||
"api_keys_status_title": "Estado de las claves API",
|
||||
"connection_status_title": "Estado de conexión",
|
||||
"connection_status_category_updates": "Actualización app/lista de juegos",
|
||||
"connection_status_category_sources": "Fuentes de juegos",
|
||||
"connection_status_checking": "Comprobando...",
|
||||
"connection_status_progress": "Comprobando... {done}/{total}",
|
||||
"connection_status_last_check": "Última comprobación: {time}",
|
||||
"menu_games": "Juegos",
|
||||
"api_keys_hint_manage": "Coloca tus claves en {path}",
|
||||
"api_key_empty_suffix": "vacío",
|
||||
@@ -186,7 +219,9 @@
|
||||
"instruction_pause_settings": "Música, opción symlink y estado de claves API",
|
||||
"instruction_pause_restart": "Reiniciar RGSX para recargar configuración",
|
||||
"instruction_pause_support": "Generar un archivo ZIP de diagnóstico para soporte",
|
||||
"instruction_pause_quit": "Salir de la aplicación RGSX",
|
||||
"instruction_pause_quit": "Acceder al menú para salir o reiniciar",
|
||||
"instruction_quit_app": "Salir de la aplicación RGSX",
|
||||
"instruction_quit_restart": "Reiniciar la aplicación RGSX",
|
||||
"instruction_controls_help": "Mostrar referencia completa de mando y teclado",
|
||||
"instruction_controls_remap": "Cambiar asignación de botones / teclas",
|
||||
"instruction_generic_back": "Volver al menú anterior",
|
||||
@@ -194,6 +229,9 @@
|
||||
"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad",
|
||||
"instruction_display_footer_font_size": "Ajustar el tamaño del texto del pie de página (versión y controles)",
|
||||
"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles",
|
||||
"instruction_display_monitor": "Seleccionar monitor para mostrar RGSX",
|
||||
"instruction_display_mode": "Alternar entre pantalla completa y ventana",
|
||||
"instruction_display_light_mode": "Activar modo rendimiento para mejores FPS",
|
||||
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg",
|
||||
"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}",
|
||||
@@ -201,11 +239,24 @@
|
||||
"instruction_games_history": "Ver descargas pasadas y su estado",
|
||||
"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada",
|
||||
"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos",
|
||||
"instruction_games_scan_owned": "Escanear las carpetas ROMs y marcar los juegos que ya posees",
|
||||
"instruction_settings_music": "Activar o desactivar música de fondo",
|
||||
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
|
||||
"instruction_settings_auto_extract": "Activar/desactivar extracción automática de archivos después de descargar",
|
||||
"instruction_settings_roms_folder": "Cambiar el directorio de descarga de ROMs por defecto",
|
||||
"instruction_settings_api_keys": "Ver claves API premium detectadas",
|
||||
"instruction_settings_connection_status": "Comprobar acceso a sitios de actualizaciones y fuentes",
|
||||
"instruction_settings_web_service": "Activar/desactivar inicio automático del servicio web",
|
||||
"instruction_settings_custom_dns": "Activar/desactivar DNS personalizado (Cloudflare 1.1.1.1) al inicio",
|
||||
"settings_auto_extract": "Extracción auto de archivos",
|
||||
"settings_auto_extract_enabled": "Activado",
|
||||
"settings_auto_extract_disabled": "Desactivado",
|
||||
"settings_roms_folder": "Carpeta ROMs",
|
||||
"settings_roms_folder_default": "Por defecto",
|
||||
"roms_folder_set": "Carpeta ROMs configurada: {0}",
|
||||
"roms_folder_set_restart": "Carpeta ROMs configurada: {0}\n¡Reinicio necesario para aplicar!",
|
||||
"roms_folder_reset": "Carpeta ROMs restablecida por defecto\n¡Reinicio necesario para aplicar!",
|
||||
"folder_browser_title_roms_root": "Seleccionar carpeta ROMs por defecto",
|
||||
"settings_web_service": "Servicio Web al Inicio",
|
||||
"settings_web_service_enabled": "Activado",
|
||||
"settings_web_service_disabled": "Desactivado",
|
||||
@@ -215,6 +266,9 @@
|
||||
"settings_web_service_success_disabled": "Servicio web desactivado al inicio",
|
||||
"settings_web_service_error": "Error: {0}",
|
||||
"settings_custom_dns": "DNS Personalizado al Inicio",
|
||||
"menu_scan_owned_roms": "Escanear ROMs disponibles",
|
||||
"popup_scan_owned_roms_done": "Escaneo ROM completado: {0} juegos añadidos en {1} plataformas",
|
||||
"popup_scan_owned_roms_error": "Error al escanear ROMs: {0}",
|
||||
"settings_custom_dns_enabled": "Activado",
|
||||
"settings_custom_dns_disabled": "Desactivado",
|
||||
"settings_custom_dns_enabling": "Activando DNS personalizado...",
|
||||
@@ -244,7 +298,12 @@
|
||||
"history_game_options_title": "Opciones del juego",
|
||||
"history_option_download_folder": "Localizar archivo",
|
||||
"history_option_extract_archive": "Extraer archivo",
|
||||
"history_option_scraper": "Scraper metadatos",
|
||||
"history_option_open_file": "Abrir archivo",
|
||||
"history_option_scraper": "Obtener metadatos",
|
||||
"history_option_remove_from_queue": "Quitar de la cola",
|
||||
"history_option_cancel_download": "Cancelar descarga",
|
||||
"history_option_pause_download": "Pausar descarga",
|
||||
"history_option_resume_download": "Reanudar descarga",
|
||||
"history_option_delete_game": "Eliminar juego",
|
||||
"history_option_error_info": "Detalles del error",
|
||||
"history_option_retry": "Reintentar descarga",
|
||||
@@ -307,6 +366,9 @@
|
||||
"web_settings_source_mode": "Fuente de juegos",
|
||||
"web_settings_custom_url": "URL personalizada",
|
||||
"web_settings_custom_url_placeholder": "Dejar vacío para /saves/ports/rgsx/games.zip o usar una URL directa como https://ejemplo.com/juegos.zip",
|
||||
"web_settings_auto_extract": "Extraer archivos automáticamente después de descargar",
|
||||
"web_settings_web_service": "Iniciar servicio web al arrancar",
|
||||
"web_settings_custom_dns": "Activar DNS personalizado al arrancar",
|
||||
"web_settings_save": "Guardar configuración",
|
||||
"web_settings_saved": "¡Configuración guardada con éxito!",
|
||||
"web_settings_saved_restart": "¡Configuración guardada con éxito!\\n\\n⚠️ Algunos ajustes requieren reiniciar el servidor:\\n- Carpeta ROMs personalizada\\n- Idioma\\n\\nPor favor, reinicie el servidor web para aplicar estos cambios.",
|
||||
@@ -330,6 +392,7 @@
|
||||
"web_history_status_completed": "Completado",
|
||||
"web_history_status_error": "Error",
|
||||
"web_settings_os": "Sistema operativo",
|
||||
"web_system_info_title": "Información del sistema",
|
||||
"web_settings_platforms_count": "Número de plataformas",
|
||||
"web_settings_show_unsupported": "Mostrar plataformas no compatibles (sistema ausente en es_systems.cfg)",
|
||||
"web_settings_allow_unknown": "Permitir extensiones desconocidas (no mostrar advertencias)",
|
||||
@@ -341,7 +404,7 @@
|
||||
"web_restart_error": "Error al reiniciar: {0}",
|
||||
"web_support": "Soporte",
|
||||
"web_support_title": "📦 Archivo de soporte generado",
|
||||
"web_support_message": "¡Archivo de soporte creado con éxito!\\n\\n📁 Contenido:\\n• Configuración de controles\\n• Historial de descargas\\n• Configuración RGSX\\n• Registros de la aplicación\\n• Registros del servidor web\\n\\n💬 Para obtener ayuda:\\n1. Únete al Discord de RGSX\\n2. Describe tu problema\\n3. Comparte este archivo ZIP\\n\\nLa descarga comenzará...",
|
||||
"web_support_message": "¡Archivo de soporte creado con éxito!\n\n📁 Contenido:\n• Configuración de controles\n• Historial de descargas\n• Configuración RGSX\n• Registros de la aplicación\n• Registros del servidor web\n\n💬 Para obtener ayuda:\n1. Únete al Discord de RGSX\n2. Describe tu problema\n3. Comparte este archivo ZIP\n\nLa descarga comenzará...",
|
||||
"web_support_generating": "Generando archivo de soporte...",
|
||||
"web_support_download": "Descargar archivo de soporte",
|
||||
"web_support_error": "Error al generar el archivo de soporte: {0}",
|
||||
@@ -370,7 +433,6 @@
|
||||
"filter_all": "Marcar todo",
|
||||
"filter_none": "Desmarcar todo",
|
||||
"filter_apply": "Aplicar filtro",
|
||||
"filter_back": "Volver",
|
||||
"accessibility_footer_font_size": "Tamaño fuente pie de página: {0}",
|
||||
"popup_layout_changed_restart": "Diseño cambiado a {0}x{1}. Reinicie la app para aplicar.",
|
||||
"web_started": "Iniciado",
|
||||
@@ -379,5 +441,51 @@
|
||||
"web_added_to_queue": "añadido a la cola",
|
||||
"web_download_success": "¡descargado con éxito!",
|
||||
"web_download_error_for": "Error al descargar",
|
||||
"web_already_present": "ya estaba presente"
|
||||
"web_already_present": "ya estaba presente",
|
||||
"filter_menu_title": "Menú de filtros",
|
||||
"filter_search_by_name": "Buscar por nombre",
|
||||
"filter_advanced": "Filtrado avanzado",
|
||||
"filter_advanced_title": "Filtrado avanzado de juegos",
|
||||
"filter_region_title": "Filtrar por región",
|
||||
"filter_region_include": "Incluir",
|
||||
"filter_region_exclude": "Excluir",
|
||||
"filter_region_usa": "EE.UU.",
|
||||
"filter_region_canada": "Canadá",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "Francia",
|
||||
"filter_region_germany": "Alemania",
|
||||
"filter_region_japan": "Japón",
|
||||
"filter_region_korea": "Corea",
|
||||
"filter_region_world": "Mundial",
|
||||
"filter_region_other": "Otros",
|
||||
"filter_other_options": "Otras opciones",
|
||||
"filter_hide_non_release": "Ocultar Demos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "Una ROM por juego",
|
||||
"filter_priority_order": "Orden de prioridad",
|
||||
"filter_priority_title": "Configuración de prioridad de regiones",
|
||||
"filter_priority_desc": "Definir orden de preferencia para \"Una ROM por juego\"",
|
||||
"filter_regex_mode": "Modo Regex",
|
||||
"filter_apply_filters": "Aplicar",
|
||||
"filter_reset_filters": "Restablecer",
|
||||
"filter_back": "Volver",
|
||||
"filter_active": "Filtro activo",
|
||||
"filter_games_shown": "{0} juego(s) mostrado(s)",
|
||||
"platform_folder_config_current": "Configurar carpeta de descarga para {0}\nActual: {1}",
|
||||
"platform_folder_config_default": "Configurar carpeta de descarga para {0}\nUsando ubicación predeterminada",
|
||||
"platform_folder_show_current": "Mostrar ruta actual",
|
||||
"platform_folder_browse": "Examinar",
|
||||
"platform_folder_reset": "Restablecer predeterminado",
|
||||
"platform_folder_set": "Carpeta establecida para {0}: {1}",
|
||||
"platform_folder_default_path": "Por defecto: {0}",
|
||||
"folder_browser_title": "Seleccionar carpeta para {0}",
|
||||
"folder_browser_parent": "Carpeta superior",
|
||||
"folder_browser_enter": "Entrar",
|
||||
"folder_browser_select": "Seleccionar",
|
||||
"folder_new_folder": "Nueva carpeta",
|
||||
"folder_new_title": "Crear nueva carpeta",
|
||||
"folder_new_confirm": "Crear",
|
||||
"folder_created": "Carpeta creada: {0}",
|
||||
"folder_create_error": "Error al crear: {0}",
|
||||
"controls_action_select_char": "Añadir",
|
||||
"folder_browser_browse": "Explorar"
|
||||
}
|
||||
@@ -24,7 +24,11 @@
|
||||
"game_count": "{0} ({1} jeux)",
|
||||
"game_filter": "Filtre actif : {0}",
|
||||
"game_search": "Filtrer : {0}",
|
||||
"global_search_title": "Recherche globale : {0}",
|
||||
"global_search_empty_query": "Saisissez un nom pour rechercher dans toutes les consoles",
|
||||
"global_search_no_results": "Aucun resultat pour : {0}",
|
||||
"game_header_name": "Nom",
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Taille",
|
||||
"history_title": "Téléchargements ({0})",
|
||||
"history_empty": "Aucun téléchargement dans l'historique",
|
||||
@@ -44,6 +48,7 @@
|
||||
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
|
||||
"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",
|
||||
"extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?",
|
||||
"extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après la configuration es_systems.cfg. Voulez-vous continuer ?",
|
||||
"extension_warning_enable_unknown_hint": "\nPour ne plus afficher ce messager : Activer l'option \"Masquer avertissement\" dans le Menu Pause>Display",
|
||||
@@ -51,6 +56,8 @@
|
||||
"confirm_exit_with_downloads": "Attention : {0} téléchargement(s) en cours. Quitter quand même ?",
|
||||
"confirm_clear_history": "Vider l'historique ?",
|
||||
"confirm_redownload_cache": "Mettre à jour la liste des jeux ?",
|
||||
"gamelist_update_prompt_with_date": "La liste des jeux n'a pas été mise à jour depuis plus de {0} jours (dernière mise à jour : {1}). Télécharger la dernière version ?",
|
||||
"gamelist_update_prompt_first_time": "Souhaitez-vous télécharger la dernière liste des jeux ?",
|
||||
"popup_redownload_success": "Le cache a été effacé, merci de relancer l'application",
|
||||
"popup_no_cache": "Aucun cache trouvé.\nVeuillez redémarrer l'application pour charger les jeux.",
|
||||
"popup_countdown": "Ce message se fermera dans {0} seconde{1}",
|
||||
@@ -64,12 +71,24 @@
|
||||
"menu_accessibility": "Accessibilité",
|
||||
"menu_display": "Affichage",
|
||||
"display_layout": "Disposition",
|
||||
"display_monitor": "Écran",
|
||||
"display_monitor_single": "Écran unique",
|
||||
"display_monitor_single_only": "Un seul écran détecté",
|
||||
"display_monitor_restart_required": "Redémarrage requis pour changer d'écran",
|
||||
"display_mode": "Mode d'affichage",
|
||||
"display_fullscreen": "Plein écran",
|
||||
"display_windowed": "Fenêtré",
|
||||
"display_mode_restart_required": "Redémarrage requis pour changer le mode",
|
||||
"display_light_mode": "Mode performance",
|
||||
"display_light_mode_enabled": "Mode performance activé - effets désactivés",
|
||||
"display_light_mode_disabled": "Mode performance désactivé - effets activés",
|
||||
"menu_redownload_cache": "Mettre à jour la liste des jeux",
|
||||
"menu_support": "Support",
|
||||
"menu_quit": "Quitter",
|
||||
"menu_quit_app": "Quitter RGSX",
|
||||
"menu_music_enabled": "Musique activée : {0}",
|
||||
"menu_music_disabled": "Musique désactivée",
|
||||
"menu_restart": "Redémarrer",
|
||||
"menu_restart": "Redémarrer RGSX",
|
||||
"menu_filter_platforms": "Filtrer les systèmes",
|
||||
"filter_platforms_title": "Affichage des systèmes",
|
||||
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
|
||||
@@ -91,6 +110,7 @@
|
||||
"controls_action_queue": "Mettre en file d'attente",
|
||||
"controls_action_history": "Historique / Téléchargements",
|
||||
"controls_action_close_history": "Fermer l'historique",
|
||||
"history_column_folder": "Dossier",
|
||||
"controls_action_delete": "Supprimer",
|
||||
"controls_action_space": "Espace",
|
||||
"controls_action_start": "Aide / Réglages",
|
||||
@@ -123,6 +143,8 @@
|
||||
"controls_confirm_select": "Confirmer/Sélectionner",
|
||||
"controls_cancel_back": "Annuler/Retour",
|
||||
"controls_filter_search": "Filtrer/Rechercher",
|
||||
"controls_action_edit_search": "Modifier recherche",
|
||||
"controls_action_show_results": "Voir resultats",
|
||||
"network_download_failed": "Échec du téléchargement après {0} tentatives",
|
||||
"network_api_error": "Erreur lors de la requête API, la clé est peut-être incorrecte: {0}",
|
||||
"network_download_error": "Erreur téléchargement {0}: {1}",
|
||||
@@ -170,7 +192,14 @@
|
||||
"status_present": "Présente",
|
||||
"status_missing": "Absente",
|
||||
"menu_api_keys_status": "Clés API",
|
||||
"menu_connection_status": "État de connexion",
|
||||
"api_keys_status_title": "Statut des clés API",
|
||||
"connection_status_title": "État de connexion",
|
||||
"connection_status_category_updates": "Mise à jour App/Liste de jeux",
|
||||
"connection_status_category_sources": "Sources de jeux",
|
||||
"connection_status_checking": "Vérification...",
|
||||
"connection_status_progress": "Vérification... {done}/{total}",
|
||||
"connection_status_last_check": "Dernière vérif : {time}",
|
||||
"menu_games": "Jeux",
|
||||
"api_keys_hint_manage": "Placez vos clés dans {path}",
|
||||
"api_key_empty_suffix": "vide",
|
||||
@@ -186,14 +215,25 @@
|
||||
"instruction_pause_settings": "Musique, option symlink & statut des clés API",
|
||||
"instruction_pause_restart": "Redémarrer RGSX pour recharger la configuration",
|
||||
"instruction_pause_support": "Générer un fichier ZIP de diagnostic pour l'assistance",
|
||||
"instruction_pause_quit": "Quitter l'application RGSX",
|
||||
"instruction_pause_quit": "Accéder au menu pour quitter ou redémarrer",
|
||||
"instruction_quit_app": "Quitter l'application RGSX",
|
||||
"instruction_quit_restart": "Redémarrer l'application RGSX",
|
||||
"instruction_controls_help": "Afficher la référence complète manette & clavier",
|
||||
"menu_nintendo_layout_on": "Disposition manette Nintendo",
|
||||
"menu_nintendo_layout_off": "Disposition manette Xbox",
|
||||
"instruction_nintendo_layout": "Inverse l'affichage des contrôles pour correspondre au layout",
|
||||
"controller_style_label": "Style manette :",
|
||||
"controller_style_nintendo": "Nintendo",
|
||||
"controller_style_xbox": "Xbox",
|
||||
"instruction_controls_remap": "Modifier l'association boutons / touches",
|
||||
"instruction_generic_back": "Revenir au menu précédent",
|
||||
"instruction_display_layout": "Changer les dimensions de la grille",
|
||||
"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité",
|
||||
"instruction_display_footer_font_size": "Ajuster la taille du texte du pied de page (version et contrôles)",
|
||||
"instruction_display_font_family": "Basculer entre les polices disponibles",
|
||||
"instruction_display_monitor": "Sélectionner l'écran pour afficher RGSX",
|
||||
"instruction_display_mode": "Basculer entre plein écran et fenêtré",
|
||||
"instruction_display_light_mode": "Activer le mode performance pour de meilleurs FPS",
|
||||
"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg",
|
||||
"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}",
|
||||
@@ -201,11 +241,24 @@
|
||||
"instruction_games_history": "Lister les téléchargements passés et leur statut",
|
||||
"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée",
|
||||
"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux",
|
||||
"instruction_games_scan_owned": "Scanner les dossiers ROMs et marquer les jeux déjà possédés",
|
||||
"instruction_settings_music": "Activer ou désactiver la lecture musicale",
|
||||
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
|
||||
"instruction_settings_auto_extract": "Activer/désactiver l'extraction automatique des archives après téléchargement",
|
||||
"instruction_settings_roms_folder": "Changer le répertoire de téléchargement des ROMs par défaut",
|
||||
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
|
||||
"instruction_settings_connection_status": "Vérifier l'accès aux sites d'update et de sources",
|
||||
"instruction_settings_web_service": "Activer/désactiver le démarrage automatique du service web",
|
||||
"instruction_settings_custom_dns": "Activer/désactiver les DNS personnalisés (Cloudflare 1.1.1.1) au démarrage",
|
||||
"settings_auto_extract": "Extraction auto des archives",
|
||||
"settings_auto_extract_enabled": "Activé",
|
||||
"settings_auto_extract_disabled": "Désactivé",
|
||||
"settings_roms_folder": "Dossier ROMs",
|
||||
"settings_roms_folder_default": "Par défaut",
|
||||
"roms_folder_set": "Dossier ROMs défini: {0}",
|
||||
"roms_folder_set_restart": "Dossier ROMs défini: {0}\nRedémarrage nécessaire pour appliquer!",
|
||||
"roms_folder_reset": "Dossier ROMs réinitialisé par défaut\nRedémarrage nécessaire pour appliquer!",
|
||||
"folder_browser_title_roms_root": "Sélectionner le dossier ROMs par défaut",
|
||||
"settings_web_service": "Service Web au démarrage",
|
||||
"settings_web_service_enabled": "Activé",
|
||||
"settings_web_service_disabled": "Désactivé",
|
||||
@@ -244,10 +297,18 @@
|
||||
"history_game_options_title": "Options du jeu",
|
||||
"history_option_download_folder": "Localiser le fichier",
|
||||
"history_option_extract_archive": "Extraire l'archive",
|
||||
"history_option_scraper": "Scraper métadonnées",
|
||||
"history_option_open_file": "Ouvrir le fichier",
|
||||
"history_option_scraper": "Récupérer métadonnées",
|
||||
"history_option_remove_from_queue": "Retirer de la file d'attente",
|
||||
"history_option_cancel_download": "Annuler le téléchargement",
|
||||
"history_option_pause_download": "Mettre en pause",
|
||||
"history_option_resume_download": "Reprendre le téléchargement",
|
||||
"history_option_delete_game": "Supprimer le jeu",
|
||||
"history_option_error_info": "Détails de l'erreur",
|
||||
"history_option_retry": "Réessayer le téléchargement",
|
||||
"history_option_retry": "Retenter le téléchargement",
|
||||
"menu_scan_owned_roms": "Scanner les ROMs présentes",
|
||||
"popup_scan_owned_roms_done": "Scan ROMs terminé : {0} jeux ajoutés sur {1} plateformes",
|
||||
"popup_scan_owned_roms_error": "Erreur scan ROMs : {0}",
|
||||
"history_option_back": "Retour",
|
||||
"history_folder_path_label": "Chemin de destination :",
|
||||
"history_scraper_not_implemented": "Scraper pas encore implémenté",
|
||||
@@ -307,6 +368,9 @@
|
||||
"web_settings_source_mode": "Source des jeux",
|
||||
"web_settings_custom_url": "URL personnalisée",
|
||||
"web_settings_custom_url_placeholder": "Laisser vide pour /saves/ports/rgsx/games.zip ou utiliser une URL directe comme https://exemple.com/jeux.zip",
|
||||
"web_settings_auto_extract": "Extraction auto des archives après téléchargement",
|
||||
"web_settings_web_service": "Lancer le service web au démarrage",
|
||||
"web_settings_custom_dns": "Activer le DNS personnalisé au démarrage",
|
||||
"web_settings_save": "Enregistrer les paramètres",
|
||||
"web_settings_saved": "Paramètres enregistrés avec succès !",
|
||||
"web_settings_saved_restart": "Paramètres enregistrés avec succès !\\n\\n⚠️ Certains paramètres nécessitent un redémarrage du serveur :\\n- Dossier ROMs personnalisé\\n- Langue\\n\\nVeuillez redémarrer le serveur web pour appliquer ces changements.",
|
||||
@@ -330,6 +394,7 @@
|
||||
"web_history_status_completed": "Terminé",
|
||||
"web_history_status_error": "Erreur",
|
||||
"web_settings_os": "Système d'exploitation",
|
||||
"web_system_info_title": "Informations système",
|
||||
"web_settings_platforms_count": "Nombre de plateformes",
|
||||
"web_settings_show_unsupported": "Afficher les systèmes non supportés (absents de es_systems.cfg)",
|
||||
"web_settings_allow_unknown": "Autoriser les extensions inconnues (ne pas afficher d'avertissement)",
|
||||
@@ -341,7 +406,7 @@
|
||||
"web_restart_error": "Erreur lors du redémarrage : {0}",
|
||||
"web_support": "Support",
|
||||
"web_support_title": "📦 Fichier de support généré",
|
||||
"web_support_message": "Le fichier de support a été créé avec succès !\\n\\n📁 Contenu :\\n• Configuration des contrôles\\n• Historique des téléchargements\\n• Paramètres RGSX\\n• Logs de l'application\\n• Logs du serveur web\\n\\n💬 Pour obtenir de l'aide :\\n1. Rejoignez le Discord RGSX\\n2. Décrivez votre problème\\n3. Partagez ce fichier ZIP\\n\\nLe téléchargement va démarrer...",
|
||||
"web_support_message": "Le fichier de support a été créé avec succès !\n\n📁 Contenu :\n• Configuration des contrôles\n• Historique des téléchargements\n• Paramètres RGSX\n• Logs de l'application\n• Logs du serveur web\n\n💬 Pour obtenir de l'aide :\n1. Rejoignez le Discord RGSX\n2. Décrivez votre problème\n3. Partagez ce fichier ZIP\n\nLe téléchargement va démarrer...",
|
||||
"web_support_generating": "Génération du fichier de support...",
|
||||
"web_support_download": "Télécharger le fichier de support",
|
||||
"web_support_error": "Erreur lors de la génération du fichier de support : {0}",
|
||||
@@ -370,7 +435,6 @@
|
||||
"filter_all": "Tout cocher",
|
||||
"filter_none": "Tout décocher",
|
||||
"filter_apply": "Appliquer filtre",
|
||||
"filter_back": "Retour",
|
||||
"accessibility_footer_font_size": "Taille police pied de page : {0}",
|
||||
"popup_layout_changed_restart": "Disposition changée en {0}x{1}. Veuillez redémarrer l'app pour appliquer.",
|
||||
"web_started": "Démarré",
|
||||
@@ -379,5 +443,51 @@
|
||||
"web_added_to_queue": "ajouté à la queue",
|
||||
"web_download_success": "téléchargé avec succès!",
|
||||
"web_download_error_for": "Erreur lors du téléchargement de",
|
||||
"web_already_present": "était déjà présent"
|
||||
"web_already_present": "était déjà présent",
|
||||
"filter_menu_title": "Menu Filtrage",
|
||||
"filter_search_by_name": "Recherche par nom",
|
||||
"filter_advanced": "Filtrage avancé",
|
||||
"filter_advanced_title": "Filtrage avancé des jeux",
|
||||
"filter_region_title": "Filtrer par région",
|
||||
"filter_region_include": "Inclure",
|
||||
"filter_region_exclude": "Exclure",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Canada",
|
||||
"filter_region_europe": "Europe",
|
||||
"filter_region_france": "France",
|
||||
"filter_region_germany": "Allemagne",
|
||||
"filter_region_japan": "Japon",
|
||||
"filter_region_korea": "Corée",
|
||||
"filter_region_world": "Monde",
|
||||
"filter_region_other": "Autres",
|
||||
"filter_other_options": "Autres options",
|
||||
"filter_hide_non_release": "Masquer Démos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "Une ROM par jeu",
|
||||
"filter_priority_order": "Ordre de priorité",
|
||||
"filter_priority_title": "Configuration de la priorité des régions",
|
||||
"filter_priority_desc": "Définir l'ordre de préférence pour \"Une ROM par jeu\"",
|
||||
"filter_regex_mode": "Mode Regex",
|
||||
"filter_apply_filters": "Appliquer",
|
||||
"filter_reset_filters": "Réinitialiser",
|
||||
"filter_back": "Retour",
|
||||
"filter_active": "Filtre actif",
|
||||
"filter_games_shown": "{0} jeu(x) affiché(s)",
|
||||
"platform_folder_config_current": "Configurer le dossier de téléchargement pour {0}\nActuel: {1}",
|
||||
"platform_folder_config_default": "Configurer le dossier de téléchargement pour {0}\nUtilise le dossier par défaut",
|
||||
"platform_folder_show_current": "Afficher le chemin actuel",
|
||||
"platform_folder_browse": "Parcourir",
|
||||
"platform_folder_reset": "Rétablir par défaut",
|
||||
"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_parent": "Dossier parent",
|
||||
"folder_browser_enter": "Entrer",
|
||||
"folder_browser_select": "Valider",
|
||||
"folder_new_folder": "Nouveau dossier",
|
||||
"folder_new_title": "Créer un nouveau dossier",
|
||||
"folder_new_confirm": "Créer",
|
||||
"folder_created": "Dossier créé: {0}",
|
||||
"folder_create_error": "Erreur lors de la création: {0}",
|
||||
"controls_action_select_char": "Ajouter",
|
||||
"folder_browser_browse": "Parcourir"
|
||||
}
|
||||
@@ -24,7 +24,11 @@
|
||||
"game_count": "{0} ({1} giochi)",
|
||||
"game_filter": "Filtro attivo: {0}",
|
||||
"game_search": "Filtro: {0}",
|
||||
"global_search_title": "Ricerca globale: {0}",
|
||||
"global_search_empty_query": "Digita un nome per cercare in tutte le console",
|
||||
"global_search_no_results": "Nessun risultato per: {0}",
|
||||
"game_header_name": "Nome",
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Dimensione",
|
||||
"history_title": "Download ({0})",
|
||||
"history_empty": "Nessun download nella cronologia",
|
||||
@@ -44,14 +48,15 @@
|
||||
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download annullato dall'utente.",
|
||||
"download_removed_from_queue": "Rimosso dalla coda di download",
|
||||
"extension_warning_zip": "Il file '{0}' è un archivio e Batocera non supporta archivi per questo sistema. L'estrazione automatica avverrà dopo il download, continuare?",
|
||||
"extension_warning_unsupported": "L'estensione del file '{0}' non è supportata da Batocera secondo la configurazione di es_systems.cfg. Vuoi continuare?",
|
||||
"extension_warning_enable_unknown_hint": "\nPer non visualizzare questo messaggio: abilita \"Nascondi avviso estensione sconosciuta\" in Menu Pausa > Schermo",
|
||||
"confirm_exit": "Uscire dall'applicazione?",
|
||||
"confirm_exit_with_downloads": "Attenzione: {0} download in corso. Uscire comunque?",
|
||||
"confirm_clear_history": "Cancellare la cronologia?",
|
||||
"confirm_redownload_cache": "Aggiornare l'elenco dei giochi?",
|
||||
"popup_redownload_success": "Cache pulita, riavvia l'applicazione",
|
||||
"confirm_redownload_cache": "Aggiornare l'elenco dei giochi?", "gamelist_update_prompt_with_date": "L'elenco dei giochi non è stato aggiornato da più di {0} giorni (ultimo aggiornamento: {1}). Scaricare l'ultima versione?",
|
||||
"gamelist_update_prompt_first_time": "Vuoi scaricare l'ultimo elenco dei giochi?", "popup_redownload_success": "Cache pulita, riavvia l'applicazione",
|
||||
"popup_no_cache": "Nessuna cache trovata.\nRiavvia l'applicazione per caricare i giochi.",
|
||||
"popup_countdown": "Questo messaggio si chiuderà tra {0} secondo{1}",
|
||||
"language_select_title": "Selezione lingua",
|
||||
@@ -59,15 +64,30 @@
|
||||
"language_changed": "Lingua cambiata in {0}",
|
||||
"menu_controls": "Controlli",
|
||||
"menu_remap_controls": "Rimappa controlli",
|
||||
"menu_nintendo_layout_on": "Layout controller Nintendo",
|
||||
"menu_nintendo_layout_off": "Layout controller Xbox",
|
||||
"instruction_nintendo_layout": "Inverti i controlli visualizzati per corrispondere al layout",
|
||||
"controller_style_label": "Stile controller :",
|
||||
"controller_style_nintendo": "Nintendo",
|
||||
"controller_style_xbox": "Xbox",
|
||||
"menu_history": "Cronologia",
|
||||
"menu_language": "Lingua",
|
||||
"menu_accessibility": "Accessibilità",
|
||||
"menu_display": "Schermo",
|
||||
"display_layout": "Layout schermo",
|
||||
"menu_redownload_cache": "Aggiorna elenco giochi",
|
||||
"display_monitor": "Monitor",
|
||||
"display_monitor_single": "Monitor singolo",
|
||||
"display_monitor_single_only": "Rilevato un solo monitor",
|
||||
"display_monitor_restart_required": "Riavvio necessario per cambiare monitor",
|
||||
"display_mode": "Modalità schermo",
|
||||
"display_fullscreen": "Schermo intero",
|
||||
"display_windowed": "Finestra",
|
||||
"display_mode_restart_required": "Riavvio necessario per cambiare modalità", "display_light_mode": "Modalità performance",
|
||||
"display_light_mode_enabled": "Modalità performance attivata - effetti disattivati",
|
||||
"display_light_mode_disabled": "Modalità performance disattivata - effetti attivati", "menu_redownload_cache": "Aggiorna elenco giochi",
|
||||
"menu_music_enabled": "Musica attivata: {0}",
|
||||
"menu_music_disabled": "Musica disattivata",
|
||||
"menu_restart": "Riavvia",
|
||||
"menu_restart": "Riavvia RGSX",
|
||||
"menu_support": "Supporto",
|
||||
"menu_filter_platforms": "Filtra sistemi",
|
||||
"filter_platforms_title": "Visibilità sistemi",
|
||||
@@ -80,6 +100,7 @@
|
||||
"menu_allow_unknown_ext_enabled": "Nascondi avviso estensione sconosciuta abilitato",
|
||||
"menu_allow_unknown_ext_disabled": "Nascondi avviso estensione sconosciuta disabilitato",
|
||||
"menu_quit": "Esci",
|
||||
"menu_quit_app": "Esci da RGSX",
|
||||
"support_dialog_title": "File di supporto",
|
||||
"support_dialog_message": "È stato creato un file di supporto con tutti i file di configurazione e di registro.\n\nFile: {0}\n\nPer ottenere aiuto:\n1. Unisciti al server Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nPremi {1} per tornare al menu.",
|
||||
"support_dialog_error": "Errore durante la generazione del file di supporto:\n{0}\n\nPremi {1} per tornare al menu.",
|
||||
@@ -90,6 +111,7 @@
|
||||
"controls_action_clear_history": "Cancella cronologia",
|
||||
"controls_action_history": "Cronologia / Downloads",
|
||||
"controls_action_close_history": "Chiudi Cronologia",
|
||||
"history_column_folder": "Cartella",
|
||||
"controls_action_delete": "Elimina",
|
||||
"controls_action_space": "Spazio",
|
||||
"controls_action_start": "Aiuto / Impostazioni",
|
||||
@@ -147,6 +169,8 @@
|
||||
"controls_confirm_select": "Conferma/Seleziona",
|
||||
"controls_cancel_back": "Annulla/Indietro",
|
||||
"controls_filter_search": "Filtro/Ricerca",
|
||||
"controls_action_edit_search": "Modifica ricerca",
|
||||
"controls_action_show_results": "Mostra risultati",
|
||||
"games_source_rgsx": "RGSX",
|
||||
"sources_mode_rgsx_select_info": "RGSX: aggiorna l'elenco dei giochi",
|
||||
"games_source_custom": "Personalizzato",
|
||||
@@ -167,7 +191,14 @@
|
||||
"status_present": "Presente",
|
||||
"status_missing": "Assente",
|
||||
"menu_api_keys_status": "Chiavi API",
|
||||
"menu_connection_status": "Stato connessione",
|
||||
"api_keys_status_title": "Stato delle chiavi API",
|
||||
"connection_status_title": "Stato connessione",
|
||||
"connection_status_category_updates": "Aggiornamento app/lista giochi",
|
||||
"connection_status_category_sources": "Sorgenti giochi",
|
||||
"connection_status_checking": "Verifica in corso...",
|
||||
"connection_status_progress": "Verifica in corso... {done}/{total}",
|
||||
"connection_status_last_check": "Ultimo controllo: {time}",
|
||||
"menu_games": "Giochi",
|
||||
"api_keys_hint_manage": "Metti le tue chiavi in {path}",
|
||||
"api_key_empty_suffix": "vuoto",
|
||||
@@ -183,7 +214,9 @@
|
||||
"instruction_pause_settings": "Musica, opzione symlink e stato chiavi API",
|
||||
"instruction_pause_restart": "Riavvia RGSX per ricaricare la configurazione",
|
||||
"instruction_pause_support": "Genera un file ZIP diagnostico per il supporto",
|
||||
"instruction_pause_quit": "Uscire dall'applicazione RGSX",
|
||||
"instruction_pause_quit": "Accedere al menu per uscire o riavviare",
|
||||
"instruction_quit_app": "Uscire dall'applicazione RGSX",
|
||||
"instruction_quit_restart": "Riavviare l'applicazione RGSX",
|
||||
"instruction_controls_help": "Mostrare riferimento completo controller & tastiera",
|
||||
"instruction_controls_remap": "Modificare associazione pulsanti / tasti",
|
||||
"instruction_generic_back": "Tornare al menu precedente",
|
||||
@@ -191,6 +224,9 @@
|
||||
"instruction_display_font_size": "Regolare dimensione testo per leggibilità",
|
||||
"instruction_display_footer_font_size": "Regola dimensione testo piè di pagina (versione e controlli)",
|
||||
"instruction_display_font_family": "Cambiare famiglia di font disponibile",
|
||||
"instruction_display_monitor": "Selezionare monitor per visualizzare RGSX",
|
||||
"instruction_display_mode": "Alternare tra schermo intero e finestra",
|
||||
"instruction_display_light_mode": "Attivare modalità performance per FPS migliori",
|
||||
"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg",
|
||||
"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}",
|
||||
@@ -198,11 +234,24 @@
|
||||
"instruction_games_history": "Elencare download passati e stato",
|
||||
"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata",
|
||||
"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi",
|
||||
"instruction_games_scan_owned": "Scansiona le cartelle ROMs e segna i giochi gia posseduti",
|
||||
"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo",
|
||||
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
|
||||
"instruction_settings_auto_extract": "Attivare/disattivare estrazione automatica archivi dopo il download",
|
||||
"instruction_settings_roms_folder": "Cambiare la directory di download ROMs predefinita",
|
||||
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
|
||||
"instruction_settings_connection_status": "Verifica accesso ai siti di aggiornamento e sorgenti",
|
||||
"instruction_settings_web_service": "Attivare/disattivare avvio automatico servizio web all'avvio",
|
||||
"instruction_settings_custom_dns": "Attivare/disattivare DNS personalizzato (Cloudflare 1.1.1.1) all'avvio",
|
||||
"settings_auto_extract": "Estrazione auto archivi",
|
||||
"settings_auto_extract_enabled": "Attivato",
|
||||
"settings_auto_extract_disabled": "Disattivato",
|
||||
"settings_roms_folder": "Cartella ROMs",
|
||||
"settings_roms_folder_default": "Predefinita",
|
||||
"roms_folder_set": "Cartella ROMs impostata: {0}",
|
||||
"roms_folder_set_restart": "Cartella ROMs impostata: {0}\nRiavvio necessario per applicare!",
|
||||
"roms_folder_reset": "Cartella ROMs ripristinata predefinita\nRiavvio necessario per applicare!",
|
||||
"folder_browser_title_roms_root": "Seleziona cartella ROMs predefinita",
|
||||
"settings_web_service": "Servizio Web all'Avvio",
|
||||
"settings_web_service_enabled": "Abilitato",
|
||||
"settings_web_service_disabled": "Disabilitato",
|
||||
@@ -217,6 +266,9 @@
|
||||
"settings_custom_dns_enabling": "Abilitazione DNS personalizzato...",
|
||||
"settings_custom_dns_disabling": "Disabilitazione DNS personalizzato...",
|
||||
"settings_custom_dns_success_enabled": "DNS personalizzato abilitato all'avvio (1.1.1.1)",
|
||||
"menu_scan_owned_roms": "Scansiona ROM presenti",
|
||||
"popup_scan_owned_roms_done": "Scansione ROM completata: {0} giochi aggiunti su {1} piattaforme",
|
||||
"popup_scan_owned_roms_error": "Errore scansione ROM: {0}",
|
||||
"settings_custom_dns_success_disabled": "DNS personalizzato disabilitato all'avvio",
|
||||
"controls_desc_confirm": "Confermare (es. A/Croce)",
|
||||
"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)",
|
||||
@@ -241,7 +293,12 @@
|
||||
"history_game_options_title": "Opzioni gioco",
|
||||
"history_option_download_folder": "Localizza file",
|
||||
"history_option_extract_archive": "Estrai archivio",
|
||||
"history_option_open_file": "Apri file",
|
||||
"history_option_scraper": "Scraper metadati",
|
||||
"history_option_remove_from_queue": "Rimuovi dalla coda",
|
||||
"history_option_cancel_download": "Annulla download",
|
||||
"history_option_pause_download": "Pausa download",
|
||||
"history_option_resume_download": "Riprendi download",
|
||||
"history_option_delete_game": "Elimina gioco",
|
||||
"history_option_error_info": "Dettagli errore",
|
||||
"history_option_retry": "Riprova download",
|
||||
@@ -304,6 +361,9 @@
|
||||
"web_settings_source_mode": "Fonte giochi",
|
||||
"web_settings_custom_url": "URL personalizzato",
|
||||
"web_settings_custom_url_placeholder": " Lasciare vuoto per /saves/ports/rgsx/games.zip o usare una URL diretta come https://esempio.com/giochi.zip",
|
||||
"web_settings_auto_extract": "Estrai automaticamente gli archivi dopo il download",
|
||||
"web_settings_web_service": "Avvia servizio web all'avvio",
|
||||
"web_settings_custom_dns": "Abilita DNS personalizzato all'avvio",
|
||||
"web_settings_save": "Salva impostazioni",
|
||||
"web_settings_saved": "Impostazioni salvate con successo!",
|
||||
"web_settings_saved_restart": "Impostazioni salvate con successo!\\n\\n⚠️ Alcune impostazioni richiedono il riavvio del server:\\n- Cartella ROMs personalizzata\\n- Lingua\\n\\nRiavviare il server web per applicare queste modifiche.",
|
||||
@@ -327,6 +387,7 @@
|
||||
"web_history_status_completed": "Completato",
|
||||
"web_history_status_error": "Errore",
|
||||
"web_settings_os": "Sistema operativo",
|
||||
"web_system_info_title": "Informazioni di sistema",
|
||||
"web_settings_platforms_count": "Numero di piattaforme",
|
||||
"web_settings_show_unsupported": "Mostra piattaforme non supportate (sistema assente in es_systems.cfg)",
|
||||
"web_settings_allow_unknown": "Consenti estensioni sconosciute (non mostrare avvisi)",
|
||||
@@ -338,7 +399,7 @@
|
||||
"web_restart_error": "Errore durante il riavvio: {0}",
|
||||
"web_support": "Supporto",
|
||||
"web_support_title": "📦 File di supporto generato",
|
||||
"web_support_message": "File di supporto creato con successo!\\n\\n📁 Contenuto:\\n• Configurazione controlli\\n• Cronologia download\\n• Impostazioni RGSX\\n• Log dell'applicazione\\n• Log del server web\\n\\n💬 Per ottenere aiuto:\\n1. Unisciti al Discord RGSX\\n2. Descrivi il tuo problema\\n3. Condividi questo file ZIP\\n\\nIl download inizierà...",
|
||||
"web_support_message": "File di supporto creato con successo!\n\n📁 Contenuto:\n• Configurazione controlli\n• Cronologia download\n• Impostazioni RGSX\n• Log dell'applicazione\n• Log del server web\n\n💬 Per ottenere aiuto:\n1. Unisciti al Discord RGSX\n2. Descrivi il tuo problema\n3. Condividi questo file ZIP\n\nIl download inizierà...",
|
||||
"web_support_generating": "Generazione file di supporto...",
|
||||
"web_support_download": "Scarica file di supporto",
|
||||
"web_support_error": "Errore nella generazione del file di supporto: {0}",
|
||||
@@ -370,7 +431,6 @@
|
||||
"filter_all": "Seleziona tutto",
|
||||
"filter_none": "Deseleziona tutto",
|
||||
"filter_apply": "Applica filtro",
|
||||
"filter_back": "Indietro",
|
||||
"accessibility_footer_font_size": "Dimensione carattere piè di pagina: {0}",
|
||||
"popup_layout_changed_restart": "Layout cambiato a {0}x{1}. Riavvia l'app per applicare.",
|
||||
"web_started": "Avviato",
|
||||
@@ -379,5 +439,51 @@
|
||||
"web_added_to_queue": "aggiunto alla coda",
|
||||
"web_download_success": "scaricato con successo!",
|
||||
"web_download_error_for": "Errore durante il download di",
|
||||
"web_already_present": "era già presente"
|
||||
"web_already_present": "era già presente",
|
||||
"filter_menu_title": "Menu filtri",
|
||||
"filter_search_by_name": "Cerca per nome",
|
||||
"filter_advanced": "Filtro avanzato",
|
||||
"filter_advanced_title": "Filtro avanzato giochi",
|
||||
"filter_region_title": "Filtra per regione",
|
||||
"filter_region_include": "Includi",
|
||||
"filter_region_exclude": "Escludi",
|
||||
"filter_region_usa": "USA",
|
||||
"filter_region_canada": "Canada",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "Francia",
|
||||
"filter_region_germany": "Germania",
|
||||
"filter_region_japan": "Giappone",
|
||||
"filter_region_korea": "Corea",
|
||||
"filter_region_world": "Mondo",
|
||||
"filter_region_other": "Altro",
|
||||
"filter_other_options": "Altre opzioni",
|
||||
"filter_hide_non_release": "Nascondi Demo/Beta/Proto",
|
||||
"filter_one_rom_per_game": "Una ROM per gioco",
|
||||
"filter_priority_order": "Ordine di priorità",
|
||||
"filter_priority_title": "Configurazione priorità regioni",
|
||||
"filter_priority_desc": "Imposta ordine di preferenza per \"Una ROM per gioco\"",
|
||||
"filter_regex_mode": "Modalità Regex",
|
||||
"filter_apply_filters": "Applica",
|
||||
"filter_reset_filters": "Reimposta",
|
||||
"filter_back": "Indietro",
|
||||
"filter_active": "Filtro attivo",
|
||||
"filter_games_shown": "{0} gioco/i mostrato/i",
|
||||
"platform_folder_config_current": "Configura cartella download per {0}\nAttuale: {1}",
|
||||
"platform_folder_config_default": "Configura cartella download per {0}\nUsando posizione predefinita",
|
||||
"platform_folder_show_current": "Mostra percorso attuale",
|
||||
"platform_folder_browse": "Sfoglia",
|
||||
"platform_folder_reset": "Ripristina predefinito",
|
||||
"platform_folder_set": "Cartella impostata per {0}: {1}",
|
||||
"platform_folder_default_path": "Predefinito: {0}",
|
||||
"folder_browser_title": "Seleziona cartella per {0}",
|
||||
"folder_browser_parent": "Cartella superiore",
|
||||
"folder_browser_enter": "Entra",
|
||||
"folder_browser_select": "Seleziona",
|
||||
"folder_new_folder": "Nuova cartella",
|
||||
"folder_new_title": "Crea nuova cartella",
|
||||
"folder_new_confirm": "Crea",
|
||||
"folder_created": "Cartella creata: {0}",
|
||||
"folder_create_error": "Errore nella creazione: {0}",
|
||||
"controls_action_select_char": "Aggiungi",
|
||||
"folder_browser_browse": "Sfoglia"
|
||||
}
|
||||
@@ -24,7 +24,11 @@
|
||||
"game_count": "{0} ({1} jogos)",
|
||||
"game_filter": "Filtro ativo: {0}",
|
||||
"game_search": "Filtro: {0}",
|
||||
"global_search_title": "Busca global: {0}",
|
||||
"global_search_empty_query": "Digite um nome para buscar em todos os consoles",
|
||||
"global_search_no_results": "Nenhum resultado para: {0}",
|
||||
"game_header_name": "Nome",
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Tamanho",
|
||||
"history_title": "Downloads ({0})",
|
||||
"history_empty": "Nenhum download no histórico",
|
||||
@@ -44,6 +48,7 @@
|
||||
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download cancelado pelo usuário.",
|
||||
"download_removed_from_queue": "Removido da fila de download",
|
||||
"extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?",
|
||||
"extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo a configuração es_systems.cfg. Deseja continuar?",
|
||||
"extension_warning_enable_unknown_hint": "\nPara não ver esta mensagem: ative \"Ocultar aviso de extensão desconhecida\" em Menu de Pausa > Exibição",
|
||||
@@ -51,6 +56,8 @@
|
||||
"confirm_exit_with_downloads": "Atenção: {0} download(s) em andamento. Sair mesmo assim?",
|
||||
"confirm_clear_history": "Limpar histórico?",
|
||||
"confirm_redownload_cache": "Atualizar lista de jogos?",
|
||||
"gamelist_update_prompt_with_date": "A lista de jogos não foi atualizada há mais de {0} dias (última atualização: {1}). Baixar a versão mais recente?",
|
||||
"gamelist_update_prompt_first_time": "Gostaria de baixar a última lista de jogos?",
|
||||
"popup_redownload_success": "Cache limpo, reinicie a aplicação",
|
||||
"popup_no_cache": "Nenhum cache encontrado.\nReinicie a aplicação para carregar os jogos.",
|
||||
"popup_countdown": "Esta mensagem fechará em {0} segundo{1}",
|
||||
@@ -59,15 +66,32 @@
|
||||
"language_changed": "Idioma alterado para {0}",
|
||||
"menu_controls": "Controles",
|
||||
"menu_remap_controls": "Remapear controles",
|
||||
"menu_nintendo_layout_on": "Layout do controle Nintendo",
|
||||
"menu_nintendo_layout_off": "Layout do controle Xbox",
|
||||
"instruction_nintendo_layout": "Inverte os controles exibidos para corresponder ao layout",
|
||||
"controller_style_label": "Estilo do controle :",
|
||||
"controller_style_nintendo": "Nintendo",
|
||||
"controller_style_xbox": "Xbox",
|
||||
"menu_history": "Histórico",
|
||||
"menu_language": "Idioma",
|
||||
"menu_accessibility": "Acessibilidade",
|
||||
"menu_display": "Exibição",
|
||||
"display_layout": "Layout de exibição",
|
||||
"display_monitor": "Monitor",
|
||||
"display_monitor_single": "Monitor único",
|
||||
"display_monitor_single_only": "Apenas um monitor detectado",
|
||||
"display_monitor_restart_required": "Reinício necessário para mudar de monitor",
|
||||
"display_mode": "Modo de tela",
|
||||
"display_fullscreen": "Tela cheia",
|
||||
"display_windowed": "Janela",
|
||||
"display_mode_restart_required": "Reinício necessário para mudar o modo",
|
||||
"display_light_mode": "Modo performance",
|
||||
"display_light_mode_enabled": "Modo performance ativado - efeitos desativados",
|
||||
"display_light_mode_disabled": "Modo performance desativado - efeitos ativados",
|
||||
"menu_redownload_cache": "Atualizar lista de jogos",
|
||||
"menu_music_enabled": "Música ativada: {0}",
|
||||
"menu_music_disabled": "Música desativada",
|
||||
"menu_restart": "Reiniciar",
|
||||
"menu_restart": "Reiniciar RGSX",
|
||||
"menu_support": "Suporte",
|
||||
"menu_filter_platforms": "Filtrar sistemas",
|
||||
"filter_platforms_title": "Visibilidade dos sistemas",
|
||||
@@ -80,6 +104,7 @@
|
||||
"menu_allow_unknown_ext_enabled": "Aviso de extensão desconhecida oculto (ativado)",
|
||||
"menu_allow_unknown_ext_disabled": "Aviso de extensão desconhecida visível (desativado)",
|
||||
"menu_quit": "Sair",
|
||||
"menu_quit_app": "Sair do RGSX",
|
||||
"support_dialog_title": "Arquivo de suporte",
|
||||
"support_dialog_message": "Foi criado um arquivo de suporte com todos os seus arquivos de configuração e logs.\n\nArquivo: {0}\n\nPara obter ajuda:\n1. Junte-se ao servidor Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nPressione {1} para voltar ao menu.",
|
||||
"support_dialog_error": "Erro ao gerar o arquivo de suporte:\n{0}\n\nPressione {1} para voltar ao menu.",
|
||||
@@ -90,6 +115,7 @@
|
||||
"controls_action_clear_history": "Limpar histórico",
|
||||
"controls_action_history": "Histórico / Downloads",
|
||||
"controls_action_close_history": "Fechar Histórico",
|
||||
"history_column_folder": "Pasta",
|
||||
"controls_action_delete": "Deletar",
|
||||
"controls_action_space": "Espaço",
|
||||
"controls_action_start": "Ajuda / Configurações",
|
||||
@@ -146,6 +172,8 @@
|
||||
"controls_confirm_select": "Confirmar/Selecionar",
|
||||
"controls_cancel_back": "Cancelar/Voltar",
|
||||
"controls_filter_search": "Filtrar/Buscar",
|
||||
"controls_action_edit_search": "Editar busca",
|
||||
"controls_action_show_results": "Ver resultados",
|
||||
"symlink_option_enabled": "Opção de symlink ativada",
|
||||
"symlink_option_disabled": "Opção de symlink desativada",
|
||||
"menu_games_source_prefix": "Fonte de jogos",
|
||||
@@ -169,7 +197,14 @@
|
||||
"status_present": "Presente",
|
||||
"status_missing": "Ausente",
|
||||
"menu_api_keys_status": "Chaves API",
|
||||
"menu_connection_status": "Estado da conexão",
|
||||
"api_keys_status_title": "Status das chaves API",
|
||||
"connection_status_title": "Estado da conexão",
|
||||
"connection_status_category_updates": "Atualização do app/lista de jogos",
|
||||
"connection_status_category_sources": "Fontes de jogos",
|
||||
"connection_status_checking": "Verificando...",
|
||||
"connection_status_progress": "Verificando... {done}/{total}",
|
||||
"connection_status_last_check": "Última verificação: {time}",
|
||||
"menu_games": "Jogos",
|
||||
"api_keys_hint_manage": "Coloque suas chaves em {path}",
|
||||
"api_key_empty_suffix": "vazio",
|
||||
@@ -185,7 +220,9 @@
|
||||
"instruction_pause_settings": "Música, opção symlink e status das chaves API",
|
||||
"instruction_pause_restart": "Reiniciar RGSX para recarregar configuração",
|
||||
"instruction_pause_support": "Gerar um arquivo ZIP de diagnóstico para suporte",
|
||||
"instruction_pause_quit": "Sair da aplicação RGSX",
|
||||
"instruction_pause_quit": "Acessar menu para sair ou reiniciar",
|
||||
"instruction_quit_app": "Sair da aplicação RGSX",
|
||||
"instruction_quit_restart": "Reiniciar a aplicação RGSX",
|
||||
"instruction_controls_help": "Mostrar referência completa de controle e teclado",
|
||||
"instruction_controls_remap": "Modificar associação de botões / teclas",
|
||||
"instruction_generic_back": "Voltar ao menu anterior",
|
||||
@@ -193,6 +230,9 @@
|
||||
"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade",
|
||||
"instruction_display_footer_font_size": "Ajustar tamanho do texto do rodapé (versão e controles)",
|
||||
"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis",
|
||||
"instruction_display_monitor": "Selecionar monitor para exibir RGSX",
|
||||
"instruction_display_mode": "Alternar entre tela cheia e janela",
|
||||
"instruction_display_light_mode": "Ativar modo performance para melhor FPS",
|
||||
"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg",
|
||||
"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg",
|
||||
"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}",
|
||||
@@ -200,11 +240,24 @@
|
||||
"instruction_games_history": "Listar downloads anteriores e status",
|
||||
"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada",
|
||||
"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos",
|
||||
"instruction_games_scan_owned": "Verificar as pastas ROMs e marcar os jogos ja existentes",
|
||||
"instruction_settings_music": "Ativar ou desativar música de fundo",
|
||||
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
|
||||
"instruction_settings_auto_extract": "Ativar/desativar extração automática de arquivos após download",
|
||||
"instruction_settings_roms_folder": "Alterar o diretório de download de ROMs padrão",
|
||||
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
|
||||
"instruction_settings_connection_status": "Verificar acesso a sites de atualização e fontes",
|
||||
"instruction_settings_web_service": "Ativar/desativar início automático do serviço web na inicialização",
|
||||
"instruction_settings_custom_dns": "Ativar/desativar DNS personalizado (Cloudflare 1.1.1.1) na inicialização",
|
||||
"settings_auto_extract": "Extração auto de arquivos",
|
||||
"settings_auto_extract_enabled": "Ativado",
|
||||
"settings_auto_extract_disabled": "Desativado",
|
||||
"settings_roms_folder": "Pasta ROMs",
|
||||
"settings_roms_folder_default": "Padrão",
|
||||
"roms_folder_set": "Pasta ROMs definida: {0}",
|
||||
"roms_folder_set_restart": "Pasta ROMs definida: {0}\nReinício necessário para aplicar!",
|
||||
"roms_folder_reset": "Pasta ROMs redefinida para padrão\nReinício necessário para aplicar!",
|
||||
"folder_browser_title_roms_root": "Selecionar pasta ROMs padrão",
|
||||
"settings_web_service": "Serviço Web na Inicialização",
|
||||
"settings_web_service_enabled": "Ativado",
|
||||
"settings_web_service_disabled": "Desativado",
|
||||
@@ -213,6 +266,9 @@
|
||||
"settings_web_service_success_enabled": "Serviço web ativado na inicialização",
|
||||
"settings_web_service_success_disabled": "Serviço web desativado na inicialização",
|
||||
"settings_web_service_error": "Erro: {0}",
|
||||
"menu_scan_owned_roms": "Verificar ROMs existentes",
|
||||
"popup_scan_owned_roms_done": "Verificacao de ROMs concluida: {0} jogos adicionados em {1} plataformas",
|
||||
"popup_scan_owned_roms_error": "Erro ao verificar ROMs: {0}",
|
||||
"settings_custom_dns": "DNS Personalizado na Inicialização",
|
||||
"settings_custom_dns_enabled": "Ativado",
|
||||
"settings_custom_dns_disabled": "Desativado",
|
||||
@@ -243,7 +299,12 @@
|
||||
"history_game_options_title": "Opções do jogo",
|
||||
"history_option_download_folder": "Localizar arquivo",
|
||||
"history_option_extract_archive": "Extrair arquivo",
|
||||
"history_option_scraper": "Scraper metadados",
|
||||
"history_option_open_file": "Abrir arquivo",
|
||||
"history_option_scraper": "Obter metadados",
|
||||
"history_option_remove_from_queue": "Remover da fila",
|
||||
"history_option_cancel_download": "Cancelar download",
|
||||
"history_option_pause_download": "Pausar download",
|
||||
"history_option_resume_download": "Retomar download",
|
||||
"history_option_delete_game": "Excluir jogo",
|
||||
"history_option_error_info": "Detalhes do erro",
|
||||
"history_option_retry": "Tentar novamente",
|
||||
@@ -306,6 +367,9 @@
|
||||
"web_settings_source_mode": "Fonte de jogos",
|
||||
"web_settings_custom_url": "URL personalizada",
|
||||
"web_settings_custom_url_placeholder": "Deixar vazio para /saves/ports/rgsx/games.zip ou usar uma URL direta como https://example.com/games.zip",
|
||||
"web_settings_auto_extract": "Extrair arquivos automaticamente após o download",
|
||||
"web_settings_web_service": "Iniciar serviço web na inicialização",
|
||||
"web_settings_custom_dns": "Ativar DNS personalizado na inicialização",
|
||||
"web_settings_save": "Salvar configurações",
|
||||
"web_settings_saved": "Configurações salvas com sucesso!",
|
||||
"web_settings_saved_restart": "Configurações salvas com sucesso!\\n\\n⚠️ Algumas configurações exigem reiniciar o servidor:\\n- Pasta ROMs personalizada\\n- Idioma\\n\\nPor favor, reinicie o servidor web para aplicar essas alterações.",
|
||||
@@ -329,6 +393,7 @@
|
||||
"web_history_status_completed": "Concluído",
|
||||
"web_history_status_error": "Erro",
|
||||
"web_settings_os": "Sistema operacional",
|
||||
"web_system_info_title": "Informações do sistema",
|
||||
"web_settings_platforms_count": "Número de plataformas",
|
||||
"web_settings_show_unsupported": "Mostrar plataformas não suportadas (sistema ausente em es_systems.cfg)",
|
||||
"web_settings_allow_unknown": "Permitir extensões desconhecidas (não mostrar avisos)",
|
||||
@@ -340,7 +405,7 @@
|
||||
"web_restart_error": "Erro ao reiniciar: {0}",
|
||||
"web_support": "Suporte",
|
||||
"web_support_title": "📦 Arquivo de suporte gerado",
|
||||
"web_support_message": "Arquivo de suporte criado com sucesso!\\n\\n📁 Conteúdo:\\n• Configuração de controles\\n• Histórico de downloads\\n• Configurações RGSX\\n• Logs da aplicação\\n• Logs do servidor web\\n\\n💬 Para obter ajuda:\\n1. Entre no Discord RGSX\\n2. Descreva seu problema\\n3. Compartilhe este arquivo ZIP\\n\\nO download vai começar...",
|
||||
"web_support_message": "Arquivo de suporte criado com sucesso!\n\n📁 Conteúdo:\n• Configuração de controles\n• Histórico de downloads\n• Configurações RGSX\n• Logs da aplicação\n• Logs do servidor web\n\n💬 Para obter ajuda:\n1. Entre no Discord RGSX\n2. Descreva seu problema\n3. Compartilhe este arquivo ZIP\n\nO download vai começar...",
|
||||
"web_support_generating": "Gerando arquivo de suporte...",
|
||||
"web_support_download": "Baixar arquivo de suporte",
|
||||
"web_support_error": "Erro ao gerar arquivo de suporte: {0}",
|
||||
@@ -370,7 +435,6 @@
|
||||
"filter_all": "Marcar tudo",
|
||||
"filter_none": "Desmarcar tudo",
|
||||
"filter_apply": "Aplicar filtro",
|
||||
"filter_back": "Voltar",
|
||||
"accessibility_footer_font_size": "Tamanho da fonte do rodapé: {0}",
|
||||
"popup_layout_changed_restart": "Layout alterado para {0}x{1}. Reinicie o app para aplicar.",
|
||||
"web_started": "Iniciado",
|
||||
@@ -379,5 +443,51 @@
|
||||
"web_added_to_queue": "adicionado à fila",
|
||||
"web_download_success": "baixado com sucesso!",
|
||||
"web_download_error_for": "Erro ao baixar",
|
||||
"web_already_present": "já estava presente"
|
||||
"web_already_present": "já estava presente",
|
||||
"filter_menu_title": "Menu de filtros",
|
||||
"filter_search_by_name": "Pesquisar por nome",
|
||||
"filter_advanced": "Filtragem avançada",
|
||||
"filter_advanced_title": "Filtragem avançada de jogos",
|
||||
"filter_region_title": "Filtrar por região",
|
||||
"filter_region_include": "Incluir",
|
||||
"filter_region_exclude": "Excluir",
|
||||
"filter_region_usa": "EUA",
|
||||
"filter_region_canada": "Canadá",
|
||||
"filter_region_europe": "Europa",
|
||||
"filter_region_france": "França",
|
||||
"filter_region_germany": "Alemanha",
|
||||
"filter_region_japan": "Japão",
|
||||
"filter_region_korea": "Coreia",
|
||||
"filter_region_world": "Mundial",
|
||||
"filter_region_other": "Outros",
|
||||
"filter_other_options": "Outras opções",
|
||||
"filter_hide_non_release": "Ocultar Demos/Betas/Protos",
|
||||
"filter_one_rom_per_game": "Uma ROM por jogo",
|
||||
"filter_priority_order": "Ordem de prioridade",
|
||||
"filter_priority_title": "Configuração de prioridade de regiões",
|
||||
"filter_priority_desc": "Definir ordem de preferência para \"Uma ROM por jogo\"",
|
||||
"filter_regex_mode": "Modo Regex",
|
||||
"filter_apply_filters": "Aplicar",
|
||||
"filter_reset_filters": "Redefinir",
|
||||
"filter_back": "Voltar",
|
||||
"filter_active": "Filtro ativo",
|
||||
"filter_games_shown": "{0} jogo(s) exibido(s)",
|
||||
"platform_folder_config_current": "Configurar pasta de download para {0}\nAtual: {1}",
|
||||
"platform_folder_config_default": "Configurar pasta de download para {0}\nUsando localização padrão",
|
||||
"platform_folder_show_current": "Mostrar caminho atual",
|
||||
"platform_folder_browse": "Navegar",
|
||||
"platform_folder_reset": "Redefinir para padrão",
|
||||
"platform_folder_set": "Pasta definida para {0}: {1}",
|
||||
"platform_folder_default_path": "Padrão: {0}",
|
||||
"folder_browser_title": "Selecionar pasta para {0}",
|
||||
"folder_browser_parent": "Pasta superior",
|
||||
"folder_browser_enter": "Entrar",
|
||||
"folder_browser_select": "Selecionar",
|
||||
"folder_new_folder": "Nova pasta",
|
||||
"folder_new_title": "Criar nova pasta",
|
||||
"folder_new_confirm": "Criar",
|
||||
"folder_created": "Pasta criada: {0}",
|
||||
"folder_create_error": "Erro ao criar: {0}",
|
||||
"controls_action_select_char": "Adicionar",
|
||||
"folder_browser_browse": "Explorar"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import requests
|
||||
import requests # type: ignore
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
@@ -15,7 +15,7 @@ try:
|
||||
except Exception:
|
||||
pygame = None # type: ignore
|
||||
from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZIP
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie
|
||||
from history import save_history
|
||||
from display import show_toast
|
||||
import logging
|
||||
@@ -32,11 +32,45 @@ from language import _ # Import de la fonction de traduction
|
||||
import re
|
||||
import html as html_module
|
||||
from urllib.parse import urljoin, unquote
|
||||
import urllib.parse
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _redact_headers(headers: dict) -> dict:
|
||||
"""Return a copy of headers with sensitive fields redacted for logs."""
|
||||
if not isinstance(headers, dict):
|
||||
return {}
|
||||
safe = headers.copy()
|
||||
if 'Cookie' in safe and safe['Cookie']:
|
||||
safe['Cookie'] = '<redacted>'
|
||||
return safe
|
||||
|
||||
|
||||
def _split_archive_org_path(url: str):
|
||||
"""Parse archive.org download URL and return (identifier, archive_name, inner_path)."""
|
||||
try:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
parts = parsed.path.split('/download/', 1)
|
||||
if len(parts) != 2:
|
||||
return None, None, None
|
||||
after = parts[1]
|
||||
identifier = after.split('/', 1)[0]
|
||||
rest = after[len(identifier):]
|
||||
if rest.startswith('/'):
|
||||
rest = rest[1:]
|
||||
rest_decoded = urllib.parse.unquote(rest)
|
||||
if '/' not in rest_decoded:
|
||||
return identifier, None, None
|
||||
first_seg, remainder = rest_decoded.split('/', 1)
|
||||
if first_seg.lower().endswith(('.zip', '.rar', '.7z')):
|
||||
return identifier, first_seg, remainder
|
||||
return identifier, None, None
|
||||
except Exception:
|
||||
return None, None, None
|
||||
|
||||
# --- File d'attente de téléchargements (worker) ---
|
||||
def download_queue_worker():
|
||||
"""Worker qui surveille la file d'attente et lance le prochain téléchargement si aucun n'est actif."""
|
||||
@@ -404,7 +438,6 @@ def test_internet():
|
||||
]
|
||||
|
||||
for test_url in test_urls:
|
||||
logger.debug(f"Test connexion HTTP vers {test_url}")
|
||||
try:
|
||||
response = requests.get(test_url, timeout=5, allow_redirects=True)
|
||||
if response.status_code == 200:
|
||||
@@ -453,8 +486,102 @@ async def check_for_updates():
|
||||
config.loading_progress = 5.0
|
||||
config.needs_redraw = True
|
||||
|
||||
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
|
||||
response.raise_for_status()
|
||||
# Liste des endpoints à essayer (GitHub principal, puis fallback)
|
||||
endpoints = [
|
||||
OTA_VERSION_ENDPOINT,
|
||||
"https://retrogamesets.fr/softs/version.json"
|
||||
]
|
||||
|
||||
response = None
|
||||
last_error = None
|
||||
|
||||
for endpoint_index, endpoint in enumerate(endpoints):
|
||||
is_fallback = endpoint_index > 0
|
||||
if is_fallback:
|
||||
logger.info(f"Tentative sur endpoint de secours : {endpoint}")
|
||||
|
||||
# Gestion des erreurs de rate limit GitHub (429) avec retry
|
||||
max_retries = 3 if not is_fallback else 1 # Moins de retries sur fallback
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
response = requests.get(endpoint, timeout=10)
|
||||
|
||||
# Gestion spécifique des erreurs 429 (Too Many Requests) - surtout pour GitHub
|
||||
if response.status_code == 429:
|
||||
retry_after = response.headers.get('retry-after')
|
||||
x_ratelimit_remaining = response.headers.get('x-ratelimit-remaining', '1')
|
||||
x_ratelimit_reset = response.headers.get('x-ratelimit-reset')
|
||||
|
||||
if retry_after:
|
||||
# En-tête retry-after présent : attendre le nombre de secondes spécifié
|
||||
wait_time = int(retry_after)
|
||||
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (retry-after header)")
|
||||
elif x_ratelimit_remaining == '0' and x_ratelimit_reset:
|
||||
# x-ratelimit-remaining est 0 : attendre jusqu'à x-ratelimit-reset
|
||||
import time
|
||||
reset_time = int(x_ratelimit_reset)
|
||||
current_time = int(time.time())
|
||||
wait_time = max(reset_time - current_time, 60) # Minimum 60s
|
||||
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s (x-ratelimit-reset)")
|
||||
else:
|
||||
# Pas d'en-têtes spécifiques : attendre au moins 60s
|
||||
wait_time = 60
|
||||
logger.warning(f"Rate limit atteint (429) sur {endpoint}. Attente de {wait_time}s par défaut")
|
||||
|
||||
if retry_count < max_retries - 1:
|
||||
logger.info(f"Nouvelle tentative dans {wait_time}s... ({retry_count + 1}/{max_retries})")
|
||||
await asyncio.sleep(wait_time)
|
||||
retry_count += 1
|
||||
continue
|
||||
else:
|
||||
# Si rate limit persistant et qu'on est sur GitHub, essayer le fallback
|
||||
if not is_fallback:
|
||||
logger.warning(f"Rate limit GitHub persistant, passage au serveur de secours")
|
||||
break # Sortir de la boucle retry pour essayer le prochain endpoint
|
||||
raise requests.exceptions.HTTPError(
|
||||
f"Limite de débit atteinte (429). Veuillez réessayer plus tard."
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
# Succès, sortir de toutes les boucles
|
||||
logger.debug(f"Version récupérée avec succès depuis : {endpoint}")
|
||||
break
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
last_error = e
|
||||
if response and response.status_code == 429:
|
||||
# 429 géré au-dessus, continuer la boucle ou passer au fallback
|
||||
retry_count += 1
|
||||
if retry_count >= max_retries:
|
||||
break # Passer au prochain endpoint
|
||||
else:
|
||||
# Erreur HTTP autre que 429
|
||||
logger.warning(f"Erreur HTTP {response.status_code if response else 'inconnue'} sur {endpoint}")
|
||||
break # Passer au prochain endpoint
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
last_error = e
|
||||
if retry_count < max_retries - 1:
|
||||
# Erreur réseau, réessayer avec backoff exponentiel
|
||||
wait_time = 2 ** retry_count # 1s, 2s, 4s
|
||||
logger.warning(f"Erreur réseau sur {endpoint}. Nouvelle tentative dans {wait_time}s...")
|
||||
await asyncio.sleep(wait_time)
|
||||
retry_count += 1
|
||||
else:
|
||||
logger.warning(f"Erreur réseau persistante sur {endpoint} : {e}")
|
||||
break # Passer au prochain endpoint
|
||||
|
||||
# Si on a une réponse valide, sortir de la boucle des endpoints
|
||||
if response and response.status_code == 200:
|
||||
break
|
||||
|
||||
# Si aucun endpoint n'a fonctionné
|
||||
if not response or response.status_code != 200:
|
||||
raise last_error if last_error else requests.exceptions.RequestException(
|
||||
"Impossible de vérifier les mises à jour sur tous les serveurs"
|
||||
)
|
||||
|
||||
# Accepter différents content-types (application/json, text/plain, text/html)
|
||||
content_type = response.headers.get("content-type", "")
|
||||
@@ -601,6 +728,8 @@ def extract_update(zip_path, dest_dir, source_url):
|
||||
progress_queues = {}
|
||||
# Cancellation and thread tracking per download task
|
||||
cancel_events = {}
|
||||
# Pause events for downloads
|
||||
pause_events = {} # {task_id: threading.Event} - Event is set when paused
|
||||
download_threads = {}
|
||||
# URLs actuellement en cours de téléchargement (pour éviter les doublons)
|
||||
urls_in_progress = set()
|
||||
@@ -624,6 +753,32 @@ def request_cancel(task_id: str) -> bool:
|
||||
logger.debug(f"No cancel event found for task_id={task_id}")
|
||||
return False
|
||||
|
||||
def toggle_pause_download(task_id: str) -> bool:
|
||||
"""Toggle pause state for a running download task. Returns True if now paused, False if resumed."""
|
||||
ev = pause_events.get(task_id)
|
||||
if ev is None:
|
||||
# Créer l'événement de pause s'il n'existe pas
|
||||
pause_events[task_id] = threading.Event()
|
||||
ev = pause_events[task_id]
|
||||
|
||||
if ev.is_set():
|
||||
# Actuellement en pause, reprendre
|
||||
ev.clear()
|
||||
logger.debug(f"Download resumed for task_id={task_id}")
|
||||
return False # Retourne False = pas en pause (repris)
|
||||
else:
|
||||
# Actuellement actif, mettre en pause
|
||||
ev.set()
|
||||
logger.debug(f"Download paused for task_id={task_id}")
|
||||
return True # Retourne True = en pause
|
||||
|
||||
def is_download_paused(task_id: str) -> bool:
|
||||
"""Check if a download is currently paused."""
|
||||
ev = pause_events.get(task_id)
|
||||
if ev is not None:
|
||||
return ev.is_set()
|
||||
return False
|
||||
|
||||
def cancel_all_downloads():
|
||||
"""Cancel all active downloads and queued downloads, and attempt to stop threads quickly."""
|
||||
# Annuler tous les téléchargements actifs via cancel_events
|
||||
@@ -700,6 +855,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
cancel_events[task_id] = threading.Event()
|
||||
|
||||
def download_thread():
|
||||
nonlocal url
|
||||
try:
|
||||
# IMPORTANT: Créer l'entrée dans config.history dès le début avec status "Downloading"
|
||||
# pour que l'interface web puisse afficher le téléchargement en cours
|
||||
@@ -743,19 +899,27 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
|
||||
cancel_ev = cancel_events.get(task_id)
|
||||
# Use symlink path if enabled
|
||||
from rgsx_settings import apply_symlink_path
|
||||
from rgsx_settings import apply_symlink_path, get_platform_custom_path
|
||||
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict.get("platform_name") == platform:
|
||||
# Priorité: clé 'folder'; fallback legacy: 'dossier'; sinon normalisation du nom de plateforme
|
||||
platform_folder = platform_dict.get("folder") or platform_dict.get("dossier") or normalize_platform_name(platform)
|
||||
# Vérifier si un dossier personnalisé est configuré pour cette plateforme
|
||||
custom_path = get_platform_custom_path(platform)
|
||||
if custom_path and os.path.isdir(custom_path):
|
||||
dest_dir = custom_path
|
||||
platform_folder = os.path.basename(dest_dir)
|
||||
logger.debug(f"Utilisation du dossier personnalisé pour {platform}: {dest_dir}")
|
||||
else:
|
||||
dest_dir = None
|
||||
platform_folder = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict.get("platform_name") == platform:
|
||||
# Priorité: clé 'folder'; fallback legacy: 'dossier'; sinon normalisation du nom de plateforme
|
||||
platform_folder = platform_dict.get("folder") or platform_dict.get("dossier") or normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}")
|
||||
break
|
||||
if not dest_dir:
|
||||
platform_folder = normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
logger.debug(f"Répertoire de destination trouvé pour {platform}: {dest_dir}")
|
||||
break
|
||||
if not dest_dir:
|
||||
platform_folder = normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
|
||||
# Spécifique: si le système est "BIOS" on force le dossier BIOS
|
||||
if platform_folder == "bios" or platform == "BIOS" or platform == "- BIOS by TMCTV -":
|
||||
@@ -831,6 +995,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.info(f"Le fichier {dest_path} existe déjà et la taille est correcte, téléchargement ignoré")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_ok").format(game_name) + _("download_already_present")
|
||||
|
||||
# Mettre à jour l'historique
|
||||
for entry in config.history:
|
||||
if entry.get("url") == url:
|
||||
entry["status"] = "Download_OK"
|
||||
entry["progress"] = 100
|
||||
entry["message"] = result[1]
|
||||
save_history(config.history)
|
||||
break
|
||||
|
||||
# Afficher un toast au lieu d'ouvrir l'historique
|
||||
try:
|
||||
show_toast(result[1])
|
||||
@@ -839,6 +1013,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
with urls_lock:
|
||||
urls_in_progress.discard(url)
|
||||
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
|
||||
|
||||
# Libérer le slot de la queue
|
||||
try:
|
||||
notify_download_finished()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result[0], result[1]
|
||||
file_found = True
|
||||
|
||||
@@ -881,6 +1062,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.info(f"Un fichier avec le même nom de base existe déjà: {existing_path}, téléchargement ignoré")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_ok").format(game_name) + _("download_already_extracted")
|
||||
|
||||
# Mettre à jour l'historique
|
||||
for entry in config.history:
|
||||
if entry.get("url") == url:
|
||||
entry["status"] = "Download_OK"
|
||||
entry["progress"] = 100
|
||||
entry["message"] = result[1]
|
||||
save_history(config.history)
|
||||
break
|
||||
|
||||
# Afficher un toast au lieu d'ouvrir l'historique
|
||||
try:
|
||||
show_toast(result[1])
|
||||
@@ -889,6 +1080,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
with urls_lock:
|
||||
urls_in_progress.discard(url)
|
||||
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
|
||||
|
||||
# Libérer le slot de la queue
|
||||
try:
|
||||
notify_download_finished()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result[0], result[1]
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de la vérification des fichiers existants: {e}")
|
||||
@@ -896,14 +1094,67 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
download_headers = headers.copy()
|
||||
download_headers['Accept'] = 'application/octet-stream, */*'
|
||||
download_headers['Referer'] = 'https://myrient.erista.me/'
|
||||
archive_cookie = load_archive_org_cookie()
|
||||
archive_alt_urls = []
|
||||
meta_json = None
|
||||
|
||||
# Préparation spécifique archive.org : récupérer quelques pages pour obtenir cookies éventuels
|
||||
# Préparation spécifique archive.org : normaliser URL + récupérer cookies/metadata
|
||||
if 'archive.org/download/' in url:
|
||||
try:
|
||||
pre_id = url.split('/download/')[1].split('/')[0]
|
||||
session.get('https://archive.org/robots.txt', timeout=20)
|
||||
session.get(f'https://archive.org/metadata/{pre_id}', timeout=20)
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
parts = parsed.path.split('/download/', 1)
|
||||
pre_id = None
|
||||
rest_decoded = None
|
||||
if len(parts) == 2:
|
||||
after = parts[1]
|
||||
pre_id = after.split('/', 1)[0]
|
||||
rest = after[len(pre_id):]
|
||||
if rest.startswith('/'):
|
||||
rest = rest[1:]
|
||||
rest_decoded = urllib.parse.unquote(rest)
|
||||
rest_encoded = urllib.parse.quote(rest_decoded, safe='/') if rest_decoded else ''
|
||||
new_path = f"/download/{pre_id}/" + rest_encoded
|
||||
url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, new_path, parsed.query, parsed.fragment))
|
||||
logger.debug(f"URL archive.org normalisée: {url}")
|
||||
if not pre_id:
|
||||
pre_id = url.split('/download/')[1].split('/')[0]
|
||||
|
||||
download_headers['Referer'] = f"https://archive.org/details/{pre_id}"
|
||||
download_headers['Origin'] = 'https://archive.org'
|
||||
if archive_cookie:
|
||||
download_headers['Cookie'] = archive_cookie
|
||||
if archive_cookie:
|
||||
# Apply cookie to session for redirects to ia*.us.archive.org
|
||||
for pair in archive_cookie.split(';'):
|
||||
if '=' in pair:
|
||||
name, value = pair.split('=', 1)
|
||||
session.cookies.set(name.strip(), value.strip(), domain='.archive.org')
|
||||
|
||||
session.get('https://archive.org/robots.txt', timeout=20, headers={'Cookie': archive_cookie} if archive_cookie else None)
|
||||
meta_resp = session.get(f'https://archive.org/metadata/{pre_id}', timeout=20, headers={'Cookie': archive_cookie} if archive_cookie else None)
|
||||
if meta_resp.status_code == 200:
|
||||
try:
|
||||
meta_json = meta_resp.json()
|
||||
except Exception:
|
||||
meta_json = None
|
||||
logger.debug(f"Pré-chargement cookies/metadata archive.org pour {pre_id}")
|
||||
|
||||
# Construire des URLs alternatives pour archive interne
|
||||
identifier, archive_name, inner_path = _split_archive_org_path(url)
|
||||
if identifier and archive_name and inner_path:
|
||||
# Variante sans préfixe archive
|
||||
archive_alt_urls.append(f"https://archive.org/download/{identifier}/" + urllib.parse.quote(inner_path, safe='/'))
|
||||
# Variante filename
|
||||
archive_alt_urls.append(f"https://archive.org/download/{identifier}/{archive_name}?filename=" + urllib.parse.quote(inner_path, safe='/'))
|
||||
# Variante view_archive.php via serveur/dir metadata
|
||||
if meta_json:
|
||||
server = meta_json.get('server')
|
||||
directory = meta_json.get('dir')
|
||||
if server and directory:
|
||||
archive_path = f"{directory}/{archive_name}"
|
||||
view_url = f"https://{server}/view_archive.php?archive=" + urllib.parse.quote(archive_path, safe='/') + "&file=" + urllib.parse.quote(inner_path, safe='/')
|
||||
# Prioriser view_archive.php (cas valide observe dans le navigateur)
|
||||
archive_alt_urls.insert(0, view_url)
|
||||
except Exception as e:
|
||||
logger.debug(f"Pré-chargement archive.org ignoré: {e}")
|
||||
|
||||
@@ -924,19 +1175,22 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
header_variants = [
|
||||
download_headers,
|
||||
{ # Variante sans Referer spécifique
|
||||
'User-Agent': headers['User-Agent'],
|
||||
'User-Agent': headers.get('User-Agent', download_headers.get('User-Agent', 'Mozilla/5.0')),
|
||||
'Accept': 'application/octet-stream,*/*;q=0.8',
|
||||
'Accept-Language': headers['Accept-Language'],
|
||||
'Connection': 'keep-alive'
|
||||
'Accept-Language': headers.get('Accept-Language', 'en-US,en;q=0.5'),
|
||||
'Connection': 'keep-alive',
|
||||
**({'Cookie': archive_cookie} if archive_cookie else {})
|
||||
},
|
||||
{ # Variante minimaliste type curl
|
||||
'User-Agent': 'curl/8.4.0',
|
||||
'Accept': '*/*'
|
||||
'Accept': '*/*',
|
||||
**({'Cookie': archive_cookie} if archive_cookie else {})
|
||||
},
|
||||
{ # Variante avec Referer archive.org
|
||||
'User-Agent': headers['User-Agent'],
|
||||
'User-Agent': headers.get('User-Agent', download_headers.get('User-Agent', 'Mozilla/5.0')),
|
||||
'Accept': '*/*',
|
||||
'Referer': 'https://archive.org/'
|
||||
'Referer': 'https://archive.org/',
|
||||
**({'Cookie': archive_cookie} if archive_cookie else {})
|
||||
}
|
||||
]
|
||||
response = None
|
||||
@@ -954,7 +1208,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
# Mettre à jour le fichier web
|
||||
# Plus besoin de update_web_progress
|
||||
|
||||
logger.debug(f"Tentative téléchargement {attempt}/{len(header_variants)} avec headers: {hv}")
|
||||
logger.debug(f"Tentative téléchargement {attempt}/{len(header_variants)} avec headers: {_redact_headers(hv)}")
|
||||
# Timeout plus long pour archive.org, avec tuple (connect_timeout, read_timeout)
|
||||
timeout_val = (60, 90) if 'archive.org' in url else 30
|
||||
r = session.get(url, stream=True, timeout=timeout_val, allow_redirects=True, headers=hv)
|
||||
@@ -998,13 +1252,36 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
time.sleep(2)
|
||||
|
||||
if response is None:
|
||||
if archive_alt_urls and (last_status in (401, 403) or last_error_type in ("timeout", "connection", "request")):
|
||||
for alt_url in archive_alt_urls:
|
||||
try:
|
||||
timeout_val = (45, 90)
|
||||
logger.debug(f"Tentative archive.org alt URL: {alt_url}")
|
||||
alt_headers = download_headers.copy()
|
||||
try:
|
||||
alt_host = urllib.parse.urlsplit(alt_url).netloc
|
||||
if alt_host.startswith("ia") and alt_host.endswith(".archive.org"):
|
||||
alt_headers["Referer"] = f"https://{alt_host}/"
|
||||
alt_headers["Origin"] = "https://archive.org"
|
||||
except Exception:
|
||||
pass
|
||||
r = session.get(alt_url, stream=True, timeout=timeout_val, allow_redirects=True, headers=alt_headers)
|
||||
if r.status_code not in (401, 403):
|
||||
r.raise_for_status()
|
||||
response = r
|
||||
url = alt_url
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Alt URL archive.org échec: {e}")
|
||||
# Fallback metadata archive.org pour message clair
|
||||
if 'archive.org/download/' in url:
|
||||
try:
|
||||
identifier = url.split('/download/')[1].split('/')[0]
|
||||
meta_resp = session.get(f'https://archive.org/metadata/{identifier}', timeout=30)
|
||||
if meta_resp.status_code == 200:
|
||||
meta_json = meta_resp.json()
|
||||
if meta_json is None:
|
||||
meta_resp = session.get(f'https://archive.org/metadata/{identifier}', timeout=30)
|
||||
if meta_resp.status_code == 200:
|
||||
meta_json = meta_resp.json()
|
||||
if meta_json:
|
||||
if meta_json.get('is_dark'):
|
||||
raise requests.HTTPError(f"Item archive.org restreint (is_dark=true): {identifier}")
|
||||
if not meta_json.get('files'):
|
||||
@@ -1013,7 +1290,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
available = [f.get('name') for f in meta_json.get('files', [])][:10]
|
||||
raise requests.HTTPError(f"Accès refusé (HTTP {last_status}). Fichiers disponibles exemples: {available}")
|
||||
else:
|
||||
raise requests.HTTPError(f"HTTP {last_status} & metadata {meta_resp.status_code} pour {identifier}")
|
||||
raise requests.HTTPError(f"HTTP {last_status} & metadata indisponible pour {identifier}")
|
||||
except requests.HTTPError:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -1075,6 +1352,15 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
download_canceled = False
|
||||
with open(dest_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
# Vérifier la pause (dynamiquement car l'événement peut être créé après le début)
|
||||
while True:
|
||||
pause_ev = pause_events.get(task_id)
|
||||
if pause_ev is None or not pause_ev.is_set():
|
||||
break # Pas en pause, continuer le téléchargement
|
||||
if cancel_ev is not None and cancel_ev.is_set():
|
||||
break # Sortir de la boucle de pause si annulation demandée
|
||||
time.sleep(0.1) # Attendre en pause
|
||||
|
||||
if cancel_ev is not None and cancel_ev.is_set():
|
||||
logger.debug(f"Annulation détectée, arrêt du téléchargement pour task_id={task_id}")
|
||||
result[0] = False
|
||||
@@ -1115,14 +1401,23 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
|
||||
# Si annulé, ne pas continuer avec extraction
|
||||
if download_canceled:
|
||||
# Libérer le slot de la queue
|
||||
try:
|
||||
notify_download_finished()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
os.chmod(dest_path, 0o644)
|
||||
logger.debug(f"Téléchargement terminé: {dest_path}")
|
||||
|
||||
# Vérifier si l'extraction automatique est activée dans les paramètres
|
||||
from rgsx_settings import get_auto_extract
|
||||
auto_extract_enabled = get_auto_extract()
|
||||
|
||||
# Forcer extraction si plateforme BIOS même si le pré-check ne l'avait pas marqué
|
||||
force_extract = is_zip_non_supported
|
||||
if not force_extract:
|
||||
force_extract = is_zip_non_supported and auto_extract_enabled
|
||||
if not force_extract and auto_extract_enabled:
|
||||
try:
|
||||
bios_like = {"BIOS", "- BIOS by TMCTV -", "- BIOS"}
|
||||
if platform_folder == "bios" or platform in bios_like:
|
||||
@@ -1184,6 +1479,21 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
|
||||
elif extension == ".7z":
|
||||
try:
|
||||
success, msg = extract_7z(dest_path, dest_dir, url)
|
||||
if success:
|
||||
logger.debug(f"Extraction 7z réussie: {msg}")
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction 7z: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction 7z: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction 7z {game_name}: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Type d'archive non supporté: {extension}")
|
||||
result[0] = True
|
||||
@@ -1234,7 +1544,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
@@ -1306,7 +1616,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
logger.debug(f"[DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
@@ -1336,6 +1646,12 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
if url in url_done_events:
|
||||
url_done_events[url].set()
|
||||
|
||||
# Libérer le slot de la queue
|
||||
try:
|
||||
notify_download_finished()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result[0], result[1]
|
||||
|
||||
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):
|
||||
@@ -1343,17 +1659,21 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
keys_info = load_api_keys()
|
||||
config.API_KEY_1FICHIER = keys_info.get('1fichier', '')
|
||||
config.API_KEY_ALLDEBRID = keys_info.get('alldebrid', '')
|
||||
config.API_KEY_DEBRIDLINK = keys_info.get('debridlink', '')
|
||||
config.API_KEY_REALDEBRID = keys_info.get('realdebrid', '')
|
||||
if not config.API_KEY_1FICHIER and config.API_KEY_ALLDEBRID:
|
||||
logger.debug("Clé 1fichier absente, utilisation fallback AllDebrid")
|
||||
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and config.API_KEY_REALDEBRID:
|
||||
logger.debug("Clé 1fichier & AllDebrid absentes, utilisation fallback RealDebrid")
|
||||
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_REALDEBRID:
|
||||
logger.debug("Aucune clé API disponible (1fichier, AllDebrid, RealDebrid)")
|
||||
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and config.API_KEY_DEBRIDLINK:
|
||||
logger.debug("Clé 1fichier & AllDebrid absentes, utilisation fallback Debrid-Link")
|
||||
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_DEBRIDLINK and config.API_KEY_REALDEBRID:
|
||||
logger.debug("Clé 1fichier, AllDebrid & Debrid-Link absentes, utilisation fallback RealDebrid")
|
||||
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_DEBRIDLINK and not config.API_KEY_REALDEBRID:
|
||||
logger.debug("Aucune clé API disponible (1fichier, AllDebrid, Debrid-Link, RealDebrid)")
|
||||
logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
|
||||
logger.debug(
|
||||
f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'} / "
|
||||
f"AllDebrid: {'présente' if config.API_KEY_ALLDEBRID else 'absente'} / "
|
||||
f"Debrid-Link: {'présente' if config.API_KEY_DEBRIDLINK else 'absente'} / "
|
||||
f"RealDebrid: {'présente' if config.API_KEY_REALDEBRID else 'absente'} (reloaded={keys_info.get('reloaded')})"
|
||||
)
|
||||
result = [None, None]
|
||||
@@ -1399,7 +1719,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
if task_id not in cancel_events:
|
||||
cancel_events[task_id] = threading.Event()
|
||||
|
||||
provider_used = None # '1F', 'AD', 'RD'
|
||||
provider_used = None # '1F', 'AD', 'DL', 'RD'
|
||||
|
||||
def _set_provider_in_history(pfx: str):
|
||||
try:
|
||||
@@ -1470,18 +1790,26 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
save_history(config.history)
|
||||
|
||||
# Use symlink path if enabled
|
||||
from rgsx_settings import apply_symlink_path
|
||||
from rgsx_settings import apply_symlink_path, get_platform_custom_path
|
||||
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict.get("platform_name") == platform:
|
||||
platform_folder = platform_dict.get("folder") or platform_dict.get("dossier") or normalize_platform_name(platform)
|
||||
# Vérifier si un dossier personnalisé est configuré pour cette plateforme
|
||||
custom_path = get_platform_custom_path(platform)
|
||||
if custom_path and os.path.isdir(custom_path):
|
||||
dest_dir = custom_path
|
||||
logger.debug(f"Utilisation du dossier personnalisé pour {platform}: {dest_dir}")
|
||||
platform_folder = os.path.basename(dest_dir)
|
||||
else:
|
||||
dest_dir = None
|
||||
platform_folder = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict.get("platform_name") == platform:
|
||||
platform_folder = platform_dict.get("folder") or platform_dict.get("dossier") or normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
break
|
||||
if not dest_dir:
|
||||
logger.warning(f"Aucun dossier 'folder'/'dossier' trouvé pour la plateforme {platform}")
|
||||
platform_folder = normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
break
|
||||
if not dest_dir:
|
||||
logger.warning(f"Aucun dossier 'folder'/'dossier' trouvé pour la plateforme {platform}")
|
||||
platform_folder = normalize_platform_name(platform)
|
||||
dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder)
|
||||
logger.debug(f"Répertoire destination déterminé: {dest_dir}")
|
||||
|
||||
# Spécifique: si le système est "- BIOS by TMCTV -" on force le dossier BIOS
|
||||
@@ -1750,6 +2078,92 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.warning(f"AllDebrid status != success: {ad_json}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur AllDebrid fallback: {e}")
|
||||
# Tentative Debrid-Link si pas de final_url
|
||||
if not final_url and getattr(config, 'API_KEY_DEBRIDLINK', ''):
|
||||
logger.debug("Tentative fallback Debrid-Link (downloader/add)")
|
||||
try:
|
||||
dl_key = config.API_KEY_DEBRIDLINK
|
||||
headers_dl = {
|
||||
"Authorization": f"Bearer {dl_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload_dl = {"url": link}
|
||||
dl_resp = requests.post(
|
||||
"https://debrid-link.com/api/v2/downloader/add",
|
||||
json=payload_dl,
|
||||
headers=headers_dl,
|
||||
timeout=30
|
||||
)
|
||||
dl_status = dl_resp.status_code
|
||||
raw_text_dl = None
|
||||
dl_json = None
|
||||
try:
|
||||
raw_text_dl = dl_resp.text
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
dl_json = dl_resp.json()
|
||||
except Exception:
|
||||
dl_json = None
|
||||
logger.debug(f"Réponse Debrid-Link code={dl_status} body_snippet={(raw_text_dl[:120] + '...') if raw_text_dl and len(raw_text_dl) > 120 else raw_text_dl}")
|
||||
|
||||
DEBRIDLINK_ERROR_MAP = {
|
||||
"badToken": "DL: Invalid API key",
|
||||
"notDebrid": "DL: Host unavailable",
|
||||
"hostNotValid": "DL: Unsupported host",
|
||||
"fileNotFound": "DL: File not found",
|
||||
"fileNotAvailable": "DL: File temporarily unavailable",
|
||||
"badFileUrl": "DL: Invalid link",
|
||||
"badFilePassword": "DL: Invalid file password",
|
||||
"notFreeHost": "DL: Premium account only",
|
||||
"maintenanceHost": "DL: Host in maintenance",
|
||||
"noServerHost": "DL: No server available",
|
||||
"maxLink": "DL: Daily link limit reached",
|
||||
"maxLinkHost": "DL: Daily host limit reached",
|
||||
"maxData": "DL: Daily data limit reached",
|
||||
"maxDataHost": "DL: Daily host data limit reached",
|
||||
"disabledServerHost": "DL: Server or VPN not allowed",
|
||||
"floodDetected": "DL: Rate limit reached",
|
||||
}
|
||||
|
||||
error_message = None
|
||||
error_message_raw = None
|
||||
if dl_json and isinstance(dl_json, dict):
|
||||
if dl_json.get('success') is True:
|
||||
value = dl_json.get('value') or {}
|
||||
if isinstance(value, dict):
|
||||
final_url = value.get('downloadUrl') or value.get('downloadURL') or value.get('link') or value.get('url')
|
||||
filename = value.get('name') or value.get('filename') or filename or game_name
|
||||
else:
|
||||
error_code = dl_json.get('error')
|
||||
if error_code:
|
||||
error_message = DEBRIDLINK_ERROR_MAP.get(error_code, f"DL: {error_code}")
|
||||
error_message_raw = str(error_code)
|
||||
if dl_status in (200, 201) and final_url:
|
||||
logger.debug("Débridage réussi via Debrid-Link")
|
||||
provider_used = 'DL'
|
||||
_set_provider_in_history(provider_used)
|
||||
elif not final_url:
|
||||
if not error_message:
|
||||
if dl_status == 401:
|
||||
error_message = "DL: Unauthorized (401)"
|
||||
elif dl_status == 429:
|
||||
error_message = "DL: Rate limited (429)"
|
||||
elif dl_status >= 500:
|
||||
error_message = f"DL: Server error ({dl_status})"
|
||||
else:
|
||||
error_message = f"DL: Unexpected status ({dl_status})"
|
||||
error_message_raw = raw_text_dl or error_message
|
||||
logger.warning(f"Debrid-Link fallback échec: {error_message}")
|
||||
result[0] = False
|
||||
result[1] = error_message
|
||||
try:
|
||||
if isinstance(result, list):
|
||||
result.append({"raw_error_debridlink": error_message_raw})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Exception Debrid-Link fallback: {e}")
|
||||
# Tentative RealDebrid si pas de final_url
|
||||
if not final_url and getattr(config, 'API_KEY_REALDEBRID', ''):
|
||||
logger.debug("Tentative fallback RealDebrid (unlock)")
|
||||
@@ -1976,9 +2390,9 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
content_length = head_response.headers.get('content-length')
|
||||
if content_length:
|
||||
remote_size = int(content_length)
|
||||
logger.debug(f"Taille du fichier serveur (AllDebrid/RealDebrid): {remote_size} octets")
|
||||
logger.debug(f"Taille du fichier serveur (AllDebrid/Debrid-Link/RealDebrid): {remote_size} octets")
|
||||
except Exception as e:
|
||||
logger.debug(f"Impossible de vérifier la taille serveur (AllDebrid/RealDebrid): {e}")
|
||||
logger.debug(f"Impossible de vérifier la taille serveur (AllDebrid/Debrid-Link/RealDebrid): {e}")
|
||||
|
||||
# Vérifier si le fichier existe déjà (exact ou avec autre extension)
|
||||
file_found = False
|
||||
@@ -2098,6 +2512,15 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.debug(f"Ouverture fichier: {dest_path}")
|
||||
with open(dest_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
# Vérifier la pause (dynamiquement car l'événement peut être créé après le début)
|
||||
while True:
|
||||
pause_ev = pause_events.get(task_id)
|
||||
if pause_ev is None or not pause_ev.is_set():
|
||||
break # Pas en pause, continuer le téléchargement
|
||||
if cancel_ev is not None and cancel_ev.is_set():
|
||||
break # Sortir de la boucle de pause si annulation demandée
|
||||
time.sleep(0.1) # Attendre en pause
|
||||
|
||||
if cancel_ev is not None and cancel_ev.is_set():
|
||||
logger.debug(f"Annulation détectée, arrêt du téléchargement 1fichier pour task_id={task_id}")
|
||||
result[0] = False
|
||||
@@ -2141,9 +2564,13 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
if download_canceled:
|
||||
return
|
||||
|
||||
# Vérifier si l'extraction automatique est activée dans les paramètres
|
||||
from rgsx_settings import get_auto_extract
|
||||
auto_extract_enabled = get_auto_extract()
|
||||
|
||||
# Déterminer si extraction est nécessaire
|
||||
force_extract = is_zip_non_supported
|
||||
if not force_extract:
|
||||
force_extract = is_zip_non_supported and auto_extract_enabled
|
||||
if not force_extract and auto_extract_enabled:
|
||||
try:
|
||||
ps3_platforms = {"ps3", "PlayStation 3"}
|
||||
if platform_folder == "ps3" or platform in ps3_platforms:
|
||||
@@ -2193,6 +2620,21 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.error(f"Exception lors de l'extraction RAR: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction RAR {game_name}: {str(e)}"
|
||||
elif extension == ".7z":
|
||||
try:
|
||||
success, msg = extract_7z(dest_path, dest_dir, url)
|
||||
logger.debug(f"Extraction 7z terminée: {msg}")
|
||||
if success:
|
||||
result[0] = True
|
||||
result[1] = _("network_download_extract_ok").format(game_name)
|
||||
else:
|
||||
logger.error(f"Erreur extraction 7z: {msg}")
|
||||
result[0] = False
|
||||
result[1] = _("network_extraction_failed").format(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception lors de l'extraction 7z: {str(e)}")
|
||||
result[0] = False
|
||||
result[1] = f"Erreur extraction 7z {game_name}: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Type d'archive non supporté: {extension}")
|
||||
result[0] = True
|
||||
@@ -2248,7 +2690,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
success, message = data[1], data[2]
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
@@ -2303,7 +2745,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.debug(f"[1F_DRAIN_QUEUE] Processing final message: success={success}, message={message[:100] if message else 'None'}")
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
if "url" in entry and entry["url"] == url and entry["status"] in ["Downloading", "Téléchargement", "Extracting", "Converting"]:
|
||||
entry["status"] = "Download_OK" if success else "Erreur"
|
||||
entry["progress"] = 100 if success else 0
|
||||
entry["message"] = message
|
||||
|
||||
@@ -17,7 +17,7 @@ import re
|
||||
import config # paths, settings, SAVE_FOLDER, etc.
|
||||
from utils import load_api_keys as _prime_api_keys # ensure API key files are created
|
||||
import network as network_mod # for progress_queues access
|
||||
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename, extract_zip_data
|
||||
from utils import load_sources, load_games, is_extension_supported, load_extensions_json, sanitize_filename
|
||||
from history import load_history, save_history, add_to_history
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url
|
||||
from rgsx_settings import get_sources_zip_url
|
||||
@@ -277,7 +277,7 @@ def cmd_games(args):
|
||||
suggestions = [] # (priority, score, game_obj)
|
||||
# 1) Substring match (full or sans extension) priority 0, score = position
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
t_lower = title.lower()
|
||||
@@ -303,7 +303,7 @@ def cmd_games(args):
|
||||
# 2) Ordered non-contiguous tokens (priority 1)
|
||||
if q_tokens:
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
tt = _tokens(title)
|
||||
@@ -313,7 +313,7 @@ def cmd_games(args):
|
||||
# 3) All tokens present, any order (priority 2), score = token set size
|
||||
if q_tokens:
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
title = g.name
|
||||
if not title:
|
||||
continue
|
||||
t_tokens = set(_tokens(title))
|
||||
@@ -322,12 +322,12 @@ def cmd_games(args):
|
||||
# Deduplicate by title keeping best (lowest priority, then score)
|
||||
best = {}
|
||||
for prio, score, g in suggestions:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
|
||||
title = g.name
|
||||
key = title.lower()
|
||||
cur = best.get(key)
|
||||
if cur is None or (prio, score) < (cur[0], cur[1]):
|
||||
best[key] = (prio, score, g)
|
||||
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower()))
|
||||
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2].name if isinstance(x[2], (list, config.Game)) and x[2] else str(x[2])).lower()))
|
||||
games = [g for _, _, g in ranked]
|
||||
# Table: Name (60) | Size (12) to allow "xxxx.xx MiB"
|
||||
NAME_W = 60
|
||||
@@ -344,7 +344,7 @@ def cmd_games(args):
|
||||
print(header)
|
||||
print(border)
|
||||
for g in games:
|
||||
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
|
||||
title = g.name
|
||||
size_val = ''
|
||||
if isinstance(g, (list, tuple)) and len(g) >= 3:
|
||||
size_val = display_size(g[2])
|
||||
@@ -447,11 +447,11 @@ def cmd_download(args):
|
||||
def _tokens(s: str) -> list[str]:
|
||||
return re.findall(r"[a-z0-9]+", s.lower())
|
||||
|
||||
def _game_title(g) -> str | None:
|
||||
return g[0] if isinstance(g, (list, tuple)) and g else None
|
||||
def _game_title(g: config.Game) -> str | None:
|
||||
return g.name
|
||||
|
||||
def _game_url(g) -> str | None:
|
||||
return g[1] if isinstance(g, (list, tuple)) and len(g) > 1 else None
|
||||
def _game_url(g: config.Game) -> str | None:
|
||||
return g.url
|
||||
|
||||
# 1) Exact match (case-insensitive), with and without extension
|
||||
match = None
|
||||
@@ -561,8 +561,8 @@ def cmd_download(args):
|
||||
size_val = ''
|
||||
size_raw = None
|
||||
for g in games:
|
||||
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
|
||||
size_raw = g[2]
|
||||
if g.name == title:
|
||||
size_raw = g.size
|
||||
break
|
||||
if size_raw is not None:
|
||||
size_val = display_size(size_raw)
|
||||
|
||||
@@ -29,7 +29,7 @@ def delete_old_files():
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Ancien fichier supprimé : {file_path}")
|
||||
print(f"Ancien fichier supprime : {file_path}")
|
||||
logger.info(f"Ancien fichier supprimé : {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
|
||||
@@ -39,7 +39,7 @@ def delete_old_files():
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Ancien fichier supprimé : {file_path}")
|
||||
print(f"Ancien fichier supprime : {file_path}")
|
||||
logger.info(f"Ancien fichier supprimé : {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
|
||||
@@ -49,6 +49,8 @@ def load_rgsx_settings():
|
||||
"""Charge tous les paramètres depuis rgsx_settings.json."""
|
||||
from config import RGSX_SETTINGS_PATH
|
||||
|
||||
#logger.debug(f"Chargement des settings depuis: {RGSX_SETTINGS_PATH}")
|
||||
|
||||
default_settings = {
|
||||
"language": "en",
|
||||
"music_enabled": True,
|
||||
@@ -58,7 +60,10 @@ def load_rgsx_settings():
|
||||
},
|
||||
"display": {
|
||||
"grid": "3x4",
|
||||
"font_family": "pixel"
|
||||
"font_family": "pixel",
|
||||
"monitor": 0,
|
||||
"fullscreen": True,
|
||||
"light_mode": False
|
||||
},
|
||||
"symlink": {
|
||||
"enabled": False,
|
||||
@@ -70,21 +75,28 @@ def load_rgsx_settings():
|
||||
},
|
||||
"show_unsupported_platforms": False,
|
||||
"allow_unknown_extensions": False,
|
||||
"nintendo_layout": False,
|
||||
"roms_folder": "",
|
||||
"web_service_at_boot": False
|
||||
"web_service_at_boot": False,
|
||||
"last_gamelist_update": None,
|
||||
"platform_custom_paths": {} # Chemins personnalisés par plateforme
|
||||
}
|
||||
|
||||
try:
|
||||
if os.path.exists(RGSX_SETTINGS_PATH):
|
||||
with open(RGSX_SETTINGS_PATH, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
#logger.debug(f"Settings JSON chargé: display={settings.get('display', {})}")
|
||||
# Fusionner avec les valeurs par défaut pour assurer la compatibilité
|
||||
for key, value in default_settings.items():
|
||||
if key not in settings:
|
||||
settings[key] = value
|
||||
return settings
|
||||
else:
|
||||
logger.warning(f"Fichier settings non trouvé: {RGSX_SETTINGS_PATH}")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement de rgsx_settings.json: {str(e)}")
|
||||
logger.error(f"Erreur chargement settings: {e}")
|
||||
|
||||
return default_settings
|
||||
|
||||
@@ -101,6 +113,27 @@ def save_rgsx_settings(settings):
|
||||
print(f"Erreur lors de la sauvegarde de rgsx_settings.json: {str(e)}")
|
||||
|
||||
|
||||
def get_last_gamelist_update(settings=None):
|
||||
"""Récupère la date de dernière mise à jour de la liste des jeux."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("last_gamelist_update", None)
|
||||
|
||||
|
||||
def set_last_gamelist_update(date_string=None):
|
||||
"""Définit la date de dernière mise à jour de la liste des jeux.
|
||||
Si date_string est None, utilise la date actuelle.
|
||||
"""
|
||||
from datetime import datetime
|
||||
settings = load_rgsx_settings()
|
||||
if date_string is None:
|
||||
date_string = datetime.now().strftime("%Y-%m-%d")
|
||||
settings["last_gamelist_update"] = date_string
|
||||
save_rgsx_settings(settings)
|
||||
logger.info(f"Date de dernière mise à jour de la liste des jeux: {date_string}")
|
||||
return date_string
|
||||
|
||||
|
||||
|
||||
def load_symlink_settings():
|
||||
"""Load symlink settings from rgsx_settings.json."""
|
||||
@@ -266,6 +299,22 @@ def set_allow_unknown_extensions(enabled: bool) -> bool:
|
||||
save_rgsx_settings(settings)
|
||||
return settings["allow_unknown_extensions"]
|
||||
|
||||
|
||||
# ----------------------- Invert ABXY layout ----------------------- #
|
||||
def get_nintendo_layout(settings=None) -> bool:
|
||||
"""Retourne True si l'inversion ABXY (icônes) est activée."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return bool(settings.get("nintendo_layout", False))
|
||||
|
||||
|
||||
def set_nintendo_layout(enabled: bool) -> bool:
|
||||
"""Active/désactive l'inversion ABXY (icônes) et sauvegarde."""
|
||||
settings = load_rgsx_settings()
|
||||
settings["nintendo_layout"] = bool(enabled)
|
||||
save_rgsx_settings(settings)
|
||||
return settings["nintendo_layout"]
|
||||
|
||||
# ----------------------- Hide premium systems toggle ----------------------- #
|
||||
|
||||
def get_hide_premium_systems(settings=None) -> bool:
|
||||
@@ -307,6 +356,92 @@ def set_display_grid(cols: int, rows: int):
|
||||
save_rgsx_settings(settings)
|
||||
return cols, rows
|
||||
|
||||
# ----------------------- Monitor/Display settings ----------------------- #
|
||||
|
||||
def get_display_monitor(settings=None):
|
||||
"""Retourne l'index du moniteur configuré (par défaut 0 = principal)."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("display", {}).get("monitor", 0)
|
||||
|
||||
def set_display_monitor(monitor_index: int):
|
||||
"""Définit et sauvegarde l'index du moniteur à utiliser."""
|
||||
settings = load_rgsx_settings()
|
||||
disp = settings.setdefault("display", {})
|
||||
disp["monitor"] = max(0, int(monitor_index))
|
||||
save_rgsx_settings(settings)
|
||||
return disp["monitor"]
|
||||
|
||||
def get_display_fullscreen(settings=None):
|
||||
"""Retourne True si le mode plein écran est activé."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("display", {}).get("fullscreen", True)
|
||||
|
||||
def set_display_fullscreen(fullscreen: bool):
|
||||
"""Définit et sauvegarde le mode plein écran."""
|
||||
settings = load_rgsx_settings()
|
||||
disp = settings.setdefault("display", {})
|
||||
disp["fullscreen"] = bool(fullscreen)
|
||||
save_rgsx_settings(settings)
|
||||
return disp["fullscreen"]
|
||||
|
||||
def get_light_mode(settings=None):
|
||||
"""Retourne True si le mode léger (performance) est activé."""
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("display", {}).get("light_mode", False)
|
||||
|
||||
def set_light_mode(enabled: bool):
|
||||
"""Définit et sauvegarde le mode léger (performance)."""
|
||||
settings = load_rgsx_settings()
|
||||
disp = settings.setdefault("display", {})
|
||||
disp["light_mode"] = bool(enabled)
|
||||
save_rgsx_settings(settings)
|
||||
return disp["light_mode"]
|
||||
|
||||
def get_available_monitors():
|
||||
"""Retourne la liste des moniteurs disponibles avec leurs informations.
|
||||
Compatible Windows, Linux (Batocera), et autres plateformes.
|
||||
Retourne une liste de dicts: [{"index": 0, "name": "Monitor 1", "resolution": "1920x1080"}, ...]
|
||||
"""
|
||||
monitors = []
|
||||
try:
|
||||
import pygame
|
||||
if not pygame.display.get_init():
|
||||
pygame.display.init()
|
||||
|
||||
num_displays = pygame.display.get_num_displays()
|
||||
for i in range(num_displays):
|
||||
try:
|
||||
# Essayer d'obtenir le mode desktop pour ce display
|
||||
mode = pygame.display.get_desktop_sizes()[i] if hasattr(pygame.display, 'get_desktop_sizes') else None
|
||||
if mode:
|
||||
width, height = mode
|
||||
else:
|
||||
# Fallback: utiliser la résolution actuelle si disponible
|
||||
info = pygame.display.Info()
|
||||
width, height = info.current_w, info.current_h
|
||||
|
||||
monitors.append({
|
||||
"index": i,
|
||||
"name": f"Monitor {i + 1}",
|
||||
"resolution": f"{width}x{height}"
|
||||
})
|
||||
except Exception as e:
|
||||
# Si on ne peut pas obtenir les infos, ajouter quand même le moniteur
|
||||
monitors.append({
|
||||
"index": i,
|
||||
"name": f"Monitor {i + 1}",
|
||||
"resolution": "Unknown"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting monitors: {e}")
|
||||
# Fallback: au moins un moniteur
|
||||
monitors = [{"index": 0, "name": "Monitor 1 (Default)", "resolution": "Auto"}]
|
||||
|
||||
return monitors if monitors else [{"index": 0, "name": "Monitor 1 (Default)", "resolution": "Auto"}]
|
||||
|
||||
def get_font_family(settings=None):
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
@@ -339,3 +474,89 @@ def get_language(settings=None):
|
||||
if settings is None:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("language", "en")
|
||||
|
||||
|
||||
def load_game_filters():
|
||||
"""Charge les filtres de jeux depuis rgsx_settings.json."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("game_filters", {})
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading game filters: {str(e)}")
|
||||
return {}
|
||||
|
||||
|
||||
def save_game_filters(filters_dict):
|
||||
"""Sauvegarde les filtres de jeux dans rgsx_settings.json."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
settings["game_filters"] = filters_dict
|
||||
save_rgsx_settings(settings)
|
||||
logger.debug(f"Game filters saved: {filters_dict}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving game filters: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def get_platform_custom_path(platform_name):
|
||||
"""Récupère le chemin personnalisé pour une plateforme."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
custom_paths = settings.get("platform_custom_paths", {})
|
||||
return custom_paths.get(platform_name, "")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting platform custom path: {str(e)}")
|
||||
return ""
|
||||
|
||||
|
||||
def set_platform_custom_path(platform_name, path):
|
||||
"""Définit le chemin personnalisé pour une plateforme."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
if "platform_custom_paths" not in settings:
|
||||
settings["platform_custom_paths"] = {}
|
||||
if path:
|
||||
settings["platform_custom_paths"][platform_name] = path
|
||||
else:
|
||||
# Si le chemin est vide, supprimer l'entrée
|
||||
settings["platform_custom_paths"].pop(platform_name, None)
|
||||
save_rgsx_settings(settings)
|
||||
logger.info(f"Platform custom path set: {platform_name} -> {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting platform custom path: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def get_all_platform_custom_paths():
|
||||
"""Récupère tous les chemins personnalisés des plateformes."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("platform_custom_paths", {})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all platform custom paths: {str(e)}")
|
||||
return {}
|
||||
|
||||
|
||||
def get_auto_extract():
|
||||
"""Récupère le paramètre d'extraction automatique des archives après téléchargement."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
return settings.get("auto_extract", True) # Activé par défaut
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting auto_extract setting: {str(e)}")
|
||||
return True
|
||||
|
||||
|
||||
def set_auto_extract(enabled: bool):
|
||||
"""Définit le paramètre d'extraction automatique des archives après téléchargement."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
settings["auto_extract"] = enabled
|
||||
save_rgsx_settings(settings)
|
||||
logger.info(f"Auto extract set to: {enabled}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting auto_extract: {str(e)}")
|
||||
return False
|
||||
|
||||
@@ -25,6 +25,7 @@ from utils import load_sources, load_games, extract_data
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
from config import Game
|
||||
|
||||
try:
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
@@ -161,7 +162,7 @@ def get_cached_sources() -> tuple[list[dict], str, datetime]:
|
||||
return copy.deepcopy(platforms), etag, last_modified
|
||||
|
||||
|
||||
def get_cached_games(platform: str) -> tuple[list[tuple], str, datetime]:
|
||||
def get_cached_games(platform: str) -> tuple[list[Game], str, datetime]:
|
||||
"""Return cached games list for platform with metadata."""
|
||||
now = time.time()
|
||||
with cache_lock:
|
||||
@@ -246,11 +247,11 @@ def get_translation(key, default=None):
|
||||
return key
|
||||
|
||||
# Fonction pour normaliser les tailles de fichier
|
||||
def normalize_size(size_str):
|
||||
def normalize_size(size_str, lang='en'):
|
||||
"""
|
||||
Normalise une taille de fichier dans différents formats (Ko, KiB, Mo, MiB, Go, GiB)
|
||||
en un format uniforme (Mo ou Go).
|
||||
Exemples: "150 Mo" -> "150 Mo", "1.5 Go" -> "1.5 Go", "500 Ko" -> "0.5 Mo", "2 GiB" -> "2.15 Go"
|
||||
en un format uniforme selon la langue (MB/GB pour anglais, Mo/Go pour français).
|
||||
Exemples: "150 Mo" -> "150 MB" (en), "1.5 Go" -> "1.5 GB" (en), "500 Ko" -> "0.5 MB"
|
||||
"""
|
||||
if not size_str:
|
||||
return None
|
||||
@@ -282,16 +283,24 @@ def normalize_size(size_str):
|
||||
elif unit in ['gio', 'gib']:
|
||||
value = value * 1024 # GiB en Mo
|
||||
|
||||
# Afficher en Go si > 1024 Mo, sinon en Mo
|
||||
if value >= 1024:
|
||||
return f"{value / 1024:.2f} Go".rstrip('0').rstrip('.')
|
||||
# Déterminer les unités selon la langue
|
||||
if lang == 'fr':
|
||||
mb_unit = 'Mo'
|
||||
gb_unit = 'Go'
|
||||
else:
|
||||
# Arrondir à 1 décimale pour Mo
|
||||
mb_unit = 'MB'
|
||||
gb_unit = 'GB'
|
||||
|
||||
# Afficher en GB/Go si > 1024 Mo, sinon en MB/Mo
|
||||
if value >= 1024:
|
||||
return f"{value / 1024:.2f} {gb_unit}".replace('.00 ', ' ').rstrip('0').rstrip('.')
|
||||
else:
|
||||
# Arrondir à 1 décimale pour MB/Mo
|
||||
rounded = round(value, 1)
|
||||
if rounded == int(rounded):
|
||||
return f"{int(rounded)} Mo"
|
||||
return f"{int(rounded)} {mb_unit}"
|
||||
else:
|
||||
return f"{rounded} Mo".rstrip('0').rstrip('.')
|
||||
return f"{rounded} {mb_unit}".rstrip('0').rstrip('.')
|
||||
except (ValueError, TypeError):
|
||||
return size_str # Retourner original si conversion échoue
|
||||
|
||||
@@ -356,7 +365,7 @@ try:
|
||||
logger.info("Test d'écriture dans le fichier de log réussi")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du test d'écriture : {e}")
|
||||
print(f"ERREUR: Impossible d'écrire dans {config.log_file_web}: {e}", file=sys.stderr)
|
||||
print(f"ERREUR: Impossible d'ecrire dans {config.log_file_web}: {e}", file=sys.stderr)
|
||||
|
||||
# Initialiser les données au démarrage
|
||||
logger.info("Chargement initial des données...")
|
||||
@@ -472,6 +481,20 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
"""Répond avec un 404 générique."""
|
||||
self._set_headers('text/plain; charset=utf-8', status=404)
|
||||
self.wfile.write(b'Not found')
|
||||
|
||||
def _get_language_from_cookies(self):
|
||||
"""Récupère la langue depuis les cookies ou retourne 'en' par défaut"""
|
||||
cookie_header = self.headers.get('Cookie', '')
|
||||
if cookie_header:
|
||||
# Parser les cookies
|
||||
cookies = {}
|
||||
for cookie in cookie_header.split(';'):
|
||||
cookie = cookie.strip()
|
||||
if '=' in cookie:
|
||||
key, value = cookie.split('=', 1)
|
||||
cookies[key] = value
|
||||
return cookies.get('language', 'en')
|
||||
return 'en'
|
||||
|
||||
def _asset_version(self, relative_path: str) -> str:
|
||||
"""Retourne un identifiant de version basé sur la date de modification du fichier statique."""
|
||||
@@ -674,14 +697,14 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
elif games_last_modified:
|
||||
latest_modified = games_last_modified
|
||||
for game in games:
|
||||
game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
|
||||
game_name = game.name
|
||||
game_name_lower = game_name.lower()
|
||||
if all(word in game_name_lower for word in search_words):
|
||||
matching_games.append({
|
||||
'game_name': game_name,
|
||||
'platform': platform_name,
|
||||
'url': game[1] if len(game) > 1 and isinstance(game, (list, tuple)) else None,
|
||||
'size': normalize_size(game[2] if len(game) > 2 and isinstance(game, (list, tuple)) else None)
|
||||
'url': game.url,
|
||||
'size': normalize_size(game.size, self._get_language_from_cookies())
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de la recherche dans {platform_name}: {e}")
|
||||
@@ -722,12 +745,15 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
platform_name = path.split('/api/games/')[-1]
|
||||
platform_name = urllib.parse.unquote(platform_name)
|
||||
|
||||
# Récupérer la langue depuis les cookies ou utiliser 'en' par défaut
|
||||
lang = self._get_language_from_cookies()
|
||||
|
||||
games, _, games_last_modified = get_cached_games(platform_name)
|
||||
games_formatted = [
|
||||
{
|
||||
'name': g[0],
|
||||
'url': g[1] if len(g) > 1 else None,
|
||||
'size': normalize_size(g[2] if len(g) > 2 else None)
|
||||
'name': g.name,
|
||||
'url': g.url,
|
||||
'size': normalize_size(g.size, lang)
|
||||
}
|
||||
for g in games
|
||||
]
|
||||
@@ -747,7 +773,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
# Lire depuis history.json - filtrer seulement les téléchargements en cours
|
||||
history = load_history() or []
|
||||
|
||||
print(f"\n[DEBUG PROGRESS] history.json chargé avec {len(history)} entrées totales")
|
||||
print(f"\n[DEBUG PROGRESS] history.json charge avec {len(history)} entrees totales")
|
||||
|
||||
# Filtrer les entrées avec status "Downloading", "Téléchargement", "Connecting", "Try X/Y"
|
||||
in_progress_statuses = ["Downloading", "Téléchargement", "Downloading", "Connecting", "Extracting"]
|
||||
@@ -772,9 +798,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
# Debug: afficher les premiers status qui ne matchent pas
|
||||
if len(downloads) < 3:
|
||||
print(f" [DEBUG] Ignoré - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
|
||||
print(f" [DEBUG] Ignore - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
|
||||
|
||||
print(f"[DEBUG PROGRESS] {len(downloads)} téléchargements en cours trouvés")
|
||||
print(f"[DEBUG PROGRESS] {len(downloads)} telechargements en cours trouves")
|
||||
if downloads:
|
||||
for url, data in list(downloads.items())[:2]:
|
||||
print(f" - URL: {url[:80]}...")
|
||||
@@ -840,9 +866,27 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
# Route: API - Settings (lecture)
|
||||
elif path == '/api/settings':
|
||||
try:
|
||||
from rgsx_settings import load_rgsx_settings
|
||||
from rgsx_settings import load_rgsx_settings, get_auto_extract
|
||||
from utils import check_web_service_status, check_custom_dns_status, load_api_keys
|
||||
settings = load_rgsx_settings()
|
||||
|
||||
# Ajouter les options dynamiques
|
||||
settings['auto_extract'] = get_auto_extract()
|
||||
|
||||
# Options Linux/Batocera
|
||||
if config.OPERATING_SYSTEM == "Linux":
|
||||
settings['web_service_at_boot'] = check_web_service_status()
|
||||
settings['custom_dns_at_boot'] = check_custom_dns_status()
|
||||
|
||||
# API Keys (filtrer la clé 'reloaded' qui n'est pas utile pour l'UI)
|
||||
api_keys_data = load_api_keys()
|
||||
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', '')
|
||||
}
|
||||
|
||||
self._send_json({
|
||||
'success': True,
|
||||
'settings': settings,
|
||||
@@ -1092,7 +1136,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
if game_name_param and game_index is None:
|
||||
game_index = None
|
||||
for idx, game in enumerate(games):
|
||||
current_game_name = game[0] if isinstance(game, (list, tuple)) else str(game)
|
||||
current_game_name = game.name
|
||||
if current_game_name == game_name_param:
|
||||
game_index = idx
|
||||
break
|
||||
@@ -1113,8 +1157,8 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
game = games[game_index]
|
||||
game_name = game[0]
|
||||
game_url = game[1] if len(game) > 1 else None
|
||||
game_name = game.name
|
||||
game_url = game.url
|
||||
|
||||
if not game_url:
|
||||
self._send_json({
|
||||
@@ -1445,7 +1489,8 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
# Route: Sauvegarder les settings
|
||||
elif path == '/api/settings':
|
||||
try:
|
||||
from rgsx_settings import save_rgsx_settings
|
||||
from rgsx_settings import save_rgsx_settings, set_auto_extract
|
||||
from utils import toggle_web_service_at_boot, toggle_custom_dns_at_boot, save_api_keys
|
||||
|
||||
settings = data.get('settings')
|
||||
if not settings:
|
||||
@@ -1455,6 +1500,37 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
}, status=400)
|
||||
return
|
||||
|
||||
# Gérer auto_extract séparément
|
||||
if 'auto_extract' in settings:
|
||||
set_auto_extract(settings['auto_extract'])
|
||||
del settings['auto_extract'] # Ne pas sauvegarder dans le fichier principal
|
||||
|
||||
# Gérer web_service_at_boot (Linux only)
|
||||
if 'web_service_at_boot' in settings:
|
||||
if config.OPERATING_SYSTEM == "Linux":
|
||||
try:
|
||||
toggle_web_service_at_boot(settings['web_service_at_boot'])
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle web service: {e}")
|
||||
del settings['web_service_at_boot']
|
||||
|
||||
# Gérer custom_dns_at_boot (Linux only)
|
||||
if 'custom_dns_at_boot' in settings:
|
||||
if config.OPERATING_SYSTEM == "Linux":
|
||||
try:
|
||||
toggle_custom_dns_at_boot(settings['custom_dns_at_boot'])
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle custom DNS: {e}")
|
||||
del settings['custom_dns_at_boot']
|
||||
|
||||
# Gérer API keys séparément
|
||||
if 'api_keys' in settings:
|
||||
try:
|
||||
save_api_keys(settings['api_keys'])
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur sauvegarde API keys: {e}")
|
||||
del settings['api_keys']
|
||||
|
||||
save_rgsx_settings(settings)
|
||||
|
||||
self._send_json({
|
||||
@@ -1469,6 +1545,47 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
# Route: Sauvegarder seulement les filtres (sauvegarde rapide)
|
||||
elif path == '/api/save_filters':
|
||||
try:
|
||||
from rgsx_settings import load_rgsx_settings, save_rgsx_settings
|
||||
|
||||
# Charger les settings actuels
|
||||
current_settings = load_rgsx_settings()
|
||||
|
||||
# Mettre à jour seulement les filtres
|
||||
if 'game_filters' not in current_settings:
|
||||
current_settings['game_filters'] = {}
|
||||
|
||||
current_settings['game_filters']['region_filters'] = data.get('region_filters', {})
|
||||
current_settings['game_filters']['hide_non_release'] = data.get('hide_non_release', False)
|
||||
current_settings['game_filters']['one_rom_per_game'] = data.get('one_rom_per_game', False)
|
||||
current_settings['game_filters']['regex_mode'] = data.get('regex_mode', False)
|
||||
current_settings['game_filters']['region_priority'] = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
|
||||
|
||||
# Sauvegarder
|
||||
save_rgsx_settings(current_settings)
|
||||
|
||||
# Mettre à jour config.game_filter_obj
|
||||
if hasattr(config, 'game_filter_obj'):
|
||||
config.game_filter_obj.region_filters = data.get('region_filters', {})
|
||||
config.game_filter_obj.hide_non_release = data.get('hide_non_release', False)
|
||||
config.game_filter_obj.one_rom_per_game = data.get('one_rom_per_game', False)
|
||||
config.game_filter_obj.regex_mode = data.get('regex_mode', False)
|
||||
config.game_filter_obj.region_priority = data.get('region_priority', ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'])
|
||||
|
||||
self._send_json({
|
||||
'success': True,
|
||||
'message': 'Filtres sauvegardés'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la sauvegarde des filtres: {e}")
|
||||
self._send_json({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
# Route: Vider l'historique
|
||||
elif path == '/api/clear-history':
|
||||
try:
|
||||
@@ -1953,18 +2070,47 @@ def run_server(host='0.0.0.0', port=5000):
|
||||
class ReuseAddrHTTPServer(HTTPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
# Tuer les processus existants utilisant le port
|
||||
# Tuer les processus existants utilisant le port (plateforme spécifique)
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True, timeout=2)
|
||||
pids = result.stdout.strip().split('\n')
|
||||
for pid in pids:
|
||||
if pid:
|
||||
try:
|
||||
subprocess.run(['kill', '-9', pid], timeout=2)
|
||||
logger.info(f"Processus {pid} tué (port {port} libéré)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
|
||||
# Windows: utiliser netstat + taskkill
|
||||
if os.name == 'nt' or getattr(config, 'OPERATING_SYSTEM', '').lower() == 'windows':
|
||||
try:
|
||||
netstat = subprocess.run(['netstat', '-ano'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=3)
|
||||
lines = netstat.stdout.splitlines()
|
||||
pids = set()
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) >= 5:
|
||||
local = parts[1]
|
||||
pid = parts[-1]
|
||||
if local.endswith(f':{port}'):
|
||||
pids.add(pid)
|
||||
for pid in pids:
|
||||
# Safer: ignore PID 0 and non-numeric entries (system / header lines)
|
||||
if not pid or not pid.isdigit():
|
||||
continue
|
||||
pid_int = int(pid)
|
||||
if pid_int <= 0:
|
||||
continue
|
||||
try:
|
||||
subprocess.run(['taskkill', '/PID', pid, '/F'], timeout=3)
|
||||
logger.info(f"Processus {pid} tué (port {port} libéré) [Windows]")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Windows port release check failed: {e}")
|
||||
else:
|
||||
# Unix-like: utiliser lsof + kill
|
||||
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=2)
|
||||
pids = result.stdout.strip().split('\n')
|
||||
for pid in pids:
|
||||
if pid:
|
||||
try:
|
||||
subprocess.run(['kill', '-9', pid], timeout=2)
|
||||
logger.info(f"Processus {pid} tué (port {port} libéré)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Impossible de libérer le port {port}: {e}")
|
||||
|
||||
@@ -2022,7 +2168,7 @@ def run_server(host='0.0.0.0', port=5000):
|
||||
if __name__ == '__main__':
|
||||
print("="*60, flush=True)
|
||||
print("Demarrage du serveur RGSX Web...", flush=True)
|
||||
print(f"Fichier de log prévu: {config.log_file_web}", flush=True)
|
||||
print(f"Fichier de log prevu: {config.log_file_web}", flush=True)
|
||||
print("="*60, flush=True)
|
||||
|
||||
parser = argparse.ArgumentParser(description='RGSX Web Server')
|
||||
|
||||
@@ -473,3 +473,91 @@ header p { opacity: 0.9; font-size: 1.1em; }
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Support */
|
||||
.support-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.support-modal-content {
|
||||
background: #2c2c2c;
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.support-modal h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #4CAF50;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.support-modal-message {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 25px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.support-modal button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.support-modal button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* System Info Collapse/Details */
|
||||
details summary {
|
||||
list-style: none;
|
||||
}
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
details summary .collapse-arrow {
|
||||
transition: transform 0.3s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
details[open] summary .collapse-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
details[open] summary {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
details summary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,53 @@
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Modal pour afficher les messages support avec formatage
|
||||
function showSupportModal(title, message) {
|
||||
// Remplacer les \n littéraux par de vrais retours à la ligne
|
||||
message = message.replace(/\\n/g, '\n');
|
||||
|
||||
// Créer la modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'support-modal';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'support-modal-content';
|
||||
|
||||
// Titre
|
||||
const titleElement = document.createElement('h2');
|
||||
titleElement.textContent = title;
|
||||
|
||||
// Message avec retours à la ligne préservés
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = 'support-modal-message';
|
||||
messageElement.textContent = message;
|
||||
|
||||
// Bouton OK
|
||||
const okButton = document.createElement('button');
|
||||
okButton.textContent = 'OK';
|
||||
okButton.onclick = () => {
|
||||
modal.style.animation = 'fadeOut 0.2s ease-in';
|
||||
setTimeout(() => modal.remove(), 200);
|
||||
};
|
||||
|
||||
// Assembler la modal
|
||||
modalContent.appendChild(titleElement);
|
||||
modalContent.appendChild(messageElement);
|
||||
modalContent.appendChild(okButton);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
// Ajouter au DOM
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Fermer en cliquant sur le fond
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.animation = 'fadeOut 0.2s ease-in';
|
||||
setTimeout(() => modal.remove(), 200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Charger les traductions au démarrage
|
||||
async function loadTranslations() {
|
||||
try {
|
||||
@@ -309,6 +356,9 @@
|
||||
|
||||
// Restaurer l'état depuis l'URL au chargement
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// Load saved filters first
|
||||
loadSavedFilters();
|
||||
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (path.startsWith('/platform/')) {
|
||||
@@ -478,9 +528,130 @@
|
||||
// Filter state: Map of region -> 'include' or 'exclude'
|
||||
let regionFilters = new Map();
|
||||
|
||||
// Checkbox filter states (stored globally to restore after page changes)
|
||||
let savedHideNonRelease = false;
|
||||
let savedOneRomPerGame = false;
|
||||
let savedRegexMode = false;
|
||||
|
||||
// Region priority order for "One ROM Per Game" (customizable)
|
||||
let regionPriorityOrder = JSON.parse(localStorage.getItem('regionPriorityOrder')) ||
|
||||
['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'];
|
||||
['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'];
|
||||
|
||||
// Save filters to backend
|
||||
async function saveFiltersToBackend() {
|
||||
try {
|
||||
const regionFiltersObj = {};
|
||||
regionFilters.forEach((mode, region) => {
|
||||
regionFiltersObj[region] = mode;
|
||||
});
|
||||
|
||||
// Update saved states from checkboxes if they exist
|
||||
if (document.getElementById('hide-non-release')) {
|
||||
savedHideNonRelease = document.getElementById('hide-non-release').checked;
|
||||
}
|
||||
if (document.getElementById('one-rom-per-game')) {
|
||||
savedOneRomPerGame = document.getElementById('one-rom-per-game').checked;
|
||||
}
|
||||
if (document.getElementById('regex-mode')) {
|
||||
savedRegexMode = document.getElementById('regex-mode').checked;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/save_filters', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
region_filters: regionFiltersObj,
|
||||
hide_non_release: savedHideNonRelease,
|
||||
one_rom_per_game: savedOneRomPerGame,
|
||||
regex_mode: savedRegexMode,
|
||||
region_priority: regionPriorityOrder
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
console.warn('Failed to save filters:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to save filters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved filters from settings
|
||||
async function loadSavedFilters() {
|
||||
try {
|
||||
const response = await fetch('/api/settings');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.settings.game_filters) {
|
||||
const filters = data.settings.game_filters;
|
||||
|
||||
// Load region filters
|
||||
if (filters.region_filters) {
|
||||
regionFilters.clear();
|
||||
Object.entries(filters.region_filters).forEach(([region, mode]) => {
|
||||
regionFilters.set(region, mode);
|
||||
});
|
||||
}
|
||||
|
||||
// Load region priority
|
||||
if (filters.region_priority) {
|
||||
regionPriorityOrder = filters.region_priority;
|
||||
localStorage.setItem('regionPriorityOrder', JSON.stringify(regionPriorityOrder));
|
||||
}
|
||||
|
||||
// Save checkbox states to global variables
|
||||
savedHideNonRelease = filters.hide_non_release || false;
|
||||
savedOneRomPerGame = filters.one_rom_per_game || false;
|
||||
savedRegexMode = filters.regex_mode || false;
|
||||
|
||||
// Load checkboxes when they exist (in games view)
|
||||
if (document.getElementById('hide-non-release')) {
|
||||
document.getElementById('hide-non-release').checked = savedHideNonRelease;
|
||||
}
|
||||
if (document.getElementById('one-rom-per-game')) {
|
||||
document.getElementById('one-rom-per-game').checked = savedOneRomPerGame;
|
||||
}
|
||||
if (document.getElementById('regex-mode')) {
|
||||
document.getElementById('regex-mode').checked = savedRegexMode;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load saved filters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore filter button states in the UI
|
||||
function restoreFilterStates() {
|
||||
// Restore region button states
|
||||
regionFilters.forEach((mode, region) => {
|
||||
const btn = document.querySelector(`.region-btn[data-region="${region}"]`);
|
||||
if (btn) {
|
||||
if (mode === 'include') {
|
||||
btn.classList.add('active');
|
||||
btn.classList.remove('excluded');
|
||||
} else if (mode === 'exclude') {
|
||||
btn.classList.remove('active');
|
||||
btn.classList.add('excluded');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Restore checkbox states
|
||||
if (document.getElementById('hide-non-release')) {
|
||||
document.getElementById('hide-non-release').checked = savedHideNonRelease;
|
||||
}
|
||||
if (document.getElementById('one-rom-per-game')) {
|
||||
document.getElementById('one-rom-per-game').checked = savedOneRomPerGame;
|
||||
}
|
||||
if (document.getElementById('regex-mode')) {
|
||||
document.getElementById('regex-mode').checked = savedRegexMode;
|
||||
}
|
||||
|
||||
// Apply filters to display the games correctly
|
||||
applyAllFilters();
|
||||
}
|
||||
|
||||
|
||||
// Helper: Extract region(s) from game name - returns array of regions
|
||||
function getGameRegions(gameName) {
|
||||
@@ -490,12 +661,16 @@
|
||||
// Common region patterns - check all, not just first match
|
||||
// Handle both "(USA)" and "(USA, Europe)" formats
|
||||
if (name.includes('USA') || name.includes('US)')) regions.push('USA');
|
||||
if (name.includes('CANADA')) regions.push('Canada');
|
||||
if (name.includes('EUROPE') || name.includes('EU)')) regions.push('Europe');
|
||||
if (name.includes('FRANCE') || name.includes('FR)')) regions.push('France');
|
||||
if (name.includes('GERMANY') || name.includes('DE)')) regions.push('Germany');
|
||||
if (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)')) regions.push('Japan');
|
||||
if (name.includes('KOREA') || name.includes('KR)')) regions.push('Korea');
|
||||
if (name.includes('WORLD')) regions.push('World');
|
||||
|
||||
// Check for other regions
|
||||
if (name.match(/\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|FRANCE|GERMANY|ITALY)\b/)) {
|
||||
// Check for other regions (excluding the ones above)
|
||||
if (name.match(/\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|SPAIN|ITALY)\b/)) {
|
||||
if (!regions.includes('Other')) regions.push('Other');
|
||||
}
|
||||
|
||||
@@ -578,7 +753,10 @@
|
||||
if (region === 'CANADA' && name.includes('CANADA')) return i;
|
||||
if (region === 'WORLD' && name.includes('WORLD')) return i;
|
||||
if (region === 'EUROPE' && (name.includes('EUROPE') || name.includes('EU)'))) return i;
|
||||
if (region === 'FRANCE' && (name.includes('FRANCE') || name.includes('FR)'))) return i;
|
||||
if (region === 'GERMANY' && (name.includes('GERMANY') || name.includes('DE)'))) return i;
|
||||
if (region === 'JAPAN' && (name.includes('JAPAN') || name.includes('JP)') || name.includes('JPN)'))) return i;
|
||||
if (region === 'KOREA' && (name.includes('KOREA') || name.includes('KR)'))) return i;
|
||||
}
|
||||
|
||||
return regionPriorityOrder.length; // Other regions (lowest priority)
|
||||
@@ -606,6 +784,7 @@
|
||||
[regionPriorityOrder[idx-1], regionPriorityOrder[idx]];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,14 +796,16 @@
|
||||
[regionPriorityOrder[idx+1], regionPriorityOrder[idx]];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset region priority to default
|
||||
function resetRegionPriority() {
|
||||
regionPriorityOrder = ['USA', 'Canada', 'World', 'Europe', 'Japan', 'Other'];
|
||||
regionPriorityOrder = ['USA', 'Canada', 'Europe', 'France', 'Germany', 'Japan', 'Korea', 'World', 'Other'];
|
||||
saveRegionPriorityOrder();
|
||||
renderRegionPriorityConfig();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
|
||||
// Render region priority configuration UI
|
||||
@@ -641,11 +822,11 @@
|
||||
<span style="font-weight: bold; color: #666; min-width: 25px;">${idx + 1}.</span>
|
||||
<span style="flex: 1; font-weight: 500;">${region}</span>
|
||||
<button onclick="moveRegionUp('${region}')"
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px;"
|
||||
${idx === 0 ? 'disabled' : ''}>▲</button>
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;"
|
||||
${idx === 0 ? 'disabled' : ''}>🔼</button>
|
||||
<button onclick="moveRegionDown('${region}')"
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px;"
|
||||
${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>▼</button>
|
||||
style="padding: 4px 8px; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 3px; font-size: 14px;"
|
||||
${idx === regionPriorityOrder.length - 1 ? 'disabled' : ''}>🔽</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -706,14 +887,15 @@
|
||||
}
|
||||
|
||||
applyAllFilters();
|
||||
saveFiltersToBackend();
|
||||
}
|
||||
|
||||
// Apply all filters
|
||||
function applyAllFilters() {
|
||||
const searchInput = document.getElementById('game-search');
|
||||
const searchTerm = searchInput ? searchInput.value : '';
|
||||
const hideNonRelease = document.getElementById('hide-non-release')?.checked || false;
|
||||
const regexMode = document.getElementById('regex-mode')?.checked || false;
|
||||
const hideNonRelease = document.getElementById('hide-non-release')?.checked || savedHideNonRelease;
|
||||
const regexMode = document.getElementById('regex-mode')?.checked || savedRegexMode;
|
||||
|
||||
const items = document.querySelectorAll('.game-item');
|
||||
let visibleCount = 0;
|
||||
@@ -804,7 +986,7 @@
|
||||
});
|
||||
|
||||
// Apply one-rom-per-game filter (after other filters)
|
||||
const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || false;
|
||||
const oneRomPerGame = document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame;
|
||||
if (oneRomPerGame) {
|
||||
// Group currently visible games by base name
|
||||
const gameGroups = new Map();
|
||||
@@ -901,13 +1083,24 @@
|
||||
const getSizeInMo = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
const text = sizeElem.textContent;
|
||||
// Les tailles sont maintenant normalisées: "100 Mo" ou "2.5 Go"
|
||||
const match = text.match(/([0-9.]+)\\s*(Mo|Go)/i);
|
||||
// Support des formats: "100 Mo", "2.5 Go" (français) et "100 MB", "2.5 GB" (anglais)
|
||||
// Plus Ko/KB, o/B, To/TB
|
||||
const match = text.match(/([0-9.]+)\s*(o|B|Ko|KB|Mo|MB|Go|GB|To|TB)/i);
|
||||
if (!match) return 0;
|
||||
let size = parseFloat(match[1]);
|
||||
// Convertir Go en Mo pour comparaison
|
||||
if (match[2].toUpperCase() === 'GO') {
|
||||
size *= 1024;
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
// Convertir tout en Mo
|
||||
if (unit === 'O' || unit === 'B') {
|
||||
size /= (1024 * 1024); // octets/bytes vers Mo
|
||||
} else if (unit === 'KO' || unit === 'KB') {
|
||||
size /= 1024; // Ko vers Mo
|
||||
} else if (unit === 'MO' || unit === 'MB') {
|
||||
// Déjà en Mo
|
||||
} else if (unit === 'GO' || unit === 'GB') {
|
||||
size *= 1024; // Go vers Mo
|
||||
} else if (unit === 'TO' || unit === 'TB') {
|
||||
size *= 1024 * 1024; // To vers Mo
|
||||
}
|
||||
return size;
|
||||
};
|
||||
@@ -1032,22 +1225,26 @@
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">${t('web_filter_region')}:</span>
|
||||
<button class="region-btn" data-region="USA" onclick="toggleRegionFilter('USA')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1fa-1f1f8.svg" style="width:16px;height:16px" /> USA</button>
|
||||
<button class="region-btn" data-region="Europe" onclick="toggleRegionFilter('Europe')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ea-1f1fa.svg" style="width:16px;height:16px" /> Europe</button>
|
||||
<button class="region-btn" data-region="Canada" onclick="toggleRegionFilter('Canada')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1e8-1f1e6.svg" style="width:16px;height:16px" /> Canada</button>
|
||||
<button class="region-btn" data-region="Europe" onclick="toggleRegionFilter('Europe')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ea-1f1fa.svg" style="width:16px;height:16px" /> Europe</button>
|
||||
<button class="region-btn" data-region="France" onclick="toggleRegionFilter('France')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1eb-1f1f7.svg" style="width:16px;height:16px" /> France</button>
|
||||
<button class="region-btn" data-region="Germany" onclick="toggleRegionFilter('Germany')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1e9-1f1ea.svg" style="width:16px;height:16px" /> Germany</button>
|
||||
<button class="region-btn" data-region="Japan" onclick="toggleRegionFilter('Japan')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1ef-1f1f5.svg" style="width:16px;height:16px" /> Japan</button>
|
||||
<button class="region-btn" data-region="Korea" onclick="toggleRegionFilter('Korea')"><img src="https://images.emojiterra.com/google/noto-emoji/unicode-16.0/color/svg/1f1f0-1f1f7.svg" style="width:16px;height:16px" /> Korea</button>
|
||||
<button class="region-btn" data-region="World" onclick="toggleRegionFilter('World')">🌍 World</button>
|
||||
<button class="region-btn" data-region="Other" onclick="toggleRegionFilter('Other')">🌐 Other</button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hide-non-release" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="hide-non-release" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_hide_non_release')}</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="regex-mode" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="regex-mode" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_regex_mode')}</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="one-rom-per-game" onchange="applyAllFilters()">
|
||||
<input type="checkbox" id="one-rom-per-game" onchange="applyAllFilters(); saveFiltersToBackend();">
|
||||
<span>${t('web_filter_one_rom_per_game')} (<span id="region-priority-display">USA → Canada → World → Europe → Japan → Other</span>)</span>
|
||||
<button onclick="showRegionPriorityConfig()" style="margin-left: 8px; padding: 2px 8px; font-size: 0.9em; background: #666; color: white; border: none; border-radius: 3px; cursor: pointer;" title="${t('web_filter_configure_priority')}">⚙️</button>
|
||||
</label>
|
||||
@@ -1082,6 +1279,9 @@
|
||||
`;
|
||||
container.innerHTML = html;
|
||||
|
||||
// Restore filter states from loaded settings
|
||||
restoreFilterStates();
|
||||
|
||||
// Appliquer le tri par défaut (A-Z)
|
||||
sortGames(currentGameSort);
|
||||
|
||||
@@ -1666,101 +1866,107 @@
|
||||
const showUnsupportedLabel = t('web_settings_show_unsupported');
|
||||
const allowUnknownLabel = t('web_settings_allow_unknown');
|
||||
|
||||
// Construire la section d'informations système détaillées
|
||||
// Construire la section d'informations système détaillées (dans un collapse fermé par défaut)
|
||||
let systemInfoHTML = '';
|
||||
if (systemInfo && (systemInfo.model || systemInfo.cpu_model)) {
|
||||
systemInfoHTML = `
|
||||
<h3 style="margin-top: 20px; margin-bottom: 15px;">🖥️ System Information</h3>
|
||||
<div class="info-grid" style="margin-bottom: 20px; background: #f0f8ff; padding: 15px; border-radius: 8px; border: 2px solid #007bff;">
|
||||
${systemInfo.model ? `
|
||||
<details style="margin-top: 20px; margin-bottom: 20px;">
|
||||
<summary style="cursor: pointer; padding: 12px 15px; background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; border-radius: 8px; font-weight: bold; font-size: 1.1em; list-style: none; display: flex; align-items: center; gap: 10px;">
|
||||
<span class="collapse-arrow">▶</span>
|
||||
🖥️ ${t('web_system_info_title') || 'System Information'}
|
||||
<span style="margin-left: auto; font-size: 0.85em; opacity: 0.9;">${systemInfo.model || systemInfo.system || ''}</span>
|
||||
</summary>
|
||||
<div class="info-grid" style="margin-top: 10px; background: #f0f8ff; padding: 15px; border-radius: 0 0 8px 8px; border: 2px solid #007bff; border-top: none;">
|
||||
${systemInfo.model ? `
|
||||
<div class="info-item">
|
||||
<strong>💻 Model</strong>
|
||||
${systemInfo.model}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.system ? `
|
||||
<div class="info-item">
|
||||
<strong>🐧 System</strong>
|
||||
${systemInfo.system}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.architecture ? `
|
||||
<div class="info-item">
|
||||
<strong>⚙️ Architecture</strong>
|
||||
${systemInfo.architecture}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_model ? `
|
||||
<div class="info-item">
|
||||
<strong>🔧 CPU Model</strong>
|
||||
${systemInfo.cpu_model}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_cores ? `
|
||||
<div class="info-item">
|
||||
<strong>🧮 CPU Cores</strong>
|
||||
${systemInfo.cpu_cores}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_max_frequency ? `
|
||||
<div class="info-item">
|
||||
<strong>⚡ CPU Frequency</strong>
|
||||
${systemInfo.cpu_max_frequency}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_features ? `
|
||||
<div class="info-item">
|
||||
<strong>✨ CPU Features</strong>
|
||||
${systemInfo.cpu_features}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.temperature ? `
|
||||
<div class="info-item">
|
||||
<strong>🌡️ Temperature</strong>
|
||||
${systemInfo.temperature}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.available_memory && systemInfo.total_memory ? `
|
||||
<div class="info-item">
|
||||
<strong>💾 Memory</strong>
|
||||
${systemInfo.available_memory} / ${systemInfo.total_memory}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.display_resolution ? `
|
||||
<div class="info-item">
|
||||
<strong>🖥️ Display Resolution</strong>
|
||||
${systemInfo.display_resolution}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.display_refresh_rate ? `
|
||||
<div class="info-item">
|
||||
<strong>🔄 Refresh Rate</strong>
|
||||
${systemInfo.display_refresh_rate}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.data_partition_format ? `
|
||||
<div class="info-item">
|
||||
<strong>💽 Partition Format</strong>
|
||||
${systemInfo.data_partition_format}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.data_partition_space ? `
|
||||
<div class="info-item">
|
||||
<strong>💿 Available Space</strong>
|
||||
${systemInfo.data_partition_space}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.network_ip ? `
|
||||
<div class="info-item">
|
||||
<strong>🌐 Network IP</strong>
|
||||
${systemInfo.network_ip}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-item">
|
||||
<strong>💻 Model</strong>
|
||||
${systemInfo.model}
|
||||
<strong>🎮 ${platformsCountLabel}</strong>
|
||||
${info.platforms_count}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.system ? `
|
||||
<div class="info-item">
|
||||
<strong>🐧 System</strong>
|
||||
${systemInfo.system}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.architecture ? `
|
||||
<div class="info-item">
|
||||
<strong>⚙️ Architecture</strong>
|
||||
${systemInfo.architecture}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_model ? `
|
||||
<div class="info-item">
|
||||
<strong>🔧 CPU Model</strong>
|
||||
${systemInfo.cpu_model}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_cores ? `
|
||||
<div class="info-item">
|
||||
<strong>🧮 CPU Cores</strong>
|
||||
${systemInfo.cpu_cores}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_max_frequency ? `
|
||||
<div class="info-item">
|
||||
<strong>⚡ CPU Frequency</strong>
|
||||
${systemInfo.cpu_max_frequency}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.cpu_features ? `
|
||||
<div class="info-item">
|
||||
<strong>✨ CPU Features</strong>
|
||||
${systemInfo.cpu_features}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.temperature ? `
|
||||
<div class="info-item">
|
||||
<strong>🌡️ Temperature</strong>
|
||||
${systemInfo.temperature}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.available_memory && systemInfo.total_memory ? `
|
||||
<div class="info-item">
|
||||
<strong>💾 Memory</strong>
|
||||
${systemInfo.available_memory} / ${systemInfo.total_memory}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.display_resolution ? `
|
||||
<div class="info-item">
|
||||
<strong>🖥️ Display Resolution</strong>
|
||||
${systemInfo.display_resolution}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.display_refresh_rate ? `
|
||||
<div class="info-item">
|
||||
<strong>🔄 Refresh Rate</strong>
|
||||
${systemInfo.display_refresh_rate}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.data_partition_format ? `
|
||||
<div class="info-item">
|
||||
<strong>💽 Partition Format</strong>
|
||||
${systemInfo.data_partition_format}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.data_partition_space ? `
|
||||
<div class="info-item">
|
||||
<strong>💿 Available Space</strong>
|
||||
${systemInfo.data_partition_space}
|
||||
</div>
|
||||
` : ''}
|
||||
${systemInfo.network_ip ? `
|
||||
<div class="info-item">
|
||||
<strong>🌐 Network IP</strong>
|
||||
${systemInfo.network_ip}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-item">
|
||||
<strong>🎮 ${platformsCountLabel}</strong>
|
||||
${info.platforms_count}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1857,6 +2063,13 @@
|
||||
placeholder="${t('web_settings_custom_url_placeholder')}">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-auto-extract" ${settings.auto_extract !== false ? 'checked' : ''}>
|
||||
<span>📦 ${t('web_settings_auto_extract')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-show-unsupported" ${settings.show_unsupported_platforms ? 'checked' : ''}>
|
||||
@@ -1871,6 +2084,50 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
${info.system === 'Linux' ? `
|
||||
<h4 style="margin-top: 25px; margin-bottom: 15px; border-top: 1px solid #ddd; padding-top: 15px;">🐧 Linux/Batocera Options</h4>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-web-service" ${settings.web_service_at_boot ? 'checked' : ''}>
|
||||
<span>🌐 ${t('web_settings_web_service')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="setting-custom-dns" ${settings.custom_dns_at_boot ? 'checked' : ''}>
|
||||
<span>🔒 ${t('web_settings_custom_dns')}</span>
|
||||
</label>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<h4 style="margin-top: 25px; margin-bottom: 15px; border-top: 1px solid #ddd; padding-top: 15px;">🔑 API Keys</h4>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label>1fichier API Key</label>
|
||||
<input type="password" id="setting-api-1fichier" value="${settings.api_keys?.['1fichier'] || ''}"
|
||||
placeholder="Enter 1fichier API key">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label>AllDebrid API Key</label>
|
||||
<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>
|
||||
<input type="password" id="setting-api-realdebrid" value="${settings.api_keys?.realdebrid || ''}"
|
||||
placeholder="Enter RealDebrid API key">
|
||||
</div>
|
||||
|
||||
<button id="save-settings-btn" style="width: 100%; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; padding: 15px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top: 10px;">
|
||||
💾 ${t('web_settings_save')}
|
||||
</button>
|
||||
@@ -1902,6 +2159,12 @@
|
||||
}
|
||||
|
||||
try {
|
||||
// Collect region filters
|
||||
const regionFiltersObj = {};
|
||||
regionFilters.forEach((mode, region) => {
|
||||
regionFiltersObj[region] = mode;
|
||||
});
|
||||
|
||||
const settings = {
|
||||
language: document.getElementById('setting-language').value,
|
||||
music_enabled: document.getElementById('setting-music').checked,
|
||||
@@ -1921,7 +2184,25 @@
|
||||
},
|
||||
show_unsupported_platforms: document.getElementById('setting-show-unsupported').checked,
|
||||
allow_unknown_extensions: document.getElementById('setting-allow-unknown').checked,
|
||||
roms_folder: document.getElementById('setting-roms-folder').value.trim()
|
||||
auto_extract: document.getElementById('setting-auto-extract').checked,
|
||||
roms_folder: document.getElementById('setting-roms-folder').value.trim(),
|
||||
// Linux/Batocera options (only if elements exist)
|
||||
web_service_at_boot: document.getElementById('setting-web-service')?.checked || false,
|
||||
custom_dns_at_boot: document.getElementById('setting-custom-dns')?.checked || false,
|
||||
// API Keys
|
||||
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: {
|
||||
region_filters: regionFiltersObj,
|
||||
hide_non_release: document.getElementById('hide-non-release')?.checked || savedHideNonRelease,
|
||||
one_rom_per_game: document.getElementById('one-rom-per-game')?.checked || savedOneRomPerGame,
|
||||
regex_mode: document.getElementById('regex-mode')?.checked || savedRegexMode,
|
||||
region_priority: regionPriorityOrder
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch('/api/settings', {
|
||||
@@ -2020,7 +2301,7 @@
|
||||
}
|
||||
|
||||
// Générer un fichier ZIP de support
|
||||
async function generateSupportZip() {
|
||||
async function generateSupportZip(event) {
|
||||
try {
|
||||
// Afficher un message de chargement
|
||||
const loadingMsg = t('web_support_generating');
|
||||
@@ -2063,8 +2344,8 @@
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Afficher le message d'instructions
|
||||
alert(t('web_support_title') + '\\n\\n' + t('web_support_message'));
|
||||
// Afficher le message d'instructions dans une modal
|
||||
showSupportModal(t('web_support_title'), t('web_support_message'));
|
||||
|
||||
// Restaurer le bouton
|
||||
if (originalButton) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import requests # type: ignore
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
@@ -6,7 +8,7 @@ import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import config
|
||||
from config import HEADLESS
|
||||
from config import HEADLESS, Game
|
||||
try:
|
||||
if not HEADLESS:
|
||||
import pygame # type: ignore
|
||||
@@ -171,6 +173,121 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
return (False, str(e), None)
|
||||
|
||||
|
||||
VERSIONCLEAN_SERVICE_NAME = "versionclean"
|
||||
VERSIONCLEAN_BACKUP_PATH = "/usr/bin/batocera-version.bak"
|
||||
|
||||
|
||||
def _get_enabled_services():
|
||||
"""Retourne la liste des services activés dans batocera-settings, ou None si indisponible."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["batocera-settings-get", "system.services"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"batocera-settings-get failed: {result.stderr}")
|
||||
return None
|
||||
return result.stdout.split()
|
||||
except FileNotFoundError:
|
||||
logger.warning("batocera-settings-get command not found")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read enabled services: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_versionclean_service():
|
||||
"""Installe et active versionclean si nécessaire.
|
||||
|
||||
- Installe uniquement si le service n'est pas déjà présent.
|
||||
- Active uniquement si le service n'est pas déjà activé.
|
||||
- Démarre uniquement si le nettoyage n'est pas déjà appliqué.
|
||||
"""
|
||||
try:
|
||||
if config.OPERATING_SYSTEM != "Linux":
|
||||
return (True, "Versionclean skipped (non-Linux)")
|
||||
|
||||
services_dir = "/userdata/system/services"
|
||||
service_file = os.path.join(services_dir, VERSIONCLEAN_SERVICE_NAME)
|
||||
source_file = os.path.join(config.APP_FOLDER, "assets", "progs", VERSIONCLEAN_SERVICE_NAME)
|
||||
|
||||
if not os.path.exists(service_file):
|
||||
try:
|
||||
os.makedirs(services_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
if not os.path.exists(source_file):
|
||||
error_msg = f"Source service file not found: {source_file}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
try:
|
||||
shutil.copy2(source_file, service_file)
|
||||
os.chmod(service_file, 0o755)
|
||||
logger.info(f"Versionclean service installed: {service_file}")
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to copy versionclean service file: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
else:
|
||||
logger.debug("Versionclean service already present, skipping install")
|
||||
|
||||
enabled_services = _get_enabled_services()
|
||||
if enabled_services is None or VERSIONCLEAN_SERVICE_NAME not in enabled_services:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["batocera-services", "enable", VERSIONCLEAN_SERVICE_NAME],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"batocera-services enable versionclean failed: {result.stderr}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
logger.debug(f"Versionclean enabled: {result.stdout}")
|
||||
except FileNotFoundError:
|
||||
error_msg = "batocera-services command not found"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to enable versionclean: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
else:
|
||||
logger.debug("Versionclean already enabled, skipping enable")
|
||||
|
||||
if os.path.exists(VERSIONCLEAN_BACKUP_PATH):
|
||||
logger.debug("Versionclean already active (backup present), skipping start")
|
||||
return (True, "Versionclean already active")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["batocera-services", "start", VERSIONCLEAN_SERVICE_NAME],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"batocera-services start versionclean warning: {result.stderr}")
|
||||
else:
|
||||
logger.debug(f"Versionclean started: {result.stdout}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to start versionclean (non-critical): {str(e)}")
|
||||
|
||||
return (True, "Versionclean ensured")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected versionclean error: {str(e)}"
|
||||
logger.exception(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
|
||||
def toggle_web_service_at_boot(enable: bool):
|
||||
"""Active ou désactive le service web au démarrage de Batocera.
|
||||
|
||||
@@ -203,6 +320,11 @@ def toggle_web_service_at_boot(enable: bool):
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 1b. Assurer versionclean (install/enable/start si nécessaire)
|
||||
ensure_ok, ensure_msg = _ensure_versionclean_service()
|
||||
if not ensure_ok:
|
||||
return (False, ensure_msg)
|
||||
|
||||
# 2. Copier le fichier rgsx_web
|
||||
try:
|
||||
@@ -339,6 +461,11 @@ def toggle_custom_dns_at_boot(enable: bool):
|
||||
error_msg = f"Failed to create services directory: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return (False, error_msg)
|
||||
|
||||
# 1b. Assurer versionclean (install/enable/start si nécessaire)
|
||||
ensure_ok, ensure_msg = _ensure_versionclean_service()
|
||||
if not ensure_ok:
|
||||
return (False, ensure_msg)
|
||||
|
||||
# 2. Copier le fichier custom_dns
|
||||
try:
|
||||
@@ -479,6 +606,149 @@ def check_custom_dns_status():
|
||||
return False
|
||||
|
||||
|
||||
CONNECTION_STATUS_TTL_SECONDS = 120
|
||||
|
||||
|
||||
def get_connection_status_targets():
|
||||
"""Retourne la liste des sites à vérifier pour le status de connexion."""
|
||||
return [
|
||||
{
|
||||
"key": "retrogamesets",
|
||||
"label": "Retrogamesets.fr",
|
||||
"url": "https://retrogamesets.fr",
|
||||
"category": "updates",
|
||||
},
|
||||
{
|
||||
"key": "github",
|
||||
"label": "GitHub.com",
|
||||
"url": "https://github.com",
|
||||
"category": "updates",
|
||||
},
|
||||
{
|
||||
"key": "myrient",
|
||||
"label": "Myrient.erista.me",
|
||||
"url": "https://myrient.erista.me",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "1fichier",
|
||||
"label": "1fichier.com",
|
||||
"url": "https://1fichier.com",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "archive",
|
||||
"label": "Archive.org",
|
||||
"url": "https://archive.org",
|
||||
"category": "sources",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _check_url_connectivity(url: str, timeout: int = 6) -> bool:
|
||||
"""Teste rapidement la connectivité à une URL (DNS + HTTPS)."""
|
||||
headers = {"User-Agent": "RGSX-Connectivity/1.0"}
|
||||
try:
|
||||
try:
|
||||
|
||||
|
||||
try:
|
||||
response = requests.head(url, timeout=timeout, allow_redirects=True, headers=headers)
|
||||
if response.status_code < 500:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
response = requests.get(url, timeout=timeout, allow_redirects=True, stream=True, headers=headers)
|
||||
return response.status_code < 500
|
||||
except Exception:
|
||||
return False
|
||||
except Exception:
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, method="HEAD", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return resp.status < 500
|
||||
except Exception:
|
||||
try:
|
||||
req = urllib.request.Request(url, method="GET", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return resp.status < 500
|
||||
except Exception:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def start_connection_status_check(force: bool = False) -> None:
|
||||
"""Lance un check asynchrone des sites (avec cache/TTL)."""
|
||||
try:
|
||||
now = time.time()
|
||||
if getattr(config, "connection_status_in_progress", False):
|
||||
return
|
||||
last_ts = getattr(config, "connection_status_timestamp", 0.0) or 0.0
|
||||
if not force and last_ts and now - last_ts < CONNECTION_STATUS_TTL_SECONDS:
|
||||
return
|
||||
|
||||
targets = get_connection_status_targets()
|
||||
status = getattr(config, "connection_status", {})
|
||||
if not isinstance(status, dict):
|
||||
status = {}
|
||||
if not status:
|
||||
for item in targets:
|
||||
status[item["key"]] = None
|
||||
config.connection_status = status
|
||||
config.connection_status_in_progress = True
|
||||
config.connection_status_progress = {"done": 0, "total": len(targets)}
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
results = {}
|
||||
done = 0
|
||||
total = len(targets)
|
||||
for item in targets:
|
||||
results[item["key"]] = _check_url_connectivity(item["url"])
|
||||
done += 1
|
||||
config.connection_status_progress = {"done": done, "total": total}
|
||||
try:
|
||||
config.needs_redraw = True
|
||||
except Exception:
|
||||
pass
|
||||
config.connection_status.update(results)
|
||||
config.connection_status_timestamp = time.time()
|
||||
try:
|
||||
summary = ", ".join([f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items()])
|
||||
logger.info(f"Connection status results: {summary}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
config.needs_redraw = True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"Connection status check failed: {e}")
|
||||
finally:
|
||||
config.connection_status_in_progress = False
|
||||
|
||||
threading.Thread(target=_worker, daemon=True).start()
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to start connection status check: {e}")
|
||||
|
||||
|
||||
def get_connection_status_snapshot():
|
||||
"""Retourne (status_dict, timestamp, in_progress, progress)."""
|
||||
status = getattr(config, "connection_status", {})
|
||||
if not isinstance(status, dict):
|
||||
status = {}
|
||||
ts = getattr(config, "connection_status_timestamp", 0.0) or 0.0
|
||||
in_progress = getattr(config, "connection_status_in_progress", False)
|
||||
progress = getattr(config, "connection_status_progress", {"done": 0, "total": 0})
|
||||
if not isinstance(progress, dict):
|
||||
progress = {"done": 0, "total": 0}
|
||||
return status, ts, in_progress, progress
|
||||
|
||||
|
||||
|
||||
_extensions_cache = None # type: ignore
|
||||
_extensions_json_regenerated = False
|
||||
@@ -659,7 +929,7 @@ def check_extension_before_download(url, platform, game_name):
|
||||
|
||||
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
|
||||
extension = os.path.splitext(sanitized_name)[1].lower()
|
||||
is_archive = extension in (".zip", ".rar")
|
||||
is_archive = extension in (".zip", ".rar", ".7z")
|
||||
|
||||
# Déterminer si le système (dossier) est connu dans extensions_data
|
||||
dest_folder_name = _get_dest_folder_name(platform)
|
||||
@@ -886,12 +1156,18 @@ def load_sources():
|
||||
for platform_name in config.platforms:
|
||||
games = load_games(platform_name)
|
||||
config.games_count[platform_name] = len(games)
|
||||
if config.games_count:
|
||||
try:
|
||||
summary = ", ".join([f"{name}: {count}" for name, count in config.games_count.items()])
|
||||
logger.debug(f"Nombre de jeux par système: {summary}")
|
||||
except Exception:
|
||||
pass
|
||||
return sources
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
|
||||
return []
|
||||
|
||||
def load_games(platform_id):
|
||||
def load_games(platform_id:str) -> list[Game]:
|
||||
try:
|
||||
# Retrouver l'objet plateforme pour accéder éventuellement à 'folder'
|
||||
platform_dict = None
|
||||
@@ -958,8 +1234,15 @@ def load_games(platform_id):
|
||||
else:
|
||||
logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}")
|
||||
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
return normalized
|
||||
if getattr(config, "games_count_log_verbose", False):
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
|
||||
games_list: list[Game] = []
|
||||
for name, url, size in normalized:
|
||||
display_name = Path(name).stem
|
||||
display_name = display_name.replace(platform_id, "")
|
||||
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
|
||||
return games_list
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
|
||||
return []
|
||||
@@ -1294,6 +1577,18 @@ def _handle_special_platforms(dest_dir, archive_path, before_dirs, iso_before=No
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
# PSVita: extraction dans ux0/app + création fichier .psvita
|
||||
psvita_dir_normal = os.path.join(config.ROMS_FOLDER, "psvita")
|
||||
psvita_dir_symlink = os.path.join(config.ROMS_FOLDER, "psvita", "psvita")
|
||||
is_psvita = (dest_dir == psvita_dir_normal or dest_dir == psvita_dir_symlink)
|
||||
|
||||
if is_psvita:
|
||||
expected_base = os.path.splitext(os.path.basename(archive_path))[0]
|
||||
items_before = before_items if before_items is not None else before_dirs
|
||||
success, error_msg = handle_psvita(dest_dir, items_before, extracted_basename=expected_base)
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
return True, None
|
||||
|
||||
def extract_zip(zip_path, dest_dir, url):
|
||||
@@ -1532,7 +1827,7 @@ def extract_rar(rar_path, dest_dir, url):
|
||||
os.chmod(os.path.join(root, dir_name), 0o755)
|
||||
|
||||
# Gestion plateformes spéciales (uniquement PS3 pour RAR)
|
||||
success, error_msg = _handle_special_platforms(dest_dir, rar_path, before_dirs)
|
||||
success, error_msg = _handle_special_platforms(dest_dir, rar_path, before_dirs, url=url)
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
@@ -1551,6 +1846,95 @@ def extract_rar(rar_path, dest_dir, url):
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}")
|
||||
|
||||
|
||||
def extract_7z(archive_path, dest_dir, url):
|
||||
"""Extrait le contenu d'un fichier 7z dans le dossier cible."""
|
||||
try:
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
if config.OPERATING_SYSTEM == "Windows":
|
||||
seven_z_cmd = config.SEVEN_Z_EXE
|
||||
else:
|
||||
seven_z_cmd = config.SEVEN_Z_LINUX
|
||||
try:
|
||||
if os.path.exists(seven_z_cmd) and not os.access(seven_z_cmd, os.X_OK):
|
||||
logger.warning("7zz n'est pas exécutable, correction des permissions...")
|
||||
os.chmod(seven_z_cmd, 0o755)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification des permissions de 7zz: {e}")
|
||||
|
||||
if not os.path.exists(seven_z_cmd):
|
||||
return False, "7z non trouvé - vérifiez que 7z.exe (Windows) ou 7zz (Linux) est présent dans assets/progs"
|
||||
|
||||
# Capture état initial
|
||||
before_dirs = _capture_directories_before_extraction(dest_dir)
|
||||
before_items = _capture_all_items_before_extraction(dest_dir)
|
||||
iso_before = set()
|
||||
for root, dirs, files in os.walk(dest_dir):
|
||||
for file in files:
|
||||
if file.lower().endswith('.iso'):
|
||||
iso_before.add(os.path.abspath(os.path.join(root, file)))
|
||||
|
||||
# Calcul taille totale via 7z l -slt (best effort)
|
||||
total_size = 0
|
||||
try:
|
||||
list_cmd = [seven_z_cmd, "l", "-slt", archive_path]
|
||||
result = subprocess.run(list_cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
current_size = None
|
||||
is_dir = False
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if current_size is not None and not is_dir:
|
||||
total_size += current_size
|
||||
current_size = None
|
||||
is_dir = False
|
||||
continue
|
||||
if line.startswith("Attributes ="):
|
||||
attrs = line.split("=", 1)[1].strip()
|
||||
if "D" in attrs:
|
||||
is_dir = True
|
||||
elif line.startswith("Size ="):
|
||||
try:
|
||||
current_size = int(line.split("=", 1)[1].strip())
|
||||
except Exception:
|
||||
current_size = None
|
||||
if current_size is not None and not is_dir:
|
||||
total_size += current_size
|
||||
except Exception as e:
|
||||
logger.debug(f"Impossible de calculer la taille 7z: {e}")
|
||||
|
||||
if url not in getattr(config, 'download_progress', {}):
|
||||
config.download_progress[url] = {}
|
||||
config.download_progress[url].update({
|
||||
"downloaded_size": 0,
|
||||
"total_size": total_size,
|
||||
"status": "Extracting",
|
||||
"progress_percent": 0
|
||||
})
|
||||
config.needs_redraw = True
|
||||
|
||||
extract_cmd = [seven_z_cmd, "x", archive_path, f"-o{dest_dir}", "-y"]
|
||||
logger.debug(f"Commande d'extraction 7z: {' '.join(extract_cmd)}")
|
||||
result = subprocess.run(extract_cmd, capture_output=True, text=True)
|
||||
if result.returncode > 2:
|
||||
error_msg = result.stderr.strip() or f"Erreur extraction 7z (code {result.returncode})"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"7z a retourné un avertissement (code {result.returncode}): {result.stderr}")
|
||||
|
||||
# Gestion plateformes spéciales
|
||||
success, error_msg = _handle_special_platforms(dest_dir, archive_path, before_dirs, iso_before, url, before_items)
|
||||
if not success:
|
||||
return False, error_msg
|
||||
|
||||
return _finalize_extraction(archive_path, dest_dir, url)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'extraction 7z: {str(e)}")
|
||||
return False, _("utils_extraction_failed").format(str(e))
|
||||
|
||||
def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archive_name=None):
|
||||
"""Gère le traitement spécifique des jeux PS3.
|
||||
PS3 Redump (ps3): Décryptage ISO + extraction dans dossier .ps3
|
||||
@@ -1577,18 +1961,31 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
# MODE PS3 : Décryptage et extraction
|
||||
# ============================================
|
||||
logger.info(f"Mode PS3 détecté pour: {archive_name}")
|
||||
|
||||
# L'extraction de l'archive est terminée; basculer l'UI en mode conversion/décryptage.
|
||||
try:
|
||||
if url:
|
||||
if url not in getattr(config, 'download_progress', {}):
|
||||
config.download_progress[url] = {}
|
||||
config.download_progress[url]["status"] = "Converting"
|
||||
config.download_progress[url]["progress_percent"] = 0
|
||||
config.needs_redraw = True
|
||||
|
||||
if isinstance(config.history, list):
|
||||
for entry in config.history:
|
||||
if entry.get("url") == url and entry.get("status") in ("Extracting", "Téléchargement", "Downloading"):
|
||||
entry["status"] = "Converting"
|
||||
entry["progress"] = 0
|
||||
entry["message"] = "PS3 conversion in progress"
|
||||
save_history(config.history)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"MAJ statut conversion PS3 ignorée: {e}")
|
||||
|
||||
try:
|
||||
# Construire l'URL de la clé en remplaçant le dossier
|
||||
if url and ("Sony%20-%20PlayStation%203/" in url or "Sony - PlayStation 3/" in url):
|
||||
key_url = url.replace("Sony%20-%20PlayStation%203/", "Sony%20-%20PlayStation%203%20-%20Disc%20Keys%20TXT/")
|
||||
key_url = key_url.replace("Sony - PlayStation 3/", "Sony - PlayStation 3 - Disc Keys TXT/")
|
||||
else:
|
||||
logger.warning("URL PS3 invalide ou manquante, tentative sans clé distante")
|
||||
key_url = None
|
||||
|
||||
ps3_keys_base_url = "https://retrogamesets.fr/softs/ps3/"
|
||||
logger.debug(f"URL jeu: {url}")
|
||||
logger.debug(f"URL clé: {key_url}")
|
||||
logger.debug(f"Base URL des clés PS3: {ps3_keys_base_url}")
|
||||
|
||||
# Chercher le fichier .iso déjà extrait
|
||||
iso_files = [f for f in os.listdir(dest_dir) if f.endswith('.iso') and not f.endswith('_decrypted.iso')]
|
||||
@@ -1599,42 +1996,51 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
iso_path = os.path.join(dest_dir, iso_file)
|
||||
logger.info(f"Fichier ISO trouvé: {iso_path}")
|
||||
|
||||
# Étape 1: Télécharger et extraire la clé si URL disponible
|
||||
# Étape 1: Télécharger directement la clé .dkey depuis la nouvelle source
|
||||
dkey_path = None
|
||||
if key_url:
|
||||
logger.info("Téléchargement de la clé de décryption...")
|
||||
key_zip_name = os.path.basename(archive_name) if archive_name else "key.zip"
|
||||
key_zip_path = os.path.join(dest_dir, f"_temp_key_{key_zip_name}")
|
||||
|
||||
logger.info("Téléchargement de la clé de décryption (.dkey)...")
|
||||
|
||||
candidate_bases = []
|
||||
|
||||
def _add_candidate_base(base_name):
|
||||
if not base_name:
|
||||
return
|
||||
cleaned = str(base_name).strip()
|
||||
if not cleaned:
|
||||
return
|
||||
if cleaned.lower().endswith('.dkey'):
|
||||
cleaned = cleaned[:-5]
|
||||
if cleaned not in candidate_bases:
|
||||
candidate_bases.append(cleaned)
|
||||
|
||||
if archive_name:
|
||||
_add_candidate_base(os.path.splitext(os.path.basename(archive_name))[0])
|
||||
if extracted_basename:
|
||||
_add_candidate_base(extracted_basename)
|
||||
_add_candidate_base(os.path.splitext(os.path.basename(iso_file))[0])
|
||||
|
||||
for base_name in candidate_bases:
|
||||
remote_name = f"{base_name}.dkey"
|
||||
encoded_name = remote_name.replace(" ", "%20")
|
||||
key_url = f"{ps3_keys_base_url}{encoded_name}"
|
||||
logger.debug(f"Tentative clé distante: {key_url}")
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(key_url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(key_zip_path, 'wb') as f:
|
||||
if response.status_code != 200:
|
||||
logger.debug(f"Clé distante introuvable ({response.status_code}): {remote_name}")
|
||||
continue
|
||||
|
||||
local_dkey_path = os.path.join(dest_dir, remote_name)
|
||||
with open(local_dkey_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
logger.info(f"Clé téléchargée: {key_zip_path}")
|
||||
|
||||
# Extraire la clé
|
||||
logger.info("Extraction de la clé...")
|
||||
with zipfile.ZipFile(key_zip_path, 'r') as zf:
|
||||
dkey_files = [f for f in zf.namelist() if f.endswith('.dkey')]
|
||||
if not dkey_files:
|
||||
logger.warning("Aucun fichier .dkey trouvé dans l'archive de clé")
|
||||
else:
|
||||
dkey_file = dkey_files[0]
|
||||
zf.extract(dkey_file, dest_dir)
|
||||
dkey_path = os.path.join(dest_dir, dkey_file)
|
||||
logger.info(f"Clé extraite: {dkey_path}")
|
||||
|
||||
# Supprimer le ZIP de la clé
|
||||
os.remove(key_zip_path)
|
||||
|
||||
|
||||
dkey_path = local_dkey_path
|
||||
logger.info(f"Clé téléchargée: {dkey_path}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du téléchargement/extraction de la clé: {e}")
|
||||
logger.warning(f"Échec téléchargement clé {remote_name}: {e}")
|
||||
|
||||
# Chercher une clé .dkey si pas téléchargée
|
||||
if not dkey_path:
|
||||
@@ -1959,6 +2365,136 @@ def handle_scummvm(dest_dir, before_items, extracted_basename=None):
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def handle_psvita(dest_dir, before_items, extracted_basename=None):
|
||||
"""Gère l'organisation spécifique des jeux PSVita extraits.
|
||||
|
||||
Structure attendue:
|
||||
- Archive RAR extraite → Dossier "Nom du jeu"/
|
||||
- Dans ce dossier → Fichier "IDJeu.zip" (ex: PCSE00890.zip)
|
||||
- Ce ZIP contient → Dossier "IDJeu" (ex: PCSE00890/)
|
||||
|
||||
Actions:
|
||||
1. Créer fichier "Nom du jeu [IDJeu].psvita" dans dest_dir
|
||||
2. Extraire IDJeu.zip dans config.SAVE_FOLDER/psvita/ux0/app/
|
||||
3. Supprimer le dossier temporaire "Nom du jeu"/
|
||||
|
||||
Args:
|
||||
dest_dir: Dossier de destination (psvita ou psvita/psvita)
|
||||
before_items: Set des éléments présents avant extraction
|
||||
extracted_basename: Nom de base de l'archive extraite (sans extension)
|
||||
"""
|
||||
logger.debug(f"Traitement spécifique PSVita dans: {dest_dir}")
|
||||
time.sleep(2) # Petite latence post-extraction
|
||||
|
||||
try:
|
||||
after_items = set(os.listdir(dest_dir))
|
||||
except Exception:
|
||||
after_items = set()
|
||||
|
||||
ignore_names = {"psvita", "images", "videos", "manuals", "media"}
|
||||
# Filtrer les nouveaux éléments (fichiers ou dossiers)
|
||||
new_items = [item for item in (after_items - before_items)
|
||||
if item not in ignore_names and not item.endswith('.psvita')]
|
||||
|
||||
if not new_items:
|
||||
logger.warning("PSVita: Aucun nouveau dossier détecté après extraction")
|
||||
return True, None
|
||||
|
||||
if not extracted_basename:
|
||||
extracted_basename = new_items[0] if new_items else "game"
|
||||
|
||||
# Chercher le dossier du jeu (normalement il n'y en a qu'un)
|
||||
game_folder = None
|
||||
for item in new_items:
|
||||
item_path = os.path.join(dest_dir, item)
|
||||
if os.path.isdir(item_path):
|
||||
game_folder = item
|
||||
game_folder_path = item_path
|
||||
break
|
||||
|
||||
if not game_folder:
|
||||
logger.error("PSVita: Aucun dossier de jeu trouvé après extraction")
|
||||
return False, "PSVita: Aucun dossier de jeu trouvé"
|
||||
|
||||
logger.debug(f"PSVita: Dossier de jeu trouvé: {game_folder}")
|
||||
|
||||
# Chercher le fichier ZIP à l'intérieur (IDJeu.zip)
|
||||
try:
|
||||
contents = os.listdir(game_folder_path)
|
||||
zip_files = [f for f in contents if f.lower().endswith('.zip')]
|
||||
|
||||
if not zip_files:
|
||||
logger.error(f"PSVita: Aucun fichier ZIP trouvé dans {game_folder}")
|
||||
return False, f"PSVita: Aucun ZIP trouvé dans {game_folder}"
|
||||
|
||||
# Prendre le premier ZIP trouvé
|
||||
zip_filename = zip_files[0]
|
||||
zip_path = os.path.join(game_folder_path, zip_filename)
|
||||
|
||||
# Extraire l'ID du jeu (nom du ZIP sans extension)
|
||||
game_id = os.path.splitext(zip_filename)[0]
|
||||
logger.debug(f"PSVita: ZIP trouvé: {zip_filename}, ID du jeu: {game_id}")
|
||||
|
||||
# 1. Créer le fichier .psvita dans dest_dir
|
||||
psvita_filename = f"{game_folder} [{game_id}].psvita"
|
||||
psvita_file_path = os.path.join(dest_dir, psvita_filename)
|
||||
|
||||
try:
|
||||
# Créer un fichier vide .psvita
|
||||
with open(psvita_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# PSVita Game\n")
|
||||
f.write(f"# Game: {game_folder}\n")
|
||||
f.write(f"# ID: {game_id}\n")
|
||||
logger.info(f"PSVita: Fichier .psvita créé: {psvita_filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"PSVita: Erreur création fichier .psvita: {e}")
|
||||
return False, f"Erreur création {psvita_filename}: {e}"
|
||||
|
||||
# 2. Extraire le ZIP dans le dossier parent de config.SAVE_FOLDER/psvita/ux0/app/
|
||||
save_parent2 = os.path.dirname(config.SAVE_FOLDER)
|
||||
save_parent = os.path.dirname(save_parent2)
|
||||
ux0_app_dir = os.path.join(save_parent, "psvita", "ux0", "app")
|
||||
os.makedirs(ux0_app_dir, exist_ok=True)
|
||||
|
||||
logger.debug(f"PSVita: Extraction de {zip_filename} dans {ux0_app_dir}")
|
||||
|
||||
try:
|
||||
import zipfile
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(ux0_app_dir)
|
||||
logger.info(f"PSVita: ZIP extrait avec succès dans {ux0_app_dir}")
|
||||
|
||||
# Vérifier que le dossier game_id existe bien
|
||||
game_id_path = os.path.join(ux0_app_dir, game_id)
|
||||
if not os.path.exists(game_id_path):
|
||||
logger.warning(f"PSVita: Le dossier {game_id} n'a pas été trouvé dans l'extraction")
|
||||
else:
|
||||
logger.info(f"PSVita: Dossier {game_id} confirmé dans ux0/app/")
|
||||
|
||||
except zipfile.BadZipFile as e:
|
||||
logger.error(f"PSVita: Fichier ZIP corrompu: {e}")
|
||||
return False, f"ZIP corrompu: {zip_filename}"
|
||||
except Exception as e:
|
||||
logger.error(f"PSVita: Erreur extraction ZIP: {e}")
|
||||
return False, f"Erreur extraction {zip_filename}: {e}"
|
||||
|
||||
# 3. Supprimer le dossier temporaire du jeu
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(game_folder_path)
|
||||
logger.info(f"PSVita: Dossier temporaire supprimé: {game_folder}")
|
||||
except Exception as e:
|
||||
logger.warning(f"PSVita: Impossible de supprimer {game_folder}: {e}")
|
||||
# Ne pas échouer pour ça, le jeu est quand même installé
|
||||
|
||||
logger.info(f"PSVita: Traitement terminé avec succès - {psvita_filename} créé, {game_id} installé dans ux0/app/")
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PSVita: Erreur générale: {e}", exc_info=True)
|
||||
return False, f"Erreur PSVita: {str(e)}"
|
||||
|
||||
|
||||
def handle_xbox(dest_dir, iso_files, url=None):
|
||||
"""Gère la conversion des fichiers Xbox extraits et met à jour l'UI (Converting)."""
|
||||
logger.debug(f"Traitement spécifique Xbox dans: {dest_dir}")
|
||||
@@ -2202,24 +2738,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
|
||||
|
||||
@@ -2253,6 +2790,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
|
||||
@@ -2260,6 +2799,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
|
||||
}
|
||||
@@ -2268,10 +2808,125 @@ def load_api_keys(force: bool = False):
|
||||
return {
|
||||
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
||||
'debridlink': getattr(config, 'API_KEY_DEBRIDLINK', ''),
|
||||
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
|
||||
'reloaded': False
|
||||
}
|
||||
|
||||
|
||||
def load_archive_org_cookie(force: bool = False) -> str:
|
||||
"""Charge le cookie Archive.org depuis un fichier texte.
|
||||
|
||||
- Fichier: config.ARCHIVE_ORG_COOKIE_PATH
|
||||
- Accepte soit une ligne brute de cookie, soit une ligne "Cookie: ..."
|
||||
- Utilise un cache mtime pour éviter les relectures
|
||||
"""
|
||||
try:
|
||||
path = getattr(config, 'ARCHIVE_ORG_COOKIE_PATH', '')
|
||||
if not path:
|
||||
return ""
|
||||
cache_attr = '_archive_cookie_cache'
|
||||
if not hasattr(config, cache_attr):
|
||||
setattr(config, cache_attr, {'mtime': None, 'value': ''})
|
||||
cache_data = getattr(config, cache_attr)
|
||||
|
||||
# Créer le fichier vide si absent
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write("")
|
||||
except Exception as ce:
|
||||
logger.error(f"Impossible de préparer le fichier cookie archive.org: {ce}")
|
||||
return ""
|
||||
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
except Exception:
|
||||
mtime = None
|
||||
|
||||
if force or (mtime is not None and mtime != cache_data.get('mtime')):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
value = f.read().strip()
|
||||
except Exception as re:
|
||||
logger.error(f"Erreur lecture cookie archive.org: {re}")
|
||||
value = ""
|
||||
|
||||
if value.lower().startswith("cookie:"):
|
||||
value = value.split(":", 1)[1].strip()
|
||||
|
||||
cache_data['mtime'] = mtime
|
||||
cache_data['value'] = value
|
||||
|
||||
return cache_data.get('value', '') or ""
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur load_archive_org_cookie: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def save_api_keys(api_keys: dict):
|
||||
"""Sauvegarde les clés API (1fichier, AllDebrid, Debrid-Link, RealDebrid) dans leurs fichiers respectifs.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
if not api_keys:
|
||||
return False
|
||||
|
||||
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', ''),
|
||||
}
|
||||
|
||||
saved_any = False
|
||||
|
||||
for key_name, path in paths.items():
|
||||
if not path:
|
||||
continue
|
||||
|
||||
# Récupérer la valeur (utiliser la clé telle quelle ou en minuscule)
|
||||
value = api_keys.get(key_name, api_keys.get(key_name.lower(), None))
|
||||
if value is None:
|
||||
continue # Ne pas modifier si la clé n'est pas fournie
|
||||
|
||||
try:
|
||||
# Créer le dossier si nécessaire
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
# Écrire la clé (valeur nettoyée)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(value.strip())
|
||||
|
||||
# Mettre à jour le cache config
|
||||
if key_name == '1fichier':
|
||||
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()
|
||||
|
||||
# Invalider le cache mtime
|
||||
cache_attr = '_api_keys_cache'
|
||||
if hasattr(config, cache_attr):
|
||||
cache_data = getattr(config, cache_attr)
|
||||
cache_data[f"{key_name}_mtime"] = None
|
||||
|
||||
saved_any = True
|
||||
logger.info(f"Clé API {key_name} sauvegardée avec succès")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur sauvegarde clé {key_name}: {e}")
|
||||
|
||||
return saved_any
|
||||
|
||||
|
||||
# Wrappers rétro-compatibilité (dépréciés)
|
||||
def load_api_key_1fichier(force: bool = False): # pragma: no cover
|
||||
return load_api_keys(force).get('1fichier', '')
|
||||
@@ -2279,6 +2934,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', '')
|
||||
|
||||
@@ -2291,19 +2949,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."""
|
||||
@@ -2311,6 +2969,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')),
|
||||
}
|
||||
|
||||
|
||||
BIN
snes/3-ji no Wide Shou - 5 Gatsugou (Japan) (Magazine)[!].bs
Normal file
BIN
snes/3-ji no Wide Shou - 5 Gatsugou (Japan) (Magazine)[!].bs
Normal file
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.3.2.6"
|
||||
"version": "2.6.0.1"
|
||||
}
|
||||
@@ -1,149 +1,387 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: Fichier de log
|
||||
if not exist "%CD%\logs" MD "%CD%\logs"
|
||||
set "LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt"
|
||||
:: Fichier de log (chemin absolu pour fiabilité)
|
||||
:: Détecter la racine (ROOT_DIR) d'abord pour construire un chemin stable
|
||||
set CURRENT_DIR=%CD%
|
||||
pushd "%CURRENT_DIR%\..\.."
|
||||
set "ROOT_DIR=%CD%"
|
||||
popd
|
||||
if not exist "%ROOT_DIR%\roms\windows\logs" MD "%ROOT_DIR%\roms\windows\logs"
|
||||
set "LOG_FILE=%ROOT_DIR%\roms\windows\logs\Retrobat_RGSX_log.txt"
|
||||
:: =============================================================================
|
||||
:: RGSX Retrobat Launcher v1.3
|
||||
:: =============================================================================
|
||||
:: Usage: "RGSX Retrobat.bat" [options]
|
||||
:: --display=N Launch on display N (0=primary, 1=secondary, etc.)
|
||||
:: --windowed Launch in windowed mode instead of fullscreen
|
||||
:: --help Show this help
|
||||
:: =============================================================================
|
||||
|
||||
:: Ajouter un horodatage au début du log
|
||||
echo [%DATE% %TIME%] Script start >> "%LOG_FILE%"
|
||||
:: Configuration des couleurs (codes ANSI)
|
||||
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
|
||||
set "ESC=%%b"
|
||||
)
|
||||
|
||||
:: Afficher un message de démarrage
|
||||
:: Couleurs
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "RED=[91m"
|
||||
set "CYAN=[96m"
|
||||
set "RESET=[0m"
|
||||
set "BOLD=[1m"
|
||||
|
||||
:: =============================================================================
|
||||
:: Traitement des arguments
|
||||
:: =============================================================================
|
||||
set "DISPLAY_NUM="
|
||||
set "WINDOWED_MODE="
|
||||
set "CONFIG_FILE="
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto :args_done
|
||||
if /i "%~1"=="--help" goto :show_help
|
||||
if /i "%~1"=="-h" goto :show_help
|
||||
if /i "%~1"=="--windowed" (
|
||||
set "WINDOWED_MODE=1"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
:: Check for --display=N format
|
||||
echo %~1 | findstr /r "^--display=" >nul
|
||||
if !ERRORLEVEL! EQU 0 (
|
||||
for /f "tokens=2 delims==" %%a in ("%~1") do set "DISPLAY_NUM=%%a"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
shift
|
||||
goto :parse_args
|
||||
|
||||
:show_help
|
||||
echo.
|
||||
echo %ESC%%CYAN%RGSX Retrobat Launcher - Help%ESC%%RESET%
|
||||
echo.
|
||||
echo Usage: "RGSX Retrobat.bat" [options]
|
||||
echo.
|
||||
echo Options:
|
||||
echo --display=N Launch on display N (0=primary, 1=secondary, etc.)
|
||||
echo --windowed Launch in windowed mode instead of fullscreen
|
||||
echo --help, -h Show this help
|
||||
echo.
|
||||
echo Examples:
|
||||
echo "RGSX Retrobat.bat" Launch on primary display
|
||||
echo "RGSX Retrobat.bat" --display=1 Launch on secondary display (TV)
|
||||
echo "RGSX Retrobat.bat" --windowed Launch in windowed mode
|
||||
echo.
|
||||
echo You can also create shortcuts with different display settings.
|
||||
echo.
|
||||
pause
|
||||
exit /b 0
|
||||
|
||||
:args_done
|
||||
|
||||
:: URL de telechargement Python
|
||||
set "PYTHON_ZIP_URL=https://github.com/RetroGameSets/RGSX/raw/main/windows/python.zip"
|
||||
|
||||
:: Obtenir le chemin du script de maniere fiable
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||
|
||||
:: Detecter le repertoire racine
|
||||
for %%I in ("%SCRIPT_DIR%\..\.." ) do set "ROOT_DIR=%%~fI"
|
||||
|
||||
:: Configuration des logs
|
||||
set "LOG_DIR=%ROOT_DIR%\roms\windows\logs"
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
set "LOG_FILE=%LOG_DIR%\Retrobat_RGSX_log.txt"
|
||||
set "LOG_BACKUP=%LOG_DIR%\Retrobat_RGSX_log.old.txt"
|
||||
|
||||
:: Rotation des logs avec backup
|
||||
if exist "%LOG_FILE%" (
|
||||
for %%A in ("%LOG_FILE%") do (
|
||||
if %%~zA GTR 100000 (
|
||||
if exist "%LOG_BACKUP%" del /q "%LOG_BACKUP%"
|
||||
move /y "%LOG_FILE%" "%LOG_BACKUP%" >nul 2>&1
|
||||
echo [%DATE% %TIME%] Log rotated - previous log saved as .old.txt > "%LOG_FILE%"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: =============================================================================
|
||||
:: Ecran d'accueil
|
||||
:: =============================================================================
|
||||
cls
|
||||
echo Running __main__.py for RetroBat...
|
||||
echo [%DATE% %TIME%] Running __main__.py for RetroBat >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo %ESC%%CYAN% ____ ____ ______ __ %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^| _ \ / ___^/ ___\ \/ / %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^| ^|_) ^| ^| _\___ \\ / %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^| _ ^<^| ^|_^| ^|___) / \ %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^|_^| \_\\____^|____/_/\_\ %ESC%%RESET%
|
||||
echo.
|
||||
echo %ESC%%BOLD% RetroBat Launcher v1.3%ESC%%RESET%
|
||||
echo --------------------------------
|
||||
if "!DISPLAY_NUM!" NEQ "0" (
|
||||
echo %ESC%%CYAN%Display: !DISPLAY_NUM!%ESC%%RESET%
|
||||
)
|
||||
if "!WINDOWED_MODE!"=="1" (
|
||||
echo %ESC%%CYAN%Mode: Windowed%ESC%%RESET%
|
||||
)
|
||||
echo.
|
||||
|
||||
:: Définir les chemins relatifs et les convertir en absolus
|
||||
set CURRENT_DIR=%CD%
|
||||
set PYTHON_EXE=python.exe
|
||||
:: Debut du log
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX Launcher v1.3 started >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Display: !DISPLAY_NUM!, Windowed: !WINDOWED_MODE! >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
|
||||
:: Détecter le répertoire racine en remontant de deux niveaux depuis le script
|
||||
pushd "%CURRENT_DIR%\..\.."
|
||||
set "ROOT_DIR=%CD%"
|
||||
popd
|
||||
|
||||
:: Définir le chemin du script principal selon les spécifications
|
||||
:: Configuration des chemins
|
||||
set "PYTHON_DIR=%ROOT_DIR%\system\tools\Python"
|
||||
set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
|
||||
set "MAIN_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\__main__.py"
|
||||
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
|
||||
|
||||
:: Definir le chemin du script de mise à jour de la gamelist Windows
|
||||
set "UPDATE_GAMELIST_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\update_gamelist_windows.py"
|
||||
:: Exporter RGSX_ROOT pour le script Python
|
||||
set "RGSX_ROOT=%ROOT_DIR%"
|
||||
|
||||
:: Convertir les chemins relatifs en absolus avec pushd/popd
|
||||
pushd "%ROOT_DIR%\system\tools\Python"
|
||||
set "PYTHON_EXE_FULL=%ROOT_DIR%\system\tools\Python\!PYTHON_EXE!"
|
||||
set "PYTHONW_EXE_FULL=%ROOT_DIR%\system\tools\Python\pythonw.exe"
|
||||
popd
|
||||
:: Logger les chemins
|
||||
echo [%DATE% %TIME%] System info: >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ROOT_DIR: %ROOT_DIR% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] PYTHON_EXE: %PYTHON_EXE% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] MAIN_SCRIPT: %MAIN_SCRIPT% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_ROOT: %RGSX_ROOT% >> "%LOG_FILE%"
|
||||
|
||||
:: Afficher et logger les variables
|
||||
:: =============================================================================
|
||||
:: Verification Python
|
||||
:: =============================================================================
|
||||
echo %ESC%%YELLOW%[1/3]%ESC%%RESET% Checking Python environment...
|
||||
echo [%DATE% %TIME%] Step 1/3: Checking Python >> "%LOG_FILE%"
|
||||
|
||||
echo ROOT_DIR : %ROOT_DIR% >> "%LOG_FILE%"
|
||||
echo CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%"
|
||||
echo ROOT_DIR : !ROOT_DIR! >> "%LOG_FILE%"
|
||||
echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
echo MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
echo UPDATE_GAMELIST_SCRIPT : !UPDATE_GAMELIST_SCRIPT! >> "%LOG_FILE%"
|
||||
|
||||
:: Vérifier si l'exécutable Python existe
|
||||
echo Checking python.exe...
|
||||
echo [%DATE% %TIME%] Checking python.exe at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
if not exist "!PYTHON_EXE_FULL!" (
|
||||
echo python.exe not found in system/tools. Preparing to extract..
|
||||
echo [%DATE% %TIME%] python.exe not found in system/tools. Preparing to extract.. >> "%LOG_FILE%"
|
||||
if not exist "%PYTHON_EXE%" (
|
||||
echo %ESC%%YELLOW%^> Python not found, installing...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Python not found, starting installation >> "%LOG_FILE%"
|
||||
|
||||
:: Créer le dossier Python s'il n'existe pas
|
||||
set "TOOLS_FOLDER_FULL=!ROOT_DIR!\system\tools"
|
||||
|
||||
if not exist "!TOOLS_FOLDER_FULL!\Python" (
|
||||
echo Creating folder !TOOLS_FOLDER_FULL!\Python...
|
||||
echo [%DATE% %TIME%] Creating folder !TOOLS_FOLDER_FULL!\Python... >> "%LOG_FILE%"
|
||||
mkdir "!TOOLS_FOLDER_FULL!\Python"
|
||||
:: Creer le dossier Python
|
||||
if not exist "%PYTHON_DIR%" (
|
||||
mkdir "%PYTHON_DIR%" 2>nul
|
||||
echo [%DATE% %TIME%] Created folder: %PYTHON_DIR% >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
|
||||
echo Extracting ZIP_FILE : !ZIP_FILE! in /system/tools/Python
|
||||
echo [%DATE% %TIME%] ZIP_FILE : !ZIP_FILE! >> "%LOG_FILE%"
|
||||
|
||||
if exist "!ZIP_FILE!" (
|
||||
echo [%DATE% %TIME%] Extracting python.zip to !TOOLS_FOLDER_FULL!... >> "%LOG_FILE%"
|
||||
tar -xf "!ZIP_FILE!" -C "!TOOLS_FOLDER_FULL!\Python" --strip-components=0
|
||||
echo Extraction finished.
|
||||
echo [%DATE% %TIME%] Extraction finished. >> "%LOG_FILE%"
|
||||
del /s /q "!ZIP_FILE!"
|
||||
echo python.zip file deleted.
|
||||
echo [%DATE% %TIME%] python.zip file deleted. >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo Error: Error python.zip not found please download it from github and put in /roms/windows folder.
|
||||
echo [%DATE% %TIME%] Error: Error python.zip not found please download it from github and put in /roms/windows folder >> "%LOG_FILE%"
|
||||
:: Verifier si le ZIP existe, sinon le telecharger
|
||||
if not exist "%ZIP_FILE%" (
|
||||
echo %ESC%%YELLOW%^> python.zip not found, downloading from GitHub...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] python.zip not found, attempting download >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Download URL: %PYTHON_ZIP_URL% >> "%LOG_FILE%"
|
||||
|
||||
:: Verifier si curl est disponible
|
||||
where curl.exe >nul 2>&1
|
||||
if !ERRORLEVEL! EQU 0 (
|
||||
echo %ESC%%CYAN%^> Using curl to download...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Using curl.exe for download >> "%LOG_FILE%"
|
||||
curl.exe -L -# -o "%ZIP_FILE%" "%PYTHON_ZIP_URL%"
|
||||
set DOWNLOAD_RESULT=!ERRORLEVEL!
|
||||
) else (
|
||||
:: Fallback sur PowerShell
|
||||
echo %ESC%%CYAN%^> Using PowerShell to download...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] curl not found, using PowerShell >> "%LOG_FILE%"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri '%PYTHON_ZIP_URL%' -OutFile '%ZIP_FILE%'"
|
||||
set DOWNLOAD_RESULT=!ERRORLEVEL!
|
||||
)
|
||||
|
||||
:: Verifier le resultat du telechargement
|
||||
if !DOWNLOAD_RESULT! NEQ 0 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Download failed!%ESC%%RESET%
|
||||
echo.
|
||||
echo Please download python.zip manually from:
|
||||
echo %ESC%%CYAN%%PYTHON_ZIP_URL%%ESC%%RESET%
|
||||
echo.
|
||||
echo And place it in:
|
||||
echo %ESC%%CYAN%%ROOT_DIR%\roms\windows\%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: Download failed with code !DOWNLOAD_RESULT! >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:: Verifier que le fichier a bien ete telecharge et n'est pas vide
|
||||
if not exist "%ZIP_FILE%" (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Download failed - file not created!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: ZIP file not created after download >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:: Verifier la taille du fichier (doit etre > 1MB pour etre valide)
|
||||
for %%A in ("%ZIP_FILE%") do set ZIP_SIZE=%%~zA
|
||||
if !ZIP_SIZE! LSS 1000000 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Downloaded file appears invalid ^(too small^)!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: Downloaded file too small: !ZIP_SIZE! bytes >> "%LOG_FILE%"
|
||||
del /q "%ZIP_FILE%" 2>nul
|
||||
goto :error
|
||||
)
|
||||
|
||||
echo %ESC%%GREEN%^> Download complete ^(!ZIP_SIZE! bytes^)%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Download successful: !ZIP_SIZE! bytes >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
:: Verifier que tar existe (Windows 10 1803+)
|
||||
where tar >nul 2>&1
|
||||
if !ERRORLEVEL! NEQ 0 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: tar command not available!%ESC%%RESET%
|
||||
echo.
|
||||
echo Please update Windows 10 or extract manually to:
|
||||
echo %ESC%%CYAN%%PYTHON_DIR%%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: tar command not found >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:: Vérifier à nouveau si python.exe existe après extraction
|
||||
if not exist "!PYTHON_EXE_FULL!" (
|
||||
echo Error: python.exe not found after extraction at !PYTHON_EXE_FULL!.
|
||||
echo [%DATE% %TIME%] Error: python.exe not found after extraction at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
:: Extraction avec progression simulee
|
||||
echo %ESC%%YELLOW%^> Extracting Python...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Extracting python.zip >> "%LOG_FILE%"
|
||||
|
||||
<nul set /p "= ["
|
||||
tar -xf "%ZIP_FILE%" -C "%PYTHON_DIR%" --strip-components=0
|
||||
set TAR_RESULT=!ERRORLEVEL!
|
||||
echo %ESC%%GREEN%##########%ESC%%RESET%] Done
|
||||
|
||||
if !TAR_RESULT! NEQ 0 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Extraction failed!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: tar extraction failed with code !TAR_RESULT! >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
echo [%DATE% %TIME%] Extraction completed >> "%LOG_FILE%"
|
||||
|
||||
:: Supprimer ZIP
|
||||
del /q "%ZIP_FILE%" 2>nul
|
||||
echo %ESC%%GREEN%^> python.zip cleaned up%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] python.zip deleted >> "%LOG_FILE%"
|
||||
|
||||
:: Verifier installation
|
||||
if not exist "%PYTHON_EXE%" (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Python not found after extraction!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: python.exe not found after extraction >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
)
|
||||
echo python.exe found.
|
||||
echo [%DATE% %TIME%] python.exe found. >> "%LOG_FILE%"
|
||||
|
||||
:: Vérifier si le script Python existe
|
||||
echo Checking __main__.py...
|
||||
echo [%DATE% %TIME%] Checking __main__.py at !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
if not exist "!MAIN_SCRIPT!" (
|
||||
echo Error: __main__.py not found at !MAIN_SCRIPT!.
|
||||
echo [%DATE% %TIME%] Error: __main__.py not found at !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
:: Afficher et logger la version Python
|
||||
for /f "tokens=*" %%v in ('"%PYTHON_EXE%" --version 2^>^&1') do set "PYTHON_VERSION=%%v"
|
||||
echo %ESC%%GREEN%^> %PYTHON_VERSION% found%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] %PYTHON_VERSION% detected >> "%LOG_FILE%"
|
||||
|
||||
:: =============================================================================
|
||||
:: Verification script principal
|
||||
:: =============================================================================
|
||||
echo %ESC%%YELLOW%[2/3]%ESC%%RESET% Checking RGSX application...
|
||||
echo [%DATE% %TIME%] Step 2/3: Checking RGSX files >> "%LOG_FILE%"
|
||||
|
||||
if not exist "%MAIN_SCRIPT%" (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: __main__.py not found!%ESC%%RESET%
|
||||
echo.
|
||||
echo Expected location:
|
||||
echo %ESC%%CYAN%%MAIN_SCRIPT%%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: __main__.py not found at %MAIN_SCRIPT% >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
echo __main__.py found.
|
||||
echo [%DATE% %TIME%] __main__.py found. >> "%LOG_FILE%"
|
||||
|
||||
:: L'étape de mise à jour de la gamelist est désormais appelée depuis __main__.py
|
||||
echo [%DATE% %TIME%] Skipping external gamelist update (handled in app). >> "%LOG_FILE%"
|
||||
echo %ESC%%GREEN%^> RGSX files OK%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] RGSX files verified >> "%LOG_FILE%"
|
||||
|
||||
echo Launching __main__.py (attached)...
|
||||
echo [%DATE% %TIME%] Preparing to launch main. >> "%LOG_FILE%"
|
||||
:: =============================================================================
|
||||
:: Lancement
|
||||
:: =============================================================================
|
||||
echo %ESC%%YELLOW%[3/3]%ESC%%RESET% Launching RGSX...
|
||||
echo [%DATE% %TIME%] Step 3/3: Launching application >> "%LOG_FILE%"
|
||||
|
||||
:: Assurer le bon dossier de travail pour l'application
|
||||
:: Changer le repertoire de travail
|
||||
cd /d "%ROOT_DIR%\roms\ports\RGSX"
|
||||
echo [%DATE% %TIME%] Working directory: %CD% >> "%LOG_FILE%"
|
||||
|
||||
:: Forcer les drivers SDL côté Windows et réduire le bruit console
|
||||
:: Configuration SDL/Pygame
|
||||
set PYGAME_HIDE_SUPPORT_PROMPT=1
|
||||
set SDL_VIDEODRIVER=windows
|
||||
set SDL_AUDIODRIVER=directsound
|
||||
echo [%DATE% %TIME%] CWD before launch: %CD% >> "%LOG_FILE%"
|
||||
set PYTHONWARNINGS=ignore::UserWarning:pygame.pkgdata
|
||||
set PYTHONIOENCODING=utf-8
|
||||
|
||||
:: Lancer l'application dans la même console et attendre sa fin
|
||||
:: Forcer python.exe pour capturer la sortie
|
||||
set "PY_MAIN_EXE=!PYTHON_EXE_FULL!"
|
||||
echo [%DATE% %TIME%] Using interpreter: !PY_MAIN_EXE! >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Launching "!MAIN_SCRIPT!" now... >> "%LOG_FILE%"
|
||||
"!PY_MAIN_EXE!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1
|
||||
set EXITCODE=!ERRORLEVEL!
|
||||
echo [%DATE% %TIME%] __main__.py exit code: !EXITCODE! >> "%LOG_FILE%"
|
||||
if "!EXITCODE!"=="0" (
|
||||
echo Execution finished successfully.
|
||||
echo [%DATE% %TIME%] Execution of __main__.py finished successfully. >> "%LOG_FILE%"
|
||||
:: =============================================================================
|
||||
:: Configuration multi-ecran
|
||||
:: =============================================================================
|
||||
:: SDL_VIDEO_FULLSCREEN_HEAD: Selectionne l'ecran pour le mode plein ecran
|
||||
:: 0 = ecran principal, 1 = ecran secondaire, etc.
|
||||
:: Ces variables ne sont definies que si --display=N ou --windowed est passe
|
||||
:: Sinon, le script Python utilisera les parametres de rgsx_settings.json
|
||||
|
||||
echo [%DATE% %TIME%] Display configuration: >> "%LOG_FILE%"
|
||||
if defined DISPLAY_NUM (
|
||||
set SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM!
|
||||
set RGSX_DISPLAY=!DISPLAY_NUM!
|
||||
echo [%DATE% %TIME%] SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_DISPLAY=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo Error: Failed to execute __main__.py (code !EXITCODE!).
|
||||
echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code !EXITCODE!. >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Display: using rgsx_settings.json config >> "%LOG_FILE%"
|
||||
)
|
||||
if defined WINDOWED_MODE (
|
||||
set RGSX_WINDOWED=!WINDOWED_MODE!
|
||||
echo [%DATE% %TIME%] RGSX_WINDOWED=!WINDOWED_MODE! ^(from --windowed arg^) >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo [%DATE% %TIME%] Windowed: using rgsx_settings.json config >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
:: Log environnement
|
||||
echo [%DATE% %TIME%] Environment variables set: >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_ROOT=%RGSX_ROOT% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] SDL_VIDEODRIVER=%SDL_VIDEODRIVER% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] SDL_AUDIODRIVER=%SDL_AUDIODRIVER% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] PYTHONIOENCODING=%PYTHONIOENCODING% >> "%LOG_FILE%"
|
||||
|
||||
echo.
|
||||
if defined DISPLAY_NUM (
|
||||
echo %ESC%%CYAN%Launching on display !DISPLAY_NUM!...%ESC%%RESET%
|
||||
)
|
||||
if defined WINDOWED_MODE (
|
||||
echo %ESC%%CYAN%Windowed mode enabled%ESC%%RESET%
|
||||
)
|
||||
echo %ESC%%CYAN%Starting RGSX application...%ESC%%RESET%
|
||||
echo %ESC%%BOLD%Press Ctrl+C to force quit if needed%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] Executing: "%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] --- Application output start --- >> "%LOG_FILE%"
|
||||
|
||||
"%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%" 2>&1
|
||||
set EXITCODE=!ERRORLEVEL!
|
||||
|
||||
echo [%DATE% %TIME%] --- Application output end --- >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Exit code: !EXITCODE! >> "%LOG_FILE%"
|
||||
|
||||
if "!EXITCODE!"=="0" (
|
||||
echo.
|
||||
echo %ESC%%GREEN%RGSX closed successfully.%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] Application closed successfully >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo.
|
||||
echo %ESC%%RED%RGSX exited with error code !EXITCODE!%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: Application exited with code !EXITCODE! >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:end
|
||||
echo Task completed.
|
||||
echo [%DATE% %TIME%] Task completed successfully. >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Session ended normally >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
timeout /t 2 >nul
|
||||
exit /b 0
|
||||
|
||||
:error
|
||||
echo An error occurred.
|
||||
echo [%DATE% %TIME%] An error occurred. >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo %ESC%%RED%An error occurred. Check the log file:%ESC%%RESET%
|
||||
echo %ESC%%CYAN%%LOG_FILE%%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Session ended with errors >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo Press any key to close...
|
||||
pause >nul
|
||||
exit /b 1
|
||||
Reference in New Issue
Block a user