🚧 More work on managers
This commit is contained in:
20
src/main.py
20
src/main.py
@@ -30,19 +30,20 @@ gi.require_version("Adw", "1")
|
|||||||
from gi.repository import Adw, Gio, GLib, Gtk
|
from gi.repository import Adw, Gio, GLib, Gtk
|
||||||
|
|
||||||
import src.shared as shared
|
import src.shared as shared
|
||||||
from src.store.store import Store
|
|
||||||
from src.details_window import DetailsWindow
|
from src.details_window import DetailsWindow
|
||||||
from src.importer.importer import Importer
|
from src.importer.importer import Importer
|
||||||
from src.importer.sources.lutris_source import (
|
from src.importer.sources.lutris_source import LutrisFlatpakSource, LutrisNativeSource
|
||||||
LutrisFlatpakSource,
|
|
||||||
LutrisNativeSource,
|
|
||||||
)
|
|
||||||
from src.importer.sources.steam_source import (
|
from src.importer.sources.steam_source import (
|
||||||
SteamNativeSource,
|
|
||||||
SteamFlatpakSource,
|
SteamFlatpakSource,
|
||||||
|
SteamNativeSource,
|
||||||
SteamWindowsSource,
|
SteamWindowsSource,
|
||||||
)
|
)
|
||||||
from src.preferences import PreferencesWindow
|
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
|
from src.window import CartridgesWindow
|
||||||
|
|
||||||
|
|
||||||
@@ -56,10 +57,13 @@ class CartridgesApplication(Adw.Application):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def do_activate(self): # pylint: disable=arguments-differ
|
def do_activate(self): # pylint: disable=arguments-differ
|
||||||
# Create the games store
|
# Create the games store and its managers
|
||||||
if not self.store:
|
if not self.store:
|
||||||
# TODO add managers to the store
|
|
||||||
self.store = 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
|
# Create the main window
|
||||||
self.win = self.props.active_window # pylint: disable=no-member
|
self.win = self.props.active_window # pylint: disable=no-member
|
||||||
|
|||||||
@@ -1,14 +1,46 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
from gi.repository import Gio
|
||||||
|
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
"""Class in charge of handling a post creation action for games.
|
"""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"]]
|
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
|
@abstractmethod
|
||||||
def run(self, game: Game) -> None:
|
def run(self, game: Game) -> None:
|
||||||
"""Pass the game through the manager.
|
"""Pass the game through the manager.
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
from src.store.manager import Manager
|
from requests import HTTPError
|
||||||
|
|
||||||
from src.game import Game
|
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):
|
class SGDBManager(Manager):
|
||||||
"""Manager in charge of downloading a game's cover from steamgriddb"""
|
"""Manager in charge of downloading a game's cover from steamgriddb"""
|
||||||
|
|
||||||
def run(self, game: Game) -> None:
|
def run(self, game: Game) -> None:
|
||||||
# TODO
|
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
|
pass
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from gi.repository import GObject
|
||||||
|
|
||||||
import src.shared as shared
|
import src.shared as shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.manager import Manager
|
from src.store.manager import Manager
|
||||||
from src.utils.task import Task
|
from src.utils.task import Task
|
||||||
|
|
||||||
|
|
||||||
class Pipeline(set):
|
class Pipeline(GObject.Object):
|
||||||
"""Class representing a set of Managers for a game"""
|
"""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
|
@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"""
|
"""Get the managers that cannot run because their dependencies aren't done"""
|
||||||
blocked = set()
|
blocked = set()
|
||||||
for manager_a in self:
|
for manager_a in self.waiting:
|
||||||
for manager_b in self:
|
for manager_b in self.not_done:
|
||||||
if manager_a == manager_b:
|
if manager_a == manager_b:
|
||||||
continue
|
continue
|
||||||
if type(manager_b) in manager_a.run_after:
|
if type(manager_b) in manager_a.run_after:
|
||||||
@@ -20,9 +41,43 @@ class Pipeline(set):
|
|||||||
return blocked
|
return blocked
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def startable_managers(self) -> set(Manager):
|
def ready(self) -> set[Manager]:
|
||||||
"""Get the managers that can be run"""
|
"""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:
|
class Store:
|
||||||
@@ -42,35 +97,20 @@ class Store:
|
|||||||
"""Add a manager class that will run when games are added"""
|
"""Add a manager class that will run when games are added"""
|
||||||
self.managers.add(manager)
|
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
|
"""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 (
|
if (
|
||||||
game.game_id in self.games
|
game.game_id in self.games
|
||||||
and not self.games[game.game_id].removed
|
and not self.games[game.game_id].removed
|
||||||
and not replace
|
and not replace
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
pipeline = Pipeline(self.managers)
|
||||||
self.games[game.game_id] = game
|
self.games[game.game_id] = game
|
||||||
self.pipelines[game.game_id] = Pipeline(self.managers)
|
self.pipelines[game.game_id] = pipeline
|
||||||
self.advance_pipeline(game)
|
pipeline.advance()
|
||||||
|
return pipeline
|
||||||
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user