From a3e36e1650398575502ad363df51cd4a3d1fb5d0 Mon Sep 17 00:00:00 2001 From: Daniel Johnson Date: Thu, 11 Jan 2024 17:12:10 -0500 Subject: [PATCH] Enable download of DLC extras from GOG. This is a bit painful; to do this we extract the downlink information when we pull the extras, and put that in the extras dict. Then, InstallerWindow keeps the dict rather than just the ID from it; it passes the entire thing back into the service so that the GOG service can obtain the download subdict and it goes from there. There are minimal changes for itch.io, the only other service with extras. The new code here just strips the IDs out and proceeds as before. --- lutris/gui/installerwindow.py | 16 +++++++------- lutris/installer/installer.py | 4 ++-- lutris/services/gog.py | 40 +++++++++++++++++++++++------------ lutris/services/itchio.py | 7 +++--- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/lutris/gui/installerwindow.py b/lutris/gui/installerwindow.py index 6c0edf3f0..7c4cf6bbe 100644 --- a/lutris/gui/installerwindow.py +++ b/lutris/gui/installerwindow.py @@ -70,7 +70,7 @@ class InstallerWindow(ModelessDialog, self.set_default_size(740, 460) self.installers = installers self.config = {} - self.selected_extra_ids = [] + self.selected_extras = [] self.install_in_progress = False self.install_complete = False self.interpreter = None @@ -137,7 +137,7 @@ class InstallerWindow(ModelessDialog, self.extras_tree_store = Gtk.TreeStore( bool, # is selected? bool, # is inconsistent? - str, # id + object, # extras dict str, # label ) @@ -499,7 +499,7 @@ class InstallerWindow(ModelessDialog, for extra_source, extras in all_extras.items(): parent = self.extras_tree_store.append(None, (None, None, None, extra_source)) for extra in extras: - self.extras_tree_store.append(parent, (False, False, extra["id"], self.get_extra_label(extra))) + self.extras_tree_store.append(parent, (False, False, extra, self.get_extra_label(extra))) self.stack.navigate_to_page(self.present_extras_page) else: @@ -577,13 +577,13 @@ class InstallerWindow(ModelessDialog, selected_extras = [] def save_extra(store, path, iter_): - selected, _inconsistent, id_, _label = store[iter_] - if selected and id_: - selected_extras.append(id_) + selected, _inconsistent, extra, _label = store[iter_] + if selected and extra: + selected_extras.append(extra) extra_store.foreach(save_extra) - self.selected_extra_ids = selected_extras + self.selected_extras = selected_extras GLib.idle_add(self.on_extras_ready) def on_extras_ready(self, *args): @@ -602,7 +602,7 @@ class InstallerWindow(ModelessDialog, patch_version = None AsyncCall(self.interpreter.installer.prepare_game_files, - self.on_files_prepared, self.selected_extra_ids, patch_version) + self.on_files_prepared, self.selected_extras, patch_version) def on_files_prepared(self, _result, error): if error: diff --git a/lutris/installer/installer.py b/lutris/installer/installer.py index ea21b154d..b4170f8a3 100644 --- a/lutris/installer/installer.py +++ b/lutris/installer/installer.py @@ -148,7 +148,7 @@ class LutrisInstaller: # pylint: disable=too-many-instance-attributes errors.append("Scripts can't have both extends and requires") return errors - def prepare_game_files(self, extra_ids, patch_version=None): + def prepare_game_files(self, extras, patch_version=None): """Gathers necessary files before iterating through them.""" if not self.script_files: return @@ -177,7 +177,7 @@ class LutrisInstaller: # pylint: disable=too-many-instance-attributes # If a patch version is given download the patch files instead of the installer installer_files = self.service.get_patch_files(self, installer_file_id) else: - content_files, extra_files = self.service.get_installer_files(self, installer_file_id, extra_ids) + content_files, extra_files = self.service.get_installer_files(self, installer_file_id, extras) self.extra_file_paths = [path for f in extra_files for path in f.get_dest_files_by_id().values()] installer_files = content_files + extra_files except UnavailableGameError as ex: diff --git a/lutris/services/gog.py b/lutris/services/gog.py index 88b610ef5..f47cae894 100644 --- a/lutris/services/gog.py +++ b/lutris/services/gog.py @@ -289,7 +289,8 @@ class GOGService(OnlineService): return self.make_api_request(url) def get_download_info(self, downlink): - """Return file download information""" + """Return file download information, a list of dict containing the 'url' and + 'filename' for each file.""" logger.info("Getting download info for %s", downlink) try: response = self.make_api_request(downlink) @@ -298,12 +299,18 @@ class GOGService(OnlineService): raise UnavailableGameError(_("The download of '%s' failed.") % downlink) from ex if not response: raise UnavailableGameError(_("The download of '%s' failed.") % downlink) + + expanded = [] for field in ("checksum", "downlink"): field_url = response[field] parsed = urlparse(field_url) query = dict(parse_qsl(parsed.query)) - response[field + "_filename"] = os.path.basename(query.get("path") or parsed.path) - return response + filename = os.path.basename(query.get("path") or parsed.path) + expanded.append({ + "url": response[field], + "filename": filename + }) + return expanded def get_downloads(self, gogid): """Return all available downloads for a GOG ID""" @@ -333,6 +340,7 @@ class GOGService(OnlineService): "type": download.get("type", "").strip(), "total_size": download.get("total_size", 0), "id": str(download["id"]), + "downlinks": [f.get("downlink") for f in download.get("files") or []] } for download in product["downloads"].get("bonus_content") or [] ] if extras: @@ -382,25 +390,31 @@ class GOGService(OnlineService): if not downlink: logger.error("No download information for %s", game_file) continue - download_info = self.get_download_info(downlink) - for field in ('checksum', 'downlink'): + for info in self.get_download_info(downlink): download_links.append({ "name": download.get("name", ""), "os": download.get("os", ""), "type": download.get("type", ""), "total_size": download.get("total_size", 0), "id": str(game_file["id"]), - "url": download_info[field], - "filename": download_info[field + "_filename"] + "url": info["url"], + "filename": info["filename"] }) return download_links - def get_extra_files(self, downloads, installer, selected_extras): + def get_extra_files(self, installer, selected_extras): extra_files = [] - for extra in downloads["bonus_content"]: - if str(extra["id"]) not in selected_extras: - continue - links = self.query_download_links(extra) + for extra in selected_extras: + if extra.get("downlinks"): + links = [info for link in extra.get("downlinks") for info in self.get_download_info(link)] + elif str(extra["id"]) in selected_extras: + links = self.query_download_links(extra) + else: + links = [] + + if not links: + logger.error("No download link for bonus content '%s' could be obtained.", extra.get("name")) + for link in links: if link["filename"].endswith(".xml"): # GOG gives a link for checksum XML files for bonus content @@ -490,7 +504,7 @@ class GOGService(OnlineService): extra_files = [] if selected_extras: - for extra_file in self.get_extra_files(downloads, installer, selected_extras): + for extra_file in self.get_extra_files(installer, selected_extras): extra_files.append(extra_file) return files, extra_files diff --git a/lutris/services/itchio.py b/lutris/services/itchio.py index 06f9a163a..22d8ac772 100644 --- a/lutris/services/itchio.py +++ b/lutris/services/itchio.py @@ -529,6 +529,7 @@ class ItchIoService(OnlineService): extra_files = [] link = None filename = "setup.zip" + selected_extras_ids = set(x["id"] for x in selected_extras or []) file = next(_file.copy() for _file in installer.script_files if _file.id == installer_file_id) if not file.url.startswith("N/A"): @@ -542,9 +543,9 @@ class ItchIoService(OnlineService): "date": int(datetime.datetime.now().timestamp()) } - if not link or len(selected_extras) > 0: + if not link or len(selected_extras_ids) > 0: for upload in uploads["uploads"]: - if selected_extras and (upload["type"] in self.extra_types): + if selected_extras_ids and (upload["type"] in self.extra_types): extras.append(upload) continue # default = games/tools ("executables") @@ -594,7 +595,7 @@ class ItchIoService(OnlineService): ) for extra in extras: - if str(extra["id"]) not in selected_extras: + if str(extra["id"]) not in selected_extras_ids: continue link = self.get_download_link(extra["id"], key) extra_files.append(