1
0
forked from Mirrors/RGSX
- NEW feature :  real-debrid API available
- implement new API process for 1fichier : use 1fichier api 1st, if not found use alldebrid, if not found use realdebrid
- graphical menu to see if api keys are found in the saves folder
- correct some minor graphics bug
- delete old code useless for loading source from old file
- update translations
- fix update downgrading is not possible anymore for testing/dev purpose only
This commit is contained in:
skymike03
2025-09-12 03:00:41 +02:00
parent 7fc5204135
commit 7a7651e582
12 changed files with 523 additions and 161 deletions

View File

@@ -567,28 +567,27 @@ async def main():
})
config.current_history_item = len(config.history) - 1 # Sélectionner l'entrée en cours
if is_1fichier_url(url):
if not config.API_KEY_1FICHIER:
# Fallback AllDebrid
try:
from utils import load_api_key_alldebrid
config.API_KEY_ALLDEBRID = load_api_key_alldebrid()
except Exception:
config.API_KEY_ALLDEBRID = getattr(config, "API_KEY_ALLDEBRID", "")
if not config.API_KEY_1FICHIER and not getattr(config, "API_KEY_ALLDEBRID", ""):
# Utilisation helpers centralisés (utils)
try:
from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
keys_info = ensure_download_provider_keys(False)
except Exception as e:
logger.error(f"Impossible de charger les clés via helpers: {e}")
keys_info = {'1fichier': getattr(config,'API_KEY_1FICHIER',''), 'alldebrid': getattr(config,'API_KEY_ALLDEBRID',''), 'realdebrid': getattr(config,'API_KEY_REALDEBRID','')}
if missing_all_provider_keys():
config.previous_menu_state = config.menu_state
config.menu_state = "error"
try:
both_paths = f"{os.path.join(config.SAVE_FOLDER,'1FichierAPI.txt')} or {os.path.join(config.SAVE_FOLDER,'AllDebridAPI.txt')}"
config.error_message = _("error_api_key").format(both_paths)
config.error_message = _("error_api_key").format(build_provider_paths_string())
except Exception:
config.error_message = "Please enter API key (1fichier or AllDebrid)"
# Mettre à jour l'entrée temporaire avec l'erreur
config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)"
# Mise à jour historique
config.history[-1]["status"] = "Erreur"
config.history[-1]["progress"] = 0
config.history[-1]["message"] = "API NOT FOUND"
save_history(config.history)
config.needs_redraw = True
logger.error("Clé API 1fichier et AllDebrid absentes")
logger.error("Aucune clé fournisseur (1fichier/AllDebrid/RealDebrid) disponible")
config.pending_download = None
continue
pending = check_extension_before_download(url, platform_name, game_name)

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.2.1.0"
app_version = "2.2.2.0"
def get_application_root():
@@ -64,9 +64,11 @@ HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json")
# Séparation chemin / valeur pour éviter les confusions lors du chargement
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.txt")
API_KEY_REALDEBRID_PATH = os.path.join(SAVE_FOLDER, "RealDebridAPI.txt")
# Valeurs chargées (remplies dynamiquement par utils.load_api_key_*).
API_KEY_1FICHIER = ""
API_KEY_ALLDEBRID = ""
API_KEY_REALDEBRID = ""
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
# URL

View File

