Allow downloading of GOG extras (Closes #2945)

This commit is contained in:
Mathieu Comandon 2020-10-11 17:10:19 -07:00
parent 88e4ffa6bb
commit 427ef2f8ec
16 changed files with 404 additions and 246 deletions

View file

@ -22,6 +22,8 @@ def save_cache_path(path):
def save_to_cache(source, destination):
"""Copy a file or folder to the cache"""
if not source:
raise ValueError("No source given to save")
if os.path.dirname(source) == destination:
logger.info("File is already cached in %s, skipping", destination)
return

View file

View file

@ -1,133 +1,18 @@
"""Widgets for the installer window"""
import os
from gettext import gettext as _
from urllib.parse import urlparse
from gi.repository import GObject, Gtk, Pango
from gi.repository import GObject, Gtk
from lutris.cache import save_to_cache
from lutris.gui.installer.widgets import InstallerLabel
from lutris.gui.widgets.common import FileChooserEntry
from lutris.gui.widgets.download_progress import DownloadProgressBox
from lutris.installer.steam_installer import SteamInstaller
from lutris.util import system
from lutris.util.log import logger
from lutris.util.strings import add_url_tags, gtk_safe
class InstallerLabel(Gtk.Label):
"""A label for installers"""
def __init__(self, text, wrap=True):
super().__init__()
if wrap:
self.set_line_wrap(True)
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
else:
self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
self.set_alignment(0, 0.5)
self.set_margin_right(12)
self.set_markup(text)
self.props.can_focus = False
self.set_tooltip_text(text)
class InstallerScriptBox(Gtk.VBox):
"""Box displaying the details of a script, with associated action buttons"""
def __init__(self, script, parent=None, revealed=False):
super().__init__()
self.script = script
self.parent = parent
self.revealer = None
self.set_margin_left(12)
self.set_margin_right(12)
box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
box.pack_start(self.get_infobox(), True, True, 0)
box.add(self.get_install_button())
self.add(box)
self.add(self.get_revealer(revealed))
def get_rating(self):
"""Return a string representation of the API rating"""
try:
rating = int(self.script["rating"])
except (ValueError, TypeError, KeyError):
return ""
return "" * rating
def get_infobox(self):
"""Return the central information box"""
info_box = Gtk.VBox(spacing=6)
title_box = Gtk.HBox(spacing=6)
title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
title_box.pack_start(InstallerLabel(""), True, True, 0)
rating_label = InstallerLabel(self.get_rating())
rating_label.set_alignment(1, 0.5)
title_box.pack_end(rating_label, False, False, 0)
info_box.add(title_box)
info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
return info_box
def get_revealer(self, revealed):
"""Return the revelaer widget"""
self.revealer = Gtk.Revealer()
self.revealer.add(self.get_notes())
self.revealer.set_reveal_child(revealed)
return self.revealer
def get_install_button(self):
"""Return the install button widget"""
align = Gtk.Alignment()
align.set(0, 0, 0, 0)
install_button = Gtk.Button(_("Install"))
install_button.connect("clicked", self.on_install_clicked)
align.add(install_button)
return align
def get_notes(self):
"""Return the notes widget"""
notes = self.script["notes"].strip()
if not notes:
return Gtk.Alignment()
notes_label = InstallerLabel(notes)
notes_label.set_margin_top(12)
notes_label.set_margin_bottom(12)
notes_label.set_margin_right(12)
notes_label.set_margin_left(12)
return notes_label
def reveal(self, reveal=True):
"""Show or hide the information in the revealer"""
if self.revealer:
self.revealer.set_reveal_child(reveal)
def on_install_clicked(self, _widget):
"""Handler to notify the parent of the selected installer"""
self.parent.emit("installer-selected", self.script["slug"])
class InstallerPicker(Gtk.ListBox):
"""List box to pick between several installers"""
__gsignals__ = {"installer-selected": (GObject.SIGNAL_RUN_FIRST, None, (str, ))}
def __init__(self, scripts):
super().__init__()
revealed = True
for script in scripts:
self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
revealed = False # Only reveal the first installer.
self.connect('row-selected', self.on_activate)
self.show_all()
@staticmethod
def on_activate(widget, row):
"""Handler for hiding and showing the revealers in children"""
for script_box_row in widget:
script_box = script_box_row.get_children()[0]
script_box.reveal(False)
installer_row = row.get_children()[0]
installer_row.reveal()
from lutris.util.strings import gtk_safe
class InstallerFileBox(Gtk.VBox):
@ -185,6 +70,8 @@ class InstallerFileBox(Gtk.VBox):
def get_file_provider_widget(self):
"""Return the widget used to track progress of file"""
box = Gtk.VBox(spacing=6)
print("PROVIDER")
print(self.provider)
if self.provider == "download":
download_progress = self.get_download_progress()
box.pack_start(download_progress, False, False, 0)
@ -218,8 +105,17 @@ class InstallerFileBox(Gtk.VBox):
info_box.add(self.state_label)
steam_box.add(info_box)
return steam_box
return Gtk.Label(self.get_file_label())
return Gtk.Label(gtk_safe(self.installer_file.url))
def get_file_label(self):
"""Return a human readable label for installer files"""
url = self.installer_file.url
if url.startswith("http"):
parsed = urlparse(url)
label = "%s on %s" % (self.installer_file.filename, parsed.netloc)
else:
label = url
return gtk_safe(label)
def get_popover(self):
"""Return the popover widget to select file source"""
@ -284,14 +180,14 @@ class InstallerFileBox(Gtk.VBox):
def get_source_button_label(self):
"""Return the label for the source button"""
if self.provider == "download":
return _("Download")
if self.provider == "pga":
return _("Cache")
if self.provider == "user":
return _("Local")
if self.provider == "steam":
return _("Steam")
provider_labels = {
"download": _("Download"),
"pga": _("Cache"),
"user": _("Local"),
"steam": _("Steam"),
}
if self.provider in provider_labels:
return provider_labels[self.provider]
raise ValueError("Unsupported provider %s" % self.provider)
def get_file_provider_label(self):
@ -312,7 +208,7 @@ class InstallerFileBox(Gtk.VBox):
cache_option.connect("toggled", self.on_user_file_cached)
box.pack_start(cache_option, False, False, 0)
return box
return InstallerLabel(gtk_safe(self.installer_file.human_url), wrap=False)
return InstallerLabel(self.get_file_label(), wrap=False)
def get_widgets(self):
"""Return the widget with the source of the file and a way to change its source"""
@ -383,77 +279,3 @@ class InstallerFileBox(Gtk.VBox):
self.installer_file.dest_file = widget.get_steam_data_path()
self.emit("file-available")
self.cache_file()
class InstallerFilesBox(Gtk.ListBox):
"""List box presenting all files needed for an installer"""
__gsignals__ = {
"files-ready": (GObject.SIGNAL_RUN_LAST, None, (bool, )),
"files-available": (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, installer_files, parent):
super().__init__()
self.parent = parent
self.installer_files = installer_files
self.ready_files = set()
self.available_files = set()
self.installer_files_boxes = {}
for installer_file in installer_files:
installer_file_box = InstallerFileBox(installer_file)
installer_file_box.connect("file-ready", self.on_file_ready)
installer_file_box.connect("file-unready", self.on_file_unready)
installer_file_box.connect("file-available", self.on_file_available)
self.installer_files_boxes[installer_file.id] = installer_file_box
self.add(installer_file_box)
if installer_file_box.is_ready:
self.ready_files.add(installer_file.id)
self.show_all()
self.check_files_ready()
def start_all(self):
"""Start all downloads"""
for file_id in self.installer_files_boxes:
self.installer_files_boxes[file_id].start()
@property
def is_ready(self):
"""Return True if all files are ready to be fetched"""
return len(self.ready_files) == len(self.installer_files)
def check_files_ready(self):
"""Checks if all installer files are ready and emit a signal if so"""
logger.debug("Files are ready? %s", self.is_ready)
self.emit("files-ready", self.is_ready)
def on_file_ready(self, widget):
"""Fired when a file has a valid provider.
If the file is user provided, it must set to a valid path.
"""
file_id = widget.installer_file.id
self.ready_files.add(file_id)
self.check_files_ready()
def on_file_unready(self, widget):
"""Fired when a file can't be provided.
Blocks the installer from continuing.
"""
file_id = widget.installer_file.id
self.ready_files.remove(file_id)
self.check_files_ready()
def on_file_available(self, widget):
"""A new file is available"""
file_id = widget.installer_file.id
self.available_files.add(file_id)
if len(self.available_files) == len(self.installer_files):
logger.info("All files available")
self.emit("files-available")
def get_game_files(self):
"""Return a mapping of the local files usable by the interpreter"""
return {
installer_file.id: installer_file.dest_file
for installer_file in self.installer_files
}

View file

@ -0,0 +1,78 @@
from gi.repository import GObject, Gtk
from lutris.gui.installer.file_box import InstallerFileBox
from lutris.util.log import logger
class InstallerFilesBox(Gtk.ListBox):
"""List box presenting all files needed for an installer"""
__gsignals__ = {
"files-ready": (GObject.SIGNAL_RUN_LAST, None, (bool, )),
"files-available": (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, installer_files, parent):
super().__init__()
self.parent = parent
self.installer_files = installer_files
self.ready_files = set()
self.available_files = set()
self.installer_files_boxes = {}
for installer_file in installer_files:
installer_file_box = InstallerFileBox(installer_file)
installer_file_box.connect("file-ready", self.on_file_ready)
installer_file_box.connect("file-unready", self.on_file_unready)
installer_file_box.connect("file-available", self.on_file_available)
self.installer_files_boxes[installer_file.id] = installer_file_box
self.add(installer_file_box)
if installer_file_box.is_ready:
self.ready_files.add(installer_file.id)
self.show_all()
self.check_files_ready()
def start_all(self):
"""Start all downloads"""
for file_id in self.installer_files_boxes:
self.installer_files_boxes[file_id].start()
@property
def is_ready(self):
"""Return True if all files are ready to be fetched"""
return len(self.ready_files) == len(self.installer_files)
def check_files_ready(self):
"""Checks if all installer files are ready and emit a signal if so"""
logger.debug("Files are ready? %s", self.is_ready)
self.emit("files-ready", self.is_ready)
def on_file_ready(self, widget):
"""Fired when a file has a valid provider.
If the file is user provided, it must set to a valid path.
"""
file_id = widget.installer_file.id
self.ready_files.add(file_id)
self.check_files_ready()
def on_file_unready(self, widget):
"""Fired when a file can't be provided.
Blocks the installer from continuing.
"""
file_id = widget.installer_file.id
self.ready_files.remove(file_id)
self.check_files_ready()
def on_file_available(self, widget):
"""A new file is available"""
file_id = widget.installer_file.id
self.available_files.add(file_id)
if len(self.available_files) == len(self.installer_files):
logger.info("All files available")
self.emit("files-available")
def get_game_files(self):
"""Return a mapping of the local files usable by the interpreter"""
return {
installer_file.id: installer_file.dest_file
for installer_file in self.installer_files
}

View file

@ -0,0 +1,27 @@
from gi.repository import GObject, Gtk
from lutris.gui.installer.script_box import InstallerScriptBox
class InstallerPicker(Gtk.ListBox):
"""List box to pick between several installers"""
__gsignals__ = {"installer-selected": (GObject.SIGNAL_RUN_FIRST, None, (str, ))}
def __init__(self, scripts):
super().__init__()
revealed = True
for script in scripts:
self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
revealed = False # Only reveal the first installer.
self.connect('row-selected', self.on_activate)
self.show_all()
@staticmethod
def on_activate(widget, row):
"""Handler for hiding and showing the revealers in children"""
for script_box_row in widget:
script_box = script_box_row.get_children()[0]
script_box.reveal(False)
installer_row = row.get_children()[0]
installer_row.reveal()

View file

@ -0,0 +1,82 @@
from gettext import gettext as _
from gi.repository import Gtk
from lutris.gui.installer.widgets import InstallerLabel
from lutris.util.strings import add_url_tags, gtk_safe
class InstallerScriptBox(Gtk.VBox):
"""Box displaying the details of a script, with associated action buttons"""
def __init__(self, script, parent=None, revealed=False):
super().__init__()
self.script = script
self.parent = parent
self.revealer = None
self.set_margin_left(12)
self.set_margin_right(12)
box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
box.pack_start(self.get_infobox(), True, True, 0)
box.add(self.get_install_button())
self.add(box)
self.add(self.get_revealer(revealed))
def get_rating(self):
"""Return a string representation of the API rating"""
try:
rating = int(self.script["rating"])
except (ValueError, TypeError, KeyError):
return ""
return "" * rating
def get_infobox(self):
"""Return the central information box"""
info_box = Gtk.VBox(spacing=6)
title_box = Gtk.HBox(spacing=6)
title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
title_box.pack_start(InstallerLabel(""), True, True, 0)
rating_label = InstallerLabel(self.get_rating())
rating_label.set_alignment(1, 0.5)
title_box.pack_end(rating_label, False, False, 0)
info_box.add(title_box)
info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
return info_box
def get_revealer(self, revealed):
"""Return the revelaer widget"""
self.revealer = Gtk.Revealer()
self.revealer.add(self.get_notes())
self.revealer.set_reveal_child(revealed)
return self.revealer
def get_install_button(self):
"""Return the install button widget"""
align = Gtk.Alignment()
align.set(0, 0, 0, 0)
install_button = Gtk.Button(_("Install"))
install_button.connect("clicked", self.on_install_clicked)
align.add(install_button)
return align
def get_notes(self):
"""Return the notes widget"""
notes = self.script["notes"].strip()
if not notes:
return Gtk.Alignment()
notes_label = InstallerLabel(notes)
notes_label.set_margin_top(12)
notes_label.set_margin_bottom(12)
notes_label.set_margin_right(12)
notes_label.set_margin_left(12)
return notes_label
def reveal(self, reveal=True):
"""Show or hide the information in the revealer"""
if self.revealer:
self.revealer.set_reveal_child(reveal)
def on_install_clicked(self, _widget):
"""Handler to notify the parent of the selected installer"""
self.parent.emit("installer-selected", self.script["slug"])

View file

@ -0,0 +1,18 @@
from gi.repository import Gtk, Pango
class InstallerLabel(Gtk.Label):
"""A label for installers"""
def __init__(self, text, wrap=True):
super().__init__()
if wrap:
self.set_line_wrap(True)
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
else:
self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
self.set_alignment(0, 0.5)
self.set_margin_right(12)
self.set_markup(text)
self.props.can_focus = False
self.set_tooltip_text(text)

View file

@ -10,15 +10,16 @@ from lutris.exceptions import UnavailableGame
from lutris.game import Game
from lutris.gui.dialogs import DirectoryDialog, InstallerSourceDialog, QuestionDialog
from lutris.gui.dialogs.cache import CacheConfigurationDialog
from lutris.gui.installer.files_box import InstallerFilesBox
from lutris.gui.installer.picker import InstallerPicker
from lutris.gui.widgets.common import FileChooserEntry, InstallerLabel
from lutris.gui.widgets.installer import InstallerFilesBox, InstallerPicker
from lutris.gui.widgets.log_text_view import LogTextView
from lutris.gui.widgets.window import BaseApplicationWindow
from lutris.installer import interpreter
from lutris.installer.errors import MissingGameDependency, ScriptingError
from lutris.util import xdgshortcuts
from lutris.util.log import logger
from lutris.util.strings import add_url_tags, gtk_safe
from lutris.util.strings import add_url_tags, gtk_safe, human_size
class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public-methods
@ -288,9 +289,14 @@ class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public
"""Enable continue button if a non-empty choice is selected"""
self.continue_button.set_sensitive(bool(widget.get_active_id()))
def on_runners_ready(self, _widget):
def on_runners_ready(self, _widget=None):
"""The runners are ready, proceed with file selection"""
self.clean_widgets()
if self.interpreter.extras is None:
extras = self.interpreter.get_extras()
if extras:
self.show_extras(extras)
return
try:
self.interpreter.installer.prepare_game_files()
except UnavailableGame as ex:
@ -320,6 +326,67 @@ class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public
"clicked", self.on_files_confirmed, installer_files_box
)
def get_extra_label(self, extra):
label = extra["name"]
_infos = []
if extra.get("total_size"):
_infos.append(human_size(extra["total_size"]))
if extra.get("type"):
_infos.append(extra["type"])
if _infos:
label += " (%s)" % ", ".join(_infos)
return label
def show_extras(self, extras):
extra_liststore = Gtk.ListStore(
bool, # is selected?
str, # id
str, # label
)
for extra in extras:
extra_liststore.append((False, extra["id"], self.get_extra_label(extra)))
treeview = Gtk.TreeView(extra_liststore)
treeview.set_headers_visible(False)
renderer_toggle = Gtk.CellRendererToggle()
renderer_toggle.connect("toggled", self.on_extra_toggled, extra_liststore)
renderer_text = Gtk.CellRendererText()
installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0)
treeview.append_column(installed_column)
label_column = Gtk.TreeViewColumn(None, renderer_text)
label_column.add_attribute(renderer_text, "text", 2)
label_column.set_property("min-width", 80)
treeview.append_column(label_column)
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=treeview,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
scrolledwindow.show_all()
self.widget_box.pack_end(scrolledwindow, True, True, 10)
self.continue_button.show()
self.continue_button.set_sensitive(True)
self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_liststore)
def on_extra_toggled(self, _widget, path, store):
row = store[path]
row[0] = not row[0]
def on_extras_confirmed(self, _button, extra_store):
logger.debug("Extras confirmed")
selected_extras = []
for extra in extra_store:
if extra[0]:
selected_extras.append(extra[1])
self.interpreter.extras = selected_extras
print(selected_extras)
self.on_runners_ready()
def on_files_ready(self, _widget, is_ready):
"""Toggle state of continue button based on ready state"""
logger.debug("Files are ready: %s", is_ready)
@ -458,7 +525,7 @@ class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public
self.widget_box.pack_start(label, False, False, 18)
def add_spinner(self):
"""Display a wait icon."""
"""Show a spinner in the middle of the view"""
self.clean_widgets()
spinner = Gtk.Spinner()
self.widget_box.pack_start(spinner, False, False, 18)

