mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-03-19 08:16:49 +01:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7c3414a5 | ||
|
|
059d3988ac | ||
|
|
50c9b9caad | ||
|
|
7d2d55fe5f | ||
|
|
14a5416d2d | ||
|
|
3193dc90f6 | ||
|
|
b437f31854 | ||
|
|
08f3e64d2a | ||
|
|
4968af2da9 | ||
|
|
920914b374 | ||
|
|
a326cb0b67 | ||
|
|
c9fdf93221 | ||
|
|
184a8c64fe | ||
|
|
9a2e4ce0db | ||
|
|
73eceeb777 | ||
|
|
2fcc4ca6df | ||
|
|
2ed889540b | ||
|
|
e9a610b5dd | ||
|
|
bd3b885736 |
104
.github/workflows/release.yml
vendored
104
.github/workflows/release.yml
vendored
@@ -61,52 +61,78 @@ 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)
|
||||
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 , 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 obcure reason)
|
||||
|
||||
#### Batocera/Knulli/Retrobat
|
||||
1. Download latest update : https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_update_latest.zip
|
||||
2. Extract zip content on in `/userdata/roms/ports/RGSX`
|
||||
3. Launch RGSX
|
||||
|
||||
|
||||
### 📖 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
|
||||
|
||||
13
README.md
13
README.md
@@ -1,11 +1,11 @@
|
||||
# 🎮 Retro Game Sets Xtra (RGSX)
|
||||
|
||||
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)**
|
||||
**[Discord Support](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[French Documentation](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md)** • **[Troubleshoot / Common Errors](https://github.com/RetroGameSets/RGSX#%EF%B8%8F-troubleshooting)** •
|
||||
|
||||
A free, user-friendly ROM downloader for Batocera, Knulli, and RetroBat with multi-source support.
|
||||
|
||||
<p align="center">
|
||||
<img width="69%" alt="platform menu" src="https://github.com/user-attachments/assets/4464b57b-06a8-45e9-a411-cc12b421545a" />
|
||||
<img width="69%" alt="main" src="https://github.com/user-attachments/assets/a98f1189-9a50-4cc3-b588-3f85245640d8" />
|
||||
<img width="30%" alt="controls help" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
|
||||
</p>
|
||||
<p align="center">
|
||||
@@ -201,11 +201,12 @@ RGSX includes a web interface that launched automatically when using RGSX for re
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart |
|
||||
| Games not showing | Pause Menu > Games > Update Game Cache |
|
||||
| Download stuck | Check API keys in `/saves/ports/rgsx/` |
|
||||
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` |
|
||||
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart app, you can try delete /roms/ports/RGSX/assets/controls/xx.json too |
|
||||
| No games ? | Pause Menu > Games > Update Game Cache |
|
||||
| Missing systems on the list? | RGSX read es_systems.cfg to show only supported systems, if you want all systems : Pause Menu > Games > Show unsupported systems |
|
||||
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` or `/roms/windows/logs/Retrobat_RGSX_log.txt` |
|
||||
| Layout change not applied | Restart RGSX after changing layout |
|
||||
| Downloading BIOS file is ok but you can't download any games? | Activate custom DNS on Pause Menu> Settings and reboot , server can be blocked by your ISP. check any threat/website protection on your router too, especially on ASUS one|
|
||||
|
||||
**Need help?** Share logs from `/roms/ports/RGSX/logs/` on [Discord](https://discord.gg/Vph9jwg3VV).
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import os
|
||||
import platform
|
||||
import warnings
|
||||
|
||||
# Ignorer le warning de deprecation de pkg_resources dans pygame
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="pygame.pkgdata")
|
||||
warnings.filterwarnings("ignore", message="pkg_resources is deprecated")
|
||||
|
||||
# Ne pas forcer SDL_FBDEV ici; si déjà défini par l'environnement, on le garde
|
||||
try:
|
||||
if "SDL_FBDEV" in os.environ:
|
||||
@@ -28,7 +34,7 @@ from display import (
|
||||
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
|
||||
@@ -91,6 +97,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 +147,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 +185,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 +227,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 +284,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 +309,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 +351,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 +391,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
|
||||
@@ -438,6 +450,10 @@ async def main():
|
||||
# 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 = []
|
||||
@@ -473,7 +489,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
|
||||
@@ -567,6 +583,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:
|
||||
@@ -670,9 +704,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",
|
||||
@@ -683,11 +720,11 @@ 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", # Visualiseur de fichiers texte
|
||||
"text_file_viewer",
|
||||
# Menus filtrage avancé
|
||||
"filter_menu_choice",
|
||||
"filter_advanced",
|
||||
@@ -726,6 +763,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)
|
||||
@@ -1078,6 +1135,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))
|
||||
@@ -1087,6 +1150,9 @@ async def main():
|
||||
elif config.menu_state == "pause_api_keys_status":
|
||||
from display import draw_pause_api_keys_status
|
||||
draw_pause_api_keys_status(screen)
|
||||
elif config.menu_state == "pause_connection_status":
|
||||
from display import draw_pause_connection_status
|
||||
draw_pause_connection_status(screen)
|
||||
elif config.menu_state == "filter_platforms":
|
||||
from display import draw_filter_platforms_menu
|
||||
draw_filter_platforms_menu(screen)
|
||||
@@ -1134,6 +1200,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":
|
||||
@@ -1224,6 +1302,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")
|
||||
@@ -1253,6 +1332,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:
|
||||
@@ -1260,6 +1340,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'}
|
||||
@@ -1365,13 +1446,51 @@ async def main():
|
||||
config.loading_progress = 80.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Dossier Data non vide, passage à {loading_step}")
|
||||
continue # Passer immédiatement à load_sources
|
||||
elif loading_step == "load_sources":
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
sources = load_sources()
|
||||
config.menu_state = "platform"
|
||||
config.loading_progress = 100.0
|
||||
config.current_loading_system = ""
|
||||
|
||||
# Vérifier si une mise à jour de la liste des jeux est nécessaire (seulement si pas déjà demandé)
|
||||
if not config.gamelist_update_prompted:
|
||||
from rgsx_settings import get_last_gamelist_update
|
||||
from config import GAMELIST_UPDATE_DAYS
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
last_update = get_last_gamelist_update()
|
||||
should_prompt_update = False
|
||||
|
||||
if last_update is None:
|
||||
# Première utilisation, proposer la mise à jour
|
||||
logger.info("Première utilisation détectée, proposition de mise à jour de la liste des jeux")
|
||||
should_prompt_update = True
|
||||
else:
|
||||
try:
|
||||
last_update_date = datetime.strptime(last_update, "%Y-%m-%d")
|
||||
days_since_update = (datetime.now() - last_update_date).days
|
||||
logger.info(f"Dernière mise à jour de la liste des jeux: {last_update} ({days_since_update} jours)")
|
||||
|
||||
if days_since_update >= GAMELIST_UPDATE_DAYS:
|
||||
logger.info(f"Mise à jour de la liste des jeux recommandée (>{GAMELIST_UPDATE_DAYS} jours)")
|
||||
should_prompt_update = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification de la date de mise à jour: {e}")
|
||||
|
||||
if should_prompt_update:
|
||||
config.menu_state = "gamelist_update_prompt"
|
||||
config.gamelist_update_selection = 1 # 0=Non, 1=Oui (par défaut)
|
||||
config.gamelist_update_prompted = True # Marquer comme déjà demandé
|
||||
logger.debug("Affichage du prompt de mise à jour de la liste des jeux")
|
||||
else:
|
||||
config.menu_state = "platform"
|
||||
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
|
||||
else:
|
||||
config.menu_state = "platform"
|
||||
logger.debug(f"Prompt déjà affiché, passage à platform, progress={config.loading_progress}")
|
||||
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
|
||||
|
||||
# Gestion de l'état de transition
|
||||
if config.transition_state == "to_game":
|
||||
|
||||
1
ports/RGSX/assets/ArchiveOrgCookie.txt
Normal file
1
ports/RGSX/assets/ArchiveOrgCookie.txt
Normal file
@@ -0,0 +1 @@
|
||||
donation-identifier=39546f3b2d3f67a664818596d81a5bec; abtest-identifier=fee0e28eb6c8d0de147d19db4303ee84; logged-in-sig=1802098179%201770562179%20AKHN8aF4EsFeR%2FundhgQTu0j27ZdFZXmgyUiqnJvXq%2BwtDGVvapqhKUFhIlI9bXAMYLMHDRJoO76bsqXI662nrIsx58efihNrafdk285r8MAdotWx03usO30baYoNPoMMEaK8iuhtbfTEyfE7oTZwdO7wjxNUTm%2Bbjjm6kmUD3HSQRzPsc0oWrrnd8Wj2x3UiuZeRnBfC60OjJHcnKC2Xv7teS%2BBx3EdKAG1i739MxTzjtEfERWw83bnaV30827qaFhZ%2BDK3%2FwCGOUwtablPA%2B0EeLR9%2BoYeC6x5aaJMZHBMjBowSIEE4QAK9IG9haBsn7%2F1PCweYuLivMIZJeA7mA%3D%3D; logged-in-user=rgsx%40outlook.fr
|
||||
69
ports/RGSX/assets/progs/versionclean
Normal file
69
ports/RGSX/assets/progs/versionclean
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# BATOCERA SERVICE
|
||||
# name: Version Clean Service
|
||||
# description: Clean batocera-version output (hide extra services)
|
||||
# author: batocera-unofficial-addons
|
||||
# depends:
|
||||
# version: 1.0
|
||||
|
||||
SERVICE_NAME="versionclean"
|
||||
TARGET="/usr/bin/batocera-version"
|
||||
BACKUP="${TARGET}.bak"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
# If we've already backed up, assume it's already "cleaned"
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: already started (backup exists at ${BACKUP})."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${SERVICE_NAME}: backing up original ${TARGET} to ${BACKUP}..."
|
||||
cp "$TARGET" "$BACKUP"
|
||||
|
||||
echo "${SERVICE_NAME}: writing clean version script to ${TARGET}..."
|
||||
cat << 'EOF' > "$TARGET"
|
||||
#!/bin/bash
|
||||
|
||||
# Clean batocera-version
|
||||
# - "batocera-version --extra" -> "none"
|
||||
# - "batocera-version" -> contents of /usr/share/batocera/batocera.version
|
||||
|
||||
if test "$1" = "--extra"; then
|
||||
echo "none"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat /usr/share/batocera/batocera.version
|
||||
EOF
|
||||
|
||||
chmod +x "$TARGET"
|
||||
echo "${SERVICE_NAME}: clean version applied."
|
||||
;;
|
||||
|
||||
stop)
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: restoring original ${TARGET} from ${BACKUP}..."
|
||||
cp "$BACKUP" "$TARGET"
|
||||
rm "$BACKUP"
|
||||
echo "${SERVICE_NAME}: restore complete."
|
||||
else
|
||||
echo "${SERVICE_NAME}: no backup found, nothing to restore."
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
if [ -f "$BACKUP" ]; then
|
||||
echo "${SERVICE_NAME}: CLEAN VERSION ACTIVE (backup present)."
|
||||
else
|
||||
echo "${SERVICE_NAME}: ORIGINAL VERSION ACTIVE."
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
@@ -13,7 +14,10 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.3.3.1"
|
||||
app_version = "2.5.0.3"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 7
|
||||
|
||||
|
||||
def get_application_root():
|
||||
@@ -133,9 +137,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)
|
||||
@@ -175,6 +186,7 @@ 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_REALDEBRID_PATH = os.path.join(SAVE_FOLDER, "RealDebridAPI.txt")
|
||||
ARCHIVE_ORG_COOKIE_PATH = os.path.join(APP_FOLDER, "assets", "ArchiveOrgCookie.txt")
|
||||
|
||||
|
||||
|
||||
@@ -342,6 +354,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
|
||||
@@ -374,6 +387,7 @@ 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
|
||||
search_mode = False # Indicateur si le mode recherche est actif
|
||||
@@ -447,6 +461,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
@@ -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
@@ -69,30 +69,67 @@ class GameFilters:
|
||||
name = game_name.upper()
|
||||
regions = []
|
||||
|
||||
# Patterns de région communs
|
||||
if 'USA' in name or 'US)' in name:
|
||||
regions.append('USA')
|
||||
# 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:
|
||||
regions.append('Canada')
|
||||
if 'EUROPE' in name or 'EU)' in name:
|
||||
regions.append('Europe')
|
||||
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:
|
||||
regions.append('France')
|
||||
if 'France' not in regions:
|
||||
regions.append('France')
|
||||
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
|
||||
regions.append('Germany')
|
||||
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name:
|
||||
regions.append('Japan')
|
||||
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:
|
||||
regions.append('Korea')
|
||||
if 'Korea' not in regions:
|
||||
regions.append('Korea')
|
||||
if 'WORLD' in name:
|
||||
regions.append('World')
|
||||
if 'World' not in regions:
|
||||
regions.append('World')
|
||||
|
||||
# Autres régions
|
||||
if re.search(r'\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
|
||||
r'SPAIN|FRANCE|GERMANY|ITALY|CANADA)\b', name):
|
||||
if 'CANADA' in name:
|
||||
regions.append('Canada')
|
||||
else:
|
||||
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
|
||||
@@ -157,14 +194,22 @@ class GameFilters:
|
||||
|
||||
def get_region_priority(self, game_name: str) -> int:
|
||||
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
|
||||
name = game_name.upper()
|
||||
# Utiliser la fonction de détection de régions pour être cohérent
|
||||
game_regions = self.get_game_regions(game_name)
|
||||
|
||||
for i, region in enumerate(self.region_priority):
|
||||
region_upper = region.upper()
|
||||
if region_upper in name:
|
||||
return i
|
||||
# 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
|
||||
|
||||
return len(self.region_priority) # Autres régions (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[Tuple]) -> List[Tuple]:
|
||||
"""
|
||||
|
||||
@@ -44,6 +44,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 +52,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,11 +62,28 @@
|
||||
"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",
|
||||
@@ -169,7 +189,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",
|
||||
@@ -195,6 +222,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}",
|
||||
@@ -204,9 +234,21 @@
|
||||
"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren",
|
||||
"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",
|
||||
@@ -247,6 +289,10 @@
|
||||
"history_option_extract_archive": "Archiv extrahieren",
|
||||
"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",
|
||||
@@ -309,6 +355,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.",
|
||||
@@ -332,6 +381,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)",
|
||||
@@ -410,5 +460,23 @@
|
||||
"filter_reset_filters": "Zurücksetzen",
|
||||
"filter_back": "Zurück",
|
||||
"filter_active": "Filter aktiv",
|
||||
"filter_games_shown": "{0} Spiel(e) angezeigt"
|
||||
"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"
|
||||
}
|
||||
@@ -44,6 +44,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 +52,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,11 +62,25 @@
|
||||
"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",
|
||||
@@ -171,7 +188,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",
|
||||
@@ -191,12 +215,18 @@
|
||||
"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}",
|
||||
@@ -206,9 +236,21 @@
|
||||
"instruction_games_update_cache": "Redownload & refresh current games list",
|
||||
"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",
|
||||
@@ -249,6 +291,10 @@
|
||||
"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",
|
||||
@@ -311,6 +357,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.",
|
||||
@@ -334,6 +383,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)",
|
||||
@@ -410,5 +460,23 @@
|
||||
"filter_reset_filters": "Reset",
|
||||
"filter_back": "Back",
|
||||
"filter_active": "Filter active",
|
||||
"filter_games_shown": "{0} game(s) shown"
|
||||
"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"
|
||||
}
|
||||
@@ -44,14 +44,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,11 +60,28 @@
|
||||
"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",
|
||||
@@ -171,7 +189,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",
|
||||
@@ -197,6 +222,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}",
|
||||
@@ -206,9 +234,21 @@
|
||||
"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos",
|
||||
"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",
|
||||
@@ -249,6 +289,10 @@
|
||||
"history_option_extract_archive": "Extraer archivo",
|
||||
"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",
|
||||
@@ -311,6 +355,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.",
|
||||
@@ -334,6 +381,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)",
|
||||
@@ -410,5 +458,23 @@
|
||||
"filter_reset_filters": "Restablecer",
|
||||
"filter_back": "Volver",
|
||||
"filter_active": "Filtro activo",
|
||||
"filter_games_shown": "{0} juego(s) mostrado(s)"
|
||||
"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"
|
||||
}
|
||||
@@ -44,6 +44,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 +52,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,6 +67,17 @@
|
||||
"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",
|
||||
@@ -171,7 +185,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",
|
||||
@@ -191,12 +212,21 @@
|
||||
"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}",
|
||||
@@ -206,9 +236,21 @@
|
||||
"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux",
|
||||
"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é",
|
||||
@@ -249,6 +291,10 @@
|
||||
"history_option_extract_archive": "Extraire l'archive",
|
||||
"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": "Retenter le téléchargement",
|
||||
@@ -311,6 +357,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.",
|
||||
@@ -334,6 +383,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)",
|
||||
@@ -410,5 +460,23 @@
|
||||
"filter_reset_filters": "Réinitialiser",
|
||||
"filter_back": "Retour",
|
||||
"filter_active": "Filtre actif",
|
||||
"filter_games_shown": "{0} jeu(x) affiché(s)"
|
||||
"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"
|
||||
}
|
||||
@@ -44,14 +44,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,12 +60,27 @@
|
||||
"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 RGSX",
|
||||
@@ -168,7 +184,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",
|
||||
@@ -194,6 +217,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}",
|
||||
@@ -203,9 +229,21 @@
|
||||
"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi",
|
||||
"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",
|
||||
@@ -246,6 +284,10 @@
|
||||
"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",
|
||||
@@ -308,6 +350,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.",
|
||||
@@ -331,6 +376,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)",
|
||||
@@ -410,5 +456,23 @@
|
||||
"filter_reset_filters": "Reimposta",
|
||||
"filter_back": "Indietro",
|
||||
"filter_active": "Filtro attivo",
|
||||
"filter_games_shown": "{0} gioco/i mostrato/i"
|
||||
"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"
|
||||
}
|
||||
@@ -44,6 +44,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 +52,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,11 +62,28 @@
|
||||
"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",
|
||||
@@ -170,7 +190,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",
|
||||
@@ -196,6 +223,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}",
|
||||
@@ -205,9 +235,21 @@
|
||||
"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos",
|
||||
"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",
|
||||
@@ -248,6 +290,10 @@
|
||||
"history_option_extract_archive": "Extrair arquivo",
|
||||
"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",
|
||||
@@ -310,6 +356,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.",
|
||||
@@ -333,6 +382,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)",
|
||||
@@ -410,5 +460,23 @@
|
||||
"filter_reset_filters": "Redefinir",
|
||||
"filter_back": "Voltar",
|
||||
"filter_active": "Filtro ativo",
|
||||
"filter_games_shown": "{0} jogo(s) exibido(s)"
|
||||
"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"
|
||||
}
|
||||
@@ -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:
|
||||
@@ -695,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()
|
||||
@@ -718,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
|
||||
@@ -794,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
|
||||
@@ -837,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 -":
|
||||
@@ -925,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])
|
||||
@@ -933,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
|
||||
|
||||
@@ -975,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])
|
||||
@@ -983,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}")
|
||||
@@ -990,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}")
|
||||
|
||||
@@ -1018,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
|
||||
@@ -1048,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)
|
||||
@@ -1092,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'):
|
||||
@@ -1107,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:
|
||||
@@ -1169,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
|
||||
@@ -1209,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:
|
||||
@@ -1278,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
|
||||
@@ -1430,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):
|
||||
@@ -1564,18 +1786,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
|
||||
@@ -2192,6 +2422,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
|
||||
@@ -2235,9 +2474,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:
|
||||
@@ -2287,6 +2530,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
|
||||
|
||||
@@ -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()
|
||||
@@ -362,3 +497,66 @@ def save_game_filters(filters_dict):
|
||||
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
|
||||
|
||||
@@ -364,7 +364,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...")
|
||||
@@ -772,7 +772,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"]
|
||||
@@ -797,9 +797,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]}...")
|
||||
@@ -865,9 +865,26 @@ 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', ''),
|
||||
'realdebrid': api_keys_data.get('realdebrid', '')
|
||||
}
|
||||
|
||||
self._send_json({
|
||||
'success': True,
|
||||
'settings': settings,
|
||||
@@ -1470,7 +1487,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:
|
||||
@@ -1480,6 +1498,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({
|
||||
@@ -2019,18 +2068,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}")
|
||||
|
||||
@@ -2088,7 +2166,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')
|
||||
|
||||
@@ -540,3 +540,24 @@ header p { opacity: 0.9; font-size: 1.1em; }
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1866,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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2057,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' : ''}>
|
||||
@@ -2071,6 +2084,44 @@
|
||||
</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: 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>
|
||||
@@ -2127,7 +2178,17 @@
|
||||
},
|
||||
show_unsupported_platforms: document.getElementById('setting-show-unsupported').checked,
|
||||
allow_unknown_extensions: document.getElementById('setting-allow-unknown').checked,
|
||||
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() || '',
|
||||
'realdebrid': document.getElementById('setting-api-realdebrid')?.value.trim() || ''
|
||||
},
|
||||
game_filters: {
|
||||
region_filters: regionFiltersObj,
|
||||
hide_non_release: document.getElementById('hide-non-release')?.checked || savedHideNonRelease,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import shutil
|
||||
import requests # type: ignore
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
@@ -171,6 +172,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 +319,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 +460,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 +605,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 +928,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,6 +1155,12 @@ 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}")
|
||||
@@ -958,7 +1233,8 @@ 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")
|
||||
if getattr(config, "games_count_log_verbose", False):
|
||||
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
|
||||
return normalized
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
|
||||
@@ -1563,6 +1839,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
|
||||
@@ -1619,7 +1984,6 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
key_zip_path = os.path.join(dest_dir, f"_temp_key_{key_zip_name}")
|
||||
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(key_url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -2414,6 +2778,117 @@ def load_api_keys(force: bool = False):
|
||||
'reloaded': False
|
||||
}
|
||||
|
||||
|
||||
def load_archive_org_cookie(force: bool = False) -> str:
|
||||
"""Charge le cookie Archive.org depuis un fichier texte.
|
||||
|
||||
- Fichier: config.ARCHIVE_ORG_COOKIE_PATH
|
||||
- Accepte soit une ligne brute de cookie, soit une ligne "Cookie: ..."
|
||||
- Utilise un cache mtime pour éviter les relectures
|
||||
"""
|
||||
try:
|
||||
path = getattr(config, 'ARCHIVE_ORG_COOKIE_PATH', '')
|
||||
if not path:
|
||||
return ""
|
||||
cache_attr = '_archive_cookie_cache'
|
||||
if not hasattr(config, cache_attr):
|
||||
setattr(config, cache_attr, {'mtime': None, 'value': ''})
|
||||
cache_data = getattr(config, cache_attr)
|
||||
|
||||
# Créer le fichier vide si absent
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write("")
|
||||
except Exception as ce:
|
||||
logger.error(f"Impossible de préparer le fichier cookie archive.org: {ce}")
|
||||
return ""
|
||||
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
except Exception:
|
||||
mtime = None
|
||||
|
||||
if force or (mtime is not None and mtime != cache_data.get('mtime')):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
value = f.read().strip()
|
||||
except Exception as re:
|
||||
logger.error(f"Erreur lecture cookie archive.org: {re}")
|
||||
value = ""
|
||||
|
||||
if value.lower().startswith("cookie:"):
|
||||
value = value.split(":", 1)[1].strip()
|
||||
|
||||
cache_data['mtime'] = mtime
|
||||
cache_data['value'] = value
|
||||
|
||||
return cache_data.get('value', '') or ""
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur load_archive_org_cookie: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def save_api_keys(api_keys: dict):
|
||||
"""Sauvegarde les clés API (1fichier, AllDebrid, RealDebrid) dans leurs fichiers respectifs.
|
||||
|
||||
Args:
|
||||
api_keys: dict avec les clés '1fichier', 'alldebrid', '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', ''),
|
||||
'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 == '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', '')
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.3.3.1"
|
||||
"version": "2.5.0.3"
|
||||
}
|
||||
@@ -1,149 +1,387 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: Fichier de log
|
||||
if not exist "%CD%\logs" MD "%CD%\logs"
|
||||
set "LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt"
|
||||
:: Fichier de log (chemin absolu pour fiabilité)
|
||||
:: Détecter la racine (ROOT_DIR) d'abord pour construire un chemin stable
|
||||
set CURRENT_DIR=%CD%
|
||||
pushd "%CURRENT_DIR%\..\.."
|
||||
set "ROOT_DIR=%CD%"
|
||||
popd
|
||||
if not exist "%ROOT_DIR%\roms\windows\logs" MD "%ROOT_DIR%\roms\windows\logs"
|
||||
set "LOG_FILE=%ROOT_DIR%\roms\windows\logs\Retrobat_RGSX_log.txt"
|
||||
:: =============================================================================
|
||||
:: RGSX Retrobat Launcher v1.3
|
||||
:: =============================================================================
|
||||
:: Usage: "RGSX Retrobat.bat" [options]
|
||||
:: --display=N Launch on display N (0=primary, 1=secondary, etc.)
|
||||
:: --windowed Launch in windowed mode instead of fullscreen
|
||||
:: --help Show this help
|
||||
:: =============================================================================
|
||||
|
||||
:: Ajouter un horodatage au début du log
|
||||
echo [%DATE% %TIME%] Script start >> "%LOG_FILE%"
|
||||
:: Configuration des couleurs (codes ANSI)
|
||||
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
|
||||
set "ESC=%%b"
|
||||
)
|
||||
|
||||
:: Afficher un message de démarrage
|
||||
:: Couleurs
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "RED=[91m"
|
||||
set "CYAN=[96m"
|
||||
set "RESET=[0m"
|
||||
set "BOLD=[1m"
|
||||
|
||||
:: =============================================================================
|
||||
:: Traitement des arguments
|
||||
:: =============================================================================
|
||||
set "DISPLAY_NUM="
|
||||
set "WINDOWED_MODE="
|
||||
set "CONFIG_FILE="
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto :args_done
|
||||
if /i "%~1"=="--help" goto :show_help
|
||||
if /i "%~1"=="-h" goto :show_help
|
||||
if /i "%~1"=="--windowed" (
|
||||
set "WINDOWED_MODE=1"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
:: Check for --display=N format
|
||||
echo %~1 | findstr /r "^--display=" >nul
|
||||
if !ERRORLEVEL! EQU 0 (
|
||||
for /f "tokens=2 delims==" %%a in ("%~1") do set "DISPLAY_NUM=%%a"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
shift
|
||||
goto :parse_args
|
||||
|
||||
:show_help
|
||||
echo.
|
||||
echo %ESC%%CYAN%RGSX Retrobat Launcher - Help%ESC%%RESET%
|
||||
echo.
|
||||
echo Usage: "RGSX Retrobat.bat" [options]
|
||||
echo.
|
||||
echo Options:
|
||||
echo --display=N Launch on display N (0=primary, 1=secondary, etc.)
|
||||
echo --windowed Launch in windowed mode instead of fullscreen
|
||||
echo --help, -h Show this help
|
||||
echo.
|
||||
echo Examples:
|
||||
echo "RGSX Retrobat.bat" Launch on primary display
|
||||
echo "RGSX Retrobat.bat" --display=1 Launch on secondary display (TV)
|
||||
echo "RGSX Retrobat.bat" --windowed Launch in windowed mode
|
||||
echo.
|
||||
echo You can also create shortcuts with different display settings.
|
||||
echo.
|
||||
pause
|
||||
exit /b 0
|
||||
|
||||
:args_done
|
||||
|
||||
:: URL de telechargement Python
|
||||
set "PYTHON_ZIP_URL=https://github.com/RetroGameSets/RGSX/raw/main/windows/python.zip"
|
||||
|
||||
:: Obtenir le chemin du script de maniere fiable
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||
|
||||
:: Detecter le repertoire racine
|
||||
for %%I in ("%SCRIPT_DIR%\..\.." ) do set "ROOT_DIR=%%~fI"
|
||||
|
||||
:: Configuration des logs
|
||||
set "LOG_DIR=%ROOT_DIR%\roms\windows\logs"
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
set "LOG_FILE=%LOG_DIR%\Retrobat_RGSX_log.txt"
|
||||
set "LOG_BACKUP=%LOG_DIR%\Retrobat_RGSX_log.old.txt"
|
||||
|
||||
:: Rotation des logs avec backup
|
||||
if exist "%LOG_FILE%" (
|
||||
for %%A in ("%LOG_FILE%") do (
|
||||
if %%~zA GTR 100000 (
|
||||
if exist "%LOG_BACKUP%" del /q "%LOG_BACKUP%"
|
||||
move /y "%LOG_FILE%" "%LOG_BACKUP%" >nul 2>&1
|
||||
echo [%DATE% %TIME%] Log rotated - previous log saved as .old.txt > "%LOG_FILE%"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: =============================================================================
|
||||
:: Ecran d'accueil
|
||||
:: =============================================================================
|
||||
cls
|
||||
echo Running __main__.py for RetroBat...
|
||||
echo [%DATE% %TIME%] Running __main__.py for RetroBat >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo %ESC%%CYAN% ____ ____ ______ __ %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^| _ \ / ___^/ ___\ \/ / %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^| ^|_) ^| ^| _\___ \\ / %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^| _ ^<^| ^|_^| ^|___) / \ %ESC%%RESET%
|
||||
echo %ESC%%CYAN% ^|_^| \_\\____^|____/_/\_\ %ESC%%RESET%
|
||||
echo.
|
||||
echo %ESC%%BOLD% RetroBat Launcher v1.3%ESC%%RESET%
|
||||
echo --------------------------------
|
||||
if "!DISPLAY_NUM!" NEQ "0" (
|
||||
echo %ESC%%CYAN%Display: !DISPLAY_NUM!%ESC%%RESET%
|
||||
)
|
||||
if "!WINDOWED_MODE!"=="1" (
|
||||
echo %ESC%%CYAN%Mode: Windowed%ESC%%RESET%
|
||||
)
|
||||
echo.
|
||||
|
||||
:: Définir les chemins relatifs et les convertir en absolus
|
||||
set CURRENT_DIR=%CD%
|
||||
set PYTHON_EXE=python.exe
|
||||
:: Debut du log
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX Launcher v1.3 started >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Display: !DISPLAY_NUM!, Windowed: !WINDOWED_MODE! >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
|
||||
:: Détecter le répertoire racine en remontant de deux niveaux depuis le script
|
||||
pushd "%CURRENT_DIR%\..\.."
|
||||
set "ROOT_DIR=%CD%"
|
||||
popd
|
||||
|
||||
:: Définir le chemin du script principal selon les spécifications
|
||||
:: Configuration des chemins
|
||||
set "PYTHON_DIR=%ROOT_DIR%\system\tools\Python"
|
||||
set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
|
||||
set "MAIN_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\__main__.py"
|
||||
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
|
||||
|
||||
:: Definir le chemin du script de mise à jour de la gamelist Windows
|
||||
set "UPDATE_GAMELIST_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\update_gamelist_windows.py"
|
||||
:: Exporter RGSX_ROOT pour le script Python
|
||||
set "RGSX_ROOT=%ROOT_DIR%"
|
||||
|
||||
:: Convertir les chemins relatifs en absolus avec pushd/popd
|
||||
pushd "%ROOT_DIR%\system\tools\Python"
|
||||
set "PYTHON_EXE_FULL=%ROOT_DIR%\system\tools\Python\!PYTHON_EXE!"
|
||||
set "PYTHONW_EXE_FULL=%ROOT_DIR%\system\tools\Python\pythonw.exe"
|
||||
popd
|
||||
:: Logger les chemins
|
||||
echo [%DATE% %TIME%] System info: >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ROOT_DIR: %ROOT_DIR% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] PYTHON_EXE: %PYTHON_EXE% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] MAIN_SCRIPT: %MAIN_SCRIPT% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_ROOT: %RGSX_ROOT% >> "%LOG_FILE%"
|
||||
|
||||
:: Afficher et logger les variables
|
||||
:: =============================================================================
|
||||
:: Verification Python
|
||||
:: =============================================================================
|
||||
echo %ESC%%YELLOW%[1/3]%ESC%%RESET% Checking Python environment...
|
||||
echo [%DATE% %TIME%] Step 1/3: Checking Python >> "%LOG_FILE%"
|
||||
|
||||
echo ROOT_DIR : %ROOT_DIR% >> "%LOG_FILE%"
|
||||
echo CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%"
|
||||
echo ROOT_DIR : !ROOT_DIR! >> "%LOG_FILE%"
|
||||
echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
echo MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
echo UPDATE_GAMELIST_SCRIPT : !UPDATE_GAMELIST_SCRIPT! >> "%LOG_FILE%"
|
||||
|
||||
:: Vérifier si l'exécutable Python existe
|
||||
echo Checking python.exe...
|
||||
echo [%DATE% %TIME%] Checking python.exe at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
if not exist "!PYTHON_EXE_FULL!" (
|
||||
echo python.exe not found in system/tools. Preparing to extract..
|
||||
echo [%DATE% %TIME%] python.exe not found in system/tools. Preparing to extract.. >> "%LOG_FILE%"
|
||||
if not exist "%PYTHON_EXE%" (
|
||||
echo %ESC%%YELLOW%^> Python not found, installing...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Python not found, starting installation >> "%LOG_FILE%"
|
||||
|
||||
:: Créer le dossier Python s'il n'existe pas
|
||||
set "TOOLS_FOLDER_FULL=!ROOT_DIR!\system\tools"
|
||||
|
||||
if not exist "!TOOLS_FOLDER_FULL!\Python" (
|
||||
echo Creating folder !TOOLS_FOLDER_FULL!\Python...
|
||||
echo [%DATE% %TIME%] Creating folder !TOOLS_FOLDER_FULL!\Python... >> "%LOG_FILE%"
|
||||
mkdir "!TOOLS_FOLDER_FULL!\Python"
|
||||
:: Creer le dossier Python
|
||||
if not exist "%PYTHON_DIR%" (
|
||||
mkdir "%PYTHON_DIR%" 2>nul
|
||||
echo [%DATE% %TIME%] Created folder: %PYTHON_DIR% >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
set "ZIP_FILE=%ROOT_DIR%\roms\windows\python.zip"
|
||||
echo Extracting ZIP_FILE : !ZIP_FILE! in /system/tools/Python
|
||||
echo [%DATE% %TIME%] ZIP_FILE : !ZIP_FILE! >> "%LOG_FILE%"
|
||||
|
||||
if exist "!ZIP_FILE!" (
|
||||
echo [%DATE% %TIME%] Extracting python.zip to !TOOLS_FOLDER_FULL!... >> "%LOG_FILE%"
|
||||
tar -xf "!ZIP_FILE!" -C "!TOOLS_FOLDER_FULL!\Python" --strip-components=0
|
||||
echo Extraction finished.
|
||||
echo [%DATE% %TIME%] Extraction finished. >> "%LOG_FILE%"
|
||||
del /s /q "!ZIP_FILE!"
|
||||
echo python.zip file deleted.
|
||||
echo [%DATE% %TIME%] python.zip file deleted. >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo Error: Error python.zip not found please download it from github and put in /roms/windows folder.
|
||||
echo [%DATE% %TIME%] Error: Error python.zip not found please download it from github and put in /roms/windows folder >> "%LOG_FILE%"
|
||||
:: Verifier si le ZIP existe, sinon le telecharger
|
||||
if not exist "%ZIP_FILE%" (
|
||||
echo %ESC%%YELLOW%^> python.zip not found, downloading from GitHub...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] python.zip not found, attempting download >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Download URL: %PYTHON_ZIP_URL% >> "%LOG_FILE%"
|
||||
|
||||
:: Verifier si curl est disponible
|
||||
where curl.exe >nul 2>&1
|
||||
if !ERRORLEVEL! EQU 0 (
|
||||
echo %ESC%%CYAN%^> Using curl to download...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Using curl.exe for download >> "%LOG_FILE%"
|
||||
curl.exe -L -# -o "%ZIP_FILE%" "%PYTHON_ZIP_URL%"
|
||||
set DOWNLOAD_RESULT=!ERRORLEVEL!
|
||||
) else (
|
||||
:: Fallback sur PowerShell
|
||||
echo %ESC%%CYAN%^> Using PowerShell to download...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] curl not found, using PowerShell >> "%LOG_FILE%"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri '%PYTHON_ZIP_URL%' -OutFile '%ZIP_FILE%'"
|
||||
set DOWNLOAD_RESULT=!ERRORLEVEL!
|
||||
)
|
||||
|
||||
:: Verifier le resultat du telechargement
|
||||
if !DOWNLOAD_RESULT! NEQ 0 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Download failed!%ESC%%RESET%
|
||||
echo.
|
||||
echo Please download python.zip manually from:
|
||||
echo %ESC%%CYAN%%PYTHON_ZIP_URL%%ESC%%RESET%
|
||||
echo.
|
||||
echo And place it in:
|
||||
echo %ESC%%CYAN%%ROOT_DIR%\roms\windows\%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: Download failed with code !DOWNLOAD_RESULT! >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:: Verifier que le fichier a bien ete telecharge et n'est pas vide
|
||||
if not exist "%ZIP_FILE%" (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Download failed - file not created!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: ZIP file not created after download >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:: Verifier la taille du fichier (doit etre > 1MB pour etre valide)
|
||||
for %%A in ("%ZIP_FILE%") do set ZIP_SIZE=%%~zA
|
||||
if !ZIP_SIZE! LSS 1000000 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Downloaded file appears invalid ^(too small^)!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: Downloaded file too small: !ZIP_SIZE! bytes >> "%LOG_FILE%"
|
||||
del /q "%ZIP_FILE%" 2>nul
|
||||
goto :error
|
||||
)
|
||||
|
||||
echo %ESC%%GREEN%^> Download complete ^(!ZIP_SIZE! bytes^)%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Download successful: !ZIP_SIZE! bytes >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
:: Verifier que tar existe (Windows 10 1803+)
|
||||
where tar >nul 2>&1
|
||||
if !ERRORLEVEL! NEQ 0 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: tar command not available!%ESC%%RESET%
|
||||
echo.
|
||||
echo Please update Windows 10 or extract manually to:
|
||||
echo %ESC%%CYAN%%PYTHON_DIR%%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: tar command not found >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:: Vérifier à nouveau si python.exe existe après extraction
|
||||
if not exist "!PYTHON_EXE_FULL!" (
|
||||
echo Error: python.exe not found after extraction at !PYTHON_EXE_FULL!.
|
||||
echo [%DATE% %TIME%] Error: python.exe not found after extraction at !PYTHON_EXE_FULL! >> "%LOG_FILE%"
|
||||
:: Extraction avec progression simulee
|
||||
echo %ESC%%YELLOW%^> Extracting Python...%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] Extracting python.zip >> "%LOG_FILE%"
|
||||
|
||||
<nul set /p "= ["
|
||||
tar -xf "%ZIP_FILE%" -C "%PYTHON_DIR%" --strip-components=0
|
||||
set TAR_RESULT=!ERRORLEVEL!
|
||||
echo %ESC%%GREEN%##########%ESC%%RESET%] Done
|
||||
|
||||
if !TAR_RESULT! NEQ 0 (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Extraction failed!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: tar extraction failed with code !TAR_RESULT! >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
echo [%DATE% %TIME%] Extraction completed >> "%LOG_FILE%"
|
||||
|
||||
:: Supprimer ZIP
|
||||
del /q "%ZIP_FILE%" 2>nul
|
||||
echo %ESC%%GREEN%^> python.zip cleaned up%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] python.zip deleted >> "%LOG_FILE%"
|
||||
|
||||
:: Verifier installation
|
||||
if not exist "%PYTHON_EXE%" (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: Python not found after extraction!%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] ERROR: python.exe not found after extraction >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
)
|
||||
echo python.exe found.
|
||||
echo [%DATE% %TIME%] python.exe found. >> "%LOG_FILE%"
|
||||
|
||||
:: Vérifier si le script Python existe
|
||||
echo Checking __main__.py...
|
||||
echo [%DATE% %TIME%] Checking __main__.py at !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
if not exist "!MAIN_SCRIPT!" (
|
||||
echo Error: __main__.py not found at !MAIN_SCRIPT!.
|
||||
echo [%DATE% %TIME%] Error: __main__.py not found at !MAIN_SCRIPT! >> "%LOG_FILE%"
|
||||
:: Afficher et logger la version Python
|
||||
for /f "tokens=*" %%v in ('"%PYTHON_EXE%" --version 2^>^&1') do set "PYTHON_VERSION=%%v"
|
||||
echo %ESC%%GREEN%^> %PYTHON_VERSION% found%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] %PYTHON_VERSION% detected >> "%LOG_FILE%"
|
||||
|
||||
:: =============================================================================
|
||||
:: Verification script principal
|
||||
:: =============================================================================
|
||||
echo %ESC%%YELLOW%[2/3]%ESC%%RESET% Checking RGSX application...
|
||||
echo [%DATE% %TIME%] Step 2/3: Checking RGSX files >> "%LOG_FILE%"
|
||||
|
||||
if not exist "%MAIN_SCRIPT%" (
|
||||
echo.
|
||||
echo %ESC%%RED% ERROR: __main__.py not found!%ESC%%RESET%
|
||||
echo.
|
||||
echo Expected location:
|
||||
echo %ESC%%CYAN%%MAIN_SCRIPT%%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: __main__.py not found at %MAIN_SCRIPT% >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
echo __main__.py found.
|
||||
echo [%DATE% %TIME%] __main__.py found. >> "%LOG_FILE%"
|
||||
|
||||
:: L'étape de mise à jour de la gamelist est désormais appelée depuis __main__.py
|
||||
echo [%DATE% %TIME%] Skipping external gamelist update (handled in app). >> "%LOG_FILE%"
|
||||
echo %ESC%%GREEN%^> RGSX files OK%ESC%%RESET%
|
||||
echo [%DATE% %TIME%] RGSX files verified >> "%LOG_FILE%"
|
||||
|
||||
echo Launching __main__.py (attached)...
|
||||
echo [%DATE% %TIME%] Preparing to launch main. >> "%LOG_FILE%"
|
||||
:: =============================================================================
|
||||
:: Lancement
|
||||
:: =============================================================================
|
||||
echo %ESC%%YELLOW%[3/3]%ESC%%RESET% Launching RGSX...
|
||||
echo [%DATE% %TIME%] Step 3/3: Launching application >> "%LOG_FILE%"
|
||||
|
||||
:: Assurer le bon dossier de travail pour l'application
|
||||
:: Changer le repertoire de travail
|
||||
cd /d "%ROOT_DIR%\roms\ports\RGSX"
|
||||
echo [%DATE% %TIME%] Working directory: %CD% >> "%LOG_FILE%"
|
||||
|
||||
:: Forcer les drivers SDL côté Windows et réduire le bruit console
|
||||
:: Configuration SDL/Pygame
|
||||
set PYGAME_HIDE_SUPPORT_PROMPT=1
|
||||
set SDL_VIDEODRIVER=windows
|
||||
set SDL_AUDIODRIVER=directsound
|
||||
echo [%DATE% %TIME%] CWD before launch: %CD% >> "%LOG_FILE%"
|
||||
set PYTHONWARNINGS=ignore::UserWarning:pygame.pkgdata
|
||||
set PYTHONIOENCODING=utf-8
|
||||
|
||||
:: Lancer l'application dans la même console et attendre sa fin
|
||||
:: Forcer python.exe pour capturer la sortie
|
||||
set "PY_MAIN_EXE=!PYTHON_EXE_FULL!"
|
||||
echo [%DATE% %TIME%] Using interpreter: !PY_MAIN_EXE! >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Launching "!MAIN_SCRIPT!" now... >> "%LOG_FILE%"
|
||||
"!PY_MAIN_EXE!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1
|
||||
set EXITCODE=!ERRORLEVEL!
|
||||
echo [%DATE% %TIME%] __main__.py exit code: !EXITCODE! >> "%LOG_FILE%"
|
||||
if "!EXITCODE!"=="0" (
|
||||
echo Execution finished successfully.
|
||||
echo [%DATE% %TIME%] Execution of __main__.py finished successfully. >> "%LOG_FILE%"
|
||||
:: =============================================================================
|
||||
:: Configuration multi-ecran
|
||||
:: =============================================================================
|
||||
:: SDL_VIDEO_FULLSCREEN_HEAD: Selectionne l'ecran pour le mode plein ecran
|
||||
:: 0 = ecran principal, 1 = ecran secondaire, etc.
|
||||
:: Ces variables ne sont definies que si --display=N ou --windowed est passe
|
||||
:: Sinon, le script Python utilisera les parametres de rgsx_settings.json
|
||||
|
||||
echo [%DATE% %TIME%] Display configuration: >> "%LOG_FILE%"
|
||||
if defined DISPLAY_NUM (
|
||||
set SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM!
|
||||
set RGSX_DISPLAY=!DISPLAY_NUM!
|
||||
echo [%DATE% %TIME%] SDL_VIDEO_FULLSCREEN_HEAD=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_DISPLAY=!DISPLAY_NUM! ^(from --display arg^) >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo Error: Failed to execute __main__.py (code !EXITCODE!).
|
||||
echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code !EXITCODE!. >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Display: using rgsx_settings.json config >> "%LOG_FILE%"
|
||||
)
|
||||
if defined WINDOWED_MODE (
|
||||
set RGSX_WINDOWED=!WINDOWED_MODE!
|
||||
echo [%DATE% %TIME%] RGSX_WINDOWED=!WINDOWED_MODE! ^(from --windowed arg^) >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo [%DATE% %TIME%] Windowed: using rgsx_settings.json config >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
:: Log environnement
|
||||
echo [%DATE% %TIME%] Environment variables set: >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] RGSX_ROOT=%RGSX_ROOT% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] SDL_VIDEODRIVER=%SDL_VIDEODRIVER% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] SDL_AUDIODRIVER=%SDL_AUDIODRIVER% >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] PYTHONIOENCODING=%PYTHONIOENCODING% >> "%LOG_FILE%"
|
||||
|
||||
echo.
|
||||
if defined DISPLAY_NUM (
|
||||
echo %ESC%%CYAN%Launching on display !DISPLAY_NUM!...%ESC%%RESET%
|
||||
)
|
||||
if defined WINDOWED_MODE (
|
||||
echo %ESC%%CYAN%Windowed mode enabled%ESC%%RESET%
|
||||
)
|
||||
echo %ESC%%CYAN%Starting RGSX application...%ESC%%RESET%
|
||||
echo %ESC%%BOLD%Press Ctrl+C to force quit if needed%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] Executing: "%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] --- Application output start --- >> "%LOG_FILE%"
|
||||
|
||||
"%PYTHON_EXE%" "%MAIN_SCRIPT%" >> "%LOG_FILE%" 2>&1
|
||||
set EXITCODE=!ERRORLEVEL!
|
||||
|
||||
echo [%DATE% %TIME%] --- Application output end --- >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Exit code: !EXITCODE! >> "%LOG_FILE%"
|
||||
|
||||
if "!EXITCODE!"=="0" (
|
||||
echo.
|
||||
echo %ESC%%GREEN%RGSX closed successfully.%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] Application closed successfully >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo.
|
||||
echo %ESC%%RED%RGSX exited with error code !EXITCODE!%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ERROR: Application exited with code !EXITCODE! >> "%LOG_FILE%"
|
||||
goto :error
|
||||
)
|
||||
|
||||
:end
|
||||
echo Task completed.
|
||||
echo [%DATE% %TIME%] Task completed successfully. >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Session ended normally >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
timeout /t 2 >nul
|
||||
exit /b 0
|
||||
|
||||
:error
|
||||
echo An error occurred.
|
||||
echo [%DATE% %TIME%] An error occurred. >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo %ESC%%RED%An error occurred. Check the log file:%ESC%%RESET%
|
||||
echo %ESC%%CYAN%%LOG_FILE%%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Session ended with errors >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo Press any key to close...
|
||||
pause >nul
|
||||
exit /b 1
|
||||
Reference in New Issue
Block a user