Compare commits

...

5 Commits

Author SHA1 Message Date
skymike03
ace6ec876f v2.3.2.3
- correct bug when using both keyboard and controller mixed that cause a repeat key holdind
2025-11-16 14:05:21 +01:00
RGS
9f759c1928 Enhance README with platform and interface images
Added images to enhance the README presentation.
2025-11-16 13:39:45 +01:00
skymike03
db287e33d7 v2.3.2.2 (2025.11.16)
- now keyboard works everytime even when a controller is plugged to be able to reconfigure mapping or navigate
2025-11-16 13:11:33 +01:00
skymike03
217392dcd1 v2.3.2.1
- add custom dns service in menu (activate to use custom DNS 1.1.1.1 at boot and avoid download problems)
- add pygame mixer error handling if crash
2025-11-13 22:40:19 +01:00
skymike03
fd9037139c v2.3.2.0 (2025.11.12)
- Enhance download cancellation handling in the display and network modules (when games are on wait list , queue is canceled on application stop
2025-11-12 19:05:25 +01:00
19 changed files with 571 additions and 579 deletions

2
.gitignore vendored
View File

@@ -17,9 +17,9 @@ ports/RGSX.bat
audit_i18n.py
prune_i18n.py
Info.txt
pygame/
# Docker test data
data/
docker-compose.test.yml
config/

View File

@@ -4,6 +4,16 @@
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="30%" alt="controls help" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
</p>
<p align="center">
<img width="49%" alt="web interface" src="https://github.com/user-attachments/assets/71f8bd39-5901-45a9-82b2-91426b3c31a7" />
<img width="49%" alt="api menu" src="https://github.com/user-attachments/assets/5bae018d-b7d9-4a95-9f1b-77db751ff24f" />
</p>
---
## 🚀 Installation
@@ -145,22 +155,13 @@ RGSX includes a web interface that launched automatically when using RGSX for re
### Enable/Disable Web Service at Boot, without the need to launch RGSX
**Method 1: From RGSX Menu**
**From RGSX Menu**
1. Open **Pause Menu** (Start/ALTGr)
2. Navigate to **Settings > Web Service**
3. Toggle **Enable at Boot**
4. Restart your device
**Method 2: Manual Configuration**
Edit `/saves/ports/rgsx/rgsx_settings.json`:
```json
{
"web_service": {
"enabled_at_boot": true
}
}
```
**Port Configuration**: The web service runs on port `5000` by default. Ensure this port is not blocked by firewall rules.
---
@@ -231,4 +232,4 @@ Free and open-source software. Use, modify, and distribute freely.
[![Stargazers over time](https://starchart.cc/RetroGameSets/RGSX.svg?variant=adaptive)](https://starchart.cc/RetroGameSets/RGSX)
**Developed with ❤️ for the retro gaming community.**
**Developed with ❤️ for the retro gaming community.**

View File

@@ -1,251 +0,0 @@
# RGSX CLI — Guide dutilisation
Ce guide couvre toutes les commandes disponibles du CLI et fournit des exemples prêts à copier (Windows PowerShell).
## Nouveau: mode interactif
Vous pouvez maintenant lancer une session interactive et enchaîner les commandes sans retaper `python rgsx_cli.py` à chaque fois :
```powershell
python rgsx_cli.py
```
Vous verrez :
```
RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.
rgsx>
```
Dans cette session tapez directement les sous-commandes :
```
rgsx> platforms
rgsx> games --platform snes --search mario
rgsx> download --platform snes --game "Super Mario World (USA).zip"
rgsx> history --tail 10
rgsx> exit
```
Extras :
- `help` ou `?` affiche laide globale.
- `exit` ou `quit` quitte la session.
- `--verbose` une fois active les logs détaillés pour toute la session.
## Tableau formaté (platforms)
La commande `platforms` affiche maintenant un tableau ASCII à largeur fixe (sauf avec `--json`) :
```
+--------------------------------+-----------------+
| Nom de plateforme | Dossier |
+--------------------------------+-----------------+
| Nintendo Entertainment System | nes |
| Super Nintendo Entertainment.. | snes |
| Sega Mega Drive | megadrive |
+--------------------------------+-----------------+
```
Colonnes : 30 caractères pour le nom, 15 pour le dossier (troncature par `...`).
## Aliases & synonymes doptions (mis à jour)
Aliases des sous-commandes :
- `platforms``p`
- `games``g`
- `download``dl`
- `clear-history``clear`
Options équivalentes (toutes les formes listées sont acceptées) :
- Plateforme : `--platform`, `--p`, `-p`
- Jeu : `--game`, `--g`, `-g`
- Recherche : `--search`, `--s`, `-s`
- Forcer (download) : `--force`, `-f`
- Mode interactif (download) : `--interactive`, `-i`
Exemples avec alias :
```powershell
python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip"
python rgsx_cli.py g --p snes --s mario
python rgsx_cli.py p --json
python rgsx_cli.py clear
```
## Sélection ambiguë lors dun download (nouveau tableau)
Quand vous tentez un téléchargement avec un titre non exact et que le mode interactif est actif (TTY ou `--interactive`), les correspondances saffichent en tableau :
```
No exact result found for this game: mario super yoshi
Select a match to download:
+------+--------------------------------------------------------------+------------+
| # | Title | Size |
+------+--------------------------------------------------------------+------------+
| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M |
| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M |
+------+--------------------------------------------------------------+------------+
Enter number (or press Enter to cancel):
```
Si vous annulez ou que le mode interactif nest pas actif, un tableau similaire est affiché (sans le prompt) suivi dun conseil.
## Recherche améliorée (multitokens) pour `games`
Loption `--search` / `--s` / `-s` utilise maintenant la même logique de classement que les suggestions du download :
1. Correspondance sous-chaîne (position la plus tôt) — priorité 0
2. Séquence de tokens dans lordre (non contiguë) — priorité 1 (écart le plus faible)
3. Tous les tokens présents dans nimporte quel ordre — priorité 2 (ensemble de tokens plus petit privilégié)
Les doublons sont dédupliqués en gardant le meilleur score. Ainsi une requête :
```powershell
python rgsx_cli.py games --p snes --s "super mario yoshi"
```
affiche toutes les variantes pertinentes de "Super Mario World 2 - Yoshi's Island" même si lordre des mots diffère.
Exemple de sortie :
```
+--------------------------------------------------------------+------------+
| Game Title | Size |
+--------------------------------------------------------------+------------+
| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M |
| Super Mario - Yoshi Island (Japan).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
+--------------------------------------------------------------+------------+
```
Si aucun résultat nest trouvé, seul len-tête est affiché puis un message.
## Prérequis
- Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne souvrira).
- Exécuter depuis le dossier contenant `rgsx_cli.py`.
## Syntaxe générale (mode classique)
Les options globales peuvent être placées avant ou après la sous-commande.
- Forme 1:
```powershell
python rgsx_cli.py [--verbose] [--force-update|-force-update] <commande> [options]
```
- Forme 2:
```powershell
python rgsx_cli.py <commande> [options] [--verbose] [--force-update|-force-update]
```
- `--verbose` active les logs détaillés (DEBUG) sur stderr.
- `--force-update` (ou `-force-update`) purge les données locales et force le re-téléchargement du pack de données (systems_list, games/*.json, images).
Quand les données sources sont manquantes, le CLI télécharge et extrait automatiquement le pack (avec progression).
## Commandes
### 1) platforms (`platforms` / `p`) — lister les plateformes
- Options:
- `--json`: sortie JSON (objets `{ name, folder }`).
Exemples:
```powershell
python rgsx_cli.py platforms
python rgsx_cli.py p --json
python rgsx_cli.py --verbose p
python rgsx_cli.py p --verbose
```
Sortie texte: une ligne par plateforme, au format `Nom<TAB>Dossier`.
### 2) games (`games` / `g`) — lister les jeux dune plateforme
- Options:
- `--platform | --p | -p <nom_ou_dossier>` (ex: `n64` ou "Nintendo 64").
- `--search | --s | -s <texte>`: filtre par sous-chaîne.
Exemples:
```powershell
python rgsx_cli.py games --platform n64
python rgsx_cli.py g --p "Nintendo 64" --s zelda
python rgsx_cli.py g -p n64 --verbose
```
Remarques:
- La plateforme est résolue par nom affiché (platform_name) ou dossier, insensible à la casse.
### 3) download (`download` / `dl`) — télécharger un jeu
- Options:
- `--platform | --p | -p <nom_ou_dossier>`
- `--game | --g | -g "<titre exact ou partiel>"`
- `--force | -f`: ignorer lavertissement dextension non supportée.
- `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact nest trouvé.
Exemples:
```powershell
# Titre exact
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Titre partiel (sélection numérotée si aucun exact)
python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)"
# Forcer malgré extension
python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f
# Verbose après sous-commande
python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
```
Pendant le téléchargement: progression %, taille (MB), vitesse (MB/s). Résultat final aussi dans lhistorique.
Notes:
- Les ROMs sont enregistrées dans le dossier plateforme correspondant (ex: `R:\roms\n64`).
- Si le fichier est une archive (zip/rar) et que la plateforme ne supporte pas lextension, un avertissement apparaît (utiliser `--force`).
### 4) history — afficher lhistorique
- Options:
- `--tail <N>`: n dernières entrées (défaut: 50)
- `--json`: sortie JSON
Exemples:
```powershell
python rgsx_cli.py history
python rgsx_cli.py history --tail 20
python rgsx_cli.py history --json
```
### 5) clear-history (`clear-history` / `clear`) — vider lhistorique
Exemple:
```powershell
python rgsx_cli.py clear
```
### Option globale: --force-update — purge + re-téléchargement des données
- Supprime `systems_list.json`, `games/`, `images/` puis retélécharge/extrait le pack.
Exemples:
```powershell
python rgsx_cli.py --force-update
python rgsx_cli.py p --force-update
```
## Comportements et conseils
- Résolution plateforme: par nom affiché ou dossier, insensible à la casse.
- `--verbose`: utile surtout pour téléchargements/extractions.
- Données manquantes: téléchargement + extraction automatiques.
- Codes de sortie (indicatif):
- `0`: succès
- `1`: échec téléchargement/erreur générique
- `2`: plateforme introuvable
- `3`: jeu introuvable
- `4`: extension non supportée (sans `--force`)
## Exemples rapides (copier-coller)
```powershell
# Démarrer le shell interactif
python rgsx_cli.py
# Lister plateformes (alias)
python rgsx_cli.py p
# Lister plateformes (JSON)
python rgsx_cli.py p --json
# Lister jeux N64 avec filtre (synonymes)
python rgsx_cli.py g --p n64 --s zelda
# Télécharger un jeu N64 (titre exact) avec alias
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Télécharger (titre partiel) + sélection
python rgsx_cli.py dl -p n64 -g "Ocarina of Time"
# Historique (20 dernières entrées)
python rgsx_cli.py history --tail 20
# Purger et recharger le pack
python rgsx_cli.py --force-update
```

View File

@@ -1,254 +0,0 @@
# RGSX CLI — Usage Guide
This guide covers all available CLI commands with copy-ready Windows PowerShell examples.
## Prerequisites
- Python installed and on PATH (the app runs in headless mode; no window will open).
- Run commands from the folder that contains `rgsx_cli.py`.
## Quick interactive mode (new)
You can now start an interactive shell once and issue multiple commands without retyping `python rgsx_cli.py` each time:
```powershell
python rgsx_cli.py
```
You will see a prompt like:
```
RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.
rgsx>
```
Inside this shell type subcommands exactly as you would after `python rgsx_cli.py`:
```
rgsx> platforms
rgsx> games --platform snes --search mario
rgsx> download --platform snes --game "Super Mario World (USA).zip"
rgsx> history --tail 10
rgsx> exit
```
Extras:
- `help` or `?` prints the global help.
- `exit` or `quit` leaves the shell.
- `--verbose` once sets persistent verbose logging for the rest of the session.
## Formatted table output (platforms)
The `platforms` command now renders a fixed-width ASCII table (unless `--json` is used):
```
+--------------------------------+-----------------+
| Platform Name | Folder |
+--------------------------------+-----------------+
| Nintendo Entertainment System | nes |
| Super Nintendo Entertainment.. | snes |
| Sega Mega Drive | megadrive |
+--------------------------------+-----------------+
```
Columns: 30 chars for name, 15 for folder (values longer are truncated with `...`).
## Aliases & option synonyms (updated)
Subcommand aliases:
- `platforms``p`
- `games``g`
- `download``dl`
- `clear-history``clear`
Option aliases (all shown forms are accepted; they are equivalent):
- Platform: `--platform`, `--p`, `-p`
- Game: `--game`, `--g`, `-g`
- Search: `--search`, `--s`, `-s`
- Force (download): `--force`, `-f`
- Interactive (download): `--interactive`, `-i`
Examples with aliases:
```powershell
python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip"
python rgsx_cli.py g --p snes --s mario
python rgsx_cli.py p --json
python rgsx_cli.py clear
```
## Ambiguous download selection (new table)
When you attempt a download with a non-exact title and interactive mode is active (TTY or `--interactive`), matches are displayed in a table:
```
No exact result found for this game: mario super yoshi
Select a match to download:
+------+--------------------------------------------------------------+------------+
| # | Title | Size |
+------+--------------------------------------------------------------+------------+
| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M |
| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M |
+------+--------------------------------------------------------------+------------+
Enter number (or press Enter to cancel):
```
If you cancel or are not in interactive mode, a similar table is still shown (without the prompt) followed by a tip.
## Improved fuzzy search for games (multi-token)
The `--search` / `--s` / `-s` option now uses the same multi-strategy ranking as the download suggestion logic:
1. Substring match (position-based) — highest priority
2. Ordered non-contiguous token sequence (smallest gap wins)
3. All tokens present in any order (smaller token set size wins)
Duplicate titles are deduplicated by keeping the best scoring strategy. This means queries like:
```powershell
python rgsx_cli.py games --p snes --s "super mario yoshi"
```
will surface all relevant "Super Mario World 2 - Yoshi's Island" variants even if the word order differs.
Example output:
```
+--------------------------------------------------------------+------------+
| Game Title | Size |
+--------------------------------------------------------------+------------+
| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M |
| Super Mario - Yoshi Island (Japan).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
+--------------------------------------------------------------+------------+
```
If no results are found the table displays only headers followed by a message.
## General syntax (non-interactive)
Global options can be placed before or after the subcommand.
- Form 1:
```powershell
python rgsx_cli.py [--verbose] [--force-update|-force-update] <command> [options]
```
- Form 2:
```powershell
python rgsx_cli.py <command> [options] [--verbose] [--force-update|-force-update]
```
- `--verbose` enables detailed logs (DEBUG) on stderr.
- `--force-update` (or `-force-update`) purges local data and re-downloads the data pack (systems_list, games/*.json, images).
When source data is missing, the CLI will automatically download and extract the data pack (with progress).
## Commands
### 1) platforms (`platforms` / `p`) — list platforms
- Options:
- `--json`: JSON output (objects `{ name, folder }`).
Examples:
```powershell
python rgsx_cli.py platforms
python rgsx_cli.py p --json
python rgsx_cli.py --verbose p
python rgsx_cli.py p --verbose
```
Text output: one line per platform, formatted as `Name<TAB>Folder`.
### 2) games (`games` / `g`) — list games for a platform
- Options:
- `--platform | --p | -p <name_or_folder>` (e.g., `n64` or "Nintendo 64").
- `--search | --s | -s <text>`: filter by substring in game title.
Examples:
```powershell
python rgsx_cli.py games --platform n64
python rgsx_cli.py g --p "Nintendo 64" --s zelda
python rgsx_cli.py g -p n64 --verbose
```
Notes:
- The platform is resolved by display name (platform_name) or folder, case-insensitively.
### 3) download (`download` / `dl`) — download a game
- Options:
- `--platform | --p | -p <name_or_folder>`
- `--game | --g | -g "<exact or partial title>"`
- `--force | -f`: ignore unsupported-extension warning for the platform.
- `--interactive | -i`: prompt to choose from matches when no exact title is found.
Examples:
```powershell
# Exact title
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Partial match (interactive numbered selection if no exact match)
python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)"
# Forced despite extension
python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f
# Verbose after subcommand
python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
```
During download, progress %, size (MB) and speed (MB/s) are shown. The final result is also written to history.
Notes:
- ROMs are saved into the corresponding platform directory (e.g., `R:\roms\n64`).
- If the file is an archive (zip/rar) and the platform doesnt support that extension, a warning is shown (you can use `--force`).
### 4) history — show history
- Options:
- `--tail <N>`: last N entries (default: 50)
- `--json`: JSON output
Examples:
```powershell
python rgsx_cli.py history
python rgsx_cli.py history --tail 20
python rgsx_cli.py history --json
```
### 5) clear-history (`clear-history` / `clear`) — clear history
Example:
```powershell
python rgsx_cli.py clear
```
### Global option: --force-update — purge + re-download data
- Removes `systems_list.json`, the `games/` and `images/` folders, then downloads/extracts the data pack again.
Examples:
```powershell
# Without subcommand: purge + re-download then exit
python rgsx_cli.py --force-update
# Placed after a subcommand (also accepted)
python rgsx_cli.py p --force-update
```
## Behavior and tips
- Platform resolution: by display name or folder, case-insensitive. For `games` and `download`, if no exact match is found a search-like suggestion list is shown.
- `--verbose` logs: most useful during downloads/extraction; printed at DEBUG level.
- Missing data download: automatic, with consistent progress (download then extraction).
- Exit codes (indicative):
- `0`: success
- `1`: download failure/generic error
- `2`: platform not found
- `3`: game not found
- `4`: unsupported extension (without `--force`)
## Quick examples (copy/paste)
```powershell
# Start interactive shell
python rgsx_cli.py
# List platforms (text)
python rgsx_cli.py p
# List platforms (JSON)
python rgsx_cli.py p --json
# List N64 games with filter (using alias synonyms)
python rgsx_cli.py g --p n64 --s zelda
# Download an N64 game (exact title) using aliases
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Download with approximate title (suggestions + interactive pick)
python rgsx_cli.py dl -p n64 -g "Ocarina of Time"
# View last 20 history entries
python rgsx_cli.py history --tail 20
# Purge and refresh data pack
python rgsx_cli.py --force-update
```

View File

@@ -238,11 +238,12 @@ except Exception as e:
# Initialisation du mixer Pygame (déférée/évitable si musique désactivée)
if getattr(config, 'music_enabled', True):
pygame.mixer.pre_init(44100, -16, 2, 4096)
try:
pygame.mixer.pre_init(44100, -16, 2, 4096)
pygame.mixer.init()
except Exception as e:
logger.warning(f"Échec init mixer: {e}")
except (NotImplementedError, AttributeError, Exception) as e:
logger.warning(f"Mixer non disponible ou échec init: {e}")
config.music_enabled = False # Désactiver la musique si mixer non disponible
# Dossier musique Batocera
music_folder = os.path.join(config.APP_FOLDER, "assets", "music")
@@ -1368,7 +1369,11 @@ async def main():
clock.tick(60)
await asyncio.sleep(0.01)
pygame.mixer.music.stop()
try:
if pygame.mixer.get_init() is not None:
pygame.mixer.music.stop()
except (AttributeError, NotImplementedError):
pass
# Cancel any ongoing downloads to prevent lingering background threads
try:
cancel_all_downloads()

View File

@@ -0,0 +1,133 @@
#!/bin/bash
# BATOCERA SERVICE
# name: Custom DNS Service for RGSX
# description: Force custom DNS servers (Cloudflare 1.1.1.1)
# author: RetroGameSets
# depends:
# version: 1.0
RESOLV_CONF="/tmp/resolv.conf"
PIDFILE="/var/run/custom_dns.pid"
LOGFILE="/userdata/roms/ports/RGSX/logs/custom_dns_service.log"
SERVICE_NAME="custom_dns"
# Fonction utilitaire : vérifie si le service est activé dans batocera-settings
is_enabled() {
local enabled_services
enabled_services="$(/usr/bin/batocera-settings-get system.services 2>/dev/null)"
for s in $enabled_services; do
if [ "$s" = "$SERVICE_NAME" ]; then
echo "enabled"
return
fi
done
echo "disabled"
}
# Fonction pour appliquer les DNS personnalisés
apply_custom_dns() {
echo "[${SERVICE_NAME}] Applying custom DNS servers..."
mkdir -p "$(dirname "$LOGFILE")"
{
echo "$(date '+%Y-%m-%d %H:%M:%S') - Applying custom DNS"
# Retirer la protection si elle existe
chattr -i "$RESOLV_CONF" 2>/dev/null || true
# Écrire la nouvelle configuration DNS
echo "# Generated by RGSX Custom DNS Service" > "$RESOLV_CONF"
echo "nameserver 1.1.1.1" >> "$RESOLV_CONF"
echo "nameserver 1.0.0.1" >> "$RESOLV_CONF"
# Protéger le fichier contre les modifications
chattr +i "$RESOLV_CONF" 2>/dev/null || true
echo "$(date '+%Y-%m-%d %H:%M:%S') - Custom DNS applied successfully"
} >> "$LOGFILE" 2>&1
echo "[${SERVICE_NAME}] Custom DNS applied (1.1.1.1, 1.0.0.1)"
}
# Fonction pour restaurer les DNS par défaut
restore_default_dns() {
echo "[${SERVICE_NAME}] Restoring default DNS..."
mkdir -p "$(dirname "$LOGFILE")"
{
echo "$(date '+%Y-%m-%d %H:%M:%S') - Restoring default DNS"
# Retirer la protection
chattr -i "$RESOLV_CONF" 2>/dev/null || true
echo "$(date '+%Y-%m-%d %H:%M:%S') - DNS protection removed"
} >> "$LOGFILE" 2>&1
echo "[${SERVICE_NAME}] Default DNS restored"
}
case "$1" in
start)
if [ -f "$PIDFILE" ]; then
echo "[${SERVICE_NAME}] Already running (PID $(cat "$PIDFILE"))"
exit 0
fi
apply_custom_dns
echo $$ > "$PIDFILE"
echo "[${SERVICE_NAME}] Started (PID $(cat "$PIDFILE"))"
;;
stop)
if [ -f "$PIDFILE" ]; then
echo "[${SERVICE_NAME}] Stopping..."
restore_default_dns
rm -f "$PIDFILE"
echo "[${SERVICE_NAME}] Stopped"
else
echo "[${SERVICE_NAME}] Not running"
fi
;;
restart)
echo "[${SERVICE_NAME}] Restarting..."
"$0" stop
sleep 1
"$0" start
;;
status)
ENABLE_STATE=$(is_enabled)
if [ -f "$PIDFILE" ]; then
if chattr -i "$RESOLV_CONF" 2>/dev/null; then
chattr +i "$RESOLV_CONF" 2>/dev/null
echo "[${SERVICE_NAME}] Running (DNS protected) - ${ENABLE_STATE} on boot"
exit 0
else
echo "[${SERVICE_NAME}] Running (DNS not protected) - ${ENABLE_STATE} on boot"
exit 0
fi
else
echo "[${SERVICE_NAME}] Not running - ${ENABLE_STATE} on boot"
exit 1
fi
;;
enable)
current=$(/usr/bin/batocera-settings-get system.services 2>/dev/null)
if echo "$current" | grep -qw "$SERVICE_NAME"; then
echo "[${SERVICE_NAME}] Already enabled on boot"
else
new_value="$current $SERVICE_NAME"
/usr/bin/batocera-settings-set system.services "$new_value"
echo "[${SERVICE_NAME}] Enabled on boot"
fi
;;
disable)
current=$(/usr/bin/batocera-settings-get system.services 2>/dev/null)
if echo "$current" | grep -qw "$SERVICE_NAME"; then
new_value=$(echo "$current" | sed "s/\b$SERVICE_NAME\b//g" | xargs)
/usr/bin/batocera-settings-set system.services "$new_value"
echo "[${SERVICE_NAME}] Disabled on boot"
else
echo "[${SERVICE_NAME}] Already disabled"
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status|enable|disable}"
exit 1
;;
esac
exit 0

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.1.9.1"
app_version = "2.3.2.3"
def get_application_root():

View File

@@ -15,7 +15,9 @@ from utils import (
load_games, check_extension_before_download, is_extension_supported,
load_extensions_json, play_random_music, sanitize_filename,
save_music_config, load_api_keys, _get_dest_folder_name,
extract_zip, extract_rar, find_file_with_or_without_extension, toggle_web_service_at_boot, check_web_service_status,
extract_zip, extract_rar, find_file_with_or_without_extension,
toggle_web_service_at_boot, check_web_service_status,
toggle_custom_dns_at_boot, check_custom_dns_status,
restart_application, generate_support_zip, load_sources,
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
)
@@ -189,21 +191,49 @@ def is_input_matched(event, action_name):
mapping = config.controls_config[action_name]
input_type = mapping["type"]
# Vérifier d'abord le mapping configuré
matched = False
if input_type == "key" and event.type == pygame.KEYDOWN:
return event.key == mapping.get("key")
matched = event.key == mapping.get("key")
elif input_type == "button" and event.type == pygame.JOYBUTTONDOWN:
return event.button == mapping.get("button")
matched = event.button == mapping.get("button")
elif input_type == "axis" and event.type == pygame.JOYAXISMOTION:
axis = mapping.get("axis")
direction = mapping.get("direction")
return event.axis == axis and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction
matched = event.axis == axis and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction
elif input_type == "hat" and event.type == pygame.JOYHATMOTION:
hat_value = mapping.get("value")
if isinstance(hat_value, list):
hat_value = tuple(hat_value)
return event.value == hat_value
matched = event.value == hat_value
elif input_type == "mouse" and event.type == pygame.MOUSEBUTTONDOWN:
return event.button == mapping.get("button")
matched = event.button == mapping.get("button")
# Si déjà matché, retourner True
if matched:
return True
# Fallback clavier pour dépannage (fonctionne toujours même avec manette configurée)
if event.type == pygame.KEYDOWN:
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"start": pygame.K_RALT,
"filter": pygame.K_f,
"history": pygame.K_h,
"clear_history": pygame.K_DELETE,
"delete": pygame.K_d,
"space": pygame.K_SPACE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
if action_name in keyboard_fallback:
return event.key == keyboard_fallback[action_name]
return False
def _launch_next_queued_download():
@@ -593,31 +623,31 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
elif is_input_matched(event, "page_up"):
config.current_game = max(0, config.current_game - config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("page_up", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "left"):
config.current_game = max(0, config.current_game - config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("left", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "page_down"):
config.current_game = min(len(games) - 1, config.current_game + config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "right"):
config.current_game = min(len(games) - 1, config.current_game + config.visible_games)
config.repeat_action = None
config.repeat_key = None
config.repeat_start_time = 0
config.repeat_last_action = current_time
update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else
event.button if event.type == pygame.JOYBUTTONDOWN else
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
event.value)
config.needs_redraw = True
elif is_input_matched(event, "filter"):
config.search_mode = True
@@ -1620,9 +1650,11 @@ def handle_controls(event, sources, joystick, screen):
# Calculer le nombre total d'options selon le système
total = 4 # music, symlink, api keys, back
web_service_index = -1
custom_dns_index = -1
if config.OPERATING_SYSTEM == "Linux":
total = 5 # music, symlink, web_service, api keys, back
total = 6 # music, symlink, web_service, custom_dns, api keys, back
web_service_index = 2
custom_dns_index = 3
if is_input_matched(event, "up"):
config.pause_settings_selection = (sel - 1) % total
@@ -1642,7 +1674,11 @@ def handle_controls(event, sources, joystick, screen):
if music_files and music_folder:
config.current_music = play_random_music(music_files, music_folder, getattr(config, "current_music", None))
else:
pygame.mixer.music.stop()
try:
if pygame.mixer.get_init() is not None:
pygame.mixer.music.stop()
except (AttributeError, NotImplementedError):
pass
config.needs_redraw = True
logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via settings")
# Option 1: Symlink toggle
@@ -1655,7 +1691,6 @@ def handle_controls(event, sources, joystick, screen):
logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via settings")
# Option 2: Web Service toggle (seulement si Linux)
elif sel == web_service_index and web_service_index >= 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
current_status = check_web_service_status()
# Afficher un message de chargement
config.popup_message = _("settings_web_service_enabling") if not current_status else _("settings_web_service_disabling")
@@ -1672,8 +1707,26 @@ def handle_controls(event, sources, joystick, screen):
else:
logger.error(f"Erreur toggle service web: {message}")
threading.Thread(target=toggle_service, daemon=True).start()
# Option 3: Custom DNS toggle (seulement si Linux)
elif sel == custom_dns_index and custom_dns_index >= 0 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")):
current_status = check_custom_dns_status()
# Afficher un message de chargement
config.popup_message = _("settings_custom_dns_enabling") if not current_status else _("settings_custom_dns_disabling")
config.popup_timer = 1000
config.needs_redraw = True
# Exécuter en thread pour ne pas bloquer l'UI
def toggle_dns():
success, message = toggle_custom_dns_at_boot(not current_status)
config.popup_message = message
config.popup_timer = 5000 if success else 7000
config.needs_redraw = True
if success:
logger.info(f"Service custom DNS {'activé' if not current_status else 'désactivé'} au démarrage")
else:
logger.error(f"Erreur toggle service custom DNS: {message}")
threading.Thread(target=toggle_dns, daemon=True).start()
# Option API Keys (index varie selon Linux ou pas)
elif sel == (web_service_index + 1 if web_service_index >= 0 else 2) and is_input_matched(event, "confirm"):
elif sel == (custom_dns_index + 1 if custom_dns_index >= 0 else 2) and is_input_matched(event, "confirm"):
config.menu_state = "pause_api_keys_status"
config.needs_redraw = True
# Option Back (dernière option)
@@ -1999,10 +2052,32 @@ def handle_controls(event, sources, joystick, screen):
# Gestion des relâchements de touches
if event.type == pygame.KEYUP:
# Mapping des touches fallback (pour le dépannage clavier)
keyboard_fallback = {
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"confirm": pygame.K_RETURN,
"cancel": pygame.K_ESCAPE,
"start": pygame.K_RALT, # AltGr
"filter": pygame.K_f,
"history": pygame.K_h,
"clear_history": pygame.K_DELETE,
"delete": pygame.K_d,
"space": pygame.K_SPACE,
"page_up": pygame.K_PAGEUP,
"page_down": pygame.K_PAGEDOWN,
}
# Vérifier quelle touche a été relâchée
for action_name in ["up", "down", "left", "right", "confirm", "cancel"]:
if config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key:
for action_name in ["up", "down", "left", "right", "page_up", "page_down", "confirm", "cancel"]:
# Vérifier le mapping configuré OU le fallback clavier
is_mapped_key = (config.controls_config.get(action_name, {}).get("type") == "key" and \
config.controls_config.get(action_name, {}).get("key") == event.key)
is_fallback_key = (action_name in keyboard_fallback and keyboard_fallback[action_name] == event.key)
if is_mapped_key or is_fallback_key:
update_key_state(action_name, False)
# Gestion spéciale pour confirm dans le menu game
@@ -2094,7 +2169,7 @@ def handle_controls(event, sources, joystick, screen):
elif event.type == pygame.JOYBUTTONUP:
# Vérifier quel bouton a été relâché
for action_name in ["up", "down", "left", "right", "confirm", "cancel"]:
for action_name in ["up", "down", "left", "right", "page_up", "page_down", "confirm", "cancel"]:
if config.controls_config.get(action_name, {}).get("type") == "button" and \
config.controls_config.get(action_name, {}).get("button") == event.button:
update_key_state(action_name, False)
@@ -2188,14 +2263,14 @@ def handle_controls(event, sources, joystick, screen):
elif event.type == pygame.JOYAXISMOTION and abs(event.value) < 0.5:
# Vérifier quel axe a été relâché
for action_name in ["up", "down", "left", "right"]:
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "axis" and \
config.controls_config.get(action_name, {}).get("axis") == event.axis:
update_key_state(action_name, False)
elif event.type == pygame.JOYHATMOTION and event.value == (0, 0):
# Vérifier quel hat a été relâché
for action_name in ["up", "down", "left", "right"]:
for action_name in ["up", "down", "left", "right", "page_up", "page_down"]:
if config.controls_config.get(action_name, {}).get("type") == "hat":
update_key_state(action_name, False)

View File

@@ -201,6 +201,8 @@ THEME_COLORS = {
"text_selected": (0, 255, 0), # utilise le même vert que fond_lignes
# Erreur
"error_text": (255, 0, 0), # rouge
# Succès
"success_text": (0, 255, 0), # vert
# Avertissement
"warning_text": (255, 100, 0), # orange
# Titres
@@ -1169,13 +1171,16 @@ def draw_history_list(screen):
status_text = str(status or "")
# Determine color dedicated to status (independent from selection for better readability)
if status == "Erreur":
if status == "Erreur" or status == "Error":
status_color = THEME_COLORS.get("error_text", (255, 0, 0))
elif status == "Canceled":
status_color = THEME_COLORS.get("warning_text", (255, 100, 0))
elif status == "Download_OK":
elif status == "Download_OK" or status == "Completed":
# Use green OK color
status_color = THEME_COLORS.get("fond_lignes", (0, 255, 0))
status_color = THEME_COLORS.get("success_text", (0, 255, 0))
elif status in ("Downloading", "Téléchargement", "downloading", "Extracting", "Converting", "Queued", "Connecting"):
# En cours - couleur bleue/cyan pour différencier des autres
status_color = THEME_COLORS.get("text_selected", (100, 180, 255))
else:
status_color = THEME_COLORS.get("text", (255, 255, 255))
@@ -2114,7 +2119,7 @@ def draw_pause_games_menu(screen, selected_index):
def draw_pause_settings_menu(screen, selected_index):
from rgsx_settings import get_symlink_option
from utils import check_web_service_status
from utils import check_web_service_status, check_custom_dns_status
# Music
if config.music_enabled:
music_name = config.current_music_name or ""
@@ -2135,10 +2140,16 @@ def draw_pause_settings_menu(screen, selected_index):
# Web Service at boot (only on Linux/Batocera)
web_service_txt = ""
custom_dns_txt = ""
if config.OPERATING_SYSTEM == "Linux":
web_service_enabled = check_web_service_status()
web_service_status = _("settings_web_service_enabled") if web_service_enabled else _("settings_web_service_disabled")
web_service_txt = f"{_('settings_web_service')} : < {web_service_status} >"
# Custom DNS at boot
custom_dns_enabled = check_custom_dns_status()
custom_dns_status = _("settings_custom_dns_enabled") if custom_dns_enabled else _("settings_custom_dns_disabled")
custom_dns_txt = f"{_('settings_custom_dns')} : < {custom_dns_status} >"
api_keys_txt = _("menu_api_keys_status") if _ else "API Keys"
back_txt = _("menu_back") if _ else "Back"
@@ -2147,6 +2158,8 @@ def draw_pause_settings_menu(screen, selected_index):
options = [music_option, symlink_option]
if web_service_txt: # Ajouter seulement si Linux/Batocera
options.append(web_service_txt)
if custom_dns_txt: # Ajouter seulement si Linux/Batocera
options.append(custom_dns_txt)
options.extend([api_keys_txt, back_txt])
_draw_submenu_generic(screen, _("menu_settings_category") if _ else "Settings", options, selected_index)
@@ -2156,6 +2169,8 @@ def draw_pause_settings_menu(screen, selected_index):
]
if web_service_txt:
instruction_keys.append("instruction_settings_web_service")
if custom_dns_txt:
instruction_keys.append("instruction_settings_custom_dns")
instruction_keys.extend([
"instruction_settings_api_keys",
"instruction_generic_back",
@@ -2579,16 +2594,18 @@ def draw_confirm_dialog(screen):
active_downloads = 0
try:
active_downloads = len(getattr(config, 'download_tasks', {}) or {})
queued_downloads = len(getattr(config, 'download_queue', []) or [])
total_downloads = active_downloads + queued_downloads
except Exception:
active_downloads = 0
if active_downloads > 0:
total_downloads = 0
if total_downloads > 0:
# Try translated key if it exists; otherwise fallback to generic message
try:
warn_tpl = _("confirm_exit_with_downloads") # optional key
# If untranslated key returns the same string, still format
message = warn_tpl.format(active_downloads)
message = warn_tpl.format(total_downloads)
except Exception:
message = f"Attention: {active_downloads} téléchargement(s) en cours. Quitter quand même ?"
message = f"Attention: {total_downloads} téléchargement(s) en cours. Quitter quand même ?"
else:
message = _("confirm_exit")
wrapped_message = wrap_text(message, config.font, config.screen_width - 80)

View File

@@ -203,6 +203,7 @@
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
"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_web_service": "Web-Dienst beim Booten",
"settings_web_service_enabled": "Aktiviert",
"settings_web_service_disabled": "Deaktiviert",
@@ -211,6 +212,13 @@
"settings_web_service_success_enabled": "Web-Dienst beim Booten aktiviert",
"settings_web_service_success_disabled": "Web-Dienst beim Booten deaktiviert",
"settings_web_service_error": "Fehler: {0}",
"settings_custom_dns": "Custom DNS beim Booten",
"settings_custom_dns_enabled": "Aktiviert",
"settings_custom_dns_disabled": "Deaktiviert",
"settings_custom_dns_enabling": "Custom DNS wird aktiviert...",
"settings_custom_dns_disabling": "Custom DNS wird deaktiviert...",
"settings_custom_dns_success_enabled": "Custom DNS beim Booten aktiviert (1.1.1.1)",
"settings_custom_dns_success_disabled": "Custom DNS beim Booten deaktiviert",
"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)",
"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)",
"controls_desc_up": "UP ↑",

View File

@@ -205,6 +205,7 @@
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
"instruction_settings_api_keys": "See detected premium provider API keys",
"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_web_service": "Web Service at Boot",
"settings_web_service_enabled": "Enabled",
"settings_web_service_disabled": "Disabled",
@@ -213,6 +214,13 @@
"settings_web_service_success_enabled": "Web service enabled at boot",
"settings_web_service_success_disabled": "Web service disabled at boot",
"settings_web_service_error": "Error: {0}",
"settings_custom_dns": "Custom DNS at Boot",
"settings_custom_dns_enabled": "Enabled",
"settings_custom_dns_disabled": "Disabled",
"settings_custom_dns_enabling": "Enabling custom DNS...",
"settings_custom_dns_disabling": "Disabling custom DNS...",
"settings_custom_dns_success_enabled": "Custom DNS enabled at boot (1.1.1.1)",
"settings_custom_dns_success_disabled": "Custom DNS disabled at boot",
"controls_desc_confirm": "Confirm (e.g. A/Cross)",
"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)",
"controls_desc_up": "UP ↑",

View File

@@ -205,6 +205,7 @@
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
"instruction_settings_api_keys": "Ver claves API premium detectadas",
"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_web_service": "Servicio Web al Inicio",
"settings_web_service_enabled": "Activado",
"settings_web_service_disabled": "Desactivado",
@@ -213,6 +214,13 @@
"settings_web_service_success_enabled": "Servicio web activado al inicio",
"settings_web_service_success_disabled": "Servicio web desactivado al inicio",
"settings_web_service_error": "Error: {0}",
"settings_custom_dns": "DNS Personalizado al Inicio",
"settings_custom_dns_enabled": "Activado",
"settings_custom_dns_disabled": "Desactivado",
"settings_custom_dns_enabling": "Activando DNS personalizado...",
"settings_custom_dns_disabling": "Desactivando DNS personalizado...",
"settings_custom_dns_success_enabled": "DNS personalizado activado al inicio (1.1.1.1)",
"settings_custom_dns_success_disabled": "DNS personalizado desactivado al inicio",
"controls_desc_confirm": "Confirmar (ej. A/Cruz)",
"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)",
"controls_desc_up": "UP ↑",

View File

@@ -205,6 +205,7 @@
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
"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_web_service": "Service Web au démarrage",
"settings_web_service_enabled": "Activé",
"settings_web_service_disabled": "Désactivé",
@@ -213,6 +214,13 @@
"settings_web_service_success_enabled": "Service web activé au démarrage",
"settings_web_service_success_disabled": "Service web désactivé au démarrage",
"settings_web_service_error": "Erreur : {0}",
"settings_custom_dns": "DNS Personnalisé au démarrage",
"settings_custom_dns_enabled": "Activé",
"settings_custom_dns_disabled": "Désactivé",
"settings_custom_dns_enabling": "Activation du DNS personnalisé...",
"settings_custom_dns_disabling": "Désactivation du DNS personnalisé...",
"settings_custom_dns_success_enabled": "DNS personnalisé activé au démarrage (1.1.1.1)",
"settings_custom_dns_success_disabled": "DNS personnalisé désactivé au démarrage",
"controls_desc_confirm": "Valider (ex: A/Croix)",
"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)",
"controls_desc_up": "UP ↑",

View File

@@ -202,6 +202,7 @@
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
"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_web_service": "Servizio Web all'Avvio",
"settings_web_service_enabled": "Abilitato",
"settings_web_service_disabled": "Disabilitato",
@@ -210,6 +211,13 @@
"settings_web_service_success_enabled": "Servizio web abilitato all'avvio",
"settings_web_service_success_disabled": "Servizio web disabilitato all'avvio",
"settings_web_service_error": "Errore: {0}",
"settings_custom_dns": "DNS Personalizzato all'Avvio",
"settings_custom_dns_enabled": "Abilitato",
"settings_custom_dns_disabled": "Disabilitato",
"settings_custom_dns_enabling": "Abilitazione DNS personalizzato...",
"settings_custom_dns_disabling": "Disabilitazione DNS personalizzato...",
"settings_custom_dns_success_enabled": "DNS personalizzato abilitato all'avvio (1.1.1.1)",
"settings_custom_dns_success_disabled": "DNS personalizzato disabilitato all'avvio",
"controls_desc_confirm": "Confermare (es. A/Croce)",
"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)",
"controls_desc_up": "UP ↑",

View File

@@ -204,6 +204,7 @@
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
"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_web_service": "Serviço Web na Inicialização",
"settings_web_service_enabled": "Ativado",
"settings_web_service_disabled": "Desativado",
@@ -212,6 +213,13 @@
"settings_web_service_success_enabled": "Serviço web ativado na inicialização",
"settings_web_service_success_disabled": "Serviço web desativado na inicialização",
"settings_web_service_error": "Erro: {0}",
"settings_custom_dns": "DNS Personalizado na Inicialização",
"settings_custom_dns_enabled": "Ativado",
"settings_custom_dns_disabled": "Desativado",
"settings_custom_dns_enabling": "Ativando DNS personalizado...",
"settings_custom_dns_disabling": "Desativando DNS personalizado...",
"settings_custom_dns_success_enabled": "DNS personalizado ativado na inicialização (1.1.1.1)",
"settings_custom_dns_success_disabled": "DNS personalizado desativado na inicialização",
"controls_desc_confirm": "Confirmar (ex. A/Cruz)",
"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)",
"controls_desc_up": "UP ↑",

View File

@@ -625,7 +625,8 @@ def request_cancel(task_id: str) -> bool:
return False
def cancel_all_downloads():
"""Cancel all active downloads and attempt to stop threads quickly."""
"""Cancel all active downloads and queued downloads, and attempt to stop threads quickly."""
# Annuler tous les téléchargements actifs via cancel_events
for tid, ev in list(cancel_events.items()):
try:
ev.set()
@@ -638,6 +639,22 @@ def cancel_all_downloads():
th.join(timeout=0.2)
except Exception:
pass
# Vider la file d'attente des téléchargements
config.download_queue.clear()
config.download_active = False
# Mettre à jour l'historique pour annuler les téléchargements en statut "Queued"
try:
history = load_history()
for entry in history:
if entry.get("status") == "Queued":
entry["status"] = "Canceled"
entry["message"] = _("download_canceled")
logger.info(f"Téléchargement en attente annulé : {entry.get('game_name', '?')}")
save_history(history)
except Exception as e:
logger.error(f"Erreur lors de l'annulation des téléchargements en attente : {e}")
@@ -1055,14 +1072,14 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
last_update_time = time.time()
last_downloaded = 0
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
download_cancelled = False
download_canceled = False
with open(dest_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
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
result[1] = _("download_canceled") if _ else "Download canceled"
download_cancelled = True
download_canceled = True
try:
f.close()
except Exception:
@@ -1097,7 +1114,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.debug(f"Mise à jour finale de progression: {downloaded}/{total_size} octets")
# Si annulé, ne pas continuer avec extraction
if download_cancelled:
if download_canceled:
return
os.chmod(dest_path, 0o644)
@@ -2077,7 +2094,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
last_update_time = time.time()
last_downloaded = 0
update_interval = 0.1 # Mettre à jour toutes les 0,1 secondes
download_cancelled = False
download_canceled = False
logger.debug(f"Ouverture fichier: {dest_path}")
with open(dest_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
@@ -2085,7 +2102,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
logger.debug(f"Annulation détectée, arrêt du téléchargement 1fichier pour task_id={task_id}")
result[0] = False
result[1] = _("download_canceled") if _ else "Download canceled"
download_cancelled = True
download_canceled = True
try:
f.close()
except Exception:
@@ -2121,7 +2138,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
progress_queues[task_id].put((task_id, downloaded, total_size, speed))
# Si annulé, ne pas continuer avec extraction
if download_cancelled:
if download_canceled:
return
# Déterminer si extraction est nécessaire

View File

@@ -20,7 +20,7 @@ import mimetypes
from datetime import datetime, timezone
from email.utils import formatdate, parsedate_to_datetime
import config
from history import load_history
from history import load_history, save_history
from utils import load_sources, load_games, extract_data
from network import download_rom, download_from_1fichier
from pathlib import Path
@@ -1363,6 +1363,16 @@ class RGSXHandler(BaseHTTPRequestHandler):
try:
cleared_count = len(config.download_queue)
config.download_queue.clear()
# Mettre à jour l'historique pour annuler les téléchargements en statut "Queued"
history = load_history()
for entry in history:
if entry.get("status") == "Queued":
entry["status"] = "Canceled"
entry["message"] = get_translation('download_canceled')
logger.info(f"Téléchargement en attente annulé : {entry.get('game_name', '?')}")
save_history(history)
logger.info(f"📋 Queue vidée ({cleared_count} éléments supprimés)")
self._send_json({
'success': True,
@@ -1394,6 +1404,16 @@ class RGSXHandler(BaseHTTPRequestHandler):
removed_item = config.download_queue.pop(idx)
logger.info(f"📋 {removed_item['game_name']} supprimé de la queue")
found = True
# Mettre à jour l'historique pour cet élément
history = load_history()
for entry in history:
if entry.get('task_id') == task_id and entry.get('status') == 'Queued':
entry['status'] = 'Canceled'
entry['message'] = get_translation('download_canceled')
logger.info(f"Téléchargement en attente annulé dans l'historique : {entry.get('game_name', '?')}")
break
save_history(history)
break
if found:

View File

@@ -34,6 +34,14 @@ logger = logging.getLogger(__name__)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
# Helper pour vérifier si pygame.mixer est disponible
def is_mixer_available():
"""Vérifie si pygame.mixer est disponible et initialisé."""
try:
return pygame is not None and hasattr(pygame, 'mixer') and pygame.mixer.get_init() is not None
except (AttributeError, NotImplementedError):
return False
# Liste globale pour stocker les systèmes avec une erreur 404
unavailable_systems = []
@@ -65,7 +73,8 @@ def restart_application(delay_ms: int = 2000):
if int(delay_ms) <= 0:
try:
try:
pygame.mixer.music.stop()
if is_mixer_available():
pygame.mixer.music.stop()
except Exception:
pass
try:
@@ -300,6 +309,176 @@ def toggle_web_service_at_boot(enable: bool):
return (False, error_msg)
def toggle_custom_dns_at_boot(enable: bool):
"""Active ou désactive le service custom DNS au démarrage de Batocera.
Args:
enable: True pour activer, False pour désactiver
Returns:
tuple: (success: bool, message: str)
"""
try:
# Vérifier si on est sur un système compatible (Linux avec batocera-services)
if config.OPERATING_SYSTEM != "Linux":
return (False, "Custom DNS service is only available on Batocera/Linux systems")
services_dir = "/userdata/system/services"
service_file = os.path.join(services_dir, "custom_dns")
source_file = os.path.join(config.APP_FOLDER, "assets", "progs", "custom_dns")
if enable:
# Mode ENABLE
logger.debug("Activation du service custom DNS au démarrage...")
# 1. Créer le dossier services s'il n'existe pas
try:
os.makedirs(services_dir, exist_ok=True)
logger.debug(f"Dossier services vérifié/créé: {services_dir}")
except Exception as e:
error_msg = f"Failed to create services directory: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
# 2. Copier le fichier custom_dns
try:
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)
shutil.copy2(source_file, service_file)
os.chmod(service_file, 0o755) # Rendre exécutable
logger.debug(f"Fichier service copié et rendu exécutable: {service_file}")
except Exception as e:
error_msg = f"Failed to copy service file: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
# 3. Activer le service avec batocera-services
try:
result = subprocess.run(
['batocera-services', 'enable', 'custom_dns'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
error_msg = f"batocera-services enable failed: {result.stderr}"
logger.error(error_msg)
return (False, error_msg)
logger.debug(f"Service activé: {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 service: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
# 4. Démarrer le service immédiatement
try:
result = subprocess.run(
['batocera-services', 'start', 'custom_dns'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
# Le service peut ne pas démarrer si déjà en cours, ce n'est pas grave
logger.warning(f"batocera-services start warning: {result.stderr}")
else:
logger.debug(f"Service démarré: {result.stdout}")
except Exception as e:
logger.warning(f"Failed to start service (non-critical): {str(e)}")
success_msg = _("settings_custom_dns_success_enabled") if _ else "Custom DNS enabled at boot"
logger.info(success_msg)
# Sauvegarder l'état dans rgsx_settings.json
settings = load_rgsx_settings()
settings["custom_dns_at_boot"] = True
save_rgsx_settings(settings)
return (True, success_msg)
else:
# Mode DISABLE
logger.debug("Désactivation du service custom DNS au démarrage...")
# 1. Désactiver le service avec batocera-services
try:
result = subprocess.run(
['batocera-services', 'disable', 'custom_dns'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
error_msg = f"batocera-services disable failed: {result.stderr}"
logger.error(error_msg)
return (False, error_msg)
logger.debug(f"Service désactivé: {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 disable service: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
# 2. Arrêter le service immédiatement
try:
result = subprocess.run(
['batocera-services', 'stop', 'custom_dns'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
logger.warning(f"batocera-services stop warning: {result.stderr}")
else:
logger.debug(f"Service arrêté: {result.stdout}")
except Exception as e:
logger.warning(f"Failed to stop service (non-critical): {str(e)}")
success_msg = _("settings_custom_dns_success_disabled") if _ else "✓ Custom DNS disabled at boot"
logger.info(success_msg)
# Sauvegarder l'état dans rgsx_settings.json
settings = load_rgsx_settings()
settings["custom_dns_at_boot"] = False
save_rgsx_settings(settings)
return (True, success_msg)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.exception(error_msg)
return (False, error_msg)
def check_custom_dns_status():
"""Vérifie si le service custom DNS est activé au démarrage.
Returns:
bool: True si activé, False sinon
"""
try:
if config.OPERATING_SYSTEM != "Linux":
return False
# Lire l'état depuis rgsx_settings.json
settings = load_rgsx_settings()
return settings.get("custom_dns_at_boot", False)
except Exception as e:
logger.debug(f"Failed to check custom DNS status: {e}")
return False
_extensions_cache = None # type: ignore
_extensions_json_regenerated = False
@@ -1983,8 +2162,9 @@ def handle_xbox(dest_dir, iso_files, url=None):
def play_random_music(music_files, music_folder, current_music=None):
if not getattr(config, "music_enabled", True):
pygame.mixer.music.stop()
if not getattr(config, "music_enabled", True) or not is_mixer_available():
if is_mixer_available():
pygame.mixer.music.stop()
return current_music
if music_files:
# Éviter de rejouer la même musique consécutivement
@@ -1997,11 +2177,12 @@ def play_random_music(music_files, music_folder, current_music=None):
def load_and_play_music():
try:
pygame.mixer.music.load(music_path)
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(loops=0) # Jouer une seule fois
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
if is_mixer_available():
pygame.mixer.music.load(music_path)
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(loops=0) # Jouer une seule fois
pygame.mixer.music.set_endevent(pygame.USEREVENT + 1) # Événement de fin
set_music_popup(music_file) # Afficher le nom de la musique dans la popup
except Exception as e:
logger.error(f"Erreur lors du chargement de la musique {music_path}: {str(e)}")

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.1.9.1"
"version": "2.3.2.3"
}