diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 4d59595..ccfb068 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -11,6 +11,7 @@ import psutil from portprotonqt.dialogs import AddGameDialog, FileExplorer from portprotonqt.game_card import GameCard from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel +from portprotonqt.portproton_api import PortProtonAPI from portprotonqt.input_manager import InputManager from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit from portprotonqt.system_overlay import SystemOverlay @@ -120,6 +121,7 @@ class MainWindow(QMainWindow): self.legendary_path = os.path.join(self.legendary_config_path, "legendary") self.downloader = Downloader(max_workers=4) + self.portproton_api = PortProtonAPI(self.downloader) # Статус-бар self.setStatusBar(QStatusBar(self)) @@ -466,8 +468,8 @@ class MainWindow(QMainWindow): builtin_cover = "" user_cover = "" - user_game_folder="" - builtin_game_folder="" + user_game_folder = "" + builtin_game_folder = "" if game_exe: exe_name = os.path.splitext(os.path.basename(game_exe))[0] @@ -475,7 +477,19 @@ class MainWindow(QMainWindow): user_game_folder = os.path.join(user_custom_folder, exe_name) os.makedirs(user_game_folder, exist_ok=True) - # Чтение обложки + # Check if local game folder is empty and download assets if it is + if not os.listdir(user_game_folder): + logger.debug(f"Local folder for {exe_name} is empty, checking repository") + def on_assets_downloaded(results): + nonlocal user_cover + if results["cover"]: + user_cover = results["cover"] + logger.info(f"Downloaded assets for {exe_name}: {results}") + if results["metadata"]: + logger.info(f"Downloaded metadata for {exe_name}: {results['metadata']}") + self.portproton_api.download_game_assets_async(exe_name, timeout=5, callback=on_assets_downloaded) + + # Read cover builtin_files = set(os.listdir(builtin_game_folder)) if os.path.exists(builtin_game_folder) else set() for ext in [".jpg", ".png", ".jpeg", ".bmp"]: candidate = f"cover{ext}" @@ -490,7 +504,7 @@ class MainWindow(QMainWindow): user_cover = os.path.join(user_game_folder, candidate) break - # Чтение статистики + # Read statistics if self.portproton_location: statistics_file = os.path.join(self.portproton_location, "data", "tmp", "statistics") try: @@ -503,17 +517,17 @@ class MainWindow(QMainWindow): playtime_seconds = playtime_data[matching_key] formatted_playtime = format_playtime(playtime_seconds) except Exception as e: - print(f"Failed to parse playtime data: {e}") + logger.error(f"Failed to parse playtime data: {e}") def on_steam_info(steam_info: dict): - # Определяем текущий язык + # Get current language language_code = get_egs_language() - # Чтение переводов из metadata.txt + # Read translations from metadata.txt user_metadata_file = os.path.join(user_game_folder, "metadata.txt") builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt") - # Сначала пытаемся загрузить пользовательские переводы + # Try user translations first translations = {'name': desktop_name, 'description': ''} if os.path.exists(user_metadata_file): translations = read_metadata_translations(user_metadata_file, language_code) diff --git a/portprotonqt/portproton_api.py b/portprotonqt/portproton_api.py new file mode 100644 index 0000000..0569b74 --- /dev/null +++ b/portprotonqt/portproton_api.py @@ -0,0 +1,125 @@ +import os +import requests +from collections.abc import Callable +from portprotonqt.downloader import Downloader, download_with_cache +from portprotonqt.logger import get_logger + +logger = get_logger(__name__) + +class PortProtonAPI: + """API to fetch game assets (cover, metadata) from the PortProtonQt repository.""" + def __init__(self, downloader: Downloader | None = None): + self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data" + self.downloader = downloader or Downloader(max_workers=4) + self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) + self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data") + os.makedirs(self.custom_data_dir, exist_ok=True) + + def _get_game_dir(self, exe_name: str) -> str: + game_dir = os.path.join(self.custom_data_dir, exe_name) + os.makedirs(game_dir, exist_ok=True) + return game_dir + + def _check_file_exists(self, url: str, timeout: int = 5) -> bool: + try: + response = requests.head(url, timeout=timeout) + response.raise_for_status() + return response.status_code == 200 + except requests.RequestException as e: + logger.debug(f"Failed to check file at {url}: {e}") + return False + + def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]: + game_dir = self._get_game_dir(exe_name) + results: dict[str, str | None] = {"cover": None, "metadata": None} + cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"] + cover_url_base = f"{self.base_url}/{exe_name}/cover" + metadata_url = f"{self.base_url}/{exe_name}/metadata.txt" + + for ext in cover_extensions: + cover_url = f"{cover_url_base}{ext}" + if self._check_file_exists(cover_url, timeout): + local_cover_path = os.path.join(game_dir, f"cover{ext}") + result = download_with_cache(cover_url, local_cover_path, timeout, self.downloader) + if result: + results["cover"] = result + logger.info(f"Downloaded cover for {exe_name} to {result}") + break + else: + logger.error(f"Failed to download cover for {exe_name} from {cover_url}") + else: + logger.debug(f"No cover found for {exe_name} with extension {ext}") + + if self._check_file_exists(metadata_url, timeout): + local_metadata_path = os.path.join(game_dir, "metadata.txt") + result = download_with_cache(metadata_url, local_metadata_path, timeout, self.downloader) + if result: + results["metadata"] = result + logger.info(f"Downloaded metadata for {exe_name} to {result}") + else: + logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}") + else: + logger.debug(f"No metadata found for {exe_name}") + + return results + + def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None: + game_dir = self._get_game_dir(exe_name) + cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"] + cover_url_base = f"{self.base_url}/{exe_name}/cover" + metadata_url = f"{self.base_url}/{exe_name}/metadata.txt" + + results: dict[str, str | None] = {"cover": None, "metadata": None} + pending_downloads = 0 + + def on_cover_downloaded(local_path: str | None, ext: str): + nonlocal pending_downloads + if local_path: + logger.info(f"Async cover downloaded for {exe_name}: {local_path}") + results["cover"] = local_path + else: + logger.debug(f"No cover downloaded for {exe_name} with extension {ext}") + pending_downloads -= 1 + check_completion() + + def on_metadata_downloaded(local_path: str | None): + nonlocal pending_downloads + if local_path: + logger.info(f"Async metadata downloaded for {exe_name}: {local_path}") + results["metadata"] = local_path + else: + logger.debug(f"No metadata downloaded for {exe_name}") + pending_downloads -= 1 + check_completion() + + def check_completion(): + if pending_downloads == 0 and callback: + callback(results) + + for ext in cover_extensions: + cover_url = f"{cover_url_base}{ext}" + if self._check_file_exists(cover_url, timeout): + local_cover_path = os.path.join(game_dir, f"cover{ext}") + pending_downloads += 1 + self.downloader.download_async( + cover_url, + local_cover_path, + timeout=timeout, + callback=lambda path, ext=ext: on_cover_downloaded(path, ext) + ) + break + + if self._check_file_exists(metadata_url, timeout): + local_metadata_path = os.path.join(game_dir, "metadata.txt") + pending_downloads += 1 + self.downloader.download_async( + metadata_url, + local_metadata_path, + timeout=timeout, + callback=on_metadata_downloaded + ) + + if pending_downloads == 0: + logger.debug(f"No assets found for {exe_name}") + if callback: + callback(results)