Compare commits

...

2 Commits

Author SHA1 Message Date
skymike03
e9a610b5dd v2.3.3.3
- Enhance download queue functionality to stop download, continue queue, remove games  and update related UI options
2025-11-25 19:21:12 +01:00
skymike03
bd3b885736 v2.3.3.2
- Fix French print statements for consistency in output messages
- improve filtering  in game list to be permanent, even in search mode, and more efficient for games that have foreign language on other region
2025-11-25 18:40:13 +01:00
15 changed files with 335 additions and 87 deletions

View File

@@ -28,7 +28,7 @@ from display import (
draw_toast, show_toast, THEME_COLORS
)
from language import _
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads, download_queue_worker
from controls import handle_controls, validate_menu_state, process_key_repeats, get_emergency_controls
from controls_mapper import map_controls, draw_controls_mapping, get_actions
from controls import load_controls_config
@@ -213,7 +213,7 @@ except Exception:
normalized_names = [n.lower() for n in joystick_names]
if not joystick_names:
joystick_names = ["Clavier"]
print("Aucun joystick détecté, utilisation du clavier par défaut")
print("Aucun joystick detecte, utilisation du clavier par defaut")
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
config.joystick = False
config.keyboard = True
@@ -438,6 +438,11 @@ async def main():
# Démarrer le serveur web en arrière-plan
start_web_server()
# Démarrer le worker de la queue de téléchargement
queue_worker_thread = threading.Thread(target=download_queue_worker, daemon=True)
queue_worker_thread.start()
logger.info("Worker de la queue de téléchargement démarré")
running = True
loading_step = "none"
sources = []
@@ -473,7 +478,7 @@ async def main():
config.needs_redraw = True
last_redraw_time = current_time
# Forcer redraw toutes les 100 ms dans history avec téléchargement actif
if config.menu_state == "history" and any(entry["status"] == "Téléchargement" for entry in config.history):
if config.menu_state == "history" and any(entry["status"] in ["Downloading", "Téléchargement"] for entry in config.history):
if current_time - last_redraw_time >= 100:
config.needs_redraw = True
last_redraw_time = current_time

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.3.3.1"
app_version = "2.3.3.3"
def get_application_root():
@@ -133,6 +133,7 @@ logger = logging.getLogger(__name__)
# File d'attente de téléchargements (jobs en attente)
download_queue = [] # Liste de dicts: {url, platform, game_name, ...}
pending_download_is_queue = False # Indique si pending_download doit être ajouté à la queue
# Indique si un téléchargement est en cours
download_active = False

View File

