import requests import subprocess import re import os import threading import pygame import zipfile import json from urllib.parse import urljoin, unquote import asyncio import config from utils import sanitize_filename import logging logger = logging.getLogger(__name__) JSON_EXTENSIONS = "/userdata/roms/ports/RGSX/rom_extensions.json" def test_internet(): logger.debug("Test de connexion Internet") try: result = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5) if result.returncode == 0: logger.debug("Connexion Internet OK") return True else: logger.debug("Échec ping 8.8.8.8") return False except Exception as e: logger.debug(f"Erreur test Internet: {str(e)}") return False def load_extensions_json(): """Charge le fichier JSON contenant les extensions supportées.""" try: with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}") return [] def is_extension_supported(filename, platform, extensions_data): """Vérifie si l'extension du fichier est supportée pour la plateforme donnée.""" extension = os.path.splitext(filename)[1].lower() dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: dest_dir = platform_dict.get("folder") break if not dest_dir: logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}") dest_dir = os.path.join("/userdata/roms", platform) for system in extensions_data: if system["folder"] == dest_dir: return extension in system["extensions"] logger.warning(f"Aucun système trouvé pour le dossier {dest_dir}") return False def extract_zip(zip_path, dest_dir, url): """Extrait le contenu du fichier ZIP dans le dossier cible avec un suivi progressif de la progression.""" try: lock = threading.Lock() with zipfile.ZipFile(zip_path, 'r') as zip_ref: total_size = sum(info.file_size for info in zip_ref.infolist() if not info.is_dir()) logger.info(f"Taille totale à extraire: {total_size} octets") if total_size == 0: logger.warning("ZIP vide ou ne contenant que des dossiers") return True, "ZIP vide extrait avec succès" extracted_size = 0 os.makedirs(dest_dir, exist_ok=True) chunk_size = 8192 for info in zip_ref.infolist(): if info.is_dir(): continue file_path = os.path.join(dest_dir, info.filename) os.makedirs(os.path.dirname(file_path), exist_ok=True) with zip_ref.open(info) as source, open(file_path, 'wb') as dest: file_size = info.file_size file_extracted = 0 while True: chunk = source.read(chunk_size) if not chunk: break dest.write(chunk) file_extracted += len(chunk) extracted_size += len(chunk) with lock: config.download_progress[url]["downloaded_size"] = extracted_size config.download_progress[url]["total_size"] = total_size config.download_progress[url]["status"] = "Extracting" config.download_progress[url]["progress_percent"] = (extracted_size / total_size * 100) if total_size > 0 else 0 config.needs_redraw = True # Forcer le redraw logger.debug(f"Extraction {info.filename}, chunk: {len(chunk)}, file_extracted: {file_extracted}/{file_size}, total_extracted: {extracted_size}/{total_size}, progression: {(extracted_size/total_size*100):.1f}%") os.chmod(file_path, 0o644) for root, dirs, _ in os.walk(dest_dir): for dir_name in dirs: os.chmod(os.path.join(root, dir_name), 0o755) os.remove(zip_path) logger.info(f"Fichier ZIP {zip_path} extrait dans {dest_dir} et supprimé") return True, "ZIP extrait avec succès" except Exception as e: logger.error(f"Erreur lors de l'extraction de {zip_path}: {e}") return False, str(e) def extract_rar(rar_path, dest_dir, url): """Extrait le contenu du fichier RAR dans le dossier cible, préservant la structure des dossiers.""" try: lock = threading.Lock() os.makedirs(dest_dir, exist_ok=True) result = subprocess.run(['unrar'], capture_output=True, text=True) if result.returncode not in [0, 1]: logger.error("Commande unrar non disponible") return False, "Commande unrar non disponible" result = subprocess.run(['unrar', 'l', '-v', rar_path], capture_output=True, text=True) if result.returncode != 0: error_msg = result.stderr.strip() logger.error(f"Erreur lors de la liste des fichiers RAR: {error_msg}") return False, f"Échec de la liste des fichiers RAR: {error_msg}" logger.debug(f"Sortie brute de 'unrar l -v {rar_path}':\n{result.stdout}") total_size = 0 files_to_extract = [] root_dirs = set() lines = result.stdout.splitlines() in_file_list = False for line in lines: if line.startswith("----"): in_file_list = not in_file_list continue if in_file_list: match = re.match(r'^\s*(\S+)\s+(\d+)\s+\d*\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(.+)$', line) if match: attrs = match.group(1) file_size = int(match.group(2)) file_date = match.group(3) file_name = match.group(4).strip() if 'D' not in attrs: files_to_extract.append((file_name, file_size)) total_size += file_size root_dir = file_name.split('/')[0] if '/' in file_name else '' if root_dir: root_dirs.add(root_dir) logger.debug(f"Ligne parsée: {file_name}, taille: {file_size}, date: {file_date}") else: logger.debug(f"Dossier ignoré: {file_name}") else: logger.debug(f"Ligne ignorée (format inattendu): {line}") logger.info(f"Taille totale à extraire (RAR): {total_size} octets") logger.debug(f"Fichiers à extraire: {files_to_extract}") logger.debug(f"Dossiers racines détectés: {root_dirs}") if total_size == 0: logger.warning("RAR vide, ne contenant que des dossiers, ou erreur de parsing") return False, "RAR vide ou erreur lors de la liste des fichiers" with lock: config.download_progress[url]["downloaded_size"] = 0 config.download_progress[url]["total_size"] = total_size config.download_progress[url]["status"] = "Extracting" config.download_progress[url]["progress_percent"] = 0 config.needs_redraw = True # Forcer le redraw escaped_rar_path = rar_path.replace(" ", "\\ ") escaped_dest_dir = dest_dir.replace(" ", "\\ ") process = subprocess.Popen(['unrar', 'x', '-y', escaped_rar_path, escaped_dest_dir], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) stdout, stderr = process.communicate() if process.returncode != 0: logger.error(f"Erreur lors de l'extraction de {rar_path}: {stderr}") return False, f"Erreur lors de l'extraction: {stderr}" extracted_size = 0 extracted_files = [] for root, _, files in os.walk(dest_dir): for file in files: file_path = os.path.join(root, file) rel_path = os.path.relpath(file_path, dest_dir).replace(os.sep, '/') for expected_file, file_size in files_to_extract: if rel_path == expected_file: extracted_size += file_size extracted_files.append(expected_file) os.chmod(file_path, 0o644) logger.debug(f"Fichier extrait: {expected_file}, taille: {file_size}, chemin: {file_path}") break missing_files = [f for f, _ in files_to_extract if f not in extracted_files] if missing_files: logger.warning(f"Fichiers non extraits: {', '.join(missing_files)}") return False, f"Fichiers non extraits: {', '.join(missing_files)}" with lock: config.download_progress[url]["downloaded_size"] = extracted_size config.download_progress[url]["total_size"] = total_size config.download_progress[url]["status"] = "Extracting" config.download_progress[url]["progress_percent"] = 100 if total_size > 0 else 0 config.needs_redraw = True # Forcer le redraw if dest_dir == "/userdata/roms/ps3" and len(root_dirs) == 1: root_dir = root_dirs.pop() old_path = os.path.join(dest_dir, root_dir) new_path = os.path.join(dest_dir, f"{root_dir}.ps3") if os.path.isdir(old_path): try: os.rename(old_path, new_path) logger.info(f"Dossier renommé: {old_path} -> {new_path}") except Exception as e: logger.error(f"Erreur lors du renommage de {old_path} en {new_path}: {str(e)}") return False, f"Erreur lors du renommage du dossier: {str(e)}" else: logger.warning(f"Dossier racine {old_path} non trouvé après extraction") elif dest_dir == "/userdata/roms/ps3" and len(root_dirs) > 1: logger.warning(f"Plusieurs dossiers racines détectés dans l'archive: {root_dirs}. Aucun renommage effectué.") for root, dirs, _ in os.walk(dest_dir): for dir_name in dirs: os.chmod(os.path.join(root, dir_name), 0o755) os.remove(rar_path) logger.info(f"Fichier RAR {rar_path} extrait dans {dest_dir} et supprimé") return True, "RAR extrait avec succès" except Exception as e: logger.error(f"Erreur lors de l'extraction de {rar_path}: {str(e)}") return False, str(e) finally: if os.path.exists(rar_path): try: os.remove(rar_path) logger.info(f"Fichier RAR {rar_path} supprimé après échec de l'extraction") except Exception as e: logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}") async def download_rom(url, platform, game_name, is_zip_non_supported=False): logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}") result = [None, None] def download_thread(): logger.debug(f"Thread téléchargement démarré pour {url}") try: dest_dir = None for platform_dict in config.platform_dicts: if platform_dict["platform"] == platform: dest_dir = platform_dict.get("folder") break if not dest_dir: logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}") dest_dir = os.path.join("/userdata/roms", platform) logger.debug(f"Vérification répertoire destination: {dest_dir}") os.makedirs(dest_dir, exist_ok=True) if not os.access(dest_dir, os.W_OK): raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}") sanitized_name = sanitize_filename(game_name) dest_path = os.path.join(dest_dir, f"{sanitized_name}") logger.debug(f"Chemin destination: {dest_path}") lock = threading.Lock() with lock: config.download_progress[url] = { "downloaded_size": 0, "total_size": 0, "status": "Téléchargement", "progress_percent": 0, "game_name": game_name } config.needs_redraw = True # Forcer le redraw logger.debug(f"Progression initialisée pour {url}") headers = {'User-Agent': 'Mozilla/5.0'} logger.debug(f"Envoi requête GET à {url}") response = requests.get(url, stream=True, headers=headers, timeout=30) logger.debug(f"Réponse reçue, status: {response.status_code}") response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) logger.debug(f"Taille totale: {total_size} octets") with lock: config.download_progress[url]["total_size"] = total_size config.needs_redraw = True # Forcer le redraw downloaded = 0 with open(dest_path, 'wb') as f: logger.debug(f"Ouverture fichier: {dest_path}") for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) with lock: config.download_progress[url]["downloaded_size"] = downloaded config.download_progress[url]["status"] = "Téléchargement" config.download_progress[url]["progress_percent"] = (downloaded / total_size * 100) if total_size > 0 else 0 config.needs_redraw = True # Forcer le redraw logger.debug(f"Progression: {downloaded}/{total_size} octets, {config.download_progress[url]['progress_percent']:.1f}%") if is_zip_non_supported: with lock: config.download_progress[url]["downloaded_size"] = 0 config.download_progress[url]["total_size"] = 0 config.download_progress[url]["status"] = "Extracting" config.download_progress[url]["progress_percent"] = 0 config.needs_redraw = True # Forcer le redraw extension = os.path.splitext(dest_path)[1].lower() if extension == ".zip": success, msg = extract_zip(dest_path, dest_dir, url) elif extension == ".rar": success, msg = extract_rar(dest_path, dest_dir, url) else: raise Exception(f"Type d'archive non supporté: {extension}") if not success: raise Exception(f"Échec de l'extraction de l'archive: {msg}") result[0] = True result[1] = f"Téléchargé et extrait : {game_name}" else: os.chmod(dest_path, 0o644) logger.debug(f"Téléchargement terminé: {dest_path}") result[0] = True result[1] = f"Téléchargé : {game_name}" except Exception as e: logger.error(f"Erreur téléchargement {url}: {str(e)}") if url in config.download_progress: with lock: del config.download_progress[url] if os.path.exists(dest_path): os.remove(dest_path) result[0] = False result[1] = str(e) finally: logger.debug(f"Thread téléchargement terminé pour {url}") with lock: config.needs_redraw = True # Forcer le redraw thread = threading.Thread(target=download_thread) logger.debug(f"Démarrage thread pour {url}") thread.start() while thread.is_alive(): pygame.event.pump() await asyncio.sleep(0.1) thread.join() logger.debug(f"Thread rejoint pour {url}") with threading.Lock(): config.download_result_message = result[1] config.download_result_error = not result[0] config.download_result_start_time = pygame.time.get_ticks() config.menu_state = "download_result" config.needs_redraw = True # Forcer le redraw logger.debug(f"Transition vers download_result, message={result[1]}, erreur={not result[0]}") return result[0], result[1] def check_extension_before_download(url, platform, game_name): """Vérifie l'extension avant de lancer le téléchargement.""" try: sanitized_name = sanitize_filename(game_name) extensions_data = load_extensions_json() if not extensions_data: logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable") return False, "Fichier de configuration des extensions introuvable", False is_supported = is_extension_supported(sanitized_name, platform, extensions_data) extension = os.path.splitext(sanitized_name)[1].lower() is_archive = extension in (".zip", ".rar") if is_supported: logger.debug(f"L'extension de {sanitized_name} est supportée pour {platform}") return True, "", False else: if is_archive: logger.debug(f"Fichier {extension.upper()} détecté pour {sanitized_name}, extraction automatique prévue") return False, f"Fichiers {extension.upper()} non supportés par cette plateforme, extraction automatique après le téléchargement.", True logger.debug(f"L'extension de {sanitized_name} n'est pas supportée pour {platform}") return False, f"L'extension de {sanitized_name} n'est pas supportée pour {platform}", False except Exception as e: logger.error(f"Erreur vérification extension {url}: {str(e)}") return False, str(e), False