@@ -589,8 +589,9 @@ def handle_controls(event, sources, joystick, screen):
config.current_history_item = len(config.history) -1
task_id = str(pygame.time.get_ticks())
if is_1fichier_url(url):
keys = load_api_keys()
if not keys.get('1fichier') and not keys.get('alldebrid'):
from utils import ensure_download_provider_keys, missing_all_provider_keys
ensure_download_provider_keys(False)
if missing_all_provider_keys():
config.history[-1]["status"] = "Erreur"
config.history[-1]["message"] = "API NOT FOUND"
save_history(config.history)
@@ -625,22 +626,22 @@ def handle_controls(event, sources, joystick, screen):
config.current_history_item = len(config.history) - 1
# Vérifier d'abord si c'est un lien 1fichier
if is_1fichier_url(url):
keys = load_api_keys()
if not keys.get('1fichier') and not keys.get('alldebrid'):
from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
ensure_download_provider_keys(False)
if missing_all_provider_keys():
config.previous_menu_state = config.menu_state
config.menu_state = "error"
try:
both_paths = f"{os.path.join(config.SAVE_FOLDER,'1FichierAPI.txt')} or {os.path.join(config.SAVE_FOLDER,'AllDebridAPI.txt')}"
config.error_message = _("error_api_key").format(both_paths)
config.error_message = _("error_api_key").format(build_provider_paths_string())
except Exception as e:
logger.error(f"Erreur lors de la traduction de error_api_key: {str(e)}")
config.error_message = "Please enter API key (1fichier or AllDebrid)"
config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)"
config.history[-1]["status"] = "Erreur"
config.history[-1]["progress"] = 0
config.history[-1]["message"] = "API NOT FOUND"
save_history(config.history)
config.needs_redraw = True
logger.error("Clé API 1fichier et AllDebrid absentes, téléchargement impossible.")
logger.error("Clés API manquantes pour tous les fournisseurs (1fichier/AllDebrid/RealDebrid).")
config.pending_download = None
return action
config.pending_download = check_extension_before_download(url, platform, game_name)
@@ -741,21 +742,21 @@ def handle_controls(event, sources, joystick, screen):
))
config.current_history_item = len(config.history) - 1
if is_1fichier_url(url):
keys = load_api_keys()
if not keys.get('1fichier') and not keys.get('alldebrid'):
from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
ensure_download_provider_keys(False)
if missing_all_provider_keys():
config.previous_menu_state = config.menu_state
config.menu_state = "error"
try:
both_paths = f"{os.path.join(config.SAVE_FOLDER,'1FichierAPI.txt')} or {os.path.join(config.SAVE_FOLDER,'AllDebridAPI.txt')}"
config.error_message = _("error_api_key").format(both_paths)
config.error_message = _("error_api_key").format(build_provider_paths_string())
except Exception:
config.error_message = "Please enter API key (1fichier or AllDebrid)"
config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)"
config.history[-1]["status"] = "Erreur"
config.history[-1]["progress"] = 0
config.history[-1]["message"] = "API NOT FOUND"
save_history(config.history)
config.needs_redraw = True
logger.error("Clé API 1fichier et AllDebrid absentes, téléchargement impossible.")
logger.error("Clés API manquantes pour tous les fournisseurs (1fichier/AllDebrid/RealDebrid).")
config.pending_download = None
return action
task_id = str(pygame.time.get_ticks())
@@ -809,8 +810,9 @@ def handle_controls(event, sources, joystick, screen):
config.current_history_item = len(config.history) -1
task_id = str(pygame.time.get_ticks())
if is_1fichier_url(url):
keys = load_api_keys()
if not keys.get('1fichier') and not keys.get('alldebrid'):
from utils import ensure_download_provider_keys, missing_all_provider_keys
ensure_download_provider_keys(False)
if missing_all_provider_keys():
config.history[-1]["status"] = "Erreur"
config.history[-1]["message"] = "API NOT FOUND"
save_history(config.history)
@@ -874,8 +876,9 @@ def handle_controls(event, sources, joystick, screen):
config.current_history_item = len(config.history) -1
task_id = str(pygame.time.get_ticks())
if is_1fichier_url(url):
keys = load_api_keys()
if not keys.get('1fichier') and not keys.get('alldebrid'):
from utils import ensure_download_provider_keys, missing_all_provider_keys
ensure_download_provider_keys(False)
if missing_all_provider_keys():
config.history[-1]["status"] = "Erreur"
config.history[-1]["message"] = "API NOT FOUND"
save_history(config.history)
@@ -956,6 +959,9 @@ def handle_controls(event, sources, joystick, screen):
return action
# Retour à l'origine capturée si disponible sinon previous_menu_state
target = getattr(config, 'history_origin', getattr(config, 'previous_menu_state', 'platform'))
# Éviter boucle si target reste 'history'
if target == 'history':
target = 'platform'
config.menu_state = validate_menu_state(target)
if hasattr(config, 'history_origin'):
try:

View File

