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
|
config.current_history_item = len(config.history) - 1 # Sélectionner l'entrée en cours
|
||||||
if is_1fichier_url(url):
|
if is_1fichier_url(url):
|
||||||
if not config.API_KEY_1FICHIER:
|
# Utilisation helpers centralisés (utils)
|
||||||
# Fallback AllDebrid
|
try:
|
||||||
try:
|
from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
|
||||||
from utils import load_api_key_alldebrid
|
keys_info = ensure_download_provider_keys(False)
|
||||||
config.API_KEY_ALLDEBRID = load_api_key_alldebrid()
|
except Exception as e:
|
||||||
except Exception:
|
logger.error(f"Impossible de charger les clés via helpers: {e}")
|
||||||
config.API_KEY_ALLDEBRID = getattr(config, "API_KEY_ALLDEBRID", "")
|
keys_info = {'1fichier': getattr(config,'API_KEY_1FICHIER',''), 'alldebrid': getattr(config,'API_KEY_ALLDEBRID',''), 'realdebrid': getattr(config,'API_KEY_REALDEBRID','')}
|
||||||
if not config.API_KEY_1FICHIER and not getattr(config, "API_KEY_ALLDEBRID", ""):
|
if missing_all_provider_keys():
|
||||||
config.previous_menu_state = config.menu_state
|
config.previous_menu_state = config.menu_state
|
||||||
config.menu_state = "error"
|
config.menu_state = "error"
|
||||||
try:
|
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(build_provider_paths_string())
|
||||||
config.error_message = _("error_api_key").format(both_paths)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
config.error_message = "Please enter API key (1fichier or AllDebrid)"
|
config.error_message = "Please enter API key (1fichier or AllDebrid or RealDebrid)"
|
||||||
# Mettre à jour l'entrée temporaire avec l'erreur
|
# Mise à jour historique
|
||||||
config.history[-1]["status"] = "Erreur"
|
config.history[-1]["status"] = "Erreur"
|
||||||
config.history[-1]["progress"] = 0
|
config.history[-1]["progress"] = 0
|
||||||
config.history[-1]["message"] = "API NOT FOUND"
|
config.history[-1]["message"] = "API NOT FOUND"
|
||||||
save_history(config.history)
|
save_history(config.history)
|
||||||
config.needs_redraw = True
|
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
|
config.pending_download = None
|
||||||
continue
|
continue
|
||||||
pending = check_extension_before_download(url, platform_name, game_name)
|
pending = check_extension_before_download(url, platform_name, game_name)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ except Exception:
|
|||||||
pygame = None # type: ignore
|
pygame = None # type: ignore
|
||||||
|
|
||||||
# Version actuelle de l'application
|
# Version actuelle de l'application
|
||||||
app_version = "2.2.1.0"
|
app_version = "2.2.2.0"
|
||||||
|
|
||||||
|
|
||||||
def get_application_root():
|
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
|
# Séparation chemin / valeur pour éviter les confusions lors du chargement
|
||||||
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
|
API_KEY_1FICHIER_PATH = os.path.join(SAVE_FOLDER, "1FichierAPI.txt")
|
||||||
API_KEY_ALLDEBRID_PATH = os.path.join(SAVE_FOLDER, "AllDebridAPI.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_*).
|
# Valeurs chargées (remplies dynamiquement par utils.load_api_key_*).
|
||||||
API_KEY_1FICHIER = ""
|
API_KEY_1FICHIER = ""
|
||||||
API_KEY_ALLDEBRID = ""
|
API_KEY_ALLDEBRID = ""
|
||||||
|
API_KEY_REALDEBRID = ""
|
||||||
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
|
RGSX_SETTINGS_PATH = os.path.join(SAVE_FOLDER, "rgsx_settings.json")
|
||||||
|
|
||||||
# URL
|
# URL
|
||||||
|
|||||||
@@ -589,8 +589,9 @@ def handle_controls(event, sources, joystick, screen):
|
|||||||
config.current_history_item = len(config.history) -1
|
config.current_history_item = len(config.history) -1
|
||||||
task_id = str(pygame.time.get_ticks())
|
task_id = str(pygame.time.get_ticks())
|
||||||
if is_1fichier_url(url):
|
if is_1fichier_url(url):
|
||||||
keys = load_api_keys()
|
from utils import ensure_download_provider_keys, missing_all_provider_keys
|
||||||
if not keys.get('1fichier') and not keys.get('alldebrid'):
|
ensure_download_provider_keys(False)
|
||||||
|
if missing_all_provider_keys():
|
||||||
config.history[-1]["status"] = "Erreur"
|
config.history[-1]["status"] = "Erreur"
|
||||||
config.history[-1]["message"] = "API NOT FOUND"
|
config.history[-1]["message"] = "API NOT FOUND"
|
||||||
save_history(config.history)
|
save_history(config.history)
|
||||||
@@ -625,22 +626,22 @@ def handle_controls(event, sources, joystick, screen):
|
|||||||
config.current_history_item = len(config.history) - 1
|
config.current_history_item = len(config.history) - 1
|
||||||
# Vérifier d'abord si c'est un lien 1fichier
|
# Vérifier d'abord si c'est un lien 1fichier
|
||||||
if is_1fichier_url(url):
|
if is_1fichier_url(url):
|
||||||
keys = load_api_keys()
|
from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
|
||||||
if not keys.get('1fichier') and not keys.get('alldebrid'):
|
ensure_download_provider_keys(False)
|
||||||
|
if missing_all_provider_keys():
|
||||||
config.previous_menu_state = config.menu_state
|
config.previous_menu_state = config.menu_state
|
||||||
config.menu_state = "error"
|
config.menu_state = "error"
|
||||||
try:
|
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(build_provider_paths_string())
|
||||||
config.error_message = _("error_api_key").format(both_paths)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur lors de la traduction de error_api_key: {str(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]["status"] = "Erreur"
|
||||||
config.history[-1]["progress"] = 0
|
config.history[-1]["progress"] = 0
|
||||||
config.history[-1]["message"] = "API NOT FOUND"
|
config.history[-1]["message"] = "API NOT FOUND"
|
||||||
save_history(config.history)
|
save_history(config.history)
|
||||||
config.needs_redraw = True
|
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
|
config.pending_download = None
|
||||||
return action
|
return action
|
||||||
config.pending_download = check_extension_before_download(url, platform, game_name)
|
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
|
config.current_history_item = len(config.history) - 1
|
||||||
if is_1fichier_url(url):
|
if is_1fichier_url(url):
|
||||||
keys = load_api_keys()
|
from utils import ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
|
||||||
if not keys.get('1fichier') and not keys.get('alldebrid'):
|
ensure_download_provider_keys(False)
|
||||||
|
if missing_all_provider_keys():
|
||||||
config.previous_menu_state = config.menu_state
|
config.previous_menu_state = config.menu_state
|
||||||
config.menu_state = "error"
|
config.menu_state = "error"
|
||||||
try:
|
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(build_provider_paths_string())
|
||||||
config.error_message = _("error_api_key").format(both_paths)
|
|
||||||
except Exception:
|
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]["status"] = "Erreur"
|
||||||
config.history[-1]["progress"] = 0
|
config.history[-1]["progress"] = 0
|
||||||
config.history[-1]["message"] = "API NOT FOUND"
|
config.history[-1]["message"] = "API NOT FOUND"
|
||||||
save_history(config.history)
|
save_history(config.history)
|
||||||
config.needs_redraw = True
|
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
|
config.pending_download = None
|
||||||
return action
|
return action
|
||||||
task_id = str(pygame.time.get_ticks())
|
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
|
config.current_history_item = len(config.history) -1
|
||||||
task_id = str(pygame.time.get_ticks())
|
task_id = str(pygame.time.get_ticks())
|
||||||
if is_1fichier_url(url):
|
if is_1fichier_url(url):
|
||||||
keys = load_api_keys()
|
from utils import ensure_download_provider_keys, missing_all_provider_keys
|
||||||
if not keys.get('1fichier') and not keys.get('alldebrid'):
|
ensure_download_provider_keys(False)
|
||||||
|
if missing_all_provider_keys():
|
||||||
config.history[-1]["status"] = "Erreur"
|
config.history[-1]["status"] = "Erreur"
|
||||||
config.history[-1]["message"] = "API NOT FOUND"
|
config.history[-1]["message"] = "API NOT FOUND"
|
||||||
save_history(config.history)
|
save_history(config.history)
|
||||||
@@ -874,8 +876,9 @@ def handle_controls(event, sources, joystick, screen):
|
|||||||
config.current_history_item = len(config.history) -1
|
config.current_history_item = len(config.history) -1
|
||||||
task_id = str(pygame.time.get_ticks())
|
task_id = str(pygame.time.get_ticks())
|
||||||
if is_1fichier_url(url):
|
if is_1fichier_url(url):
|
||||||
keys = load_api_keys()
|
from utils import ensure_download_provider_keys, missing_all_provider_keys
|
||||||
if not keys.get('1fichier') and not keys.get('alldebrid'):
|
ensure_download_provider_keys(False)
|
||||||
|
if missing_all_provider_keys():
|
||||||
config.history[-1]["status"] = "Erreur"
|
config.history[-1]["status"] = "Erreur"
|
||||||
config.history[-1]["message"] = "API NOT FOUND"
|
config.history[-1]["message"] = "API NOT FOUND"
|
||||||
save_history(config.history)
|
save_history(config.history)
|
||||||
@@ -956,6 +959,9 @@ def handle_controls(event, sources, joystick, screen):
|
|||||||
return action
|
return action
|
||||||
# Retour à l'origine capturée si disponible sinon previous_menu_state
|
# Retour à l'origine capturée si disponible sinon previous_menu_state
|
||||||
target = getattr(config, 'history_origin', getattr(config, 'previous_menu_state', 'platform'))
|
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)
|
config.menu_state = validate_menu_state(target)
|
||||||
if hasattr(config, 'history_origin'):
|
if hasattr(config, 'history_origin'):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -427,7 +427,19 @@ def draw_platform_grid(screen):
|
|||||||
platform_name = config.platform_names.get(platform, platform)
|
platform_name = config.platform_names.get(platform, platform)
|
||||||
|
|
||||||
# Affichage du titre avec animation subtile
|
# 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_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 = title_surface.get_rect(center=(config.screen_width // 2, title_surface.get_height() // 2 + 20))
|
||||||
title_rect_inflated = title_rect.inflate(60, 30)
|
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)
|
pygame.draw.rect(screen, THEME_COLORS["border"], title_rect_inflated, 2, border_radius=12)
|
||||||
screen.blit(title_surface, title_rect)
|
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 = {
|
column_width_percentages = {
|
||||||
"platform": 0.20, # platform column
|
"platform": 0.15, # narrower platform column
|
||||||
"game_name": 0.50, # game name column
|
"game_name": 0.45, # game name column
|
||||||
"size": 0.10, # size column
|
"size": 0.10, # size column remains compact
|
||||||
"status": 0.20 # status column
|
"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
|
available_width = int(0.95 * config.screen_width - 60) # Total available width for columns
|
||||||
col_platform_width = int(available_width * column_width_percentages["platform"])
|
col_platform_width = int(available_width * column_width_percentages["platform"])
|
||||||
@@ -886,36 +898,51 @@ def draw_history_list(screen):
|
|||||||
|
|
||||||
status = entry.get("status", "Inconnu")
|
status = entry.get("status", "Inconnu")
|
||||||
progress = entry.get("progress", 0)
|
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"]:
|
if status in ["Téléchargement", "downloading"]:
|
||||||
status_text = _("history_status_downloading").format(progress)
|
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":
|
elif status == "Extracting":
|
||||||
status_text = _("history_status_extracting").format(progress)
|
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":
|
elif status == "Download_OK":
|
||||||
|
# Completed: no provider prefix (per requirement)
|
||||||
status_text = _("history_status_completed")
|
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":
|
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":
|
elif status == "Canceled":
|
||||||
status_text = _("history_status_canceled")
|
status_text = _("history_status_canceled")
|
||||||
else:
|
else:
|
||||||
status_text = status
|
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)
|
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)
|
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)
|
platform_surface = config.small_font.render(platform_text, True, color)
|
||||||
game_surface = config.small_font.render(game_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
|
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))
|
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))
|
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))
|
screen.blit(OVERLAY, (0,0))
|
||||||
from utils import load_api_keys
|
from utils import load_api_keys
|
||||||
keys = load_api_keys()
|
keys = load_api_keys()
|
||||||
# Layout simple
|
title = _("api_keys_status_title") if _ else "API Keys Status"
|
||||||
lines = [
|
# Préparer données avec masquage partiel des clés (afficher 4 premiers et 2 derniers caractères si longueur > 10)
|
||||||
_("api_keys_status_title") if _ else "API Keys Status",
|
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')),
|
("1fichier", keys.get('1fichier')),
|
||||||
("AllDebrid", keys.get('alldebrid'))
|
("AllDebrid", keys.get('alldebrid')),
|
||||||
|
("RealDebrid", keys.get('realdebrid'))
|
||||||
]
|
]
|
||||||
menu_width = int(config.screen_width * 0.55)
|
# Dimensions dynamiques en fonction du contenu
|
||||||
menu_height = int(config.screen_height * 0.35)
|
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_x = (config.screen_width - menu_width)//2
|
||||||
menu_y = (config.screen_height - menu_height)//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["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=16)
|
pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=22)
|
||||||
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))
|
# 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)
|
screen.blit(title_surface, title_rect)
|
||||||
status_on = _("status_present") if _ else "Present"
|
|
||||||
status_off = _("status_missing") if _ else "Missing"
|
status_present_txt = _("status_present") if _ else "Present"
|
||||||
y = title_rect.bottom + 20
|
status_missing_txt = _("status_missing") if _ else "Missing"
|
||||||
for provider, present in lines[1:]:
|
# Plus de légende textuelle Présent / Missing (demandé) – seules les pastilles couleur serviront.
|
||||||
status_txt = status_on if present else status_off
|
legend_rect = pygame.Rect(0,0,0,0)
|
||||||
text = f"{provider}: {status_txt}"
|
|
||||||
surf = config.small_font.render(text, True, THEME_COLORS["text"])
|
# Colonnes: Provider | Status badge | (key masked)
|
||||||
rect = surf.get_rect(center=(config.screen_width//2, y))
|
col_provider_x = menu_x + 40
|
||||||
screen.blit(surf, rect)
|
col_status_x = menu_x + int(menu_width * 0.40)
|
||||||
y += surf.get_height() + 12
|
col_key_x = menu_x + int(menu_width * 0.58)
|
||||||
back_txt = _("menu_back") if _ else "Back"
|
|
||||||
back_surf = config.small_font.render(back_txt, True, THEME_COLORS["fond_lignes"]) # Indication
|
# Démarrage des lignes sous le titre avec un padding
|
||||||
back_rect = back_surf.get_rect(center=(config.screen_width//2, menu_y + menu_height - 30))
|
y = title_rect.bottom + 24
|
||||||
screen.blit(back_surf, back_rect)
|
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):
|
def draw_filter_platforms_menu(screen):
|
||||||
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""
|
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""
|
||||||
|
|||||||
@@ -147,5 +147,7 @@
|
|||||||
"status_missing": "Fehlt",
|
"status_missing": "Fehlt",
|
||||||
"menu_api_keys_status": "API-Schlüssel",
|
"menu_api_keys_status": "API-Schlüssel",
|
||||||
"api_keys_status_title": "Status der 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",
|
"status_missing": "Missing",
|
||||||
"menu_api_keys_status": "API Keys",
|
"menu_api_keys_status": "API Keys",
|
||||||
"api_keys_status_title": "API Keys Status",
|
"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",
|
"status_missing": "Ausente",
|
||||||
"menu_api_keys_status": "Claves API",
|
"menu_api_keys_status": "Claves API",
|
||||||
"api_keys_status_title": "Estado de las 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",
|
"status_missing": "Absente",
|
||||||
"menu_api_keys_status": "Clés API",
|
"menu_api_keys_status": "Clés API",
|
||||||
"api_keys_status_title": "Statut des 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",
|
"status_missing": "Assente",
|
||||||
"menu_api_keys_status": "Chiavi API",
|
"menu_api_keys_status": "Chiavi API",
|
||||||
"api_keys_status_title": "Stato delle 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",
|
"status_missing": "Ausente",
|
||||||
"menu_api_keys_status": "Chaves API",
|
"menu_api_keys_status": "Chaves API",
|
||||||
"api_keys_status_title": "Status das 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.current_loading_system = _("network_checking_updates")
|
||||||
config.loading_progress = 5.0
|
config.loading_progress = 5.0
|
||||||
config.needs_redraw = True
|
config.needs_redraw = True
|
||||||
|
|
||||||
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
|
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
if response.headers.get("content-type") != "application/json":
|
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()
|
version_data = response.json()
|
||||||
latest_version = version_data.get("version")
|
latest_version = version_data.get("version")
|
||||||
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_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")
|
UPDATE_ZIP = OTA_UPDATE_ZIP.replace("RGSX.zip", f"RGSX_v{latest_version}.zip")
|
||||||
logger.debug(f"URL de mise à jour : {UPDATE_ZIP}")
|
logger.debug(f"URL de mise à jour : {UPDATE_ZIP}")
|
||||||
|
|
||||||
if latest_version != config.app_version:
|
if latest_version != config.app_version:
|
||||||
config.current_loading_system = _("network_update_available").format(latest_version)
|
config.current_loading_system = _("network_update_available").format(latest_version)
|
||||||
config.loading_progress = 10.0
|
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()
|
keys_info = load_api_keys()
|
||||||
config.API_KEY_1FICHIER = keys_info.get('1fichier', '')
|
config.API_KEY_1FICHIER = keys_info.get('1fichier', '')
|
||||||
config.API_KEY_ALLDEBRID = keys_info.get('alldebrid', '')
|
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:
|
if not config.API_KEY_1FICHIER and config.API_KEY_ALLDEBRID:
|
||||||
logger.debug("Clé 1fichier absente, utilisation fallback AllDebrid")
|
logger.debug("Clé 1fichier absente, utilisation fallback AllDebrid")
|
||||||
elif not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID:
|
if not config.API_KEY_1FICHIER and not config.API_KEY_ALLDEBRID and config.API_KEY_REALDEBRID:
|
||||||
logger.debug("Aucune clé API disponible (1fichier ni AllDebrid)")
|
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"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]
|
result = [None, None]
|
||||||
|
|
||||||
# Créer une queue spécifique pour cette tâche
|
# 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:
|
if task_id not in cancel_events:
|
||||||
cancel_events[task_id] = threading.Event()
|
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():
|
def download_thread():
|
||||||
logger.debug(f"Thread téléchargement 1fichier démarré pour {url}, task_id={task_id}")
|
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:
|
try:
|
||||||
cancel_ev = cancel_events.get(task_id)
|
cancel_ev = cancel_events.get(task_id)
|
||||||
link = url.split('&af=')[0]
|
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}")
|
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)
|
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}")
|
logger.debug(f"Réponse file/info reçue, code: {response.status_code}")
|
||||||
response.raise_for_status()
|
file_info = None
|
||||||
file_info = response.json()
|
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":
|
if "error" in file_info and file_info["error"] == "Resource not found":
|
||||||
logger.error(f"Le fichier {game_name} n'existe pas sur 1fichier")
|
logger.error(f"Le fichier {game_name} n'existe pas sur 1fichier")
|
||||||
result[0] = False
|
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
|
return
|
||||||
filename = file_info.get("filename", "").strip()
|
filename = file_info.get("filename", "").strip()
|
||||||
if not filename:
|
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"Chemin destination: {dest_path}")
|
||||||
logger.debug(f"Envoi requête 1fichier get_token pour {link}")
|
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)
|
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()
|
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")
|
final_url = download_info.get("url")
|
||||||
if not final_url:
|
if not final_url:
|
||||||
logger.error("Impossible de récupérer l'URL de téléchargement")
|
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")
|
result[1] = _("network_cannot_get_download_url")
|
||||||
return
|
return
|
||||||
logger.debug(f"URL de téléchargement obtenue via 1fichier: {final_url}")
|
logger.debug(f"URL de téléchargement obtenue via 1fichier: {final_url}")
|
||||||
|
provider_used = '1F'
|
||||||
|
_set_provider_in_history(provider_used)
|
||||||
else:
|
else:
|
||||||
# AllDebrid: débrider l'URL 1fichier vers une URL directe
|
final_url = None
|
||||||
logger.debug("Mode téléchargement sélectionné: AllDebrid (fallback, débridage 1fichier)")
|
filename = None
|
||||||
if not getattr(config, 'API_KEY_ALLDEBRID', ''):
|
# Tentative AllDebrid
|
||||||
logger.error("Aucune clé API (1fichier/AllDebrid) disponible")
|
if getattr(config, 'API_KEY_ALLDEBRID', ''):
|
||||||
result[0] = False
|
logger.debug("Mode téléchargement sélectionné: AllDebrid (fallback 1)")
|
||||||
result[1] = _("network_api_error").format("Missing API key") if _ else "API key missing"
|
try:
|
||||||
return
|
ad_key = config.API_KEY_ALLDEBRID
|
||||||
ad_key = config.API_KEY_ALLDEBRID
|
params = {'agent': 'RGSX', 'apikey': ad_key, 'link': link}
|
||||||
# AllDebrid API v4 example: GET https://api.alldebrid.com/v4/link/unlock?agent=<app>&apikey=<key>&link=<url>
|
logger.debug("Requête AllDebrid link/unlock en cours")
|
||||||
params = {
|
response = requests.get("https://api.alldebrid.com/v4/link/unlock", params=params, timeout=30)
|
||||||
'agent': 'RGSX',
|
logger.debug(f"Réponse AllDebrid reçue, code: {response.status_code}")
|
||||||
'apikey': ad_key,
|
response.raise_for_status()
|
||||||
'link': link
|
ad_json = response.json()
|
||||||
}
|
if ad_json.get('status') == 'success':
|
||||||
logger.debug("Requête AllDebrid link/unlock en cours")
|
data = ad_json.get('data', {})
|
||||||
response = requests.get("https://api.alldebrid.com/v4/link/unlock", params=params, timeout=30)
|
filename = data.get('filename') or game_name
|
||||||
logger.debug(f"Réponse AllDebrid reçue, code: {response.status_code}")
|
final_url = data.get('link') or data.get('download') or data.get('streamingLink')
|
||||||
response.raise_for_status()
|
if final_url:
|
||||||
ad_json = response.json()
|
logger.debug("Débridage réussi via AllDebrid")
|
||||||
if ad_json.get('status') != 'success':
|
provider_used = 'AD'
|
||||||
err = ad_json.get('error', {}).get('code') or ad_json
|
_set_provider_in_history(provider_used)
|
||||||
logger.error(f"AllDebrid échec débridage: {err}")
|
else:
|
||||||
result[0] = False
|
logger.warning(f"AllDebrid status != success: {ad_json}")
|
||||||
result[1] = _("network_api_error").format(f"AllDebrid unlock failed: {err}") if _ else f"AllDebrid unlock failed: {err}"
|
except Exception as e:
|
||||||
return
|
logger.error(f"Erreur AllDebrid fallback: {e}")
|
||||||
data = ad_json.get('data', {})
|
# Tentative RealDebrid si pas de final_url
|
||||||
filename = data.get('filename') or game_name
|
if not final_url and getattr(config, 'API_KEY_REALDEBRID', ''):
|
||||||
final_url = data.get('link') or data.get('download') or data.get('streamingLink')
|
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:
|
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[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
|
return
|
||||||
|
if not filename:
|
||||||
|
filename = game_name
|
||||||
sanitized_filename = sanitize_filename(filename)
|
sanitized_filename = sanitize_filename(filename)
|
||||||
dest_path = os.path.join(dest_dir, sanitized_filename)
|
dest_path = os.path.join(dest_dir, sanitized_filename)
|
||||||
logger.debug(f"URL directe obtenue via AllDebrid: {final_url}")
|
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
retries = 10
|
retries = 10
|
||||||
retry_delay = 10
|
retry_delay = 10
|
||||||
|
|||||||
@@ -342,35 +342,6 @@ def _get_dest_folder_name(platform_key: str) -> str:
|
|||||||
# Fonction pour charger sources.json
|
# Fonction pour charger sources.json
|
||||||
def load_sources():
|
def load_sources():
|
||||||
try:
|
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 = []
|
sources = []
|
||||||
if os.path.exists(config.SOURCES_FILE):
|
if os.path.exists(config.SOURCES_FILE):
|
||||||
with open(config.SOURCES_FILE, 'r', encoding='utf-8') as f:
|
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
|
config.needs_redraw = True # Forcer le redraw pour afficher le nom de la musique
|
||||||
|
|
||||||
def load_api_keys(force: bool = False):
|
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
|
- 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
|
- Utilise un cache basé sur le mtime pour éviter des relectures
|
||||||
- force=True ignore le cache et relit systématiquement
|
- 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:
|
try:
|
||||||
paths = {
|
paths = {
|
||||||
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
|
'1fichier': getattr(config, 'API_KEY_1FICHIER_PATH', ''),
|
||||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
|
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID_PATH', ''),
|
||||||
|
'realdebrid': getattr(config, 'API_KEY_REALDEBRID_PATH', ''),
|
||||||
}
|
}
|
||||||
cache_attr = '_api_keys_cache'
|
cache_attr = '_api_keys_cache'
|
||||||
if not hasattr(config, cache_attr):
|
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)
|
cache_data = getattr(config, cache_attr)
|
||||||
reloaded = False
|
reloaded = False
|
||||||
|
|
||||||
@@ -1299,13 +1271,16 @@ def load_api_keys(force: bool = False):
|
|||||||
# Assignation dans config
|
# Assignation dans config
|
||||||
if key_name == '1fichier':
|
if key_name == '1fichier':
|
||||||
config.API_KEY_1FICHIER = value
|
config.API_KEY_1FICHIER = value
|
||||||
else:
|
elif key_name == 'alldebrid':
|
||||||
config.API_KEY_ALLDEBRID = value
|
config.API_KEY_ALLDEBRID = value
|
||||||
|
elif key_name == 'realdebrid':
|
||||||
|
config.API_KEY_REALDEBRID = value
|
||||||
cache_data[cache_key] = mtime
|
cache_data[cache_key] = mtime
|
||||||
reloaded = True
|
reloaded = True
|
||||||
return {
|
return {
|
||||||
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
||||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
||||||
|
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
|
||||||
'reloaded': reloaded
|
'reloaded': reloaded
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1313,6 +1288,7 @@ def load_api_keys(force: bool = False):
|
|||||||
return {
|
return {
|
||||||
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
'1fichier': getattr(config, 'API_KEY_1FICHIER', ''),
|
||||||
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
'alldebrid': getattr(config, 'API_KEY_ALLDEBRID', ''),
|
||||||
|
'realdebrid': getattr(config, 'API_KEY_REALDEBRID', ''),
|
||||||
'reloaded': False
|
'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
|
def load_api_key_alldebrid(force: bool = False): # pragma: no cover
|
||||||
return load_api_keys(force).get('alldebrid', '')
|
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
|
# Ancien nom conservé comme alias
|
||||||
def ensure_api_keys_loaded(force: bool = False): # pragma: no cover
|
def ensure_api_keys_loaded(force: bool = False): # pragma: no cover
|
||||||
return load_api_keys(force)
|
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():
|
def load_music_config():
|
||||||
"""Charge la configuration musique depuis rgsx_settings.json."""
|
"""Charge la configuration musique depuis rgsx_settings.json."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user