mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 13:13:36 +02:00
Compare commits
13 Commits
v2.6.1.1
...
v2.6.1.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7dad84108 | ||
|
|
c9f48d20dd | ||
|
|
cd7795f70e | ||
|
|
6813a0bc3d | ||
|
|
21b39c66b9 | ||
|
|
42b2204aeb | ||
|
|
67a38c45aa | ||
|
|
893b73ecc5 | ||
|
|
5e1a684275 | ||
|
|
9226a818f3 | ||
|
|
2fd1bcaf01 | ||
|
|
875bf8fa23 | ||
|
|
f9cbf0196e |
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -148,3 +148,16 @@ jobs:
|
||||
dist/RGSX_full_latest.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Send changelog to Discord
|
||||
run: |
|
||||
CHANGELOG=$(git log -1 --format=%B ${{ github.ref_name }} | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||
|
||||
curl -H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "{
|
||||
\"username\": \"RGSX Releases Bot\",
|
||||
\"avatar_url\": \"https://retrogamesets.fr/assets/images/avatar.png\",
|
||||
\"content\": \"📦 **RGSX ${{ github.ref_name }}**\n\n📝 **Changelog :**\n${CHANGELOG}\"
|
||||
}" \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -35,7 +35,7 @@ from display import (
|
||||
draw_toast, show_toast, THEME_COLORS, sync_display_metrics
|
||||
)
|
||||
from language import _
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, cancel_all_downloads, download_queue_worker
|
||||
from network import test_internet, download_rom, is_1fichier_url, download_from_1fichier, check_for_updates, apply_pending_update, cancel_all_downloads, 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
|
||||
@@ -99,6 +99,9 @@ _run_windows_gamelist_update()
|
||||
try:
|
||||
config.update_checked = False
|
||||
config.gamelist_update_prompted = False # Flag pour ne pas redemander la mise à jour plusieurs fois
|
||||
config.pending_update_version = ""
|
||||
config.startup_update_confirmed = False
|
||||
config.text_file_mode = ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -457,6 +460,7 @@ async def main():
|
||||
|
||||
running = True
|
||||
loading_step = "none"
|
||||
ota_update_task = None
|
||||
sources = []
|
||||
config.last_state_change_time = 0
|
||||
config.debounce_delay = 50
|
||||
@@ -489,6 +493,9 @@ async def main():
|
||||
if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100:
|
||||
config.needs_redraw = True
|
||||
last_redraw_time = current_time
|
||||
if config.menu_state == "loading" and current_time - last_redraw_time >= 100:
|
||||
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"] in ["Downloading", "Téléchargement"] for entry in config.history):
|
||||
if current_time - last_redraw_time >= 100:
|
||||
@@ -1043,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]
|
||||
@@ -1065,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]
|
||||
@@ -1104,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}")
|
||||
@@ -1357,6 +1367,10 @@ async def main():
|
||||
config.error_message = message or _("error_check_updates_failed")
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Erreur OTA : {message}")
|
||||
elif getattr(config, "pending_update_version", ""):
|
||||
loading_step = "await_ota_confirmation"
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
else:
|
||||
loading_step = "check_data"
|
||||
config.current_loading_system = _("loading_downloading_games_images")
|
||||
@@ -1364,6 +1378,38 @@ async def main():
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
continue # Passer immédiatement à check_data
|
||||
elif loading_step == "await_ota_confirmation":
|
||||
if not getattr(config, "startup_update_confirmed", False):
|
||||
await asyncio.sleep(0.01)
|
||||
continue
|
||||
|
||||
latest_version = getattr(config, "pending_update_version", "")
|
||||
config.startup_update_confirmed = False
|
||||
ota_update_task = asyncio.create_task(apply_pending_update(latest_version))
|
||||
loading_step = "apply_ota_update"
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
elif loading_step == "apply_ota_update":
|
||||
if ota_update_task is None:
|
||||
loading_step = "check_data"
|
||||
continue
|
||||
if not ota_update_task.done():
|
||||
await asyncio.sleep(0.01)
|
||||
continue
|
||||
|
||||
success, message = await ota_update_task
|
||||
ota_update_task = None
|
||||
if not success:
|
||||
config.menu_state = "error"
|
||||
config.error_message = message or _("error_check_updates_failed")
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
config.pending_update_version = ""
|
||||
config.text_file_mode = ""
|
||||
config.text_file_content = ""
|
||||
config.loading_detail_lines = []
|
||||
config.needs_redraw = True
|
||||
continue
|
||||
elif loading_step == "check_data":
|
||||
is_data_empty = not os.path.exists(config.GAMES_FOLDER) or not any(os.scandir(config.GAMES_FOLDER))
|
||||
if is_data_empty:
|
||||
|
||||
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.1"
|
||||
app_version = "2.6.1.6.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()
|
||||
@@ -494,6 +496,10 @@ hide_premium_systems = False # Indicateur pour masquer les systèmes premium
|
||||
|
||||
# Variables diverses
|
||||
update_checked = False
|
||||
pending_update_version = ""
|
||||
startup_update_confirmed = False
|
||||
text_file_mode = ""
|
||||
loading_detail_lines = []
|
||||
extension_confirm_selection = 0 # Index de sélection pour confirmation d'extension
|
||||
controls_config = {} # Configuration des contrôles personnalisés
|
||||
selected_key = (0, 0) # Position du curseur dans le clavier virtuel
|
||||
|
||||
@@ -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
|
||||
@@ -1733,6 +1766,9 @@ def handle_controls(event, sources, joystick, screen):
|
||||
is_zip_non_supported = pending_download[3] if len(pending_download) > 3 else False
|
||||
|
||||
if is_1fichier_url(url):
|
||||
ensure_download_provider_keys(False)
|
||||
if missing_all_provider_keys():
|
||||
logger.warning("Aucune clé API - Mode gratuit 1fichier sera utilisé (attente requise)")
|
||||
task = asyncio.create_task(download_from_1fichier(url, platform, game_name, is_zip_non_supported, task_id))
|
||||
else:
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported, task_id))
|
||||
@@ -1828,60 +1864,6 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
|
||||
# Affichage détails erreur
|
||||
# Visualiseur de fichiers texte
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
if content:
|
||||
lines = content.split('\n')
|
||||
line_height = config.small_font.get_height() + 2
|
||||
|
||||
# Calculer le nombre de lignes visibles (approximation)
|
||||
controls_y = config.screen_height - int(config.screen_height * 0.037)
|
||||
margin = 40
|
||||
header_height = 60
|
||||
content_area_height = controls_y - 2 * margin - 10 - header_height - 20
|
||||
visible_lines = int(content_area_height / line_height)
|
||||
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
max_scroll = max(0, len(lines) - visible_lines)
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - 1)
|
||||
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"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + 1)
|
||||
update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - visible_lines)
|
||||
update_key_state("page_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, "page_down"):
|
||||
config.text_file_scroll_offset = min(max_scroll, scroll_offset + visible_lines)
|
||||
update_key_state("page_down", True, event.type, event.key if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
# Si pas de contenu, retourner au menu précédent
|
||||
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
|
||||
# Visualiseur de fichiers texte
|
||||
elif config.menu_state == "text_file_viewer":
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
@@ -1912,6 +1894,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
max_scroll = max(0, len(wrapped_lines) - visible_lines)
|
||||
viewer_mode = getattr(config, 'text_file_mode', '')
|
||||
|
||||
if is_input_matched(event, "up"):
|
||||
config.text_file_scroll_offset = max(0, scroll_offset - 1)
|
||||
@@ -1941,7 +1924,11 @@ def handle_controls(event, sources, joystick, screen):
|
||||
(event.axis, event.value) if event.type == pygame.JOYAXISMOTION else
|
||||
event.value)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel") or is_input_matched(event, "confirm"):
|
||||
elif viewer_mode == "ota_update" and is_input_matched(event, "confirm"):
|
||||
config.startup_update_confirmed = True
|
||||
config.menu_state = "loading"
|
||||
config.needs_redraw = True
|
||||
elif viewer_mode != "ota_update" and (is_input_matched(event, "cancel") or is_input_matched(event, "confirm")):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
@@ -2999,6 +2986,25 @@ def handle_controls(event, sources, joystick, screen):
|
||||
if config.folder_browser_selection >= config.folder_browser_scroll_offset + config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
jump_size = 10
|
||||
if config.folder_browser_selection > 0:
|
||||
config.folder_browser_selection = max(0, config.folder_browser_selection - jump_size)
|
||||
config.folder_browser_scroll_offset = min(
|
||||
config.folder_browser_scroll_offset,
|
||||
config.folder_browser_selection
|
||||
)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
jump_size = 10
|
||||
if config.folder_browser_selection < len(config.folder_browser_items) - 1:
|
||||
config.folder_browser_selection = min(
|
||||
len(config.folder_browser_items) - 1,
|
||||
config.folder_browser_selection + jump_size
|
||||
)
|
||||
if config.folder_browser_selection >= config.folder_browser_scroll_offset + config.folder_browser_visible_items:
|
||||
config.folder_browser_scroll_offset = config.folder_browser_selection - config.folder_browser_visible_items + 1
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.folder_browser_items:
|
||||
selected_item = config.folder_browser_items[config.folder_browser_selection]
|
||||
@@ -3716,6 +3722,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):
|
||||
@@ -3848,6 +3858,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):
|
||||
|
||||
@@ -795,7 +795,11 @@ def draw_loading_screen(screen):
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
loading_y = rect_y + rect_height + int(config.screen_height * 0.0926)
|
||||
text = config.small_font.render(truncate_text_middle(f"{config.current_loading_system}", config.small_font, config.screen_width - 2 * margin_horizontal), True, THEME_COLORS["text"])
|
||||
text = config.small_font.render(
|
||||
truncate_text_middle(f"{config.current_loading_system}", config.small_font, config.screen_width - 2 * margin_horizontal, is_filename=False),
|
||||
True,
|
||||
THEME_COLORS["text"]
|
||||
)
|
||||
text_rect = text.get_rect(center=(config.screen_width // 2, loading_y))
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
@@ -805,9 +809,24 @@ def draw_loading_screen(screen):
|
||||
|
||||
bar_width = int(config.screen_width * 0.2083)
|
||||
bar_height = int(config.screen_height * 0.037)
|
||||
bar_y = loading_y + int(config.screen_height * 0.0926)
|
||||
progress_width = (bar_width * config.loading_progress) / 100
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (config.screen_width // 2 - bar_width // 2, loading_y + int(config.screen_height * 0.0926), bar_width, bar_height), border_radius=8)
|
||||
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (config.screen_width // 2 - bar_width // 2, loading_y + int(config.screen_height * 0.0926), progress_width, bar_height), border_radius=8)
|
||||
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (config.screen_width // 2 - bar_width // 2, bar_y, bar_width, bar_height), border_radius=8)
|
||||
pygame.draw.rect(screen, THEME_COLORS["fond_lignes"], (config.screen_width // 2 - bar_width // 2, bar_y, progress_width, bar_height), border_radius=8)
|
||||
|
||||
detail_lines = getattr(config, 'loading_detail_lines', []) or []
|
||||
detail_y = bar_y + bar_height + 14
|
||||
max_detail_width = config.screen_width - 2 * margin_horizontal
|
||||
rendered_lines = []
|
||||
for detail_line in detail_lines:
|
||||
if not detail_line:
|
||||
continue
|
||||
rendered_lines.extend(wrap_text(str(detail_line), config.small_font, max_detail_width))
|
||||
|
||||
for index, detail_line in enumerate(rendered_lines[:3]):
|
||||
detail_surface = config.small_font.render(detail_line, True, THEME_COLORS["title_text"])
|
||||
detail_rect = detail_surface.get_rect(center=(config.screen_width // 2, detail_y + index * (config.small_font.get_height() + 4)))
|
||||
screen.blit(detail_surface, detail_rect)
|
||||
|
||||
# Écran d'erreur
|
||||
def draw_error_screen(screen):
|
||||
@@ -2906,6 +2925,7 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
|
||||
],
|
||||
"folder_browser": [
|
||||
("confirm", _("folder_browser_enter")),
|
||||
(("page_up", "page_down"), _("controls_pages")),
|
||||
("history", _("folder_browser_select")),
|
||||
("clear_history", _("folder_new_folder")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
@@ -4720,8 +4740,17 @@ def draw_folder_browser(screen):
|
||||
list_y = panel_y + 100
|
||||
list_height = panel_height - 180
|
||||
item_height = max(35, config.small_font.get_height() + 10)
|
||||
visible_items = min(visible_items, list_height // item_height)
|
||||
visible_items = max(1, list_height // item_height)
|
||||
config.folder_browser_visible_items = visible_items
|
||||
|
||||
max_scroll_offset = max(0, len(items) - visible_items)
|
||||
if scroll_offset > max_scroll_offset:
|
||||
scroll_offset = max_scroll_offset
|
||||
config.folder_browser_scroll_offset = scroll_offset
|
||||
|
||||
if selection >= len(items) and items:
|
||||
selection = len(items) - 1
|
||||
config.folder_browser_selection = selection
|
||||
|
||||
# Afficher les éléments visibles
|
||||
for i in range(visible_items):
|
||||
@@ -5388,6 +5417,7 @@ def draw_text_file_viewer(screen):
|
||||
content = getattr(config, 'text_file_content', '')
|
||||
filename = getattr(config, 'text_file_name', 'Unknown')
|
||||
scroll_offset = getattr(config, 'text_file_scroll_offset', 0)
|
||||
viewer_mode = getattr(config, 'text_file_mode', '')
|
||||
|
||||
# Dimensions
|
||||
margin = 40
|
||||
@@ -5464,6 +5494,11 @@ def draw_text_file_viewer(screen):
|
||||
position_surface = config.small_font.render(position_text, True, THEME_COLORS["title_text"])
|
||||
position_rect = position_surface.get_rect(right=rect_x + rect_width - 30, bottom=rect_y + rect_height - 10)
|
||||
screen.blit(position_surface, position_rect)
|
||||
|
||||
if viewer_mode == 'ota_update':
|
||||
hint_surface = config.small_font.render("Confirm: Update", True, THEME_COLORS["text_selected"])
|
||||
hint_rect = hint_surface.get_rect(left=rect_x + 30, bottom=rect_y + rect_height - 10)
|
||||
screen.blit(hint_surface, hint_rect)
|
||||
else:
|
||||
# Aucun contenu
|
||||
no_content_text = "Empty file"
|
||||
|
||||
@@ -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}",
|
||||
@@ -46,6 +47,9 @@
|
||||
"free_mode_submitting": "[Kostenloser Modus] Formular wird gesendet...",
|
||||
"free_mode_link_found": "[Kostenloser Modus] Link gefunden: {0}...",
|
||||
"free_mode_completed": "[Kostenloser Modus] Abgeschlossen: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: Der kostenlose Gast-Download ist vorübergehend nicht verfügbar (alle Slots sind belegt). Bitte versuchen Sie es später erneut.",
|
||||
"free_mode_unavailable_in_app": "1fichier: Dieser Download ist derzeit in der Anwendung nicht verfügbar. Bitte versuchen Sie es später erneut.",
|
||||
"free_mode_premium_advice": "Für unbegrenzte Downloads jederzeit und mit voller Geschwindigkeit benötigen Sie ein Premium-Konto oder einen Debrid-Dienst und müssen dessen API-Schlüssel in RGSX eintragen.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download vom Benutzer abgebrochen.",
|
||||
"download_removed_from_queue": "Aus der Download-Warteschlange entfernt",
|
||||
|
||||
@@ -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}",
|
||||
@@ -46,6 +47,9 @@
|
||||
"free_mode_submitting": "[Free mode] Submitting form...",
|
||||
"free_mode_link_found": "[Free mode] Link found: {0}...",
|
||||
"free_mode_completed": "[Free mode] Completed: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: free guest download is temporarily unavailable (all slots are currently in use). Please try again later.",
|
||||
"free_mode_unavailable_in_app": "1fichier: this download is not available in the application right now. Please try again later.",
|
||||
"free_mode_premium_advice": "For unlimited, on-demand, full-speed downloads, you need a premium account or debrid service and must enter its API key in RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download canceled by user.",
|
||||
"download_removed_from_queue": "Removed from download queue",
|
||||
|
||||
@@ -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}",
|
||||
@@ -46,6 +47,9 @@
|
||||
"free_mode_submitting": "[Modo gratuito] Enviando formulario...",
|
||||
"free_mode_link_found": "[Modo gratuito] Enlace encontrado: {0}...",
|
||||
"free_mode_completed": "[Modo gratuito] Completado: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: la descarga gratuita como invitado no está disponible temporalmente (todos los cupos están ocupados). Inténtelo de nuevo más tarde.",
|
||||
"free_mode_unavailable_in_app": "1fichier: esta descarga no está disponible en la aplicación en este momento. Inténtelo de nuevo más tarde.",
|
||||
"free_mode_premium_advice": "Para descargar de forma ilimitada, cuando quiera y a máxima velocidad, necesita una cuenta premium o un desbridizador y debe introducir su clave API en RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Descarga cancelada por el usuario.",
|
||||
"download_removed_from_queue": "Eliminado de la cola de descarga",
|
||||
|
||||
@@ -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}",
|
||||
@@ -46,6 +47,9 @@
|
||||
"free_mode_submitting": "[Mode gratuit] Soumission formulaire...",
|
||||
"free_mode_link_found": "[Mode gratuit] Lien trouvé: {0}...",
|
||||
"free_mode_completed": "[Mode gratuit] Terminé: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier : le téléchargement gratuit invité est temporairement indisponible (tous les créneaux sont occupés). Réessayez plus tard.",
|
||||
"free_mode_unavailable_in_app": "1fichier : ce téléchargement n'est pas disponible dans l'application pour le moment. Réessayez plus tard.",
|
||||
"free_mode_premium_advice": "Pour télécharger de manière illimitée, quand vous voulez et à pleine vitesse, vous devez obtenir un compte premium ou un débrideur et entrer votre clé API dans RGSX.",
|
||||
"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",
|
||||
|
||||
@@ -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}",
|
||||
@@ -46,6 +47,9 @@
|
||||
"free_mode_submitting": "[Modalità gratuita] Invio modulo...",
|
||||
"free_mode_link_found": "[Modalità gratuita] Link trovato: {0}...",
|
||||
"free_mode_completed": "[Modalità gratuita] Completato: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: il download gratuito come ospite non è temporaneamente disponibile (tutti gli slot sono occupati). Riprova più tardi.",
|
||||
"free_mode_unavailable_in_app": "1fichier: questo download non è disponibile nell'applicazione in questo momento. Riprova più tardi.",
|
||||
"free_mode_premium_advice": "Per scaricare senza limiti, quando vuoi e alla massima velocità, hai bisogno di un account premium o di un servizio debrid e devi inserire la sua chiave API in RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download annullato dall'utente.",
|
||||
"download_removed_from_queue": "Rimosso dalla coda di download",
|
||||
|
||||
@@ -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}",
|
||||
@@ -46,6 +47,9 @@
|
||||
"free_mode_submitting": "[Modo gratuito] Enviando formulário...",
|
||||
"free_mode_link_found": "[Modo gratuito] Link encontrado: {0}...",
|
||||
"free_mode_completed": "[Modo gratuito] Concluído: {0}",
|
||||
"free_mode_guest_slots_unavailable": "1fichier: o download gratuito como convidado está temporariamente indisponível (todos os slots estão ocupados). Tente novamente mais tarde.",
|
||||
"free_mode_unavailable_in_app": "1fichier: este download não está disponível no aplicativo no momento. Tente novamente mais tarde.",
|
||||
"free_mode_premium_advice": "Para baixar sem limites, quando quiser e em velocidade máxima, você precisa de uma conta premium ou de um serviço debrid e deve inserir a chave API no RGSX.",
|
||||
"download_status": "{0}: {1}",
|
||||
"download_canceled": "Download cancelado pelo usuário.",
|
||||
"download_removed_from_queue": "Removido da fila de download",
|
||||
|
||||
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.1"
|
||||
"version": "2.6.1.6.1"
|
||||
}
|
||||
Reference in New Issue
Block a user