mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 20:55:24 +02:00
Compare commits
2 Commits
v2.6.1.4
...
v2.6.1.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7795f70e | ||
|
|
6813a0bc3d |
@@ -1050,7 +1050,8 @@ async def main():
|
||||
if success:
|
||||
toast_msg = f"[OK] {game_name}\n{_('download_completed') if _ else 'Download completed'}"
|
||||
else:
|
||||
toast_msg = f"[ERROR] {game_name}\n{_('download_failed') if _ else 'Download failed'}"
|
||||
toast_body = message or (_('download_failed') if _ else 'Download failed')
|
||||
toast_msg = f"[ERROR] {game_name}\n{toast_body}"
|
||||
show_toast(toast_msg, 3000)
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
@@ -1072,7 +1073,8 @@ async def main():
|
||||
config.download_progress.clear()
|
||||
config.pending_download = None
|
||||
# Afficher un toast au lieu de changer de page
|
||||
toast_msg = f"[ERROR] {game_name}\n{_('download_failed') if _ else 'Download failed'}"
|
||||
toast_body = message or (_('download_failed') if _ else 'Download failed')
|
||||
toast_msg = f"[ERROR] {game_name}\n{toast_body}"
|
||||
show_toast(toast_msg, 3000)
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
@@ -1111,7 +1113,8 @@ async def main():
|
||||
if success:
|
||||
toast_msg = f"[OK] {game_name}\n{_('download_completed') if _ else 'Download completed'}"
|
||||
else:
|
||||
toast_msg = f"[ERROR] {game_name}\n{_('download_failed') if _ else 'Download failed'}"
|
||||
toast_body = message or (_('download_failed') if _ else 'Download failed')
|
||||
toast_msg = f"[ERROR] {game_name}\n{toast_body}"
|
||||
show_toast(toast_msg, 3000)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"[DOWNLOAD_TASK] Toast displayed after completion, task_id={task_id}")
|
||||
|
||||
BIN
ports/RGSX/assets/progs/aria2c.exe
Normal file
BIN
ports/RGSX/assets/progs/aria2c.exe
Normal file
Binary file not shown.
@@ -9,8 +9,8 @@ from dataclasses import dataclass
|
||||
@dataclass(slots=True)
|
||||
class Game:
|
||||
name: str
|
||||
url: str
|
||||
size: str
|
||||
url: Optional[str]
|
||||
size: Optional[str]
|
||||
display_name: str # name withou file extension or platform prefix
|
||||
regions: Optional[list[str]] = None
|
||||
is_non_release: Optional[bool] = None
|
||||
@@ -27,7 +27,7 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.6.1.4"
|
||||
app_version = "2.6.1.5.1"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 1
|
||||
@@ -225,6 +225,8 @@ PS3DEC_EXE = os.path.join(APP_FOLDER,"assets", "progs", "ps3dec_win.exe")
|
||||
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")
|
||||
|
||||
# Détection du système d'exploitation (une seule fois au démarrage)
|
||||
OPERATING_SYSTEM = platform.system()
|
||||
|
||||
@@ -19,7 +19,7 @@ from utils import (
|
||||
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,
|
||||
move_files_to_directory
|
||||
move_files_to_directory, parse_torrent_download_url
|
||||
)
|
||||
from history import load_history, clear_history, add_to_history, save_history, scan_roms_for_downloaded_games
|
||||
from language import _, get_available_languages, set_language
|
||||
@@ -40,6 +40,35 @@ logger = logging.getLogger(__name__)
|
||||
ARCHIVE_EXTENSIONS = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
|
||||
|
||||
|
||||
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'}")
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _wrap_index(current_index: int, delta: int, item_count: int) -> int:
|
||||
if item_count <= 0:
|
||||
return 0
|
||||
return (current_index + delta) % item_count
|
||||
|
||||
|
||||
# Variables globales pour la répétition
|
||||
key_states = {} # Dictionnaire pour suivre l'état des touches
|
||||
|
||||
@@ -559,9 +588,11 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
game_name = result.get("game_name")
|
||||
display_name = result.get("display_name") or get_clean_display_name(game_name, platform)
|
||||
|
||||
if not url or not platform or not game_name:
|
||||
if not platform or not game_name:
|
||||
logger.error(f"Resultat de recherche globale invalide: {result}")
|
||||
return
|
||||
if not _has_download_url(url, game_name):
|
||||
return
|
||||
|
||||
pending_download = check_extension_before_download(url, platform, game_name)
|
||||
if not pending_download:
|
||||
@@ -1077,16 +1108,16 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
else:
|
||||
if is_input_matched(event, "up"):
|
||||
if config.current_game > 0:
|
||||
config.current_game -= 1
|
||||
if games:
|
||||
config.current_game = _wrap_index(config.current_game, -1, len(games))
|
||||
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)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
if config.current_game < len(games) - 1:
|
||||
config.current_game += 1
|
||||
if games:
|
||||
config.current_game = _wrap_index(config.current_game, 1, len(games))
|
||||
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
|
||||
@@ -1140,6 +1171,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
url = game.url
|
||||
game_name = game.name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
if not _has_download_url(url, game_name):
|
||||
return action
|
||||
|
||||
pending_download = check_extension_before_download(url, platform, game_name)
|
||||
if pending_download:
|
||||
@@ -1320,8 +1353,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
history = config.history
|
||||
if is_input_matched(event, "up"):
|
||||
# L'historique est inversé à l'affichage, donc UP descend dans l'index (incrément)
|
||||
if config.current_history_item < len(history) - 1:
|
||||
config.current_history_item += 1
|
||||
if history:
|
||||
config.current_history_item = _wrap_index(config.current_history_item, 1, len(history))
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
@@ -1329,8 +1362,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
# L'historique est inversé à l'affichage, donc DOWN monte dans l'index (décrement)
|
||||
if config.current_history_item > 0:
|
||||
config.current_history_item -= 1
|
||||
if history:
|
||||
config.current_history_item = _wrap_index(config.current_history_item, -1, len(history))
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
@@ -1719,7 +1752,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.menu_state = "history"
|
||||
# Réinitialiser l'entrée et relancer
|
||||
url = entry.get("url")
|
||||
if url:
|
||||
if _has_download_url(url, game_name):
|
||||
# Mettre à jour le statut
|
||||
entry["status"] = "Downloading"
|
||||
entry["progress"] = 0
|
||||
@@ -3686,6 +3719,10 @@ def handle_controls(event, sources, joystick, screen):
|
||||
game_name = games[config.current_game].name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
|
||||
if not _has_download_url(url, game_name):
|
||||
config.confirm_press_start_time = 0
|
||||
config.confirm_long_press_triggered = False
|
||||
return action
|
||||
|
||||
# Vérifier d'abord l'extension avant d'ajouter à l'historique
|
||||
if is_1fichier_url(url):
|
||||
@@ -3818,6 +3855,10 @@ def handle_controls(event, sources, joystick, screen):
|
||||
game_name = games[config.current_game].name
|
||||
platform = config.platforms[config.current_platform]["name"] if isinstance(config.platforms[config.current_platform], dict) else config.platforms[config.current_platform]
|
||||
logger.debug(f"Appui court sur confirm ({press_duration}ms), téléchargement pour {game_name}, URL: {url}")
|
||||
if not _has_download_url(url, game_name):
|
||||
config.confirm_press_start_time = 0
|
||||
config.confirm_long_press_triggered = False
|
||||
return action
|
||||
|
||||
# Vérifier d'abord l'extension avant d'ajouter à l'historique
|
||||
if is_1fichier_url(url):
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Keine Internetverbindung. Überprüfe dein Netzwerk.",
|
||||
"error_api_key": "Achtung, du musst deinen API-Schlüssel (nur Premium) in der Datei {0} eingeben",
|
||||
"error_invalid_download_data": "Ungültige Downloaddaten",
|
||||
"popup_torrent_in_maintenance": "Torrent in Wartung, bitte warten",
|
||||
"error_delete_sources": "Fehler beim Löschen der Datei systems_list.json oder Ordner",
|
||||
"platform_no_platform": "Keine Plattform",
|
||||
"platform_page": "Seite {0}/{1}",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "No Internet connection. Check your network.",
|
||||
"error_api_key": "Please enter your API key (premium only) in the file {0}",
|
||||
"error_invalid_download_data": "Invalid download data",
|
||||
"popup_torrent_in_maintenance": "Torrent under maintenance, please wait",
|
||||
"error_delete_sources": "Error deleting systems_list.json file or folders",
|
||||
"platform_no_platform": "No platform",
|
||||
"platform_page": "Page {0}/{1}",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Sin conexión a Internet. Verifica tu red.",
|
||||
"error_api_key": "Atención, debes ingresar tu clave API (solo premium) en el archivo {0}",
|
||||
"error_invalid_download_data": "Datos de descarga no válidos",
|
||||
"popup_torrent_in_maintenance": "Torrent en mantenimiento, por favor espere",
|
||||
"error_delete_sources": "Error al eliminar el archivo systems_list.json o carpetas",
|
||||
"platform_no_platform": "Ninguna plataforma",
|
||||
"platform_page": "Página {0}/{1}",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Pas de connexion Internet. Vérifiez votre réseau.",
|
||||
"error_api_key": "Attention il faut renseigner sa clé API (premium only) dans le fichier {0}",
|
||||
"error_invalid_download_data": "Données de téléchargement invalides",
|
||||
"popup_torrent_in_maintenance": "Torrent en maintenance, veuillez patienter",
|
||||
"error_delete_sources": "Erreur lors de la suppression du fichier systems_list.json ou dossiers",
|
||||
"platform_no_platform": "Aucune plateforme",
|
||||
"platform_page": "Page {0}/{1}",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Nessuna connessione Internet. Controlla la rete.",
|
||||
"error_api_key": "Inserisci la tua API key (solo premium) nel file {0}",
|
||||
"error_invalid_download_data": "Dati di download non validi",
|
||||
"popup_torrent_in_maintenance": "Torrent in manutenzione, attendere prego",
|
||||
"error_delete_sources": "Errore nell'eliminazione del file systems_list.json o delle cartelle",
|
||||
"platform_no_platform": "Nessuna piattaforma",
|
||||
"platform_page": "Pagina {0}/{1}",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"error_no_internet": "Sem conexão com a Internet. Verifique sua rede.",
|
||||
"error_api_key": "Insira sua chave API (somente premium) no arquivo {0}",
|
||||
"error_invalid_download_data": "Dados de download inválidos",
|
||||
"popup_torrent_in_maintenance": "Torrent em manutenção, aguarde",
|
||||
"error_delete_sources": "Erro ao deletar arquivo sources.json ou pastas",
|
||||
"platform_no_platform": "Sem plataforma",
|
||||
"platform_page": "Página {0}/{1}",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
@@ -1161,9 +1161,18 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
game_url = game.url
|
||||
|
||||
if not game_url:
|
||||
torrent_message = TRANSLATIONS.get('popup_torrent_in_maintenance', 'torrent in maintence')
|
||||
self._send_json({
|
||||
'success': False,
|
||||
'error': 'URL de téléchargement non disponible'
|
||||
'error': torrent_message
|
||||
}, 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
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import config
|
||||
from config import HEADLESS, Game
|
||||
try:
|
||||
@@ -62,6 +63,8 @@ def get_clean_display_name(raw_name, platform_id=None):
|
||||
return display_name.strip(" -_/")
|
||||
|
||||
_games_cache = {}
|
||||
_torrent_manifest_cache = {}
|
||||
_TORRENT_DOWNLOAD_SCHEME = "rgsx+torrent"
|
||||
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
@@ -79,6 +82,238 @@ unavailable_systems = []
|
||||
|
||||
# Cache/process flags for extensions generation/loading
|
||||
|
||||
|
||||
def _format_size_bytes(size_bytes: int) -> str:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
|
||||
value = float(size_bytes)
|
||||
for unit in ["KB", "MB", "GB", "TB", "PB"]:
|
||||
value /= 1024.0
|
||||
if value < 1024.0 or unit == "PB":
|
||||
return f"{value:.2f} {unit}"
|
||||
|
||||
return f"{size_bytes} B"
|
||||
|
||||
|
||||
def _decode_bencode_text(value) -> str:
|
||||
if isinstance(value, bytes):
|
||||
for encoding in ("utf-8", "utf-8-sig", "latin-1"):
|
||||
try:
|
||||
return value.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return value.decode("utf-8", errors="replace")
|
||||
return str(value or "")
|
||||
|
||||
|
||||
def _bdecode(data: bytes, index: int = 0):
|
||||
token = data[index:index + 1]
|
||||
if token == b"i":
|
||||
end = data.index(b"e", index)
|
||||
return int(data[index + 1:end]), end + 1
|
||||
if token == b"l":
|
||||
items = []
|
||||
index += 1
|
||||
while data[index:index + 1] != b"e":
|
||||
value, index = _bdecode(data, index)
|
||||
items.append(value)
|
||||
return items, index + 1
|
||||
if token == b"d":
|
||||
values = {}
|
||||
index += 1
|
||||
while data[index:index + 1] != b"e":
|
||||
key, index = _bdecode(data, index)
|
||||
value, index = _bdecode(data, index)
|
||||
values[key] = value
|
||||
return values, index + 1
|
||||
if token.isdigit():
|
||||
sep = data.index(b":", index)
|
||||
length = int(data[index:sep])
|
||||
start = sep + 1
|
||||
end = start + length
|
||||
return data[start:end], end
|
||||
raise ValueError(f"Invalid bencode token at offset {index}: {token!r}")
|
||||
|
||||
|
||||
def is_torrent_manifest_url(url: str | None) -> bool:
|
||||
if not url or not isinstance(url, str):
|
||||
return False
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(url.strip())
|
||||
except Exception:
|
||||
return False
|
||||
return (parsed.path or "").lower().endswith(".torrent")
|
||||
|
||||
|
||||
def build_torrent_download_url(source_url: str, file_index: int, relative_path: str, size_bytes: int | None = None) -> str:
|
||||
params = {
|
||||
"source": source_url,
|
||||
"index": str(max(1, int(file_index))),
|
||||
"path": relative_path,
|
||||
}
|
||||
if isinstance(size_bytes, int) and size_bytes > 0:
|
||||
params["size"] = str(size_bytes)
|
||||
return f"{_TORRENT_DOWNLOAD_SCHEME}://download?{urllib.parse.urlencode(params, quote_via=urllib.parse.quote)}"
|
||||
|
||||
|
||||
def is_torrent_download_url(url: str | None) -> bool:
|
||||
if not url or not isinstance(url, str):
|
||||
return False
|
||||
try:
|
||||
return urllib.parse.urlparse(url).scheme == _TORRENT_DOWNLOAD_SCHEME
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def parse_torrent_download_url(url: str | None) -> dict[str, str | int] | None:
|
||||
if not is_torrent_download_url(url):
|
||||
return None
|
||||
parsed = urllib.parse.urlparse(str(url))
|
||||
query = urllib.parse.parse_qs(parsed.query)
|
||||
source_url = (query.get("source") or [""])[0].strip()
|
||||
relative_path = (query.get("path") or [""])[0].strip()
|
||||
try:
|
||||
file_index = int((query.get("index") or ["1"])[0])
|
||||
except (TypeError, ValueError):
|
||||
file_index = 1
|
||||
try:
|
||||
size_bytes = int((query.get("size") or ["0"])[0])
|
||||
except (TypeError, ValueError):
|
||||
size_bytes = 0
|
||||
if not source_url or not relative_path:
|
||||
return None
|
||||
return {
|
||||
"source_url": source_url,
|
||||
"file_index": max(1, file_index),
|
||||
"relative_path": relative_path,
|
||||
"size_bytes": max(0, size_bytes),
|
||||
}
|
||||
|
||||
|
||||
def _extract_torrent_entries_from_bytes(payload: bytes, source_url: str) -> list[dict[str, str | int]]:
|
||||
torrent_data, _ = _bdecode(payload)
|
||||
if not isinstance(torrent_data, dict):
|
||||
raise ValueError("Torrent root metadata is not a dictionary")
|
||||
|
||||
info = torrent_data.get(b"info")
|
||||
if not isinstance(info, dict):
|
||||
raise ValueError("Torrent metadata does not contain an info dictionary")
|
||||
|
||||
entries: list[dict[str, str | int]] = []
|
||||
files = info.get(b"files")
|
||||
root_name = _decode_bencode_text(info.get(b"name.utf-8") or info.get(b"name") or "").strip()
|
||||
if isinstance(files, list):
|
||||
for file_index, file_entry in enumerate(files, start=1):
|
||||
if not isinstance(file_entry, dict):
|
||||
continue
|
||||
path_parts = file_entry.get(b"path.utf-8") or file_entry.get(b"path") or []
|
||||
if not isinstance(path_parts, list):
|
||||
continue
|
||||
parts = [_decode_bencode_text(part).strip() for part in path_parts]
|
||||
parts = [part for part in parts if part]
|
||||
if not parts:
|
||||
continue
|
||||
full_path = "/".join(parts)
|
||||
download_path = "/".join([p for p in [root_name, full_path] if p])
|
||||
entries.append({
|
||||
"name": parts[-1],
|
||||
"path": full_path,
|
||||
"download_path": download_path or full_path,
|
||||
"index": file_index,
|
||||
"size_bytes": int(file_entry.get(b"length") or 0),
|
||||
"source_url": source_url,
|
||||
})
|
||||
else:
|
||||
if root_name:
|
||||
entries.append({
|
||||
"name": root_name,
|
||||
"path": root_name,
|
||||
"download_path": root_name,
|
||||
"index": 1,
|
||||
"size_bytes": int(info.get(b"length") or 0),
|
||||
"source_url": source_url,
|
||||
})
|
||||
|
||||
duplicate_names = {}
|
||||
for entry in entries:
|
||||
name = str(entry["name"])
|
||||
duplicate_names[name] = duplicate_names.get(name, 0) + 1
|
||||
|
||||
for entry in entries:
|
||||
if duplicate_names.get(str(entry["name"]), 0) > 1:
|
||||
entry["name"] = str(entry["path"])
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _get_torrent_entries(source_url: str) -> list[dict[str, str | int]]:
|
||||
cached = _torrent_manifest_cache.get(source_url)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
||||
"Accept": "*/*",
|
||||
}
|
||||
response = requests.get(source_url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
entries = _extract_torrent_entries_from_bytes(response.content, source_url)
|
||||
_torrent_manifest_cache[source_url] = entries
|
||||
return entries
|
||||
|
||||
|
||||
def _extract_torrent_source(item) -> tuple[str, str] | None:
|
||||
if isinstance(item, (list, tuple)):
|
||||
if len(item) < 2:
|
||||
return None
|
||||
source_name = str(item[0] or "").strip()
|
||||
source_url = item[1] if isinstance(item[1], str) else None
|
||||
if source_url and is_torrent_manifest_url(source_url):
|
||||
return source_name, source_url.strip()
|
||||
return None
|
||||
|
||||
if isinstance(item, dict):
|
||||
source_url = item.get("torrent_url") or item.get("url") or item.get("download") or item.get("link")
|
||||
if not isinstance(source_url, str) or not source_url.strip():
|
||||
return None
|
||||
source_type = str(item.get("type") or item.get("source_type") or item.get("source") or "").strip().lower()
|
||||
if source_type == "torrent" or is_torrent_manifest_url(source_url):
|
||||
source_name = item.get("game_name") or item.get("name") or item.get("title") or item.get("game") or item.get("label")
|
||||
if not source_name:
|
||||
parsed = urllib.parse.urlparse(source_url)
|
||||
source_name = urllib.parse.unquote(Path(parsed.path).name)
|
||||
return str(source_name or "").strip(), source_url.strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _expand_torrent_source(item, platform_id: str) -> list[tuple[str, None, str | None]] | None:
|
||||
source = _extract_torrent_source(item)
|
||||
if not source:
|
||||
return None
|
||||
|
||||
source_name, source_url = source
|
||||
try:
|
||||
entries = _get_torrent_entries(source_url)
|
||||
except Exception as exc:
|
||||
label = source_name or source_url
|
||||
logger.error(f"Erreur chargement torrent pour {platform_id} ({label}): {exc}")
|
||||
return []
|
||||
|
||||
expanded: list[tuple[str, None, str | None]] = []
|
||||
for entry in entries:
|
||||
game_name = str(entry.get("name") or "").strip()
|
||||
if not game_name:
|
||||
continue
|
||||
size_bytes = int(entry.get("size_bytes") or 0)
|
||||
file_index = int(entry.get("index") or 1)
|
||||
relative_path = str(entry.get("download_path") or entry.get("path") or game_name)
|
||||
download_url = build_torrent_download_url(source_url, file_index, relative_path, size_bytes)
|
||||
expanded.append((game_name, download_url, _format_size_bytes(size_bytes) if size_bytes > 0 else None))
|
||||
return expanded
|
||||
|
||||
|
||||
def restart_application(delay_ms: int = 2000):
|
||||
"""Schedule a restart with a visible popup; actual restart happens in the main loop.
|
||||
@@ -1244,6 +1479,10 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
normalized = [] # (name, url, size)
|
||||
|
||||
def extract_from_dict(d):
|
||||
torrent_rows = _expand_torrent_source(d, platform_id)
|
||||
if torrent_rows is not None:
|
||||
normalized.extend(torrent_rows)
|
||||
return
|
||||
name = d.get('game_name') or d.get('name') or d.get('title') or d.get('game')
|
||||
url = d.get('url') or d.get('download') or d.get('link') or d.get('href')
|
||||
size = d.get('size') or d.get('filesize') or d.get('length')
|
||||
@@ -1253,6 +1492,10 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
if isinstance(item, (list, tuple)):
|
||||
torrent_rows = _expand_torrent_source(item, platform_id)
|
||||
if torrent_rows is not None:
|
||||
normalized.extend(torrent_rows)
|
||||
continue
|
||||
if len(item) == 0:
|
||||
continue
|
||||
name = str(item[0])
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.1.4"
|
||||
"version": "2.6.1.5.1"
|
||||
}
|
||||
Reference in New Issue
Block a user