This commit is contained in:
Mathieu Comandon
2014-10-13 23:21:25 +02:00
8 changed files with 252 additions and 221 deletions

View File

@@ -5,9 +5,7 @@ import urllib2
import socket
from lutris import settings
from lutris import pga
from lutris.util import http
from lutris.util import resources
from lutris.util.log import logger
@@ -73,114 +71,3 @@ def get_games(slugs):
game_set = ';'.join(slugs)
url = settings.SITE_URL + "api/v1/game/set/%s/" % game_set
return http.download_json(url, params="?format=json")['objects']
def sync(caller=None):
"""Synchronize from remote to local library.
:param caller: The LutrisWindow object
:return: The synchronized games (slugs)
:rtype: set of strings
"""
logger.debug("Syncing game library")
# Get local library
local_library = pga.get_games()
local_slugs = set([game['slug'] for game in local_library])
logger.debug("%d games in local library", len(local_slugs))
# Get remote library
remote_library = get_library()
remote_slugs = set([game['slug'] for game in remote_library])
logger.debug("%d games in remote library (inc. unpublished)",
len(remote_slugs))
not_in_local = remote_slugs.difference(local_slugs)
added = sync_missing_games(not_in_local, remote_library, caller)
updated = sync_game_details(remote_library, caller)
return added.update(updated)
def sync_missing_games(not_in_local, remote_library, caller=None):
"""Get missing games in local library from remote library.
:param caller: The LutrisWindow object
:return: The slugs of the added games
:rtype: set
"""
if not not_in_local:
return set()
for game in remote_library:
slug = game['slug']
# Sync
if slug in not_in_local:
logger.debug("Adding to local library: %s", slug)
pga.add_game(
game['name'], slug=slug, year=game['year'],
updated=game['updated'], steamid=game['steamid']
)
if caller:
caller.add_game_to_view(slug)
else:
not_in_local.discard(slug)
logger.debug("%d games added", len(not_in_local))
return not_in_local
def sync_game_details(remote_library, caller):
"""Update local game details,
:param caller: The LutrisWindow object
:return: The slugs of the updated games.
:rtype: set
"""
if not remote_library:
return set()
updated = set()
# Get remote games (TODO: use this when switched API to DRF)
# remote_games = get_games(sorted(local_slugs))
# if not remote_games:
# return set()
for game in remote_library:
slug = game['slug']
sync = False
sync_icons = True
local_game = pga.get_game_by_slug(slug)
if not local_game:
continue
# Sync updated
if game['updated'] > local_game['updated']:
sync = True
# Sync new fields
else:
for key, value in local_game.iteritems():
if value or not key in game:
continue
if game[key]:
sync = True
sync_icons = False
if not sync:
continue
logger.debug("Syncing details for %s" % slug)
pga.add_or_update(
local_game['name'], local_game['runner'], slug,
year=game['year'], updated=game['updated'],
steamid=game['steamid']
)
caller.view.update_row(game)
# Sync icons (TODO: Only update if icon actually updated)
if sync_icons:
resources.download_icon(slug, 'banner', overwrite=True,
callback=caller.on_image_downloaded)
resources.download_icon(slug, 'icon', overwrite=True,
callback=caller.on_image_downloaded)
updated.add(slug)
logger.debug("%d games updated", len(updated))
return updated

View File

