From abe41635fd71df22ef7e24ab9ef872633a33dbe5 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 23 May 2023 15:26:48 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20More=20work=20on=20managers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 20 +++++--- src/store/manager.py | 34 ++++++++++++- src/store/sgdb_manager.py | 19 ++++++-- src/store/store.py | 100 ++++++++++++++++++++++++++------------ 4 files changed, 130 insertions(+), 43 deletions(-) diff --git a/src/main.py b/src/main.py index 0cbb3a7..73b5f63 100644 --- a/src/main.py +++ b/src/main.py @@ -30,19 +30,20 @@ gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GLib, Gtk import src.shared as shared -from src.store.store import Store from src.details_window import DetailsWindow from src.importer.importer import Importer -from src.importer.sources.lutris_source import ( - LutrisFlatpakSource, - LutrisNativeSource, -) +from src.importer.sources.lutris_source import LutrisFlatpakSource, LutrisNativeSource from src.importer.sources.steam_source import ( - SteamNativeSource, SteamFlatpakSource, + SteamNativeSource, SteamWindowsSource, ) from src.preferences import PreferencesWindow +from src.store.display_manager import DisplayManager +from src.store.file_manager import FileManager +from src.store.sgdb_manager import SGDBManager +from src.store.steam_api_manager import SteamAPIManager +from src.store.store import Store from src.window import CartridgesWindow @@ -56,10 +57,13 @@ class CartridgesApplication(Adw.Application): ) def do_activate(self): # pylint: disable=arguments-differ - # Create the games store + # Create the games store and its managers if not self.store: - # TODO add managers to the store self.store = Store() + self.store.add_manager(SteamAPIManager()) + self.store.add_manager(SGDBManager()) + self.store.add_manager(FileManager()) + self.store.add_manager(DisplayManager()) # Create the main window self.win = self.props.active_window # pylint: disable=no-member diff --git a/src/store/manager.py b/src/store/manager.py index 799851a..361f63a 100644 --- a/src/store/manager.py +++ b/src/store/manager.py @@ -1,14 +1,46 @@ from abc import abstractmethod +from gi.repository import Gio from src.game import Game class Manager: """Class in charge of handling a post creation action for games. - May connect to signals on the game to handle them.""" + + * May connect to signals on the game to handle them. + * May cancel its running tasks on critical error, + in that case a new cancellable must be generated for new tasks to run. + """ run_after: set[type["Manager"]] + cancellable: Gio.Cancellable + errors: list[Exception] + + def __init__(self) -> None: + super().__init__() + self.cancellable = Gio.Cancellable() + self.errors = list() + + def cancel_tasks(self): + """Cancel all tasks for this manager""" + self.cancellable.cancel() + + def reset_cancellable(self): + """Reset the cancellable for this manager. + Alreadyn scheduled Tasks will no longer be cancellable.""" + self.cancellable = Gio.Cancellable() + + def report_error(self, error: Exception): + """Report an error that happened in of run""" + self.errors.append(error) + + def collect_errors(self) -> list[Exception]: + """Get the errors produced by the manager and remove them from self.errors""" + errors = list(self.errors) + self.errors.clear() + return errors + @abstractmethod def run(self, game: Game) -> None: """Pass the game through the manager. diff --git a/src/store/sgdb_manager.py b/src/store/sgdb_manager.py index 22f5699..faec500 100644 --- a/src/store/sgdb_manager.py +++ b/src/store/sgdb_manager.py @@ -1,11 +1,22 @@ -from src.store.manager import Manager +from requests import HTTPError + from src.game import Game -from src.utils.steamgriddb import SGDBHelper +from src.store.manager import Manager +from src.utils.steamgriddb import SGDBAuthError, SGDBError, SGDBHelper class SGDBManager(Manager): """Manager in charge of downloading a game's cover from steamgriddb""" def run(self, game: Game) -> None: - # TODO - pass + try: + sgdb = SGDBHelper() + sgdb.conditionaly_update_cover(game) + except SGDBAuthError as error: + # If invalid auth, cancel all SGDBManager tasks + self.cancellable.cancel() + self.report_error(error) + except (HTTPError, SGDBError) as error: + # On other error, just report it + self.report_error(error) + pass diff --git a/src/store/store.py b/src/store/store.py index 1c80032..6aea015 100644 --- a/src/store/store.py +++ b/src/store/store.py @@ -1,18 +1,39 @@ +from typing import Iterable + +from gi.repository import GObject + import src.shared as shared from src.game import Game from src.store.manager import Manager from src.utils.task import Task -class Pipeline(set): - """Class representing a set of Managers for a game""" +class Pipeline(GObject.Object): + """Class representing a set of managers for a game""" + + game: Game + + waiting: set[Manager] + running: set[Manager] + done: set[Manager] + + def __init__(self, managers: Iterable[Manager]) -> None: + super().__init__() + self.waiting = set(managers) + self.running = set() + self.done = set() @property - def blocked_managers(self) -> set(Manager): + def not_done(self) -> set[Manager]: + """Get the managers that are not done yet""" + return self.waiting + self.running + + @property + def blocked(self) -> set[Manager]: """Get the managers that cannot run because their dependencies aren't done""" blocked = set() - for manager_a in self: - for manager_b in self: + for manager_a in self.waiting: + for manager_b in self.not_done: if manager_a == manager_b: continue if type(manager_b) in manager_a.run_after: @@ -20,9 +41,43 @@ class Pipeline(set): return blocked @property - def startable_managers(self) -> set(Manager): + def ready(self) -> set[Manager]: """Get the managers that can be run""" - return self - self.blocked_managers + return self.waiting - self.blocked + + def advance(self): + """Spawn tasks for managers that are able to run for a game""" + for manager in self.ready: + self.waiting.remove(manager) + self.running.add(manager) + data = (manager,) + task = Task.new(self, manager.cancellable, self.manager_task_callback, data) + task.set_task_data(data) + task.run_in_thread(self.manager_task_thread_func) + + @GObject.Signal(name="manager-started") + def manager_started(self, manager: Manager) -> None: + """Signal emitted when a manager is started""" + pass + + def manager_task_thread_func(self, _task, _source_object, data, cancellable): + """Thread function for manager tasks""" + manager, *_rest = data + self.emit("manager-started", manager) + manager.run(self.game, cancellable) + + @GObject.Signal(name="manager-done") + def manager_done(self, manager: Manager) -> None: + """Signal emitted when a manager is done""" + pass + + def manager_task_callback(self, _source_object, _result, data): + """Callback function for manager tasks""" + manager, *_rest = data + self.running.remove(manager) + self.done.add(manager) + self.emit("manager-done", manager) + self.advance() class Store: @@ -42,35 +97,20 @@ class Store: """Add a manager class that will run when games are added""" self.managers.add(manager) - def add_game(self, game: Game, replace=False): + def add_game(self, game: Game, replace=False) -> Pipeline: """Add a game to the app if not already there - :param replace bool: Replace the game if it already exists""" + :param replace bool: Replace the game if it already exists + :return: + """ if ( game.game_id in self.games and not self.games[game.game_id].removed and not replace ): return + pipeline = Pipeline(self.managers) self.games[game.game_id] = game - self.pipelines[game.game_id] = Pipeline(self.managers) - self.advance_pipeline(game) - - def advance_pipeline(self, game: Game): - """Spawn tasks for managers that are able to run for a game""" - for manager in self.pipelines[game.game_id].startable_managers: - data = (manager, game) - task = Task.new(None, None, self.manager_task_callback, data) - task.set_task_data(data) - task.run_in_thread(self.manager_task_thread_func) - - def manager_task_thread_func(self, _task, _source_object, data, _cancellable): - """Thread function for manager tasks""" - manager, game, *_rest = data - manager.run(game) - - def manager_task_callback(self, _source_object, _result, data): - """Callback function for manager tasks""" - manager, game, *_rest = data - self.pipelines[game.game_id].remove(manager) - self.advance_pipeline(game) + self.pipelines[game.game_id] = pipeline + pipeline.advance() + return pipeline