Compare commits

...
1 Commits
Author SHA1 Message Date
Boria138 c4b80522d1 chore: switch GOG downloads to the managed gogdl fork
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-10-17 00:19:38 +05:00
4 changed files with 100 additions and 31 deletions

No files matched your search

+1
View File
@@ -9,6 +9,7 @@
### Changed
- Загрузка игр GOG переведена на форк `linux-gaming-ru/gogdl` с выбором CDN по скорости, исправленной статистикой загрузки и ограничением повторных запросов; ранее установленный gogdl автоматически заменяется форком
### Fixed
- Отключена проверка обновлений AppImage при запуске с `--silent`, чтобы не показывать диалог обновления
+14 -10
View File
@@ -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
View File
@@ -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:
+9 -15
View File
@@ -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)])