forked from Boria138/PortProtonQt
feat: open ppdb on portproton badge click
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -9,6 +9,8 @@ from portprotonqt.config_utils import read_favorites, save_favorites, read_displ
|
|||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
from portprotonqt.config_utils import read_theme_from_config
|
||||||
from portprotonqt.custom_widgets import ClickableLabel
|
from portprotonqt.custom_widgets import ClickableLabel
|
||||||
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
|
from portprotonqt.downloader import Downloader
|
||||||
import weakref
|
import weakref
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
@ -56,6 +58,8 @@ class GameCard(QFrame):
|
|||||||
|
|
||||||
self.display_filter = read_display_filter()
|
self.display_filter = read_display_filter()
|
||||||
self.current_theme_name = read_theme_from_config()
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
self.downloader = Downloader(max_workers=4)
|
||||||
|
self.portproton_api = PortProtonAPI(self.downloader)
|
||||||
|
|
||||||
self.steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
self.steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||||
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||||
@ -194,13 +198,13 @@ class GameCard(QFrame):
|
|||||||
parent=coverWidget,
|
parent=coverWidget,
|
||||||
icon_size=icon_size,
|
icon_size=icon_size,
|
||||||
icon_space=icon_space,
|
icon_space=icon_space,
|
||||||
font_scale_factor=font_scale_factor,
|
font_scale_factor=font_scale_factor
|
||||||
change_cursor=False
|
|
||||||
)
|
)
|
||||||
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
self.portprotonLabel.setFixedWidth(badge_width)
|
self.portprotonLabel.setFixedWidth(badge_width)
|
||||||
self.portprotonLabel.setCardWidth(card_width)
|
self.portprotonLabel.setCardWidth(card_width)
|
||||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||||
|
self.portprotonLabel.clicked.connect(self.open_portproton_forum_topic)
|
||||||
|
|
||||||
# WeAntiCheatYet бейдж
|
# WeAntiCheatYet бейдж
|
||||||
anticheat_text = self.getAntiCheatText(anticheat_status)
|
anticheat_text = self.getAntiCheatText(anticheat_status)
|
||||||
@ -385,6 +389,16 @@ class GameCard(QFrame):
|
|||||||
return "broken"
|
return "broken"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def open_portproton_forum_topic(self):
|
||||||
|
"""Open the PortProton forum topic or search page for this game."""
|
||||||
|
result = self.portproton_api.get_forum_topic_slug(self.name)
|
||||||
|
base_url = "https://linux-gaming.ru/"
|
||||||
|
if result.startswith("search?q="):
|
||||||
|
url = QUrl(f"{base_url}{result}")
|
||||||
|
else:
|
||||||
|
url = QUrl(f"{base_url}t/{result}")
|
||||||
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def open_protondb_report(self):
|
def open_protondb_report(self):
|
||||||
url = QUrl(f"https://www.protondb.com/app/{self.appid}")
|
url = QUrl(f"https://www.protondb.com/app/{self.appid}")
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
@ -248,6 +248,16 @@ class MainWindow(QMainWindow):
|
|||||||
self.updateGameGrid()
|
self.updateGameGrid()
|
||||||
self.progress_bar.setVisible(False)
|
self.progress_bar.setVisible(False)
|
||||||
|
|
||||||
|
def open_portproton_forum_topic(self, topic_name: str):
|
||||||
|
"""Open the PortProton forum topic or search page for this game."""
|
||||||
|
result = self.portproton_api.get_forum_topic_slug(topic_name)
|
||||||
|
base_url = "https://linux-gaming.ru/"
|
||||||
|
if result.startswith("search?q="):
|
||||||
|
url = QUrl(f"{base_url}{result}")
|
||||||
|
else:
|
||||||
|
url = QUrl(f"{base_url}t/{result}")
|
||||||
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def _on_card_focused(self, game_name: str, is_focused: bool):
|
def _on_card_focused(self, game_name: str, is_focused: bool):
|
||||||
"""Обработчик сигнала focusChanged от GameCard."""
|
"""Обработчик сигнала focusChanged от GameCard."""
|
||||||
card_key = None
|
card_key = None
|
||||||
@ -1639,11 +1649,11 @@ class MainWindow(QMainWindow):
|
|||||||
parent=coverFrame,
|
parent=coverFrame,
|
||||||
icon_size=16,
|
icon_size=16,
|
||||||
icon_space=5,
|
icon_space=5,
|
||||||
change_cursor=False
|
|
||||||
)
|
)
|
||||||
portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
portprotonLabel.setFixedWidth(badge_width)
|
portprotonLabel.setFixedWidth(badge_width)
|
||||||
portprotonLabel.setVisible(portproton_visible)
|
portprotonLabel.setVisible(portproton_visible)
|
||||||
|
portprotonLabel.clicked.connect(lambda: self.open_portproton_forum_topic(name))
|
||||||
|
|
||||||
# WeAntiCheatYet бейдж
|
# WeAntiCheatYet бейдж
|
||||||
anticheat_text = GameCard.getAntiCheatText(anticheat_status)
|
anticheat_text = GameCard.getAntiCheatText(anticheat_status)
|
||||||
|
@ -1,19 +1,33 @@
|
|||||||
import os
|
import os
|
||||||
|
import tarfile
|
||||||
|
import orjson
|
||||||
import requests
|
import requests
|
||||||
|
import urllib.parse
|
||||||
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from portprotonqt.downloader import Downloader, download_with_cache
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||||
|
|
||||||
|
def get_cache_dir():
|
||||||
|
"""Return the cache directory path, creating it if necessary."""
|
||||||
|
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||||
|
cache_dir = os.path.join(xdg_cache_home, "PortProtonQt")
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
return cache_dir
|
||||||
|
|
||||||
class PortProtonAPI:
|
class PortProtonAPI:
|
||||||
"""API to fetch game assets (cover, metadata) from the PortProtonQt repository."""
|
"""API to fetch game assets (cover, metadata) and forum topics from the PortProtonQt repository."""
|
||||||
def __init__(self, downloader: Downloader | None = None):
|
def __init__(self, downloader: Downloader | None = None):
|
||||||
self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data"
|
self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data"
|
||||||
|
self.topics_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/linux_gaming_topics.tar.xz"
|
||||||
self.downloader = downloader or Downloader(max_workers=4)
|
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.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")
|
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
||||||
os.makedirs(self.custom_data_dir, exist_ok=True)
|
os.makedirs(self.custom_data_dir, exist_ok=True)
|
||||||
|
self._topics_data = None
|
||||||
|
|
||||||
def _get_game_dir(self, exe_name: str) -> str:
|
def _get_game_dir(self, exe_name: str) -> str:
|
||||||
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
||||||
@ -40,7 +54,7 @@ class PortProtonAPI:
|
|||||||
cover_url = f"{cover_url_base}{ext}"
|
cover_url = f"{cover_url_base}{ext}"
|
||||||
if self._check_file_exists(cover_url, timeout):
|
if self._check_file_exists(cover_url, timeout):
|
||||||
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
||||||
result = download_with_cache(cover_url, local_cover_path, timeout, self.downloader)
|
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
|
||||||
if result:
|
if result:
|
||||||
results["cover"] = result
|
results["cover"] = result
|
||||||
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
||||||
@ -52,7 +66,7 @@ class PortProtonAPI:
|
|||||||
|
|
||||||
if self._check_file_exists(metadata_url, timeout):
|
if self._check_file_exists(metadata_url, timeout):
|
||||||
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
||||||
result = download_with_cache(metadata_url, local_metadata_path, timeout, self.downloader)
|
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
|
||||||
if result:
|
if result:
|
||||||
results["metadata"] = result
|
results["metadata"] = result
|
||||||
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
||||||
@ -123,3 +137,66 @@ class PortProtonAPI:
|
|||||||
logger.debug(f"No assets found for {exe_name}")
|
logger.debug(f"No assets found for {exe_name}")
|
||||||
if callback:
|
if callback:
|
||||||
callback(results)
|
callback(results)
|
||||||
|
|
||||||
|
def _load_topics_data(self):
|
||||||
|
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||||
|
if self._topics_data is not None:
|
||||||
|
return self._topics_data
|
||||||
|
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
cache_tar = os.path.join(cache_dir, "linux_gaming_topics.tar.xz")
|
||||||
|
cache_json = os.path.join(cache_dir, "linux_gaming_topics_min.json")
|
||||||
|
|
||||||
|
if os.path.exists(cache_json) and (time.time() - os.path.getmtime(cache_json) < CACHE_DURATION):
|
||||||
|
logger.info("Using cached topics JSON: %s", cache_json)
|
||||||
|
try:
|
||||||
|
with open(cache_json, "rb") as f:
|
||||||
|
self._topics_data = orjson.loads(f.read())
|
||||||
|
logger.debug("Loaded %d topics from cache", len(self._topics_data))
|
||||||
|
return self._topics_data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error reading cached topics JSON: %s", e)
|
||||||
|
self._topics_data = []
|
||||||
|
|
||||||
|
def process_tar(result: str | None):
|
||||||
|
if not result or not os.path.exists(result):
|
||||||
|
logger.error("Failed to download topics archive")
|
||||||
|
self._topics_data = []
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with tarfile.open(result, mode="r:xz") as tar:
|
||||||
|
member = next((m for m in tar.getmembers() if m.name == "linux_gaming_topics_min.json"), None)
|
||||||
|
if member is None:
|
||||||
|
raise RuntimeError("linux_gaming_topics_min.json not found in archive")
|
||||||
|
fobj = tar.extractfile(member)
|
||||||
|
if fobj is None:
|
||||||
|
raise RuntimeError("Failed to extract linux_gaming_topics_min.json from archive")
|
||||||
|
raw = fobj.read()
|
||||||
|
fobj.close()
|
||||||
|
self._topics_data = orjson.loads(raw)
|
||||||
|
with open(cache_json, "wb") as f:
|
||||||
|
f.write(orjson.dumps(self._topics_data))
|
||||||
|
if os.path.exists(cache_tar):
|
||||||
|
os.remove(cache_tar)
|
||||||
|
logger.info("Archive %s deleted after extraction", cache_tar)
|
||||||
|
logger.info("Loaded %d topics from archive", len(self._topics_data))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error processing topics archive: %s", e)
|
||||||
|
self._topics_data = []
|
||||||
|
|
||||||
|
self.downloader.download_async(self.topics_url, cache_tar, timeout=5, callback=process_tar)
|
||||||
|
# Wait for async download to complete if called synchronously
|
||||||
|
while self._topics_data is None:
|
||||||
|
time.sleep(0.1)
|
||||||
|
return self._topics_data
|
||||||
|
|
||||||
|
def get_forum_topic_slug(self, game_name: str) -> str:
|
||||||
|
"""Get the forum topic slug or search URL for a given game name."""
|
||||||
|
topics = self._load_topics_data()
|
||||||
|
normalized_name = game_name.lower().replace(" ", "-")
|
||||||
|
for topic in topics:
|
||||||
|
if topic["normalized_title"] == normalized_name:
|
||||||
|
return topic["slug"]
|
||||||
|
logger.debug("No forum topic found for game: %s, redirecting to search", game_name)
|
||||||
|
encoded_name = urllib.parse.quote(f"#ppdb {game_name}")
|
||||||
|
return f"search?q={encoded_name}"
|
||||||
|
Reference in New Issue
Block a user