@@ -159,7 +159,7 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
dev_field = preset.get('device') if isinstance(preset, dict) else None
if isinstance(dev_field, str) and _sanitize(dev_field) == target_norm:
logging.getLogger(__name__).info(f"Chargement préréglage (device) depuis le fichier: {fname}")
print(f"Chargement préréglage (device) depuis le fichier: {fname}")
print(f"Chargement prereglage (device) depuis le fichier: {fname}")
return preset
except Exception as e:
logging.getLogger(__name__).warning(f"Échec scan préréglages par device: {e}")
@@ -559,7 +559,11 @@ def handle_controls(event, sources, joystick, screen):
config.needs_redraw = True
elif is_input_matched(event, "confirm"):
config.search_query += keyboard_layout[row][col]
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -567,14 +571,22 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "delete"):
if config.search_query:
config.search_query = config.search_query[:-1]
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
#logger.debug(f"Suppression caractère: query={config.search_query}, jeux filtrés={len(config.filtered_games)}")
elif is_input_matched(event, "space"):
config.search_query += " "
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -583,7 +595,13 @@ def handle_controls(event, sources, joystick, screen):
config.search_mode = False
config.search_query = ""
config.selected_key = (0, 0)
config.filtered_games = config.games
# Restaurer les jeux filtrés par les filtres avancés si actifs
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -605,8 +623,13 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "cancel"):
config.search_mode = False
config.search_query = ""
config.filtered_games = config.games
config.filter_active = False
# Restaurer les jeux filtrés par les filtres avancés si actifs
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
config.filter_active = True
else:
config.filtered_games = config.games
config.filter_active = False
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -615,7 +638,11 @@ def handle_controls(event, sources, joystick, screen):
# Saisie de texte alphanumérique
if event.unicode.isalnum() or event.unicode == ' ':
config.search_query += event.unicode
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -624,7 +651,11 @@ def handle_controls(event, sources, joystick, screen):
elif is_input_matched(event, "delete"):
if config.search_query:
config.search_query = config.search_query[:-1]
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()]
# Appliquer d'abord les filtres avancés si actifs, puis le filtre par nom
base_games = config.games
if config.game_filter_obj and config.game_filter_obj.is_active():
base_games = config.game_filter_obj.apply_filters(config.games)
config.filtered_games = [game for game in base_games if config.search_query.lower() in game[0].lower()]
config.current_game = 0
config.scroll_offset = 0
config.needs_redraw = True
@@ -709,6 +740,7 @@ def handle_controls(event, sources, joystick, screen):
# Si extension non supportée ET pas en archive connu, afficher avertissement
if (not is_supported and not zip_ok) and not allow_unknown:
config.pending_download = pending_download
config.pending_download_is_queue = True # Marquer comme action queue
config.previous_menu_state = config.menu_state
config.menu_state = "extension_warning"
config.extension_confirm_selection = 0
@@ -782,29 +814,69 @@ def handle_controls(event, sources, joystick, screen):
if config.extension_confirm_selection == 0: # 0 = Oui, 1 = Non
if config.pending_download and len(config.pending_download) == 4:
url, platform, game_name, is_zip_non_supported = config.pending_download
if is_1fichier_url(url):
ensure_download_provider_keys(False)
# Avertissement si pas de clé (utilisation mode gratuit)
if missing_all_provider_keys():
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
# Vérifier si c'est une action queue
is_queue_action = getattr(config, 'pending_download_is_queue', False)
if is_queue_action:
# Ajouter à la queue au lieu de télécharger immédiatement
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
queue_item = {
'url': url,
'platform': platform,
'game_name': game_name,
'is_zip_non_supported': is_zip_non_supported,
'is_1fichier': is_1fichier_url(url),
'task_id': task_id,
'status': 'Queued'
}
config.download_queue.append(queue_item)
# Ajouter une entrée à l'historique avec status "Queued"
config.history.append({
'platform': platform,
'game_name': game_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)
# Afficher un toast de notification
show_toast(f"{game_name}\n{_('download_queued')}")
# Le worker de la queue détectera automatiquement le nouvel élément
logger.debug(f"{game_name} ajouté à la file d'attente après confirmation. Queue size: {len(config.download_queue)}")
else:
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
config.download_tasks[task_id] = (task, url, game_name, platform)
# Afficher un toast de notification
show_toast(f"{_('download_started')}: {game_name}")
# Téléchargement immédiat
if is_1fichier_url(url):
ensure_download_provider_keys(False)
# Avertissement si pas de clé (utilisation mode gratuit)
if missing_all_provider_keys():
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
else:
task_id = str(pygame.time.get_ticks())
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
config.download_tasks[task_id] = (task, url, game_name, platform)
# Afficher un toast de notification
show_toast(f"{_('download_started')}: {game_name}")
logger.debug(f"[CONTROLS_EXT_WARNING] Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}, task_id={task_id}")
config.previous_menu_state = validate_menu_state(config.previous_menu_state)
config.needs_redraw = True
logger.debug(f"[CONTROLS_EXT_WARNING] Téléchargement confirmé après avertissement: {game_name} pour {platform} depuis {url}, task_id={task_id}")
config.pending_download = None
config.pending_download_is_queue = False
config.extension_confirm_selection = 0 # Réinitialiser la sélection
action = "download"
# Téléchargement simple - retourner au menu précédent
# Retourner au menu précédent
config.menu_state = config.previous_menu_state if config.previous_menu_state else "game"
logger.debug(f"[CONTROLS_EXT_WARNING] Retour au menu {config.menu_state} après confirmation")
else:
@@ -912,22 +984,35 @@ def handle_controls(event, sources, joystick, screen):
if is_input_matched(event, "confirm"):
if config.confirm_cancel_selection == 1: # Oui
entry = config.history[config.current_history_item]
task_id = entry.get("task_id")
url = entry.get("url")
# Annuler la tâche correspondante
for task_id, (task, task_url, game_name, platform) in list(config.download_tasks.items()):
if task_url == url:
game_name = entry.get("game_name", "Unknown")
# Annuler via cancel_events (pour les threads de téléchargement)
try:
request_cancel(task_id)
logger.debug(f"Signal d'annulation envoyé pour task_id={task_id}")
except Exception as e:
logger.debug(f"Erreur lors de l'envoi du signal d'annulation: {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:
try:
request_cancel(task_id)
except Exception:
pass
task.cancel()
del config.download_tasks[task_id]
entry["status"] = "Canceled"
entry["progress"] = 0
entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history)
logger.debug(f"Téléchargement annulé: {game_name}")
task.cancel()
del config.download_tasks[tid]
logger.debug(f"Tâche asyncio annulée: {tname}")
except Exception as e:
logger.debug(f"Erreur lors de l'annulation de la tâche asyncio: {e}")
break
# Mettre à jour l'entrée historique
entry["status"] = "Canceled"
entry["progress"] = 0
entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history)
logger.debug(f"Téléchargement annulé: {game_name}")
config.menu_state = "history"
config.needs_redraw = True
else: # Non
@@ -1003,7 +1088,13 @@ def handle_controls(event, sources, joystick, screen):
options.append("scraper")
# Options selon statut
if status == "Download_OK" or status == "Completed":
if status == "Queued":
# En attente dans la queue
options.append("remove_from_queue")
elif status in ["Downloading", "Téléchargement", "Extracting"]:
# Téléchargement en cours
options.append("cancel_download")
elif status == "Download_OK" or status == "Completed":
# Vérifier si c'est une archive ET si le fichier existe
if actual_filename and file_exists:
ext = os.path.splitext(actual_filename)[1].lower()
@@ -1046,7 +1137,37 @@ def handle_controls(event, sources, joystick, screen):
selected_option = options[sel]
logger.debug(f"history_game_options: CONFIRM option={selected_option}")
if selected_option == "download_folder":
if selected_option == "remove_from_queue":
# Retirer de la queue
task_id = entry.get("task_id")
url = entry.get("url")
# Chercher et retirer de la queue
for i, queue_item in enumerate(config.download_queue):
if queue_item.get("task_id") == task_id or queue_item.get("url") == url:
config.download_queue.pop(i)
logger.debug(f"Jeu retiré de la queue: {game_name}")
break
# Mettre à jour l'entrée historique avec status Canceled
entry["status"] = "Canceled"
entry["progress"] = 0
entry["message"] = _("download_canceled") if _ else "Download canceled"
save_history(config.history)
# Retour à l'historique
config.menu_state = "history"
config.needs_redraw = True
elif selected_option == "cancel_download":
# Rediriger vers le dialogue de confirmation (même que bouton cancel)
config.previous_menu_state = "history"
config.menu_state = "confirm_cancel_download"
config.confirm_cancel_selection = 0
config.needs_redraw = True
logger.debug("Redirection vers confirm_cancel_download depuis history_game_options")
elif selected_option == "download_folder":
# Afficher le chemin de destination
config.previous_menu_state = "history_game_options"
config.menu_state = "history_show_folder"
@@ -2140,7 +2261,11 @@ def handle_controls(event, sources, joystick, screen):
# Recherche par nom (mode existant)
config.search_mode = True
config.search_query = ""
config.filtered_games = config.games
# Initialiser avec les jeux déjà filtrés par les filtres avancés si actifs
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
config.filtered_games = config.game_filter_obj.apply_filters(config.games)
else:
config.filtered_games = config.games
config.current_game = 0
config.scroll_offset = 0
config.selected_key = (0, 0)

