Download missing Lutris media when installing games during service refresh.

This extends sync_media() so it can download for a specific set of games, so we can download only the installed games, not games you had already.

Resolves #4545
This commit is contained in:
Daniel Johnson 2024-01-07 14:40:32 -05:00
parent 8cabab49c7
commit a903b67180
8 changed files with 88 additions and 48 deletions

View file

@ -11,6 +11,7 @@ from lutris.database.games import add_game, get_game_by_field
from lutris.database.services import ServiceGameCollection
from lutris.game import Game
from lutris.services.base import BaseService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util.battlenet.definitions import ProductDbInfo
@ -18,12 +19,12 @@ from lutris.util.log import logger
try:
from lutris.util.battlenet.product_db_pb2 import ProductDb
BNET_ENABLED = True
except (ImportError, TypeError) as ex:
logger.warning("The Battle.net source is unavailable because Google protobuf could not be loaded: %s", ex)
BNET_ENABLED = False
GAME_IDS = {
's1': ('s1', 'StarCraft', 'S1', 'starcraft-remastered'),
's2': ('s2', 'StarCraft II', 'S2', 'starcraft-ii'),
@ -118,8 +119,12 @@ class BattleNetService(BaseService):
raise RuntimeError("Battle.net is not installed in Lutris")
bnet_prefix = bnet_game["directory"].split("drive_c")[0]
parser = BlizzardProductDbParser(bnet_prefix)
installed_slugs = []
for game in parser.games:
self.install_from_battlenet(bnet_game, game)
slug = self.install_from_battlenet(bnet_game, game)
if slug:
installed_slugs.append(slug)
sync_media(installed_slugs)
def install_from_battlenet(self, bnet_game, game):
app_id = game.ngdp
@ -135,10 +140,11 @@ class BattleNetService(BaseService):
game_config = LutrisConfig(game_config_id=bnet_game["configpath"]).game_level
game_config["game"]["args"] = '--exec="launch %s"' % game.ngdp
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
slug = self.get_installed_slug(bnet_game)
add_game(
name=service_game["name"],
runner=bnet_game["runner"],
slug=service_game["slug"],
slug=slug,
directory=bnet_game["directory"],
installed=1,
installer_slug=lutris_game_id,
@ -147,7 +153,7 @@ class BattleNetService(BaseService):
service_id=app_id,
platform="Windows"
)
return game_id
return slug
def get_installed_slug(self, db_game):
return db_game.get("lutris_slug") or db_game["slug"]

View file

@ -17,6 +17,7 @@ from lutris.database.services import ServiceGameCollection
from lutris.game import Game
from lutris.installer import get_installers
from lutris.services.base import OnlineService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util.log import logger
@ -313,14 +314,15 @@ class EAAppService(OnlineService):
logger.error("Invalid install of EA App at %s", ea_app_prefix)
return
ea_app_launcher = EAAppGames(ea_app_prefix)
installed_games = 0
installed_slugs = []
for content_ids in ea_app_launcher.get_installed_games_content_ids():
self.install_from_ea_app(ea_app_game, content_ids)
installed_games += 1
logger.debug("Installed %s EA games", installed_games)
slug = self.install_from_ea_app(ea_app_game, content_ids)
if slug:
installed_slugs.append(slug)
sync_media(installed_slugs)
logger.debug("Installed %s EA games", len(installed_slugs))
def install_from_ea_app(self, ea_game, content_ids):
offer_id = content_ids[0]
logger.debug("Installing EA game %s", offer_id)
service_game = ServiceGameCollection.get_game("ea_app", offer_id)
@ -334,10 +336,11 @@ class EAAppService(OnlineService):
game_config = LutrisConfig(game_config_id=ea_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(",".join(content_ids))
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
slug = self.get_installed_slug(ea_game)
add_game(
name=service_game["name"],
runner=ea_game["runner"],
slug=slugify(service_game["name"]),
slug=slug,
directory=ea_game["directory"],
installed=1,
installer_slug=lutris_game_id,
@ -345,7 +348,7 @@ class EAAppService(OnlineService):
service=self.id,
service_id=offer_id,
)
return game_id
return slug
def generate_installer(self, db_game, ea_db_game):
ea_game = Game(ea_db_game["id"])

View file

@ -14,6 +14,7 @@ from lutris.game import Game
from lutris.gui.widgets.utils import Image, paste_overlay, thumbnail_image
from lutris.installer import get_installers
from lutris.services.base import AuthTokenExpiredError, OnlineService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util import system
@ -336,10 +337,11 @@ class EpicGamesStoreService(OnlineService):
game_config = LutrisConfig(game_config_id=egs_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(app_name)
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
slug = self.get_installed_slug(egs_game)
add_game(
name=service_game["name"],
runner=egs_game["runner"],
slug=slugify(service_game["name"]),
slug=slug,
directory=egs_game["directory"],
installed=1,
installer_slug=lutris_game_id,
@ -347,7 +349,7 @@ class EpicGamesStoreService(OnlineService):
service=self.id,
service_id=app_name,
)
return game_id
return slug
def add_installed_games(self):
"""Scan an existing EGS install for games"""
@ -362,8 +364,12 @@ class EpicGamesStoreService(OnlineService):
logger.error("Invalid install of EGS at %s", egs_prefix)
return
egs_launcher = EGSLauncher(egs_prefix)
installed_slugs = []
for manifest in egs_launcher.iter_manifests():
self.install_from_egs(egs_game, manifest)
slug = self.install_from_egs(egs_game, manifest)
if slug:
installed_slugs.append(slug)
sync_media(installed_slugs)
logger.debug("All EGS games imported")
def generate_installer(self, db_game, egs_db_game):

View file

@ -182,13 +182,13 @@ class FlathubService(BaseService):
"installer": [
{
"execute":
{
"file": flatpak_cmd[0],
"args": " ".join(flatpak_cmd[1:])
+ f" install --{self.install_type} --app --noninteractive flathub "
f"app/{db_game['appid']}/{self.arch}/{self.branch}",
"disable_runtime": True
}
{
"file": flatpak_cmd[0],
"args": " ".join(flatpak_cmd[1:])
+ f" install --{self.install_type} --app --noninteractive flathub "
f"app/{db_game['appid']}/{self.arch}/{self.branch}",
"disable_runtime": True
}
}
]
}

View file

@ -2,7 +2,7 @@ import json
import os
import urllib.parse
from gettext import gettext as _
from typing import List
from typing import Dict, Iterable, List
from gi.repository import Gio
@ -180,8 +180,17 @@ def download_lutris_media(slug):
download_media({slug: cover_url}, LutrisCoverart())
def sync_media() -> dict:
"""Downlad all missing media"""
def sync_media(slugs: Iterable[str] = None) -> Dict[str, int]:
"""Download missing media for Lutris games; if a set of slugs
is not provided, downloads them for all games in the PGA."""
if slugs is None:
slugs = {game["slug"] for game in get_games()}
else:
slugs = set(s for s in slugs if s)
if not slugs:
return {}
banners_available = {fn.split(".")[0] for fn in os.listdir(settings.BANNER_PATH)}
icons_available = {
fn.split(".")[0].replace("lutris_", "")
@ -190,18 +199,18 @@ def sync_media() -> dict:
}
covers_available = {fn.split(".")[0] for fn in os.listdir(settings.COVERART_PATH)}
complete_games = banners_available.intersection(icons_available).intersection(covers_available)
all_slugs = {game["slug"] for game in get_games()}
slugs = all_slugs - complete_games
if not slugs:
slugs_to_download = slugs - complete_games
if not slugs_to_download:
return {}
games = get_api_games(list(slugs))
games = get_api_games(list(slugs_to_download))
alias_map = {}
api_slugs = set()
for game in games:
api_slugs.add(game["slug"])
for alias in game["aliases"]:
if alias["slug"] in slugs:
if alias["slug"] in slugs_to_download:
alias_map[game["slug"]] = alias["slug"]
alias_slugs = set(alias_map.values())
used_alias_slugs = alias_slugs - api_slugs

View file

@ -18,6 +18,7 @@ from lutris.database.services import ServiceGameCollection
from lutris.game import Game
from lutris.installer import get_installers
from lutris.services.base import OnlineService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util.log import logger
@ -304,11 +305,13 @@ class OriginService(OnlineService):
logger.error("Invalid install of Origin at %s", origin_prefix)
return
origin_launcher = OriginLauncher(origin_prefix)
installed_games = 0
installed_slugs = []
for manifest in origin_launcher.iter_manifests():
self.install_from_origin(origin_game, manifest)
installed_games += 1
logger.debug("Installed %s Origin games", installed_games)
slug = self.install_from_origin(origin_game, manifest)
if slug:
installed_slugs.append(slug)
sync_media(installed_slugs)
logger.debug("Installed %s Origin games", len(installed_slugs))
def install_from_origin(self, origin_game, manifest):
if "id" not in manifest:
@ -326,10 +329,11 @@ class OriginService(OnlineService):
game_config = LutrisConfig(game_config_id=origin_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(manifest["id"])
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
slug = self.get_installed_slug(service_game)
add_game(
name=service_game["name"],
runner=origin_game["runner"],
slug=slugify(service_game["name"]),
slug=slug,
directory=origin_game["directory"],
installed=1,
installer_slug=lutris_game_id,
@ -337,7 +341,7 @@ class OriginService(OnlineService):
service=self.id,
service_id=offer_id,
)
return game_id
return slug
def generate_installer(self, db_game, origin_db_game):
origin_game = Game(origin_db_game["id"])

View file

@ -13,6 +13,7 @@ from lutris.database.services import ServiceGameCollection
from lutris.game import Game
from lutris.installer.installer_file import InstallerFile
from lutris.services.base import BaseService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util.log import logger
@ -132,10 +133,11 @@ class SteamService(BaseService):
game_config = LutrisConfig().game_level
game_config["game"]["appid"] = appid
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
slug = self.get_installed_slug(service_game)
add_game(
name=service_game["name"],
runner="steam",
slug=slugify(service_game["name"]),
slug=slug,
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
@ -143,7 +145,7 @@ class SteamService(BaseService):
service=self.id,
service_id=appid,
)
return game_id
return slug
@property
def steamapps_paths(self):
@ -152,6 +154,7 @@ class SteamService(BaseService):
def add_installed_games(self):
"""Syncs installed Steam games with Lutris"""
stats = {"installed": 0, "removed": 0, "deduped": 0, "paths": []}
installed_slugs = []
installed_appids = []
for steamapps_path in self.steamapps_paths:
for appmanifest_file in get_appmanifests(steamapps_path):
@ -160,7 +163,9 @@ class SteamService(BaseService):
app_manifest_path = os.path.join(steamapps_path, appmanifest_file)
app_manifest = AppManifest(app_manifest_path)
installed_appids.append(app_manifest.steamid)
self.install_from_steam(app_manifest)
slug = self.install_from_steam(app_manifest)
if slug:
installed_slugs.append(slug)
stats["installed"] += 1
if stats["paths"]:
logger.debug("%s Steam games detected and installed", stats["installed"])
@ -196,6 +201,7 @@ class SteamService(BaseService):
steam_game.remove(no_signal=True)
steam_game.delete(no_signal=True)
stats["deduped"] += 1
sync_media(installed_slugs)
logger.debug("%s Steam games deduplicated", stats["deduped"])
def generate_installer(self, db_game):

View file

@ -14,6 +14,7 @@ from lutris.database.services import ServiceGameCollection
from lutris.game import Game
from lutris.installer import get_installers
from lutris.services.base import OnlineService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util.log import logger
@ -191,12 +192,13 @@ class UbisoftConnectService(OnlineService):
launch_id = details.get("launchId") or details.get("installId") or details.get("spaceId")
game_config["game"]["args"] = f"uplay://launch/{launch_id}"
configpath = write_game_config(lutris_game_id, game_config)
slug = self.get_installed_slug(game)
if existing_game:
update_existing(
id=existing_game["id"],
name=game["name"],
runner=self.runner,
slug=slugify(game["name"]),
slug=slug,
directory=ubisoft_connect["directory"],
installed=1,
installer_slug=lutris_game_id,
@ -205,10 +207,10 @@ class UbisoftConnectService(OnlineService):
service_id=game["appid"],
)
return existing_game["id"]
game_id = add_game(
add_game(
name=game["name"],
runner=self.runner,
slug=slugify(game["name"]),
slug=slug,
directory=ubisoft_connect["directory"],
installed=1,
installer_slug=lutris_game_id,
@ -216,7 +218,7 @@ class UbisoftConnectService(OnlineService):
service=self.id,
service_id=game["appid"],
)
return game_id
return slug
def add_installed_games(self):
ubisoft_connect = get_game_by_field(self.client_installer, "slug")
@ -225,12 +227,16 @@ class UbisoftConnectService(OnlineService):
return
prefix_path = ubisoft_connect["directory"].split("drive_c")[0]
prefix = WinePrefixManager(prefix_path)
installed_slugs = []
for game in ServiceGameCollection.get_for_service(self.id):
details = json.loads(game["details"])
install_path = get_ubisoft_registry(prefix, details.get("registryPath"))
exe = get_ubisoft_registry(prefix, details.get("exe"))
if install_path and exe:
self.install_from_ubisoft(ubisoft_connect, game)
slug = self.install_from_ubisoft(ubisoft_connect, game)
if slug:
installed_slugs.append(slug)
sync_media(installed_slugs)
def generate_installer(self, db_game, ubi_db_game):
ubisoft_connect = Game(ubi_db_game["id"])