Compare commits

...

4 Commits

Author SHA1 Message Date
skymike03
ce39722351 ## v2.6.1.7 (2026.01.02)
- correct version heading check to changelog extraction logic
2026-04-02 22:56:16 +02:00
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
12 changed files with 201 additions and 24 deletions

View File

@@ -27,7 +27,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.6.1.5"
app_version = "2.6.1.7"
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
GAMELIST_UPDATE_DAYS = 1

View File

@@ -19,7 +19,7 @@ from utils import (
restart_application, generate_support_zip, load_sources,
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
start_connection_status_check, get_clean_display_name, get_existing_history_matches,
move_files_to_directory
move_files_to_directory, parse_torrent_download_url
)
from history import load_history, clear_history, add_to_history, save_history, scan_roms_for_downloaded_games
from language import _, get_available_languages, set_language
@@ -52,6 +52,10 @@ def _notify_torrent_in_maintenance(game_name: str | None = None) -> None:
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)
@@ -1762,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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ import html as html_module
from urllib.parse import urljoin, unquote
import urllib.parse
import tempfile
import unicodedata
@@ -383,6 +384,92 @@ def extract_wait_seconds_1f(html_text):
return seconds
return 0
def _extract_visible_text_from_html(html_text: str) -> str:
if not html_text:
return ""
text = re.sub(r'(?is)<script[^>]*>.*?</script>', ' ', html_text)
text = re.sub(r'(?is)<style[^>]*>.*?</style>', ' ', text)
text = re.sub(r'(?is)<[^>]+>', ' ', text)
text = html_module.unescape(text).replace('\xa0', ' ')
return re.sub(r'\s+', ' ', text).strip()
def _normalize_1fichier_text(text: str) -> str:
if not text:
return ""
normalized = unicodedata.normalize("NFKD", text)
normalized = normalized.encode("ascii", "ignore").decode("ascii")
return re.sub(r'\s+', ' ', normalized).strip().lower()
def _translate_free_mode_message(key: str, fallback: str) -> str:
try:
translated = _(key)
if translated and translated != key:
return translated
except Exception:
pass
return fallback
def _append_1fichier_upgrade_advice(message: str) -> str:
advice = _translate_free_mode_message(
"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.",
)
base_message = (message or "").strip()
if not base_message:
return advice
return f"{base_message}\n{advice}"
def _extract_1fichier_free_mode_block_reason(html_text: str) -> str | None:
visible_text = _extract_visible_text_from_html(html_text)
normalized = _normalize_1fichier_text(visible_text)
if not normalized:
return None
if (
"telechargement gratuit est temporairement limite" in normalized
and "identifiez-vous immediatement" in normalized
):
return _append_1fichier_upgrade_advice(
_translate_free_mode_message(
"free_mode_guest_slots_unavailable",
"1fichier: free guest download is temporarily unavailable (all slots are currently in use). Please try again later.",
)
)
if (
"free download is temporarily limited" in normalized
and "all free slots for guests are currently used" in normalized
):
return _append_1fichier_upgrade_advice(
_translate_free_mode_message(
"free_mode_guest_slots_unavailable",
"1fichier: free guest download is temporarily unavailable (all slots are currently in use). Please try again later.",
)
)
if "identifiez-vous immediatement pour continuer votre telechargement" in normalized:
return _append_1fichier_upgrade_advice(
_translate_free_mode_message(
"free_mode_unavailable_in_app",
"1fichier: this download is not available in the application right now. Please try again later.",
)
)
if "sign in immediately to continue your download" in normalized:
return _append_1fichier_upgrade_advice(
_translate_free_mode_message(
"free_mode_unavailable_in_app",
"1fichier: this download is not available in the application right now. Please try again later.",
)
)
return None
def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progress_callback=None, wait_callback=None, cancel_event=None):
"""
Télécharge un fichier depuis 1fichier.com en mode gratuit (sans API key).
@@ -434,6 +521,7 @@ def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progr
r = session.get(url, allow_redirects=True, timeout=30)
r.raise_for_status()
html = r.text
page_url = str(r.url)
# 2. Détection compte à rebours
wait_s = extract_wait_seconds_1f(html)
@@ -462,8 +550,12 @@ def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progr
name_m = re.search(r'name=[\"\']([^\"\']+)', inp)
value_m = re.search(r'value=[\"\']([^\"\']*)', inp)
type_m = re.search(r'type=["\']([^"\']+)', inp, re.IGNORECASE)
if name_m:
input_type = type_m.group(1).strip().lower() if type_m else 'text'
if input_type in {'checkbox', 'radio'} and 'checked' not in inp.lower():
continue
name = name_m.group(1)
value = value_m.group(1) if value_m else ''
data[name] = html_module.unescape(value)
@@ -478,9 +570,15 @@ def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progr
while post_attempt < max_post_attempts:
post_attempt += 1
try:
r2 = session.post(str(r.url), data=data, allow_redirects=True, timeout=30)
parsed_page = urllib.parse.urlparse(page_url)
post_headers = {
'Referer': page_url,
'Origin': f"{parsed_page.scheme}://{parsed_page.netloc}" if parsed_page.scheme and parsed_page.netloc else page_url,
}
r2 = session.post(page_url, data=data, headers=post_headers, allow_redirects=True, timeout=30)
r2.raise_for_status()
html = r2.text
page_url = str(r2.url)
except Exception as pe:
logger.debug(f"1fichier: POST attempt {post_attempt} failed: {pe}")
if post_attempt >= max_post_attempts:
@@ -505,6 +603,11 @@ def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progr
if html is None:
return (False, None, "Erreur lors de la soumission du formulaire")
blocked_reason = _extract_1fichier_free_mode_block_reason(html)
if blocked_reason:
logger.warning(f"1fichier: free mode blocked after form submit: {blocked_reason}")
return (False, None, blocked_reason)
# 4. Chercher lien de téléchargement
if cancel_event and cancel_event.is_set():
@@ -517,19 +620,40 @@ def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progr
]
direct_link = None
# Examine each pattern and validate the candidate link via HEAD/GET to avoid landing pages (/register, /login)
for idx, pattern in enumerate(patterns):
match = re.search(pattern, html, re.IGNORECASE)
if not match:
continue
try:
captured_link = match.group(1)
except IndexError:
logger.warning(f"1fichier: Pattern {idx} matched but no capture group(1)")
continue
candidate_entries: list[tuple[int, str]] = []
seen_candidates: set[str] = set()
# Resolve relative links
candidate = captured_link if captured_link.startswith(('http://', 'https://')) else urljoin(str(r.url), captured_link)
for anchor_match in re.finditer(r'<a[^>]+href=[\"\']([^\"\']+)[\"\'][^>]*>(.*?)</a>', html, re.IGNORECASE | re.DOTALL):
href = html_module.unescape(anchor_match.group(1).strip())
anchor_text = re.sub(r'<[^>]+>', ' ', anchor_match.group(2))
normalized_anchor_text = _normalize_1fichier_text(anchor_text)
if not href or not normalized_anchor_text:
continue
if not any(token in normalized_anchor_text for token in ('download', 'telecharg', 'tlcharg', 'click', 'cliquer')):
continue
candidate = href if href.startswith(('http://', 'https://')) else urljoin(page_url, href)
if candidate in seen_candidates:
continue
seen_candidates.add(candidate)
candidate_entries.append((0, candidate))
for idx, pattern in enumerate(patterns):
for match in re.finditer(pattern, html, re.IGNORECASE):
try:
captured_link = html_module.unescape(match.group(1).strip())
except (IndexError, AttributeError):
logger.warning(f"1fichier: Pattern {idx} matched but no usable capture group(1)")
continue
if not captured_link:
continue
candidate = captured_link if captured_link.startswith(('http://', 'https://')) else urljoin(page_url, captured_link)
if candidate in seen_candidates:
continue
seen_candidates.add(candidate)
candidate_entries.append((idx, candidate))
# Examine each pattern and validate the candidate link via HEAD/GET to avoid landing pages (/register, /login)
for idx, candidate in candidate_entries:
logger.debug(f"1fichier: Pattern {idx} matched, candidate link: {candidate}")
# Quick heuristic: skip known non-download endpoints
@@ -573,6 +697,10 @@ def download_1fichier_free_mode(url, dest_dir, session, log_callback=None, progr
continue
if not direct_link:
blocked_reason = _extract_1fichier_free_mode_block_reason(html)
if blocked_reason:
logger.warning(f"1fichier: no direct link because free mode is blocked: {blocked_reason}")
return (False, None, blocked_reason)
logger.error(f"1fichier: No valid download link found. HTML preview (first 700 chars): {html[:700]}")
return (False, None, "Lien de téléchargement introuvable")
@@ -740,6 +868,14 @@ def _extract_changelog_section(raw_text):
if not text:
return ""
def _is_version_heading(heading_text):
normalized = str(heading_text or "").strip()
return re.match(
r"^(?:release\s+)?(?:v(?:ersion)?\s*)?\d+(?:\.\d+)+(?:\s*[(:-].*)?$",
normalized,
re.IGNORECASE,
) is not None
lines = text.split("\n")
heading_re = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
changelog_start = None
@@ -763,7 +899,7 @@ def _extract_changelog_section(raw_text):
if stripped == "---":
break
match = heading_re.match(stripped)
if match and len(match.group(1)) <= changelog_level:
if match and len(match.group(1)) <= changelog_level and not _is_version_heading(match.group(2)):
break
extracted.append(line)
@@ -2775,7 +2911,12 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
try:
# Créer une session requests pour le mode gratuit
free_session = requests.Session()
free_session.headers.update({'User-Agent': 'Mozilla/5.0'})
free_session.headers.update(
_build_browser_download_headers(
referer=link,
accept='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
)
)
# Callbacks pour le mode gratuit
def log_cb(msg):
@@ -2862,7 +3003,10 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
else:
logger.error(f"Échec téléchargement gratuit: {error_msg}")
result[0] = False
result[1] = f"Error Downloading with free mode: {error_msg}"
if isinstance(error_msg, str) and error_msg.startswith("1fichier:"):
result[1] = error_msg
else:
result[1] = f"Error Downloading with free mode: {error_msg}"
return
except Exception as e:

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, get_clean_display_name
from utils import load_sources, load_games, extract_data, get_clean_display_name, parse_torrent_download_url
from network import download_rom, download_from_1fichier
from pathlib import Path
from rgsx_settings import get_language
@@ -1167,6 +1167,14 @@ class RGSXHandler(BaseHTTPRequestHandler):
'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
# Vérifier l'extension et déterminer si extraction nécessaire
from utils import check_extension_before_download

View File

@@ -289,10 +289,10 @@ def _extract_torrent_source(item) -> tuple[str, str] | None:
return None
def _expand_torrent_source(item, platform_id: str) -> list[tuple[str, None, str | 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 []
return None
source_name, source_url = source
try:
@@ -1480,7 +1480,7 @@ def load_games(platform_id:str) -> list[Game]:
def extract_from_dict(d):
torrent_rows = _expand_torrent_source(d, platform_id)
if torrent_rows:
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')
@@ -1493,7 +1493,7 @@ def load_games(platform_id:str) -> list[Game]:
for item in data:
if isinstance(item, (list, tuple)):
torrent_rows = _expand_torrent_source(item, platform_id)
if torrent_rows:
if torrent_rows is not None:
normalized.extend(torrent_rows)
continue
if len(item) == 0:

View File

@@ -1,3 +1,3 @@
{
"version": "2.6.1.5"
"version": "2.6.1.7"
}