View File

@@ -1042,14 +1042,14 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
screen.blit(title_surface, title_rect)
elif config.filter_active:
# Display filter active indicator with count
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
total_games = len(config.games)
filtered_count = len(games)
filter_text = _("filter_games_shown").format(filtered_count, total_games)
else:
filter_text = _("game_filter").format(config.search_query)
title_surface = config.font.render(filter_text, True, THEME_COLORS["green"])
# Afficher le nom de la plateforme avec indicateur de filtre actif
filter_indicator = " (Active Filter)"
if config.search_query:
# Si recherche par nom active, afficher aussi la recherche
filter_indicator = f" - {_('game_filter').format(config.search_query)}"
title_text = _("game_count").format(platform_name, game_count) + filter_indicator
title_surface = config.title_font.render(title_text, True, THEME_COLORS["green"])
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)
title_rect_inflated.topleft = ((config.screen_width - title_rect_inflated.width) // 2, 10)
@@ -1057,7 +1057,12 @@ def draw_game_list(screen):
pygame.draw.rect(screen, THEME_COLORS["border_selected"], title_rect_inflated, 3, border_radius=12)
screen.blit(title_surface, title_rect)
else:
title_text = _("game_count").format(platform_name, game_count)
# Ajouter indicateur de filtre actif si filtres avancés sont actifs
filter_indicator = ""
if hasattr(config, 'game_filter_obj') and config.game_filter_obj and config.game_filter_obj.is_active():
filter_indicator = " (Active Filter)"
title_text = _("game_count").format(platform_name, game_count) + filter_indicator
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)
@@ -3158,7 +3163,15 @@ def draw_history_game_options(screen):
option_labels.append(_("history_option_scraper"))
# Options selon statut
if status == "Download_OK" or status == "Completed":
if status == "Queued":
# En attente dans la queue
options.append("remove_from_queue")
option_labels.append(_("history_option_remove_from_queue"))
elif status in ["Downloading", "Téléchargement", "Extracting"]:
# Téléchargement en cours
options.append("cancel_download")
option_labels.append(_("history_option_cancel_download"))
elif status == "Download_OK" or status == "Completed":
# Vérifier si c'est une archive ET si le fichier existe
if actual_filename and file_exists:
ext = os.path.splitext(actual_filename)[1].lower()

