mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 20:05:28 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14aa7a1c12 | ||
|
|
2c7f6d1751 | ||
|
|
6a27dd0e40 | ||
|
|
69c43d7922 | ||
|
|
1fe8bf6515 | ||
|
|
6b9c3c8348 | ||
|
|
92a8d8567d | ||
|
|
3e9aefc149 | ||
|
|
d7449073d2 | ||
|
|
e0d34304d5 | ||
|
|
65584e411a | ||
|
|
142fffcfb1 | ||
|
|
ffd186f69b | ||
|
|
fd3695f78d | ||
|
|
579d0a1c28 | ||
|
|
60ca7bc375 |
114
README.md
114
README.md
@@ -57,31 +57,44 @@ Download latest release : [RGSX_update_latest.zip](https://github.com/RetroGameS
|
||||
|
||||
### Pause Menu Structure
|
||||
|
||||
**Root categories**
|
||||
- Games (downloads, scans, platform visibility)
|
||||
- Language (switch UI language)
|
||||
- Controls (help and remap)
|
||||
- Display (layout, fonts, monitor/mode, visual options)
|
||||
- Settings (music, symlink, auto extract, network and API status)
|
||||
- Support (generate support ZIP/log bundle)
|
||||
- Quit (exit or restart)
|
||||
|
||||
**Controls**
|
||||
- View Controls Help
|
||||
- Remap Controls
|
||||
- View Controls Help (shows current mapped actions)
|
||||
- Remap Controls (reconfigure keyboard/controller mapping)
|
||||
|
||||
**Display**
|
||||
- Layout (3×3, 3×4, 4×3, 4×4)
|
||||
- Font Size (general UI)
|
||||
- Footer Font Size (controls/version text)
|
||||
- Font Family (pixel fonts)
|
||||
- Hide Unknown Extension Warning
|
||||
- Font Size submenu (general UI + footer text)
|
||||
- Font Family (Pixel or DejaVu)
|
||||
- Monitor selection (when multiple monitors are detected)
|
||||
- Screen Mode (Windows only)
|
||||
- Light Mode (performance-friendly rendering)
|
||||
- Hide Unknown Extension Warning (toggle unsupported extension warnings)
|
||||
|
||||
**Games**
|
||||
- Download History
|
||||
- Source Mode (RGSX / Custom)
|
||||
- Update Game Cache
|
||||
- Show Unsupported Platforms
|
||||
- Hide Premium Systems
|
||||
- Filter Platforms
|
||||
- Update Game Cache (redownload systems/games data)
|
||||
- Scan Owned ROMs (add locally owned ROMs to history)
|
||||
- Download History (view/manage download entries)
|
||||
- Show Unsupported Platforms (toggle platforms without local ROM folders)
|
||||
- Filter Platforms (source/platform visibility menu)
|
||||
|
||||
**Settings**
|
||||
- Background Music Toggle
|
||||
- Symlink Options (Batocera)
|
||||
- Web Service (Batocera)
|
||||
- API Keys Management
|
||||
- Language Selection
|
||||
- Background Music Toggle (enable/disable music)
|
||||
- Symlink Options (choose copy/symlink behavior)
|
||||
- Auto Extract Toggle (automatic archive extraction)
|
||||
- ROMs Folder Selector (set custom ROM root folder)
|
||||
- Web Service (Batocera/Knulli) (start web UI at boot)
|
||||
- Custom DNS (Batocera/Knulli) (workaround for ISP/domain blocking)
|
||||
- API Keys Status (check provider key presence)
|
||||
- Connection Status (test required updates/sources sites)
|
||||
|
||||
---
|
||||
|
||||
@@ -89,18 +102,18 @@ Download latest release : [RGSX_update_latest.zip](https://github.com/RetroGameS
|
||||
|
||||
- 🎯 **Smart System Detection** – Auto-discovers supported systems from `es_systems.cfg`
|
||||
- 📦 **Intelligent Archive Handling** – Auto-extracts archives when systems don't support ZIP files
|
||||
- 🔑 **Premium Unlocking** – 1Fichier API + AllDebrid/Real-Debrid fallback for unlimited downloads
|
||||
- 🔑 **Premium Unlocking** – 1Fichier API + AllDebrid/Debrid-Link/Real-Debrid fallback for unlimited downloads
|
||||
- 🎨 **Fully Customizable** – Layout (3×3 to 4×4), fonts, font sizes (UI + footer), languages (EN/FR/DE/ES/IT/PT)
|
||||
- 🎮 **Controller-First Design** – Auto-mapping for popular controllers + custom remapping support
|
||||
- 🔍 **Advanced Filtering** – Search by name, hide/show unsupported systems, filter platforms
|
||||
- 📊 **Download Management** – Queue system, history tracking, progress notifications
|
||||
- 🌐 **Custom Sources** – Use your own game repository URLs
|
||||
- ♿ **Accessibility** – Separate font scaling for UI and footer, keyboard-only mode support
|
||||
|
||||
> ### 🔑 API Keys Setup
|
||||
> For unlimited 1Fichier downloads, add your API key(s) to `/saves/ports/rgsx/`:
|
||||
> - `1FichierAPI.txt` – 1Fichier API key (recommended)
|
||||
> - `AllDebridAPI.txt` – AllDebrid fallback (optional)
|
||||
> - `DebridLinkAPI.txt` – Debrid-Link fallback (optional)
|
||||
> - `RealDebridAPI.txt` – Real-Debrid fallback (optional)
|
||||
>
|
||||
> **Each file must contain ONLY the key, no extra text.**
|
||||
@@ -112,23 +125,6 @@ Download latest release : [RGSX_update_latest.zip](https://github.com/RetroGameS
|
||||
3. **Queue Download**: Press `X` (West button)
|
||||
4. Track progress in **History** menu or via popup notifications
|
||||
|
||||
### Custom Game Sources
|
||||
|
||||
Switch to custom sources via **Pause Menu > Games > Source Mode**.
|
||||
|
||||
Configure in `/saves/ports/rgsx/rgsx_settings.json`:
|
||||
```json
|
||||
{
|
||||
"sources": {
|
||||
"mode": "custom",
|
||||
"custom_url": "https://example.com/my-sources.zip"
|
||||
}
|
||||
}
|
||||
```
|
||||
**Note**: If custom mode activated but Invalid/empty URL = using /saves/ports/rgsx/games.zip . You need to update games cache on RGSX menu after fixing URL.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Web Interface (Batocera/Knulli Only)
|
||||
|
||||
RGSX includes a web interface that launched automatically when using RGSX for remote browsing and downloading games from any device on your network.
|
||||
@@ -169,30 +165,40 @@ RGSX includes a web interface that launched automatically when using RGSX for re
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
/roms/ports/RGSX/
|
||||
├── __main__.py # Entry point
|
||||
├── controls.py # Input handling
|
||||
├── display.py # Rendering engine
|
||||
├── network.py # Download manager
|
||||
├── rgsx_settings.py # Settings manager
|
||||
├── assets/controls/ # Controller profiles
|
||||
├── languages/ # Translations (EN/FR/DE/ES/IT/PT)
|
||||
└── logs/RGSX.log # Runtime logs
|
||||
|
||||
/roms/windows/RGSX/
|
||||
└── RGSX Retrobat.bat # RetroBat launcher
|
||||
/roms/
|
||||
├── ports/
|
||||
│ ├── RGSX/
|
||||
│ │ ├── __main__.py # Entry point
|
||||
│ │ ├── controls.py # Input handling
|
||||
│ │ ├── display.py # Rendering engine
|
||||
│ │ ├── network.py # Download manager
|
||||
│ │ ├── rgsx_settings.py # Settings manager
|
||||
│ │ ├── assets/controls/ # Controller profiles
|
||||
│ │ ├── languages/ # Translations (EN/FR/DE/ES/IT/PT)
|
||||
│ │ └── logs/RGSX.log # Runtime logs
|
||||
│ ├── gamelist.xml
|
||||
│ ├── images/
|
||||
│ └── videos/
|
||||
└── windows/
|
||||
├── RGSX Retrobat.bat # Launcher for Windows only (can be used without retrobat too)
|
||||
├── gamelist.xml
|
||||
├── images/
|
||||
└── videos/
|
||||
|
||||
/saves/ports/rgsx/
|
||||
├── rgsx_settings.json # User preferences
|
||||
├── controls.json # Control mapping
|
||||
├── history.json # Download history
|
||||
├── rom_extensions.json # Supported extensions cache
|
||||
├── systems_list.json # Detected systems
|
||||
├── global_search_index.json # Global search index cache
|
||||
├── platform_games_count_cache.json
|
||||
├── torrent_manifest_cache.json
|
||||
├── games/ # Game databases (per platform)
|
||||
├── images/ # Platform images
|
||||
├── 1FichierAPI.txt # 1Fichier API key
|
||||
├── AllDebridAPI.txt # AllDebrid API key
|
||||
└── RealDebridAPI.txt # Real-Debrid API key
|
||||
├── 1FichierAPI.txt # 1Fichier API key
|
||||
├── AllDebridAPI.txt # AllDebrid API key
|
||||
├── DebridLinkAPI.txt # Debrid-Link API key
|
||||
└── RealDebridAPI.txt # Real-Debrid API key
|
||||
```
|
||||
|
||||
---
|
||||
@@ -202,11 +208,11 @@ RGSX includes a web interface that launched automatically when using RGSX for re
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Controls not working | Delete `/saves/ports/rgsx/controls.json` + restart app, you can try delete /roms/ports/RGSX/assets/controls/xx.json too |
|
||||
| No games ? | Pause Menu > Games > Update Game Cache |
|
||||
| No games ? | Pause Menu > Games > Update Game Cache, then check Pause Menu > Games > Filter Platforms and Show Unsupported Platforms |
|
||||
| Missing systems on the list? | RGSX read es_systems.cfg to show only supported systems, if you want all systems : Pause Menu > Games > Show unsupported systems |
|
||||
| App crashes | Check `/roms/ports/RGSX/logs/RGSX.log` or `/roms/windows/logs/Retrobat_RGSX_log.txt` |
|
||||
| Layout change not applied | Restart RGSX after changing layout |
|
||||
| Downloading BIOS file is ok but you can't download any games? | Activate custom DNS on Pause Menu> Settings and reboot , server can be blocked by your ISP. check any threat/website protection on your router too, especially on ASUS one|
|
||||
| Problem downloading some Games ? | Open Pause Menu > Settings > Connection Status. If one or more required sites are red, enable Custom DNS in Settings and reboot. Also check ISP/router protections (especially ASUS web threat blocking). |
|
||||
|
||||
**Need help?** Share logs from `/roms/ports/RGSX/logs/` on [Discord](https://discord.gg/Vph9jwg3VV).
|
||||
|
||||
|
||||
219
README_FR.md
219
README_FR.md
@@ -1,16 +1,16 @@
|
||||
# 🎮 Retro Game Sets Xtra (RGSX)
|
||||
|
||||
**[Support / Aide Discord](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[Documentation anglaise](https://github.com/RetroGameSets/RGSX/blob/main/README.md)**
|
||||
**[Support Discord](https://discord.gg/Vph9jwg3VV)** • **[Installation](#-installation)** • **[Documentation anglaise](https://github.com/RetroGameSets/RGSX/blob/main/README.md)** • **[Dépannage / Erreurs courantes](https://github.com/RetroGameSets/RGSX/blob/main/README_FR.md#%EF%B8%8F-d%C3%A9pannage)** •
|
||||
|
||||
Un téléchargeur de ROMs gratuit et facile à utiliser pour Batocera, Knulli et RetroBat avec support multi-sources.
|
||||
Un téléchargeur de ROMs gratuit et simple d'utilisation pour Batocera, Knulli et RetroBat, avec support multi-sources.
|
||||
|
||||
<p align="center">
|
||||
<img width="69%" alt="menu plateformes" src="https://github.com/user-attachments/assets/4464b57b-06a8-45e9-a411-cc12b421545a" />
|
||||
<img width="69%" alt="main" src="https://github.com/user-attachments/assets/a98f1189-9a50-4cc3-b588-3f85245640d8" />
|
||||
<img width="30%" alt="aide contrôles" src="https://github.com/user-attachments/assets/38cac7e6-14f2-4e83-91da-0679669822ee" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img width="49%" alt="interface web" src="https://github.com/user-attachments/assets/71f8bd39-5901-45a9-82b2-91426b3c31a7" />
|
||||
<img width="49%" alt="menu API" src="https://github.com/user-attachments/assets/5bae018d-b7d9-4a95-9f1b-77db751ff24f" />
|
||||
<img width="49%" alt="menu api" src="https://github.com/user-attachments/assets/5bae018d-b7d9-4a95-9f1b-77db751ff24f" />
|
||||
</p>
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ Un téléchargeur de ROMs gratuit et facile à utiliser pour Batocera, Knulli et
|
||||
curl -L bit.ly/rgsx-install | sh
|
||||
```
|
||||
|
||||
Après l'installation :
|
||||
Après installation :
|
||||
1. Mettez à jour les listes de jeux : `Menu > Paramètres des jeux > Mettre à jour la liste des jeux`
|
||||
2. Trouvez RGSX dans **PORTS** ou **Jeux amateurs et portages**
|
||||
2. Trouvez RGSX dans **PORTS** ou **Homebrew and ports**
|
||||
|
||||
### Installation manuelle (Tous systèmes)
|
||||
1. **Télécharger** : [RGSX_full_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
|
||||
@@ -37,9 +37,9 @@ Après l'installation :
|
||||
3. **Rafraîchir** : `Menu > Paramètres des jeux > Mettre à jour la liste des jeux`
|
||||
|
||||
### Mise à jour manuelle (si la mise à jour automatique a échoué)
|
||||
Téléchargez la dernière version : [RGSX_update_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
|
||||
Télécharger la dernière release : [RGSX_update_latest.zip](https://github.com/RetroGameSets/RGSX/releases/latest/download/RGSX_full_latest.zip)
|
||||
|
||||
**Chemins d'installation :**
|
||||
**Chemins installés :**
|
||||
- `/roms/ports/RGSX` (tous systèmes)
|
||||
- `/roms/windows/RGSX` (RetroBat uniquement)
|
||||
|
||||
@@ -49,106 +49,102 @@ Téléchargez la dernière version : [RGSX_update_latest.zip](https://github.com
|
||||
|
||||
### Premier lancement
|
||||
|
||||
- Téléchargement automatique des images systèmes et des listes de jeux
|
||||
- Configuration automatique des contrôles si votre manette est reconnue
|
||||
- **Contrôles cassés ?** Supprimez `/saves/ports/rgsx/controls.json` puis relancez
|
||||
- Télécharge automatiquement les images systèmes et les listes de jeux
|
||||
- Configure automatiquement les contrôles si votre manette est reconnue
|
||||
- **Contrôles cassés ?** Supprimez `/saves/ports/rgsx/controls.json` puis redémarrez
|
||||
|
||||
**Mode clavier** : lorsqu'aucune manette n'est détectée, les contrôles s'affichent sous forme de `[Touche]` au lieu d'icônes.
|
||||
|
||||
### Structure du menu pause
|
||||
|
||||
**Catégories racine**
|
||||
- Jeux (téléchargements, scans, visibilité des plateformes)
|
||||
- Langue (changer la langue de l'interface)
|
||||
- Contrôles (aide et remap)
|
||||
- Affichage (layout, polices, moniteur/mode, options visuelles)
|
||||
- Paramètres (musique, symlink, extraction auto, réseau et statut API)
|
||||
- Support (génération d'une archive support ZIP/logs)
|
||||
- Quitter (quitter ou redémarrer)
|
||||
|
||||
**Contrôles**
|
||||
- Voir l'aide des contrôles
|
||||
- Remapper les contrôles
|
||||
- Voir l'aide des contrôles (affiche les actions actuellement mappées)
|
||||
- Remapper les contrôles (reconfigurer clavier/manette)
|
||||
|
||||
**Affichage**
|
||||
- Disposition (3×3, 3×4, 4×3, 4×4)
|
||||
- Taille de police (UI générale)
|
||||
- Taille de police du footer (texte des contrôles/version)
|
||||
- Famille de police (polices pixel)
|
||||
- Masquer l'avertissement d'extension inconnue
|
||||
- Layout (3×3, 3×4, 4×3, 4×4)
|
||||
- Sous-menu taille de police (UI générale + texte du footer)
|
||||
- Famille de police (Pixel ou DejaVu)
|
||||
- Sélection du moniteur (quand plusieurs moniteurs sont détectés)
|
||||
- Mode d'écran (Windows uniquement)
|
||||
- Mode léger (rendu plus performant)
|
||||
- Masquer l'avertissement d'extension inconnue (toggle des warnings d'extensions non supportées)
|
||||
|
||||
**Jeux**
|
||||
- Historique des téléchargements
|
||||
+- Mode des sources (RGSX / Personnalisé)
|
||||
- Mettre à jour le cache des jeux
|
||||
- Afficher les plateformes non supportées
|
||||
- Masquer les systèmes premium
|
||||
- Filtrer les plateformes
|
||||
- Mettre à jour le cache des jeux (retélécharger les données systèmes/jeux)
|
||||
- Scanner les ROMs possédées (ajouter vos ROMs locales à l'historique)
|
||||
- Historique des téléchargements (consulter/gérer les entrées)
|
||||
- Afficher les plateformes non supportées (toggle des plateformes sans dossier ROM local)
|
||||
- Filtrer les plateformes (menu de visibilité source/plateforme)
|
||||
|
||||
**Paramètres**
|
||||
- Musique de fond (on/off)
|
||||
- Options de symlink (Batocera)
|
||||
- Service web (Batocera)
|
||||
- Gestion des clés API
|
||||
- Sélection de la langue
|
||||
- Musique de fond (activer/désactiver)
|
||||
- Options de symlink (choisir copie/symlink)
|
||||
- Extraction auto (activation/désactivation)
|
||||
- Sélecteur du dossier ROMs (définir un dossier ROM racine personnalisé)
|
||||
- Service Web (Batocera/Knulli) (démarrer l'interface web au boot)
|
||||
- DNS personnalisé (Batocera/Knulli) (contourner certains blocages ISP/domaine)
|
||||
- Statut des clés API (vérifier la présence des clés providers)
|
||||
- Statut de connexion (tester les sites requis updates/sources)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Fonctionnalités
|
||||
|
||||
- 🎯 **Détection intelligente des systèmes** – Découverte automatique des systèmes supportés depuis `es_systems.cfg`
|
||||
- 📦 **Gestion intelligente des archives** – Extraction automatique quand un système ne supporte pas les fichiers ZIP
|
||||
- 🔑 **Débloquage premium** – API 1Fichier + fallback AllDebrid/Real-Debrid pour des téléchargements illimités
|
||||
- 🎨 **Entièrement personnalisable** – Disposition (3×3 à 4×4), polices, tailles de police (UI + footer), langues (EN/FR/DE/ES/IT/PT)
|
||||
- 🎮 **Pensé manette d'abord** – Auto-mapping pour les manettes populaires + remapping personnalisé
|
||||
- 🔍 **Filtrage avancé** – Recherche par nom, affichage/masquage des systèmes non supportés, filtre de plateformes
|
||||
- 🎯 **Détection intelligente des systèmes** – Détecte automatiquement les systèmes supportés depuis `es_systems.cfg`
|
||||
- 📦 **Gestion intelligente des archives** – Extrait automatiquement les archives quand un système ne supporte pas les ZIP
|
||||
- 🔑 **Déblocage premium** – API 1Fichier + fallback AllDebrid/Debrid-Link/Real-Debrid pour des téléchargements illimités
|
||||
- 🎨 **Personnalisation complète** – Layout (3×3 à 4×4), polices, tailles de police (UI + footer), langues (EN/FR/DE/ES/IT/PT)
|
||||
- 🎮 **Pensé manette avant tout** – Auto-mapping pour les manettes populaires + remapping personnalisé
|
||||
- 🔍 **Filtrage avancé** – Recherche par nom, afficher/masquer les systèmes non supportés, filtre de plateformes
|
||||
- 📊 **Gestion des téléchargements** – File d'attente, historique, notifications de progression
|
||||
- 🌐 **Sources personnalisées** – Utilisez vos propres URLs de dépôt de jeux
|
||||
- ♿ **Accessibilité** – Échelles de police séparées pour l'UI et le footer, support du mode clavier seul
|
||||
- ♿ **Accessibilité** – Échelle de police séparée pour l'UI et le footer, support du mode clavier uniquement
|
||||
|
||||
> ### 🔑 Configuration des clés API
|
||||
> Pour des téléchargements 1Fichier illimités, ajoutez vos clés API dans `/saves/ports/rgsx/` :
|
||||
> - `1FichierAPI.txt` – Clé API 1Fichier (recommandé)
|
||||
> - `AllDebridAPI.txt` – Fallback AllDebrid (optionnel)
|
||||
> - `RealDebridAPI.txt` – Fallback Real-Debrid (optionnel)
|
||||
> - `1FichierAPI.txt` – clé API 1Fichier (recommandé)
|
||||
> - `AllDebridAPI.txt` – fallback AllDebrid (optionnel)
|
||||
> - `DebridLinkAPI.txt` – fallback Debrid-Link (optionnel)
|
||||
> - `RealDebridAPI.txt` – fallback Real-Debrid (optionnel)
|
||||
>
|
||||
> **Chaque fichier ne doit contenir QUE la clé, sans texte supplémentaire.**
|
||||
> **Chaque fichier doit contenir UNIQUEMENT la clé, sans texte supplémentaire.**
|
||||
|
||||
### Télécharger des jeux
|
||||
|
||||
1. Parcourez les plateformes → sélectionnez un jeu
|
||||
1. Parcourez les plateformes → Sélectionnez un jeu
|
||||
2. **Téléchargement direct** : appuyez sur `Confirmer`
|
||||
3. **Ajout à la file d'attente** : appuyez sur `X` (bouton Ouest)
|
||||
4. Suivez la progression dans le menu **Historique** ou via les popups de notification
|
||||
3. **File d'attente** : appuyez sur `X` (bouton Ouest)
|
||||
4. Suivez la progression dans le menu **Historique** ou via les notifications popup
|
||||
|
||||
### Sources de jeux personnalisées
|
||||
## 🌐 Interface Web (Batocera/Knulli uniquement)
|
||||
|
||||
Basculez vers les sources personnalisées via **Menu pause > Jeux > Mode des sources**.
|
||||
|
||||
Configurez dans `/saves/ports/rgsx/rgsx_settings.json` :
|
||||
```json
|
||||
{
|
||||
"sources": {
|
||||
"mode": "custom",
|
||||
"custom_url": "https://example.com/my-sources.zip"
|
||||
}
|
||||
}
|
||||
```
|
||||
**Note** : si le mode personnalisé est activé mais que l'URL est invalide/vide = utilisation de `/saves/ports/rgsx/games.zip`. Vous devez mettre à jour le cache des jeux dans le menu RGSX après avoir corrigé l'URL.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Interface web (Batocera/Knulli uniquement)
|
||||
|
||||
RGSX inclut une interface web qui se lance automatiquement avec RGSX pour parcourir et télécharger des jeux à distance depuis n'importe quel appareil de votre réseau.
|
||||
RGSX inclut une interface web qui se lance automatiquement quand vous utilisez RGSX, pour parcourir et télécharger des jeux à distance depuis n'importe quel appareil de votre réseau.
|
||||
|
||||
### Accéder à l'interface web
|
||||
|
||||
1. **Trouvez l'adresse IP de votre Batocera** :
|
||||
- Dans le menu Batocera : `Paramètres réseau`
|
||||
1. **Trouvez l'IP de votre Batocera** :
|
||||
- Vérifiez dans le menu Batocera : `Paramètres réseau`
|
||||
- Ou depuis un terminal : `ip addr show`
|
||||
|
||||
2. **Ouvrez dans un navigateur** : `http://[IP_BATO]:5000` ou `http://BATOCERA:5000`
|
||||
2. **Ouvrez dans un navigateur** : `http://[IP_BATOCERA]:5000` ou `http://BATOCERA:5000`
|
||||
- Exemple : `http://192.168.1.100:5000`
|
||||
|
||||
3. **Accessible depuis n'importe quel appareil** : téléphone, tablette, PC sur le même réseau
|
||||
3. **Disponible depuis n'importe quel appareil** : téléphone, tablette, PC sur le même réseau
|
||||
|
||||
### Fonctionnalités de l'interface web
|
||||
|
||||
- 📱 **Compatible mobile** – Design responsive qui fonctionne sur tous les écrans
|
||||
- 🔍 **Parcourir tous les systèmes** – Voir toutes les plateformes et les jeux
|
||||
- ⬇️ **Téléchargements à distance** – Ajouter des téléchargements directement sur votre Batocera
|
||||
- 📱 **Compatible mobile** – Design responsive sur tous les formats d'écran
|
||||
- 🔍 **Parcourir tous les systèmes** – Voir toutes les plateformes et jeux
|
||||
- ⬇️ **Téléchargements à distance** – Ajouter des téléchargements directement vers Batocera
|
||||
- 📊 **Statut en temps réel** – Voir les téléchargements actifs et l'historique
|
||||
- 🎮 **Même liste de jeux** – Utilise les mêmes sources que l'application principale
|
||||
|
||||
@@ -157,42 +153,52 @@ RGSX inclut une interface web qui se lance automatiquement avec RGSX pour parcou
|
||||
|
||||
**Depuis le menu RGSX**
|
||||
1. Ouvrez le **menu pause** (Start/ALTGr)
|
||||
2. Allez dans **Paramètres > Service web**
|
||||
3. Basculez sur **Activer au démarrage**
|
||||
2. Allez dans **Paramètres > Service Web**
|
||||
3. Activez/Désactivez **Activer au démarrage**
|
||||
4. Redémarrez votre appareil
|
||||
|
||||
|
||||
**Configuration du port** : le service web utilise le port `5000` par défaut. Assurez-vous qu'il n'est pas bloqué par un pare-feu.
|
||||
**Configuration du port** : le service web utilise le port `5000` par défaut. Assurez-vous que ce port n'est pas bloqué par votre pare-feu.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Structure des fichiers
|
||||
|
||||
```
|
||||
/roms/ports/RGSX/
|
||||
├── __main__.py # Point d'entrée
|
||||
├── controls.py # Gestion des entrées
|
||||
├── display.py # Moteur de rendu
|
||||
├── network.py # Gestionnaire de téléchargements
|
||||
├── rgsx_settings.py # Gestionnaire de paramètres
|
||||
├── assets/controls/ # Profils de manettes
|
||||
├── languages/ # Traductions (EN/FR/DE/ES/IT/PT)
|
||||
└── logs/RGSX.log # Logs d'exécution
|
||||
|
||||
/roms/windows/RGSX/
|
||||
└── RGSX Retrobat.bat # Lanceur RetroBat
|
||||
/roms/
|
||||
├── ports/
|
||||
│ ├── RGSX/
|
||||
│ │ ├── __main__.py # Point d'entrée
|
||||
│ │ ├── controls.py # Gestion des entrées
|
||||
│ │ ├── display.py # Moteur de rendu
|
||||
│ │ ├── network.py # Gestionnaire de téléchargements
|
||||
│ │ ├── rgsx_settings.py # Gestionnaire des paramètres
|
||||
│ │ ├── assets/controls/ # Profils de manettes
|
||||
│ │ ├── languages/ # Traductions (EN/FR/DE/ES/IT/PT)
|
||||
│ │ └── logs/RGSX.log # Logs d'exécution
|
||||
│ ├── gamelist.xml
|
||||
│ ├── images/
|
||||
│ └── videos/
|
||||
└── windows/
|
||||
├── RGSX Retrobat.bat # Lanceur Windows uniquement (utilisable même sans RetroBat)
|
||||
├── gamelist.xml
|
||||
├── images/
|
||||
└── videos/
|
||||
|
||||
/saves/ports/rgsx/
|
||||
├── rgsx_settings.json # Préférences utilisateur
|
||||
├── controls.json # Mappage des contrôles
|
||||
├── controls.json # Mapping des contrôles
|
||||
├── history.json # Historique des téléchargements
|
||||
├── rom_extensions.json # Cache des extensions supportées
|
||||
├── systems_list.json # Systèmes détectés
|
||||
├── games/ # Bases de données de jeux (par plateforme)
|
||||
├── images/ # Images des plateformes
|
||||
├── 1FichierAPI.txt # Clé API 1Fichier
|
||||
├── AllDebridAPI.txt # Clé API AllDebrid
|
||||
└── RealDebridAPI.txt # Clé API Real-Debrid
|
||||
├── global_search_index.json # Cache de l'index de recherche globale
|
||||
├── platform_games_count_cache.json
|
||||
├── torrent_manifest_cache.json
|
||||
├── games/ # Bases de données des jeux (par plateforme)
|
||||
├── images/ # Images de plateformes
|
||||
├── 1FichierAPI.txt # Clé API 1Fichier
|
||||
├── AllDebridAPI.txt # Clé API AllDebrid
|
||||
├── DebridLinkAPI.txt # Clé API Debrid-Link
|
||||
└── RealDebridAPI.txt # Clé API Real-Debrid
|
||||
```
|
||||
|
||||
---
|
||||
@@ -201,36 +207,37 @@ RGSX inclut une interface web qui se lance automatiquement avec RGSX pour parcou
|
||||
|
||||
| Problème | Solution |
|
||||
|----------|----------|
|
||||
| Contrôles qui ne répondent plus | Supprimer `/saves/ports/rgsx/controls.json` + redémarrer |
|
||||
| Jeux non affichés | Menu pause > Jeux > Mettre à jour le cache des jeux |
|
||||
| Téléchargement bloqué | Vérifier les clés API dans `/saves/ports/rgsx/` |
|
||||
| Crash de l'application | Vérifier `/roms/ports/RGSX/logs/RGSX.log` |
|
||||
| Changement de layout non pris en compte | Redémarrer RGSX après modification du layout |
|
||||
| Les contrôles ne fonctionnent pas | Supprimez `/saves/ports/rgsx/controls.json` puis redémarrez, vous pouvez aussi supprimer `/roms/ports/RGSX/assets/controls/xx.json` |
|
||||
| Aucun jeu ? | Menu Pause > Jeux > Mettre à jour le cache des jeux, puis vérifier Menu Pause > Jeux > Filtrer les plateformes et Afficher les plateformes non supportées |
|
||||
| Des systèmes manquent dans la liste ? | RGSX lit `es_systems.cfg` pour afficher uniquement les systèmes supportés. Si vous voulez tous les systèmes : Menu Pause > Jeux > Afficher les plateformes non supportées |
|
||||
| L'application crash | Vérifiez `/roms/ports/RGSX/logs/RGSX.log` ou `/roms/windows/logs/Retrobat_RGSX_log.txt` |
|
||||
| Changement de layout non appliqué | Redémarrez RGSX après modification du layout |
|
||||
| Problème de téléchargement de certains jeux ? | Ouvrez Menu Pause > Paramètres > Statut de connexion. Si un ou plusieurs sites requis sont en rouge, activez DNS personnalisé dans Paramètres et redémarrez. Vérifiez aussi les protections ISP/routeur (notamment ASUS web threat blocking). |
|
||||
|
||||
**Besoin d'aide ?** Partagez les logs depuis `/roms/ports/RGSX/logs/` sur [Discord](https://discord.gg/Vph9jwg3VV).
|
||||
**Besoin d'aide ?** Partagez les logs de `/roms/ports/RGSX/logs/` sur [Discord](https://discord.gg/Vph9jwg3VV).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contribution
|
||||
|
||||
- **Rapports de bugs** : ouvrez une issue GitHub avec les logs ou postez sur Discord
|
||||
- **Rapports de bugs** : ouvrez une issue GitHub avec les logs, ou postez sur Discord
|
||||
- **Demandes de fonctionnalités** : discutez d'abord sur Discord, puis ouvrez une issue
|
||||
- **Contributions de code** :
|
||||
- **Contributions code** :
|
||||
```bash
|
||||
git checkout -b feature/your-feature
|
||||
# Testez sur Batocera/RetroBat
|
||||
# Soumettez une Pull Request
|
||||
# Tester sur Batocera/RetroBat
|
||||
# Soumettre une Pull Request
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Licence
|
||||
|
||||
Logiciel libre et open-source. Utilisation, modification et distribution autorisées librement.
|
||||
Logiciel gratuit et open-source. Utilisation, modification et distribution libres.
|
||||
|
||||
## Merci à tous les contributeurs et aux personnes qui suivent l'application
|
||||
## Merci à tous les contributeurs et suiveurs du projet
|
||||
|
||||
**Si vous voulez soutenir mon projet, vous pouvez m'offrir une bière : https://bit.ly/donate-to-rgsx**
|
||||
[](https://starchart.cc/RetroGameSets/RGSX)
|
||||
|
||||
**Développé avec ❤️ pour la communauté du retrogaming.**
|
||||
|
||||
**Développé avec ❤️ pour la communauté retrogaming.**
|
||||
|
||||
@@ -83,7 +83,7 @@ from display import (
|
||||
draw_toast, show_toast, THEME_COLORS, sync_display_metrics
|
||||
)
|
||||
from language import _
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, apply_pending_update, cancel_all_downloads, download_queue_worker
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, apply_pending_update, cancel_all_downloads, shutdown_downloads, download_queue_worker
|
||||
from controls import handle_controls, validate_menu_state, process_key_repeats, get_emergency_controls
|
||||
from controls_mapper import map_controls, draw_controls_mapping, get_actions
|
||||
from controls import load_controls_config
|
||||
@@ -91,19 +91,27 @@ from utils import (
|
||||
load_sources, check_extension_before_download, extract_data,
|
||||
play_random_music, load_music_config, load_api_keys, _refresh_loading_feedback, _format_size_bytes
|
||||
)
|
||||
from history import load_history, save_history, load_downloaded_games
|
||||
from history import load_history, save_history, load_downloaded_games, check_history_write_access, get_history_write_status
|
||||
from config import OTA_data_ZIP
|
||||
from rgsx_settings import get_sources_mode, get_custom_sources_url, get_sources_zip_url, get_display_fullscreen
|
||||
from accessibility import load_accessibility_settings
|
||||
|
||||
# Configuration du logging
|
||||
# RotatingFileHandler : 20 MB max par fichier, 2 backups → 60 MB total maximum.
|
||||
# Évite les fichiers RGSX.log de 1.5 GB causés par le flood aria2c debug (résolu
|
||||
# par aria2_trace_enabled=False dans network.py, mais la rotation sert de garde-fou).
|
||||
try:
|
||||
os.makedirs(config.log_dir, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
filename=config.log_file,
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
from logging.handlers import RotatingFileHandler as _RotatingFileHandler
|
||||
_log_handler = _RotatingFileHandler(
|
||||
config.log_file,
|
||||
maxBytes=20 * 1024 * 1024, # 20 MB
|
||||
backupCount=2,
|
||||
encoding='utf-8',
|
||||
)
|
||||
_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
||||
logging.root.setLevel(logging.DEBUG)
|
||||
logging.root.addHandler(_log_handler)
|
||||
except Exception as e:
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
@@ -209,6 +217,14 @@ except Exception:
|
||||
# fallback: si l'import ou la lecture échoue, conserver la valeur par défaut dans config
|
||||
logger.debug("Impossible de charger nintendo_layout depuis rgsx_settings")
|
||||
|
||||
# Charger la limite de téléchargements simultanés depuis les settings
|
||||
try:
|
||||
from rgsx_settings import get_max_simultaneous_downloads
|
||||
config.max_simultaneous_downloads = get_max_simultaneous_downloads()
|
||||
logger.debug(f"max_simultaneous_downloads initial: {config.max_simultaneous_downloads}")
|
||||
except Exception:
|
||||
logger.debug("Impossible de charger max_simultaneous_downloads depuis rgsx_settings")
|
||||
|
||||
# Détection du système grace a une commande windows / linux (on oublie is non-pc c'est juste pour connaitre le materiel et le systeme d'exploitation)
|
||||
def detect_system_info():
|
||||
"""Détecte les informations système (OS, architecture) via des commandes appropriées."""
|
||||
@@ -335,6 +351,23 @@ config.current_music = current_music # Met à jour la musique en cours dans con
|
||||
config.history = load_history()
|
||||
logger.debug(f"Historique de téléchargement : {len(config.history)} entrées")
|
||||
|
||||
# Vérifier explicitement la capacité d'écriture de history.json
|
||||
history_write_ok, history_write_error_probe = check_history_write_access(force=True)
|
||||
if not history_write_ok:
|
||||
history_write_status = get_history_write_status() or {}
|
||||
history_write_message = history_write_status.get("message") or (
|
||||
"Erreur ecriture history.json. "
|
||||
"Le telechargement continue sans historique temps reel."
|
||||
)
|
||||
logger.error(history_write_message)
|
||||
config.popup_message = history_write_message
|
||||
config.popup_timer = max(int(getattr(config, 'popup_timer', 0) or 0), 5000)
|
||||
config.needs_redraw = True
|
||||
try:
|
||||
show_toast(history_write_message, duration=5000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Chargement des jeux téléchargés
|
||||
config.downloaded_games = load_downloaded_games()
|
||||
|
||||
@@ -508,7 +541,34 @@ async def main():
|
||||
# Démarrer le worker de la queue de téléchargement
|
||||
queue_worker_thread = threading.Thread(target=download_queue_worker, daemon=True)
|
||||
queue_worker_thread.start()
|
||||
|
||||
|
||||
# Reprendre les téléchargements interrompus (statut "Téléchargement"/"Downloading")
|
||||
# Ces entrées proviennent d'une session précédente fermée proprement sans annulation.
|
||||
try:
|
||||
interrupted = [
|
||||
e for e in config.history
|
||||
if e.get("status") in ("Téléchargement", "Downloading") and e.get("url")
|
||||
]
|
||||
if interrupted:
|
||||
logger.info(f"[RESUME] {len(interrupted)} téléchargement(s) interrompu(s) détecté(s), reprise...")
|
||||
for entry in interrupted:
|
||||
_url = entry["url"]
|
||||
_platform = entry.get("platform", "")
|
||||
_game_name = entry.get("game_name", "")
|
||||
_task_id = f"resume_{pygame.time.get_ticks()}_{id(entry)}"
|
||||
_is_zip = entry.get("is_zip_non_supported", False)
|
||||
logger.info(f"[RESUME] Reprise: {_game_name} ({_platform}) task_id={_task_id}")
|
||||
if is_1fichier_url(_url):
|
||||
_coro = download_from_1fichier(_url, _platform, _game_name, _is_zip, _task_id)
|
||||
else:
|
||||
_coro = download_rom(_url, _platform, _game_name, _is_zip, _task_id)
|
||||
config.download_tasks[_task_id] = (
|
||||
asyncio.ensure_future(_coro),
|
||||
_url, _game_name, _platform
|
||||
)
|
||||
except Exception as _e:
|
||||
logger.error(f"[RESUME] Erreur lors de la reprise des téléchargements: {_e}")
|
||||
|
||||
running = True
|
||||
loading_step = "none"
|
||||
ota_update_task = None
|
||||
@@ -1724,11 +1784,12 @@ async def main():
|
||||
pygame.mixer.music.stop()
|
||||
except (AttributeError, NotImplementedError):
|
||||
pass
|
||||
# Cancel any ongoing downloads to prevent lingering background threads
|
||||
# Quitter proprement : vider la queue sans annuler les téléchargements actifs
|
||||
# (les threads daemon s'arrêtent avec Python, l'historique reste en 'Téléchargement')
|
||||
try:
|
||||
cancel_all_downloads()
|
||||
shutdown_downloads()
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de l'annulation globale des téléchargements: {e}")
|
||||
logger.debug(f"Erreur lors du shutdown des téléchargements: {e}")
|
||||
|
||||
# Arrêter le serveur web
|
||||
stop_web_server()
|
||||
|
||||
1
ports/RGSX/assets/TheGamesDBAPI.txt
Normal file
1
ports/RGSX/assets/TheGamesDBAPI.txt
Normal file
@@ -0,0 +1 @@
|
||||
ea388d0d46cd18c3606b1abdba68790b6d7f66ee19ce3bb4f99a26fadafcc77a
|
||||
1
ports/RGSX/assets/images/archive.svg
Normal file
1
ports/RGSX/assets/images/archive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height='86' viewBox='0 0 76 86' width='76' xmlns='http://www.w3.org/2000/svg'><path d='m76 82v4h-76l.00080851-4zm-3-6v5h-70v-5zm-62.6696277-54 .8344146.4217275.4176066 6.7436084.4176065 10.9576581v10.5383496l-.4176065 13.1364492-.0694681 8.8498268-1.1825531.3523804h-4.17367003l-1.25202116-.3523804-.48627608-8.8498268-.41840503-13.0662957v-10.5375432l.41840503-11.028618.38167482-6.7798947.87034634-.3854412zm60.0004653 0 .8353798.4217275.4168913 6.7436084.4168913 10.9576581v10.5383496l-.4168913 13.1364492-.0686832 8.8498268-1.1835879.3523804h-4.1737047l-1.2522712-.3523804-.4879704-8.8498268-.4168913-13.0662957v-10.5375432l.4168913-11.028618.3833483-6.7798947.8697215-.3854412zm-42.000632 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2529447-.3523804-.4863246-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8688361-.3854412zm23 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2521462-.3523804-.4871231-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8696347-.3854412zm21.6697944-9v7h-70v-7zm-35.7200748-13 36.7200748 8.4088317-1.4720205 2.5911683h-70.32799254l-2.19998696-2.10140371z' fill='#2c2c2c' fill-rule='evenodd'/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
20
ports/RGSX/assets/images/edgeemu.svg
Normal file
20
ports/RGSX/assets/images/edgeemu.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="128" height="128" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc" shape-rendering="geometricPrecision">
|
||||
<title id="title">EdgeEmu icon</title>
|
||||
<desc id="desc">EdgeEmu favicon-inspired glyph with bold stacked blocks and cyan accents.</desc>
|
||||
|
||||
<g fill="#1f1f1f">
|
||||
<rect x="6" y="8" width="42" height="24" rx="3"/>
|
||||
<rect x="6" y="38" width="42" height="24" rx="3"/>
|
||||
<rect x="6" y="68" width="42" height="24" rx="3"/>
|
||||
|
||||
<rect x="56" y="10" width="38" height="16" rx="3"/>
|
||||
<rect x="56" y="42" width="38" height="16" rx="3"/>
|
||||
<rect x="56" y="74" width="38" height="16" rx="3"/>
|
||||
</g>
|
||||
|
||||
<g fill="#0388ef">
|
||||
<rect x="12" y="14" width="30" height="8" rx="1.5"/>
|
||||
<rect x="12" y="44" width="30" height="12" rx="1.5"/>
|
||||
<rect x="12" y="76" width="30" height="8" rx="1.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 875 B |
18
ports/RGSX/assets/images/lolroms.svg
Normal file
18
ports/RGSX/assets/images/lolroms.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title desc" role="img">
|
||||
<title id="title">LOL</title>
|
||||
<desc id="desc">Texte LOL style gaming retro</desc>
|
||||
|
||||
<!-- Lettre L (gauche) - rose/violet -->
|
||||
<path d="M35 45 L35 155 L65 155 L65 135 L50 135 L50 45 Z" fill="#ff00ff"/>
|
||||
|
||||
<!-- Lettre O (milieu) - cyan -->
|
||||
<ellipse cx="105" cy="100" rx="29" ry="48" fill="none" stroke="#00ffff" stroke-width="19"/>
|
||||
|
||||
<!-- Lettre L (droite) - rose/violet -->
|
||||
<path d="M145 45 L145 155 L175 155 L175 135 L160 135 L160 45 Z" fill="#ff00ff"/>
|
||||
|
||||
<!-- Effet néon plus prononcé -->
|
||||
<path d="M35 45 L35 155 L65 155 L65 135 L50 135 L50 45 Z" fill="none" stroke="#ff00ff" stroke-width="9" opacity="0.85"/>
|
||||
<ellipse cx="105" cy="100" rx="29" ry="48" fill="none" stroke="#00ffff" stroke-width="9" opacity="0.85"/>
|
||||
<path d="M145 45 L145 155 L175 155 L175 135 L160 135 L160 45 Z" fill="none" stroke="#ff00ff" stroke-width="9" opacity="0.85"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1006 B |
63
ports/RGSX/assets/images/torrent.svg
Normal file
63
ports/RGSX/assets/images/torrent.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="500"
|
||||
height="498"
|
||||
viewBox="0 0 500 497.99999"
|
||||
version="1.1"
|
||||
id="svg20"
|
||||
sodipodi:docname="μTorrent logo with title.svg"
|
||||
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs24" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.40844424"
|
||||
inkscape:cx="634.11348"
|
||||
inkscape:cy="73.449437"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="974"
|
||||
inkscape:window-x="-11"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg20" />
|
||||
<!-- Generator: Sketch 56.3 (81716) - https://sketch.com -->
|
||||
<title
|
||||
id="title2">Group</title>
|
||||
<desc
|
||||
id="desc4">Created with Sketch.</desc>
|
||||
<g
|
||||
id="g186"
|
||||
transform="scale(2.0742981)">
|
||||
<path
|
||||
d="m 137.38383,239.99418 c 44.64832,-6.2824 81.43169,-37.04615 96.37094,-78.28306 -1.17843,0.43717 -2.61663,0.95348 -4.33344,1.46978 -20.0859,5.93191 -34.17804,-4.68447 -37.02056,-6.51981 -2.84253,-1.81651 -5.88835,-4.82769 -6.45686,-4.6694 -1.83729,11.38519 -12.22849,27.19481 -35.94756,34.36661 -12.98901,3.93827 -26.97196,4.02872 -36.94903,-1.56777 l 3.29055,8.26095 c 1.31773,3.29382 3.52774,8.69811 4.89817,11.97687 0,0 8.61793,20.53931 16.14779,34.96583"
|
||||
id="Fill-18"
|
||||
style="fill:#76b83f;fill-rule:evenodd;stroke:none;stroke-width:5.02241" />
|
||||
<path
|
||||
d="M 27.337163,71.292153 62.88564,64.663041 c 3.237841,-0.591683 6.82582,1.247434 7.962829,4.085251 l 24.423106,61.011188 c 3.324434,6.36154 4.002122,7.76725 6.166955,10.48071 0,0 16.86313,24.09695 42.57008,18.21027 17.33374,-3.96466 25.42834,-16.98922 25.97802,-26.25641 0.5685,-2.97349 -0.32002,-6.71579 -1.86364,-10.20936 L 138.86947,55.784027 c -1.17465,-2.668226 0.21837,-5.28746 3.07219,-5.833919 l 29.64506,-5.528656 c 2.71828,-0.48616 5.90718,1.202209 7.09313,3.783756 l 32.401,69.437962 c 1.30643,2.77752 3.95318,7.15673 5.89964,9.70812 0,0 6.73923,9.68928 17.53705,7.85017 2.66934,0 5.7114,-1.44718 5.7114,-1.44718 0.47438,-4.32267 0.72663,-8.70565 0.72663,-13.16023 C 240.95557,53.986366 187.01165,0 120.46273,0 53.932634,0 0,53.986366 0,120.59405 c 0,53.44744 34.739017,98.7583 82.851068,114.57923 -3.249135,-6.69318 -6.53592,-14.0911 -9.227845,-21.31567 L 23.067731,77.872271 c -1.110654,-2.984796 0.835815,-5.935674 4.269432,-6.580118"
|
||||
id="Fill-19"
|
||||
style="fill:#76b83f;fill-rule:evenodd;stroke:none;stroke-width:5.02241" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata26">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Group</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
29
ports/RGSX/assets/images/vimms.svg
Normal file
29
ports/RGSX/assets/images/vimms.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Vimm button</title>
|
||||
<desc id="desc">Circular Vimm-inspired badge with dark blue background, white ring, and red V outlined in blue.</desc>
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="38%" cy="30%" r="70%">
|
||||
<stop offset="0%" stop-color="#10217a"/>
|
||||
<stop offset="58%" stop-color="#08114f"/>
|
||||
<stop offset="100%" stop-color="#030726"/>
|
||||
</radialGradient>
|
||||
<filter id="buttonShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="1.2" stdDeviation="1.4" flood-color="#000000" flood-opacity="0.45"/>
|
||||
</filter>
|
||||
<filter id="vShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0.8" dy="1.1" stdDeviation="0.8" flood-color="#000000" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<circle cx="50" cy="50" r="47" fill="#3f3f3f" opacity="0.5"/>
|
||||
<g filter="url(#buttonShadow)">
|
||||
<circle cx="50" cy="50" r="45.5" fill="url(#bg)" stroke="#ffffff" stroke-width="4.5"/>
|
||||
<circle cx="50" cy="50" r="44" fill="none" stroke="#9aa0b5" stroke-width="0.9" opacity="0.55"/>
|
||||
<ellipse cx="38" cy="23" rx="17" ry="9" fill="#ffffff" opacity="0.08"/>
|
||||
</g>
|
||||
|
||||
<g filter="url(#vShadow)">
|
||||
<path d="M26 18 L44 80 L69 18" fill="none" stroke="#0a2fd8" stroke-width="14" stroke-linecap="square" stroke-linejoin="miter" stroke-miterlimit="10"/>
|
||||
<path d="M26 18 L44 80 L69 18" fill="none" stroke="#ff1b1b" stroke-width="8.5" stroke-linecap="square" stroke-linejoin="miter" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
ports/RGSX/assets/progs/aria2c_linux
Normal file
BIN
ports/RGSX/assets/progs/aria2c_linux
Normal file
Binary file not shown.
@@ -27,7 +27,7 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.6.3.1"
|
||||
app_version = "2.6.4.1"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 1
|
||||
@@ -151,8 +151,12 @@ logger = logging.getLogger(__name__)
|
||||
# File d'attente de téléchargements (jobs en attente)
|
||||
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
|
||||
pending_download_is_queue = False # Indique si pending_download doit être ajouté à la queue
|
||||
# Indique si un téléchargement est en cours
|
||||
# Indique si un téléchargement est en cours (True quand active_download_count > 0)
|
||||
download_active = False
|
||||
# Nombre de téléchargements actuellement actifs (mis à jour par controls.py et network.py)
|
||||
active_download_count = 0
|
||||
# Limite de téléchargements simultanés (chargée depuis rgsx_settings au démarrage, défaut 5)
|
||||
max_simultaneous_downloads = 5
|
||||
|
||||
# Cache status de connexion (menu pause > settings)
|
||||
connection_status = {}
|
||||
@@ -205,6 +209,7 @@ API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.txt")
|
||||
API_KEY_DEBRIDLINK_PATH = os.path.join(SAVE_FOLDER, "DebridLinkAPI.txt")
|
||||
API_KEY_REALDEBRID_PATH = os.path.join(SAVE_FOLDER, "RealDebridAPI.txt")
|
||||
ARCHIVE_ORG_COOKIE_PATH = os.path.join(APP_FOLDER, "assets", "ArchiveOrgCookie.txt")
|
||||
THEGAMESDB_API_KEY_PATH = os.path.join(APP_FOLDER, "assets", "TheGamesDBAPI.txt")
|
||||
|
||||
|
||||
|
||||
@@ -221,6 +226,54 @@ OTA_VERSION_ENDPOINT = "https://raw.githubusercontent.com/RetroGameSets/RGSX/ref
|
||||
OTA_SERVER_URL = "https://retrogamesets.fr/softs/"
|
||||
OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "games.zip")
|
||||
|
||||
# Sites testés dans le menu pause_connection_status.
|
||||
# Pour personnaliser: modifier cette liste (ajouter/supprimer/changer URL, label, catégorie).
|
||||
# Catégories conseillées: "updates" et "sources".
|
||||
CONNECTION_STATUS_TARGETS = [
|
||||
{
|
||||
"key": "retrogamesets",
|
||||
"label": "RetroGameSets",
|
||||
"url": "https://retrogamesets.fr",
|
||||
"category": "updates",
|
||||
},
|
||||
{
|
||||
"key": "github",
|
||||
"label": "GitHub",
|
||||
"url": "https://github.com",
|
||||
"category": "updates",
|
||||
},
|
||||
{
|
||||
"key": "archive",
|
||||
"label": "Archive.org",
|
||||
"url": "https://archive.org",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "1fichier",
|
||||
"label": "1fichier",
|
||||
"url": "https://1fichier.com",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "lolroms",
|
||||
"label": "LolRoms",
|
||||
"url": "https://lolroms.com",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "vimms",
|
||||
"label": "Vimms Lair",
|
||||
"url": "https://vimm.net/",
|
||||
"category": "sources",
|
||||
},
|
||||
{
|
||||
"key": "edgeemu",
|
||||
"label": "EdgeEmu",
|
||||
"url": "https://edgeemu.net",
|
||||
"category": "sources",
|
||||
},
|
||||
]
|
||||
|
||||
#CHEMINS DES EXECUTABLES
|
||||
UNRAR_EXE = os.path.join(APP_FOLDER,"assets","progs","unrar.exe")
|
||||
XISO_EXE = os.path.join(APP_FOLDER,"assets", "progs", "extract-xiso_win.exe")
|
||||
@@ -230,7 +283,7 @@ PS3DEC_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "ps3dec_linux")
|
||||
SEVEN_Z_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "7zz")
|
||||
SEVEN_Z_EXE = os.path.join(APP_FOLDER,"assets", "progs", "7z.exe")
|
||||
ARIA2C_EXE = os.path.join(APP_FOLDER,"assets", "progs", "aria2c.exe")
|
||||
ARIA2C_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "aria2c")
|
||||
ARIA2C_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "aria2c_linux")
|
||||
|
||||
# Détection du système d'exploitation (une seule fois au démarrage)
|
||||
OPERATING_SYSTEM = platform.system()
|
||||
@@ -406,7 +459,9 @@ platform_dicts = [] # Liste des dictionnaires de plateformes
|
||||
selected_filter_index = 0 # index dans la liste visible triée
|
||||
filter_platforms_scroll_offset = 0 # défilement si liste longue
|
||||
filter_platforms_dirty = False # indique si modifications non sauvegardées
|
||||
filter_platforms_selection = [] # copie de travail des plateformes visibles (bool masque?) structure: list of (name, hidden_bool)
|
||||
filter_platforms_selection = [] # copie de travail: list[(platform_name, is_hidden)]
|
||||
filter_platforms_source_map = {} # mapping source -> liste des plateformes associées
|
||||
filter_platforms_expanded_sources = [] # sources ouvertes dans la vue collapsible
|
||||
|
||||
# Affichage des jeux et sélection
|
||||
games: list[Game] = [] # Liste des jeux pour la plateforme actuelle
|
||||
@@ -472,6 +527,11 @@ history = [] # Liste des entrées d'historique avec platform, game_name, status
|
||||
pending_download = None # Objet de téléchargement en attente
|
||||
download_progress = {} # Dictionnaire de progression des téléchargements actifs
|
||||
download_tasks = {} # Dictionnaire pour les tâches de téléchargement
|
||||
history_write_ok = True # Etat courant de l'ecriture de history.json
|
||||
history_write_error = "" # Message explicite si l'ecriture de history.json echoue
|
||||
history_write_failure_count = 0 # Nombre d'echecs consecutifs d'ecriture
|
||||
history_write_last_failure_ts = 0.0 # Timestamp du dernier echec d'ecriture
|
||||
history_write_last_toast_at = 0.0 # Anti-spam pour les toasts d'erreur d'ecriture
|
||||
download_result_message = "" # Message de résultat du dernier téléchargement
|
||||
download_result_error = False # Indicateur d'erreur pour le résultat de téléchargement
|
||||
download_result_start_time = 0 # Timestamp de début du résultat affiché
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import config
|
||||
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE, CONTROLS_CONFIG_PATH, Game
|
||||
from display import draw_validation_transition, show_toast
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel
|
||||
from network import download_rom, download_from_1fichier, is_1fichier_url, request_cancel, cleanup_torrent_temp
|
||||
from utils import (
|
||||
load_games, check_extension_before_download, is_extension_supported,
|
||||
load_extensions_json, play_random_music, sanitize_filename,
|
||||
@@ -18,7 +18,7 @@ from utils import (
|
||||
extract_zip, extract_rar, extract_7z, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
|
||||
restart_application, generate_support_zip, load_sources,
|
||||
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches, remember_history_local_match,
|
||||
clear_torrent_manifest_cache,
|
||||
request_torrent_manifest_refresh,
|
||||
clear_platform_game_count_cache,
|
||||
@@ -32,8 +32,7 @@ from language import _, get_available_languages, set_language
|
||||
from rgsx_settings import (
|
||||
get_allow_unknown_extensions, set_display_grid, get_font_family, set_font_family,
|
||||
get_show_unsupported_platforms, set_show_unsupported_platforms,
|
||||
set_allow_unknown_extensions, get_hide_premium_systems, set_hide_premium_systems,
|
||||
get_sources_mode, set_sources_mode, set_symlink_option, get_symlink_option,
|
||||
set_allow_unknown_extensions, set_symlink_option, get_symlink_option,
|
||||
get_global_sort_option, set_global_sort_option,
|
||||
load_rgsx_settings, save_rgsx_settings
|
||||
)
|
||||
@@ -56,24 +55,14 @@ GLOBAL_SORT_OPTIONS = [
|
||||
|
||||
|
||||
def _notify_torrent_in_maintenance(game_name: str | None = None) -> None:
|
||||
try:
|
||||
message = _("popup_torrent_in_maintenance")
|
||||
except Exception:
|
||||
message = "torrent in maintence"
|
||||
|
||||
show_toast(message, 3000)
|
||||
logger.info(f"Source torrent non telechargeable pour le moment: {game_name or 'unknown game'}")
|
||||
# Fonction devenue inutile, ne fait plus rien
|
||||
pass
|
||||
|
||||
|
||||
def _has_download_url(url, game_name: str | None = None) -> bool:
|
||||
if isinstance(url, str) and url.strip():
|
||||
if parse_torrent_download_url(url) is not None:
|
||||
_notify_torrent_in_maintenance(game_name)
|
||||
config.needs_redraw = True
|
||||
return False
|
||||
return True
|
||||
|
||||
_notify_torrent_in_maintenance(game_name)
|
||||
config.needs_redraw = True
|
||||
return False
|
||||
|
||||
@@ -129,6 +118,72 @@ def _apply_sorted_active_filters() -> list[Game]:
|
||||
return config.games
|
||||
|
||||
|
||||
def _is_windows_os() -> bool:
|
||||
return str(getattr(config, 'OPERATING_SYSTEM', '') or '').lower() == "windows" or os.name == 'nt'
|
||||
|
||||
|
||||
def _is_windows_drive_root(path: str) -> bool:
|
||||
if not _is_windows_os() or not path:
|
||||
return False
|
||||
normalized = os.path.normpath(path)
|
||||
drive, tail = os.path.splitdrive(normalized)
|
||||
return bool(drive) and tail in ('\\', '/')
|
||||
|
||||
|
||||
def _get_available_windows_drives() -> list[str]:
|
||||
drives = []
|
||||
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
||||
drive = f"{letter}:\\"
|
||||
if os.path.isdir(drive):
|
||||
drives.append(drive)
|
||||
return drives
|
||||
|
||||
|
||||
def _load_folder_browser_items(path: str) -> list[str]:
|
||||
if _is_windows_os() and not path:
|
||||
return _get_available_windows_drives()
|
||||
|
||||
target_path = path
|
||||
if not target_path:
|
||||
target_path = "/"
|
||||
|
||||
items = [".."]
|
||||
try:
|
||||
for item in sorted(os.listdir(target_path)):
|
||||
full_path = os.path.join(target_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {target_path}: {e}")
|
||||
return [".."] if target_path else []
|
||||
return items
|
||||
|
||||
|
||||
def _set_folder_browser_location(path: str | None, reset_selection: bool = True) -> None:
|
||||
if _is_windows_os():
|
||||
normalized_path = os.path.normpath(path) if path else ""
|
||||
if normalized_path in ('\\', '/'):
|
||||
normalized_path = ""
|
||||
if normalized_path and not os.path.isdir(normalized_path):
|
||||
normalized_path = ""
|
||||
else:
|
||||
normalized_path = path or "/"
|
||||
if not os.path.isdir(normalized_path):
|
||||
normalized_path = "/"
|
||||
|
||||
config.folder_browser_path = normalized_path
|
||||
config.folder_browser_items = _load_folder_browser_items(normalized_path)
|
||||
|
||||
if reset_selection:
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
else:
|
||||
max_index = max(0, len(config.folder_browser_items) - 1)
|
||||
config.folder_browser_selection = max(0, min(config.folder_browser_selection, max_index))
|
||||
max_scroll = max(0, len(config.folder_browser_items) - max(1, int(getattr(config, 'folder_browser_visible_items', 10) or 10)))
|
||||
config.folder_browser_scroll_offset = max(0, min(config.folder_browser_scroll_offset, max_scroll))
|
||||
|
||||
|
||||
def _build_filter_menu_entries(context: str) -> list[dict[str, str]]:
|
||||
global_search_label = 'Recherche globale' if (_ is None or _("global_search_title") == "global_search_title") else _("global_search_title").format("").replace(" : ", "").rstrip(': ')
|
||||
platform_search_label = 'Recherche sur cette plateforme' if (_ is None or _("platform_search_title") == "platform_search_title") else _("platform_search_title")
|
||||
@@ -491,23 +546,29 @@ def is_global_search_input_matched(event, action_name):
|
||||
|
||||
return False
|
||||
|
||||
def _launch_next_queued_download():
|
||||
"""Lance le prochain téléchargement de la queue si aucun n'est actif.
|
||||
Gère la liaison entre le système Desktop et le système de download_rom/download_from_1fichier.
|
||||
def _launch_next_queued_download(force: bool = False):
|
||||
"""Lance le(s) prochain(s) téléchargement(s) de la queue selon les slots disponibles.
|
||||
Si force=True, ignore la limite (Force Download depuis l'UI).
|
||||
Peut être appelée plusieurs fois pour remplir tous les slots libres.
|
||||
"""
|
||||
if config.download_active or not config.download_queue:
|
||||
max_dl = getattr(config, 'max_simultaneous_downloads', 5)
|
||||
active = getattr(config, 'active_download_count', 0)
|
||||
if not force and active >= max_dl:
|
||||
return
|
||||
|
||||
if not config.download_queue:
|
||||
return
|
||||
|
||||
queue_item = config.download_queue.pop(0)
|
||||
config.active_download_count = active + 1
|
||||
config.download_active = True
|
||||
|
||||
|
||||
url = queue_item['url']
|
||||
platform = queue_item['platform']
|
||||
game_name = queue_item['game_name']
|
||||
is_zip_non_supported = queue_item['is_zip_non_supported']
|
||||
is_1fichier = queue_item['is_1fichier']
|
||||
task_id = queue_item['task_id']
|
||||
|
||||
|
||||
# Mettre à jour le statut dans l'historique: queued -> Downloading
|
||||
for entry in config.history:
|
||||
if entry.get('task_id') == task_id and entry.get('status') == 'Queued':
|
||||
@@ -515,9 +576,9 @@ def _launch_next_queued_download():
|
||||
entry['message'] = _("download_in_progress")
|
||||
save_history(config.history)
|
||||
break
|
||||
|
||||
logger.info(f"📋 Lancement du téléchargement de la queue: {game_name} (task_id={task_id})")
|
||||
|
||||
|
||||
logger.info(f"📋 Lancement téléchargement (slot {config.active_download_count}/{max_dl}): {game_name} (task_id={task_id})")
|
||||
|
||||
# Lancer le téléchargement de manière asynchrone avec callback
|
||||
try:
|
||||
if is_1fichier:
|
||||
@@ -537,17 +598,18 @@ def _launch_next_queued_download():
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur tâche download {game_name}: {e}")
|
||||
finally:
|
||||
# Toujours marquer comme inactif et lancer le prochain
|
||||
config.download_active = False
|
||||
if config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
# Décrémenter le compteur et lancer le prochain si queue non vide
|
||||
config.active_download_count = max(0, getattr(config, 'active_download_count', 1) - 1)
|
||||
config.download_active = config.active_download_count > 0
|
||||
_launch_next_queued_download()
|
||||
|
||||
# Ajouter le callback à la tâche
|
||||
task.add_done_callback(on_task_done)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lancement queue download: {e}")
|
||||
config.download_active = False
|
||||
config.active_download_count = max(0, getattr(config, 'active_download_count', 1) - 1)
|
||||
config.download_active = config.active_download_count > 0
|
||||
# Mettre à jour l'historique en erreur
|
||||
for entry in config.history:
|
||||
if entry.get('task_id') == task_id:
|
||||
@@ -556,8 +618,7 @@ def _launch_next_queued_download():
|
||||
save_history(config.history)
|
||||
break
|
||||
# Relancer le suivant
|
||||
if config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
_launch_next_queued_download()
|
||||
|
||||
def filter_games_by_search_query() -> list[Game]:
|
||||
base_games = config.games
|
||||
@@ -936,8 +997,43 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"{game_name} ajoute a la file d'attente depuis la recherche globale. Queue size: {len(config.download_queue)}")
|
||||
|
||||
if not config.download_active and config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
_launch_next_queued_download()
|
||||
return
|
||||
|
||||
# Téléchargement direct, démarrer immédiatement,
|
||||
# sinon mettre automatiquement en queue (même comportement que queue_only).
|
||||
max_dl = getattr(config, 'max_simultaneous_downloads', 5)
|
||||
active = getattr(config, 'active_download_count', 0)
|
||||
if active >= max_dl:
|
||||
# Plus de slots disponibles → auto-queue
|
||||
task_id = str(pygame.time.get_ticks())
|
||||
queue_item = {
|
||||
'url': url,
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'is_zip_non_supported': pending_download[3],
|
||||
'is_1fichier': is_1fichier_url(url),
|
||||
'task_id': task_id,
|
||||
'status': 'Queued'
|
||||
}
|
||||
config.download_queue.append(queue_item)
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': display_name,
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
'message': _("download_queued"),
|
||||
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'downloaded_size': 0,
|
||||
'total_size': 0,
|
||||
'task_id': task_id
|
||||
})
|
||||
save_history(config.history)
|
||||
show_toast(f"{display_name}\n{_('download_queued')}")
|
||||
config.needs_redraw = True
|
||||
logger.info(f"{game_name} auto-queue (slots {active}/{max_dl} utilisés)")
|
||||
return
|
||||
|
||||
if is_1fichier_url(url):
|
||||
@@ -950,11 +1046,12 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
task_id = str(pygame.time.get_ticks())
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, pending_download[3], task_id))
|
||||
|
||||
config.active_download_count = getattr(config, 'active_download_count', 0) + 1
|
||||
config.download_active = True
|
||||
config.download_tasks[task_id] = (task, url, game_name, platform)
|
||||
show_toast(f"{_('download_started')}: {display_name}")
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Telechargement demarre depuis la recherche globale: {game_name} pour {platform}, task_id={task_id}")
|
||||
...
|
||||
|
||||
def handle_controls(event, sources, joystick, screen):
|
||||
"""Gère un événement clavier/joystick/souris et la répétition automatique.
|
||||
@@ -1512,8 +1609,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
logger.debug(f"{game_name} ajouté à la file d'attente. Queue size: {len(config.download_queue)}")
|
||||
|
||||
# Si aucun téléchargement actif, lancer le premier de la queue
|
||||
if not config.download_active and config.download_queue:
|
||||
_launch_next_queued_download()
|
||||
_launch_next_queued_download()
|
||||
else:
|
||||
logger.error(f"config.pending_download est None pour {game_name}")
|
||||
config.needs_redraw = True
|
||||
@@ -1726,6 +1822,14 @@ def handle_controls(event, sources, joystick, screen):
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur lors de l'envoi du signal d'annulation: {e}")
|
||||
|
||||
# Supprimer le dossier temp torrent (cas où cancel_events est vide :
|
||||
# téléchargement déjà terminé côté thread mais UI encore "Téléchargement").
|
||||
try:
|
||||
if cleanup_torrent_temp(task_id):
|
||||
logger.debug(f"Dossier temp torrent supprimé pour task_id={task_id}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur nettoyage temp torrent: {e}")
|
||||
|
||||
# Annuler aussi la tâche asyncio si elle existe (pour les téléchargements directs)
|
||||
for tid, (task, task_url, tname, tplatform) in list(config.download_tasks.items()):
|
||||
if tid == task_id or task_url == url:
|
||||
@@ -1808,11 +1912,29 @@ def handle_controls(event, sources, joystick, screen):
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
local_path = entry.get("local_path")
|
||||
local_filename = entry.get("local_filename")
|
||||
if not file_exists and local_path and os.path.isfile(local_path):
|
||||
actual_filename = os.path.basename(local_path)
|
||||
actual_path = local_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS] direct local_path match used: %s", actual_path)
|
||||
elif not file_exists and local_filename:
|
||||
local_filename_path = os.path.join(base_path, str(local_filename))
|
||||
if os.path.isfile(local_filename_path):
|
||||
actual_filename = os.path.basename(local_filename_path)
|
||||
actual_path = local_filename_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS] direct local_filename match used: %s", actual_path)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
if file_exists and actual_path:
|
||||
remember_history_local_match(entry, actual_filename, actual_path)
|
||||
config.history_actual_matches = actual_matches
|
||||
|
||||
# Stocker les informations pour les autres handlers
|
||||
@@ -1828,6 +1950,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Options selon statut
|
||||
if status == "Queued":
|
||||
# En attente dans la queue
|
||||
options.append("force_download")
|
||||
options.append("remove_from_queue")
|
||||
elif status in ["Downloading", "Téléchargement", "Extracting", "Paused"]:
|
||||
# Téléchargement en cours ou en pause - ajouter pause/resume avant cancel
|
||||
@@ -1852,6 +1975,31 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Option commune: retour
|
||||
options.append("back")
|
||||
|
||||
diagnostics_signature = (
|
||||
entry.get("url", ""),
|
||||
status,
|
||||
file_exists,
|
||||
actual_filename or "",
|
||||
actual_path or "",
|
||||
tuple(options),
|
||||
)
|
||||
if getattr(config, 'history_options_diagnostics_signature', None) != diagnostics_signature:
|
||||
config.history_options_diagnostics_signature = diagnostics_signature
|
||||
logger.debug(
|
||||
"[HISTORY_OPTIONS] platform=%s game=%s status=%s dest_folder=%s base_path=%s file_exists=%s actual_filename=%s actual_path=%s local_path=%s moved_paths=%s options=%s",
|
||||
platform,
|
||||
game_name,
|
||||
status,
|
||||
dest_folder,
|
||||
base_path,
|
||||
file_exists,
|
||||
actual_filename,
|
||||
actual_path,
|
||||
entry.get("local_path"),
|
||||
entry.get("moved_paths"),
|
||||
options,
|
||||
)
|
||||
|
||||
total_options = len(options)
|
||||
sel = getattr(config, 'history_game_option_selection', 0)
|
||||
@@ -1876,7 +2024,22 @@ def handle_controls(event, sources, joystick, screen):
|
||||
selected_option = options[sel]
|
||||
logger.debug(f"history_game_options: CONFIRM option={selected_option}")
|
||||
|
||||
if selected_option == "remove_from_queue":
|
||||
if selected_option == "force_download":
|
||||
# Forcer le démarrage immédiat d'un téléchargement en file d'attente,
|
||||
# en ignorant la limite de slots simultanés.
|
||||
task_id = entry.get("task_id")
|
||||
url = entry.get("url")
|
||||
# Retirer cet item de la queue (où qu'il soit)
|
||||
for i, qi in enumerate(config.download_queue):
|
||||
if qi.get("task_id") == task_id or qi.get("url") == url:
|
||||
config.download_queue.insert(0, config.download_queue.pop(i))
|
||||
break
|
||||
_launch_next_queued_download(force=True)
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
logger.info(f"Force Download: {game_name} (task_id={task_id})")
|
||||
|
||||
elif selected_option == "remove_from_queue":
|
||||
# Retirer de la queue
|
||||
task_id = entry.get("task_id")
|
||||
url = entry.get("url")
|
||||
@@ -2103,16 +2266,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.folder_browser_mode = "history_move"
|
||||
config.platform_config_name = entry.get("display_name") or get_clean_display_name(entry.get("game_name", ""), entry.get("platform", ""))
|
||||
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(start_path)):
|
||||
full_path = os.path.join(start_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {start_path}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
_set_folder_browser_location(start_path, reset_selection=True)
|
||||
|
||||
config.menu_state = "folder_browser"
|
||||
config.needs_redraw = True
|
||||
@@ -2362,16 +2516,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.confirm_selection == 0: # Quit RGSX
|
||||
# Mark all in-progress downloads as canceled in history
|
||||
try:
|
||||
for entry in getattr(config, 'history', []) or []:
|
||||
if entry.get("status") in ["Downloading", "Téléchargement", "Extracting"]:
|
||||
entry["status"] = "Canceled"
|
||||
entry["progress"] = 0
|
||||
entry["message"] = _("download_canceled") if _ else "Download canceled"
|
||||
save_history(config.history)
|
||||
except Exception:
|
||||
pass
|
||||
# Les téléchargements actifs restent en état "Téléchargement" dans l'historique
|
||||
# pour permettre la reprise au prochain démarrage (fichiers .aria2 conservés).
|
||||
return "quit"
|
||||
elif config.confirm_selection == 1: # Restart RGSX
|
||||
restart_application(2000)
|
||||
@@ -2763,7 +2909,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Sous-menu Games
|
||||
elif config.menu_state == "pause_games_menu":
|
||||
sel = getattr(config, 'pause_games_selection', 0)
|
||||
total = 8 # update cache, scan roms, history, source, unsupported, hide premium, filter, back
|
||||
total = 6 # update cache, scan roms, history, unsupported, filter, back
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_games_selection = (sel - 1) % total
|
||||
config.needs_redraw = True
|
||||
@@ -2794,22 +2940,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.previous_menu_state = "pause_games_menu"
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # source mode
|
||||
try:
|
||||
current_mode = get_sources_mode()
|
||||
new_mode = set_sources_mode('custom' if current_mode == 'rgsx' else 'rgsx')
|
||||
config.sources_mode = new_mode
|
||||
if new_mode == 'custom':
|
||||
config.popup_message = _("sources_mode_custom_select_info").format(config.RGSX_SETTINGS_PATH)
|
||||
config.popup_timer = 10000
|
||||
else:
|
||||
config.popup_message = _("sources_mode_rgsx_select_info")
|
||||
config.popup_timer = 4000
|
||||
config.needs_redraw = True
|
||||
logger.info(f"Changement du mode des sources vers {new_mode}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement mode sources: {e}")
|
||||
elif sel == 4 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
|
||||
elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # unsupported toggle
|
||||
try:
|
||||
current = get_show_unsupported_platforms()
|
||||
new_val = set_show_unsupported_platforms(not current)
|
||||
@@ -2819,22 +2950,15 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle unsupported: {e}")
|
||||
elif sel == 5 and (is_input_matched(event, "confirm") or is_input_matched(event, "left") or is_input_matched(event, "right")): # hide premium
|
||||
try:
|
||||
cur = get_hide_premium_systems()
|
||||
new_val = set_hide_premium_systems(not cur)
|
||||
config.popup_message = ("Premium hidden" if new_val else "Premium visible") if _ is None else (_("popup_hide_premium_on") if new_val else _("popup_hide_premium_off"))
|
||||
config.popup_timer = 2500
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle hide_premium_systems: {e}")
|
||||
elif sel == 6 and is_input_matched(event, "confirm"): # filter platforms
|
||||
elif sel == 4 and is_input_matched(event, "confirm"): # filter platforms
|
||||
config.filter_return_to = "pause_games_menu"
|
||||
config.menu_state = "filter_platforms"
|
||||
config.selected_filter_index = 0
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.filter_platforms_source_map = {}
|
||||
config.filter_platforms_expanded_sources = []
|
||||
config.needs_redraw = True
|
||||
elif sel == 7 and is_input_matched(event, "confirm"): # back
|
||||
elif sel == 5 and is_input_matched(event, "confirm"): # back
|
||||
config.menu_state = "pause_menu"
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
@@ -2846,24 +2970,24 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Sous-menu Settings
|
||||
elif config.menu_state == "pause_settings_menu":
|
||||
sel = getattr(config, 'pause_settings_selection', 0)
|
||||
# Calculer le nombre total d'options selon le système
|
||||
# Liste des options : music, symlink, auto_extract, roms_folder, [web_service], [custom_dns], api keys, connection_status, back
|
||||
total = 7 # music, symlink, auto_extract, roms_folder, api keys, connection_status, back (Windows)
|
||||
# Liste des options : music, symlink, auto_extract, roms_folder, max_dl_slots, [web_service], [custom_dns], api keys, connection_status, back
|
||||
total = 8 # music, symlink, auto_extract, roms_folder, max_dl_slots, api keys, connection_status, back (Windows)
|
||||
auto_extract_index = 2
|
||||
roms_folder_index = 3
|
||||
max_dl_index = 4
|
||||
web_service_index = -1
|
||||
custom_dns_index = -1
|
||||
api_keys_index = 4
|
||||
connection_status_index = 5
|
||||
back_index = 6
|
||||
|
||||
api_keys_index = 5
|
||||
connection_status_index = 6
|
||||
back_index = 7
|
||||
|
||||
if config.OPERATING_SYSTEM == "Linux":
|
||||
total = 9 # music, symlink, auto_extract, roms_folder, web_service, custom_dns, api keys, connection_status, back
|
||||
web_service_index = 4
|
||||
custom_dns_index = 5
|
||||
api_keys_index = 6
|
||||
connection_status_index = 7
|
||||
back_index = 8
|
||||
total = 10 # music, symlink, auto_extract, roms_folder, max_dl_slots, web_service, custom_dns, api keys, connection_status, back
|
||||
web_service_index = 5
|
||||
custom_dns_index = 6
|
||||
api_keys_index = 7
|
||||
connection_status_index = 8
|
||||
back_index = 9
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_settings_selection = (sel - 1) % total
|
||||
@@ -2917,17 +3041,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
config.folder_browser_mode = "roms_root"
|
||||
# Charger la liste des dossiers
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(start_path)):
|
||||
full_path = os.path.join(start_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {start_path}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
_set_folder_browser_location(start_path, reset_selection=True)
|
||||
config.menu_state = "folder_browser"
|
||||
config.needs_redraw = True
|
||||
logger.info("Ouverture navigateur dossier ROMs principal")
|
||||
@@ -2941,7 +3055,21 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.popup_timer = 5000
|
||||
logger.info("Dossier ROMs réinitialisé par défaut")
|
||||
config.needs_redraw = True
|
||||
# Option 4: Web Service toggle (seulement si Linux)
|
||||
# Option 4: Max simultaneous downloads (left/right pour -/+)
|
||||
elif sel == max_dl_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
from rgsx_settings import get_max_simultaneous_downloads, set_max_simultaneous_downloads
|
||||
current_max = get_max_simultaneous_downloads()
|
||||
if is_input_matched(event, "left"):
|
||||
new_max = max(1, current_max - 1)
|
||||
else:
|
||||
new_max = min(10, current_max + 1)
|
||||
if new_max != current_max:
|
||||
set_max_simultaneous_downloads(new_max)
|
||||
config.popup_message = _("settings_max_dl_changed").format(new_max) if _ else f"Max simultaneous downloads: {new_max}"
|
||||
config.popup_timer = 1500
|
||||
config.needs_redraw = True
|
||||
logger.info(f"max_simultaneous_downloads réglé à {new_max}")
|
||||
# Option 5: 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()
|
||||
@@ -3131,6 +3259,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "filter_platforms"
|
||||
config.selected_filter_index = 0
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.filter_platforms_source_map = {}
|
||||
config.filter_platforms_expanded_sources = []
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "pause_menu"
|
||||
@@ -3241,17 +3371,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
config.folder_browser_mode = "platform"
|
||||
# Charger la liste des dossiers
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(current_path)):
|
||||
full_path = os.path.join(current_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {current_path}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
_set_folder_browser_location(current_path, reset_selection=True)
|
||||
config.menu_state = "folder_browser"
|
||||
config.needs_redraw = True
|
||||
elif config.platform_folder_selection == 2: # Reset
|
||||
@@ -3307,44 +3427,31 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.folder_browser_items:
|
||||
selected_item = config.folder_browser_items[config.folder_browser_selection]
|
||||
if selected_item == "..":
|
||||
# Remonter d'un niveau
|
||||
parent = os.path.dirname(config.folder_browser_path)
|
||||
if parent and parent != config.folder_browser_path:
|
||||
config.folder_browser_path = parent
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(parent)):
|
||||
full_path = os.path.join(parent, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {parent}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
current_path = config.folder_browser_path
|
||||
if _is_windows_os() and _is_windows_drive_root(current_path):
|
||||
parent = ""
|
||||
else:
|
||||
parent = os.path.dirname(current_path) if current_path else ("" if _is_windows_os() else "/")
|
||||
if parent != current_path:
|
||||
_set_folder_browser_location(parent, reset_selection=True)
|
||||
else:
|
||||
# Entrer dans le dossier sélectionné
|
||||
new_path = os.path.join(config.folder_browser_path, selected_item)
|
||||
if _is_windows_os() and not config.folder_browser_path:
|
||||
new_path = selected_item
|
||||
else:
|
||||
new_path = os.path.join(config.folder_browser_path, selected_item)
|
||||
if os.path.isdir(new_path):
|
||||
config.folder_browser_path = new_path
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(new_path)):
|
||||
full_path = os.path.join(new_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {new_path}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
_set_folder_browser_location(new_path, reset_selection=True)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "history"):
|
||||
# Valider et sélectionner le dossier actuel (touche X/Y)
|
||||
browser_mode = getattr(config, 'folder_browser_mode', 'platform')
|
||||
selected_path = config.folder_browser_path
|
||||
|
||||
if not selected_path or not os.path.isdir(selected_path):
|
||||
config.popup_message = "Please enter a drive/folder first"
|
||||
config.popup_timer = 2200
|
||||
config.needs_redraw = True
|
||||
return action
|
||||
|
||||
if browser_mode == "roms_root":
|
||||
# Mode dossier ROMs principal
|
||||
@@ -3471,22 +3578,11 @@ def handle_controls(event, sources, joystick, screen):
|
||||
logger.info(f"Dossier créé: {new_folder_path}")
|
||||
config.popup_message = _("folder_created").format(folder_name) if _ else f"Folder created: {folder_name}"
|
||||
config.popup_timer = 2000
|
||||
# Rafraîchir la liste des dossiers et sélectionner le nouveau
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(config.folder_browser_path)):
|
||||
full_path = os.path.join(config.folder_browser_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
# Sélectionner le nouveau dossier
|
||||
if folder_name in items:
|
||||
config.folder_browser_selection = items.index(folder_name)
|
||||
# Ajuster le scroll si nécessaire
|
||||
if config.folder_browser_selection >= config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur rafraîchissement liste: {e}")
|
||||
config.folder_browser_items = _load_folder_browser_items(config.folder_browser_path)
|
||||
if folder_name in config.folder_browser_items:
|
||||
config.folder_browser_selection = config.folder_browser_items.index(folder_name)
|
||||
if config.folder_browser_selection >= config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur création dossier {new_folder_path}: {e}")
|
||||
config.popup_message = _("folder_create_error").format(str(e)) if _ else f"Error: {e}"
|
||||
@@ -3944,109 +4040,89 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Menu filtre plateformes
|
||||
elif config.menu_state == "filter_platforms":
|
||||
total_items = len(config.filter_platforms_selection)
|
||||
action_buttons = 4
|
||||
# Indices: 0-3 = boutons, 4+ = liste des systèmes
|
||||
extended_max = action_buttons + total_items - 1
|
||||
if is_input_matched(event, "up"):
|
||||
if config.selected_filter_index > 0:
|
||||
config.selected_filter_index -= 1
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Wrap vers le bas (dernière ligne de la liste)
|
||||
config.selected_filter_index = extended_max
|
||||
config.needs_redraw = True
|
||||
# Activer la répétition automatique
|
||||
update_key_state("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)
|
||||
elif is_input_matched(event, "down"):
|
||||
if config.selected_filter_index < extended_max:
|
||||
config.selected_filter_index += 1
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Wrap retour en haut (premier bouton)
|
||||
def _extract_source(platform_name: str) -> str:
|
||||
match = re.search(r'\(([^()]+)\)\s*$', str(platform_name).strip())
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
fallback = _("games_source_rgsx") if _ else "RGSX"
|
||||
return fallback if fallback != "games_source_rgsx" else "RGSX"
|
||||
|
||||
def _build_source_map() -> dict:
|
||||
source_map_local = {}
|
||||
for entry in config.platform_dicts:
|
||||
platform_name = entry.get("platform_name", "") if isinstance(entry, dict) else ""
|
||||
platform_name = str(platform_name).strip()
|
||||
if not platform_name:
|
||||
continue
|
||||
source_name = _extract_source(platform_name)
|
||||
source_map_local.setdefault(source_name, []).append(platform_name)
|
||||
|
||||
normalized = {}
|
||||
for source_name in sorted(source_map_local.keys(), key=lambda s: str(s).lower()):
|
||||
unique_names = sorted(set(source_map_local.get(source_name, [])), key=lambda s: str(s).lower())
|
||||
normalized[source_name] = unique_names
|
||||
return normalized
|
||||
|
||||
def _all_platform_names(source_map_local: dict) -> list:
|
||||
names = []
|
||||
for source_name in sorted(source_map_local.keys(), key=lambda s: str(s).lower()):
|
||||
names.extend(source_map_local.get(source_name, []))
|
||||
return names
|
||||
|
||||
def _ensure_working_selection(source_map_local: dict) -> None:
|
||||
all_platform_names = _all_platform_names(source_map_local)
|
||||
current = getattr(config, 'filter_platforms_selection', [])
|
||||
current_map = {}
|
||||
if isinstance(current, list):
|
||||
for item in current:
|
||||
if isinstance(item, (list, tuple)) and len(item) == 2:
|
||||
name = str(item[0]).strip()
|
||||
if name:
|
||||
current_map[name] = bool(item[1])
|
||||
|
||||
expected_set = set(all_platform_names)
|
||||
if set(current_map.keys()) != expected_set:
|
||||
settings_local = load_rgsx_settings()
|
||||
hidden_platforms = set(settings_local.get("hidden_platforms", [])) if isinstance(settings_local, dict) else set()
|
||||
config.filter_platforms_selection = [(name, name in hidden_platforms) for name in all_platform_names]
|
||||
config.selected_filter_index = 0
|
||||
config.needs_redraw = True
|
||||
# Activer la répétition automatique
|
||||
update_key_state("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)
|
||||
elif is_input_matched(event, "left"):
|
||||
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
|
||||
if config.selected_filter_index < action_buttons:
|
||||
if config.selected_filter_index > 0:
|
||||
config.selected_filter_index -= 1
|
||||
config.needs_redraw = True
|
||||
# sinon ignorer (dans la liste)
|
||||
elif is_input_matched(event, "right"):
|
||||
# Navigation gauche/droite uniquement pour les boutons (indices 0-3)
|
||||
if config.selected_filter_index < action_buttons:
|
||||
if config.selected_filter_index < action_buttons - 1:
|
||||
config.selected_filter_index += 1
|
||||
config.needs_redraw = True
|
||||
# sinon ignorer (dans la liste)
|
||||
elif is_input_matched(event, "confirm"):
|
||||
# Indices 0-3 = boutons, 4+ = liste
|
||||
if config.selected_filter_index < action_buttons:
|
||||
# Action sur un bouton
|
||||
btn_idx = config.selected_filter_index
|
||||
settings = load_rgsx_settings()
|
||||
if btn_idx == 0: # all visible
|
||||
config.filter_platforms_selection = [(n, False) for n, _ in config.filter_platforms_selection]
|
||||
config.filter_platforms_dirty = True
|
||||
elif btn_idx == 1: # none visible
|
||||
config.filter_platforms_selection = [(n, True) for n, _ in config.filter_platforms_selection]
|
||||
config.filter_platforms_dirty = True
|
||||
elif btn_idx == 2: # apply
|
||||
hidden_list = [n for n, h in config.filter_platforms_selection if h]
|
||||
settings["hidden_platforms"] = hidden_list
|
||||
save_rgsx_settings(settings)
|
||||
load_sources()
|
||||
# Recalibrer la sélection et la page courante si elles dépassent la nouvelle liste visible
|
||||
try:
|
||||
systems_per_page = config.GRID_COLS * config.GRID_ROWS
|
||||
if config.current_page * systems_per_page >= len(config.platforms):
|
||||
config.current_page = 0
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = 0
|
||||
except Exception:
|
||||
# Sécurité: en cas d'erreur on remet simplement à 0
|
||||
config.current_page = 0
|
||||
config.selected_platform = 0
|
||||
config.filter_platforms_dirty = False
|
||||
# Return either to display menu or pause menu depending on origin
|
||||
target = getattr(config, 'filter_return_to', 'pause_menu')
|
||||
config.menu_state = target
|
||||
if target == 'display_menu': # ancien cas (fallback)
|
||||
config.display_menu_selection = 3
|
||||
elif target == 'pause_display_menu': # nouveau sous-menu hiérarchique
|
||||
config.pause_display_selection = 4 # positionner sur Filter
|
||||
else:
|
||||
config.selected_option = 5 # keep pointer on Filter in pause menu
|
||||
config.filter_return_to = None
|
||||
elif btn_idx == 3: # back
|
||||
target = getattr(config, 'filter_return_to', 'pause_menu')
|
||||
config.menu_state = target
|
||||
if target == 'display_menu':
|
||||
config.display_menu_selection = 3
|
||||
elif target == 'pause_display_menu':
|
||||
config.pause_display_selection = 4
|
||||
else:
|
||||
config.selected_option = 5
|
||||
config.filter_return_to = None
|
||||
config.needs_redraw = True
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.filter_platforms_dirty = False
|
||||
else:
|
||||
# Action sur un élément de la liste (indices >= action_buttons)
|
||||
list_index = config.selected_filter_index - action_buttons
|
||||
if list_index < total_items:
|
||||
name, hidden = config.filter_platforms_selection[list_index]
|
||||
config.filter_platforms_selection[list_index] = (name, not hidden)
|
||||
config.filter_platforms_dirty = True
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.filter_platforms_selection = [(name, current_map.get(name, False)) for name in all_platform_names]
|
||||
|
||||
def _hidden_map() -> dict:
|
||||
return {name: bool(is_hidden) for name, is_hidden in config.filter_platforms_selection}
|
||||
|
||||
def _set_hidden_map(hidden_map_local: dict, source_map_local: dict) -> None:
|
||||
ordered_names = _all_platform_names(source_map_local)
|
||||
config.filter_platforms_selection = [(name, bool(hidden_map_local.get(name, False))) for name in ordered_names]
|
||||
|
||||
def _build_rows(source_map_local: dict, hidden_map_local: dict, expanded_sources_local: set) -> list:
|
||||
rows_local = []
|
||||
for source_name in sorted(source_map_local.keys(), key=lambda s: str(s).lower()):
|
||||
platforms = source_map_local.get(source_name, [])
|
||||
total = len(platforms)
|
||||
hidden_count = sum(1 for platform_name in platforms if hidden_map_local.get(platform_name, False))
|
||||
rows_local.append({
|
||||
"type": "source",
|
||||
"source": source_name,
|
||||
"platforms": platforms,
|
||||
"total": total,
|
||||
"hidden_count": hidden_count,
|
||||
"expanded": source_name in expanded_sources_local,
|
||||
})
|
||||
if source_name in expanded_sources_local:
|
||||
for platform_name in platforms:
|
||||
rows_local.append({
|
||||
"type": "platform",
|
||||
"source": source_name,
|
||||
"platform": platform_name,
|
||||
"hidden": bool(hidden_map_local.get(platform_name, False)),
|
||||
})
|
||||
return rows_local
|
||||
|
||||
def _return_from_filter() -> None:
|
||||
target = getattr(config, 'filter_return_to', 'pause_menu')
|
||||
config.menu_state = target
|
||||
if target == 'display_menu':
|
||||
@@ -4056,6 +4132,144 @@ def handle_controls(event, sources, joystick, screen):
|
||||
else:
|
||||
config.selected_option = 5
|
||||
config.filter_return_to = None
|
||||
|
||||
def _show_unsaved_exit_toast() -> None:
|
||||
if not getattr(config, 'filter_platforms_dirty', False):
|
||||
return
|
||||
try:
|
||||
msg_tpl = _("filter_unsaved_toast") if _ else ""
|
||||
if msg_tpl and msg_tpl != "filter_unsaved_toast":
|
||||
show_toast(msg_tpl, 3000)
|
||||
else:
|
||||
show_toast("Unsaved changes\nApply with History/Start before leaving", 3000)
|
||||
except Exception as toast_error:
|
||||
logger.debug(f"Impossible d'afficher le toast unsaved filter: {toast_error}")
|
||||
|
||||
def _apply_filter() -> None:
|
||||
settings_local = load_rgsx_settings()
|
||||
hidden_map_local = _hidden_map()
|
||||
hidden_list = sorted({name for name, is_hidden in hidden_map_local.items() if is_hidden}, key=lambda s: str(s).lower())
|
||||
settings_local["hidden_platforms"] = hidden_list
|
||||
save_rgsx_settings(settings_local)
|
||||
load_sources()
|
||||
try:
|
||||
systems_per_page = config.GRID_COLS * config.GRID_ROWS
|
||||
if config.current_page * systems_per_page >= len(config.platforms):
|
||||
config.current_page = 0
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = 0
|
||||
except Exception:
|
||||
config.current_page = 0
|
||||
config.selected_platform = 0
|
||||
config.filter_platforms_dirty = False
|
||||
_return_from_filter()
|
||||
|
||||
source_map = _build_source_map()
|
||||
config.filter_platforms_source_map = source_map
|
||||
_ensure_working_selection(source_map)
|
||||
|
||||
expanded_raw = getattr(config, 'filter_platforms_expanded_sources', [])
|
||||
expanded_sources = set(expanded_raw if isinstance(expanded_raw, list) else [])
|
||||
expanded_sources = {source for source in expanded_sources if source in source_map}
|
||||
config.filter_platforms_expanded_sources = sorted(expanded_sources, key=lambda s: str(s).lower())
|
||||
|
||||
rows = _build_rows(source_map, _hidden_map(), expanded_sources)
|
||||
total_rows = len(rows)
|
||||
|
||||
if total_rows <= 0:
|
||||
config.selected_filter_index = 0
|
||||
else:
|
||||
if config.selected_filter_index < 0:
|
||||
config.selected_filter_index = 0
|
||||
elif config.selected_filter_index >= total_rows:
|
||||
config.selected_filter_index = total_rows - 1
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
if total_rows > 0:
|
||||
if config.selected_filter_index > 0:
|
||||
config.selected_filter_index -= 1
|
||||
else:
|
||||
config.selected_filter_index = total_rows - 1
|
||||
config.needs_redraw = True
|
||||
update_key_state("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)
|
||||
elif is_input_matched(event, "down"):
|
||||
if total_rows > 0:
|
||||
if config.selected_filter_index < total_rows - 1:
|
||||
config.selected_filter_index += 1
|
||||
else:
|
||||
config.selected_filter_index = 0
|
||||
config.needs_redraw = True
|
||||
update_key_state("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)
|
||||
elif is_input_matched(event, "left"):
|
||||
if total_rows > 0:
|
||||
current_row = rows[config.selected_filter_index]
|
||||
if current_row.get("type") == "source":
|
||||
source_name = current_row.get("source")
|
||||
if source_name in expanded_sources:
|
||||
expanded_sources.discard(source_name)
|
||||
config.filter_platforms_expanded_sources = sorted(expanded_sources, key=lambda s: str(s).lower())
|
||||
config.needs_redraw = True
|
||||
elif current_row.get("type") == "platform":
|
||||
source_name = current_row.get("source")
|
||||
if source_name in expanded_sources:
|
||||
expanded_sources.discard(source_name)
|
||||
config.filter_platforms_expanded_sources = sorted(expanded_sources, key=lambda s: str(s).lower())
|
||||
refreshed_rows = _build_rows(source_map, _hidden_map(), expanded_sources)
|
||||
for idx, row in enumerate(refreshed_rows):
|
||||
if row.get("type") == "source" and row.get("source") == source_name:
|
||||
config.selected_filter_index = idx
|
||||
break
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "right"):
|
||||
if total_rows > 0:
|
||||
current_row = rows[config.selected_filter_index]
|
||||
source_name = current_row.get("source")
|
||||
if current_row.get("type") == "source" and source_name not in expanded_sources:
|
||||
expanded_sources.add(source_name)
|
||||
config.filter_platforms_expanded_sources = sorted(expanded_sources, key=lambda s: str(s).lower())
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if total_rows > 0:
|
||||
current_row = rows[config.selected_filter_index]
|
||||
hidden_map = _hidden_map()
|
||||
if current_row.get("type") == "source":
|
||||
platforms = current_row.get("platforms", [])
|
||||
all_visible = all(not hidden_map.get(platform_name, False) for platform_name in platforms)
|
||||
new_hidden = True if all_visible else False
|
||||
for platform_name in platforms:
|
||||
hidden_map[platform_name] = new_hidden
|
||||
else:
|
||||
platform_name = current_row.get("platform")
|
||||
hidden_map[platform_name] = not hidden_map.get(platform_name, False)
|
||||
_set_hidden_map(hidden_map, source_map)
|
||||
config.filter_platforms_dirty = True
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
hidden_map = _hidden_map()
|
||||
for platform_name in list(hidden_map.keys()):
|
||||
hidden_map[platform_name] = False
|
||||
_set_hidden_map(hidden_map, source_map)
|
||||
config.filter_platforms_dirty = True
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
hidden_map = _hidden_map()
|
||||
for platform_name in list(hidden_map.keys()):
|
||||
hidden_map[platform_name] = True
|
||||
_set_hidden_map(hidden_map, source_map)
|
||||
config.filter_platforms_dirty = True
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "history") or is_input_matched(event, "start"):
|
||||
_apply_filter()
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
_show_unsaved_exit_toast()
|
||||
_return_from_filter()
|
||||
config.needs_redraw = True
|
||||
|
||||
|
||||
|
||||
@@ -12,16 +12,16 @@ from utils import (truncate_text_middle, wrap_text, load_system_image, truncate_
|
||||
check_web_service_status, check_custom_dns_status, load_api_keys,
|
||||
_get_dest_folder_name, find_file_with_or_without_extension, find_matching_files,
|
||||
get_connection_status_targets, get_connection_status_snapshot,
|
||||
get_clean_display_name, get_existing_history_matches,
|
||||
sort_games_list)
|
||||
get_clean_display_name, get_existing_history_matches, remember_history_local_match,
|
||||
sort_games_list, get_platform_source_badge_key, get_platform_source_badge_surface)
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from history import load_history, is_game_downloaded
|
||||
from language import _, get_size_units, get_speed_unit, get_available_languages, get_language_name
|
||||
from rgsx_settings import (load_rgsx_settings, get_light_mode, get_show_unsupported_platforms,
|
||||
get_allow_unknown_extensions, get_display_monitor, get_display_fullscreen,
|
||||
get_available_monitors, get_font_family, get_sources_mode,
|
||||
get_hide_premium_systems, get_symlink_option)
|
||||
get_available_monitors, get_font_family, get_symlink_option)
|
||||
from game_filters import GameFilters
|
||||
|
||||
import json
|
||||
@@ -1317,6 +1317,22 @@ def get_display_resolution_line():
|
||||
return ""
|
||||
|
||||
|
||||
def draw_platform_source_badge(screen, platform_name, container_rect):
|
||||
source_key = get_platform_source_badge_key(platform_name)
|
||||
if not source_key:
|
||||
return
|
||||
|
||||
badge_size = max(20, min(int(min(container_rect.width, container_rect.height) * 0.24), 44))
|
||||
badge_surface = get_platform_source_badge_surface(source_key, badge_size)
|
||||
if badge_surface is None:
|
||||
return
|
||||
|
||||
inset = max(5, badge_size // 6)
|
||||
badge_x = container_rect.right - badge_size - inset
|
||||
badge_y = container_rect.top + inset
|
||||
screen.blit(badge_surface, (badge_x, badge_y))
|
||||
|
||||
|
||||
def draw_platform_header_info(screen, light_mode=False, badge_x=None, max_badge_width=None, include_details=True):
|
||||
"""Affiche version, controleur connecte et IP reseau dans un cartouche en haut a droite."""
|
||||
lines = get_platform_header_info_lines(max_badge_width, include_details=include_details)
|
||||
@@ -1420,17 +1436,7 @@ def draw_platform_grid(screen):
|
||||
# Effet de pulsation subtil pour le titre - calculé une seule fois par frame
|
||||
current_time = pygame.time.get_ticks()
|
||||
|
||||
# Filtrage éventuel des systèmes premium selon réglage
|
||||
try:
|
||||
from rgsx_settings import get_hide_premium_systems
|
||||
hide_premium = get_hide_premium_systems()
|
||||
except Exception:
|
||||
hide_premium = False
|
||||
premium_markers = getattr(config, 'PREMIUM_HOST_MARKERS', [])
|
||||
if hide_premium and premium_markers:
|
||||
visible_platforms = [p for p in config.platforms if not any(m.lower() in p.lower() for m in premium_markers)]
|
||||
else:
|
||||
visible_platforms = list(config.platforms)
|
||||
visible_platforms = list(config.platforms)
|
||||
|
||||
# Ajuster selected_platform et current_platform/page si liste réduite
|
||||
if config.selected_platform >= len(visible_platforms):
|
||||
@@ -1796,6 +1802,8 @@ def draw_platform_grid(screen):
|
||||
screen.blit(temp_image, centered_image_rect)
|
||||
else:
|
||||
screen.blit(scaled_image, centered_image_rect)
|
||||
|
||||
draw_platform_source_badge(screen, display_name, border_rect)
|
||||
|
||||
# Nettoyer le cache périodiquement (garder seulement les images utilisées récemment)
|
||||
if len(platform_images_cache) > 50: # Limite arbitraire pour éviter une croissance excessive
|
||||
@@ -2441,6 +2449,32 @@ def format_size(size):
|
||||
return f"{size:.1f} {units[-1]}" # Dernier niveau (Po/PB)
|
||||
|
||||
|
||||
def format_speed_adaptive(speed_mib_s):
|
||||
"""Formate une vitesse stockée en MiB/s avec une unité lisible selon son ordre de grandeur."""
|
||||
try:
|
||||
speed_mib_s = float(speed_mib_s or 0.0)
|
||||
except Exception:
|
||||
speed_mib_s = 0.0
|
||||
|
||||
if speed_mib_s <= 0:
|
||||
units = get_size_units()
|
||||
base = units[0] if units else "B"
|
||||
return f"0 {base}/s"
|
||||
|
||||
bytes_per_second = speed_mib_s * 1024.0 * 1024.0
|
||||
units = get_size_units()
|
||||
if not units or len(units) < 4:
|
||||
units = ["B", "KB", "MB", "GB"]
|
||||
|
||||
if bytes_per_second < 1024.0:
|
||||
return f"{bytes_per_second:.0f} {units[0]}/s"
|
||||
if bytes_per_second < (1024.0 ** 2):
|
||||
return f"{bytes_per_second / 1024.0:.1f} {units[1]}/s"
|
||||
if bytes_per_second < (1024.0 ** 3):
|
||||
return f"{bytes_per_second / (1024.0 ** 2):.2f} {units[2]}/s"
|
||||
return f"{bytes_per_second / (1024.0 ** 3):.2f} {units[3]}/s"
|
||||
|
||||
|
||||
def draw_history_list(screen):
|
||||
# logger.debug(f"Dessin historique, history={config.history}, needs_redraw={config.needs_redraw}")
|
||||
history = config.history if hasattr(config, 'history') else load_history()
|
||||
@@ -2459,17 +2493,51 @@ def draw_history_list(screen):
|
||||
else:
|
||||
current_history_item_inverted = 0
|
||||
|
||||
# Cherche une entrée en cours de téléchargement pour afficher la vitesse
|
||||
speed_str = ""
|
||||
for entry in history:
|
||||
if entry.get("status") in ["Téléchargement", "Downloading"]:
|
||||
speed = entry.get("speed", 0.0)
|
||||
if speed and speed > 0:
|
||||
speed_str = f" - {speed:.2f} {get_speed_unit()}"
|
||||
break
|
||||
active_statuses = {"Téléchargement", "Downloading", "Extracting", "Converting", "Connecting", "Queued"}
|
||||
completed_statuses = {"Download_OK", "Completed"}
|
||||
error_statuses = {"Erreur", "Error"}
|
||||
canceled_statuses = {"Canceled", "Cancelled", "Annulé", "Annule"}
|
||||
|
||||
selected_entry = history[current_history_item_inverted] if history and 0 <= current_history_item_inverted < len(history) else None
|
||||
selected_status = str((selected_entry or {}).get("status") or "")
|
||||
|
||||
if selected_entry and selected_status in active_statuses:
|
||||
downloaded_size = int(selected_entry.get("downloaded_size", 0) or 0)
|
||||
size_text = format_size(downloaded_size)
|
||||
try:
|
||||
selected_speed = float(selected_entry.get("speed", 0.0) or 0.0)
|
||||
except Exception:
|
||||
selected_speed = 0.0
|
||||
speed_text = format_speed_adaptive(selected_speed)
|
||||
title_text = _("history_title_downloading_active").format(size_text, speed_text)
|
||||
# Afficher SD/CN dans le titre
|
||||
_sd = int(selected_entry.get("seeds", 0) or 0)
|
||||
_cn = int(selected_entry.get("connections", 0) or 0)
|
||||
title_text = f"{title_text} [{_sd}SD/{_cn}CN]"
|
||||
# Afficher l'étape aria2c courante dans le titre (connecting / verifying / waiting).
|
||||
# On ne montre rien quand on télécharge activement (speed > 0) car l'info de vitesse suffit.
|
||||
_aria2_phase = str(selected_entry.get("aria2_phase") or "")
|
||||
_phase_labels = {
|
||||
"connecting": _("aria2_phase_connecting"),
|
||||
"verifying": _("aria2_phase_verifying"),
|
||||
"waiting": _("aria2_phase_waiting"),
|
||||
}
|
||||
_phase_label = _phase_labels.get(_aria2_phase, "")
|
||||
if _phase_label:
|
||||
title_text = f"{title_text} [{_phase_label}]"
|
||||
elif selected_entry and selected_status in completed_statuses:
|
||||
completed_count = sum(1 for item in history if str(item.get("status") or "") in completed_statuses)
|
||||
title_text = _("history_title_completed_count").format(completed_count)
|
||||
elif selected_entry and selected_status in error_statuses:
|
||||
error_count = sum(1 for item in history if str(item.get("status") or "") in error_statuses)
|
||||
title_text = _("history_title_error_count").format(error_count)
|
||||
elif selected_entry and selected_status in canceled_statuses:
|
||||
canceled_count = sum(1 for item in history if str(item.get("status") or "") in canceled_statuses)
|
||||
title_text = _("history_title_canceled_count").format(canceled_count)
|
||||
else:
|
||||
title_text = _("history_title").format(history_count)
|
||||
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
title_text = _("history_title").format(history_count) + speed_str
|
||||
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
|
||||
title_rect_inflated = title_rect.inflate(60, 30)
|
||||
@@ -2510,17 +2578,6 @@ def draw_history_list(screen):
|
||||
else:
|
||||
current_history_item_inverted = 0
|
||||
|
||||
speed = 0.0
|
||||
if history and history[current_history_item_inverted].get("status") in ["Téléchargement", "Downloading"]:
|
||||
speed = history[current_history_item_inverted].get("speed", 0.0)
|
||||
if speed > 0:
|
||||
speed_str = f"{speed:.2f} {get_speed_unit()}"
|
||||
title_text = _("history_title").format(history_count) + f" {speed_str}"
|
||||
else:
|
||||
title_text = _("history_title").format(history_count)
|
||||
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
|
||||
|
||||
|
||||
if not history:
|
||||
logger.debug("Aucun historique disponible")
|
||||
message = _("history_empty")
|
||||
@@ -2616,6 +2673,8 @@ def draw_history_list(screen):
|
||||
custom_message = entry.get('message', '')
|
||||
total_size_value = int(entry.get("total_size", 0) or 0)
|
||||
downloaded_size_value = int(entry.get("downloaded_size", 0) or 0)
|
||||
seeds_value = int(entry.get("seeds", 0) or 0)
|
||||
connections_value = int(entry.get("connections", 0) or 0)
|
||||
# Détecter les messages du mode gratuit (commencent par '[' dans toutes les langues)
|
||||
if custom_message and custom_message.strip().startswith('['):
|
||||
# Utiliser le message personnalisé pour le mode gratuit
|
||||
@@ -2624,7 +2683,9 @@ def draw_history_list(screen):
|
||||
status_text = str(status)
|
||||
else:
|
||||
# Comportement normal: afficher le pourcentage
|
||||
status_text = _("history_status_downloading").format(progress)
|
||||
display_progress = "<1" if (progress <= 0 and total_size_value > 0 and downloaded_size_value > 0) else progress
|
||||
status_text = _("history_status_downloading").format(display_progress)
|
||||
# SD/CN sont maintenant affichés dans le titre, pas ici
|
||||
# Coerce to string and prefix provider when relevant
|
||||
status_text = str(status_text or "")
|
||||
if provider_prefix and not status_text.startswith(provider_prefix):
|
||||
@@ -3055,6 +3116,13 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
|
||||
"pause_connection_status": [
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"filter_platforms": [
|
||||
("confirm", _("controls_confirm_select")),
|
||||
(("left", "right"), (_("filter_expand_collapse") if _ and _("filter_expand_collapse") != "filter_expand_collapse" else "Expand/Collapse")),
|
||||
(("page_up", "page_down"), f"{_('filter_all')} / {_('filter_none')}"),
|
||||
("history", _("filter_apply")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"support_dialog": [
|
||||
("start", _("controls_cancel_back")),
|
||||
],
|
||||
@@ -3894,9 +3962,6 @@ def draw_pause_display_font_menu(screen, selected_index):
|
||||
_draw_submenu_generic(screen, _("submenu_display_font_size") if _ else "Font Size", options, selected_index, instruction_text)
|
||||
|
||||
def draw_pause_games_menu(screen, selected_index):
|
||||
mode = get_sources_mode()
|
||||
source_label = _("games_source_rgsx") if mode == "rgsx" else _("games_source_custom")
|
||||
source_txt = f"{_('menu_games_source_prefix')}: < {source_label} >"
|
||||
update_txt = _("menu_redownload_cache")
|
||||
scan_txt = _("menu_scan_owned_roms") if _ else "Scan owned ROMs"
|
||||
history_txt = _("menu_history") if _ else "History"
|
||||
@@ -3909,24 +3974,16 @@ def draw_pause_games_menu(screen, selected_index):
|
||||
raw_unsupported_label = raw_unsupported_label.split('{status}')[0].rstrip(' :')
|
||||
unsupported_txt = f"{raw_unsupported_label}: < {status_unsupported} >"
|
||||
|
||||
# Hide premium systems
|
||||
hide_premium = get_hide_premium_systems()
|
||||
status_hide_premium = _('status_on') if hide_premium else _('status_off')
|
||||
hide_premium_label = _('menu_hide_premium_systems') if _ else 'Hide Premium systems'
|
||||
hide_premium_txt = f"{hide_premium_label}: < {status_hide_premium} >"
|
||||
|
||||
# Filter platforms
|
||||
filter_txt = _("submenu_display_filter_platforms") if _ else "Show/Hide Platforms"
|
||||
|
||||
back_txt = _("menu_back") if _ else "Back"
|
||||
options = [update_txt, scan_txt, history_txt, source_txt, unsupported_txt, hide_premium_txt, filter_txt, back_txt]
|
||||
options = [update_txt, scan_txt, history_txt, unsupported_txt, filter_txt, back_txt]
|
||||
instruction_keys = [
|
||||
"instruction_games_update_cache",
|
||||
"instruction_games_scan_owned",
|
||||
"instruction_games_history",
|
||||
"instruction_games_source_mode",
|
||||
"instruction_display_show_unsupported",
|
||||
"instruction_display_hide_premium",
|
||||
"instruction_display_filter_platforms",
|
||||
"instruction_generic_back",
|
||||
]
|
||||
@@ -3934,34 +3991,11 @@ def draw_pause_games_menu(screen, selected_index):
|
||||
instruction_text = None
|
||||
if key:
|
||||
instruction_text = _(key)
|
||||
if key == "instruction_display_hide_premium":
|
||||
# Inject dynamic list of premium providers from config.PREMIUM_HOST_MARKERS
|
||||
try:
|
||||
from config import PREMIUM_HOST_MARKERS
|
||||
# Clean, preserve order, remove duplicates (case-insensitive)
|
||||
seen = set()
|
||||
providers_clean = []
|
||||
for p in PREMIUM_HOST_MARKERS:
|
||||
p_lower = p.lower()
|
||||
if p_lower not in seen:
|
||||
seen.add(p_lower)
|
||||
providers_clean.append(p)
|
||||
providers_str = ", ".join(providers_clean)
|
||||
if not providers_str:
|
||||
providers_str = "1fichier, etc."
|
||||
if "{providers}" in instruction_text:
|
||||
instruction_text = instruction_text.format(providers=providers_str)
|
||||
else:
|
||||
# fallback si placeholder absent
|
||||
instruction_text = f"{instruction_text} ({providers_str})"
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_draw_submenu_generic(screen, _("menu_games") if _ else "Games", options, selected_index, instruction_text)
|
||||
|
||||
def draw_pause_settings_menu(screen, selected_index):
|
||||
from rgsx_settings import get_auto_extract, get_roms_folder
|
||||
from rgsx_settings import get_auto_extract, get_roms_folder, get_max_simultaneous_downloads
|
||||
# Music
|
||||
if config.music_enabled:
|
||||
music_name = config.current_music_name or ""
|
||||
@@ -3994,6 +4028,10 @@ def draw_pause_settings_menu(screen, selected_index):
|
||||
roms_folder_txt = f"{_('settings_roms_folder')} : {display_path}"
|
||||
else:
|
||||
roms_folder_txt = f"{_('settings_roms_folder')} : < {_('settings_roms_folder_default')} >"
|
||||
|
||||
# Max simultaneous downloads option
|
||||
max_dl = get_max_simultaneous_downloads()
|
||||
max_dl_txt = f"{_('settings_max_simultaneous_dl')} : < {max_dl} >"
|
||||
|
||||
# Web Service at boot (only on Linux/Batocera)
|
||||
web_service_txt = ""
|
||||
@@ -4013,22 +4051,23 @@ def draw_pause_settings_menu(screen, selected_index):
|
||||
back_txt = _("menu_back") if _ else "Back"
|
||||
|
||||
# Construction de la liste des options
|
||||
options = [music_option, symlink_option, auto_extract_txt, roms_folder_txt]
|
||||
options = [music_option, symlink_option, auto_extract_txt, roms_folder_txt, max_dl_txt]
|
||||
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, connection_status_txt, back_txt])
|
||||
|
||||
|
||||
# Index de l'option Dossier ROMs
|
||||
roms_folder_index = 3
|
||||
|
||||
|
||||
# Instructions textuelles pour chaque option
|
||||
instruction_keys = [
|
||||
"instruction_settings_music",
|
||||
"instruction_settings_symlink",
|
||||
"instruction_settings_auto_extract",
|
||||
"instruction_settings_roms_folder",
|
||||
"instruction_settings_max_simultaneous_dl",
|
||||
]
|
||||
if web_service_txt:
|
||||
instruction_keys.append("instruction_settings_web_service")
|
||||
@@ -4159,17 +4198,29 @@ def draw_pause_connection_status(screen):
|
||||
cat_sources = _("connection_status_category_sources") if _ else "Sources"
|
||||
|
||||
# Group rows by category
|
||||
categories_order = ["updates", "sources"]
|
||||
category_labels = {
|
||||
category_labels_map = {
|
||||
"updates": cat_updates,
|
||||
"sources": cat_sources,
|
||||
}
|
||||
|
||||
categories_order = []
|
||||
for target in targets:
|
||||
cat = str(target.get("category", "sources")).strip().lower() or "sources"
|
||||
if cat not in categories_order:
|
||||
categories_order.append(cat)
|
||||
|
||||
def _category_label(cat_key: str) -> str:
|
||||
if cat_key in category_labels_map:
|
||||
return category_labels_map[cat_key]
|
||||
cleaned = cat_key.replace("_", " ").strip()
|
||||
return cleaned.title() if cleaned else cat_sources
|
||||
|
||||
rows = [] # list of (type, data)
|
||||
for cat in categories_order:
|
||||
cat_items = [t for t in targets if t.get("category") == cat]
|
||||
cat_items = [t for t in targets if str(t.get("category", "sources")).strip().lower() == cat]
|
||||
if not cat_items:
|
||||
continue
|
||||
rows.append(("header", category_labels.get(cat, cat)))
|
||||
rows.append(("header", _category_label(cat)))
|
||||
for item in cat_items:
|
||||
rows.append(("item", item))
|
||||
|
||||
@@ -4291,24 +4342,94 @@ def draw_pause_connection_status(screen):
|
||||
|
||||
|
||||
def draw_filter_platforms_menu(screen):
|
||||
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""
|
||||
"""Affiche le menu de filtrage des plateformes (sources + plateformes collapsibles)."""
|
||||
screen.blit(OVERLAY, (0, 0))
|
||||
settings = load_rgsx_settings()
|
||||
hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set()
|
||||
|
||||
# Initialiser la copie de travail si vide ou taille différente
|
||||
if not config.filter_platforms_selection or len(config.filter_platforms_selection) != len(config.platform_dicts):
|
||||
# Liste alphabétique complète (sans filtrer hidden existant)
|
||||
all_names = sorted([d.get("platform_name", "") for d in config.platform_dicts if d.get("platform_name")])
|
||||
config.filter_platforms_selection = [(name, name in hidden) for name in all_names]
|
||||
def _extract_source(platform_name: str) -> str:
|
||||
match = re.search(r'\(([^()]+)\)\s*$', str(platform_name).strip())
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
fallback = _("games_source_rgsx") if _ else "RGSX"
|
||||
return fallback if fallback != "games_source_rgsx" else "RGSX"
|
||||
|
||||
def _strip_source_suffix(platform_name: str) -> str:
|
||||
return re.sub(r'\s*\([^()]+\)\s*$', '', str(platform_name)).strip()
|
||||
|
||||
# Construire mapping source -> plateformes (trié, sans doublons)
|
||||
source_to_platforms = {}
|
||||
for entry in config.platform_dicts:
|
||||
platform_name = entry.get("platform_name", "") if isinstance(entry, dict) else ""
|
||||
platform_name = str(platform_name).strip()
|
||||
if not platform_name:
|
||||
continue
|
||||
source_name = _extract_source(platform_name)
|
||||
source_to_platforms.setdefault(source_name, []).append(platform_name)
|
||||
|
||||
for source_name in list(source_to_platforms.keys()):
|
||||
source_to_platforms[source_name] = sorted(set(source_to_platforms[source_name]), key=lambda s: str(s).lower())
|
||||
source_to_platforms = dict(sorted(source_to_platforms.items(), key=lambda kv: str(kv[0]).lower()))
|
||||
config.filter_platforms_source_map = source_to_platforms
|
||||
|
||||
all_platform_names = []
|
||||
for source_name in source_to_platforms:
|
||||
all_platform_names.extend(source_to_platforms[source_name])
|
||||
|
||||
# Initialiser/synchroniser la copie de travail par plateforme
|
||||
current_map = {}
|
||||
if isinstance(config.filter_platforms_selection, list):
|
||||
for item in config.filter_platforms_selection:
|
||||
if isinstance(item, (list, tuple)) and len(item) == 2:
|
||||
name = str(item[0]).strip()
|
||||
if name:
|
||||
current_map[name] = bool(item[1])
|
||||
|
||||
expected_set = set(all_platform_names)
|
||||
if set(current_map.keys()) != expected_set:
|
||||
config.filter_platforms_selection = [(name, name in hidden) for name in all_platform_names]
|
||||
config.selected_filter_index = 0
|
||||
config.filter_platforms_scroll_offset = 0
|
||||
config.filter_platforms_dirty = False
|
||||
else:
|
||||
config.filter_platforms_selection = [(name, current_map.get(name, False)) for name in all_platform_names]
|
||||
|
||||
hidden_map = {name: bool(is_hidden) for name, is_hidden in config.filter_platforms_selection}
|
||||
|
||||
expanded_raw = getattr(config, 'filter_platforms_expanded_sources', [])
|
||||
expanded_sources = set(expanded_raw if isinstance(expanded_raw, list) else [])
|
||||
expanded_sources = {source_name for source_name in expanded_sources if source_name in source_to_platforms}
|
||||
config.filter_platforms_expanded_sources = sorted(expanded_sources, key=lambda s: str(s).lower())
|
||||
|
||||
rows = []
|
||||
for source_name, platforms in source_to_platforms.items():
|
||||
total = len(platforms)
|
||||
hidden_count = sum(1 for platform_name in platforms if hidden_map.get(platform_name, False))
|
||||
rows.append({
|
||||
"type": "source",
|
||||
"source": source_name,
|
||||
"platforms": platforms,
|
||||
"total": total,
|
||||
"hidden_count": hidden_count,
|
||||
"expanded": source_name in expanded_sources,
|
||||
})
|
||||
if source_name in expanded_sources:
|
||||
for platform_name in platforms:
|
||||
rows.append({
|
||||
"type": "platform",
|
||||
"source": source_name,
|
||||
"platform": platform_name,
|
||||
"hidden": bool(hidden_map.get(platform_name, False)),
|
||||
})
|
||||
|
||||
if rows:
|
||||
config.selected_filter_index = max(0, min(config.selected_filter_index, len(rows) - 1))
|
||||
else:
|
||||
config.selected_filter_index = 0
|
||||
|
||||
title_text = _("filter_platforms_title")
|
||||
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 14))
|
||||
# Padding responsive réduit
|
||||
hpad = max(36, min(64, int(config.screen_width * 0.06)))
|
||||
vpad = max(10, min(20, int(title_surface.get_height() * 0.45)))
|
||||
title_rect_inflated = title_rect.inflate(hpad, vpad)
|
||||
@@ -4317,86 +4438,90 @@ def draw_filter_platforms_menu(screen):
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
# Boutons d'action en haut (avant la liste)
|
||||
btn_width = 220
|
||||
btn_height = int(config.screen_height * 0.0463)
|
||||
spacing = 30
|
||||
buttons_y = title_rect_inflated.bottom + 20
|
||||
center_x = config.screen_width // 2
|
||||
actions = [
|
||||
("filter_all", 0),
|
||||
("filter_none", 1),
|
||||
("filter_apply", 2),
|
||||
("filter_back", 3)
|
||||
]
|
||||
total_items = len(config.filter_platforms_selection)
|
||||
action_buttons = len(actions)
|
||||
|
||||
for idx, (key, btn_idx) in enumerate(actions):
|
||||
btn_x = center_x - (len(actions) * (btn_width + spacing) - spacing) // 2 + idx * (btn_width + spacing)
|
||||
is_selected = (config.selected_filter_index == btn_idx)
|
||||
label = _(key)
|
||||
draw_stylized_button(screen, label, btn_x, buttons_y, btn_width, btn_height, selected=is_selected)
|
||||
|
||||
# Zone liste (après les boutons)
|
||||
list_width = int(config.screen_width * 0.7)
|
||||
list_height = int(config.screen_height * 0.5)
|
||||
# Zone liste: laisser de la place au footer de controls + infos
|
||||
footer_reserved = max(95, int(config.screen_height * 0.15))
|
||||
list_width = int(config.screen_width * 0.78)
|
||||
list_x = (config.screen_width - list_width) // 2
|
||||
list_y = buttons_y + btn_height + 20
|
||||
list_y = title_rect_inflated.bottom + 16
|
||||
list_bottom_limit = config.screen_height - footer_reserved - 38
|
||||
list_height = max(140, list_bottom_limit - list_y)
|
||||
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (list_x, list_y, list_width, list_height), border_radius=12)
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], (list_x, list_y, list_width, list_height), 2, border_radius=12)
|
||||
|
||||
line_height = config.small_font.get_height() + 8
|
||||
visible_items = list_height // line_height - 1 # laisser un peu d'espace bas
|
||||
total_items = len(config.filter_platforms_selection)
|
||||
if config.selected_filter_index < 0:
|
||||
config.selected_filter_index = 0
|
||||
# Ne pas forcer la réduction si on est sur les boutons (indices >= total_items)
|
||||
# Laisser controls.py gérer la borne max étendue
|
||||
# Ajuster scroll
|
||||
visible_items = max(4, (list_height - 20) // line_height)
|
||||
total_items = len(rows)
|
||||
|
||||
if config.selected_filter_index < config.filter_platforms_scroll_offset:
|
||||
config.filter_platforms_scroll_offset = config.selected_filter_index
|
||||
elif config.selected_filter_index >= config.filter_platforms_scroll_offset + visible_items:
|
||||
config.filter_platforms_scroll_offset = config.selected_filter_index - visible_items + 1
|
||||
config.filter_platforms_scroll_offset = max(0, min(config.filter_platforms_scroll_offset, max(0, total_items - visible_items)))
|
||||
|
||||
# Dessiner items (les indices de la liste commencent à action_buttons)
|
||||
for i in range(config.filter_platforms_scroll_offset, min(config.filter_platforms_scroll_offset + visible_items, total_items)):
|
||||
name, is_hidden = config.filter_platforms_selection[i]
|
||||
idx_on_screen = i - config.filter_platforms_scroll_offset
|
||||
# Dessin des lignes source + plateformes
|
||||
start = config.filter_platforms_scroll_offset
|
||||
end = min(start + visible_items, total_items)
|
||||
for i in range(start, end):
|
||||
row = rows[i]
|
||||
idx_on_screen = i - start
|
||||
y_center = list_y + 10 + idx_on_screen * line_height + line_height // 2
|
||||
# Les éléments de la liste ont des indices à partir de action_buttons
|
||||
selected = (config.selected_filter_index == action_buttons + i)
|
||||
checkbox = "[ ]" if is_hidden else "[X]" # inversé: coché signifie visible
|
||||
# Correction: on veut [X] si visible => is_hidden False
|
||||
checkbox = "[X]" if not is_hidden else "[ ]"
|
||||
display_text = f"{checkbox} {name}"
|
||||
color = THEME_COLORS["fond_lignes"] if selected else THEME_COLORS["text"]
|
||||
text_surface = config.small_font.render(display_text, True, color)
|
||||
text_rect = text_surface.get_rect(midleft=(list_x + 20, y_center))
|
||||
selected = (config.selected_filter_index == i)
|
||||
|
||||
if selected:
|
||||
glow_surface = pygame.Surface((list_width - 40, line_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, list_width - 40, line_height), border_radius=8)
|
||||
screen.blit(glow_surface, (list_x + 20, y_center - line_height // 2))
|
||||
glow_surface = pygame.Surface((list_width - 32, line_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(glow_surface, THEME_COLORS["fond_lignes"] + (50,), (0, 0, list_width - 32, line_height), border_radius=8)
|
||||
screen.blit(glow_surface, (list_x + 16, y_center - line_height // 2))
|
||||
|
||||
if row.get("type") == "source":
|
||||
total = max(1, int(row.get("total", 0)))
|
||||
hidden_count = int(row.get("hidden_count", 0))
|
||||
visible_count = max(0, total - hidden_count)
|
||||
if hidden_count == 0:
|
||||
checkbox = "[X]"
|
||||
elif hidden_count >= total:
|
||||
checkbox = "[ ]"
|
||||
else:
|
||||
checkbox = "[-]"
|
||||
collapse = "v" if row.get("expanded") else ">"
|
||||
display_text = f"{checkbox} {collapse} {row.get('source', '')} ({visible_count}/{total})"
|
||||
text_x = list_x + 20
|
||||
else:
|
||||
platform_name = row.get("platform", "")
|
||||
checkbox = "[X]" if not row.get("hidden") else "[ ]"
|
||||
clean_name = _strip_source_suffix(platform_name) or platform_name
|
||||
display_text = f"{checkbox} {clean_name}"
|
||||
text_x = list_x + 44
|
||||
|
||||
max_text_w = max(60, list_width - (text_x - list_x) - 38)
|
||||
fitted_text = truncate_text_end(display_text, config.small_font, max_text_w)
|
||||
color = THEME_COLORS["fond_lignes"] if selected else THEME_COLORS["text"]
|
||||
text_surface = config.small_font.render(fitted_text, True, color)
|
||||
text_rect = text_surface.get_rect(midleft=(text_x, y_center))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
# Scrollbar
|
||||
if total_items > visible_items:
|
||||
scroll_height = int((visible_items / total_items) * (list_height - 20))
|
||||
scroll_y = int((config.filter_platforms_scroll_offset / max(1, total_items - visible_items)) * (list_height - 20 - scroll_height))
|
||||
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 25, list_y + 10 + scroll_y, 10, scroll_height), border_radius=4)
|
||||
scroll_track_height = list_height - 20
|
||||
scroll_height = int((visible_items / total_items) * scroll_track_height)
|
||||
scroll_height = max(20, scroll_height)
|
||||
scroll_range = max(1, total_items - visible_items)
|
||||
scroll_y = int((config.filter_platforms_scroll_offset / scroll_range) * (scroll_track_height - scroll_height))
|
||||
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (list_x + list_width - 22, list_y + 10 + scroll_y, 9, scroll_height), border_radius=4)
|
||||
|
||||
# Infos bas
|
||||
hidden_count = sum(1 for _, h in config.filter_platforms_selection if h)
|
||||
visible_count = total_items - hidden_count
|
||||
info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_items)
|
||||
total_platforms = len(all_platform_names)
|
||||
hidden_count = sum(1 for _, is_hidden in config.filter_platforms_selection if is_hidden)
|
||||
visible_count = total_platforms - hidden_count
|
||||
info_text = _("filter_platforms_info").format(visible_count, hidden_count, total_platforms)
|
||||
info_surface = config.small_font.render(info_text, True, THEME_COLORS["text"])
|
||||
info_rect = info_surface.get_rect(center=(config.screen_width // 2, list_y + list_height + 20))
|
||||
info_rect = info_surface.get_rect(center=(config.screen_width // 2, list_y + list_height + 18))
|
||||
screen.blit(info_surface, info_rect)
|
||||
|
||||
if config.filter_platforms_dirty:
|
||||
dirty_text = _("filter_unsaved_warning")
|
||||
dirty_surface = config.small_font.render(dirty_text, True, THEME_COLORS["warning_text"])
|
||||
dirty_rect = dirty_surface.get_rect(center=(config.screen_width // 2, info_rect.bottom + 25))
|
||||
dirty_rect = dirty_surface.get_rect(center=(config.screen_width // 2, info_rect.bottom + 22))
|
||||
screen.blit(dirty_surface, dirty_rect)
|
||||
|
||||
# Menu aide contrôles
|
||||
@@ -4842,6 +4967,8 @@ def draw_folder_browser(screen):
|
||||
# Chemin actuel (tronqué si trop long)
|
||||
path_max_width = panel_width - 40
|
||||
path_display = current_path
|
||||
if not path_display and os.name == 'nt':
|
||||
path_display = "Available drives"
|
||||
while config.small_font.size(path_display)[0] > path_max_width and len(path_display) > 10:
|
||||
path_display = "..." + path_display[4:]
|
||||
path_text = config.small_font.render(path_display, True, THEME_COLORS["highlight"])
|
||||
@@ -4881,15 +5008,19 @@ def draw_folder_browser(screen):
|
||||
pygame.draw.rect(screen, THEME_COLORS["highlight"], sel_rect, 2, border_radius=6)
|
||||
|
||||
# Icône dossier (texte simple au lieu d'emoji)
|
||||
folder_icon = "[..]" if item == ".." else "[D]"
|
||||
is_drive = isinstance(item, str) and len(item) >= 2 and item[1] == ':'
|
||||
folder_icon = "[..]" if item == ".." else ("[DRV]" if is_drive else "[D]")
|
||||
icon_text = config.small_font.render(folder_icon, True, THEME_COLORS["highlight"] if item == ".." else THEME_COLORS["text"])
|
||||
screen.blit(icon_text, (panel_x + 30, item_y + (item_height - icon_text.get_height()) // 2))
|
||||
icon_x = panel_x + 30
|
||||
icon_y = item_y + (item_height - icon_text.get_height()) // 2
|
||||
screen.blit(icon_text, (icon_x, icon_y))
|
||||
|
||||
# Nom du dossier
|
||||
display_name = _("folder_browser_parent") if item == ".." and _ else (".." if item == ".." else item)
|
||||
text_color = THEME_COLORS["highlight"] if is_selected else THEME_COLORS["text"]
|
||||
item_text = config.small_font.render(display_name, True, text_color)
|
||||
screen.blit(item_text, (panel_x + 70, item_y + (item_height - item_text.get_height()) // 2))
|
||||
text_x = icon_x + icon_text.get_width() + 12
|
||||
screen.blit(item_text, (text_x, item_y + (item_height - item_text.get_height()) // 2))
|
||||
|
||||
# Indicateur de scroll si nécessaire
|
||||
if len(items) > visible_items:
|
||||
@@ -5192,11 +5323,29 @@ def draw_history_game_options(screen):
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
local_path = entry.get("local_path")
|
||||
local_filename = entry.get("local_filename")
|
||||
if not file_exists and local_path and os.path.isfile(local_path):
|
||||
actual_filename = os.path.basename(local_path)
|
||||
actual_path = local_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS_RENDER] direct local_path match used: %s", actual_path)
|
||||
elif not file_exists and local_filename:
|
||||
local_filename_path = os.path.join(base_path, str(local_filename))
|
||||
if os.path.isfile(local_filename_path):
|
||||
actual_filename = os.path.basename(local_filename_path)
|
||||
actual_path = local_filename_path
|
||||
file_exists = True
|
||||
actual_matches = [(actual_filename, actual_path)]
|
||||
logger.debug("[HISTORY_OPTIONS_RENDER] direct local_filename match used: %s", actual_path)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
if file_exists and actual_path:
|
||||
remember_history_local_match(entry, actual_filename, actual_path)
|
||||
|
||||
# Déterminer les options disponibles selon le statut
|
||||
options = []
|
||||
@@ -5210,6 +5359,8 @@ def draw_history_game_options(screen):
|
||||
# Options selon statut
|
||||
if status == "Queued":
|
||||
# En attente dans la queue
|
||||
options.append("force_download")
|
||||
option_labels.append(_("history_option_force_download"))
|
||||
options.append("remove_from_queue")
|
||||
option_labels.append(_("history_option_remove_from_queue"))
|
||||
elif status in ["Downloading", "Téléchargement", "Extracting", "Paused"]:
|
||||
@@ -5246,6 +5397,31 @@ def draw_history_game_options(screen):
|
||||
option_labels.append(_("history_option_delete_game"))
|
||||
options.append("back")
|
||||
option_labels.append(_("history_option_back"))
|
||||
|
||||
diagnostics_signature = (
|
||||
entry.get("url", ""),
|
||||
status,
|
||||
file_exists,
|
||||
actual_filename or "",
|
||||
actual_path or "",
|
||||
tuple(options),
|
||||
)
|
||||
if getattr(config, 'history_options_render_signature', None) != diagnostics_signature:
|
||||
config.history_options_render_signature = diagnostics_signature
|
||||
logger.debug(
|
||||
"[HISTORY_OPTIONS_RENDER] platform=%s game=%s status=%s dest_folder=%s base_path=%s file_exists=%s actual_filename=%s actual_path=%s local_path=%s moved_paths=%s options=%s",
|
||||
platform,
|
||||
game_name,
|
||||
status,
|
||||
dest_folder,
|
||||
base_path,
|
||||
file_exists,
|
||||
actual_filename,
|
||||
actual_path,
|
||||
entry.get("local_path"),
|
||||
entry.get("moved_paths"),
|
||||
options,
|
||||
)
|
||||
|
||||
# Calculer dimensions
|
||||
title = _("history_game_options_title")
|
||||
|
||||
@@ -9,6 +9,131 @@ from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_history_write_state_lock = threading.Lock()
|
||||
_history_write_failures = 0
|
||||
_history_write_last_error = ""
|
||||
_history_write_last_failure_ts = 0.0
|
||||
_history_write_last_log_ts = 0.0
|
||||
_history_write_last_probe_ts = 0.0
|
||||
_history_write_ok = True
|
||||
|
||||
_HISTORY_WRITE_FAILURE_COOLDOWN_SEC = 1.5
|
||||
_HISTORY_WRITE_PROBE_CACHE_SEC = 8.0
|
||||
|
||||
|
||||
def _set_history_write_status(ok, error_message=""):
|
||||
global _history_write_ok, _history_write_last_error
|
||||
with _history_write_state_lock:
|
||||
_history_write_ok = bool(ok)
|
||||
_history_write_last_error = error_message or ""
|
||||
failures = _history_write_failures
|
||||
last_failure_ts = _history_write_last_failure_ts
|
||||
|
||||
setattr(config, "history_write_ok", bool(ok))
|
||||
setattr(config, "history_write_error", error_message or "")
|
||||
setattr(config, "history_write_failure_count", failures)
|
||||
setattr(config, "history_write_last_failure_ts", last_failure_ts)
|
||||
|
||||
|
||||
def _register_history_write_failure(exc):
|
||||
global _history_write_failures, _history_write_last_failure_ts, _history_write_last_log_ts
|
||||
|
||||
now = time.time()
|
||||
raw_error = str(exc)
|
||||
message = (
|
||||
f"Erreur ecriture history.json: {raw_error}. "
|
||||
"Le telechargement continue sans sauvegarde temps reel."
|
||||
)
|
||||
|
||||
with _history_write_state_lock:
|
||||
_history_write_failures += 1
|
||||
_history_write_last_failure_ts = now
|
||||
|
||||
_set_history_write_status(False, message)
|
||||
|
||||
should_log = False
|
||||
with _history_write_state_lock:
|
||||
if _history_write_failures in (1, 5, 20):
|
||||
should_log = True
|
||||
elif now - _history_write_last_log_ts >= 5.0:
|
||||
should_log = True
|
||||
if should_log:
|
||||
_history_write_last_log_ts = now
|
||||
|
||||
if should_log:
|
||||
logger.error(message)
|
||||
|
||||
|
||||
def _register_history_write_success():
|
||||
global _history_write_failures
|
||||
|
||||
recovered = False
|
||||
with _history_write_state_lock:
|
||||
if _history_write_failures > 0:
|
||||
recovered = True
|
||||
_history_write_failures = 0
|
||||
|
||||
_set_history_write_status(True, "")
|
||||
if recovered:
|
||||
logger.info("Ecriture history.json retablie")
|
||||
|
||||
|
||||
def get_history_write_status():
|
||||
with _history_write_state_lock:
|
||||
return {
|
||||
"ok": _history_write_ok,
|
||||
"message": _history_write_last_error,
|
||||
"failures": _history_write_failures,
|
||||
"last_failure_ts": _history_write_last_failure_ts,
|
||||
"last_probe_ts": _history_write_last_probe_ts,
|
||||
}
|
||||
|
||||
|
||||
def check_history_write_access(force=False):
|
||||
global _history_write_last_probe_ts
|
||||
|
||||
history_path = getattr(config, 'HISTORY_PATH')
|
||||
now = time.time()
|
||||
|
||||
with _history_write_state_lock:
|
||||
use_cached_probe = (
|
||||
not force
|
||||
and (now - _history_write_last_probe_ts) < _HISTORY_WRITE_PROBE_CACHE_SEC
|
||||
)
|
||||
if use_cached_probe:
|
||||
return _history_write_ok, _history_write_last_error
|
||||
|
||||
probe_a = f"{history_path}.probe.{os.getpid()}.{threading.get_ident()}.a"
|
||||
probe_b = f"{history_path}.probe.{os.getpid()}.{threading.get_ident()}.b"
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(history_path), exist_ok=True)
|
||||
|
||||
with open(probe_a, "w", encoding="utf-8") as handle:
|
||||
handle.write("[]")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
|
||||
os.replace(probe_a, probe_b)
|
||||
os.remove(probe_b)
|
||||
|
||||
with _history_write_state_lock:
|
||||
_history_write_last_probe_ts = now
|
||||
_register_history_write_success()
|
||||
return True, ""
|
||||
except Exception as exc:
|
||||
with _history_write_state_lock:
|
||||
_history_write_last_probe_ts = now
|
||||
_register_history_write_failure(exc)
|
||||
return False, get_history_write_status().get("message", "")
|
||||
finally:
|
||||
for temp_path in (probe_a, probe_b):
|
||||
try:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _atomic_write_json(target_path, payload):
|
||||
temp_path = f"{target_path}.{os.getpid()}.{threading.get_ident()}.tmp"
|
||||
@@ -24,7 +149,7 @@ def _atomic_write_json(target_path, payload):
|
||||
os.replace(temp_path, target_path)
|
||||
last_error = None
|
||||
break
|
||||
except PermissionError as e:
|
||||
except (PermissionError, OSError) as e:
|
||||
last_error = e
|
||||
time.sleep(0.15 * (attempt + 1))
|
||||
|
||||
@@ -53,6 +178,7 @@ def init_history():
|
||||
logger.error(f"Erreur lors de la création du fichier d'historique : {e}")
|
||||
else:
|
||||
logger.info(f"Fichier d'historique trouvé : {history_path}")
|
||||
check_history_write_access(force=True)
|
||||
return history_path
|
||||
|
||||
def load_history():
|
||||
@@ -102,14 +228,27 @@ def load_history():
|
||||
logger.error(f"Erreur inattendue lors de la lecture de {history_path} : {e}")
|
||||
return []
|
||||
|
||||
def save_history(history):
|
||||
"""Sauvegarde l'historique dans history.json de manière atomique."""
|
||||
def save_history(history, force=False):
|
||||
"""Sauvegarde l'historique dans history.json de manière atomique (mode non-bloquant en cas d'erreur)."""
|
||||
history_path = getattr(config, 'HISTORY_PATH')
|
||||
|
||||
now = time.time()
|
||||
state = get_history_write_status()
|
||||
if (
|
||||
not force
|
||||
and not state.get("ok", True)
|
||||
and (now - float(state.get("last_failure_ts", 0.0) or 0.0)) < _HISTORY_WRITE_FAILURE_COOLDOWN_SEC
|
||||
):
|
||||
return False
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(history_path), exist_ok=True)
|
||||
_atomic_write_json(history_path, history)
|
||||
_register_history_write_success()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'écriture de {history_path} : {e}")
|
||||
_register_history_write_failure(e)
|
||||
return False
|
||||
|
||||
def add_to_history(platform, game_name, status, url=None, progress=0, message=None, timestamp=None):
|
||||
"""Ajoute une entrée à l'historique."""
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Größe",
|
||||
"history_title": "Downloads ({0})",
|
||||
"history_title_downloading_active": "Download - {0} - {1}",
|
||||
"history_title_completed_count": "Abgeschlossene Downloads ({0})",
|
||||
"history_title_error_count": "Fehlgeschlagene Downloads ({0})",
|
||||
"history_title_canceled_count": "Abgebrochene Downloads ({0})",
|
||||
"aria2_phase_connecting": "Verbinden...",
|
||||
"aria2_phase_verifying": "Überprüfen...",
|
||||
"aria2_phase_waiting": "Warten...",
|
||||
"history_empty": "Keine Downloads im Verlauf",
|
||||
"history_column_system": "System",
|
||||
"history_column_game": "Spielname",
|
||||
@@ -109,6 +116,7 @@
|
||||
"filter_platforms_title": "Systemsichtbarkeit",
|
||||
"filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}",
|
||||
"filter_unsaved_warning": "Ungespeicherte Änderungen",
|
||||
"filter_unsaved_toast": "Ungespeicherte Anderungen\nMit History/Start anwenden, bevor du zuruckgehst",
|
||||
"menu_show_unsupported_enabled": "Sichtbarkeit nicht unterstützter Systeme aktiviert",
|
||||
"menu_show_unsupported_disabled": "Sichtbarkeit nicht unterstützter Systeme deaktiviert",
|
||||
"menu_allow_unknown_ext_on": "Warnung bei unbekannter Erweiterung ausblenden: Ja",
|
||||
@@ -256,11 +264,14 @@
|
||||
"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten",
|
||||
"instruction_settings_auto_extract": "Automatische Archivextraktion nach Download aktivieren/deaktivieren",
|
||||
"instruction_settings_roms_folder": "Standard-Download-Verzeichnis für ROMs ändern",
|
||||
"instruction_settings_max_simultaneous_dl": "Max. gleichzeitige Downloads einstellen (links/rechts, 1-10)",
|
||||
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
|
||||
"instruction_settings_connection_status": "Zugriff auf Update- und Quellen-Seiten prüfen",
|
||||
"instruction_settings_web_service": "Web-Dienst Autostart beim Booten aktivieren/deaktivieren",
|
||||
"instruction_settings_custom_dns": "Custom DNS (Cloudflare 1.1.1.1) beim Booten aktivieren/deaktivieren",
|
||||
"settings_auto_extract": "Auto-Extraktion Archive",
|
||||
"settings_max_simultaneous_dl": "Max. gleichzeitige Downloads",
|
||||
"settings_max_dl_changed": "Max. gleichzeitige Downloads: {0}",
|
||||
"settings_auto_extract_enabled": "Aktiviert",
|
||||
"settings_auto_extract_disabled": "Deaktiviert",
|
||||
"settings_roms_folder": "ROMs-Ordner",
|
||||
@@ -312,6 +323,7 @@
|
||||
"history_option_extract_archive": "Archivsextraktion erzwingen",
|
||||
"history_option_open_file": "Datei öffnen",
|
||||
"history_option_scraper": "Metadaten abrufen",
|
||||
"history_option_force_download": "Jetzt herunterladen erzwingen",
|
||||
"history_option_remove_from_queue": "Aus Warteschlange entfernen",
|
||||
"history_option_cancel_download": "Download abbrechen",
|
||||
"history_option_pause_download": "Download pausieren",
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Size",
|
||||
"history_title": "Downloads ({0})",
|
||||
"history_title_downloading_active": "Downloading - {0} - {1}",
|
||||
"history_title_completed_count": "Completed downloads ({0})",
|
||||
"history_title_error_count": "Failed downloads ({0})",
|
||||
"history_title_canceled_count": "Canceled downloads ({0})",
|
||||
"aria2_phase_connecting": "Connecting...",
|
||||
"aria2_phase_verifying": "Verifying...",
|
||||
"aria2_phase_waiting": "Waiting...",
|
||||
"history_empty": "No downloads in history",
|
||||
"history_column_system": "System",
|
||||
"history_column_game": "Game name",
|
||||
@@ -105,6 +112,7 @@
|
||||
"filter_platforms_title": "Systems visibility",
|
||||
"filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Unsaved changes",
|
||||
"filter_unsaved_toast": "Unsaved changes\nApply with History/Start before leaving",
|
||||
"menu_show_unsupported_enabled": "Unsupported systems visibility enabled",
|
||||
"menu_show_unsupported_disabled": "Unsupported systems visibility disabled",
|
||||
"menu_allow_unknown_ext_on": "Hide unknown extension warning: Yes",
|
||||
@@ -258,11 +266,14 @@
|
||||
"instruction_settings_symlink": "Toggle using filesystem symlinks for installs",
|
||||
"instruction_settings_auto_extract": "Toggle automatic archive extraction after download",
|
||||
"instruction_settings_roms_folder": "Change the default ROMs download directory",
|
||||
"instruction_settings_max_simultaneous_dl": "Set the maximum number of simultaneous downloads (left/right, 1-10)",
|
||||
"instruction_settings_api_keys": "See detected premium provider API keys",
|
||||
"instruction_settings_connection_status": "Check access to update and source sites",
|
||||
"instruction_settings_web_service": "Enable/disable web service auto-start at boot",
|
||||
"instruction_settings_custom_dns": "Enable/disable custom DNS (Cloudflare 1.1.1.1) at boot",
|
||||
"settings_auto_extract": "Auto Extract Archives",
|
||||
"settings_max_simultaneous_dl": "Max Simultaneous Downloads",
|
||||
"settings_max_dl_changed": "Max simultaneous downloads: {0}",
|
||||
"settings_auto_extract_enabled": "Enabled",
|
||||
"settings_auto_extract_disabled": "Disabled",
|
||||
"settings_roms_folder": "ROMs Folder",
|
||||
@@ -311,6 +322,7 @@
|
||||
"history_option_extract_archive": "Force extract archive",
|
||||
"history_option_open_file": "Open file",
|
||||
"history_option_scraper": "Scrape metadata",
|
||||
"history_option_force_download": "Force download now",
|
||||
"history_option_remove_from_queue": "Remove from queue",
|
||||
"history_option_cancel_download": "Cancel download",
|
||||
"history_option_pause_download": "Pause download",
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Tamaño",
|
||||
"history_title": "Descargas ({0})",
|
||||
"history_title_downloading_active": "Descargando - {0} - {1}",
|
||||
"history_title_completed_count": "Descargas completadas ({0})",
|
||||
"history_title_error_count": "Descargas con error ({0})",
|
||||
"history_title_canceled_count": "Descargas canceladas ({0})",
|
||||
"aria2_phase_connecting": "Conectando...",
|
||||
"aria2_phase_verifying": "Verificando...",
|
||||
"aria2_phase_waiting": "Esperando...",
|
||||
"history_empty": "No hay descargas en el historial",
|
||||
"history_column_system": "Sistema",
|
||||
"history_column_game": "Nombre del juego",
|
||||
@@ -107,6 +114,7 @@
|
||||
"filter_platforms_title": "Visibilidad de sistemas",
|
||||
"filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Cambios no guardados",
|
||||
"filter_unsaved_toast": "Cambios no guardados\nAplica con History/Start antes de salir",
|
||||
"menu_show_unsupported_enabled": "Visibilidad de sistemas no soportados activada",
|
||||
"menu_show_unsupported_disabled": "Visibilidad de sistemas no soportados desactivada",
|
||||
"menu_allow_unknown_ext_on": "Ocultar aviso de extensión desconocida: Sí",
|
||||
@@ -256,11 +264,14 @@
|
||||
"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones",
|
||||
"instruction_settings_auto_extract": "Activar/desactivar extracción automática de archivos después de descargar",
|
||||
"instruction_settings_roms_folder": "Cambiar el directorio de descarga de ROMs por defecto",
|
||||
"instruction_settings_max_simultaneous_dl": "Establecer máx. descargas simultáneas (izq/der, 1-10)",
|
||||
"instruction_settings_api_keys": "Ver claves API premium detectadas",
|
||||
"instruction_settings_connection_status": "Comprobar acceso a sitios de actualizaciones y fuentes",
|
||||
"instruction_settings_web_service": "Activar/desactivar inicio automático del servicio web",
|
||||
"instruction_settings_custom_dns": "Activar/desactivar DNS personalizado (Cloudflare 1.1.1.1) al inicio",
|
||||
"settings_auto_extract": "Extracción auto de archivos",
|
||||
"settings_max_simultaneous_dl": "Máx. descargas simultáneas",
|
||||
"settings_max_dl_changed": "Máx. descargas simultáneas: {0}",
|
||||
"settings_auto_extract_enabled": "Activado",
|
||||
"settings_auto_extract_disabled": "Desactivado",
|
||||
"settings_roms_folder": "Carpeta ROMs",
|
||||
@@ -312,6 +323,7 @@
|
||||
"history_option_extract_archive": "Forzar extraccion del archivo",
|
||||
"history_option_open_file": "Abrir archivo",
|
||||
"history_option_scraper": "Obtener metadatos",
|
||||
"history_option_force_download": "Forzar descarga ahora",
|
||||
"history_option_remove_from_queue": "Quitar de la cola",
|
||||
"history_option_cancel_download": "Cancelar descarga",
|
||||
"history_option_pause_download": "Pausar descarga",
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Taille",
|
||||
"history_title": "Téléchargements ({0})",
|
||||
"history_title_downloading_active": "Téléchargement - {0} - {1}",
|
||||
"history_title_completed_count": "Téléchargements terminés ({0})",
|
||||
"history_title_error_count": "Téléchargements en erreur ({0})",
|
||||
"history_title_canceled_count": "Téléchargements annulés ({0})",
|
||||
"aria2_phase_connecting": "Connexion...",
|
||||
"aria2_phase_verifying": "Vérification...",
|
||||
"aria2_phase_waiting": "En attente...",
|
||||
"history_empty": "Aucun téléchargement dans l'historique",
|
||||
"history_column_system": "Système",
|
||||
"history_column_game": "Nom du jeu",
|
||||
@@ -105,6 +112,7 @@
|
||||
"filter_platforms_title": "Affichage des systèmes",
|
||||
"filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Modifications non sauvegardées",
|
||||
"filter_unsaved_toast": "Modifications non sauvegardees\nValider avec History/Start avant de quitter",
|
||||
"menu_show_unsupported_enabled": "Affichage systèmes non supportés activé",
|
||||
"support_dialog_title": "Fichier de support",
|
||||
"support_dialog_message": "Un fichier de support a été créé avec tous vos fichiers de configuration et logs.\n\nFichier: {0}\n\nPour obtenir de l'aide :\n1. Rejoignez le Discord RGSX\n2. Décrivez votre problème\n3. Partagez ce fichier ZIP\n\nAppuyez sur {1} pour revenir au menu.",
|
||||
@@ -258,11 +266,14 @@
|
||||
"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation",
|
||||
"instruction_settings_auto_extract": "Activer/désactiver l'extraction automatique des archives après téléchargement",
|
||||
"instruction_settings_roms_folder": "Changer le répertoire de téléchargement des ROMs par défaut",
|
||||
"instruction_settings_max_simultaneous_dl": "Définir le nombre max de téléchargements simultanés (gauche/droite, 1-10)",
|
||||
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
|
||||
"instruction_settings_connection_status": "Vérifier l'accès aux sites d'update et de sources",
|
||||
"instruction_settings_web_service": "Activer/désactiver le démarrage automatique du service web",
|
||||
"instruction_settings_custom_dns": "Activer/désactiver les DNS personnalisés (Cloudflare 1.1.1.1) au démarrage",
|
||||
"settings_auto_extract": "Extraction auto des archives",
|
||||
"settings_max_simultaneous_dl": "Téléchargements simultanés max",
|
||||
"settings_max_dl_changed": "Téléchargements simultanés max : {0}",
|
||||
"settings_auto_extract_enabled": "Activé",
|
||||
"settings_auto_extract_disabled": "Désactivé",
|
||||
"settings_roms_folder": "Dossier ROMs",
|
||||
@@ -311,6 +322,8 @@
|
||||
"history_option_extract_archive": "Forcer l'extraction",
|
||||
"history_option_open_file": "Ouvrir le fichier",
|
||||
"history_option_scraper": "Récupérer métadonnées",
|
||||
"history_option_force_download": "Forcer le téléchargement maintenant",
|
||||
"history_option_force_download": "Forcer le téléchargement maintenant",
|
||||
"history_option_remove_from_queue": "Retirer de la file d'attente",
|
||||
"history_option_cancel_download": "Annuler le téléchargement",
|
||||
"history_option_pause_download": "Mettre en pause",
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Dimensione",
|
||||
"history_title": "Download ({0})",
|
||||
"history_title_downloading_active": "Download in corso - {0} - {1}",
|
||||
"history_title_completed_count": "Download completati ({0})",
|
||||
"history_title_error_count": "Download con errore ({0})",
|
||||
"history_title_canceled_count": "Download annullati ({0})",
|
||||
"aria2_phase_connecting": "Connessione...",
|
||||
"aria2_phase_verifying": "Verifica...",
|
||||
"aria2_phase_waiting": "In attesa...",
|
||||
"history_empty": "Nessun download nella cronologia",
|
||||
"history_column_system": "Sistema",
|
||||
"history_column_game": "Nome del gioco",
|
||||
@@ -105,6 +112,7 @@
|
||||
"filter_platforms_title": "Visibilità sistemi",
|
||||
"filter_platforms_info": "Visibili: {0} | Nascosti: {1} / Totale: {2}",
|
||||
"filter_unsaved_warning": "Modifiche non salvate",
|
||||
"filter_unsaved_toast": "Modifiche non salvate\nApplica con History/Start prima di uscire",
|
||||
"menu_show_unsupported_enabled": "Visibilità sistemi non supportati abilitata",
|
||||
"menu_show_unsupported_disabled": "Visibilità sistemi non supportati disabilitata",
|
||||
"menu_allow_unknown_ext_on": "Nascondi avviso estensione sconosciuta: Sì",
|
||||
@@ -251,11 +259,14 @@
|
||||
"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni",
|
||||
"instruction_settings_auto_extract": "Attivare/disattivare estrazione automatica archivi dopo il download",
|
||||
"instruction_settings_roms_folder": "Cambiare la directory di download ROMs predefinita",
|
||||
"instruction_settings_max_simultaneous_dl": "Imposta il max di download simultanei (sinistra/destra, 1-10)",
|
||||
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
|
||||
"instruction_settings_connection_status": "Verifica accesso ai siti di aggiornamento e sorgenti",
|
||||
"instruction_settings_web_service": "Attivare/disattivare avvio automatico servizio web all'avvio",
|
||||
"instruction_settings_custom_dns": "Attivare/disattivare DNS personalizzato (Cloudflare 1.1.1.1) all'avvio",
|
||||
"settings_auto_extract": "Estrazione auto archivi",
|
||||
"settings_max_simultaneous_dl": "Max download simultanei",
|
||||
"settings_max_dl_changed": "Max download simultanei: {0}",
|
||||
"settings_auto_extract_enabled": "Attivato",
|
||||
"settings_auto_extract_disabled": "Disattivato",
|
||||
"settings_roms_folder": "Cartella ROMs",
|
||||
@@ -307,6 +318,7 @@
|
||||
"history_option_extract_archive": "Forza estrazione archivio",
|
||||
"history_option_open_file": "Apri file",
|
||||
"history_option_scraper": "Scraper metadati",
|
||||
"history_option_force_download": "Forza il download ora",
|
||||
"history_option_remove_from_queue": "Rimuovi dalla coda",
|
||||
"history_option_cancel_download": "Annulla download",
|
||||
"history_option_pause_download": "Pausa download",
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
"game_header_ext": "Ext",
|
||||
"game_header_size": "Tamanho",
|
||||
"history_title": "Downloads ({0})",
|
||||
"history_title_downloading_active": "Baixando - {0} - {1}",
|
||||
"history_title_completed_count": "Downloads concluídos ({0})",
|
||||
"history_title_error_count": "Downloads com erro ({0})",
|
||||
"history_title_canceled_count": "Downloads cancelados ({0})",
|
||||
"aria2_phase_connecting": "Conectando...",
|
||||
"aria2_phase_verifying": "Verificando...",
|
||||
"aria2_phase_waiting": "Aguardando...",
|
||||
"history_empty": "Nenhum download no histórico",
|
||||
"history_column_system": "Sistema",
|
||||
"history_column_game": "Nome do jogo",
|
||||
@@ -109,6 +116,7 @@
|
||||
"filter_platforms_title": "Visibilidade dos sistemas",
|
||||
"filter_platforms_info": "Visíveis: {0} | Ocultos: {1} / Total: {2}",
|
||||
"filter_unsaved_warning": "Alterações não salvas",
|
||||
"filter_unsaved_toast": "Alteracoes nao salvas\nAplique com History/Start antes de sair",
|
||||
"menu_show_unsupported_enabled": "Visibilidade de sistemas não suportados ativada",
|
||||
"menu_show_unsupported_disabled": "Visibilidade de sistemas não suportados desativada",
|
||||
"menu_allow_unknown_ext_on": "Ocultar aviso de extensão desconhecida: Sim",
|
||||
@@ -257,11 +265,14 @@
|
||||
"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações",
|
||||
"instruction_settings_auto_extract": "Ativar/desativar extração automática de arquivos após download",
|
||||
"instruction_settings_roms_folder": "Alterar o diretório de download de ROMs padrão",
|
||||
"instruction_settings_max_simultaneous_dl": "Definir máx. downloads simultâneos (esq/dir, 1-10)",
|
||||
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
|
||||
"instruction_settings_connection_status": "Verificar acesso a sites de atualização e fontes",
|
||||
"instruction_settings_web_service": "Ativar/desativar início automático do serviço web na inicialização",
|
||||
"instruction_settings_custom_dns": "Ativar/desativar DNS personalizado (Cloudflare 1.1.1.1) na inicialização",
|
||||
"settings_auto_extract": "Extração auto de arquivos",
|
||||
"settings_max_simultaneous_dl": "Máx. downloads simultâneos",
|
||||
"settings_max_dl_changed": "Máx. downloads simultâneos: {0}",
|
||||
"settings_auto_extract_enabled": "Ativado",
|
||||
"settings_auto_extract_disabled": "Desativado",
|
||||
"settings_roms_folder": "Pasta ROMs",
|
||||
@@ -313,6 +324,7 @@
|
||||
"history_option_extract_archive": "Forcar extracao do arquivo",
|
||||
"history_option_open_file": "Abrir arquivo",
|
||||
"history_option_scraper": "Obter metadados",
|
||||
"history_option_force_download": "Forçar download agora",
|
||||
"history_option_remove_from_queue": "Remover da fila",
|
||||
"history_option_cancel_download": "Cancelar download",
|
||||
"history_option_pause_download": "Pausar download",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,7 +86,8 @@ def load_rgsx_settings():
|
||||
"last_gamelist_update": None,
|
||||
"last_gamelist_prompt_remote_update": None,
|
||||
"global_sort_option": "name_asc",
|
||||
"platform_custom_paths": {} # Chemins personnalisés par plateforme
|
||||
"platform_custom_paths": {}, # Chemins personnalisés par plateforme
|
||||
"max_simultaneous_downloads": 5 # Limite de téléchargements simultanés
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -342,33 +343,15 @@ def get_sources_zip_url(fallback_url):
|
||||
def find_local_custom_sources_zip():
|
||||
"""Recherche un fichier ZIP local à la racine de SAVE_FOLDER pour le mode custom.
|
||||
|
||||
Priorité sur quelques noms courants afin d'éviter toute ambiguïté.
|
||||
Ne prend en compte QUE games.zip.
|
||||
Retourne le chemin absolu du ZIP si trouvé, sinon None.
|
||||
"""
|
||||
try:
|
||||
from config import SAVE_FOLDER
|
||||
candidates = [
|
||||
"games.zip",
|
||||
"custom_sources.zip",
|
||||
"rgsx_custom_sources.zip",
|
||||
"data.zip",
|
||||
]
|
||||
if not os.path.isdir(SAVE_FOLDER):
|
||||
return None
|
||||
for name in candidates:
|
||||
p = os.path.join(SAVE_FOLDER, name)
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
# Option avancée: prendre le plus récent *.zip si aucun nom connu trouvé
|
||||
try:
|
||||
zips = [os.path.join(SAVE_FOLDER, f) for f in os.listdir(SAVE_FOLDER) if f.lower().endswith('.zip')]
|
||||
zips = [z for z in zips if os.path.isfile(z)]
|
||||
if zips:
|
||||
newest = max(zips, key=lambda z: os.path.getmtime(z))
|
||||
return newest
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
games_zip = os.path.join(SAVE_FOLDER, "games.zip")
|
||||
return games_zip if os.path.isfile(games_zip) else None
|
||||
except Exception as e:
|
||||
logger.debug(f"find_local_custom_sources_zip error: {e}")
|
||||
return None
|
||||
@@ -689,3 +672,27 @@ def set_auto_extract(enabled: bool):
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting auto_extract: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def get_max_simultaneous_downloads() -> int:
|
||||
"""Récupère la limite de téléchargements simultanés (1-10, défaut 5)."""
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
val = settings.get("max_simultaneous_downloads", 5)
|
||||
return max(1, min(10, int(val)))
|
||||
except Exception:
|
||||
return 5
|
||||
|
||||
|
||||
def set_max_simultaneous_downloads(value: int) -> int:
|
||||
"""Définit la limite de téléchargements simultanés et met à jour config. Retourne la valeur appliquée."""
|
||||
value = max(1, min(10, int(value)))
|
||||
try:
|
||||
settings = load_rgsx_settings()
|
||||
settings["max_simultaneous_downloads"] = value
|
||||
save_rgsx_settings(settings)
|
||||
config.max_simultaneous_downloads = value
|
||||
logger.info(f"max_simultaneous_downloads set to {value}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting max_simultaneous_downloads: {e}")
|
||||
return value
|
||||
|
||||
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
import config
|
||||
from history import load_history, save_history
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url, request_torrent_manifest_refresh
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url, request_torrent_manifest_refresh, _resolve_platform_image_path
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
@@ -464,8 +464,12 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
self._set_headers('application/json', status, etag=etag, last_modified=cached_dt)
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
try:
|
||||
self._set_headers('application/json', status, etag=etag, last_modified=cached_dt)
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
except (ConnectionAbortedError, BrokenPipeError) as e:
|
||||
logger.debug(f"Connexion fermée par le client pendant l'envoi JSON: {e}")
|
||||
return
|
||||
|
||||
def _send_html(self, html, status=200, etag=None, last_modified=None):
|
||||
"""Envoie une réponse HTML"""
|
||||
@@ -791,6 +795,8 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
'status': status,
|
||||
'progress_percent': entry.get('progress', 0),
|
||||
'speed': entry.get('speed', 0),
|
||||
'seeds': entry.get('seeds', 0),
|
||||
'connections': entry.get('connections', 0),
|
||||
'game_name': entry.get('game_name', ''),
|
||||
'platform': entry.get('platform', ''),
|
||||
'timestamp': entry.get('timestamp', '')
|
||||
@@ -1173,13 +1179,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
}, status=400)
|
||||
return
|
||||
|
||||
if parse_torrent_download_url(game_url) is not None:
|
||||
torrent_message = TRANSLATIONS.get('popup_torrent_in_maintenance', 'torrent in maintence')
|
||||
self._send_json({
|
||||
'success': False,
|
||||
'error': torrent_message
|
||||
}, status=400)
|
||||
return
|
||||
# Suppression du blocage torrent : on laisse passer les URLs rgsx+torrent
|
||||
|
||||
# Vérifier l'extension et déterminer si extraction nécessaire
|
||||
from utils import check_extension_before_download
|
||||
@@ -1782,86 +1782,28 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
platform_dict = pd
|
||||
break
|
||||
|
||||
# Dossiers où chercher les images
|
||||
image_folders = [
|
||||
config.IMAGES_FOLDER, # Dossier utilisateur (saves/ports/rgsx/images)
|
||||
os.path.join(config.APP_FOLDER, 'assets', 'images') # Dossier app
|
||||
]
|
||||
|
||||
# Extensions possibles
|
||||
extensions = ['.png', '.jpg', '.jpeg', '.gif']
|
||||
|
||||
# Construire la liste des noms de fichiers à chercher (ordre de priorité)
|
||||
candidates = []
|
||||
|
||||
if platform_dict:
|
||||
# 1. platform_image explicite (priorité max)
|
||||
platform_image_field = (platform_dict.get('platform_image') or '').strip()
|
||||
if platform_image_field:
|
||||
candidates.append(platform_image_field)
|
||||
|
||||
# 2. platform_name.png
|
||||
candidates.append(platform_name)
|
||||
|
||||
# 3. folder.png si disponible
|
||||
folder_name = platform_dict.get('folder')
|
||||
if folder_name:
|
||||
candidates.append(folder_name)
|
||||
else:
|
||||
# Pas de platform_dict trouvé, juste essayer le nom
|
||||
candidates.append(platform_name)
|
||||
|
||||
# Chercher le fichier image
|
||||
image_path = None
|
||||
for candidate in candidates:
|
||||
# Retirer l'extension si déjà présente
|
||||
candidate_base = os.path.splitext(candidate)[0]
|
||||
|
||||
for folder in image_folders:
|
||||
if not os.path.exists(folder):
|
||||
continue
|
||||
|
||||
# Essayer avec chaque extension
|
||||
for ext in extensions:
|
||||
test_path = os.path.join(folder, candidate_base + ext)
|
||||
if os.path.exists(test_path):
|
||||
image_path = test_path
|
||||
break
|
||||
|
||||
if image_path:
|
||||
break
|
||||
|
||||
if image_path:
|
||||
break
|
||||
|
||||
# Si pas trouvé, chercher default.png
|
||||
if not image_path:
|
||||
for folder in image_folders:
|
||||
default_path = os.path.join(folder, 'default.png')
|
||||
if os.path.exists(default_path):
|
||||
image_path = default_path
|
||||
break
|
||||
|
||||
# Envoyer l'image
|
||||
payload_platform = platform_dict or {'platform_name': platform_name}
|
||||
image_path = _resolve_platform_image_path(payload_platform)
|
||||
|
||||
if image_path and os.path.exists(image_path):
|
||||
# Déterminer le type MIME
|
||||
ext = os.path.splitext(image_path)[1].lower()
|
||||
mime_types = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif'
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
}
|
||||
content_type = mime_types.get(ext, 'image/png')
|
||||
|
||||
# Lire et envoyer l'image avec headers de cache
|
||||
with open(image_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
|
||||
|
||||
# Ajouter les headers de cache (1 heure)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', content_type)
|
||||
self.send_header('Cache-Control', 'public, max-age=3600') # 1 heure
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(image_data)
|
||||
@@ -1870,7 +1812,9 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
logger.warning(f"Aucune image trouvée pour {platform_name}, envoi PNG transparent")
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'image/png')
|
||||
self.send_header('Cache-Control', 'public, max-age=3600')
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
# PNG transparent 1x1 pixel
|
||||
@@ -1881,7 +1825,9 @@ DO NOT share this file publicly as it may contain sensitive information.
|
||||
logger.error(f"Erreur lors du chargement de l'image {platform_name}: {e}", exc_info=True)
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'image/png')
|
||||
self.send_header('Cache-Control', 'public, max-age=60') # Cache court pour les erreurs
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
# PNG transparent en cas d'erreur
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
Module de scraping pour récupérer les métadonnées des jeux depuis TheGamesDB.net API v1
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import re
|
||||
from io import BytesIO
|
||||
import pygame
|
||||
import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Clé API publique pour TheGamesDB
|
||||
API_KEY = "bdbb4a1ce5f1c12c1bcc119aeb4d4923d3887e22ad336d576e9b9e5da5ecaa3c"
|
||||
SOURCE_SUFFIXES = {"Archive", "LolRoms", "Torrent", "1Fichier"}
|
||||
THEGAMESDB_API_KEY_PATH = getattr(config, "THEGAMESDB_API_KEY_PATH", os.path.join(config.APP_FOLDER, "assets", "TheGamesDBAPI.txt"))
|
||||
|
||||
API_BASE_URL = "https://api.thegamesdb.net/v1"
|
||||
|
||||
# Mapping des noms de plateformes vers leurs IDs sur TheGamesDB API
|
||||
@@ -141,6 +144,55 @@ PLATFORM_MAPPING = {
|
||||
"Amiga": 4911
|
||||
}
|
||||
|
||||
SCRAPER_PLATFORM_ALIASES = {
|
||||
"Family Computer Disk System (Famicom)": "Family Computer Disk System",
|
||||
"Nintendo Famicom Disk System": "Family Computer Disk System",
|
||||
}
|
||||
|
||||
|
||||
def normalize_scraper_platform_name(platform_name):
|
||||
"""Map display platform names back to scraper-compatible base names."""
|
||||
text = str(platform_name or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
while True:
|
||||
match = re.search(r'\(([^()]+)\)\s*$', text)
|
||||
if not match:
|
||||
break
|
||||
suffix = match.group(1).strip()
|
||||
if suffix in SOURCE_SUFFIXES or suffix == "RomHacks":
|
||||
text = text[:match.start()].rstrip()
|
||||
continue
|
||||
break
|
||||
|
||||
return SCRAPER_PLATFORM_ALIASES.get(text, text)
|
||||
|
||||
|
||||
def get_thegamesdb_api_key():
|
||||
"""Load TheGamesDB API key from env or shared local file."""
|
||||
env_key = os.environ.get("RGSX_THEGAMESDB_API_KEY", "").strip()
|
||||
if env_key:
|
||||
return env_key, "env"
|
||||
|
||||
try:
|
||||
if not os.path.exists(THEGAMESDB_API_KEY_PATH):
|
||||
os.makedirs(os.path.dirname(THEGAMESDB_API_KEY_PATH), exist_ok=True)
|
||||
with open(THEGAMESDB_API_KEY_PATH, 'w', encoding='utf-8') as handle:
|
||||
handle.write("")
|
||||
except Exception as exc:
|
||||
logger.warning(f"Impossible de préparer le fichier de clé TheGamesDB: {exc}")
|
||||
|
||||
try:
|
||||
with open(THEGAMESDB_API_KEY_PATH, 'r', encoding='utf-8') as handle:
|
||||
file_key = handle.read().strip()
|
||||
if file_key:
|
||||
return file_key, THEGAMESDB_API_KEY_PATH
|
||||
except Exception as exc:
|
||||
logger.warning(f"Impossible de lire la clé TheGamesDB: {exc}")
|
||||
|
||||
return "", "missing"
|
||||
|
||||
|
||||
def clean_game_name(game_name):
|
||||
"""
|
||||
@@ -180,12 +232,24 @@ def get_game_metadata(game_name, platform_name):
|
||||
Keys: image_url, game_page_url, description, genre, release_date, error
|
||||
"""
|
||||
clean_name = clean_game_name(game_name)
|
||||
logger.info(f"Recherche métadonnées pour: '{clean_name}' sur plateforme '{platform_name}'")
|
||||
normalized_platform_name = normalize_scraper_platform_name(platform_name)
|
||||
api_key, api_key_source = get_thegamesdb_api_key()
|
||||
|
||||
if not api_key:
|
||||
logger.warning("Clé API TheGamesDB absente")
|
||||
return {"error": "Clé API TheGamesDB manquante"}
|
||||
|
||||
logger.info(
|
||||
f"Recherche métadonnées pour: '{clean_name}' sur plateforme '{platform_name}'"
|
||||
f" -> '{normalized_platform_name}'"
|
||||
)
|
||||
|
||||
# Obtenir l'ID de la plateforme
|
||||
platform_id = PLATFORM_MAPPING.get(platform_name)
|
||||
platform_id = PLATFORM_MAPPING.get(normalized_platform_name)
|
||||
if not platform_id:
|
||||
logger.warning(f"Plateforme '{platform_name}' non trouvée dans le mapping")
|
||||
logger.warning(
|
||||
f"Plateforme '{platform_name}' normalisée en '{normalized_platform_name}' non trouvée dans le mapping"
|
||||
)
|
||||
return {"error": f"Plateforme '{platform_name}' non supportée"}
|
||||
|
||||
try:
|
||||
@@ -193,14 +257,16 @@ def get_game_metadata(game_name, platform_name):
|
||||
# Documentation: https://api.thegamesdb.net/#/Games/GamesbyName
|
||||
url = f"{API_BASE_URL}/Games/ByGameName"
|
||||
params = {
|
||||
"apikey": API_KEY,
|
||||
"apikey": api_key,
|
||||
"name": clean_name,
|
||||
"filter[platform]": platform_id,
|
||||
"fields": "players,publishers,genres,overview,last_updated,rating,platform,coop,youtube,os,processor,ram,hdd,video,sound,alternates",
|
||||
"include": "boxart"
|
||||
}
|
||||
|
||||
logger.debug(f"Requête API: {url} avec name='{clean_name}', platform={platform_id}")
|
||||
logger.debug(
|
||||
f"Requête API: {url} avec name='{clean_name}', platform={platform_id}, key_source={api_key_source}"
|
||||
)
|
||||
response = requests.get(url, params=params, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
@@ -247,7 +313,7 @@ def get_game_metadata(game_name, platform_name):
|
||||
try:
|
||||
images_url = f"{API_BASE_URL}/Games/Images"
|
||||
images_params = {
|
||||
"apikey": API_KEY,
|
||||
"apikey": api_key,
|
||||
"games_id": game_id,
|
||||
"filter[type]": "boxart"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
let currentPlatform = null;
|
||||
let currentGameSort = 'name_asc'; // Type de tri actuel: 'name_asc', 'name_desc', 'size_asc', 'size_desc'
|
||||
let currentGames = []; // Stocke les jeux actuels pour le tri
|
||||
const loggedUnparsedSizeTexts = new Set();
|
||||
let lastProgressUpdate = Date.now();
|
||||
let autoRefreshTimeout = null;
|
||||
let progressInterval = null;
|
||||
@@ -196,6 +197,28 @@
|
||||
const lang = translations['_language'] || navigator.language.substring(0, 2);
|
||||
return lang === 'fr' ? 'Mo/s' : 'MB/s';
|
||||
}
|
||||
|
||||
function formatSpeedFromMib(speedMib) {
|
||||
const speed = Number(speedMib || 0);
|
||||
if (!Number.isFinite(speed) || speed <= 0) return '';
|
||||
|
||||
const lang = translations['_language'] || navigator.language.substring(0, 2);
|
||||
const units = lang === 'fr'
|
||||
? ['o/s', 'Ko/s', 'Mo/s', 'Go/s']
|
||||
: ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
||||
|
||||
const bytesPerSecond = speed * 1024 * 1024;
|
||||
if (bytesPerSecond < 1024) {
|
||||
return `${bytesPerSecond.toFixed(0)} ${units[0]}`;
|
||||
}
|
||||
if (bytesPerSecond < 1024 * 1024) {
|
||||
return `${(bytesPerSecond / 1024).toFixed(1)} ${units[1]}`;
|
||||
}
|
||||
if (bytesPerSecond < 1024 * 1024 * 1024) {
|
||||
return `${(bytesPerSecond / (1024 * 1024)).toFixed(2)} ${units[2]}`;
|
||||
}
|
||||
return `${(bytesPerSecond / (1024 * 1024 * 1024)).toFixed(2)} ${units[3]}`;
|
||||
}
|
||||
|
||||
// Fonction pour formater une taille en octets
|
||||
function formatSize(bytes) {
|
||||
@@ -209,6 +232,74 @@
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function parseSizeToBytes(sizeText) {
|
||||
if (!sizeText) return 0;
|
||||
|
||||
const rawText = String(sizeText).trim();
|
||||
let normalized = rawText.replace(/octets?/gi, 'B');
|
||||
|
||||
if (normalized.includes(',') && normalized.includes('.')) {
|
||||
normalized = normalized.replace(/,/g, '');
|
||||
} else if (normalized.includes(',')) {
|
||||
normalized = normalized.replace(',', '.');
|
||||
}
|
||||
|
||||
const match = normalized.match(/^([0-9]+(?:\.[0-9]+)?)\s*([a-zA-Z]+)/);
|
||||
if (!match) {
|
||||
if (!loggedUnparsedSizeTexts.has(rawText)) {
|
||||
loggedUnparsedSizeTexts.add(rawText);
|
||||
console.warn('[RGSX][sort] Taille non interpretable:', rawText);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
if (Number.isNaN(value)) return 0;
|
||||
|
||||
const unit = match[2].toLowerCase();
|
||||
const multipliers = {
|
||||
b: 1,
|
||||
byte: 1,
|
||||
bytes: 1,
|
||||
o: 1,
|
||||
k: 1024,
|
||||
ko: 1024,
|
||||
kb: 1024,
|
||||
kib: 1024,
|
||||
kio: 1024,
|
||||
m: 1024 ** 2,
|
||||
mo: 1024 ** 2,
|
||||
mb: 1024 ** 2,
|
||||
mib: 1024 ** 2,
|
||||
mio: 1024 ** 2,
|
||||
g: 1024 ** 3,
|
||||
go: 1024 ** 3,
|
||||
gb: 1024 ** 3,
|
||||
gib: 1024 ** 3,
|
||||
gio: 1024 ** 3,
|
||||
t: 1024 ** 4,
|
||||
to: 1024 ** 4,
|
||||
tb: 1024 ** 4,
|
||||
tib: 1024 ** 4,
|
||||
tio: 1024 ** 4,
|
||||
p: 1024 ** 5,
|
||||
po: 1024 ** 5,
|
||||
pb: 1024 ** 5,
|
||||
pib: 1024 ** 5,
|
||||
pio: 1024 ** 5,
|
||||
};
|
||||
|
||||
if (!multipliers[unit]) {
|
||||
if (!loggedUnparsedSizeTexts.has(rawText)) {
|
||||
loggedUnparsedSizeTexts.add(rawText);
|
||||
console.warn('[RGSX][sort] Unite de taille non supportee:', rawText, '->', unit);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(value * multipliers[unit]);
|
||||
}
|
||||
|
||||
// Appliquer les traductions à tous les éléments marqués
|
||||
function applyTranslations() {
|
||||
@@ -1071,6 +1162,38 @@
|
||||
currentGameSort = sortType;
|
||||
const items = Array.from(document.querySelectorAll('.game-item'));
|
||||
const gamesList = document.querySelector('.games-list');
|
||||
|
||||
if (!gamesList) {
|
||||
console.warn('[RGSX][sort] .games-list introuvable pour le tri', sortType);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldLogSizeSort = sortType === 'size_asc' || sortType === 'size_desc';
|
||||
const getSizeInBytes = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
return parseSizeToBytes(sizeElem.textContent);
|
||||
};
|
||||
|
||||
if (shouldLogSizeSort) {
|
||||
const previewBefore = items.slice(0, 5).map(item => {
|
||||
const sizeText = item.querySelector('.game-size')?.textContent?.trim() || '';
|
||||
return {
|
||||
name: item.querySelector('.game-name')?.textContent?.trim() || '',
|
||||
sizeText,
|
||||
sizeBytes: getSizeInBytes(item.querySelector('.game-size')),
|
||||
};
|
||||
});
|
||||
const zeroSizedCount = items.filter(item => {
|
||||
const sizeElem = item.querySelector('.game-size');
|
||||
return sizeElem && getSizeInBytes(sizeElem) === 0;
|
||||
}).length;
|
||||
console.debug('[RGSX][sort] Debut tri taille', {
|
||||
sortType,
|
||||
totalItems: items.length,
|
||||
zeroSizedCount,
|
||||
previewBefore,
|
||||
});
|
||||
}
|
||||
|
||||
// Trier les éléments
|
||||
items.sort((a, b) => {
|
||||
@@ -1079,41 +1202,15 @@
|
||||
const sizeElemA = a.querySelector('.game-size');
|
||||
const sizeElemB = b.querySelector('.game-size');
|
||||
|
||||
// Extraire la taille en Mo (normalisée)
|
||||
const getSizeInMo = (sizeElem) => {
|
||||
if (!sizeElem) return 0;
|
||||
const text = sizeElem.textContent;
|
||||
// Support des formats: "100 Mo", "2.5 Go" (français) et "100 MB", "2.5 GB" (anglais)
|
||||
// Plus Ko/KB, o/B, To/TB
|
||||
const match = text.match(/([0-9.]+)\s*(o|B|Ko|KB|Mo|MB|Go|GB|To|TB)/i);
|
||||
if (!match) return 0;
|
||||
let size = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
// Convertir tout en Mo
|
||||
if (unit === 'O' || unit === 'B') {
|
||||
size /= (1024 * 1024); // octets/bytes vers Mo
|
||||
} else if (unit === 'KO' || unit === 'KB') {
|
||||
size /= 1024; // Ko vers Mo
|
||||
} else if (unit === 'MO' || unit === 'MB') {
|
||||
// Déjà en Mo
|
||||
} else if (unit === 'GO' || unit === 'GB') {
|
||||
size *= 1024; // Go vers Mo
|
||||
} else if (unit === 'TO' || unit === 'TB') {
|
||||
size *= 1024 * 1024; // To vers Mo
|
||||
}
|
||||
return size;
|
||||
};
|
||||
|
||||
switch(sortType) {
|
||||
case 'name_asc':
|
||||
return nameA.localeCompare(nameB);
|
||||
case 'name_desc':
|
||||
return nameB.localeCompare(nameA);
|
||||
case 'size_asc':
|
||||
return getSizeInMo(sizeElemA) - getSizeInMo(sizeElemB);
|
||||
return getSizeInBytes(sizeElemA) - getSizeInBytes(sizeElemB);
|
||||
case 'size_desc':
|
||||
return getSizeInMo(sizeElemB) - getSizeInMo(sizeElemA);
|
||||
return getSizeInBytes(sizeElemB) - getSizeInBytes(sizeElemA);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -1124,6 +1221,18 @@
|
||||
items.forEach(item => {
|
||||
gamesList.appendChild(item);
|
||||
});
|
||||
|
||||
if (shouldLogSizeSort) {
|
||||
const previewAfter = items.slice(0, 5).map(item => ({
|
||||
name: item.querySelector('.game-name')?.textContent?.trim() || '',
|
||||
sizeText: item.querySelector('.game-size')?.textContent?.trim() || '',
|
||||
sizeBytes: getSizeInBytes(item.querySelector('.game-size')),
|
||||
}));
|
||||
console.debug('[RGSX][sort] Fin tri taille', {
|
||||
sortType,
|
||||
previewAfter,
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour les boutons de tri
|
||||
document.querySelectorAll('.sort-btn').forEach(btn => {
|
||||
@@ -1150,6 +1259,7 @@
|
||||
|
||||
// Construire le HTML avec les traductions
|
||||
let searchPlaceholder = t('web_search_platform');
|
||||
const platformImageCacheBuster = Date.now();
|
||||
let html = `
|
||||
<div class="search-box">
|
||||
<input type="text" id="platform-search" placeholder="🔍 ${searchPlaceholder}"
|
||||
@@ -1164,9 +1274,9 @@
|
||||
let gameCountText = t('web_game_count', '📦', p.games_count || 0);
|
||||
html += `
|
||||
<div class="platform-card" onclick='loadGames("${p.platform_name.replace(/"/g, """).replace(/'/g, "'")}")'>
|
||||
<img src="/api/image/${encodeURIComponent(p.platform_name)}"
|
||||
<img src="/api/image/${encodeURIComponent(p.platform_name)}?v=${platformImageCacheBuster}"
|
||||
alt="${p.platform_name}"
|
||||
onerror="this.src='/api/image/default'">
|
||||
onerror="this.src='/api/image/default?v=${platformImageCacheBuster}'">
|
||||
<h3>${p.platform_name}</h3>
|
||||
<div class="count">${gameCountText}</div>
|
||||
</div>
|
||||
@@ -1452,7 +1562,7 @@
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9em;">
|
||||
<span>${status} - ${percent.toFixed(1)}%</span>
|
||||
<span>${speed > 0 ? speed.toFixed(2) + ' ' + getSpeedUnit() : ''}</span>
|
||||
<span>${formatSpeedFromMib(speed)}</span>
|
||||
</div>
|
||||
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${formatSize(downloaded)} / ${formatSize(total)}</div>` : ''}
|
||||
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
|
||||
@@ -1520,7 +1630,7 @@
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9em;">
|
||||
<span>${status} - ${percent.toFixed(1)}%</span>
|
||||
<span>${speed > 0 ? speed.toFixed(2) + ' ' + getSpeedUnit() : ''}</span>
|
||||
<span>${formatSpeedFromMib(speed)}</span>
|
||||
</div>
|
||||
${total > 0 ? `<div style="font-size: 0.85em; color: #666;">${formatSize(downloaded)} / ${formatSize(total)}</div>` : ''}
|
||||
<div style="margin-top: 3px; font-size: 0.85em; color: #666;">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import io
|
||||
import shutil
|
||||
import requests # type: ignore
|
||||
import re
|
||||
@@ -29,11 +30,391 @@ from language import _
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import tempfile
|
||||
try:
|
||||
from PIL import Image # type: ignore
|
||||
except Exception:
|
||||
Image = None # type: ignore
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
|
||||
|
||||
_SOURCE_BADGE_CACHE = {}
|
||||
|
||||
|
||||
def _platform_image_folders():
|
||||
return [
|
||||
config.IMAGES_FOLDER,
|
||||
os.path.join(config.APP_FOLDER, "assets", "images"),
|
||||
os.path.join(config.APP_FOLDER, "images"),
|
||||
]
|
||||
|
||||
|
||||
def _resolve_platform_image_path(platform_dict):
|
||||
platform_name = platform_dict.get("platform_name", "unknown")
|
||||
folder_name = platform_dict.get("folder") or ""
|
||||
|
||||
candidates = []
|
||||
platform_image_field = (platform_dict.get("platform_image") or "").strip()
|
||||
if platform_image_field:
|
||||
candidates.append(platform_image_field)
|
||||
candidates.append(platform_name)
|
||||
if folder_name:
|
||||
candidates.append(folder_name)
|
||||
|
||||
image_path = None
|
||||
for candidate in candidates:
|
||||
candidate_base = os.path.splitext(candidate)[0]
|
||||
for folder in _platform_image_folders():
|
||||
if not os.path.exists(folder):
|
||||
continue
|
||||
for ext in ('.png', '.jpg', '.jpeg', '.gif', '.webp'):
|
||||
test_path = os.path.join(folder, candidate_base + ext)
|
||||
if os.path.exists(test_path):
|
||||
image_path = test_path
|
||||
break
|
||||
if image_path:
|
||||
break
|
||||
if image_path:
|
||||
break
|
||||
|
||||
if image_path:
|
||||
return image_path
|
||||
|
||||
for folder in _platform_image_folders():
|
||||
default_path = os.path.join(folder, 'default.png')
|
||||
if os.path.exists(default_path):
|
||||
return default_path
|
||||
return None
|
||||
|
||||
|
||||
def get_platform_source_badge_key(platform_name: str):
|
||||
text = str(platform_name).strip()
|
||||
if not text:
|
||||
return None
|
||||
match = re.search(r'\(([^()]+)\)\s*$', text)
|
||||
if not match:
|
||||
return None
|
||||
source = match.group(1).strip().lower()
|
||||
mapping = {
|
||||
'archive': 'Archive',
|
||||
'lolroms': 'LolRoms',
|
||||
'torrent': 'Torrent',
|
||||
'1fichier': '1Fichier',
|
||||
'vimms': 'Vimms',
|
||||
'edgeemu': 'EdgeEmu',
|
||||
'edgeemu.net': 'EdgeEmu',
|
||||
}
|
||||
return mapping.get(source)
|
||||
|
||||
|
||||
def _load_svg_badge_surface(svg_path: str, size: int):
|
||||
if pygame is None:
|
||||
return None
|
||||
try:
|
||||
surf = pygame.image.load(svg_path)
|
||||
width, height = surf.get_size()
|
||||
if width <= 0 or height <= 0:
|
||||
return None
|
||||
scale = min(size / width, size / height)
|
||||
scaled = pygame.transform.smoothscale(
|
||||
surf,
|
||||
(max(1, int(width * scale)), max(1, int(height * scale))),
|
||||
)
|
||||
return _safe_convert_alpha(scaled)
|
||||
except Exception as exc:
|
||||
logger.debug(f"Badge SVG load failed for {svg_path}: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def _safe_convert_alpha(surface):
|
||||
if pygame is None or surface is None:
|
||||
return surface
|
||||
try:
|
||||
if pygame.display.get_surface() is not None:
|
||||
return surface.convert_alpha()
|
||||
except Exception:
|
||||
pass
|
||||
return surface.copy()
|
||||
|
||||
|
||||
def _build_text_badge_icon(label: str, size: int, color):
|
||||
if pygame is None:
|
||||
return None
|
||||
try:
|
||||
if not pygame.font.get_init():
|
||||
pygame.font.init()
|
||||
font_size = max(11, int(size * (0.34 if len(label) <= 2 else 0.26)))
|
||||
font = pygame.font.SysFont('arial', font_size, bold=True)
|
||||
surface = font.render(label, True, color)
|
||||
return _safe_convert_alpha(surface)
|
||||
except Exception as exc:
|
||||
logger.debug(f"Badge text render failed for {label}: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def _build_1fichier_badge_icon(size: int):
|
||||
if pygame is None:
|
||||
return None
|
||||
try:
|
||||
if not pygame.font.get_init():
|
||||
pygame.font.init()
|
||||
|
||||
width = max(size, int(size * 0.98))
|
||||
height = max(16, int(size * 0.90))
|
||||
surface = pygame.Surface((width, height), pygame.SRCALPHA)
|
||||
|
||||
box_width = max(16, int(width * 0.88))
|
||||
box_height = max(14, int(height * 0.78))
|
||||
box_left = (width - box_width) // 2
|
||||
box_top = (height - box_height) // 2
|
||||
|
||||
front = [
|
||||
(box_left + box_width * 0.10, box_top + box_height * 0.24),
|
||||
(box_left + box_width * 0.74, box_top + box_height * 0.24),
|
||||
(box_left + box_width * 0.74, box_top + box_height * 0.86),
|
||||
(box_left + box_width * 0.10, box_top + box_height * 0.86),
|
||||
]
|
||||
right = [
|
||||
(box_left + box_width * 0.74, box_top + box_height * 0.24),
|
||||
(box_left + box_width * 0.92, box_top + box_height * 0.12),
|
||||
(box_left + box_width * 0.92, box_top + box_height * 0.70),
|
||||
(box_left + box_width * 0.74, box_top + box_height * 0.86),
|
||||
]
|
||||
top = [
|
||||
(box_left + box_width * 0.10, box_top + box_height * 0.24),
|
||||
(box_left + box_width * 0.30, box_top + box_height * 0.04),
|
||||
(box_left + box_width * 0.92, box_top + box_height * 0.12),
|
||||
(box_left + box_width * 0.74, box_top + box_height * 0.24),
|
||||
]
|
||||
|
||||
pygame.draw.polygon(surface, (229, 133, 20), [(int(x), int(y)) for x, y in front])
|
||||
pygame.draw.polygon(surface, (191, 104, 8), [(int(x), int(y)) for x, y in right])
|
||||
pygame.draw.polygon(surface, (248, 177, 63), [(int(x), int(y)) for x, y in top])
|
||||
pygame.draw.lines(surface, (111, 61, 6), True, [(int(x), int(y)) for x, y in front], 1)
|
||||
pygame.draw.lines(surface, (111, 61, 6), True, [(int(x), int(y)) for x, y in right], 1)
|
||||
pygame.draw.lines(surface, (111, 61, 6), True, [(int(x), int(y)) for x, y in top], 1)
|
||||
|
||||
accent_radius = max(1, int(box_height * 0.08))
|
||||
pygame.draw.circle(surface, (48, 48, 48), (int(box_left + box_width * 0.88), int(box_top + box_height * 0.08)), accent_radius)
|
||||
pygame.draw.circle(surface, (48, 48, 48), (int(box_left + box_width * 0.95), int(box_top + box_height * 0.22)), accent_radius)
|
||||
|
||||
one_font = pygame.font.SysFont('arial', max(12, int(box_height * 0.60)), bold=True)
|
||||
f_font = pygame.font.SysFont('arial', max(12, int(box_height * 0.64)), bold=True)
|
||||
one_surface = one_font.render('1', True, (92, 92, 92))
|
||||
f_outline_surface = f_font.render('F', True, (32, 32, 32))
|
||||
f_surface = f_font.render('F', True, (236, 129, 18))
|
||||
|
||||
total_text_width = one_surface.get_width() + f_surface.get_width() - max(1, int(box_height * 0.06))
|
||||
text_x = int(box_left + box_width * 0.16)
|
||||
max_text_x = box_left + box_width - total_text_width - max(2, int(box_width * 0.08))
|
||||
text_x = min(text_x, max_text_x)
|
||||
text_y = int(box_top + (box_height - max(one_surface.get_height(), f_surface.get_height())) / 2)
|
||||
surface.blit(one_surface, (text_x, text_y))
|
||||
f_x = text_x + one_surface.get_width() - max(1, int(box_height * 0.06))
|
||||
f_y = text_y - 1
|
||||
for offset_x, offset_y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
surface.blit(f_outline_surface, (f_x + offset_x, f_y + offset_y))
|
||||
surface.blit(f_surface, (f_x, f_y))
|
||||
return _safe_convert_alpha(surface)
|
||||
except Exception as exc:
|
||||
logger.debug(f"1Fichier badge render failed: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def get_platform_source_badge_surface(source_key: str, badge_size: int):
|
||||
cache_key = (source_key, badge_size)
|
||||
if cache_key in _SOURCE_BADGE_CACHE:
|
||||
return _SOURCE_BADGE_CACHE[cache_key]
|
||||
|
||||
if pygame is None:
|
||||
_SOURCE_BADGE_CACHE[cache_key] = None
|
||||
return None
|
||||
|
||||
style_map = {
|
||||
'Archive': {
|
||||
'svg': 'archive.svg',
|
||||
'bg': (255, 255, 255, 242),
|
||||
'border': (48, 48, 48, 235),
|
||||
'label': 'AR',
|
||||
'text': (35, 35, 35),
|
||||
},
|
||||
'LolRoms': {
|
||||
'svg': 'lolroms.svg',
|
||||
'bg': (255, 255, 255, 242),
|
||||
'border': (0, 255, 255, 230),
|
||||
'label': 'LOL',
|
||||
'text': (61, 19, 110),
|
||||
},
|
||||
'Vimms': {
|
||||
'svg': 'vimms.svg',
|
||||
'bg': (255, 255, 255, 242),
|
||||
'border': (208, 208, 208, 235),
|
||||
'label': 'VL',
|
||||
'text': (24, 77, 176),
|
||||
},
|
||||
'Torrent': {
|
||||
'svg': 'torrent.svg',
|
||||
'bg': (255, 255, 255, 242),
|
||||
'border': (97, 164, 64, 235),
|
||||
'label': 'TOR',
|
||||
'text': (37, 90, 24),
|
||||
},
|
||||
'1Fichier': {
|
||||
'svg': None,
|
||||
'bg': (255, 255, 255, 242),
|
||||
'border': (208, 208, 208, 235),
|
||||
'label': '1F',
|
||||
'text': (24, 77, 176),
|
||||
},
|
||||
'EdgeEmu': {
|
||||
'svg': 'edgeemu.svg',
|
||||
'bg': (255, 255, 255, 242),
|
||||
'border': (41, 126, 196, 235),
|
||||
'label': 'EMU',
|
||||
'text': (24, 77, 176),
|
||||
'icon_scale': 0.90,
|
||||
},
|
||||
}
|
||||
style = style_map.get(source_key)
|
||||
if not style:
|
||||
_SOURCE_BADGE_CACHE[cache_key] = None
|
||||
return None
|
||||
|
||||
badge_surface = pygame.Surface((badge_size, badge_size), pygame.SRCALPHA)
|
||||
border_radius = max(10, badge_size // 4)
|
||||
pygame.draw.rect(badge_surface, style['bg'], (0, 0, badge_size, badge_size), border_radius=border_radius)
|
||||
|
||||
inner_margin = max(3, badge_size // 14)
|
||||
inner_rect = pygame.Rect(inner_margin, inner_margin, badge_size - inner_margin * 2, badge_size - inner_margin * 2)
|
||||
pygame.draw.rect(badge_surface, (255, 255, 255, 50), inner_rect, border_radius=max(8, border_radius - 3))
|
||||
|
||||
highlight_height = max(8, badge_size // 3)
|
||||
highlight = pygame.Surface((badge_size - 8, highlight_height), pygame.SRCALPHA)
|
||||
highlight.fill((255, 255, 255, 34))
|
||||
badge_surface.blit(highlight, (4, 4))
|
||||
pygame.draw.rect(badge_surface, style['border'], (0, 0, badge_size, badge_size), max(2, badge_size // 18), border_radius=border_radius)
|
||||
|
||||
icon_surface = None
|
||||
svg_name = style.get('svg')
|
||||
icon_scale = float(style.get('icon_scale', 0.76))
|
||||
target_icon_size = max(22, int(badge_size * icon_scale))
|
||||
if svg_name:
|
||||
svg_path = os.path.join(config.APP_FOLDER, 'assets', 'images', svg_name)
|
||||
if os.path.exists(svg_path):
|
||||
icon_surface = _load_svg_badge_surface(svg_path, target_icon_size)
|
||||
elif source_key == '1Fichier':
|
||||
icon_surface = _build_1fichier_badge_icon(target_icon_size)
|
||||
if icon_surface is None:
|
||||
icon_surface = _build_text_badge_icon(style['label'], badge_size, style['text'])
|
||||
|
||||
if icon_surface is not None:
|
||||
try:
|
||||
icon_shadow = pygame.mask.from_surface(icon_surface).to_surface(
|
||||
setcolor=(0, 0, 0, 90),
|
||||
unsetcolor=(0, 0, 0, 0),
|
||||
)
|
||||
icon_shadow.set_colorkey((0, 0, 0))
|
||||
shadow_rect = icon_shadow.get_rect(center=(badge_size // 2 + 2, badge_size // 2 + 2))
|
||||
badge_surface.blit(icon_shadow, shadow_rect)
|
||||
except Exception:
|
||||
pass
|
||||
icon_rect = icon_surface.get_rect(center=(badge_size // 2, badge_size // 2))
|
||||
badge_surface.blit(icon_surface, icon_rect)
|
||||
|
||||
_SOURCE_BADGE_CACHE[cache_key] = badge_surface
|
||||
return badge_surface
|
||||
|
||||
|
||||
def _apply_platform_source_badge(base_surface, platform_name: str):
|
||||
if pygame is None or base_surface is None:
|
||||
return base_surface
|
||||
source_key = get_platform_source_badge_key(platform_name)
|
||||
if not source_key:
|
||||
return base_surface
|
||||
|
||||
composed = _safe_convert_alpha(base_surface)
|
||||
width, height = composed.get_size()
|
||||
badge_size = max(34, min(int(min(width, height) * 0.34), 180))
|
||||
badge_surface = get_platform_source_badge_surface(source_key, badge_size)
|
||||
if badge_surface is None:
|
||||
return composed
|
||||
|
||||
margin = max(5, badge_size // 10)
|
||||
shadow = pygame.Surface((badge_size + 16, badge_size + 16), pygame.SRCALPHA)
|
||||
pygame.draw.rect(shadow, (0, 0, 0, 115), (8, 8, badge_size, badge_size), border_radius=max(10, badge_size // 4))
|
||||
shadow_x = max(0, width - badge_size - margin - 8)
|
||||
shadow_y = margin - 3
|
||||
composed.blit(shadow, (shadow_x, shadow_y))
|
||||
|
||||
badge_x = max(0, width - badge_size - margin)
|
||||
badge_y = margin
|
||||
composed.blit(badge_surface, (badge_x, badge_y))
|
||||
return composed
|
||||
|
||||
|
||||
def _surface_to_png_bytes(surface):
|
||||
if pygame is None or surface is None:
|
||||
return None
|
||||
try:
|
||||
raw_bytes = pygame.image.tobytes(surface, 'RGBA')
|
||||
if Image is not None:
|
||||
image = Image.frombytes('RGBA', surface.get_size(), raw_bytes)
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format='PNG')
|
||||
return buffer.getvalue()
|
||||
except Exception as exc:
|
||||
logger.debug(f"Surface to PNG conversion failed: {exc}")
|
||||
|
||||
tmp_path = None
|
||||
try:
|
||||
fd, tmp_path = tempfile.mkstemp(suffix='.png')
|
||||
os.close(fd)
|
||||
pygame.image.save(surface, tmp_path)
|
||||
with open(tmp_path, 'rb') as handle:
|
||||
return handle.read()
|
||||
except Exception as exc:
|
||||
logger.debug(f"Surface temp export failed: {exc}")
|
||||
return None
|
||||
finally:
|
||||
if tmp_path and os.path.exists(tmp_path):
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def get_platform_image_payload(platform_dict):
|
||||
image_path = _resolve_platform_image_path(platform_dict)
|
||||
if not image_path:
|
||||
return None, None
|
||||
|
||||
platform_name = platform_dict.get('platform_name', 'unknown')
|
||||
try:
|
||||
if pygame is not None and get_platform_source_badge_key(platform_name):
|
||||
surface = _safe_convert_alpha(pygame.image.load(image_path))
|
||||
surface = _apply_platform_source_badge(surface, platform_name)
|
||||
png_bytes = _surface_to_png_bytes(surface)
|
||||
if png_bytes:
|
||||
return png_bytes, 'image/png'
|
||||
except Exception as exc:
|
||||
logger.debug(f"Badged image payload failed for {platform_name}: {exc}")
|
||||
|
||||
ext = os.path.splitext(image_path)[1].lower()
|
||||
mime_types = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
}
|
||||
try:
|
||||
with open(image_path, 'rb') as handle:
|
||||
return handle.read(), mime_types.get(ext, 'image/png')
|
||||
except Exception as exc:
|
||||
logger.error(f"Unable to read platform image payload {image_path}: {exc}")
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_game_size_to_bytes(value) -> int:
|
||||
if isinstance(value, (int, float)):
|
||||
@@ -41,10 +422,21 @@ def parse_game_size_to_bytes(value) -> int:
|
||||
if not isinstance(value, str):
|
||||
return 0
|
||||
|
||||
text = value.strip().replace(',', '.')
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return 0
|
||||
|
||||
if ',' in text and '.' in text:
|
||||
text = text.replace(',', '')
|
||||
elif ',' in text:
|
||||
numeric_part_match = re.match(r'^([0-9][0-9,]*)', text)
|
||||
numeric_part = numeric_part_match.group(1) if numeric_part_match else ''
|
||||
comma_groups = numeric_part.split(',')
|
||||
if len(comma_groups) > 1 and all(group.isdigit() for group in comma_groups) and all(len(group) == 3 for group in comma_groups[1:]):
|
||||
text = text.replace(',', '')
|
||||
else:
|
||||
text = text.replace(',', '.')
|
||||
|
||||
match = re.match(r'^([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z]+)?$', text)
|
||||
if not match:
|
||||
return 0
|
||||
@@ -1425,7 +1817,7 @@ CONNECTION_STATUS_TTL_SECONDS = 120
|
||||
|
||||
def get_connection_status_targets():
|
||||
"""Retourne la liste des sites à vérifier pour le status de connexion."""
|
||||
return [
|
||||
default_targets = [
|
||||
{
|
||||
"key": "retrogamesets",
|
||||
"label": "Retrogamesets.fr",
|
||||
@@ -1458,6 +1850,37 @@ def get_connection_status_targets():
|
||||
},
|
||||
]
|
||||
|
||||
configured = getattr(config, "CONNECTION_STATUS_TARGETS", None)
|
||||
if not isinstance(configured, list):
|
||||
return default_targets
|
||||
|
||||
normalized = []
|
||||
seen_keys = set()
|
||||
for index, item in enumerate(configured):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
raw_key = str(item.get("key", "")).strip().lower()
|
||||
key = raw_key if raw_key else f"target_{index + 1}"
|
||||
if key in seen_keys:
|
||||
continue
|
||||
|
||||
url = str(item.get("url", "")).strip()
|
||||
if not url:
|
||||
continue
|
||||
|
||||
label = str(item.get("label", "")).strip() or url
|
||||
category = str(item.get("category", "sources")).strip().lower() or "sources"
|
||||
|
||||
normalized.append({
|
||||
"key": key,
|
||||
"label": label,
|
||||
"url": url,
|
||||
"category": category,
|
||||
})
|
||||
seen_keys.add(key)
|
||||
|
||||
return normalized if normalized else default_targets
|
||||
|
||||
|
||||
def _check_url_connectivity(url: str, timeout: int = 6) -> bool:
|
||||
"""Teste rapidement la connectivité à une URL (DNS + HTTPS)."""
|
||||
@@ -2245,48 +2668,13 @@ def load_system_image(platform_dict):
|
||||
Cela évite d'échouer lorsque le nom affiché ne correspond pas au fichier image
|
||||
et respecte un mapping explicite fourni par systems_list.json."""
|
||||
platform_name = platform_dict.get("platform_name", "unknown")
|
||||
folder_name = platform_dict.get("folder") or ""
|
||||
|
||||
# Dossiers d'images
|
||||
save_images = config.IMAGES_FOLDER
|
||||
app_images = os.path.join(config.APP_FOLDER, "images")
|
||||
|
||||
# Candidats, par ordre de priorité
|
||||
candidates = []
|
||||
platform_image_field = (platform_dict.get("platform_image") or "").strip()
|
||||
if platform_image_field:
|
||||
candidates.append(os.path.join(save_images, platform_image_field))
|
||||
candidates.append(os.path.join(save_images, f"{platform_name}.png"))
|
||||
if folder_name:
|
||||
candidates.append(os.path.join(save_images, f"{folder_name}.png"))
|
||||
|
||||
# Fallback: images packagées avec l'app
|
||||
if platform_image_field:
|
||||
candidates.append(os.path.join(app_images, platform_image_field))
|
||||
candidates.append(os.path.join(app_images, f"{platform_name}.png"))
|
||||
if folder_name:
|
||||
candidates.append(os.path.join(app_images, f"{folder_name}.png"))
|
||||
|
||||
# Charger le premier fichier existant
|
||||
try:
|
||||
for path in candidates:
|
||||
if path and os.path.exists(path):
|
||||
return pygame.image.load(path).convert_alpha()
|
||||
image_path = _resolve_platform_image_path(platform_dict)
|
||||
if not image_path:
|
||||
logger.error(f"Aucune image trouvée pour {platform_name}.")
|
||||
return None
|
||||
|
||||
# default.png (save d'abord, sinon app)
|
||||
default_save = os.path.join(save_images, "default.png")
|
||||
if os.path.exists(default_save):
|
||||
return pygame.image.load(default_save).convert_alpha()
|
||||
default_app = os.path.join(app_images, "default.png")
|
||||
if os.path.exists(default_app):
|
||||
return pygame.image.load(default_app).convert_alpha()
|
||||
|
||||
logger.error(
|
||||
f"Aucune image trouvée pour {platform_name}. Candidats: "
|
||||
+ ", ".join(candidates)
|
||||
+ f"; default cherchés: {default_save}, {default_app}"
|
||||
)
|
||||
return None
|
||||
return _safe_convert_alpha(pygame.image.load(image_path))
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
|
||||
return None
|
||||
@@ -2851,7 +3239,28 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
# Chercher le fichier .iso déjà extrait
|
||||
iso_files = [f for f in os.listdir(dest_dir) if f.endswith('.iso') and not f.endswith('_decrypted.iso')]
|
||||
if not iso_files:
|
||||
return False, "Aucun fichier .iso trouvé après extraction"
|
||||
# Vérifier si le jeu est déjà extrait dans un dossier
|
||||
game_folders = []
|
||||
for f in os.listdir(dest_dir):
|
||||
f_path = os.path.join(dest_dir, f)
|
||||
if os.path.isdir(f_path):
|
||||
if f.endswith('.ps3'):
|
||||
game_folders.append(f)
|
||||
elif os.path.exists(os.path.join(f_path, 'PS3_GAME')):
|
||||
game_folders.append(f)
|
||||
if game_folders:
|
||||
game_folder = game_folders[0]
|
||||
game_folder_path = os.path.join(dest_dir, game_folder)
|
||||
if not game_folder.endswith('.ps3'):
|
||||
new_name = game_folder + '.ps3'
|
||||
new_path = os.path.join(dest_dir, new_name)
|
||||
os.rename(game_folder_path, new_path)
|
||||
logger.info(f"Dossier renommé: {game_folder} -> {new_name}")
|
||||
game_folder = new_name
|
||||
logger.info(f"Dossier de jeu PS3 déjà présent: {game_folder}")
|
||||
return True, f"Jeu PS3 déjà extrait dans {game_folder}"
|
||||
else:
|
||||
return False, "Aucun fichier .iso trouvé après extraction"
|
||||
|
||||
iso_file = iso_files[0]
|
||||
iso_path = os.path.join(dest_dir, iso_file)
|
||||
@@ -2944,7 +3353,7 @@ def handle_ps3(dest_dir, new_dirs=None, extracted_basename=None, url=None, archi
|
||||
|
||||
cmd = [
|
||||
"powershell", "-Command",
|
||||
f'$key = (Get-Content "{dkey_escaped}" -Raw).Trim(); ' +
|
||||
f'$key = [System.IO.File]::ReadAllText("{dkey_escaped}").Trim(); ' +
|
||||
f'& "{ps3dec_escaped}" d key $key "{iso_escaped}" "{decrypted_escaped}"'
|
||||
]
|
||||
else: # Linux
|
||||
@@ -3874,17 +4283,32 @@ def find_matching_files(base_path, filename):
|
||||
if not base_path or not os.path.exists(base_path):
|
||||
return []
|
||||
|
||||
candidate_name = Path(str(filename or "")).name
|
||||
requested_stem, requested_ext = os.path.splitext(candidate_name)
|
||||
requested_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', requested_stem)).strip().lower()
|
||||
raw_filename = str(filename or "")
|
||||
candidate_names = []
|
||||
for candidate in (Path(raw_filename).name, sanitize_filename(raw_filename)):
|
||||
if candidate and candidate not in candidate_names:
|
||||
candidate_names.append(candidate)
|
||||
|
||||
if not candidate_names:
|
||||
return []
|
||||
|
||||
requested_variants = []
|
||||
for candidate_name in candidate_names:
|
||||
requested_stem, requested_ext = os.path.splitext(candidate_name)
|
||||
requested_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', requested_stem)).strip().lower()
|
||||
requested_variants.append((candidate_name, requested_stem, requested_ext, requested_normalized))
|
||||
|
||||
archive_exts = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
|
||||
matches = []
|
||||
seen_paths = set()
|
||||
|
||||
full_path = os.path.join(base_path, candidate_name)
|
||||
if os.path.exists(full_path) and os.path.isfile(full_path):
|
||||
seen_paths.add(os.path.normcase(full_path))
|
||||
matches.append((1000, candidate_name, full_path))
|
||||
for candidate_name, _, _, _ in requested_variants:
|
||||
full_path = os.path.join(base_path, candidate_name)
|
||||
if os.path.exists(full_path) and os.path.isfile(full_path):
|
||||
normalized_path = os.path.normcase(full_path)
|
||||
if normalized_path not in seen_paths:
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((1000, candidate_name, full_path))
|
||||
|
||||
for existing_file in os.listdir(base_path):
|
||||
existing_path = os.path.join(base_path, existing_file)
|
||||
@@ -3898,17 +4322,21 @@ def find_matching_files(base_path, filename):
|
||||
existing_stem, existing_ext = os.path.splitext(existing_file)
|
||||
score = None
|
||||
|
||||
if requested_stem and existing_stem == requested_stem:
|
||||
score = 900
|
||||
else:
|
||||
existing_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', existing_stem)).strip().lower()
|
||||
if requested_normalized and existing_normalized and existing_normalized == requested_normalized:
|
||||
score = 0
|
||||
existing_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', existing_stem)).strip().lower()
|
||||
for _, requested_stem, requested_ext, requested_normalized in requested_variants:
|
||||
candidate_score = None
|
||||
if requested_stem and existing_stem == requested_stem:
|
||||
candidate_score = 900
|
||||
elif requested_normalized and existing_normalized and existing_normalized == requested_normalized:
|
||||
candidate_score = 0
|
||||
if requested_ext and existing_ext.lower() == requested_ext.lower():
|
||||
score += 4
|
||||
candidate_score += 4
|
||||
if existing_ext.lower() not in archive_exts:
|
||||
score += 3
|
||||
score -= abs(len(existing_stem) - len(requested_stem))
|
||||
candidate_score += 3
|
||||
candidate_score -= abs(len(existing_stem) - len(requested_stem))
|
||||
|
||||
if candidate_score is not None:
|
||||
score = candidate_score if score is None else max(score, candidate_score)
|
||||
|
||||
if score is not None:
|
||||
seen_paths.add(normalized_path)
|
||||
@@ -3924,24 +4352,115 @@ def get_existing_history_matches(entry):
|
||||
return []
|
||||
|
||||
moved_paths = entry.get("moved_paths", []) or []
|
||||
local_path = entry.get("local_path")
|
||||
local_filename = entry.get("local_filename")
|
||||
game_name = entry.get("game_name", "")
|
||||
if local_path:
|
||||
moved_paths = [local_path, *moved_paths]
|
||||
|
||||
direct_matches = []
|
||||
direct_candidates = []
|
||||
if local_path:
|
||||
direct_candidates.append(str(local_path))
|
||||
|
||||
platform_name = (entry.get("platform") or "").strip()
|
||||
base_path = None
|
||||
if platform_name:
|
||||
try:
|
||||
base_path = os.path.join(config.ROMS_FOLDER, _get_dest_folder_name(platform_name))
|
||||
except Exception:
|
||||
base_path = None
|
||||
|
||||
if local_filename and base_path:
|
||||
direct_candidates.append(os.path.join(base_path, str(local_filename)))
|
||||
|
||||
seen_direct = set()
|
||||
for candidate in direct_candidates:
|
||||
actual_path = os.path.abspath(str(candidate))
|
||||
normalized_path = os.path.normcase(actual_path)
|
||||
if normalized_path in seen_direct:
|
||||
continue
|
||||
seen_direct.add(normalized_path)
|
||||
exists = os.path.isfile(actual_path)
|
||||
if exists:
|
||||
direct_matches.append((os.path.basename(actual_path), actual_path))
|
||||
|
||||
if direct_matches:
|
||||
return direct_matches
|
||||
|
||||
candidate_paths = []
|
||||
for raw_path in moved_paths:
|
||||
if raw_path:
|
||||
candidate_paths.append(str(raw_path))
|
||||
if local_filename and base_path:
|
||||
candidate_paths.insert(0, os.path.join(base_path, str(local_filename)))
|
||||
|
||||
matches = []
|
||||
seen_paths = set()
|
||||
|
||||
for raw_path in moved_paths:
|
||||
for raw_path in candidate_paths:
|
||||
if not raw_path:
|
||||
continue
|
||||
|
||||
actual_path = os.path.abspath(str(raw_path))
|
||||
normalized_path = os.path.normcase(actual_path)
|
||||
if normalized_path in seen_paths or not os.path.isfile(actual_path):
|
||||
continue
|
||||
raw_path = str(raw_path)
|
||||
fallback_paths = [os.path.abspath(raw_path)]
|
||||
if base_path:
|
||||
fallback_paths.append(os.path.join(base_path, os.path.basename(raw_path)))
|
||||
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((os.path.basename(actual_path), actual_path))
|
||||
for actual_path in fallback_paths:
|
||||
normalized_path = os.path.normcase(actual_path)
|
||||
exists = os.path.isfile(actual_path)
|
||||
if normalized_path in seen_paths or not exists:
|
||||
continue
|
||||
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((os.path.basename(actual_path), actual_path))
|
||||
break
|
||||
|
||||
if not matches:
|
||||
logger.debug(
|
||||
"[HISTORY_MATCH_LOOKUP] no_match game=%s platform=%s",
|
||||
game_name,
|
||||
platform_name,
|
||||
)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def remember_history_local_match(entry, actual_filename, actual_path):
|
||||
"""Persist a resolved local path for a history entry so later lookups are exact."""
|
||||
if not isinstance(entry, dict) or not actual_path:
|
||||
return False
|
||||
|
||||
absolute_path = os.path.abspath(str(actual_path))
|
||||
filename = actual_filename or os.path.basename(absolute_path)
|
||||
changed = False
|
||||
|
||||
if entry.get("local_path") != absolute_path:
|
||||
entry["local_path"] = absolute_path
|
||||
changed = True
|
||||
if entry.get("local_filename") != filename:
|
||||
entry["local_filename"] = filename
|
||||
changed = True
|
||||
|
||||
moved_paths = entry.get("moved_paths")
|
||||
if not isinstance(moved_paths, list):
|
||||
moved_paths = []
|
||||
if absolute_path not in moved_paths:
|
||||
moved_paths.insert(0, absolute_path)
|
||||
changed = True
|
||||
entry["moved_paths"] = moved_paths
|
||||
|
||||
if changed:
|
||||
try:
|
||||
from history import save_history
|
||||
|
||||
save_history(config.history)
|
||||
except Exception as e:
|
||||
logger.debug(f"Impossible de mémoriser le chemin local de l'historique: {e}")
|
||||
return changed
|
||||
|
||||
|
||||
def move_files_to_directory(file_paths, destination_dir):
|
||||
"""Move files to a destination directory, avoiding name collisions."""
|
||||
if not destination_dir:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.3.1"
|
||||
"version": "2.6.4.1"
|
||||
}
|
||||
@@ -357,8 +357,12 @@ if "!EXITCODE!"=="0" (
|
||||
echo.
|
||||
echo %ESC%%GREEN%RGSX closed successfully.%ESC%%RESET%
|
||||
echo.
|
||||
echo [%DATE% %TIME%] Application closed successfully >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo [%DATE% %TIME%] Application closed successfully >> "%LOG_FILE%") else if "!EXITCODE!"=="1" (
|
||||
echo.
|
||||
echo %ESC%%GREEN%RGSX closed normally.%ESC%%RESET%
|
||||
echo.
|
||||
>> "%LOG_FILE%" echo [%DATE% %TIME%] Application closed normally >> "%LOG_FILE%"
|
||||
goto :end) else (
|
||||
echo.
|
||||
echo %ESC%%RED%RGSX exited with error code !EXITCODE!%ESC%%RESET%
|
||||
echo.
|
||||
@@ -370,7 +374,7 @@ if "!EXITCODE!"=="0" (
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] Session ended normally >> "%LOG_FILE%"
|
||||
echo [%DATE% %TIME%] ========================================== >> "%LOG_FILE%"
|
||||
timeout /t 2 >nul
|
||||
ping -n 1 -w 5000 127.255.255.255 >nul
|
||||
exit /b 0
|
||||
|
||||
:error
|
||||
|
||||
Reference in New Issue
Block a user