diff --git a/src/importer/decorators.py b/src/importer/decorators.py index 5c36753..75d5622 100644 --- a/src/importer/decorators.py +++ b/src/importer/decorators.py @@ -17,25 +17,38 @@ from pathlib import Path from os import PathLike from functools import wraps -def replaced_by_path(path: PathLike): # Decorator builder + +def replaced_by_path(path: PathLike): # Decorator builder """Replace the method's returned path with the override if the override exists on disk""" - def decorator(original_function): # Built decorator (closure) + + def decorator(original_function): # Built decorator (closure) @wraps(original_function) - def wrapper(*args, **kwargs): # func's override + def wrapper(*args, **kwargs): # func's override p = Path(path).expanduser() - if p.exists(): return p - else: return original_function(*args, **kwargs) + if p.exists(): + return p + else: + return original_function(*args, **kwargs) + return wrapper + return decorator -def replaced_by_schema_key(key: str): # Decorator builder + +def replaced_by_schema_key(key: str): # Decorator builder """Replace the method's returned path with the path pointed by the key if it exists on disk""" - def decorator(original_function): # Built decorator (closure) + + def decorator(original_function): # Built decorator (closure) @wraps(original_function) - def wrapper(*args, **kwargs): # func's override + def wrapper(*args, **kwargs): # func's override schema = args[0].win.schema - try: override = schema.get_string(key) - except Exception: return original_function(*args, **kwargs) - else: return replaced_by_path(override)(*args, **kwargs) + try: + override = schema.get_string(key) + except Exception: + return original_function(*args, **kwargs) + else: + return replaced_by_path(override)(*args, **kwargs) + return wrapper - return decorator \ No newline at end of file + + return decorator diff --git a/src/importer/importer.py b/src/importer/importer.py index 7ca3b07..7969d2c 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -1,21 +1,45 @@ +from threading import Thread, Lock from gi.repository import Adw, Gtk, Gio -class Importer(): +from .game import Game +from .steamgriddb import SGDBSave - # Display values + +class Importer: win = None progressbar = None import_statuspage = None import_dialog = None + sources = None - # Importer values - count_total = 0 - count_done = 0 - sources = list() + progress_lock = None + counts = None + + games_lock = None + games = None def __init__(self, win) -> None: + self.games = set() + self.sources = list() + self.counts = dict() + self.games_lock = Lock() + self.progress_lock = Lock() self.win = win + @property + def progress(self): + # Compute overall values + done = 0 + total = 0 + for source in self.sources: + done += self.counts[source.id]["done"] + total += self.counts[source.id]["total"] + # Compute progress + progress = 1 + if total > 0: + progress = 1 - done / total + return progress + def create_dialog(self): """Create the import dialog""" self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12) @@ -37,44 +61,83 @@ class Importer(): """Close the import dialog""" self.import_dialog.close() - def get_progress(self): - """Get the current progression as a number between 0 and 1""" - progress = 1 - if self.total_queue > 0: - progress = 1 - self.queue / self.total_queue - return progress - def update_progressbar(self): """Update the progress bar""" - progress = self.get_progress() + progress = self.progress() self.progressbar.set_fraction(progress) def add_source(self, source): """Add a source to import games from""" self.sources.append(source) + self.counts[source.id] = {"done": 0, "total": 0} def import_games(self): """Import games from the specified sources""" + self.create_dialog() - # TODO make that async, you doofus - # Every source does its job on the side, informing of the amount of work and when a game is done. - # At the end of the task, it returns the games. - - # Idea 1 - Work stealing queue - # 1. Sources added to the queue - # 2. Worker A takes source X and advances it - # 3. Worker A puts back source X to the queue - # 4. Worker B takes source X, that has ended - # 5. Worker B doesn't add source X back to the queue - - # Idea 2 - Gio.Task - # 1. A task is created for every source - # 2. Source X finishes - # 3. Importer adds the games - + # Scan all sources + threads = [] for source in self.sources: - for game in source: - game.save() + t = Thread( + None, + self.__import_from_source, + args=tuple( + source, + ), + ) + threads.append(t) + t.start() - self.close_dialog() \ No newline at end of file + # Wait for all of them to finish + for t in threads: + t.join() + + self.close_dialog() + + def __import_from_source(self, *args, **kwargs): + """Source import thread entry point""" + # TODO just get Game objects from the sources + source, *rest = args + + iterator = source.__iter__() + for game_values in iterator: + game = Game(self.win, game_values) + + self.games_lock.acquire() + self.games.add(game) + self.games_lock.release() + + self.progress_lock.acquire() + self.counts[source.id]["total"] = len(iterator) + if not game.blacklisted: + self.counts[source.id]["done"] += 1 + self.update_progressbar() + self.progress_lock.release() + + # TODO remove after not needed + def save_game(self, values=None, cover_path=None): + if values: + game = Game(self.win, values) + + if save_cover: + save_cover(self.win, game.game_id, resize_cover(self.win, cover_path)) + + self.games.add(game) + + self.games_no += 1 + if game.blacklisted: + self.games_no -= 1 + + self.queue -= 1 + self.update_progressbar() + + if self.queue == 0 and not self.blocker: + if self.games: + self.total_queue = len(self.games) + self.queue = len(self.games) + self.import_statuspage.set_title(_("Importing Covers…")) + self.update_progressbar() + SGDBSave(self.win, self.games, self) + else: + self.done() diff --git a/src/importer/source.py b/src/importer/source.py index 55f9227..fd2b7c4 100644 --- a/src/importer/source.py +++ b/src/importer/source.py @@ -9,7 +9,7 @@ class SourceIterator(Iterator): class States(IntEnum): DEFAULT = auto() READY = auto() - + state = States.DEFAULT source = None @@ -28,7 +28,7 @@ class SourceIterator(Iterator): class Source(Iterable): """Source of games. E.g an installed app with a config file that lists game directories""" - win = None # TODO maybe not depend on that ? + win = None # TODO maybe not depend on that ? name: str variant: str @@ -42,17 +42,25 @@ class Source(Iterable): """The source's full name""" s = self.name if self.variant is not None: - s += " (%s)" % self.variant + s += f" ({self.variant})" + return s + + @property + def id(self): + """The source's identifier""" + s = self.name.lower() + if self.variant is not None: + s += f"_{self.variant.lower()}" return s @property def game_id_format(self): """The string format used to construct game IDs""" - _format = self.name.lower() - if self.variant is not None: - _format += "_" + self.variant.lower() - _format += "_{game_id}_{game_internal_id}" - return _format + f = self.name.lower() + if self.variant is not None: + f += f"_{self.variant.lower()}" + f += "_{game_id}" + return f @property @abstractmethod @@ -63,4 +71,4 @@ class Source(Iterable): @abstractmethod def __iter__(self): """Get the source's iterator, to use in for loops""" - pass \ No newline at end of file + pass diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 5f76f72..9a832d4 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -1,21 +1,20 @@ from functools import cached_property from sqlite3 import connect -from src.game2 import Game +from src.utils.save_cover import resize_cover, save_cover from src.importer.source import Source, SourceIterator from src.importer.decorators import replaced_by_schema_key, replaced_by_path class LutrisSourceIterator(SourceIterator): - - ignore_steam_games = False + ignore_steam_games = False # TODO get that value db_connection = None db_cursor = None db_location = None db_request = None - def __init__(self, ignore_steam_games) -> None: + def __init__(self, ignore_steam_games): super().__init__() self.ignore_steam_games = ignore_steam_games self.db_connection = None @@ -43,10 +42,10 @@ class LutrisSourceIterator(SourceIterator): self.db_cursor = self.db_connection.execute(self.db_request) self.state = self.States.READY - # Get next DB value while True: + # Get next DB value try: - row = self.db_cursor.__next__() + row = self.db_cursor.__next__() except StopIteration as e: self.db_connection.close() raise e @@ -54,29 +53,41 @@ class LutrisSourceIterator(SourceIterator): # Ignore steam games if requested if row[3] == "steam" and self.ignore_steam_games: continue - + # Build basic game values = { - "name" : row[1], - "hidden" : row[4], - "source" : self.source.full_name, - "game_id" : self.source.game_id_format.format(game_id=row[2]), + "hidden": row[4], + "name": row[1], + "source": f"{self.source.id}_{row[3]}", + "game_id": self.source.game_id_format.format( + game_id=row[2], game_internal_id=row[0] + ), "executable": self.source.executable_format.format(game_id=row[2]), - "developer" : None, # TODO get developer metadata on Lutris + "developer": None, # TODO get developer metadata on Lutris } - # TODO Add official image - # TODO Add SGDB image + + # Save official image + image_path = self.source.cache_location / "coverart" / f"{row[2]}.jpg" + if image_path.exists(): + resized = resize_cover(self.win, image_path) + save_cover(self.win, values["game_id"], resized) + + # Save SGDB + return values + class LutrisSource(Source): - name = "Lutris" executable_format = "xdg-open lutris:rungameid/{game_id}" - location = None cache_location = None - def __init__(self, win) -> None: + @property + def game_id_format(self): + return super().game_id_format + "_{game_internal_id}" + + def __init__(self, win): super().__init__(win) def __iter__(self): @@ -116,4 +127,4 @@ class LutrisFlatpakSource(LutrisSource): @replaced_by_schema_key("lutris-flatpak-cache-location") @replaced_by_path("~/.var/app/net.lutris.Lutris/data/lutris/covers") def cache_location(self): - raise FileNotFoundError() \ No newline at end of file + raise FileNotFoundError()