Compare commits

..

46 Commits

Author SHA1 Message Date
skymike03
2f437c1aa4 v2.6.0.2 (2025.03.17)
- Add support and donation information to release notes
- Add normalize game name for roms scanning (ie. Game (USA).ext will be shown owned for a rom named only "Game.ext"
2026-03-17 23:12:50 +01:00
skymike03
054b174c18 v2.6.0.1 (2025.03.16)
• add Debrid-Link API key support in desktop and web settings
• add Debrid-Link fallback for premium link generation in addition to AllDebrid and RealDebrid
2026-03-16 20:28:19 +01:00
skymike03
fbb1a2aa68 v2.5.0.7 (2026.03.16)
• improve filters/search performance with lazy cache without slowing unfiltered game list access
• fix fbneo logging traceback and avoid re-downloading/parsing fbneo gamelist when cache is already available
• add rom scan to rebuild downloaded games database from local files with extension-insensitive matching and backward compatibility
• fix clear history to remove stale converting entries and keep only real active transfers
• fix keyboard mode controls display to ignore joystick-only mappings and restore proper keyboard labels
• update displayed keyboard labels with ascii-safe names (Esc/Echap, Debut, Verr Def)
• move version/controller info to a top-right header badge and add page number badge on top-left
• simplify platform footer and loading footer layout
• add global search from platform menu across all available systems
• fix global search input handling and routing
• update global search confirm action to download directly and allow queue action from results
• add size and ext columns to global search results
• add ext column to game list and history
• add folder column to history to show download destination folder
• fix return from history to restore current game list instead of jumping back to platform list
• fix history crash caused by ext column text truncation
2026-03-16 19:09:47 +01:00
skymike03
1dbc741617 v2.5.0.6 (2026.03.15)
update download status handling for converting state and ps3 dec function
2026-03-15 23:47:32 +01:00
skymike03
c4913a5fc2 v2.5.0.5 (2026.03.05)
Merge pull request [#48](https://github.com/RetroGameSets/RGSX/issues/48) from elieserdejesus

- Fixing and upgrade Filters and search
- showing fbneo 'full name' instead rom name.
2026-03-05 18:45:01 +01:00
RGS
bf9d3d2de5 Merge pull request #48 from elieserdejesus/filters
Fixing Filters
2026-03-05 15:37:33 +01:00
Elieser de Jesus
9979949bdc Applying filters using 'display_name'
Adding a Game class with a display_name used do show games. The 'display_name'
is the game file name without suffix (.zip, .7z, .bin, etc) and without platform
prefix. Many platforms line NES, mega drive was showing the plaftorm name as prefix.

Now the filters are working with the 'display_name'.

I see the filters are no applyed until the "apply" button is clicked. Now the filters
are applyed everytime the game list is showed.
2026-03-05 11:06:07 -03:00
Elieser de Jesus
9ed264544f adding bootleg filter 2026-03-05 08:40:33 -03:00
Elieser de Jesus
779c060927 removing duplicated entries 2026-03-05 08:36:51 -03:00
RGS
88400e538f Merge pull request #47 from elieserdejesus/main
Showing and filtering FBneo games using 'full name' instead 'rom name'
2026-03-05 09:19:12 +01:00
Elieser de Jesus
cbab067dd6 filtering fbneo games by full name instead rom name 2026-03-04 18:45:18 -03:00
Elieser de Jesus
b4ed0b355d showing fbneo 'full name' instead rom name.
The full names are downloaded from github fbneo
repo only when user select fbneo in platforms screen
2026-03-04 18:42:21 -03:00
skymike03
51ad08ff33 v2.5.0.4 (2026.02.15)
- add some new cool musics :P
2026-02-15 20:34:28 +01:00
skymike03
d6a5c4b27e Add type ignore comment for requests import in network.py 2026-02-08 18:13:03 +01:00
skymike03
2c7c3414a5 v2.5.0.3 (2026.02.08)
- add 7z support for extract games
- add cookie test for archive.org downloads (new romhacks platforms added)
2026-02-08 16:58:11 +01:00
skymike03
059d3988ac v2.5.0.2 (2026.02.06)
- add "versionclean" script to clean batocera version after activating custom rgsx service at boot . Thanks to the BUA project for the script.
- add encoding UTF-8 for retrobat launcher to avoid controller with non ASCII characters to crash rgsx (issue #43) thanks to Crover81
- add new menu in Menu>Settings to test connection to all usefull urls and logging
- some log trim
2026-02-06 16:26:40 +01:00
skymike03
50c9b9caad v2.5.0.1 (2026.01.27)
- add Nintendo/Xbox Layout in Menu>Controls>Controls Help to invert displayed buttons
2026-01-29 18:58:56 +01:00
skymike03
7d2d55fe5f v2.5.0.0 (2026.01.17)
- add "disable auto-extract" function in MENU>SETTINGS
- add  "ROMS folder" option to select a custom folder for all downloads in MENU>SETTINGS (or Web Interface settings)
- add new menu to choose custom download folder for a specific system (long press CONFIRM button on a selected system/platform)
- add pause option in history when downloading games
- update submenus display layout to have more space for new options
- add missing settings options in rsgx_web  (disable auto-extract, API keys, activate web service / custom dns at boot)
2026-01-17 00:54:10 +01:00
skymike03
14a5416d2d v2.5.0.0 (2026.01.17)
- add "disable auto-extract" function in MENU>SETTINGS
- add  "ROMS folder" option to select a custom folder for all downloads in MENU>SETTINGS (or Web Interface settings)
- add new menu to choose custom download folder for a specific system (long press CONFIRM button on a selected system/platform)
- add pause option in history when downloading games
- update submenus display layout to have more space for new options
- add missing settings options in rsgx_web  (disable auto-extract, API keys, activate web service / custom dns at boot)
2026-01-17 00:45:41 +01:00
skymike03
3193dc90f6 v2.4.2.0 (2026.01.15)
- add menu to choose custom download folder for a specific system (long press validate on a system/platform)
- add pause menu when downloading game
2026-01-14 23:19:58 +01:00
skymike03
b437f31854 v2.4.1.0 (2026.01.14)
- add gamelist update check at rgsx start to warn if you didn't update gamlist since few days
- add view for grid mode (Settings > Display)
- use submenu for fonts (Settings > Display)
- performance mode (Settings>Display) updated, now it runs faster without any effects
2026-01-14 21:32:14 +01:00
skymike03
08f3e64d2a v2.4.0.2 (2026.01.14)
- correct some bugs/errors on display and logging functionality; update language files with new options
2026-01-14 20:25:47 +01:00
RGS
4968af2da9 Update image source in README.md 2026-01-07 14:48:30 +01:00
skymike03
920914b374 v2.4.0.1 (2026.01.07 bis)
- remove windowed mode (useless)
2026-01-07 14:36:00 +01:00
skymike03
a326cb0b67 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2026-01-07 14:25:18 +01:00
skymike03
c9fdf93221 v2.4.0.0 (2026.01.07)
- add performance mode to disable some 3d effects for a better experience on low-end devices
- multi screen support (choose default screen in Pause>Display menu )
- update windows launcher for retrobat
- ignore some useless warnings
- Merge pull request [#34](https://github.com/RetroGameSets/RGSX/issues/34) from aaronson2012/start-menu-fix
(feat: enable circular navigation for pause menu options)
2026-01-07 14:25:15 +01:00
RGS
184a8c64fe Update troubleshooting steps for app crashes 2026-01-03 16:37:17 +01:00
RGS
9a2e4ce0db Merge pull request #34 from aaronson2012/start-menu-fix
feat: enable circular navigation for pause menu options
2025-12-06 13:06:31 +01:00
Jacob Christie
73eceeb777 feat: enable circular navigation for pause menu options 2025-12-06 05:26:16 -06:00
RGS
2fcc4ca6df Update README to include troubleshooting link
Added a link to the troubleshooting section in the README.
2025-11-25 19:54:03 +01:00
RGS
2ed889540b Enhance troubleshooting section in README
Updated troubleshooting solutions for controls and games visibility issues.
2025-11-25 19:50:20 +01:00
skymike03
e9a610b5dd v2.3.3.3
- Enhance download queue functionality to stop download, continue queue, remove games  and update related UI options
2025-11-25 19:21:12 +01:00
skymike03
bd3b885736 v2.3.3.2
- Fix French print statements for consistency in output messages
- improve filtering  in game list to be permanent, even in search mode, and more efficient for games that have foreign language on other region
2025-11-25 18:40:13 +01:00
skymike03
1592671ddc v2.3.3.1 (2025.11.24)
- correct menu control handling bug
- enhance UI elements for improved user experience
2025-11-24 22:31:53 +01:00
skymike03
4e029aabf1 Remove unnecessary files from RGSX package builds to streamline release process 2025-11-24 13:16:52 +01:00
skymike03
970fcaf197 Update installation instructions for clarity and add manual update notes 2025-11-24 13:14:30 +01:00
skymike03
ff30e6d297 v2.3.3.0 (2025.11.23)
- add a workaround to github update checking
2025-11-23 16:00:38 +01:00
skymike03
5c7fa0484f v2.3.2.9 (2025.11.23)
- Enhance UI with modern effects and improve PSVita game handling (auto extract and create .psvita file for batocera)
- add text file viewer for game txt informations (windows)
2025-11-23 01:25:15 +01:00
skymike03
814861e9ee - add text file viewer for game txt informations 2025-11-21 00:28:46 +01:00
skymike03
56c87ab05f v2.3.2.8 (2025.11.20)
- Improving virtual keyboard navigation when filtering game list (thanks elieserdejesus)
- web interface : Add modal for displaying support messages
- normalize sizes in bytes when not in french
- Refactor control navigation and improve button rendering in UI
2025-11-20 23:19:31 +01:00
skymike03
b12d645fbf Add support modal for displaying formatted support messages 2025-11-20 18:20:12 +01:00
skymike03
04e68adef0 Merge branch 'main' of https://github.com/RetroGameSets/RGSX 2025-11-20 18:02:29 +01:00
skymike03
52f2b960c2 Refactor control navigation and improve button rendering in UI 2025-11-20 18:02:26 +01:00
RGS
1ea604840e Merge pull request #33 from elieserdejesus/main
Improving virtual keyboard navigation when filtering game list to circular navigate
2025-11-20 17:37:59 +01:00
Elieser de Jesus
802696e78f Improving virtual keyboard navigation when filtering game list
The general idea is allow something like "circular buffer" logic when selecting a key in the virtual keyboard.

When the virtual keyboard is displayed:
 - If you are in the first line and press UP jump to last line
 - If you are in the last line and press DOWN jump to first line
 - If you are in the first col and press LEFT jump to last col
 - If you are in the last col and press RIGHT jump to first col
2025-11-20 12:55:30 -03:00
skymike03
6f17173a8c v2.3.2.7 (2025.11.19)
- BETA : add filtering options of games in RGSX main app / synced with options sets on web interface
Filter by Region, hide beta and demos, show only one rom per game and select prefered display order
2025-11-19 23:15:12 +01:00
53 changed files with 8447 additions and 1229 deletions

View File

@@ -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
View File

@@ -23,3 +23,4 @@ pygame/
data/
docker-compose.test.yml
config/
pyrightconfig.json

View File

@@ -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).

View File

@@ -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":

View 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

View File

@@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} Spiele)",
"game_filter": "Aktiver Filter: {0}",
"game_search": "Filtern: {0}",
"global_search_title": "Globale Suche: {0}",
"global_search_empty_query": "Geben Sie einen Namen ein, um alle Systeme zu durchsuchen",
"global_search_no_results": "Keine Ergebnisse fur: {0}",
"game_header_name": "Name",
"game_header_ext": "Ext",
"game_header_size": "Größe",
"history_title": "Downloads ({0})",
"history_empty": "Keine Downloads im Verlauf",
@@ -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"
}

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} games)",
"game_filter": "Active filter: {0}",
"game_search": "Filter: {0}",
"global_search_title": "Global search: {0}",
"global_search_empty_query": "Type a game name to search across all systems",
"global_search_no_results": "No results for: {0}",
"game_header_name": "Name",
"game_header_ext": "Ext",
"game_header_size": "Size",
"history_title": "Downloads ({0})",
"history_empty": "No downloads in history",
@@ -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"
}

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} juegos)",
"game_filter": "Filtro activo: {0}",
"game_search": "Filtrar: {0}",
"global_search_title": "Busqueda global: {0}",
"global_search_empty_query": "Escribe un nombre para buscar en todas las consolas",
"global_search_no_results": "Sin resultados para: {0}",
"game_header_name": "Nombre",
"game_header_ext": "Ext",
"game_header_size": "Tamaño",
"history_title": "Descargas ({0})",
"history_empty": "No hay descargas en el historial",
@@ -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"
}

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} jeux)",
"game_filter": "Filtre actif : {0}",
"game_search": "Filtrer : {0}",
"global_search_title": "Recherche globale : {0}",
"global_search_empty_query": "Saisissez un nom pour rechercher dans toutes les consoles",
"global_search_no_results": "Aucun resultat pour : {0}",
"game_header_name": "Nom",
"game_header_ext": "Ext",
"game_header_size": "Taille",
"history_title": "Téléchargements ({0})",
"history_empty": "Aucun téléchargement dans l'historique",
@@ -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"
}

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} giochi)",
"game_filter": "Filtro attivo: {0}",
"game_search": "Filtro: {0}",
"global_search_title": "Ricerca globale: {0}",
"global_search_empty_query": "Digita un nome per cercare in tutte le console",
"global_search_no_results": "Nessun risultato per: {0}",
"game_header_name": "Nome",
"game_header_ext": "Ext",
"game_header_size": "Dimensione",
"history_title": "Download ({0})",
"history_empty": "Nessun download nella cronologia",
@@ -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"
}

View File

@@ -24,7 +24,11 @@
"game_count": "{0} ({1} jogos)",
"game_filter": "Filtro ativo: {0}",
"game_search": "Filtro: {0}",
"global_search_title": "Busca global: {0}",
"global_search_empty_query": "Digite um nome para buscar em todos os consoles",
"global_search_no_results": "Nenhum resultado para: {0}",
"game_header_name": "Nome",
"game_header_ext": "Ext",
"game_header_size": "Tamanho",
"history_title": "Downloads ({0})",
"history_empty": "Nenhum download no histórico",
@@ -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"
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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;
}

View File

@@ -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) {

View File

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

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.2.6"
"version": "2.6.0.1"
}

View File

@@ -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