@@ -427,7 +427,19 @@ def draw_platform_grid(screen):
platform_name = config.platform_names.get(platform, platform)
# Affichage du titre avec animation subtile
title_text = f"{platform_name}"
# Afficher le nombre total de jeux disponibles (tous systèmes) pour cohérence avec l'écran jeux
# Nombre de jeux pour la plateforme sélectionnée (utilise le cache pre-calculé si disponible)
game_count = 0
try:
if hasattr(config, 'games_count') and isinstance(config.games_count, dict):
game_count = config.games_count.get(platform_name, 0)
# Fallback dynamique si pas dans le cache (ex: plateformes modifiées à chaud)
if game_count == 0 and hasattr(config, 'platform_dict_by_name'):
from utils import load_games # import local pour éviter import circulaire global
game_count = len(load_games(platform_name))
except Exception:
game_count = 0
title_text = f"{platform_name} ({game_count})" if game_count > 0 else f"{platform_name}"
title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
title_rect_inflated = title_rect.inflate(60, 30)
@@ -770,12 +782,12 @@ 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
# Define column widths as percentages of available space (give more space to status/error messages)
column_width_percentages = {
"platform": 0.20, # platform column
"game_name": 0.50, # game name column
"size": 0.10, # size column
"status": 0.20 # status column
"platform": 0.15, # narrower platform column
"game_name": 0.45, # game name column
"size": 0.10, # size column remains compact
"status": 0.30 # wider status column for long error codes/messages
}
available_width = int(0.95 * config.screen_width - 60) # Total available width for columns
col_platform_width = int(available_width * column_width_percentages["platform"])
@@ -886,36 +898,51 @@ def draw_history_list(screen):
status = entry.get("status", "Inconnu")
progress = entry.get("progress", 0)
progress = max(0, min(100, progress)) # Clamp progress between 0 and 100
# Personnaliser l'affichage du statut
# Compute status text (optimized version without redundant prefix for errors)
if status in ["Téléchargement", "downloading"]:
status_text = _("history_status_downloading").format(progress)
# logger.debug(f"Affichage progression: {progress:.1f}% pour {game_name}, status={status_text}")
provider_prefix = entry.get("provider_prefix") or (entry.get("provider") + ":" if entry.get("provider") else "")
if provider_prefix and not status_text.startswith(provider_prefix):
status_text = f"{provider_prefix} {status_text}"
elif status == "Extracting":
status_text = _("history_status_extracting").format(progress)
# logger.debug(f"Affichage extraction: {progress:.1f}% pour {game_name}, status={status_text}")
provider_prefix = entry.get("provider_prefix") or (entry.get("provider") + ":" if entry.get("provider") else "")
if provider_prefix and not status_text.startswith(provider_prefix):
status_text = f"{provider_prefix} {status_text}"
elif status == "Download_OK":
# Completed: no provider prefix (per requirement)
status_text = _("history_status_completed")
# S'assurer que le pourcentage est entre 0 et 100
progress = max(0, min(100, progress))
# Personnaliser l'affichage du statut
if status in ["Téléchargement", "downloading"]:
status_text = _("history_status_downloading").format(progress)
# logger.debug(f"Affichage progression: {progress:.1f}% pour {game_name}, status={status_text}")
elif status == "Extracting":
status_text = _("history_status_extracting").format(progress)
# logger.debug(f"Affichage extraction: {progress:.1f}% pour {game_name}, status={status_text}")
elif status == "Download_OK":
status_text = _("history_status_completed")
# logger.debug(f"Affichage terminé: {game_name}, status={status_text}")
elif status == "Erreur":
status_text = _("history_status_error").format(entry.get('message', 'Échec'))
# Prefer friendly mapped message now stored in 'message'
status_text = entry.get('message')
if not status_text:
# Some legacy entries might have only raw in result[1] or auxiliary field
status_text = entry.get('raw_error_realdebrid') or entry.get('error') or 'Échec'
# Strip redundant prefixes if any
for prefix in ["Erreur :", "Erreur:", "Error:", "Error :"]:
if status_text.startswith(prefix):
status_text = status_text[len(prefix):].strip()
break
provider_prefix = entry.get("provider_prefix") or (entry.get("provider") + ":" if entry.get("provider") else "")
if provider_prefix and not status_text.startswith(provider_prefix):
status_text = f"{provider_prefix} {status_text}"
elif status == "Canceled":
status_text = _("history_status_canceled")
else:
status_text = status
#logger.debug(f"Affichage statut inconnu: {game_name}, status={status_text}")
# Determine color dedicated to status (independent from selection for better readability)
if status == "Erreur":
status_color = THEME_COLORS.get("error_text", (255, 0, 0))
elif status == "Canceled":
status_color = THEME_COLORS.get("warning_text", (255, 100, 0))
elif status == "Download_OK":
# Use green OK color
status_color = THEME_COLORS.get("fond_lignes", (0, 255, 0))
else:
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(game_name, config.small_font, col_game_width - 10)
@@ -926,7 +953,7 @@ def draw_history_list(screen):
platform_surface = config.small_font.render(platform_text, True, color)
game_surface = config.small_font.render(game_text, True, color)
size_surface = config.small_font.render(size_text, True, color) # Correction ici
status_surface = config.small_font.render(status_text, True, color)
status_surface = config.small_font.render(status_text, True, status_color)
platform_rect = platform_surface.get_rect(center=(header_x_positions[0], y_pos))
game_rect = game_surface.get_rect(center=(header_x_positions[1], y_pos))
@@ -1545,35 +1572,104 @@ def draw_pause_api_keys_status(screen):
screen.blit(OVERLAY, (0,0))
from utils import load_api_keys
keys = load_api_keys()
# Layout simple
lines = [
_("api_keys_status_title") if _ else "API Keys Status",
title = _("api_keys_status_title") if _ else "API Keys Status"
# Préparer données avec masquage partiel des clés (afficher 4 premiers et 2 derniers caractères si longueur > 10)
def mask_key(value: str|None):
if not value:
return "" # rien si absent
v = value.strip()
if len(v) <= 10:
return v # courte, afficher entière
return f"{v[:4]}{v[-2:]}" # masque au milieu
providers = [
("1fichier", keys.get('1fichier')),
("AllDebrid", keys.get('alldebrid'))
("AllDebrid", keys.get('alldebrid')),
("RealDebrid", keys.get('realdebrid'))
]
menu_width = int(config.screen_width * 0.55)
menu_height = int(config.screen_height * 0.35)
# Dimensions dynamiques en fonction du contenu
row_height = config.small_font.get_height() + 14
header_height = 60
inner_rows = len(providers)
menu_width = int(config.screen_width * 0.60)
menu_height = header_height + inner_rows * row_height + 80
menu_x = (config.screen_width - menu_width)//2
menu_y = (config.screen_height - menu_height)//2
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=16)
pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=16)
title_surface = config.font.render(lines[0], True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width//2, menu_y + 40))
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=22)
pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=22)
# Titre
title_surface = config.font.render(title, True, THEME_COLORS["text"])
title_rect = title_surface.get_rect(center=(config.screen_width//2, menu_y + 36))
screen.blit(title_surface, title_rect)
status_on = _("status_present") if _ else "Present"
status_off = _("status_missing") if _ else "Missing"
y = title_rect.bottom + 20
for provider, present in lines[1:]:
status_txt = status_on if present else status_off
text = f"{provider}: {status_txt}"
surf = config.small_font.render(text, True, THEME_COLORS["text"])
rect = surf.get_rect(center=(config.screen_width//2, y))
screen.blit(surf, rect)
y += surf.get_height() + 12
back_txt = _("menu_back") if _ else "Back"
back_surf = config.small_font.render(back_txt, True, THEME_COLORS["fond_lignes"]) # Indication
back_rect = back_surf.get_rect(center=(config.screen_width//2, menu_y + menu_height - 30))
screen.blit(back_surf, back_rect)
status_present_txt = _("status_present") if _ else "Present"
status_missing_txt = _("status_missing") if _ else "Missing"
# Plus de légende textuelle Présent / Missing (demandé) seules les pastilles couleur serviront.
legend_rect = pygame.Rect(0,0,0,0)
# Colonnes: Provider | Status badge | (key masked)
col_provider_x = menu_x + 40
col_status_x = menu_x + int(menu_width * 0.40)
col_key_x = menu_x + int(menu_width * 0.58)
# Démarrage des lignes sous le titre avec un padding
y = title_rect.bottom + 24
badge_font = config.tiny_font if hasattr(config, 'tiny_font') else config.small_font
for provider, value in providers:
present = bool(value)
# Provider name
prov_surf = config.small_font.render(provider, True, THEME_COLORS["text"])
screen.blit(prov_surf, (col_provider_x, y))
# Pastille circulaire simple (couleur = statut)
circle_color = (60, 170, 60) if present else (180, 55, 55)
circle_bg = (30, 70, 30) if present else (70, 25, 25)
radius = 14
center_x = col_status_x + radius
center_y = y + badge_font.get_height()//2
pygame.draw.circle(screen, circle_bg, (center_x, center_y), radius)
pygame.draw.circle(screen, circle_color, (center_x, center_y), radius, 2)
# Masked key (dim color) or hint
if present:
masked = mask_key(value)
key_color = THEME_COLORS.get("text_dim", (180,180,180))
key_label = masked
else:
key_color = THEME_COLORS.get("text_dim", (150,150,150))
# Afficher nom de fichier + 'empty'
filename_display = {
'1fichier': '1FichierAPI.txt',
'AllDebrid': 'AllDebridAPI.txt',
'RealDebrid': 'RealDebridAPI.txt'
}.get(provider, 'key.txt')
empty_suffix = _("api_key_empty_suffix") if _ and _("api_key_empty_suffix") != "api_key_empty_suffix" else "empty"
key_label = f"{filename_display} {empty_suffix}"
key_surf = config.tiny_font.render(key_label, True, key_color) if hasattr(config, 'tiny_font') else config.small_font.render(key_label, True, key_color)
screen.blit(key_surf, (col_key_x, y))
# Ligne séparatrice (optionnelle)
sep_y = y + row_height - 8
if provider != providers[-1][0]:
pygame.draw.line(screen, THEME_COLORS["border"], (menu_x + 25, sep_y), (menu_x + menu_width - 25, sep_y), 1)
y += row_height
# Indication basique: utiliser config.SAVE_FOLDER (chemin dynamique)
save_folder_path = getattr(config, 'SAVE_FOLDER', '/saves/ports/rgsx')
# Utiliser placeholder {path} si traduction fournie
if _ and _("api_keys_hint_manage") != "api_keys_hint_manage":
try:
hint_txt = _("api_keys_hint_manage").format(path=save_folder_path)
except Exception:
hint_txt = f"Put your keys in {save_folder_path}"
else:
hint_txt = f"Put your keys in {save_folder_path}"
hint_font = config.tiny_font if hasattr(config, 'tiny_font') else config.small_font
hint_surf = hint_font.render(hint_txt, True, THEME_COLORS.get("text_dim", THEME_COLORS["text"]))
# Positionné un peu plus haut pour aérer
hint_rect = hint_surf.get_rect(center=(config.screen_width//2, menu_y + menu_height - 30))
screen.blit(hint_surf, hint_rect)
def draw_filter_platforms_menu(screen):
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""

View File

@@ -147,5 +147,7 @@
"status_missing": "Fehlt",
"menu_api_keys_status": "API-Schlüssel",
"api_keys_status_title": "Status der API-Schlüssel",
"menu_games": "Spiele"
"menu_games": "Spiele",
"api_keys_hint_manage": "Legen Sie Ihre Schlüssel in {path}",
"api_key_empty_suffix": "leer"
}

View File

@@ -147,5 +147,7 @@
"status_missing": "Missing",
"menu_api_keys_status": "API Keys",
"api_keys_status_title": "API Keys Status",
"menu_games": "Games"
"menu_games": "Games",
"api_keys_hint_manage": "Put your keys in {path}",
"api_key_empty_suffix": "empty"
}

View File

@@ -147,5 +147,7 @@
"status_missing": "Ausente",
"menu_api_keys_status": "Claves API",
"api_keys_status_title": "Estado de las claves API",
"menu_games": "Juegos"
"menu_games": "Juegos",
"api_keys_hint_manage": "Coloca tus claves en {path}",
"api_key_empty_suffix": "vacío"
}

View File

@@ -147,5 +147,7 @@
"status_missing": "Absente",
"menu_api_keys_status": "Clés API",
"api_keys_status_title": "Statut des clés API",
"menu_games": "Jeux"
"menu_games": "Jeux",
"api_keys_hint_manage": "Placez vos clés dans {path}",
"api_key_empty_suffix": "vide"
}

View File

@@ -147,5 +147,7 @@
"status_missing": "Assente",
"menu_api_keys_status": "Chiavi API",
"api_keys_status_title": "Stato delle chiavi API",
"menu_games": "Giochi"
"menu_games": "Giochi",
"api_keys_hint_manage": "Metti le tue chiavi in {path}",
"api_key_empty_suffix": "vuoto"
}

View File

@@ -147,5 +147,7 @@
"status_missing": "Ausente",
"menu_api_keys_status": "Chaves API",
"api_keys_status_title": "Status das chaves API",
"menu_games": "Jogos"
"menu_games": "Jogos",
"api_keys_hint_manage": "Coloque suas chaves em {path}",
"api_key_empty_suffix": "vazio"
}

View File

@@ -136,15 +136,40 @@ async def check_for_updates():
config.current_loading_system = _("network_checking_updates")
config.loading_progress = 5.0
config.needs_redraw = True
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
response.raise_for_status()
if response.headers.get("content-type") != "application/json":
raise ValueError(f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})")
raise ValueError(
f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})"
)
version_data = response.json()
latest_version = version_data.get("version")
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_version}")
# --- Protection anti-downgrade ---
def _parse_version(v: str):
try:
return [int(p) for p in str(v).strip().split('.') if p.isdigit()]
except Exception:
return [0]
local_parts = _parse_version(getattr(config, 'app_version', '0'))
remote_parts = _parse_version(latest_version or '0')
# Normaliser longueur
max_len = max(len(local_parts), len(remote_parts))
local_parts += [0] * (max_len - len(local_parts))
remote_parts += [0] * (max_len - len(remote_parts))
logger.debug(f"Comparaison versions normalisées local={local_parts} remote={remote_parts}")
if remote_parts <= local_parts:
# Pas de mise à jour si version distante identique ou inférieure (empêche downgrade accidentel)
logger.info("Version distante inférieure ou égale skip mise à jour (anti-downgrade)")
return True, _("network_no_update_available") if _ else "No update (local >= remote)"
# À ce stade latest_version est strictement > version locale
UPDATE_ZIP = OTA_UPDATE_ZIP.replace("RGSX.zip", f"RGSX_v{latest_version}.zip")
logger.debug(f"URL de mise à jour : {UPDATE_ZIP}")
if latest_version != config.app_version:
config.current_loading_system = _("network_update_available").format(latest_version)
config.loading_progress = 10.0
@@ -638,12 +663,19 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
keys_info = load_api_keys()
config.API_KEY_1FICHIER = keys_info.get('1fichier', '')
config.API_KEY_ALLDEBRID = keys_info.get('alldebrid', '')
config.API_KEY_REALDEBRID = keys_info.get('realdebrid', '')
if not config.API_KEY_1FICHIER and config.API_KEY_ALLDEBRID:
logger.debug("Clé 1fichier absente, utilisation fallback AllDebrid")
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID:
logger.debug("Aucune clé API disponible (1fichier ni AllDebrid)")
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and config.API_KEY_REALDEBRID:
logger.debug("Clé 1fichier & AllDebrid absentes, utilisation fallback RealDebrid")
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and not config.API_KEY_REALDEBRID:
logger.debug("Aucune clé API disponible (1fichier, AllDebrid, RealDebrid)")
logger.debug(f"Début téléchargement 1fichier: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}, task_id={task_id}")
logger.debug(f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'} / AllDebrid: {'présente' if config.API_KEY_ALLDEBRID else 'absente'} (reloaded={keys_info.get('reloaded')})")
logger.debug(
f"Clé API 1fichier: {'présente' if config.API_KEY_1FICHIER else 'absente'} / "
f"AllDebrid: {'présente' if config.API_KEY_ALLDEBRID else 'absente'} / "
f"RealDebrid: {'présente' if config.API_KEY_REALDEBRID else 'absente'} (reloaded={keys_info.get('reloaded')})"
)
result = [None, None]
# Créer une queue spécifique pour cette tâche
@@ -653,8 +685,30 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
if task_id not in cancel_events:
cancel_events[task_id] = threading.Event()
provider_used = None # '1F', 'AD', 'RD'
def _set_provider_in_history(pfx: str):
try:
if not pfx:
return
if isinstance(config.history, list):
for entry in config.history:
if entry.get("url") == url:
entry["provider"] = pfx
entry["provider_prefix"] = f"{pfx}:"
try:
save_history(config.history)
except Exception:
pass
config.needs_redraw = True
break
except Exception:
pass
def download_thread():
logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}")
# Assurer l'accès à provider_used dans cette closure (lecture/écriture)
nonlocal provider_used
try:
cancel_ev = cancel_events.get(task_id)
link = url.split('&af=')[0]
@@ -700,12 +754,46 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
logger.debug(f"Préparation requête 1fichier file/info pour {link}")
response = requests.post("https://api.1fichier.com/v1/file/info.cgi", headers=headers, json=payload, timeout=30)
logger.debug(f"Réponse file/info reçue, code: {response.status_code}")
response.raise_for_status()
file_info = response.json()
file_info = None
raw_fileinfo_text = None
try:
raw_fileinfo_text = response.text
except Exception:
pass
try:
file_info = response.json()
except Exception:
file_info = None
if response.status_code != 200:
# 403 souvent = clé invalide ou accès interdit
friendly = None
raw_err = None
if isinstance(file_info, dict):
raw_err = file_info.get('message') or file_info.get('error') or file_info.get('status')
if raw_err == 'Bad token':
friendly = "1F: Clé API 1fichier invalide"
elif raw_err:
friendly = f"1F: {raw_err}"
if not friendly:
if response.status_code == 403:
friendly = "1F: Accès refusé (403)"
elif response.status_code == 401:
friendly = "1F: Non autorisé (401)"
else:
friendly = f"1F: Erreur HTTP {response.status_code}"
result[0] = False
result[1] = friendly
try:
result.append({"raw_error_1fichier_fileinfo": raw_err or raw_fileinfo_text})
except Exception:
pass
return
# Status 200 requis à partir d'ici
file_info = file_info if isinstance(file_info, dict) else {}
if "error" in file_info and file_info["error"] == "Resource not found":
logger.error(f"Le fichier {game_name} n'existe pas sur 1fichier")
result[0] = False
result[1] = _("network_file_not_found").format(game_name)
result[1] = f"1F: {_("network_file_not_found").format(game_name)}" if _ else f"1F: File not found {game_name}"
return
filename = file_info.get("filename", "").strip()
if not filename:
@@ -718,9 +806,57 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
logger.debug(f"Chemin destination: {dest_path}")
logger.debug(f"Envoi requête 1fichier get_token pour {link}")
response = requests.post("https://api.1fichier.com/v1/download/get_token.cgi", headers=headers, json=payload, timeout=30)
logger.debug(f"Réponse get_token reçue, code: {response.status_code}")
status_1f = response.status_code
raw_text_1f = None
try:
raw_text_1f = response.text
except Exception:
pass
logger.debug(f"Réponse get_token reçue, code: {status_1f} body_snippet={(raw_text_1f[:120] + '...') if raw_text_1f and len(raw_text_1f) > 120 else raw_text_1f}")
download_info = None
try:
download_info = response.json()
except Exception:
download_info = None
# Même en cas de code !=200 on tente de récupérer un message JSON exploitable
if status_1f != 200:
friendly_1f = None
raw_error_1f = None
if isinstance(download_info, dict):
# Exemples de réponses d'erreur 1fichier: {"status":"KO","message":"Bad token"} ou autres
raw_error_1f = download_info.get('message') or download_info.get('status')
# Mapping simple pour les messages fréquents / cas premium requis
ONEFICHIER_ERROR_MAP = {
"Bad token": "1F: Clé API invalide",
"Must be a customer (Premium, Access) #236": "1F: Compte Premium requis",
}
if raw_error_1f:
friendly_1f = ONEFICHIER_ERROR_MAP.get(raw_error_1f)
if not friendly_1f:
# Fallback générique sur code HTTP
if status_1f == 403:
friendly_1f = "1F: Accès refusé (403)"
elif status_1f == 401:
friendly_1f = "1F: Non autorisé (401)"
elif status_1f >= 500:
friendly_1f = f"1F: Erreur serveur ({status_1f})"
else:
friendly_1f = f"1F: Erreur ({status_1f})"
# Stocker et retourner tôt car pas de token valide
result[0] = False
result[1] = friendly_1f
try:
result.append({"raw_error_1fichier": raw_error_1f or raw_text_1f})
except Exception:
pass
return
# Si status 200 on continue normalement
response.raise_for_status()
download_info = response.json()
if not isinstance(download_info, dict):
logger.error("Réponse 1fichier inattendue (pas un JSON) pour get_token")
result[0] = False
result[1] = _("network_api_error").format("1fichier invalid JSON") if _ else "1fichier invalid JSON"
return
final_url = download_info.get("url")
if not final_url:
logger.error("Impossible de récupérer l'URL de téléchargement")
@@ -728,43 +864,147 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
result[1] = _("network_cannot_get_download_url")
return
logger.debug(f"URL de téléchargement obtenue via 1fichier: {final_url}")
provider_used = '1F'
_set_provider_in_history(provider_used)
else:
# AllDebrid: débrider l'URL 1fichier vers une URL directe
logger.debug("Mode téléchargement sélectionné: AllDebrid (fallback, débridage 1fichier)")
if not getattr(config, 'API_KEY_ALLDEBRID', ''):
logger.error("Aucune clé API (1fichier/AllDebrid) disponible")
result[0] = False
result[1] = _("network_api_error").format("Missing API key") if _ else "API key missing"
return
ad_key = config.API_KEY_ALLDEBRID
# AllDebrid API v4 example: GET https://api.alldebrid.com/v4/link/unlock?agent=<app>&apikey=<key>&link=<url>
params = {
'agent': 'RGSX',
'apikey': ad_key,
'link': link
}
logger.debug("Requête AllDebrid link/unlock en cours")
response = requests.get("https://api.alldebrid.com/v4/link/unlock", params=params, timeout=30)
logger.debug(f"Réponse AllDebrid reçue, code: {response.status_code}")
response.raise_for_status()
ad_json = response.json()
if ad_json.get('status') != 'success':
err = ad_json.get('error', {}).get('code') or ad_json
logger.error(f"AllDebrid échec débridage: {err}")
result[0] = False
result[1] = _("network_api_error").format(f"AllDebrid unlock failed: {err}") if _ else f"AllDebrid unlock failed: {err}"
return
data = ad_json.get('data', {})
filename = data.get('filename') or game_name
final_url = data.get('link') or data.get('download') or data.get('streamingLink')
final_url = None
filename = None
# Tentative AllDebrid
if getattr(config, 'API_KEY_ALLDEBRID', ''):
logger.debug("Mode téléchargement sélectionné: AllDebrid (fallback 1)")
try:
ad_key = config.API_KEY_ALLDEBRID
params = {'agent': 'RGSX', 'apikey': ad_key, 'link': link}
logger.debug("Requête AllDebrid link/unlock en cours")
response = requests.get("https://api.alldebrid.com/v4/link/unlock", params=params, timeout=30)
logger.debug(f"Réponse AllDebrid reçue, code: {response.status_code}")
response.raise_for_status()
ad_json = response.json()
if ad_json.get('status') == 'success':
data = ad_json.get('data', {})
filename = data.get('filename') or game_name
final_url = data.get('link') or data.get('download') or data.get('streamingLink')
if final_url:
logger.debug("Débridage réussi via AllDebrid")
provider_used = 'AD'
_set_provider_in_history(provider_used)
else:
logger.warning(f"AllDebrid status != success: {ad_json}")
except Exception as e:
logger.error(f"Erreur AllDebrid fallback: {e}")
# Tentative RealDebrid si pas de final_url
if not final_url and getattr(config, 'API_KEY_REALDEBRID', ''):
logger.debug("Tentative fallback RealDebrid (unlock)")
try:
rd_key = config.API_KEY_REALDEBRID
headers_rd = {"Authorization": f"Bearer {rd_key}"}
rd_resp = requests.post(
"https://api.real-debrid.com/rest/1.0/unrestrict/link",
data={"link": link},
headers=headers_rd,
timeout=30
)
status = rd_resp.status_code
raw_text = None
rd_json = None
try:
raw_text = rd_resp.text
except Exception:
pass
# Tenter JSON même si statut != 200
try:
rd_json = rd_resp.json()
except Exception:
rd_json = None
logger.debug(f"Réponse RealDebrid code={status} body_snippet={(raw_text[:120] + '...') if raw_text and len(raw_text) > 120 else raw_text}")
# Mapping erreurs RD (liste partielle, extensible)
REALDEBRID_ERROR_MAP = {
# Values intentionally WITHOUT prefix; we'll add 'RD:' dynamically
1: "Bad request",
2: "Unsupported hoster",
3: "Temporarily unavailable",
4: "File not found",
5: "Too many requests",
6: "Access denied",
8: "Not premium account",
9: "No traffic left",
11: "Internal error",
20: "Premium account only", # normalisation wording
}
error_code = None
error_message = None # Friendly / mapped message (to display in history)
error_message_raw = None # Raw provider message ('error') kept for debugging if needed
if rd_json and isinstance(rd_json, dict):
# Format attendu quand erreur: {'error_code': int, 'error': 'message'}
error_code = rd_json.get('error_code') or rd_json.get('error') if isinstance(rd_json.get('error'), int) else rd_json.get('error_code')
if isinstance(error_code, str) and error_code.isdigit():
error_code = int(error_code)
api_error_text = rd_json.get('error') if isinstance(rd_json.get('error'), str) else None
if error_code is not None:
mapped = REALDEBRID_ERROR_MAP.get(error_code)
# Raw API error sometimes returns 'hoster_not_free' while code=20
if api_error_text and api_error_text.strip().lower() == 'hoster_not_free':
api_error_text = 'Premium account only'
if mapped and not mapped.lower().startswith('rd:'):
mapped = f"RD: {mapped}"
if not mapped and api_error_text and not api_error_text.lower().startswith('rd:'):
api_error_text = f"RD: {api_error_text}"
error_message = mapped or api_error_text or f"RD: error {error_code}"
# Conserver la version brute séparément
error_message_raw = api_error_text if api_error_text and api_error_text != error_message else None
# Succès si 200 et presence 'download'
if status == 200 and rd_json and rd_json.get('download'):
final_url = rd_json.get('download')
filename = rd_json.get('filename') or filename or game_name
logger.debug("Débridage réussi via RealDebrid")
provider_used = 'RD'
_set_provider_in_history(provider_used)
else:
if error_message:
logger.warning(f"RealDebrid a renvoyé une erreur (code interne {error_code}): {error_message}")
else:
# Pas d'erreur structurée -> traiter statut HTTP
if status == 503:
error_message = "RD: service unavailable (503)"
elif status >= 500:
error_message = f"RD: server error ({status})"
elif status == 429:
error_message = "RD: rate limited (429)"
else:
error_message = f"RD: unexpected status ({status})"
logger.warning(f"RealDebrid fallback échec: {error_message}")
# Pas de détail JSON -> utiliser friendly comme raw aussi
error_message_raw = error_message
# Conserver message dans result si aucun autre provider ne réussit
if not final_url:
# Marquer le provider même en cas d'erreur pour affichage du préfixe dans l'historique
if provider_used is None:
provider_used = 'RD'
_set_provider_in_history(provider_used)
result[0] = False
# Pour l'interface: stocker le message friendly en priorité
result[1] = error_message or error_message_raw
# Stocker la version brute pour éventuel usage avancé
try:
if isinstance(result, list):
# Ajouter un dict auxiliaire pour meta erreurs
result.append({"raw_error_realdebrid": error_message_raw})
except Exception:
pass
except Exception as e:
logger.error(f"Exception RealDebrid fallback: {e}")
if not final_url:
logger.error("AllDebrid n'a pas renvoyé de lien direct")
logger.error("Aucune URL directe obtenue (AllDebrid & RealDebrid échoués ou absents)")
result[0] = False
result[1] = _("network_cannot_get_download_url")
if result[1] is None:
result[1] = _("network_api_error").format("No provider available") if _ else "No provider available"
return
if not filename:
filename = game_name
sanitized_filename = sanitize_filename(filename)
dest_path = os.path.join(dest_dir, sanitized_filename)
logger.debug(f"URL directe obtenue via AllDebrid: {final_url}")
lock = threading.Lock()
retries = 10
retry_delay = 10