@@ -155,7 +155,7 @@ class LutrisWindow(object):
self.toggle_connection(False)
sync = Sync()
async_call(
sync.sync_steam,
sync.sync_steam_local,
lambda r, e: async_call(self.sync_icons, None),
caller=self
)
@@ -166,19 +166,6 @@ class LutrisWindow(object):
if self.view.__class__.__name__ == "GameGridView" \
else 'list'
def switch_splash_screen(self):
if self.view.n_games == 0:
self.splash_box.show()
self.games_scrollwindow.hide()
else:
self.splash_box.hide()
self.games_scrollwindow.show()
def sync_icons(self):
game_list = pga.get_games()
resources.fetch_icons([game_info['slug'] for game_info in game_list],
callback=self.on_image_downloaded)
def connect_signals(self):
"""Connect signals from the view with the main window.
This must be called each time the view is rebuilt.
@@ -209,6 +196,59 @@ class LutrisWindow(object):
def get_size(self, widget, _):
self.window_size = widget.get_size()
def switch_splash_screen(self):
if self.view.n_games == 0:
self.splash_box.show()
self.games_scrollwindow.hide()
else:
self.splash_box.hide()
self.games_scrollwindow.show()
def switch_view(self, view_type):
"""Switch between grid view and list view."""
logger.debug("Switching view")
self.icon_type = self.get_icon_type(view_type)
self.view.destroy()
self.view = load_view(
view_type,
get_game_list(filter_installed=self.filter_installed),
filter_text=self.search_entry.get_text(),
icon_type=self.icon_type
)
self.view.contextual_menu = self.menu
self.connect_signals()
self.games_scrollwindow.add(self.view)
self.view.show_all()
self.view.check_resize()
# Note: set_active(True *or* False) apparently makes ALL the menuitems
# in the group send the activate signal...
if self.icon_type == 'banner_small':
self.banner_small_menuitem.set_active(True)
if self.icon_type == 'icon':
self.icon_menuitem.set_active(True)
if self.icon_type == 'banner':
self.banner_menuitem.set_active(True)
def sync_library(self):
def set_library_synced(result, error):
self.set_status("Library synced")
self.switch_splash_screen()
self.set_status("Syncing library")
sync = Sync()
async_call(
sync.sync_all,
lambda r, e: async_call(self.sync_icons, set_library_synced),
caller=self
)
def sync_icons(self):
game_list = pga.get_games()
resources.fetch_icons([game_info['slug'] for game_info in game_list],
callback=self.on_image_downloaded)
def set_status(self, text):
self.status_label.set_text(text)
def refresh_status(self):
"""Refresh status bar."""
if self.running_game:
@@ -236,12 +276,28 @@ class LutrisWindow(object):
"""Open the about dialog."""
dialogs.AboutDialog()
def reset(self, *args):
"""Reset the desktop to it's initial state."""
if self.running_game:
self.running_game.quit_game()
self.status_label.set_text("Stopped %s" % self.running_game.name)
self.running_game = None
self.stop_button.set_sensitive(False)
# Callbacks
def on_connect(self, *args):
"""Callback when a user connects to his account."""
login_dialog = dialogs.ClientLoginDialog()
login_dialog.connect('connected', self.on_connect_success)
def on_connect_success(self, dialog, credentials):
if isinstance(credentials, str):
username = credentials
else:
username = credentials["username"]
self.toggle_connection(True, username)
self.sync_library()
def on_disconnect(self, *args):
api.disconnect()
self.toggle_connection(False)
@@ -262,14 +318,6 @@ class LutrisWindow(object):
logger.info(connection_status)
connection_label.set_text(connection_status)
def on_connect_success(self, dialog, credentials):
if isinstance(credentials, str):
username = credentials
else:
username = credentials["username"]
self.toggle_connection(True, username)
self.sync_library()
def on_destroy(self, *args):
"""Signal for window close."""
view_type = 'grid' if 'GridView' in str(type(self.view)) else 'list'
@@ -280,9 +328,6 @@ class LutrisWindow(object):
Gtk.main_quit(*args)
logger.debug("Quitting lutris")
def on_game_installed(self, view, slug):
view.set_installed(Game(slug))
def on_runners_activate(self, _widget, _data=None):
"""Callback when manage runners is activated."""
RunnersDialog()
@@ -302,10 +347,6 @@ class LutrisWindow(object):
def on_pga_menuitem_activate(self, _widget, _data=None):
dialogs.PgaSourceDialog()
def on_image_downloaded(self, game_slug):
is_installed = Game(game_slug).is_installed
self.view.update_image(game_slug, is_installed)
def on_search_entry_changed(self, widget):
self.view.emit('filter-updated', widget.get_text())
@@ -325,29 +366,6 @@ class LutrisWindow(object):
else:
InstallerDialog(game_slug, self)
def set_status(self, text):
self.status_label.set_text(text)
def sync_library(self):
def set_library_synced(result, error):
self.set_status("Library synced")
self.switch_splash_screen()
self.set_status("Syncing library")
sync = Sync()
async_call(
sync.sync_all,
lambda r, e: async_call(self.sync_icons, set_library_synced),
caller=self
)
def reset(self, *args):
"""Reset the desktop to it's initial state."""
if self.running_game:
self.running_game.quit_game()
self.status_label.set_text("Stopped %s" % self.running_game.name)
self.running_game = None
self.stop_button.set_sensitive(False)
def game_selection_changed(self, _widget):
# Emulate double click to workaround GTK bug #484640
# https://bugzilla.gnome.org/show_bug.cgi?id=484640
@@ -363,6 +381,27 @@ class LutrisWindow(object):
self.play_button.set_sensitive(sensitive)
self.delete_button.set_sensitive(sensitive)
def on_game_installed(self, view, slug):
view.set_installed(Game(slug))
def on_image_downloaded(self, game_slug):
is_installed = Game(game_slug).is_installed
self.view.update_image(game_slug, is_installed)
def add_manually(self, *args):
game = Game(self.view.selected_game)
add_game_dialog = AddGameDialog(self, game)
add_game_dialog.run()
if add_game_dialog.installed:
self.view.set_installed(game)
def add_game(self, _widget, _data=None):
"""Add a new game."""
add_game_dialog = AddGameDialog(self)
add_game_dialog.run()
if add_game_dialog.runner_name and add_game_dialog.slug:
self.add_game_to_view(add_game_dialog.slug)
def add_game_to_view(self, slug):
if not slug:
raise ValueError("Missing game slug")
@@ -373,20 +412,6 @@ class LutrisWindow(object):
self.switch_splash_screen()
GLib.idle_add(do_add_game)
def add_game(self, _widget, _data=None):
"""Add a new game."""
add_game_dialog = AddGameDialog(self)
add_game_dialog.run()
if add_game_dialog.runner_name and add_game_dialog.slug:
self.add_game_to_view(add_game_dialog.slug)
def add_manually(self, *args):
game = Game(self.view.selected_game)
add_game_dialog = AddGameDialog(self, game)
add_game_dialog.run()
if add_game_dialog.installed:
self.view.set_installed(game)
def on_remove_game(self, _widget, _data=None):
selected_game = self.view.selected_game
UninstallGameDialog(slug=selected_game,
@@ -439,31 +464,6 @@ class LutrisWindow(object):
self.grid_view_menuitem.set_active(view_type == 'grid')
self.list_view_menuitem.set_active(view_type == 'list')
def switch_view(self, view_type):
"""Switch between grid view and list view."""
logger.debug("Switching view")
self.icon_type = self.get_icon_type(view_type)
self.view.destroy()
self.view = load_view(
view_type,
get_game_list(filter_installed=self.filter_installed),
filter_text=self.search_entry.get_text(),
icon_type=self.icon_type
)
self.view.contextual_menu = self.menu
self.connect_signals()
self.games_scrollwindow.add(self.view)
self.view.show_all()
self.view.check_resize()
# Note: set_active(True *or* False) apparently makes ALL the menuitems
# in the group send the activate signal...
if self.icon_type == 'banner_small':
self.banner_small_menuitem.set_active(True)
if self.icon_type == 'icon':
self.icon_menuitem.set_active(True)
if self.icon_type == 'banner':
self.banner_menuitem.set_active(True)
def on_icon_type_activate(self, menuitem):
icon_type = menuitem.get_name()
if icon_type == self.view.icon_type or not menuitem.get_active():

View File

@@ -207,10 +207,10 @@ class GameView(object):
"""Update a game row to show as installed"""
row = self.get_row_by_slug(game.slug)
if not row:
logger.error("Can't find row for %s", game.slug)
return
row[COL_RUNNER] = game.runner_name
self.update_image(game.slug, is_installed=True)
self.add_game(game)
else:
row[COL_RUNNER] = game.runner_name
self.update_image(game.slug, is_installed=True)
def set_uninstalled(self, game_slug):
"""Update a game row to show as uninstalled"""

View File

@@ -159,10 +159,13 @@ class ScriptInterpreter(object):
"Downloading file %d of %d",
len(self.game_files) + 1, len(self.script["files"])
)
file_index = len(self.game_files)
try:
self._download_file(self.script["files"][len(self.game_files)])
current_file = self.script["files"][file_index]
except KeyError:
raise ScriptingError("Badly formatted script", self.script)
raise ScriptingError("Error getting file %d in %s",
file_index, self.script['files'])
self._download_file(current_file)
else:
self.current_command = 0
self._prepare_commands()
@@ -553,7 +556,8 @@ class ScriptInterpreter(object):
def _append_steam_data_to_files(self, runner_class):
steam_runner = runner_class()
data_path = steam_runner.get_game_data_path(self.steam_data['appid'])
data_path = steam_runner.get_game_path_from_appid(
self.steam_data['appid'])
if not data_path or not os.path.exists(data_path):
raise ScriptingError("Unable to get Steam data for game")
logger.debug("got data path: %s" % data_path)
@@ -591,7 +595,7 @@ class ScriptInterpreter(object):
def install_steam_game(self, runner_class):
steam_runner = runner_class()
appid = self.steam_data['appid']
if not steam_runner.get_game_data_path(appid):
if not steam_runner.get_game_path_from_appid(appid):
logger.debug("Installing steam game %s" % appid)
# Here the user must wait for the game to finish installing, a
# better way to handle this would be to poll StateFlags on the

