Compare commits
1
Commits
tray-wine
...
goggl-fork
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b80522d1
|
No files matched your search
@@ -9,6 +9,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Загрузка игр GOG переведена на форк `linux-gaming-ru/gogdl` с выбором CDN по скорости, исправленной статистикой загрузки и ограничением повторных запросов; ранее установленный gogdl автоматически заменяется форком
|
||||
### Fixed
|
||||
|
||||
- Отключена проверка обновлений AppImage при запуске с `--silent`, чтобы не показывать диалог обновления
|
||||
|
||||
+14
-10
@@ -4,7 +4,6 @@ import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import zlib
|
||||
@@ -25,7 +24,8 @@ GOG_LOGIN_URL = (
|
||||
"redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&"
|
||||
"response_type=code&layout=galaxy"
|
||||
)
|
||||
GOGDL_RELEASE_URL = "https://api.github.com/repos/Heroic-Games-Launcher/heroic-gogdl/releases/latest"
|
||||
GOGDL_RELEASE_SOURCE = "linux-gaming-ru/gogdl"
|
||||
GOGDL_RELEASE_URL = f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/releases/latest"
|
||||
GOGDL_AUTH_TIMEOUT = 60
|
||||
GOGDL_UPDATE_INTERVAL = 30 * 24 * 60 * 60
|
||||
GOG_USER_TIMEOUT = 15
|
||||
@@ -77,11 +77,11 @@ class GOGAPI:
|
||||
self.games_dir = Path.home() / "Games"
|
||||
|
||||
def get_gogdl_path(self) -> str | None:
|
||||
"""Return an available gogdl executable."""
|
||||
"""Return the managed gogdl fork executable when installed."""
|
||||
bundled = self.bin_dir / "gogdl"
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK):
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK) and self._get_gogdl_release_tag():
|
||||
return str(bundled)
|
||||
return shutil.which("gogdl")
|
||||
return None
|
||||
|
||||
def build_command(self, arguments: list[str]) -> list[str]:
|
||||
"""Build a gogdl command with private configuration paths."""
|
||||
@@ -94,7 +94,7 @@ class GOGAPI:
|
||||
return [gogdl, "--auth-config-path", str(self.auth_path), *arguments]
|
||||
|
||||
def ensure_gogdl(self) -> str:
|
||||
"""Download and verify the latest official Heroic gogdl binary."""
|
||||
"""Download and verify the latest linux-gaming-ru gogdl binary."""
|
||||
existing = self.get_gogdl_path()
|
||||
if existing:
|
||||
return existing
|
||||
@@ -102,9 +102,9 @@ class GOGAPI:
|
||||
return self._install_gogdl_release(asset, release_tag)
|
||||
|
||||
def update_gogdl(self) -> str:
|
||||
"""Update the bundled gogdl binary to the latest official release."""
|
||||
"""Replace other gogdl binaries and update the managed fork."""
|
||||
bundled = self.bin_dir / "gogdl"
|
||||
if bundled.is_file() and os.access(bundled, os.X_OK):
|
||||
if self.get_gogdl_path():
|
||||
try:
|
||||
last_check = self.gogdl_version_path.stat().st_mtime
|
||||
except OSError:
|
||||
@@ -143,13 +143,17 @@ class GOGAPI:
|
||||
|
||||
def _get_gogdl_release_tag(self) -> str:
|
||||
try:
|
||||
return self.gogdl_version_path.read_text(encoding="utf-8").strip()
|
||||
version = self.gogdl_version_path.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return ""
|
||||
source, _, tag = version.partition(":")
|
||||
return tag if source == GOGDL_RELEASE_SOURCE else ""
|
||||
|
||||
def _install_gogdl_release(self, asset: dict, release_tag: str) -> str:
|
||||
path = self._download_gogdl_asset(asset)
|
||||
self.gogdl_version_path.write_text(release_tag, encoding="utf-8")
|
||||
self.gogdl_version_path.write_text(
|
||||
f"{GOGDL_RELEASE_SOURCE}:{release_tag}", encoding="utf-8"
|
||||
)
|
||||
return path
|
||||
|
||||
def _download_gogdl_asset(self, asset: dict) -> str:
|
||||
|
||||
+76
-6
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import zlib
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
@@ -7,7 +8,7 @@ from unittest.mock import Mock
|
||||
import orjson
|
||||
import pytest
|
||||
|
||||
from portprotonqt.gog_api import GOGAPI, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
|
||||
from portprotonqt.gog_api import GOGAPI, GOGDL_RELEASE_SOURCE, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
|
||||
from portprotonqt.localization import LOCALE_MAP
|
||||
|
||||
|
||||
@@ -392,13 +393,82 @@ def test_ensure_gogdl_downloads_matching_architecture(tmp_path: Path, monkeypatc
|
||||
response = Mock()
|
||||
response.json.return_value = {"assets": [asset], "tag_name": "v1.2.1"}
|
||||
monkeypatch.setattr("portprotonqt.gog_api.platform.machine", lambda: "x86_64")
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: response)
|
||||
request = Mock(return_value=response)
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", request)
|
||||
monkeypatch.setattr(
|
||||
api, "_install_gogdl_release",
|
||||
lambda selected, tag: f'{selected["name"]}:{tag}',
|
||||
)
|
||||
|
||||
assert api.ensure_gogdl() == "gogdl_linux_x86_64:v1.2.1"
|
||||
assert request.call_args.args[0] == f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/releases/latest"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["ensure_gogdl", "update_gogdl"])
|
||||
@pytest.mark.parametrize("version", ["", "v1.3.2", "Heroic-Games-Launcher/heroic-gogdl:v1.3.2"])
|
||||
def test_gogdl_replaces_existing_upstream(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, operation: str, version: str
|
||||
) -> None:
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
api = GOGAPI()
|
||||
api.bin_dir.mkdir(parents=True)
|
||||
binary = api.bin_dir / "gogdl"
|
||||
binary.write_bytes(b"upstream")
|
||||
binary.chmod(0o755)
|
||||
if version:
|
||||
api.gogdl_version_path.write_text(version)
|
||||
latest = Mock(return_value=({"name": "gogdl_linux_x86_64"}, "v1.3.2"))
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", latest)
|
||||
|
||||
def download(_asset: dict) -> str:
|
||||
binary.write_bytes(b"fork")
|
||||
return str(binary)
|
||||
|
||||
monkeypatch.setattr(api, "_download_gogdl_asset", download)
|
||||
assert api.get_gogdl_path() is None
|
||||
assert getattr(api, operation)() == str(binary)
|
||||
latest.assert_called_once_with()
|
||||
assert binary.read_bytes() == b"fork"
|
||||
assert api.gogdl_version_path.read_text() == f"{GOGDL_RELEASE_SOURCE}:v1.3.2"
|
||||
assert api.get_gogdl_path() == str(binary)
|
||||
|
||||
|
||||
def test_gogdl_does_not_use_system_binary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
api = GOGAPI()
|
||||
which = Mock(return_value="/usr/bin/gogdl")
|
||||
monkeypatch.setattr("shutil.which", which)
|
||||
install = Mock(return_value=str(api.bin_dir / "gogdl"))
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", lambda: ({}, "v1.3.2"))
|
||||
monkeypatch.setattr(api, "_install_gogdl_release", install)
|
||||
|
||||
assert api.get_gogdl_path() is None
|
||||
assert api.ensure_gogdl() == str(api.bin_dir / "gogdl")
|
||||
install.assert_called_once_with({}, "v1.3.2")
|
||||
which.assert_not_called()
|
||||
|
||||
|
||||
def test_failed_gogdl_migration_keeps_old_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
||||
api = GOGAPI()
|
||||
api.bin_dir.mkdir(parents=True)
|
||||
binary = api.bin_dir / "gogdl"
|
||||
binary.write_bytes(b"upstream")
|
||||
binary.chmod(0o755)
|
||||
api.gogdl_version_path.write_text("v1.3.2")
|
||||
response = Mock()
|
||||
response.iter_content.return_value = [b"corrupted download"]
|
||||
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: nullcontext(response))
|
||||
asset = {"browser_download_url": "https://github.com/linux-gaming-ru/gogdl/releases/download/v1.3.2/gogdl_linux_x86_64", "digest": "sha256:invalid"}
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", lambda: (asset, "v1.3.2"))
|
||||
|
||||
with pytest.raises(OSError, match="checksum verification failed"):
|
||||
api.update_gogdl()
|
||||
|
||||
assert binary.read_bytes() == b"upstream"
|
||||
assert api.gogdl_version_path.read_text() == "v1.3.2"
|
||||
assert not binary.with_suffix(".part").exists()
|
||||
assert api.get_gogdl_path() is None
|
||||
|
||||
|
||||
def test_update_gogdl_skips_current_release(tmp_path: Path, monkeypatch) -> None:
|
||||
@@ -410,7 +480,7 @@ def test_update_gogdl_skips_current_release(tmp_path: Path, monkeypatch) -> None
|
||||
gogdl_path.parent.mkdir()
|
||||
gogdl_path.touch()
|
||||
gogdl_path.chmod(0o755)
|
||||
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
|
||||
latest_release = Mock(return_value=({}, "v1.2.1"))
|
||||
monkeypatch.setattr(api, "_get_latest_gogdl_release", latest_release)
|
||||
install = Mock()
|
||||
@@ -430,7 +500,7 @@ def test_update_gogdl_checks_release_after_month(tmp_path: Path, monkeypatch) ->
|
||||
gogdl_path.parent.mkdir()
|
||||
gogdl_path.touch()
|
||||
gogdl_path.chmod(0o755)
|
||||
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
|
||||
os.utime(api.gogdl_version_path, (1, 1))
|
||||
last_check = api.gogdl_version_path.stat().st_mtime
|
||||
monkeypatch.setattr(
|
||||
@@ -453,7 +523,7 @@ def test_update_gogdl_records_failed_monthly_check(tmp_path: Path, monkeypatch)
|
||||
gogdl_path.parent.mkdir()
|
||||
gogdl_path.touch()
|
||||
gogdl_path.chmod(0o755)
|
||||
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
|
||||
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
|
||||
os.utime(api.gogdl_version_path, (1, 1))
|
||||
last_check = api.gogdl_version_path.stat().st_mtime
|
||||
monkeypatch.setattr(
|
||||
@@ -492,7 +562,7 @@ def test_install_gogdl_release_records_version(tmp_path: Path, monkeypatch) -> N
|
||||
monkeypatch.setattr(api, "_download_gogdl_asset", lambda _asset: "/bin/gogdl")
|
||||
|
||||
assert api._install_gogdl_release({}, "v1.2.1") == "/bin/gogdl"
|
||||
assert api.gogdl_version_path.read_text(encoding="utf-8") == "v1.2.1"
|
||||
assert api.gogdl_version_path.read_text(encoding="utf-8") == f"{GOGDL_RELEASE_SOURCE}:v1.2.1"
|
||||
|
||||
|
||||
def test_authenticate_returns_gogdl_error(monkeypatch) -> None:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Progressive library loading, drawing and input regressions."""
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
from time import perf_counter
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from unittest.mock import MagicMock
|
||||
@@ -399,7 +397,7 @@ def real_cover_scene(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("animation_type", ["gradient", "glow"])
|
||||
def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
def test_real_cover_loading_yields_and_focuses_first_paint(
|
||||
real_cover_scene: tuple[GameLibraryManager, Path],
|
||||
monkeypatch: pytest.MonkeyPatch, animation_type: str,
|
||||
) -> None:
|
||||
@@ -407,14 +405,14 @@ def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
monkeypatch.setattr(manager.theme, "GAME_CARD_ANIMATION",
|
||||
{**manager.theme.GAME_CARD_ANIMATION, "card_animation_type": animation_type})
|
||||
threads: list[bool] = []
|
||||
paints: list[tuple[float, bool]] = []
|
||||
ticks: list[float] = []
|
||||
paints: list[bool] = []
|
||||
ticks: list[tuple[int, int]] = []
|
||||
original_paint = GameCard.paintEvent
|
||||
original_cover = GameCard.on_cover_loaded
|
||||
|
||||
def record_paint(card: GameCard, event: Any) -> None:
|
||||
if card.name == "Game 0":
|
||||
paints.append((perf_counter(), card._focused or card._hovered))
|
||||
paints.append(card._focused or card._hovered)
|
||||
original_paint(card, event)
|
||||
|
||||
def record_cover(card: GameCard, pixmap: QPixmap) -> None:
|
||||
@@ -422,7 +420,7 @@ def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
original_cover(card, pixmap)
|
||||
|
||||
def sample() -> None:
|
||||
ticks.append(perf_counter())
|
||||
ticks.append((len(manager.game_card_cache), len(threads)))
|
||||
if len(threads) == len(games) and not manager._incremental_add_queue:
|
||||
loop.quit()
|
||||
|
||||
@@ -434,18 +432,14 @@ def test_real_cover_loading_and_first_paint_focus_latency(
|
||||
timer.setInterval(5)
|
||||
timer.timeout.connect(sample)
|
||||
timer.start()
|
||||
QTimer.singleShot(2000, loop.quit)
|
||||
QTimer.singleShot(5000, loop.quit)
|
||||
manager.set_games(games)
|
||||
loop.exec()
|
||||
timer.stop()
|
||||
assert len(threads) == len(games) and all(threads), threads
|
||||
highlighted = next((paint for paint in paints if paint[1]), None)
|
||||
assert highlighted is not None
|
||||
focus_delay = highlighted[0] - paints[0][0]
|
||||
max_gap = max(after - before for before, after in zip(ticks, ticks[1:], strict=False))
|
||||
logging.getLogger(__name__).info("%s: highlight %.1f ms, UI pause %.1f ms", animation_type, focus_delay * 1000, max_gap * 1000)
|
||||
assert focus_delay < 0.05, paints
|
||||
assert max_gap < 0.1, ticks
|
||||
assert paints and paints[0], paints
|
||||
assert any(0 < cards < len(games) for cards, _ in ticks), ticks
|
||||
assert any(0 < covers < len(games) for _, covers in ticks), ticks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", [("grid", 0), ("grid", 1), ("vertical", 0), ("vertical", 1)])
|
||||
|
||||
Reference in new issue
Block a user