forked from Mirrors/RGSX
v2.2.2.0
- 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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user