View File

@@ -84,6 +84,10 @@ class dosbox(Runner):
"x64": "dosbox-0.74-x86_64.tar.gz",
}
@property
def main_file(self):
return self.settings.get('game', {}).get('main_file') or ''
@property
def browse_dir(self):
"""Return the path to open with the Browse Files action."""
@@ -99,7 +103,7 @@ class dosbox(Runner):
return os.path.join(settings.RUNNER_DIR, "dosbox/bin/dosbox")
def play(self):
main_file = self.settings["game"]["main_file"]
main_file = self.main_file
if not os.path.exists(main_file):
return {'error': "FILE_NOT_FOUND", 'file': main_file}

View File

@@ -58,7 +58,7 @@ class steam(Runner):
@property
def game_path(self):
appid = self.settings['game'].get('appid')
appid = self.config['game'].get('appid')
for apps_path in self.get_steamapps_dirs():
game_path = get_path_from_appmanifest(apps_path, appid)
if game_path:
@@ -84,6 +84,14 @@ class steam(Runner):
if os.path.exists(path):
return path
def get_game_path_from_appid(self, appid):
"""Return the game directory"""
for apps_path in self.get_steamapps_dirs():
game_path = get_path_from_appmanifest(apps_path, appid)
if game_path:
return game_path
logger.warning("Data path for SteamApp %s not found.", appid)
def get_steamapps_dirs(self):
"""Return a list of the Steam library main + custom folders."""
dirs = []

