Compare commits

..

14 Commits

Author SHA1 Message Date
skymike03
a7dad84108 ## v2.6.1.6.1 (2026.01.02)
- fix update 1fichier error message with free download bacause sometimes the host block download on free mode (only way to bypass is to get an api key / premium account)
2026-04-02 22:47:29 +02:00
skymike03
c9f48d20dd ## v2.6.1.6 (2026.01.02)
- update 1fichier error message with free download bacause sometimes the host block download on free mode (only way to bypass is to get an api key / premium account)
2026-04-02 21:47:38 +02:00
skymike03
cd7795f70e v2.6.1.5.1 (2026.01.02)
- fix some torrent  handling for minerva FUTURE using. Games are available, but download torrent through RGSX is not available for now. So don't ask about dl ps2/ps3/gc/ds games , and if you try to download ,you will have a "maintenance" message
2026-04-02 18:44:55 +02:00
skymike03
6813a0bc3d v2.6.1.5 (2026.04.01)
- test implant torrent handling for minerva support (disabled for now)
2026-04-01 18:34:48 +02:00
skymike03
21b39c66b9 v2.6.1.4 (2026.03.30)
- Add browser-like headers for file downloads with debrids and enhance AllDebrid link handling
2026-03-30 21:12:59 +02:00
skymike03
42b2204aeb Reverted back to original version after test 2026-03-22 12:25:45 +01:00
skymike03
67a38c45aa ## v2.6.3.1.0 TEST (2026.03.22)
- Test discord auto release changelog
- Test
2026-03-22 12:20:46 +01:00
skymike03
893b73ecc5 Refactor Discord changelog notification step in release workflow 2026-03-22 12:09:32 +01:00
skymike03
5e1a684275 Enhance Discord notifications with changelog and bot details 2026-03-22 12:06:06 +01:00
skymike03
9226a818f3 v2.6.3.1 (test update) 2026-03-22 11:56:58 +01:00
skymike03
2fd1bcaf01 test discord 2026-03-22 11:55:58 +01:00
skymike03
875bf8fa23 v2.6.1.3 (2026.03.21)
- add update changelog on start before applying new update
2026-03-21 18:26:39 +01:00
skymike03
f9cbf0196e v2.6.1.2 (2026.03.21)
- added paging navigation on folder browser and full page list
2026-03-21 17:36:06 +01:00
skymike03
eb86d69895 v2.6.1.1 (2026.21.03)
- Improved History/Downloads table readability by giving more space to game titles and using middle truncation for long names
- Cleaned displayed game names to remove platform/path prefixes from titles
- Improved file matching for downloaded and extracted games, including support for filename variants and tag differences
- Updated Locate file to show all matching files instead of only one path
- Added a Move action from the locate screen, using the existing folder browser to move all listed files to a selected destination
- Added collision-safe file moves and persisted moved paths in history
- Added localized labels/messages for the new move flow
- Fixed a startup crash caused by a translation function name conflict
- Fixed navigation after move so OK and Back work correctly from the locate screen
2026-03-21 17:29:39 +01:00
16 changed files with 1998 additions and 658 deletions

View File

@@ -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 }}

View File

@@ -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:

Binary file not shown.

View File

@@ -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

View File

@@ -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):

View File

@@ -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"

View 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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -1,3 +1,3 @@
{
"version": "2.6.1.0"
"version": "2.6.1.6.1"
}