View File

@@ -342,35 +342,6 @@ def _get_dest_folder_name(platform_key: str) -> str:
# Fonction pour charger sources.json
def load_sources():
try:
# Détection legacy: si sources.json (ancien format) existe encore, déclencher redownload automatique
legacy_path = os.path.join(config.SAVE_FOLDER, "sources.json")
if os.path.exists(legacy_path):
logger.warning("Ancien fichier sources.json détecté: déclenchement redownload cache jeux")
try:
# Supprimer ancien cache et forcer redémarrage logique comme dans l'option de menu
if os.path.exists(config.SOURCES_FILE):
try:
os.remove(config.SOURCES_FILE)
except Exception:
pass
if os.path.exists(config.GAMES_FOLDER):
shutil.rmtree(config.GAMES_FOLDER, ignore_errors=True)
if os.path.exists(config.IMAGES_FOLDER):
shutil.rmtree(config.IMAGES_FOLDER, ignore_errors=True)
# Renommer legacy pour éviter boucle
try:
os.replace(legacy_path, legacy_path + ".bak")
except Exception:
pass
# Préparer popup redémarrage si contexte graphique chargé
config.popup_message = _("popup_redownload_success") if hasattr(config, 'popup_message') else "Cache jeux réinitialisé"
config.popup_timer = 5000 if hasattr(config, 'popup_timer') else 0
config.menu_state = "restart_popup" if hasattr(config, 'menu_state') else getattr(config, 'menu_state', 'platform')
config.needs_redraw = True
logger.info("Redownload cache déclenché automatiquement (legacy sources.json)")
return [] # On sort pour laisser le processus de redémarrage gérer le rechargement
except Exception as e:
logger.error(f"Échec redownload automatique depuis legacy sources.json: {e}")
sources = []
if os.path.exists(config.SOURCES_FILE):
with open(config.SOURCES_FILE, 'r', encoding='utf-8') as f:
@@ -1251,23 +1222,24 @@ def set_music_popup(music_name):
config.needs_redraw = True # Forcer le redraw pour afficher le nom de la musique
def load_api_keys(force: bool = False):
"""Charge les clés API (1fichier, AllDebrid) en une seule passe.
"""Charge les clés API (1fichier, AllDebrid, RealDebrid) en une seule passe.
- Crée les fichiers vides s'ils n'existent pas
- Met à jour config.API_KEY_1FICHIER et config.API_KEY_ALLDEBRID
- Met à jour config.API_KEY_1FICHIER, config.API_KEY_ALLDEBRID, config.API_KEY_REALDEBRID
- Utilise un cache basé sur le mtime pour éviter des relectures
- force=True ignore le cache et relit systématiquement
Retourne: { '1fichier': str, 'alldebrid': str, 'reloaded': bool }
Retourne: { '1fichier': str, 'alldebrid': str, 'realdebrid': str, 'reloaded': bool }
"""
try:
paths = {
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID_PATH', ''),
}
cache_attr = '_api_keys_cache'
if not hasattr(config, cache_attr):
setattr(config, cache_attr, {'1fichier_mtime': None, 'alldebrid_mtime': None})
setattr(config, cache_attr, {'1fichier_mtime': None, 'alldebrid_mtime': None, 'realdebrid_mtime': None})
cache_data = getattr(config, cache_attr)
reloaded = False
@@ -1299,13 +1271,16 @@ def load_api_keys(force: bool = False):
# Assignation dans config
if key_name == '1fichier':
config.API_KEY_1FICHIER = value
else:
elif key_name == 'alldebrid':
config.API_KEY_ALLDEBRID = value
elif key_name == 'realdebrid':
config.API_KEY_REALDEBRID = value
cache_data[cache_key] = mtime
reloaded = True
return {
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
'reloaded': reloaded
}
except Exception as e:
@@ -1313,6 +1288,7 @@ def load_api_keys(force: bool = False):
return {
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
'reloaded': False
}
@@ -1323,10 +1299,41 @@ def load_api_key_1fichier(force: bool = False): # pragma: no cover
def load_api_key_alldebrid(force: bool = False): # pragma: no cover
return load_api_keys(force).get('alldebrid', '')
def load_api_key_realdebrid(force: bool = False): # pragma: no cover
return load_api_keys(force).get('realdebrid', '')
# Ancien nom conservé comme alias
def ensure_api_keys_loaded(force: bool = False): # pragma: no cover
return load_api_keys(force)
# ------------------------------
# Helpers centralisés pour gestion des fournisseurs de téléchargement
# ------------------------------
def build_provider_paths_string():
"""Retourne une chaîne listant les chemins des fichiers de clés pour affichage/erreurs."""
return f"{getattr(config, 'API_KEY_1FICHIER_PATH', '')} or {getattr(config, 'API_KEY_ALLDEBRID_PATH', '')} or {getattr(config, 'API_KEY_REALDEBRID_PATH', '')}"
def ensure_download_provider_keys(force: bool = False): # pragma: no cover
"""S'assure que les clés 1fichier/AllDebrid/RealDebrid sont chargées et retourne le dict.
Utilise load_api_keys (cache mtime). force=True invalide le cache.
"""
return load_api_keys(force)
def missing_all_provider_keys(): # pragma: no cover
"""True si aucune des trois clés n'est définie."""
keys = load_api_keys(False)
return not keys.get('1fichier') and not keys.get('alldebrid') and not keys.get('realdebrid')
def provider_keys_status(): # pragma: no cover
"""Retourne un dict de présence pour debug/log."""
keys = load_api_keys(False)
return {
'1fichier': bool(keys.get('1fichier')),
'alldebrid': bool(keys.get('alldebrid')),
'realdebrid': bool(keys.get('realdebrid')),
}
def load_music_config():
"""Charge la configuration musique depuis rgsx_settings.json."""
try: