mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-04-07 19:32:55 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b39c66b9 | ||
|
|
42b2204aeb | ||
|
|
67a38c45aa | ||
|
|
893b73ecc5 | ||
|
|
5e1a684275 | ||
|
|
9226a818f3 | ||
|
|
2fd1bcaf01 | ||
|
|
875bf8fa23 | ||
|
|
f9cbf0196e | ||
|
|
eb86d69895 | ||
|
|
b09b3da371 | ||
|
|
0915a90fbe | ||
|
|
3ae3c151eb | ||
|
|
7460b12d71 |
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 }}
|
||||
|
||||
@@ -32,10 +32,10 @@ from display import (
|
||||
draw_display_menu, draw_filter_menu_choice, draw_filter_advanced, draw_filter_priority_config,
|
||||
draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog,
|
||||
draw_confirm_dialog, draw_reload_games_data_dialog, draw_popup, draw_gradient,
|
||||
draw_toast, show_toast, THEME_COLORS
|
||||
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
|
||||
@@ -45,7 +45,7 @@ from utils import (
|
||||
)
|
||||
from history import load_history, save_history, load_downloaded_games
|
||||
from config import OTA_data_ZIP
|
||||
from rgsx_settings import get_sources_mode, get_custom_sources_url, get_sources_zip_url
|
||||
from rgsx_settings import get_sources_mode, get_custom_sources_url, get_sources_zip_url, get_display_fullscreen
|
||||
from accessibility import load_accessibility_settings
|
||||
|
||||
# Configuration du logging
|
||||
@@ -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
|
||||
|
||||
@@ -430,7 +433,7 @@ def stop_web_server():
|
||||
|
||||
# Boucle principale
|
||||
async def main():
|
||||
global current_music, music_files, music_folder, joystick
|
||||
global current_music, music_files, music_folder, joystick, screen
|
||||
logger.debug("Début main")
|
||||
|
||||
# Charger les filtres de jeux sauvegardés
|
||||
@@ -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:
|
||||
@@ -610,6 +617,27 @@ async def main():
|
||||
current_music = play_random_music(music_files, music_folder, current_music)
|
||||
continue
|
||||
|
||||
resize_events = {
|
||||
getattr(pygame, 'VIDEORESIZE', -1),
|
||||
getattr(pygame, 'WINDOWSIZECHANGED', -2),
|
||||
getattr(pygame, 'WINDOWRESIZED', -3),
|
||||
}
|
||||
if event.type in resize_events and not get_display_fullscreen():
|
||||
try:
|
||||
if event.type == getattr(pygame, 'VIDEORESIZE', -1):
|
||||
new_width = max(640, int(getattr(event, 'w', config.screen_width)))
|
||||
new_height = max(360, int(getattr(event, 'h', config.screen_height)))
|
||||
screen = pygame.display.set_mode((new_width, new_height), pygame.RESIZABLE)
|
||||
else:
|
||||
screen = pygame.display.get_surface() or screen
|
||||
|
||||
sync_display_metrics(screen)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Fenêtre redimensionnée: {config.screen_width}x{config.screen_height}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du redimensionnement de la fenêtre: {e}")
|
||||
continue
|
||||
|
||||
if event.type == pygame.QUIT:
|
||||
config.menu_state = "confirm_exit"
|
||||
config.confirm_selection = 0
|
||||
@@ -1336,6 +1364,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")
|
||||
@@ -1343,6 +1375,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:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
import socket
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -26,10 +27,10 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.6.0.1"
|
||||
app_version = "2.6.1.4"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 7
|
||||
GAMELIST_UPDATE_DAYS = 1
|
||||
|
||||
|
||||
def get_application_root():
|
||||
@@ -250,6 +251,30 @@ SYSTEM_INFO = {
|
||||
def get_batocera_system_info():
|
||||
"""Récupère les informations système via la commande batocera-info."""
|
||||
global SYSTEM_INFO
|
||||
|
||||
def get_local_network_ip():
|
||||
try:
|
||||
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
udp_socket.connect(("8.8.8.8", 80))
|
||||
local_ip = udp_socket.getsockname()[0]
|
||||
if local_ip and not local_ip.startswith("127."):
|
||||
return local_ip
|
||||
finally:
|
||||
udp_socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
local_ip = socket.gethostbyname(hostname)
|
||||
if local_ip and not local_ip.startswith("127."):
|
||||
return local_ip
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ""
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['batocera-info'], capture_output=True, text=True, timeout=5)
|
||||
@@ -305,6 +330,7 @@ def get_batocera_system_info():
|
||||
SYSTEM_INFO["system"] = f"{platform.system()} {platform.release()}"
|
||||
SYSTEM_INFO["architecture"] = platform.machine()
|
||||
SYSTEM_INFO["cpu_model"] = platform.processor() or "Unknown"
|
||||
SYSTEM_INFO["network_ip"] = get_local_network_ip()
|
||||
|
||||
return False
|
||||
|
||||
@@ -468,6 +494,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
|
||||
)
|
||||
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
|
||||
@@ -442,12 +443,14 @@ def build_global_search_index() -> list[dict]:
|
||||
platform_id = _get_platform_id(platform)
|
||||
platform_label = _get_platform_label(platform_id)
|
||||
for game in load_games(platform_id):
|
||||
display_name = game.display_name or Path(game.name).stem
|
||||
indexed_games.append({
|
||||
"platform_id": platform_id,
|
||||
"platform_label": platform_label,
|
||||
"platform_index": platform_index,
|
||||
"game_name": game.name,
|
||||
"display_name": game.display_name or Path(game.name).stem,
|
||||
"display_name": display_name,
|
||||
"search_name": display_name.lower(),
|
||||
"url": game.url,
|
||||
"size": game.size,
|
||||
})
|
||||
@@ -463,7 +466,7 @@ def refresh_global_search_results(reset_selection: bool = True) -> None:
|
||||
else:
|
||||
config.global_search_results = [
|
||||
item for item in config.global_search_index
|
||||
if query in item["display_name"].lower()
|
||||
if query in item.get("search_name", item["display_name"].lower())
|
||||
]
|
||||
|
||||
if reset_selection:
|
||||
@@ -476,7 +479,10 @@ def refresh_global_search_results(reset_selection: bool = True) -> None:
|
||||
|
||||
|
||||
def enter_global_search() -> None:
|
||||
config.global_search_index = build_global_search_index()
|
||||
index_signature = tuple(config.platforms)
|
||||
if not getattr(config, 'global_search_index', None) or getattr(config, 'global_search_index_signature', None) != index_signature:
|
||||
config.global_search_index = build_global_search_index()
|
||||
config.global_search_index_signature = index_signature
|
||||
config.global_search_query = ""
|
||||
config.global_search_results = []
|
||||
config.global_search_selected = 0
|
||||
@@ -551,7 +557,7 @@ 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:
|
||||
logger.error(f"Resultat de recherche globale invalide: {result}")
|
||||
@@ -597,6 +603,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,
|
||||
@@ -1171,6 +1178,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,
|
||||
@@ -1243,6 +1251,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,
|
||||
@@ -1483,6 +1492,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
|
||||
@@ -1742,7 +1758,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
|
||||
|
||||
@@ -1772,60 +1828,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', '')
|
||||
@@ -1856,6 +1858,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)
|
||||
@@ -1885,7 +1888,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:
|
||||
@@ -2212,11 +2219,27 @@ def handle_controls(event, sources, joystick, screen):
|
||||
# Sous-menu Display
|
||||
elif config.menu_state == "pause_display_menu":
|
||||
sel = getattr(config, 'pause_display_selection', 0)
|
||||
# layout, font submenu, family, [monitor if multi], light, unknown, back
|
||||
# layout, font submenu, family, [monitor if multi], [display mode on Windows], light, unknown, back
|
||||
from rgsx_settings import get_available_monitors
|
||||
monitors = get_available_monitors()
|
||||
show_monitor = len(monitors) > 1
|
||||
total = 7 if show_monitor else 6 # dynamic total based on monitor count
|
||||
show_display_mode = getattr(config, 'OPERATING_SYSTEM', '') == "Windows"
|
||||
|
||||
monitor_index = 3 if show_monitor else None
|
||||
display_mode_index = 4 if show_monitor else 3
|
||||
if not show_display_mode:
|
||||
display_mode_index = None
|
||||
|
||||
next_index = 3
|
||||
if show_monitor:
|
||||
next_index += 1
|
||||
if show_display_mode:
|
||||
next_index += 1
|
||||
|
||||
light_index = next_index
|
||||
unknown_index = light_index + 1
|
||||
back_index = unknown_index + 1
|
||||
total = back_index + 1
|
||||
if is_input_matched(event, "up"):
|
||||
config.pause_display_selection = (sel - 1) % total
|
||||
config.needs_redraw = True
|
||||
@@ -2274,8 +2297,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement font family: {e}")
|
||||
# 3 monitor selection (only if multiple monitors)
|
||||
elif sel == 3 and show_monitor and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# Monitor selection (only if multiple monitors)
|
||||
elif monitor_index is not None and sel == monitor_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_display_monitor, set_display_monitor
|
||||
current = get_display_monitor()
|
||||
@@ -2286,8 +2309,19 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur changement moniteur: {e}")
|
||||
# light mode toggle (index 4 if show_monitor, else 3)
|
||||
elif ((sel == 4 and show_monitor) or (sel == 3 and not show_monitor)) and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# Display mode toggle (Windows only)
|
||||
elif display_mode_index is not None and sel == display_mode_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_display_fullscreen, set_display_fullscreen
|
||||
current = get_display_fullscreen()
|
||||
set_display_fullscreen(not current)
|
||||
config.popup_message = _("display_mode_restart_required") if _ else "Restart required to apply screen mode"
|
||||
config.popup_timer = 3000
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle fullscreen/windowed: {e}")
|
||||
# Light mode toggle
|
||||
elif sel == light_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
from rgsx_settings import get_light_mode, set_light_mode
|
||||
current = get_light_mode()
|
||||
@@ -2297,8 +2331,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle light mode: {e}")
|
||||
# allow unknown extensions (index 5 if show_monitor, else 4)
|
||||
elif ((sel == 5 and show_monitor) or (sel == 4 and not show_monitor)) and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
# Allow unknown extensions
|
||||
elif sel == unknown_index and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")):
|
||||
try:
|
||||
current = get_allow_unknown_extensions()
|
||||
new_val = set_allow_unknown_extensions(not current)
|
||||
@@ -2307,8 +2341,8 @@ def handle_controls(event, sources, joystick, screen):
|
||||
config.needs_redraw = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur toggle allow_unknown_extensions: {e}")
|
||||
# back (index 6 if show_monitor, else 5)
|
||||
elif ((sel == 6 and show_monitor) or (sel == 5 and not show_monitor)) and is_input_matched(event, "confirm"):
|
||||
# Back
|
||||
elif sel == back_index and is_input_matched(event, "confirm"):
|
||||
config.menu_state = "pause_menu"
|
||||
config.last_state_change_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
@@ -2916,6 +2950,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]
|
||||
@@ -2969,6 +3022,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
|
||||
@@ -2984,6 +3065,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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import config
|
||||
from datetime import datetime
|
||||
|
||||
@@ -168,7 +169,7 @@ IGNORED_ROM_SCAN_EXTENSIONS = {
|
||||
|
||||
|
||||
def normalize_downloaded_game_name(game_name):
|
||||
"""Normalise un nom de jeu pour les comparaisons en ignorant l'extension."""
|
||||
"""Normalise un nom de jeu pour les comparaisons en ignorant extension et tags."""
|
||||
if not isinstance(game_name, str):
|
||||
return ""
|
||||
|
||||
@@ -176,7 +177,10 @@ def normalize_downloaded_game_name(game_name):
|
||||
if not normalized:
|
||||
return ""
|
||||
|
||||
return os.path.splitext(normalized)[0].strip().lower()
|
||||
normalized = os.path.splitext(normalized)[0]
|
||||
normalized = re.sub(r'\s*[\[(][^\])]*[\])]', '', normalized)
|
||||
normalized = re.sub(r'\s+', ' ', normalized)
|
||||
return normalized.strip().lower()
|
||||
|
||||
|
||||
def _normalize_downloaded_games_dict(downloaded):
|
||||
|
||||
@@ -307,6 +307,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 +317,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 +483,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",
|
||||
|
||||
@@ -306,6 +306,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 +319,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 +483,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",
|
||||
|
||||
@@ -307,6 +307,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 +317,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 +481,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",
|
||||
|
||||
@@ -306,6 +306,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 +319,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 +483,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",
|
||||
|
||||
@@ -302,6 +302,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 +312,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 +479,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",
|
||||
|
||||
@@ -308,6 +308,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 +318,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 +483,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",
|
||||
|
||||
@@ -15,7 +15,7 @@ try:
|
||||
except Exception:
|
||||
pygame = None # type: ignore
|
||||
from config import OTA_VERSION_ENDPOINT,APP_FOLDER, UPDATE_FOLDER, OTA_UPDATE_ZIP
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie
|
||||
from utils import sanitize_filename, extract_zip, extract_rar, extract_7z, load_api_key_1fichier, load_api_key_alldebrid, normalize_platform_name, load_api_keys, load_archive_org_cookie, get_clean_display_name
|
||||
from history import save_history
|
||||
from display import show_toast
|
||||
import logging
|
||||
@@ -39,6 +39,21 @@ import urllib.parse
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_browser_download_headers(referer: str | None = None, accept: str = 'application/octet-stream,*/*;q=0.8') -> dict:
|
||||
"""Build browser-like headers for file downloads that reject minimal clients."""
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
'Accept': accept,
|
||||
'Accept-Language': 'en-US,en;q=0.9,fr;q=0.8',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'DNT': '1',
|
||||
}
|
||||
if referer:
|
||||
headers['Referer'] = referer
|
||||
return headers
|
||||
|
||||
|
||||
def _redact_headers(headers: dict) -> dict:
|
||||
"""Return a copy of headers with sensitive fields redacted for logs."""
|
||||
if not isinstance(headers, dict):
|
||||
@@ -479,6 +494,211 @@ def test_internet():
|
||||
return True
|
||||
|
||||
|
||||
def _normalize_release_notes(raw_notes):
|
||||
if not raw_notes:
|
||||
return ""
|
||||
notes = html_module.unescape(str(raw_notes))
|
||||
notes = notes.replace("\r\n", "\n").replace("\r", "\n")
|
||||
notes = re.sub(r"\n{3,}", "\n\n", notes)
|
||||
return notes.strip()
|
||||
|
||||
|
||||
def _extract_changelog_section(raw_text):
|
||||
text = _normalize_release_notes(raw_text)
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
lines = text.split("\n")
|
||||
heading_re = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
|
||||
changelog_start = None
|
||||
changelog_level = None
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
match = heading_re.match(line.strip())
|
||||
if not match:
|
||||
continue
|
||||
if "changelog" in match.group(2).lower():
|
||||
changelog_start = index + 1
|
||||
changelog_level = len(match.group(1))
|
||||
break
|
||||
|
||||
if changelog_start is None:
|
||||
return text
|
||||
|
||||
extracted = []
|
||||
for line in lines[changelog_start:]:
|
||||
stripped = line.strip()
|
||||
if stripped == "---":
|
||||
break
|
||||
match = heading_re.match(stripped)
|
||||
if match and len(match.group(1)) <= changelog_level:
|
||||
break
|
||||
extracted.append(line)
|
||||
|
||||
return _normalize_release_notes("\n".join(extracted))
|
||||
|
||||
|
||||
def _fetch_recent_release_changelogs(limit=5):
|
||||
repo = getattr(config, "GITHUB_REPO", "RetroGameSets/RGSX")
|
||||
api_url = f"https://api.github.com/repos/{repo}/releases"
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "RGSX",
|
||||
}
|
||||
|
||||
response = requests.get(api_url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
releases = response.json()
|
||||
if not isinstance(releases, list):
|
||||
return []
|
||||
|
||||
changelogs = []
|
||||
for release in releases:
|
||||
if not isinstance(release, dict) or release.get("draft"):
|
||||
continue
|
||||
|
||||
version_label = (
|
||||
release.get("tag_name")
|
||||
or release.get("name")
|
||||
or release.get("published_at")
|
||||
or "Unknown"
|
||||
)
|
||||
release_body = _extract_changelog_section(release.get("body", ""))
|
||||
if not release_body:
|
||||
release_body = "Changelog unavailable"
|
||||
|
||||
changelogs.append({
|
||||
"version": str(version_label).strip(),
|
||||
"body": release_body,
|
||||
})
|
||||
|
||||
if len(changelogs) >= limit:
|
||||
break
|
||||
|
||||
return changelogs
|
||||
|
||||
|
||||
def _build_recent_changelog_text(latest_version, limit=5):
|
||||
changelogs = _fetch_recent_release_changelogs(limit=limit)
|
||||
title = _("network_update_available").format(latest_version) if _ else f"Update available: {latest_version}"
|
||||
intro = f"{title}\n\nLast {len(changelogs) if changelogs else limit} changelogs:\n"
|
||||
|
||||
if not changelogs:
|
||||
return f"{intro}\nChangelog unavailable"
|
||||
|
||||
blocks = []
|
||||
for item in changelogs:
|
||||
blocks.append(f"=== {item['version']} ===\n{item['body']}")
|
||||
|
||||
return intro + "\n\n".join(blocks) + "\n\nPress Confirm to install the update."
|
||||
|
||||
|
||||
def _format_size(num_bytes):
|
||||
value = float(max(0, num_bytes))
|
||||
units = ["B", "KB", "MB", "GB"]
|
||||
unit_index = 0
|
||||
while value >= 1024 and unit_index < len(units) - 1:
|
||||
value /= 1024.0
|
||||
unit_index += 1
|
||||
if unit_index == 0:
|
||||
return f"{int(value)} {units[unit_index]}"
|
||||
return f"{value:.1f} {units[unit_index]}"
|
||||
|
||||
|
||||
def _set_loading_details(*lines):
|
||||
config.loading_detail_lines = [str(line) for line in lines if line]
|
||||
config.needs_redraw = True
|
||||
|
||||
|
||||
def _safe_remove_file(file_path, retries=8, delay=0.25):
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return True
|
||||
|
||||
last_error = None
|
||||
for _ in range(retries):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
return True
|
||||
except PermissionError as error:
|
||||
last_error = error
|
||||
time.sleep(delay)
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
except Exception as error:
|
||||
last_error = error
|
||||
break
|
||||
|
||||
if last_error is not None:
|
||||
logger.warning(f"Impossible de supprimer temporairement {file_path}: {last_error}")
|
||||
return False
|
||||
|
||||
|
||||
async def apply_pending_update(latest_version):
|
||||
UPDATE_ZIP = OTA_UPDATE_ZIP
|
||||
logger.debug(f"URL de mise à jour : {UPDATE_ZIP} (version {latest_version})")
|
||||
|
||||
config.current_loading_system = _("network_update_available").format(latest_version)
|
||||
config.loading_progress = 10.0
|
||||
_set_loading_details("Preparing update...")
|
||||
logger.debug(f"Téléchargement du ZIP de mise à jour : {UPDATE_ZIP}")
|
||||
|
||||
os.makedirs(UPDATE_FOLDER, exist_ok=True)
|
||||
update_zip_path = os.path.join(UPDATE_FOLDER, f"RGSX_update_v{latest_version}.zip")
|
||||
logger.debug(f"Téléchargement de {UPDATE_ZIP} vers {update_zip_path}")
|
||||
|
||||
with requests.get(UPDATE_ZIP, stream=True, timeout=10) as r:
|
||||
r.raise_for_status()
|
||||
total_size = int(r.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
with open(update_zip_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
config.loading_progress = 10.0 + (40.0 * downloaded / total_size) if total_size > 0 else 10.0
|
||||
elapsed = max(time.time() - start_time, 0.001)
|
||||
speed = downloaded / elapsed
|
||||
progress_line = f"Download: {_format_size(downloaded)} / {_format_size(total_size)}" if total_size > 0 else f"Download: {_format_size(downloaded)}"
|
||||
_set_loading_details(progress_line, f"Speed: {_format_size(speed)}/s")
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {update_zip_path}")
|
||||
|
||||
config.current_loading_system = _("network_extracting_update")
|
||||
config.loading_progress = 60.0
|
||||
_set_loading_details(f"Archive: {_format_size(os.path.getsize(update_zip_path))}")
|
||||
success, message = await asyncio.to_thread(extract_update, update_zip_path, APP_FOLDER, UPDATE_ZIP)
|
||||
if not success:
|
||||
logger.error(f"Échec de l'extraction : {message}")
|
||||
return False, _("network_extraction_failed").format(message)
|
||||
|
||||
if _safe_remove_file(update_zip_path):
|
||||
logger.debug(f"Fichier ZIP {update_zip_path} supprimé")
|
||||
|
||||
config.current_loading_system = _("network_update_completed")
|
||||
config.loading_progress = 100.0
|
||||
_set_loading_details("Update installed successfully")
|
||||
logger.debug("Mise à jour terminée avec succès")
|
||||
|
||||
config.menu_state = "restart_popup"
|
||||
config.update_result_message = _("network_update_success").format(latest_version)
|
||||
config.popup_message = config.update_result_message
|
||||
config.popup_timer = 2000
|
||||
config.update_result_error = False
|
||||
config.update_result_start_time = pygame.time.get_ticks() if pygame is not None else 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Affichage de la popup de mise à jour réussie, redémarrage imminent")
|
||||
|
||||
try:
|
||||
from utils import restart_application
|
||||
restart_application(2000)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du redémarrage après mise à jour: {e}")
|
||||
|
||||
return True, _("network_update_success_message")
|
||||
|
||||
|
||||
async def check_for_updates():
|
||||
try:
|
||||
logger.debug("Vérification de la version disponible sur le serveur")
|
||||
@@ -614,75 +834,27 @@ async def check_for_updates():
|
||||
logger.info("Version distante inférieure ou égale – skip mise à jour (anti-downgrade)")
|
||||
return True, _("network_no_update_available") if _ else "No update (local >= remote)"
|
||||
|
||||
# À ce stade latest_version est strictement > version locale
|
||||
# Utiliser l'URL RGSX_latest.zip qui pointe toujours vers la dernière version sur GitHub
|
||||
UPDATE_ZIP = OTA_UPDATE_ZIP
|
||||
logger.debug(f"URL de mise à jour : {UPDATE_ZIP} (version {latest_version})")
|
||||
|
||||
if latest_version != config.app_version:
|
||||
config.current_loading_system = _("network_update_available").format(latest_version)
|
||||
config.loading_progress = 10.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Téléchargement du ZIP de mise à jour : {UPDATE_ZIP}")
|
||||
|
||||
# Créer le dossier UPDATE_FOLDER s'il n'existe pas
|
||||
os.makedirs(UPDATE_FOLDER, exist_ok=True)
|
||||
update_zip_path = os.path.join(UPDATE_FOLDER, f"RGSX_update_v{latest_version}.zip")
|
||||
logger.debug(f"Téléchargement de {UPDATE_ZIP} vers {update_zip_path}")
|
||||
|
||||
# Télécharger le ZIP
|
||||
with requests.get(UPDATE_ZIP, stream=True, timeout=10) as r:
|
||||
r.raise_for_status()
|
||||
total_size = int(r.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
with open(update_zip_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
config.loading_progress = 10.0 + (40.0 * downloaded / total_size) if total_size > 0 else 10.0
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {update_zip_path}")
|
||||
|
||||
# Extraire le contenu du ZIP dans APP_FOLDER
|
||||
config.current_loading_system = _("network_extracting_update")
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
success, message = extract_update(update_zip_path, APP_FOLDER, UPDATE_ZIP)
|
||||
if not success:
|
||||
logger.error(f"Échec de l'extraction : {message}")
|
||||
return False, _("network_extraction_failed").format(message)
|
||||
|
||||
# Supprimer le fichier ZIP après extraction
|
||||
if os.path.exists(update_zip_path):
|
||||
os.remove(update_zip_path)
|
||||
logger.debug(f"Fichier ZIP {update_zip_path} supprimé")
|
||||
|
||||
config.current_loading_system = _("network_update_completed")
|
||||
config.loading_progress = 100.0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Mise à jour terminée avec succès")
|
||||
|
||||
# Configurer la popup puis redémarrer automatiquement
|
||||
config.menu_state = "restart_popup"
|
||||
config.update_result_message = _("network_update_success").format(latest_version)
|
||||
config.popup_message = config.update_result_message
|
||||
config.popup_timer = 2000
|
||||
config.update_result_error = False
|
||||
config.update_result_start_time = pygame.time.get_ticks() if pygame is not None else 0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Affichage de la popup de mise à jour réussie, redémarrage imminent")
|
||||
|
||||
try:
|
||||
from utils import restart_application
|
||||
restart_application(2000)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du redémarrage après mise à jour: {e}")
|
||||
changelog_text = _build_recent_changelog_text(latest_version, limit=5)
|
||||
except Exception as changelog_error:
|
||||
logger.warning(f"Impossible de récupérer les changelogs récents: {changelog_error}")
|
||||
changelog_text = "Changelog unavailable"
|
||||
|
||||
return True, _("network_update_success_message")
|
||||
config.pending_update_version = latest_version
|
||||
config.startup_update_confirmed = False
|
||||
config.text_file_name = f"RGSX {latest_version}"
|
||||
config.text_file_content = changelog_text
|
||||
config.text_file_scroll_offset = 0
|
||||
config.text_file_mode = "ota_update"
|
||||
config.previous_menu_state = "loading"
|
||||
config.menu_state = "text_file_viewer"
|
||||
config.update_checked = True
|
||||
config.needs_redraw = True
|
||||
return True, _("network_update_available").format(latest_version)
|
||||
else:
|
||||
logger.debug("Aucune mise à jour disponible")
|
||||
config.update_checked = True
|
||||
return True, _("network_no_update_available")
|
||||
|
||||
except Exception as e:
|
||||
@@ -704,9 +876,20 @@ def extract_update(zip_path, dest_dir, source_url):
|
||||
# Extraire le ZIP
|
||||
skipped_files = []
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
file_infos = [info for info in zip_ref.infolist() if not info.is_dir()]
|
||||
total_bytes = sum(max(0, info.file_size) for info in file_infos)
|
||||
extracted_bytes = 0
|
||||
for file_info in zip_ref.infolist():
|
||||
try:
|
||||
zip_ref.extract(file_info, dest_dir)
|
||||
if not file_info.is_dir():
|
||||
extracted_bytes += max(0, file_info.file_size)
|
||||
if total_bytes > 0:
|
||||
config.loading_progress = 60.0 + (40.0 * extracted_bytes / total_bytes)
|
||||
_set_loading_details(
|
||||
f"Extracting: {_format_size(extracted_bytes)} / {_format_size(total_bytes)}" if total_bytes > 0 else f"Extracting: {file_info.filename}",
|
||||
file_info.filename
|
||||
)
|
||||
except PermissionError as e:
|
||||
logger.warning(f"Impossible d'extraire {file_info.filename}: {str(e)}")
|
||||
skipped_files.append(file_info.filename)
|
||||
@@ -874,6 +1057,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
entry["downloaded_size"] = 0
|
||||
entry["platform"] = platform
|
||||
entry["game_name"] = game_name
|
||||
entry["display_name"] = get_clean_display_name(game_name, platform)
|
||||
entry["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
entry["task_id"] = task_id
|
||||
break
|
||||
@@ -883,6 +1067,7 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas
|
||||
config.history.append({
|
||||
"platform": platform,
|
||||
"game_name": game_name,
|
||||
"display_name": get_clean_display_name(game_name, platform),
|
||||
"url": url,
|
||||
"status": "Downloading",
|
||||
"progress": 0,
|
||||
@@ -1743,6 +1928,25 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}")
|
||||
# Assurer l'accès à provider_used dans cette closure (lecture/écriture)
|
||||
nonlocal provider_used
|
||||
|
||||
def _refresh_alldebrid_final_url(current_link):
|
||||
"""Request a fresh AllDebrid unlock URL after transient download failures."""
|
||||
ad_key = getattr(config, 'API_KEY_ALLDEBRID', '')
|
||||
if not ad_key:
|
||||
return None, None
|
||||
params = {'agent': 'RGSX', 'apikey': ad_key, 'link': current_link}
|
||||
refresh_resp = requests.get("https://api.alldebrid.com/v4/link/unlock", params=params, timeout=30)
|
||||
refresh_resp.raise_for_status()
|
||||
refresh_json = refresh_resp.json()
|
||||
if refresh_json.get('status') != 'success':
|
||||
logger.warning(f"AllDebrid refresh status != success: {refresh_json}")
|
||||
return None, None
|
||||
refresh_data = refresh_json.get('data', {})
|
||||
return (
|
||||
refresh_data.get('link') or refresh_data.get('download') or refresh_data.get('streamingLink'),
|
||||
refresh_data.get('filename'),
|
||||
)
|
||||
|
||||
try:
|
||||
cancel_ev = cancel_events.get(task_id)
|
||||
link = url.split('&af=')[0]
|
||||
@@ -1766,6 +1970,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
entry["downloaded_size"] = 0
|
||||
entry["platform"] = platform
|
||||
entry["game_name"] = game_name
|
||||
entry["display_name"] = get_clean_display_name(game_name, platform)
|
||||
entry["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
entry["task_id"] = task_id
|
||||
break
|
||||
@@ -1775,6 +1980,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
config.history.append({
|
||||
"platform": platform,
|
||||
"game_name": game_name,
|
||||
"display_name": get_clean_display_name(game_name, platform),
|
||||
"url": url,
|
||||
"status": "Downloading",
|
||||
"progress": 0,
|
||||
@@ -2380,17 +2586,23 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
filename = game_name
|
||||
sanitized_filename = sanitize_filename(filename)
|
||||
dest_path = os.path.join(dest_dir, sanitized_filename)
|
||||
|
||||
provider_download_session = requests.Session()
|
||||
provider_download_headers = _build_browser_download_headers()
|
||||
provider_download_session.headers.update(provider_download_headers)
|
||||
|
||||
# Essayer de récupérer la taille du serveur via HEAD request
|
||||
remote_size = None
|
||||
try:
|
||||
if final_url:
|
||||
head_response = requests.head(final_url, timeout=10, allow_redirects=True)
|
||||
if final_url and provider_used not in {'AD', 'DL', 'RD'}:
|
||||
head_response = provider_download_session.head(final_url, timeout=10, allow_redirects=True)
|
||||
if head_response.status_code == 200:
|
||||
content_length = head_response.headers.get('content-length')
|
||||
if content_length:
|
||||
remote_size = int(content_length)
|
||||
logger.debug(f"Taille du fichier serveur (AllDebrid/Debrid-Link/RealDebrid): {remote_size} octets")
|
||||
elif final_url:
|
||||
logger.debug(f"Saut du HEAD préliminaire pour provider {provider_used}: URL temporaire potentiellement sensible ({final_url})")
|
||||
except Exception as e:
|
||||
logger.debug(f"Impossible de vérifier la taille serveur (AllDebrid/Debrid-Link/RealDebrid): {e}")
|
||||
|
||||
@@ -2478,13 +2690,39 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
|
||||
lock = threading.Lock()
|
||||
retries = 10
|
||||
retry_delay = 10
|
||||
download_header_variants = [
|
||||
provider_download_headers,
|
||||
_build_browser_download_headers(accept='*/*'),
|
||||
{
|
||||
'User-Agent': 'curl/8.4.0',
|
||||
'Accept': '*/*',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
]
|
||||
logger.debug(f"Initialisation progression avec taille inconnue pour task_id={task_id}")
|
||||
progress_queues[task_id].put((task_id, 0, 0)) # Taille initiale inconnue
|
||||
for attempt in range(retries):
|
||||
logger.debug(f"Début tentative {attempt + 1} pour télécharger {final_url}")
|
||||
try:
|
||||
with requests.get(final_url, stream=True, headers={'User-Agent': 'Mozilla/5.0'}, timeout=30) as response:
|
||||
attempt_headers = download_header_variants[min(attempt, len(download_header_variants) - 1)]
|
||||
logger.debug(f"Headers tentative {attempt + 1}: {_redact_headers(attempt_headers)}")
|
||||
with provider_download_session.get(final_url, stream=True, headers=attempt_headers, timeout=(30, 120), allow_redirects=True) as response:
|
||||
logger.debug(f"Réponse GET reçue, code: {response.status_code}")
|
||||
if response.status_code == 503 and provider_used == 'AD' and attempt < retries - 1:
|
||||
logger.warning("AllDebrid a renvoyé 503 sur l'URL débridée, tentative de régénération du lien")
|
||||
try:
|
||||
refreshed_url, refreshed_filename = _refresh_alldebrid_final_url(link)
|
||||
if refreshed_url:
|
||||
if refreshed_url != final_url:
|
||||
logger.debug(f"Nouvelle URL AllDebrid obtenue: {refreshed_url}")
|
||||
final_url = refreshed_url
|
||||
if refreshed_filename:
|
||||
filename = refreshed_filename
|
||||
sanitized_filename = sanitize_filename(filename)
|
||||
dest_path = os.path.join(dest_dir, sanitized_filename)
|
||||
except Exception as refresh_error:
|
||||
logger.warning(f"Impossible de régénérer le lien AllDebrid après 503: {refresh_error}")
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale: {total_size} octets")
|
||||
|
||||
@@ -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
|
||||
from network import download_rom, download_from_1fichier
|
||||
from pathlib import Path
|
||||
from rgsx_settings import get_language
|
||||
@@ -1243,6 +1243,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 +1281,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,
|
||||
|
||||
@@ -33,6 +33,36 @@ 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 = {}
|
||||
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
|
||||
@@ -1195,9 +1225,15 @@ def load_games(platform_id:str) -> list[Game]:
|
||||
game_file = c
|
||||
break
|
||||
if not game_file:
|
||||
_games_cache.pop(platform_id, None)
|
||||
logger.warning(f"Aucun fichier de jeux trouvé pour {platform_id} (candidats: {candidates})")
|
||||
return []
|
||||
|
||||
game_mtime_ns = os.stat(game_file).st_mtime_ns
|
||||
cached_entry = _games_cache.get(platform_id)
|
||||
if cached_entry and cached_entry.get("path") == game_file and cached_entry.get("mtime_ns") == game_mtime_ns:
|
||||
return cached_entry["games"]
|
||||
|
||||
with open(game_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
@@ -1239,11 +1275,17 @@ 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] = {
|
||||
"path": game_file,
|
||||
"mtime_ns": game_mtime_ns,
|
||||
"games": games_list,
|
||||
}
|
||||
return games_list
|
||||
except Exception as e:
|
||||
_games_cache.pop(platform_id, None)
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")
|
||||
return []
|
||||
|
||||
@@ -3020,26 +3062,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
|
||||
|
||||
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.0.1"
|
||||
"version": "2.6.1.4"
|
||||
}
|
||||
Reference in New Issue
Block a user