mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 10:43:35 +02:00
Compare commits
14 Commits
v2.6.1.0
...
v2.6.1.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7dad84108 | ||
|
|
c9f48d20dd | ||
|
|
cd7795f70e | ||
|
|
6813a0bc3d | ||
|
|
21b39c66b9 | ||
|
|
42b2204aeb | ||
|
|
67a38c45aa | ||
|
|
893b73ecc5 | ||
|
|
5e1a684275 | ||
|
|
9226a818f3 | ||
|
|
2fd1bcaf01 | ||
|
|
875bf8fa23 | ||
|
|
f9cbf0196e | ||
|
|
eb86d69895 |
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.0"
|
||||
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
|
||||
|
||||
@@ -15,10 +15,11 @@ from utils import (
|
||||
load_games, check_extension_before_download, is_extension_supported,
|
||||
load_extensions_json, play_random_music, sanitize_filename,
|
||||
save_music_config, load_api_keys, _get_dest_folder_name,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, toggle_web_service_at_boot, check_web_service_status,
|
||||
extract_zip, extract_rar, find_file_with_or_without_extension, find_matching_files, toggle_web_service_at_boot, check_web_service_status,
|
||||
restart_application, generate_support_zip, load_sources,
|
||||
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
|
||||
start_connection_status_check
|
||||
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
|
||||
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
|
||||
@@ -39,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
|
||||
|
||||
@@ -556,11 +586,13 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
url = result.get("url")
|
||||
platform = result.get("platform_id")
|
||||
game_name = result.get("game_name")
|
||||
display_name = result.get("display_name") or 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:
|
||||
@@ -602,6 +634,7 @@ def trigger_global_search_download(queue_only: bool = False) -> None:
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': display_name,
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -1075,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
|
||||
@@ -1138,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:
|
||||
@@ -1176,6 +1211,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -1248,6 +1284,7 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.history.append({
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': url,
|
||||
'progress': 0,
|
||||
@@ -1316,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
|
||||
@@ -1325,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
|
||||
@@ -1488,6 +1525,13 @@ def handle_controls(event, sources, joystick, screen):
|
||||
dest_folder = _get_dest_folder_name(platform)
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
config.history_actual_matches = actual_matches
|
||||
|
||||
# Stocker les informations pour les autres handlers
|
||||
config.history_actual_filename = actual_filename
|
||||
@@ -1708,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
|
||||
@@ -1722,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))
|
||||
@@ -1747,7 +1794,47 @@ def handle_controls(event, sources, joystick, screen):
|
||||
|
||||
# Affichage du dossier de téléchargement
|
||||
elif config.menu_state == "history_show_folder":
|
||||
if is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
|
||||
if is_input_matched(event, "clear_history"):
|
||||
if not config.history or config.current_history_item >= len(config.history):
|
||||
config.menu_state = "history"
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
entry = config.history[config.current_history_item]
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
|
||||
start_path = None
|
||||
if actual_matches:
|
||||
start_path = os.path.dirname(actual_matches[0][1])
|
||||
else:
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
if actual_path and os.path.exists(actual_path):
|
||||
start_path = os.path.dirname(actual_path)
|
||||
|
||||
if not start_path or not os.path.isdir(start_path):
|
||||
start_path = config.ROMS_FOLDER
|
||||
|
||||
config.folder_browser_path = start_path
|
||||
config.folder_browser_selection = 0
|
||||
config.folder_browser_scroll_offset = 0
|
||||
config.folder_browser_mode = "history_move"
|
||||
config.platform_config_name = entry.get("display_name") or get_clean_display_name(entry.get("game_name", ""), entry.get("platform", ""))
|
||||
|
||||
try:
|
||||
items = [".."]
|
||||
for item in sorted(os.listdir(start_path)):
|
||||
full_path = os.path.join(start_path, item)
|
||||
if os.path.isdir(full_path):
|
||||
items.append(item)
|
||||
config.folder_browser_items = items
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture dossier {start_path}: {e}")
|
||||
config.folder_browser_items = [".."]
|
||||
|
||||
config.menu_state = "folder_browser"
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm") or is_input_matched(event, "cancel"):
|
||||
config.menu_state = validate_menu_state(config.previous_menu_state)
|
||||
config.needs_redraw = True
|
||||
|
||||
@@ -1777,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', '')
|
||||
@@ -1861,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)
|
||||
@@ -1890,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:
|
||||
@@ -2948,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]
|
||||
@@ -3001,6 +3058,34 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Informer qu'un redémarrage est nécessaire
|
||||
config.popup_message = _("roms_folder_set_restart").format(selected_path) if _ else f"ROMs folder set: {selected_path}\nRestart required!"
|
||||
config.menu_state = "pause_settings_menu"
|
||||
elif browser_mode == "history_move":
|
||||
entry = config.history[config.current_history_item] if config.history and config.current_history_item < len(config.history) else None
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
if not actual_matches and entry:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
|
||||
source_paths = [match_path for _, match_path in actual_matches]
|
||||
if not source_paths:
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
if actual_path:
|
||||
source_paths = [actual_path]
|
||||
|
||||
success, moved_matches, error_message = move_files_to_directory(source_paths, selected_path)
|
||||
if success:
|
||||
config.history_actual_matches = moved_matches
|
||||
if moved_matches:
|
||||
config.history_actual_filename, config.history_actual_path = moved_matches[0]
|
||||
if entry is not None:
|
||||
entry["moved_paths"] = [path for _, path in moved_matches]
|
||||
save_history(config.history)
|
||||
config.popup_message = _("history_move_success").format(len(moved_matches), selected_path) if _ else f"Moved {len(moved_matches)} file(s) to {selected_path}"
|
||||
config.popup_timer = 3000
|
||||
logger.info(f"Déplacement historique terminé vers {selected_path}: {len(moved_matches)} fichier(s)")
|
||||
else:
|
||||
config.popup_message = _("history_move_error").format(error_message) if _ else f"Move error: {error_message}"
|
||||
config.popup_timer = 4000
|
||||
logger.error(f"Erreur déplacement historique vers {selected_path}: {error_message}")
|
||||
config.menu_state = "history_show_folder"
|
||||
else:
|
||||
# Mode dossier plateforme
|
||||
from rgsx_settings import set_platform_custom_path
|
||||
@@ -3016,6 +3101,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
browser_mode = getattr(config, 'folder_browser_mode', 'platform')
|
||||
if browser_mode == "roms_root":
|
||||
config.menu_state = "pause_settings_menu"
|
||||
elif browser_mode == "history_move":
|
||||
config.menu_state = "history_show_folder"
|
||||
else:
|
||||
config.menu_state = "platform_folder_config"
|
||||
config.needs_redraw = True
|
||||
@@ -3635,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):
|
||||
@@ -3767,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):
|
||||
|
||||
@@ -10,8 +10,9 @@ from datetime import datetime
|
||||
import config
|
||||
from utils import (truncate_text_middle, wrap_text, load_system_image, truncate_text_end,
|
||||
check_web_service_status, check_custom_dns_status, load_api_keys,
|
||||
_get_dest_folder_name, find_file_with_or_without_extension,
|
||||
get_connection_status_targets, get_connection_status_snapshot)
|
||||
_get_dest_folder_name, find_file_with_or_without_extension, find_matching_files,
|
||||
get_connection_status_targets, get_connection_status_snapshot,
|
||||
get_clean_display_name, get_existing_history_matches)
|
||||
import logging
|
||||
import math
|
||||
from history import load_history, is_game_downloaded
|
||||
@@ -794,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)
|
||||
|
||||
@@ -804,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):
|
||||
@@ -2358,14 +2378,14 @@ def draw_history_list(screen):
|
||||
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
|
||||
screen.blit(title_surface, title_rect)
|
||||
|
||||
# Define column widths as percentages of available space (give more space to status/error messages)
|
||||
# Prioritize the game title by shrinking size/status columns.
|
||||
column_width_percentages = {
|
||||
"platform": 0.13,
|
||||
"game_name": 0.25,
|
||||
"ext": 0.08,
|
||||
"folder": 0.12,
|
||||
"size": 0.08,
|
||||
"status": 0.34
|
||||
"game_name": 0.40,
|
||||
"ext": 0.07,
|
||||
"folder": 0.16,
|
||||
"size": 0.06,
|
||||
"status": 0.18
|
||||
}
|
||||
available_width = int(0.95 * config.screen_width - 60) # Total available width for columns
|
||||
col_platform_width = int(available_width * column_width_percentages["platform"])
|
||||
@@ -2471,8 +2491,9 @@ def draw_history_list(screen):
|
||||
for idx, i in enumerate(range(config.history_scroll_offset, min(config.history_scroll_offset + items_per_page, len(history)))):
|
||||
entry = history[i]
|
||||
platform = entry.get("platform", "Inconnu")
|
||||
game_name = entry.get("game_name", "Inconnu")
|
||||
ext_text = get_display_extension(game_name)
|
||||
raw_game_name = entry.get("game_name", "Inconnu")
|
||||
game_name = entry.get("display_name") or get_clean_display_name(raw_game_name, platform)
|
||||
ext_text = get_display_extension(raw_game_name)
|
||||
folder_text = _get_dest_folder_name(platform)
|
||||
|
||||
# Correction du calcul de la taille
|
||||
@@ -2547,7 +2568,7 @@ def draw_history_list(screen):
|
||||
status_color = THEME_COLORS.get("text", (255, 255, 255))
|
||||
|
||||
platform_text = truncate_text_end(platform, config.small_font, col_platform_width - 10)
|
||||
game_text = truncate_text_end(str(game_name).rsplit('.', 1)[0] if '.' in str(game_name) else str(game_name), config.small_font, col_game_width - 10)
|
||||
game_text = truncate_text_middle(str(game_name), config.small_font, col_game_width - 10, is_filename=False)
|
||||
ext_text = truncate_text_end(ext_text, config.small_font, col_ext_width - 10)
|
||||
folder_text = truncate_text_end(folder_text, config.small_font, col_folder_width - 10)
|
||||
size_text = truncate_text_end(size_text, config.small_font, col_size_width - 10)
|
||||
@@ -2883,6 +2904,11 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
|
||||
("history", _("controls_action_close_history")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"history_show_folder": [
|
||||
("confirm", _("button_OK")),
|
||||
("clear_history", _("history_move_action")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
],
|
||||
"scraper": [
|
||||
("confirm", _("controls_confirm_select")),
|
||||
("cancel", _("controls_cancel_back")),
|
||||
@@ -2899,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")),
|
||||
@@ -4692,6 +4719,8 @@ def draw_folder_browser(screen):
|
||||
# Titre selon le mode
|
||||
if browser_mode == "roms_root":
|
||||
title = _("folder_browser_title_roms_root") if _ else "Select default ROMs folder"
|
||||
elif browser_mode == "history_move":
|
||||
title = _("folder_browser_title_history_move") if _ else "Select destination folder"
|
||||
else:
|
||||
title = _("folder_browser_title").format(platform_name) if _ else f"Select folder for {platform_name}"
|
||||
title_text = config.font.render(title, True, THEME_COLORS["text"])
|
||||
@@ -4711,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):
|
||||
@@ -5046,6 +5084,12 @@ def draw_history_game_options(screen):
|
||||
dest_folder = _get_dest_folder_name(platform)
|
||||
base_path = os.path.join(config.ROMS_FOLDER, dest_folder)
|
||||
file_exists, actual_filename, actual_path = find_file_with_or_without_extension(base_path, game_name)
|
||||
actual_matches = find_matching_files(base_path, game_name)
|
||||
if not actual_matches:
|
||||
actual_matches = get_existing_history_matches(entry)
|
||||
if actual_matches:
|
||||
actual_filename, actual_path = actual_matches[0]
|
||||
file_exists = True
|
||||
|
||||
# Déterminer les options disponibles selon le statut
|
||||
options = []
|
||||
@@ -5156,6 +5200,7 @@ def draw_history_show_folder(screen):
|
||||
# Utiliser le chemin réel trouvé (avec ou sans extension)
|
||||
actual_path = getattr(config, 'history_actual_path', None)
|
||||
actual_filename = getattr(config, 'history_actual_filename', None)
|
||||
actual_matches = getattr(config, 'history_actual_matches', None) or []
|
||||
|
||||
if not actual_path or not actual_filename:
|
||||
# Fallback si pas trouvé
|
||||
@@ -5164,7 +5209,7 @@ def draw_history_show_folder(screen):
|
||||
actual_filename = game_name
|
||||
|
||||
# Vérifier si le fichier existe
|
||||
file_exists = os.path.exists(actual_path)
|
||||
file_exists = bool(actual_matches) or os.path.exists(actual_path)
|
||||
|
||||
# Message
|
||||
title = _("history_folder_path_label") if _ else "Destination path:"
|
||||
@@ -5175,8 +5220,18 @@ def draw_history_show_folder(screen):
|
||||
margin_top_bottom = 30
|
||||
rect_width = min(config.screen_width - 100, 800)
|
||||
|
||||
# Wrapper le chemin avec la bonne largeur (largeur de la boîte - marges)
|
||||
path_wrapped = wrap_text(actual_path, config.small_font, rect_width - 80)
|
||||
# Wrapper les chemins avec la bonne largeur (largeur de la boîte - marges)
|
||||
if actual_matches:
|
||||
path_wrapped = []
|
||||
for index, (match_filename, match_path) in enumerate(actual_matches, start=1):
|
||||
wrapped_match = wrap_text(match_path, config.small_font, rect_width - 80)
|
||||
if wrapped_match:
|
||||
path_wrapped.append(f"{index}. {wrapped_match[0]}")
|
||||
path_wrapped.extend(wrapped_match[1:])
|
||||
else:
|
||||
path_wrapped.append(f"{index}. {match_path}")
|
||||
else:
|
||||
path_wrapped = wrap_text(actual_path, config.small_font, rect_width - 80)
|
||||
|
||||
# Ajouter un message si le fichier n'existe pas
|
||||
warning_lines = []
|
||||
@@ -5362,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
|
||||
@@ -5438,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",
|
||||
@@ -307,6 +311,7 @@
|
||||
"history_option_delete_game": "Spiel löschen",
|
||||
"history_option_error_info": "Fehlerdetails",
|
||||
"history_option_retry": "Download wiederholen",
|
||||
"history_move_action": "Verschieben",
|
||||
"history_option_back": "Zurück",
|
||||
"history_folder_path_label": "Zielpfad:",
|
||||
"history_scraper_not_implemented": "Scraper noch nicht implementiert",
|
||||
@@ -316,6 +321,8 @@
|
||||
"history_extracted": "Extrahiert",
|
||||
"history_delete_success": "Spiel erfolgreich gelöscht",
|
||||
"history_delete_error": "Fehler beim Löschen des Spiels: {0}",
|
||||
"history_move_success": "{0} Datei(en) verschoben nach: {1}",
|
||||
"history_move_error": "Fehler beim Verschieben: {0}",
|
||||
"history_error_details_title": "Fehlerdetails",
|
||||
"history_no_error_message": "Keine Fehlermeldung verfügbar",
|
||||
"web_title": "RGSX Web-Oberfläche",
|
||||
@@ -480,6 +487,7 @@
|
||||
"platform_folder_set": "Ordner für {0} festgelegt: {1}",
|
||||
"platform_folder_default_path": "Standard: {0}",
|
||||
"folder_browser_title": "Ordner für {0} auswählen",
|
||||
"folder_browser_title_history_move": "Zielordner auswählen",
|
||||
"folder_browser_parent": "Übergeordneter Ordner",
|
||||
"folder_browser_enter": "Öffnen",
|
||||
"folder_browser_select": "Auswählen",
|
||||
|
||||
@@ -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",
|
||||
@@ -306,6 +310,7 @@
|
||||
"history_option_delete_game": "Delete game",
|
||||
"history_option_error_info": "Error details",
|
||||
"history_option_retry": "Retry download",
|
||||
"history_move_action": "Move",
|
||||
"menu_scan_owned_roms": "Scan owned ROMs",
|
||||
"popup_scan_owned_roms_done": "ROM scan complete: {0} games added across {1} platforms",
|
||||
"popup_scan_owned_roms_error": "ROM scan error: {0}",
|
||||
@@ -318,6 +323,8 @@
|
||||
"history_extracted": "Extracted",
|
||||
"history_delete_success": "Game deleted successfully",
|
||||
"history_delete_error": "Error deleting game: {0}",
|
||||
"history_move_success": "Moved {0} file(s) to: {1}",
|
||||
"history_move_error": "Error while moving files: {0}",
|
||||
"history_error_details_title": "Error Details",
|
||||
"history_no_error_message": "No error message available",
|
||||
"web_title": "RGSX Web Interface",
|
||||
@@ -480,6 +487,7 @@
|
||||
"platform_folder_set": "Folder set for {0}: {1}",
|
||||
"platform_folder_default_path": "Default: {0}",
|
||||
"folder_browser_title": "Select folder for {0}",
|
||||
"folder_browser_title_history_move": "Select destination folder",
|
||||
"folder_browser_parent": "Parent folder",
|
||||
"folder_browser_enter": "Enter",
|
||||
"folder_browser_select": "Select",
|
||||
|
||||
@@ -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",
|
||||
@@ -307,6 +311,7 @@
|
||||
"history_option_delete_game": "Eliminar juego",
|
||||
"history_option_error_info": "Detalles del error",
|
||||
"history_option_retry": "Reintentar descarga",
|
||||
"history_move_action": "Mover",
|
||||
"history_option_back": "Volver",
|
||||
"history_folder_path_label": "Ruta de destino:",
|
||||
"history_scraper_not_implemented": "Scraper aún no implementado",
|
||||
@@ -316,6 +321,8 @@
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Juego eliminado con éxito",
|
||||
"history_delete_error": "Error al eliminar juego: {0}",
|
||||
"history_move_success": "{0} archivo(s) movido(s) a: {1}",
|
||||
"history_move_error": "Error al mover los archivos: {0}",
|
||||
"history_error_details_title": "Detalles del error",
|
||||
"history_no_error_message": "No hay mensaje de error disponible",
|
||||
"web_title": "Interfaz Web RGSX",
|
||||
@@ -478,6 +485,7 @@
|
||||
"platform_folder_set": "Carpeta establecida para {0}: {1}",
|
||||
"platform_folder_default_path": "Por defecto: {0}",
|
||||
"folder_browser_title": "Seleccionar carpeta para {0}",
|
||||
"folder_browser_title_history_move": "Seleccionar carpeta de destino",
|
||||
"folder_browser_parent": "Carpeta superior",
|
||||
"folder_browser_enter": "Entrar",
|
||||
"folder_browser_select": "Seleccionar",
|
||||
|
||||
@@ -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",
|
||||
@@ -306,6 +310,7 @@
|
||||
"history_option_delete_game": "Supprimer le jeu",
|
||||
"history_option_error_info": "Détails de l'erreur",
|
||||
"history_option_retry": "Retenter le téléchargement",
|
||||
"history_move_action": "Déplacer",
|
||||
"menu_scan_owned_roms": "Scanner les ROMs présentes",
|
||||
"popup_scan_owned_roms_done": "Scan ROMs terminé : {0} jeux ajoutés sur {1} plateformes",
|
||||
"popup_scan_owned_roms_error": "Erreur scan ROMs : {0}",
|
||||
@@ -318,6 +323,8 @@
|
||||
"history_extracted": "Extrait",
|
||||
"history_delete_success": "Jeu supprimé avec succès",
|
||||
"history_delete_error": "Erreur lors de la suppression du jeu : {0}",
|
||||
"history_move_success": "{0} fichier(s) déplacé(s) vers : {1}",
|
||||
"history_move_error": "Erreur lors du déplacement : {0}",
|
||||
"history_error_details_title": "Détails de l'erreur",
|
||||
"history_no_error_message": "Aucun message d'erreur disponible",
|
||||
"web_title": "Interface Web RGSX",
|
||||
@@ -480,6 +487,7 @@
|
||||
"platform_folder_set": "Dossier défini pour {0}: {1}",
|
||||
"platform_folder_default_path": "Par défaut: {0}",
|
||||
"folder_browser_title": "Sélectionner le dossier pour {0}",
|
||||
"folder_browser_title_history_move": "Sélectionner le dossier de destination",
|
||||
"folder_browser_parent": "Dossier parent",
|
||||
"folder_browser_enter": "Entrer",
|
||||
"folder_browser_select": "Valider",
|
||||
|
||||
@@ -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",
|
||||
@@ -302,6 +306,7 @@
|
||||
"history_option_delete_game": "Elimina gioco",
|
||||
"history_option_error_info": "Dettagli errore",
|
||||
"history_option_retry": "Riprova download",
|
||||
"history_move_action": "Sposta",
|
||||
"history_option_back": "Indietro",
|
||||
"history_folder_path_label": "Percorso destinazione:",
|
||||
"history_scraper_not_implemented": "Scraper non ancora implementato",
|
||||
@@ -311,6 +316,8 @@
|
||||
"history_extracted": "Estratto",
|
||||
"history_delete_success": "Gioco eliminato con successo",
|
||||
"history_delete_error": "Errore durante l'eliminazione del gioco: {0}",
|
||||
"history_move_success": "{0} file spostato/i in: {1}",
|
||||
"history_move_error": "Errore durante lo spostamento: {0}",
|
||||
"history_error_details_title": "Dettagli errore",
|
||||
"history_no_error_message": "Nessun messaggio di errore disponibile",
|
||||
"web_title": "Interfaccia Web RGSX",
|
||||
@@ -476,6 +483,7 @@
|
||||
"platform_folder_set": "Cartella impostata per {0}: {1}",
|
||||
"platform_folder_default_path": "Predefinito: {0}",
|
||||
"folder_browser_title": "Seleziona cartella per {0}",
|
||||
"folder_browser_title_history_move": "Seleziona cartella di destinazione",
|
||||
"folder_browser_parent": "Cartella superiore",
|
||||
"folder_browser_enter": "Entra",
|
||||
"folder_browser_select": "Seleziona",
|
||||
|
||||
@@ -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",
|
||||
@@ -308,6 +312,7 @@
|
||||
"history_option_delete_game": "Excluir jogo",
|
||||
"history_option_error_info": "Detalhes do erro",
|
||||
"history_option_retry": "Tentar novamente",
|
||||
"history_move_action": "Mover",
|
||||
"history_option_back": "Voltar",
|
||||
"history_folder_path_label": "Caminho de destino:",
|
||||
"history_scraper_not_implemented": "Scraper ainda não implementado",
|
||||
@@ -317,6 +322,8 @@
|
||||
"history_extracted": "Extraído",
|
||||
"history_delete_success": "Jogo excluído com sucesso",
|
||||
"history_delete_error": "Erro ao excluir jogo: {0}",
|
||||
"history_move_success": "{0} arquivo(s) movido(s) para: {1}",
|
||||
"history_move_error": "Erro ao mover os arquivos: {0}",
|
||||
"history_error_details_title": "Detalhes do erro",
|
||||
"history_no_error_message": "Nenhuma mensagem de erro disponível",
|
||||
"web_title": "Interface Web RGSX",
|
||||
@@ -480,6 +487,7 @@
|
||||
"platform_folder_set": "Pasta definida para {0}: {1}",
|
||||
"platform_folder_default_path": "Padrão: {0}",
|
||||
"folder_browser_title": "Selecionar pasta para {0}",
|
||||
"folder_browser_title_history_move": "Selecionar pasta de destino",
|
||||
"folder_browser_parent": "Pasta superior",
|
||||
"folder_browser_enter": "Entrar",
|
||||
"folder_browser_select": "Selecionar",
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1243,6 +1252,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
queue_history_entry = {
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Queued',
|
||||
'url': game_url,
|
||||
'progress': 0,
|
||||
@@ -1280,6 +1290,7 @@ class RGSXHandler(BaseHTTPRequestHandler):
|
||||
download_history_entry = {
|
||||
'platform': platform,
|
||||
'game_name': game_name,
|
||||
'display_name': get_clean_display_name(game_name, platform),
|
||||
'status': 'Downloading',
|
||||
'url': game_url,
|
||||
'progress': 0,
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import logging
|
||||
import platform
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import config
|
||||
from config import HEADLESS, Game
|
||||
try:
|
||||
@@ -33,7 +34,37 @@ import tempfile
|
||||
logger = logging.getLogger(__name__)
|
||||
# Désactiver les logs DEBUG de urllib3 e requests pour supprimer les messages de connexion HTTP
|
||||
|
||||
|
||||
def get_clean_display_name(raw_name, platform_id=None):
|
||||
"""Return a user-facing game title from a raw file/path entry."""
|
||||
text = str(raw_name or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
normalized = text.replace("\\", "/")
|
||||
leaf_name = normalized.rsplit("/", 1)[-1]
|
||||
display_name = Path(leaf_name).stem.strip()
|
||||
|
||||
prefixes = []
|
||||
if platform_id:
|
||||
prefixes.append(str(platform_id).strip())
|
||||
platform_label = getattr(config, "platform_names", {}).get(platform_id)
|
||||
if platform_label:
|
||||
prefixes.append(str(platform_label).strip())
|
||||
|
||||
for prefix in prefixes:
|
||||
if not prefix:
|
||||
continue
|
||||
pattern = rf"^{re.escape(prefix)}[\s\-_:]+"
|
||||
updated_name = re.sub(pattern, "", display_name, flags=re.IGNORECASE).strip()
|
||||
if updated_name:
|
||||
display_name = updated_name
|
||||
|
||||
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)
|
||||
@@ -51,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.
|
||||
@@ -1216,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')
|
||||
@@ -1225,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])
|
||||
@@ -1247,8 +1518,7 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
|
||||
games_list: list[Game] = []
|
||||
for name, url, size in normalized:
|
||||
display_name = Path(name).stem
|
||||
display_name = display_name.replace(platform_id, "")
|
||||
display_name = get_clean_display_name(name, platform_id)
|
||||
games_list.append(Game(name=name, url=url, size=size, display_name=display_name))
|
||||
|
||||
_games_cache[platform_id] = {
|
||||
@@ -3035,26 +3305,150 @@ def normalize_platform_name(platform):
|
||||
return platform.lower().replace(" ", "")
|
||||
|
||||
|
||||
def find_matching_files(base_path, filename):
|
||||
"""Return all matching files for a requested download name within a ROM folder."""
|
||||
if not base_path or not os.path.exists(base_path):
|
||||
return []
|
||||
|
||||
candidate_name = Path(str(filename or "")).name
|
||||
requested_stem, requested_ext = os.path.splitext(candidate_name)
|
||||
requested_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', requested_stem)).strip().lower()
|
||||
archive_exts = {'.zip', '.7z', '.rar', '.tar', '.gz', '.xz', '.bz2'}
|
||||
matches = []
|
||||
seen_paths = set()
|
||||
|
||||
full_path = os.path.join(base_path, candidate_name)
|
||||
if os.path.exists(full_path) and os.path.isfile(full_path):
|
||||
seen_paths.add(os.path.normcase(full_path))
|
||||
matches.append((1000, candidate_name, full_path))
|
||||
|
||||
for existing_file in os.listdir(base_path):
|
||||
existing_path = os.path.join(base_path, existing_file)
|
||||
if not os.path.isfile(existing_path):
|
||||
continue
|
||||
|
||||
normalized_path = os.path.normcase(existing_path)
|
||||
if normalized_path in seen_paths:
|
||||
continue
|
||||
|
||||
existing_stem, existing_ext = os.path.splitext(existing_file)
|
||||
score = None
|
||||
|
||||
if requested_stem and existing_stem == requested_stem:
|
||||
score = 900
|
||||
else:
|
||||
existing_normalized = re.sub(r'\s+', ' ', re.sub(r'\s*[\[(][^\])]*[\])]', '', existing_stem)).strip().lower()
|
||||
if requested_normalized and existing_normalized and existing_normalized == requested_normalized:
|
||||
score = 0
|
||||
if requested_ext and existing_ext.lower() == requested_ext.lower():
|
||||
score += 4
|
||||
if existing_ext.lower() not in archive_exts:
|
||||
score += 3
|
||||
score -= abs(len(existing_stem) - len(requested_stem))
|
||||
|
||||
if score is not None:
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((score, existing_file, existing_path))
|
||||
|
||||
matches.sort(key=lambda item: item[0], reverse=True)
|
||||
return [(actual_filename, actual_path) for _, actual_filename, actual_path in matches]
|
||||
|
||||
|
||||
def get_existing_history_matches(entry):
|
||||
"""Return persisted moved paths that still exist for a history entry."""
|
||||
if not isinstance(entry, dict):
|
||||
return []
|
||||
|
||||
moved_paths = entry.get("moved_paths", []) or []
|
||||
matches = []
|
||||
seen_paths = set()
|
||||
|
||||
for raw_path in moved_paths:
|
||||
if not raw_path:
|
||||
continue
|
||||
|
||||
actual_path = os.path.abspath(str(raw_path))
|
||||
normalized_path = os.path.normcase(actual_path)
|
||||
if normalized_path in seen_paths or not os.path.isfile(actual_path):
|
||||
continue
|
||||
|
||||
seen_paths.add(normalized_path)
|
||||
matches.append((os.path.basename(actual_path), actual_path))
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def move_files_to_directory(file_paths, destination_dir):
|
||||
"""Move files to a destination directory, avoiding name collisions."""
|
||||
if not destination_dir:
|
||||
return False, [], "Destination directory is empty"
|
||||
|
||||
if not any(file_paths or []):
|
||||
return False, [], "No files to move"
|
||||
|
||||
try:
|
||||
os.makedirs(destination_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Impossible de créer le dossier de destination {destination_dir}: {e}")
|
||||
return False, [], str(e)
|
||||
|
||||
moved_matches = []
|
||||
seen_sources = set()
|
||||
reserved_targets = set()
|
||||
|
||||
for raw_source in file_paths:
|
||||
if not raw_source:
|
||||
continue
|
||||
|
||||
source_path = os.path.abspath(str(raw_source))
|
||||
normalized_source = os.path.normcase(source_path)
|
||||
if normalized_source in seen_sources:
|
||||
continue
|
||||
seen_sources.add(normalized_source)
|
||||
|
||||
if not os.path.isfile(source_path):
|
||||
error_message = f"File not found: {source_path}"
|
||||
logger.warning(error_message)
|
||||
return False, moved_matches, error_message
|
||||
|
||||
source_name = os.path.basename(source_path)
|
||||
target_path = os.path.join(destination_dir, source_name)
|
||||
target_root, target_ext = os.path.splitext(target_path)
|
||||
suffix = 1
|
||||
|
||||
while os.path.normcase(target_path) in reserved_targets or (
|
||||
os.path.exists(target_path)
|
||||
and os.path.normcase(target_path) != os.path.normcase(source_path)
|
||||
):
|
||||
target_path = f"{target_root} ({suffix}){target_ext}"
|
||||
suffix += 1
|
||||
|
||||
reserved_targets.add(os.path.normcase(target_path))
|
||||
|
||||
try:
|
||||
if os.path.normcase(source_path) != os.path.normcase(target_path):
|
||||
shutil.move(source_path, target_path)
|
||||
logger.info(f"Fichier déplacé: {source_path} -> {target_path}")
|
||||
else:
|
||||
logger.debug(f"Déplacement ignoré, même chemin source/destination: {source_path}")
|
||||
moved_matches.append((os.path.basename(target_path), target_path))
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du déplacement de {source_path} vers {target_path}: {e}")
|
||||
return False, moved_matches, str(e)
|
||||
|
||||
return True, moved_matches, None
|
||||
|
||||
|
||||
def find_file_with_or_without_extension(base_path, filename):
|
||||
"""
|
||||
Cherche un fichier, avec son extension ou sans (cherche jeuxxx.* si jeuxxx.zip n'existe pas).
|
||||
Retourne (file_exists, actual_filename, actual_path).
|
||||
"""
|
||||
# 1. Tester d'abord le fichier tel quel
|
||||
full_path = os.path.join(base_path, filename)
|
||||
if os.path.exists(full_path):
|
||||
return True, filename, full_path
|
||||
|
||||
# 2. Si pas trouvé et que le fichier a une extension, chercher sans extension
|
||||
name_without_ext, ext = os.path.splitext(filename)
|
||||
if ext: # Si le fichier a une extension
|
||||
# Chercher tous les fichiers commençant par le nom sans extension
|
||||
if os.path.exists(base_path):
|
||||
for existing_file in os.listdir(base_path):
|
||||
existing_name, _ = os.path.splitext(existing_file)
|
||||
if existing_name == name_without_ext:
|
||||
found_path = os.path.join(base_path, existing_file)
|
||||
return True, existing_file, found_path
|
||||
|
||||
# 3. Fichier non trouvé
|
||||
return False, filename, full_path
|
||||
candidate_name = Path(str(filename or "")).name
|
||||
full_path = os.path.join(base_path, candidate_name)
|
||||
matches = find_matching_files(base_path, candidate_name)
|
||||
if matches:
|
||||
actual_filename, actual_path = matches[0]
|
||||
return True, actual_filename, actual_path
|
||||
|
||||
return False, candidate_name, full_path
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.1.0"
|
||||
"version": "2.6.1.6.1"
|
||||
}
|
||||
Reference in New Issue
Block a user