mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 20:55:24 +02:00
Compare commits
5 Commits
v2.3.1.9.1
...
v2.3.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ace6ec876f | ||
|
|
9f759c1928 | ||
|
|
db287e33d7 | ||
|
|
217392dcd1 | ||
|
|
fd9037139c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
23
README.md
23
README.md
@@ -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.
|
||||
|
||||
[](https://starchart.cc/RetroGameSets/RGSX)
|
||||
|
||||
**Developed with ❤️ for the retro gaming community.**
|
||||
**Developed with ❤️ for the retro gaming community.**
|
||||
|
||||
251
README_CLI.md
251
README_CLI.md
@@ -1,251 +0,0 @@
|
||||
# RGSX CLI — Guide d’utilisation
|
||||
|
||||
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 l’aide 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 d’options (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 d’un 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 s’affichent 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 n’est pas actif, un tableau similaire est affiché (sans le prompt) suivi d’un conseil.
|
||||
|
||||
## Recherche améliorée (multi‑tokens) pour `games`
|
||||
L’option `--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 l’ordre (non contiguë) — priorité 1 (écart le plus faible)
|
||||
3. Tous les tokens présents dans n’importe 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 l’ordre 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 n’est trouvé, seul l’en-tête est affiché puis un message.
|
||||
|
||||
## Prérequis
|
||||
- Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne s’ouvrira).
|
||||
- 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 d’une 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 l’avertissement d’extension non supportée.
|
||||
- `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact n’est 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 l’historique.
|
||||
|
||||
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 l’extension, un avertissement apparaît (utiliser `--force`).
|
||||
|
||||
### 4) history — afficher l’historique
|
||||
- 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 l’historique
|
||||
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
|
||||
```
|
||||
254
README_CLI_EN.md
254
README_CLI_EN.md
@@ -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 doesn’t 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
|
||||
```
|
||||
@@ -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()
|
||||
|
||||
133
ports/RGSX/assets/progs/custom_dns
Normal file
133
ports/RGSX/assets/progs/custom_dns
Normal 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
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ↑",
|
||||
|
||||
@@ -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 ↑",
|
||||
|
||||
@@ -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 ↑",
|
||||
|
||||
@@ -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 ↑",
|
||||
|
||||
@@ -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 ↑",
|
||||
|
||||
@@ -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 ↑",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.3.1.9.1"
|
||||
"version": "2.3.2.3"
|
||||
}
|
||||
Reference in New Issue
Block a user