View file

@ -1,11 +1,9 @@
# Standard Library
from gettext import gettext as _
# Third Party Libraries
from gi.repository import GLib, GObject, Gtk, Pango
# Lutris Modules
from lutris.util.downloader import Downloader
from lutris.util.strings import gtk_safe
class DownloadProgressBox(Gtk.Box):
@ -93,7 +91,7 @@ class DownloadProgressBox(Gtk.Box):
if self.downloader.state == self.downloader.CANCELLED:
self._set_text(_("Download interrupted"))
else:
self._set_text(self.downloader.error)
self._set_text(self.downloader.error[:80])
if self.downloader.state == self.downloader.CANCELLED:
self.emit("cancel", {})
return False
@ -115,5 +113,5 @@ class DownloadProgressBox(Gtk.Box):
return True
def _set_text(self, text):
markup = u"<span size='10000'>{}</span>".format(text)
markup = u"<span size='10000'>{}</span>".format(gtk_safe(text))
self.progress_label.set_markup(markup)

View file

@ -144,9 +144,15 @@ class LutrisInstaller: # pylint: disable=too-many-instance-attributes
if not installer_file_id:
logger.warning("Could not find a file for this service")
return
logger.info("Should install %s", self.interpreter.extras)
if self.service.has_extras:
self.service.selected_extras = self.interpreter.extras
installer_files = self.service.get_installer_files(self, installer_file_id)
for installer_file in installer_files:
self.files.append(installer_file)
if not installer_files:
# Failed to get the service game, put back a user provided file
self.files.insert(0, "N/A: Provider installer file")
def _substitute_config(self, script_config):
"""Substitute values such as $GAMEDIR in a config dict."""

