- Enhance download queue functionality to stop download, continue queue, remove games  and update related UI options
This commit is contained in:
skymike03
2025-11-25 19:21:12 +01:00
parent bd3b885736
commit e9a610b5dd
12 changed files with 200 additions and 37 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
@@ -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.2"
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

@@ -740,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
@@ -813,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:
@@ -943,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
@@ -1034,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()
@@ -1077,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"

View File

@@ -3163,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

@@ -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

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