View File

@@ -69,30 +69,67 @@ class GameFilters:
name = game_name.upper()
regions = []
# Patterns de région communs
if 'USA' in name or 'US)' in name:
regions.append('USA')
# Patterns de région communs - chercher les codes entre parenthèses d'abord
# Codes de région/langue dans les parenthèses (Ex: (Fr,De) ou (En,Nl))
paren_content = re.findall(r'\(([^)]+)\)', name)
for content in paren_content:
# Codes de langue/région séparés par virgules
codes = [c.strip() for c in content.split(',')]
for code in codes:
if code in ['FR', 'FRA']:
if 'France' not in regions:
regions.append('France')
elif code in ['DE', 'GER', 'DEU']:
if 'Germany' not in regions:
regions.append('Germany')
elif code in ['EN', 'ENG'] or code.startswith('EN-'):
# EN peut être USA, Europe ou autre - on vérifie le contexte
if 'EU' in codes or 'EUR' in codes:
if 'Europe' not in regions:
regions.append('Europe')
elif code in ['ES', 'ESP', 'SPA']:
if 'Other' not in regions:
regions.append('Other')
elif code in ['IT', 'ITA']:
if 'Other' not in regions:
regions.append('Other')
elif code in ['NL', 'NLD', 'DU', 'DUT']:
if 'Europe' not in regions:
regions.append('Europe')
elif code in ['PT', 'POR']:
if 'Other' not in regions:
regions.append('Other')
# Patterns de région complets (mots entiers)
if 'USA' in name or 'US)' in name or re.search(r'\bUS\b', name):
if 'USA' not in regions:
regions.append('USA')
if 'CANADA' in name or 'CA)' in name:
regions.append('Canada')
if 'EUROPE' in name or 'EU)' in name:
regions.append('Europe')
if 'Canada' not in regions:
regions.append('Canada')
if 'EUROPE' in name or 'EU)' in name or re.search(r'\bEU\b', name):
if 'Europe' not in regions:
regions.append('Europe')
if 'FRANCE' in name or 'FR)' in name:
regions.append('France')
if 'France' not in regions:
regions.append('France')
if 'GERMANY' in name or 'DE)' in name or 'GER)' in name:
regions.append('Germany')
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name:
regions.append('Japan')
if 'Germany' not in regions:
regions.append('Germany')
if 'JAPAN' in name or 'JP)' in name or 'JPN)' in name or re.search(r'\bJP\b', name):
if 'Japan' not in regions:
regions.append('Japan')
if 'KOREA' in name or 'KR)' in name or 'KOR)' in name:
regions.append('Korea')
if 'Korea' not in regions:
regions.append('Korea')
if 'WORLD' in name:
regions.append('World')
if 'World' not in regions:
regions.append('World')
# Autres régions
if re.search(r'\b(AUSTRALIA|ASIA|KOREA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
r'SPAIN|FRANCE|GERMANY|ITALY|CANADA)\b', name):
if 'CANADA' in name:
regions.append('Canada')
else:
if re.search(r'\b(AUSTRALIA|ASIA|BRAZIL|CHINA|RUSSIA|SCANDINAVIA|'
r'SPAIN|ITALY)\b', name):
if 'Other' not in regions:
regions.append('Other')
# Si aucune région trouvée
@@ -157,14 +194,22 @@ class GameFilters:
def get_region_priority(self, game_name: str) -> int:
"""Obtient la priorité de région pour un jeu (pour one-rom-per-game)"""
name = game_name.upper()
# Utiliser la fonction de détection de régions pour être cohérent
game_regions = self.get_game_regions(game_name)
for i, region in enumerate(self.region_priority):
region_upper = region.upper()
if region_upper in name:
return i
# Trouver la meilleure priorité parmi toutes les régions détectées
best_priority = len(self.region_priority) # Par défaut: priorité la plus basse
return len(self.region_priority) # Autres régions (priorité la plus basse)
for region in game_regions:
try:
priority = self.region_priority.index(region)
if priority < best_priority:
best_priority = priority
except ValueError:
# La région n'est pas dans la liste de priorité
continue
return best_priority
def apply_filters(self, games: List[Tuple]) -> List[Tuple]:
"""

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download vom Benutzer abgebrochen.",
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
"extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?",
"extension_warning_unsupported": "Die Dateierweiterung für '{0}' wird laut der Konfiguration es_systems.cfg von Batocera nicht unterstützt. Möchtest du fortfahren?",
"extension_warning_enable_unknown_hint": "\nUm diese Meldung auszublenden: \"Warnung bei unbekannter Erweiterung ausblenden\" in Pausenmenü > Anzeige aktivieren",
@@ -247,6 +248,8 @@
"history_option_extract_archive": "Archiv extrahieren",
"history_option_open_file": "Datei öffnen",
"history_option_scraper": "Metadaten abrufen",
"history_option_remove_from_queue": "Aus Warteschlange entfernen",
"history_option_cancel_download": "Download abbrechen",
"history_option_delete_game": "Spiel löschen",
"history_option_error_info": "Fehlerdetails",
"history_option_retry": "Download wiederholen",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Free mode] Completed: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download canceled by user.",
"download_removed_from_queue": "Removed from download queue",
"extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?",
"extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the es_systems.cfg configuration. Do you want to continue?",
"extension_warning_enable_unknown_hint": "\nTo hide this message: enable \"Hide unknown extension warning\" in Pause Menu > Display",
@@ -249,6 +250,8 @@
"history_option_extract_archive": "Extract archive",
"history_option_open_file": "Open file",
"history_option_scraper": "Scrape metadata",
"history_option_remove_from_queue": "Remove from queue",
"history_option_cancel_download": "Cancel download",
"history_option_delete_game": "Delete game",
"history_option_error_info": "Error details",
"history_option_retry": "Retry download",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modo gratuito] Completado: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Descarga cancelada por el usuario.",
"download_removed_from_queue": "Eliminado de la cola de descarga",
"extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?",
"extension_warning_unsupported": "La extensión del archivo '{0}' no está soportada por Batocera según la configuración es_systems.cfg. ¿Deseas continuar?",
"extension_warning_enable_unknown_hint": "\nPara no mostrar este mensaje: activa \"Ocultar aviso de extensión desconocida\" en Menú de pausa > Pantalla",
@@ -249,6 +250,8 @@
"history_option_extract_archive": "Extraer archivo",
"history_option_open_file": "Abrir archivo",
"history_option_scraper": "Obtener metadatos",
"history_option_remove_from_queue": "Quitar de la cola",
"history_option_cancel_download": "Cancelar descarga",
"history_option_delete_game": "Eliminar juego",
"history_option_error_info": "Detalles del error",
"history_option_retry": "Reintentar descarga",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
"download_status": "{0} : {1}",
"download_canceled": "Téléchargement annulé par l'utilisateur.",
"download_removed_from_queue": "Retiré de la file de téléchargement",
"extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?",
"extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après la configuration es_systems.cfg. Voulez-vous continuer ?",
"extension_warning_enable_unknown_hint": "\nPour ne plus afficher ce messager : Activer l'option \"Masquer avertissement\" dans le Menu Pause>Display",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download annullato dall'utente.",
"download_removed_from_queue": "Rimosso dalla coda di download",
"extension_warning_zip": "Il file '{0}' è un archivio e Batocera non supporta archivi per questo sistema. L'estrazione automatica avverrà dopo il download, continuare?",
"extension_warning_unsupported": "L'estensione del file '{0}' non è supportata da Batocera secondo la configurazione di es_systems.cfg. Vuoi continuare?",
"extension_warning_enable_unknown_hint": "\nPer non visualizzare questo messaggio: abilita \"Nascondi avviso estensione sconosciuta\" in Menu Pausa > Schermo",

View File

@@ -44,6 +44,7 @@
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
"download_status": "{0}: {1}",
"download_canceled": "Download cancelado pelo usuário.",
"download_removed_from_queue": "Removido da fila de download",
"extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?",
"extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo a configuração es_systems.cfg. Deseja continuar?",
"extension_warning_enable_unknown_hint": "\nPara não ver esta mensagem: ative \"Ocultar aviso de extensão desconhecida\" em Menu de Pausa > Exibição",
@@ -248,6 +249,8 @@
"history_option_extract_archive": "Extrair arquivo",
"history_option_open_file": "Abrir arquivo",
"history_option_scraper": "Obter metadados",
"history_option_remove_from_queue": "Remover da fila",
"history_option_cancel_download": "Cancelar download",
"history_option_delete_game": "Excluir jogo",
"history_option_error_info": "Detalhes do erro",
"history_option_retry": "Tentar novamente",

View File

@@ -925,6 +925,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.info(f"Le fichier {dest_path} existe déjà et la taille est correcte, téléchargement ignoré")
result[0] = True
result[1] = _("network_download_ok").format(game_name) + _("download_already_present")
# Mettre à jour l'historique
for entry in config.history:
if entry.get("url") == url:
entry["status"] = "Download_OK"
entry["progress"] = 100
entry["message"] = result[1]
save_history(config.history)
break
# Afficher un toast au lieu d'ouvrir l'historique
try:
show_toast(result[1])
@@ -933,6 +943,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
with urls_lock:
urls_in_progress.discard(url)
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
file_found = True
@@ -975,6 +992,16 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
logger.info(f"Un fichier avec le même nom de base existe déjà: {existing_path}, téléchargement ignoré")
result[0] = True
result[1] = _("network_download_ok").format(game_name) + _("download_already_extracted")
# Mettre à jour l'historique
for entry in config.history:
if entry.get("url") == url:
entry["status"] = "Download_OK"
entry["progress"] = 100
entry["message"] = result[1]
save_history(config.history)
break
# Afficher un toast au lieu d'ouvrir l'historique
try:
show_toast(result[1])
@@ -983,6 +1010,13 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
with urls_lock:
urls_in_progress.discard(url)
logger.debug(f"URL supprimée du set des téléchargements en cours: {url} (URLs restantes: {len(urls_in_progress)})")
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
except Exception as e:
logger.debug(f"Erreur lors de la vérification des fichiers existants: {e}")
@@ -1209,6 +1243,11 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
# Si annulé, ne pas continuer avec extraction
if download_canceled:
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return
os.chmod(dest_path, 0o644)
@@ -1430,6 +1469,12 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
if url in url_done_events:
url_done_events[url].set()
# Libérer le slot de la queue
try:
notify_download_finished()
except Exception:
pass
return result[0], result[1]
async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=False, task_id=None):

View File

@@ -29,7 +29,7 @@ def delete_old_files():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"Ancien fichier supprimé : {file_path}")
print(f"Ancien fichier supprime : {file_path}")
logger.info(f"Ancien fichier supprimé : {file_path}")
except Exception as e:
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")
@@ -39,7 +39,7 @@ def delete_old_files():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"Ancien fichier supprimé : {file_path}")
print(f"Ancien fichier supprime : {file_path}")
logger.info(f"Ancien fichier supprimé : {file_path}")
except Exception as e:
print(f"Erreur lors de la suppression de {file_path} : {str(e)}")

View File

@@ -364,7 +364,7 @@ try:
logger.info("Test d'écriture dans le fichier de log réussi")
except Exception as e:
logger.error(f"Erreur lors du test d'écriture : {e}")
print(f"ERREUR: Impossible d'écrire dans {config.log_file_web}: {e}", file=sys.stderr)
print(f"ERREUR: Impossible d'ecrire dans {config.log_file_web}: {e}", file=sys.stderr)
# Initialiser les données au démarrage
logger.info("Chargement initial des données...")
@@ -772,7 +772,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
# Lire depuis history.json - filtrer seulement les téléchargements en cours
history = load_history() or []
print(f"\n[DEBUG PROGRESS] history.json chargé avec {len(history)} entrées totales")
print(f"\n[DEBUG PROGRESS] history.json charge avec {len(history)} entrees totales")
# Filtrer les entrées avec status "Downloading", "Téléchargement", "Connecting", "Try X/Y"
in_progress_statuses = ["Downloading", "Téléchargement", "Downloading", "Connecting", "Extracting"]
@@ -797,9 +797,9 @@ class RGSXHandler(BaseHTTPRequestHandler):
else:
# Debug: afficher les premiers status qui ne matchent pas
if len(downloads) < 3:
print(f" [DEBUG] Ignoré - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
print(f" [DEBUG] Ignore - Status: '{status}', Game: {entry.get('game_name', '')[:50]}")
print(f"[DEBUG PROGRESS] {len(downloads)} téléchargements en cours trouvés")
print(f"[DEBUG PROGRESS] {len(downloads)} telechargements en cours trouves")
if downloads:
for url, data in list(downloads.items())[:2]:
print(f" - URL: {url[:80]}...")
@@ -2088,7 +2088,7 @@ def run_server(host='0.0.0.0', port=5000):
if __name__ == '__main__':
print("="*60, flush=True)
print("Demarrage du serveur RGSX Web...", flush=True)
print(f"Fichier de log prévu: {config.log_file_web}", flush=True)
print(f"Fichier de log prevu: {config.log_file_web}", flush=True)
print("="*60, flush=True)
parser = argparse.ArgumentParser(description='RGSX Web Server')

View File

@@ -1,3 +1,3 @@
{
"version": "2.3.3.1"
"version": "2.3.3.3"
}