feat: add game assets downloading from repository
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -11,6 +11,7 @@ import psutil
|
|||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
|
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
|
||||||
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
from portprotonqt.input_manager import InputManager
|
from portprotonqt.input_manager import InputManager
|
||||||
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
|
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
|
||||||
from portprotonqt.system_overlay import SystemOverlay
|
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.legendary_path = os.path.join(self.legendary_config_path, "legendary")
|
||||||
self.downloader = Downloader(max_workers=4)
|
self.downloader = Downloader(max_workers=4)
|
||||||
|
self.portproton_api = PortProtonAPI(self.downloader)
|
||||||
|
|
||||||
# Статус-бар
|
# Статус-бар
|
||||||
self.setStatusBar(QStatusBar(self))
|
self.setStatusBar(QStatusBar(self))
|
||||||
@ -475,7 +477,19 @@ class MainWindow(QMainWindow):
|
|||||||
user_game_folder = os.path.join(user_custom_folder, exe_name)
|
user_game_folder = os.path.join(user_custom_folder, exe_name)
|
||||||
os.makedirs(user_game_folder, exist_ok=True)
|
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()
|
builtin_files = set(os.listdir(builtin_game_folder)) if os.path.exists(builtin_game_folder) else set()
|
||||||
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
|
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
|
||||||
candidate = f"cover{ext}"
|
candidate = f"cover{ext}"
|
||||||
@ -490,7 +504,7 @@ class MainWindow(QMainWindow):
|
|||||||
user_cover = os.path.join(user_game_folder, candidate)
|
user_cover = os.path.join(user_game_folder, candidate)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Чтение статистики
|
# Read statistics
|
||||||
if self.portproton_location:
|
if self.portproton_location:
|
||||||
statistics_file = os.path.join(self.portproton_location, "data", "tmp", "statistics")
|
statistics_file = os.path.join(self.portproton_location, "data", "tmp", "statistics")
|
||||||
try:
|
try:
|
||||||
@ -503,17 +517,17 @@ class MainWindow(QMainWindow):
|
|||||||
playtime_seconds = playtime_data[matching_key]
|
playtime_seconds = playtime_data[matching_key]
|
||||||
formatted_playtime = format_playtime(playtime_seconds)
|
formatted_playtime = format_playtime(playtime_seconds)
|
||||||
except Exception as e:
|
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):
|
def on_steam_info(steam_info: dict):
|
||||||
# Определяем текущий язык
|
# Get current language
|
||||||
language_code = get_egs_language()
|
language_code = get_egs_language()
|
||||||
|
|
||||||
# Чтение переводов из metadata.txt
|
# Read translations from metadata.txt
|
||||||
user_metadata_file = os.path.join(user_game_folder, "metadata.txt")
|
user_metadata_file = os.path.join(user_game_folder, "metadata.txt")
|
||||||
builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt")
|
builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt")
|
||||||
|
|
||||||
# Сначала пытаемся загрузить пользовательские переводы
|
# Try user translations first
|
||||||
translations = {'name': desktop_name, 'description': ''}
|
translations = {'name': desktop_name, 'description': ''}
|
||||||
if os.path.exists(user_metadata_file):
|
if os.path.exists(user_metadata_file):
|
||||||
translations = read_metadata_translations(user_metadata_file, language_code)
|
translations = read_metadata_translations(user_metadata_file, language_code)
|
||||||
|
125
portprotonqt/portproton_api.py
Normal file
125
portprotonqt/portproton_api.py
Normal file
@ -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)
|
Reference in New Issue
Block a user