View file

@ -36,6 +36,9 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
self.service = parent.service if parent else None
self.appid = parent.appid if parent else None
self.game_dir_created = False # Whether a game folder was created during the install
# Extra files for installers, either None if the extras haven't been checked yet.
# Or a list of IDs of extras to be downloaded during the install
self.extras = None
self.game_disc = None
self.game_files = {}
self.cancelled = False
@ -147,6 +150,13 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
self.target_path = game["directory"]
self.requires = game["installer_slug"]
def get_extras(self):
"""Get extras and store them to move them at the end of the install"""
if not self.service or not self.service.has_extras:
self.extras = []
self.extras = self.service.get_extras(self.appid)
return self.extras
def launch_install(self):
"""Launch the install process"""
self.runners_to_install = self.get_runners_to_install()
@ -262,6 +272,12 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
os.chdir(self.target_path)
if not os.path.exists(self.cache_path):
os.mkdir(self.cache_path)
# Copy extras to game folder
for extra in self.extras:
self.installer.script["installer"].append(
{"copy": {"src": extra, "dst": "$GAMEDIR/extras"}}
)
self._iter_commands()
def _iter_commands(self, result=None, exception=None):

View file

@ -20,6 +20,7 @@ class BaseService(GObject.Object):
"""Base class for local services"""
id = NotImplemented
_matcher = None
has_extras = False
name = NotImplemented
icon = NotImplemented
online = False

