mirror of
https://github.com/RetroGameSets/RGSX.git
synced 2026-05-19 15:43:37 +02:00
Compare commits
2 Commits
v2.6.1.5.1
...
v2.6.1.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7dad84108 | ||
|
|
c9f48d20dd |
@@ -27,7 +27,7 @@ except Exception:
|
||||
pygame = None # type: ignore
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "2.6.1.5.1"
|
||||
app_version = "2.6.1.6.1"
|
||||
|
||||
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
|
||||
GAMELIST_UPDATE_DAYS = 1
|
||||
|
||||
@@ -1766,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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -2775,7 +2903,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 +2995,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:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.6.1.5.1"
|
||||
"version": "2.6.1.6.1"
|
||||
}
|
||||
Reference in New Issue
Block a user