View File

@@ -69,12 +69,18 @@ class winesteam(wine.wine):
{
'option': 'appid',
'type': 'string',
'label': 'appid'
'label': 'Application ID',
'help': ("The application ID can be retrieved from the game's "
"page at steampowered.com. Example: 235320 is the "
"app ID for <i>Original War</i> in: \n"
"http://store.steampowered.com/app/<b>235320</b>/")
},
{
'option': 'args',
'type': 'string',
'label': 'arguments'
'label': 'Arguments',
'help': ("Windows command line arguments used when launching "
"Steam")
},
{
'option': 'prefix',
@@ -173,6 +179,14 @@ class winesteam(wine.wine):
apps = config['apps']
return apps.keys()
def get_game_path_from_appid(self, appid):
"""Return the game directory"""
for apps_path in self.get_steamapps_dirs():
game_path = get_path_from_appmanifest(apps_path, appid)
if game_path:
return game_path
logger.warning("Data path for SteamApp %s not found.", appid)
def get_steamapps_dirs(self):
"""Return a list of the Steam library main + custom folders."""
dirs = []

View File

@@ -2,11 +2,12 @@
"""Synchronization of the game library with the server and other platforms."""
import os
from lutris.util.log import logger
from lutris import api, pga
from lutris.game import Game
from lutris.runners.steam import steam
from lutris.runners.winesteam import winesteam
from lutris.util import resources
from lutris.util.log import logger
class Sync(object):
@@ -14,10 +15,120 @@ class Sync(object):
self.library = pga.get_games()
def sync_all(self, caller):
api.sync(caller)
self.sync_steam(caller)
self.sync_from_remote(caller)
self.sync_steam_local(caller)
def sync_steam(self, caller):
def sync_from_remote(self, caller=None):
"""Synchronize from remote to local library.
:param caller: The LutrisWindow object
:return: The synchronized games (slugs)
:rtype: set of strings
"""
logger.debug("Syncing game library")
# Get local library
local_slugs = set([game['slug'] for game in self.library])
logger.debug("%d games in local library", len(local_slugs))
# Get remote library
remote_library = api.get_library()
remote_slugs = set([game['slug'] for game in remote_library])
logger.debug("%d games in remote library (inc. unpublished)",
len(remote_slugs))
not_in_local = remote_slugs.difference(local_slugs)
added = self.sync_missing_games(not_in_local, remote_library, caller)
updated = self.sync_game_details(remote_library, caller)
return added.update(updated)
@staticmethod
def sync_missing_games(not_in_local, remote_library, caller=None):
"""Get missing games in local library from remote library.
:param caller: The LutrisWindow object
:return: The slugs of the added games
:rtype: set
"""
if not not_in_local:
return set()
for game in remote_library:
slug = game['slug']
# Sync
if slug in not_in_local:
logger.debug("Adding to local library: %s", slug)
pga.add_game(
game['name'], slug=slug, year=game['year'],
updated=game['updated'], steamid=game['steamid']
)
if caller:
caller.add_game_to_view(slug)
else:
not_in_local.discard(slug)
logger.debug("%d games added", len(not_in_local))
return not_in_local
@staticmethod
def sync_game_details(remote_library, caller):
"""Update local game details,
:param caller: The LutrisWindow object
:return: The slugs of the updated games.
:rtype: set
"""
if not remote_library:
return set()
updated = set()
# Get remote games (TODO: use this when switched API to DRF)
# remote_games = get_games(sorted(local_slugs))
# if not remote_games:
# return set()
for game in remote_library:
slug = game['slug']
sync = False
sync_icons = True
local_game = pga.get_game_by_slug(slug)
if not local_game:
continue
# Sync updated
if game['updated'] > local_game['updated']:
sync = True
# Sync new DB fields
else:
for key, value in local_game.iteritems():
if value or not key in game:
continue
if game[key]:
sync = True
sync_icons = False
if not sync:
continue
logger.debug("Syncing details for %s" % slug)
pga.add_or_update(
local_game['name'], local_game['runner'], slug,
year=game['year'], updated=game['updated'],
steamid=game['steamid']
)
caller.view.update_row(game)
# Sync icons (TODO: Only update if icon actually updated)
if sync_icons:
resources.download_icon(slug, 'banner', overwrite=True,
callback=caller.on_image_downloaded)
resources.download_icon(slug, 'icon', overwrite=True,
callback=caller.on_image_downloaded)
updated.add(slug)
logger.debug("%d games updated", len(updated))
return updated
def sync_steam_local(self, caller):
"""Sync Steam games in library with Steam and Wine Steam"""
logger.debug("Syncing local steam games")
steam_ = steam()
winesteam_ = winesteam()
@@ -33,19 +144,21 @@ class Sync(object):
# Set installed (steam linux only)
if installed_in_steam and not game_info['installed']:
logger.debug("Setting %s as installed" % game_info['name'])
pga.add_or_update(game_info['name'], 'steam',
game_info['slug'],
installed=1)
game.config.game_config.update({'game':
{'appid': str(steamid)}})
game.config.save()
caller.view.set_installed(game)
caller.view.set_installed(Game(game_info['slug']))
continue
# Set uninstalled
if not (installed_in_steam or installed_in_winesteam) \
and game_info['installed'] \
and game_info['runner'] in ['steam', 'winesteam']:
logger.debug("Setting %s as uninstalled" % game_info['name'])
pga.add_or_update(game_info['name'], '',
game_info['slug'],
installed=0)
@@ -53,6 +166,7 @@ class Sync(object):
@staticmethod
def _get_installed_steamapps(runner):
"""Return a list of appIDs of the installed Steam games."""
if not runner.is_installed():
return []
installed = []