View file

@ -6,7 +6,7 @@ from gettext import gettext as _
from urllib.parse import parse_qsl, urlencode, urlparse
from lutris import settings
from lutris.exceptions import AuthenticationError, MultipleInstallerError, UnavailableGame
from lutris.exceptions import AuthenticationError, UnavailableGame
from lutris.gui.dialogs import WebConnectDialog
from lutris.installer import AUTO_ELF_EXE, AUTO_WIN32_EXE
from lutris.installer.installer_file import InstallerFile
@ -64,6 +64,7 @@ class GOGService(OnlineService):
id = "gog"
name = _("GOG")
icon = "gog"
has_extras = True
medias = {
"banner_small": GogSmallBanner,
"banner": GogMediumBanner,
@ -85,6 +86,10 @@ class GOGService(OnlineService):
is_loading = False
def __init__(self):
super().__init__()
self.selected_extras = None
@property
def login_url(self):
"""Return authentication URL"""
@ -221,7 +226,6 @@ class GOGService(OnlineService):
def get_library(self):
"""Return the user's library of GOG games"""
if system.path_exists(self.cache_path):
logger.debug("Returning cached GOG library")
with open(self.cache_path, "r") as gog_cache:
@ -265,10 +269,11 @@ class GOGService(OnlineService):
logger.info("Getting download info for %s", downlink)
try:
response = self.make_api_request(downlink)
except HTTPError:
raise UnavailableGame()
except HTTPError as ex:
logger.error("HTTP error: %s", ex)
raise UnavailableGame
if not response:
raise UnavailableGame()
raise UnavailableGame
for field in ("checksum", "downlink"):
field_url = response[field]
parsed = urlparse(field_url)
@ -277,15 +282,27 @@ class GOGService(OnlineService):
return response
def get_downloads(self, gogid):
"""Return all available downloads for a GOG ID"""
gog_data = self.get_game_details(gogid)
if not gog_data:
logger.warning("Unable to get GOG data for game %s", gogid)
return []
return gog_data["downloads"]
def get_installers(self, gogid, runner, language="en"):
"""Return available installers for a GOG game"""
def get_extras(self, gogid):
"""Return a list of bonus content available for a GOG ID"""
downloads = self.get_downloads(gogid)
return [
{
"name": download.get("name", ""),
"type": download.get("type", ""),
"total_size": download.get("total_size", 0),
"id": str(download["id"]),
} for download in downloads.get("bonus_content") or []
]
def get_installers(self, downloads, runner, language="en"):
"""Return available installers for a GOG game"""
# Filter out Mac installers
gog_installers = [installer for installer in downloads["installers"] if installer["os"] != "mac"]
@ -303,43 +320,66 @@ class GOGService(OnlineService):
gog_installers = [installer for installer in gog_installers if installer["language"] == language]
return gog_installers
def get_installer(self, gogid, runner):
"""Return a single installer for a given runner"""
if not self.is_connected():
logger.info("You are not connected to GOG")
self.login()
if not self.is_connected():
raise UnavailableGame
gog_installers = self.get_installers(gogid, runner)
if len(gog_installers) > 1:
raise MultipleInstallerError()
try:
installer = gog_installers[0]
except IndexError:
raise UnavailableGame
return installer
def get_gog_download_links(self, gogid, runner):
"""Return a list of downloadable links for a GOG game"""
installer = self.get_installer(gogid, runner)
def query_download_links(self, download):
"""Convert files from the GOG API to a format compatible with lutris installers"""
download_links = []
for game_file in installer.get('files', []):
for game_file in download.get("files", []):
downlink = game_file.get("downlink")
if not downlink:
logger.error("No download information for %s", installer)
logger.error("No download information for %s", game_file)
continue
download_info = self.get_download_info(downlink)
for field in ('checksum', 'downlink'):
download_links.append({"url": download_info[field], "filename": download_info[field + "_filename"]})
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"]
})
return download_links
def get_extra_files(self, downloads, installer):
extra_files = []
for extra in downloads["bonus_content"]:
if str(extra["id"]) not in self.selected_extras:
continue
links = self.query_download_links(extra)
for link in links:
if link["filename"].endswith(".xml"):
# GOG gives a link for checksum XML files for bonus content
# but downloading them results in a 404 error.
continue
extra_files.append(
InstallerFile(installer.game_slug, str(extra["id"]), {
"url": link["url"],
"filename": link["filename"],
})
)
return extra_files
def get_installer_files(self, installer, installer_file_id):
if not self.is_connected():
self.login()
if not self.is_connected():
logger.warning("Not connected to GOG, not returning any files")
return []
try:
links = self.get_gog_download_links(installer.service_appid, installer.runner)
downloads = self.get_downloads(installer.service_appid)
gog_installers = self.get_installers(downloads, installer.runner)
if not gog_installers:
return []
if len(gog_installers) > 1:
logger.warning("More than 1 GOG installer found, picking first.")
_installer = gog_installers[0]
links = self.query_download_links(_installer)
except HTTPError:
raise UnavailableGame("Couldn't load the download links for this game")
if not links:
raise UnavailableGame("Could not fing GOG game")
files = []
file_id_provided = False # Only assign installer_file_id once
for index, link in enumerate(links):
@ -361,6 +401,9 @@ class GOGService(OnlineService):
)
if not file_id_provided:
raise UnavailableGame("Unable to determine correct file to launch installer")
if self.selected_extras:
for extra_file in self.get_extra_files(downloads, installer):
files.append(extra_file)
return files
def generate_installer(self, db_game):

View file

@ -1,11 +1,8 @@
# Standard Library
import os
import time
# Third Party Libraries
import requests
# Lutris Modules
from lutris import __version__
from lutris.util import jobs
from lutris.util.log import logger

View file

@ -32,6 +32,7 @@ setup(
'lutris.gui',
'lutris.gui.config',
'lutris.gui.dialogs',
'lutris.gui.installer',
'lutris.gui.views',
'lutris.gui.widgets',
'lutris.installer',

View file

@ -102,7 +102,7 @@ class TestGameDialog(TestCase):
self.assertTrue(pga_game)
game = Game(pga_game['id'])
self.assertEqual(game.name, 'Test game')
game.remove(from_library=True)
game.remove()
class TestSort(TestCase):