feat: added playtime and last launch to EGS
All checks were successful
Code and build check / Check code (pull_request) Successful in 1m50s
Code and build check / Build with uv (pull_request) Successful in 54s

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-06-16 23:11:43 +05:00
parent 5a1ac2206b
commit a6ea998535

View File

@ -12,6 +12,10 @@ from collections.abc import Callable
from portprotonqt.localization import get_egs_language, _ from portprotonqt.localization import get_egs_language, _
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.image_utils import load_pixmap_async from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
from portprotonqt.config_utils import get_portproton_location
from portprotonqt.steam_api import get_weanticheatyet_status_async
from PySide6.QtGui import QPixmap from PySide6.QtGui import QPixmap
logger = get_logger(__name__) logger = get_logger(__name__)
@ -27,7 +31,6 @@ def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "") executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "")
if install_path and executable: if install_path and executable:
return os.path.join(install_path, executable) return os.path.join(install_path, executable)
logger.warning(f"No executable found for EGS app_name: {app_name}")
return None return None
except FileNotFoundError: except FileNotFoundError:
logger.error(f"installed.json not found at {installed_json_path}") logger.error(f"installed.json not found at {installed_json_path}")
@ -304,6 +307,7 @@ def get_egs_game_description_async(
thread = threading.Thread(target=fetch_description, daemon=True) thread = threading.Thread(target=fetch_description, daemon=True)
thread.start() thread.start()
def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]): def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]):
""" """
Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback. Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback.
@ -349,6 +353,7 @@ def run_legendary_list_async(legendary_path: str, callback: Callable[[list | Non
def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]): def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
""" """
Асинхронно загружает Epic Games Store игры с использованием legendary CLI. Асинхронно загружает Epic Games Store игры с использованием legendary CLI.
Читает статистику времени игры и последнего запуска из файла statistics.
""" """
logger.debug("Starting to load Epic Games Store games") logger.debug("Starting to load Epic Games Store games")
games: list[tuple] = [] games: list[tuple] = []
@ -357,6 +362,14 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
cache_file = cache_dir / "legendary_games.json" cache_file = cache_dir / "legendary_games.json"
cache_ttl = 3600 # Cache TTL in seconds (1 hour) cache_ttl = 3600 # Cache TTL in seconds (1 hour)
# Путь к файлу statistics
portproton_location = get_portproton_location()
if portproton_location is None:
logger.error("PortProton location is not set, cannot locate statistics file")
statistics_file = ""
else:
statistics_file = os.path.join(portproton_location, "data", "tmp", "statistics")
if not os.path.exists(legendary_path): if not os.path.exists(legendary_path):
logger.info("Legendary binary not found, downloading...") logger.info("Legendary binary not found, downloading...")
def on_legendary_downloaded(result): def on_legendary_downloaded(result):
@ -368,7 +381,7 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
logger.error(f"Failed to make legendary binary executable: {e}") logger.error(f"Failed to make legendary binary executable: {e}")
callback(games) # Return empty games list on failure callback(games) # Return empty games list on failure
return return
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message) _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file)
else: else:
logger.error("Failed to download legendary binary") logger.error("Failed to download legendary binary")
callback(games) # Return empty games list on failure callback(games) # Return empty games list on failure
@ -379,9 +392,9 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
callback(games) callback(games)
return return
else: else:
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message) _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file)
def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]): def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None], statistics_file: str):
""" """
Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI. Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI.
""" """
@ -433,6 +446,33 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
callback(final_games) callback(final_games)
return return
# Получаем путь к .exe для извлечения имени
game_exe = get_egs_executable(app_name, os.path.dirname(legendary_path))
exe_name = ""
if game_exe:
exe_name = os.path.splitext(os.path.basename(game_exe))[0]
# Читаем статистику из файла statistics
playtime_seconds = 0
formatted_playtime = ""
last_launch = _("Never")
last_launch_timestamp = 0
if exe_name and os.path.exists(statistics_file):
try:
playtime_data = parse_playtime_file(statistics_file)
matching_key = next(
(key for key in playtime_data if os.path.basename(key).split('.')[0] == exe_name),
None
)
if matching_key:
playtime_seconds = playtime_data[matching_key]
formatted_playtime = format_playtime(playtime_seconds)
except Exception as e:
logger.error(f"Failed to parse playtime data for {app_name}: {e}")
if exe_name:
last_launch = get_last_launch(exe_name) or _("Never")
last_launch_timestamp = get_last_launch_timestamp(exe_name)
metadata_file = metadata_dir / f"{app_name}.json" metadata_file = metadata_dir / f"{app_name}.json"
cover_url = "" cover_url = ""
try: try:
@ -453,7 +493,6 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
final_description = api_description or _("No description available") final_description = api_description or _("No description available")
def on_cover_loaded(pixmap: QPixmap): def on_cover_loaded(pixmap: QPixmap):
from portprotonqt.steam_api import get_weanticheatyet_status_async
def on_anticheat_status(status: str): def on_anticheat_status(status: str):
nonlocal pending_images nonlocal pending_images
with results_lock: with results_lock:
@ -464,12 +503,12 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
app_name, app_name,
f"legendary:launch:{app_name}", f"legendary:launch:{app_name}",
"", "",
_("Never"), last_launch, # Время последнего запуска
"", formatted_playtime, # Форматированное время игры
"", "",
status or "", status or "",
0, last_launch_timestamp, # Временная метка последнего запуска
0, playtime_seconds, # Время игры в секундах
"epic" "epic"
) )
pending_images -= 1 pending_images -= 1