Files
cartridges/src/importer/importer.py
GeoffreyCoulaud 98f02da36c 🎨 SourceIterator can yield addtitional data
SourceIterator-s can yield a game and a tuple of additional data.
This data will be passed to the Store, Pipeline and Managers.
2023-06-07 14:01:06 +02:00

217 lines
7.1 KiB
Python

import logging
from gi.repository import Adw, Gio, Gtk
from src import shared
from src.game import Game
from src.utils.task import Task
from src.store.pipeline import Pipeline
from src.importer.sources.source import Source
# pylint: disable=too-many-instance-attributes
class Importer:
"""A class in charge of scanning sources for games"""
progressbar = None
import_statuspage = None
import_dialog = None
sources: set[Source] = None
n_source_tasks_created: int = 0
n_source_tasks_done: int = 0
n_pipelines_done: int = 0
game_pipelines: set[Pipeline] = None
def __init__(self):
self.game_pipelines = set()
self.sources = set()
@property
def n_games_added(self):
return sum(
[
1 if not (pipeline.game.blacklisted or pipeline.game.removed) else 0
for pipeline in self.game_pipelines
]
)
@property
def pipelines_progress(self):
progress = sum([pipeline.progress for pipeline in self.game_pipelines])
try:
progress = progress / len(self.game_pipelines)
except ZeroDivisionError:
progress = 1
return progress
@property
def finished(self):
return (
self.n_source_tasks_created == self.n_source_tasks_done
and len(self.game_pipelines) == self.n_pipelines_done
)
def add_source(self, source):
self.sources.add(source)
def run(self):
"""Use several Gio.Task to import games from added sources"""
self.create_dialog()
# Single SGDB cancellable shared by all its tasks
# (If SGDB auth is bad, cancel all SGDB tasks)
self.sgdb_cancellable = Gio.Cancellable()
for source in self.sources:
logging.debug("Importing games from source %s", source.id)
task = Task.new(None, None, self.source_callback, (source,))
self.n_source_tasks_created += 1
task.set_task_data((source,))
task.run_in_thread(self.source_task_thread_func)
def create_dialog(self):
"""Create the import dialog"""
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
self.import_statuspage = Adw.StatusPage(
title=_("Importing Games…"),
child=self.progressbar,
)
self.import_dialog = Adw.Window(
content=self.import_statuspage,
modal=True,
default_width=350,
default_height=-1,
transient_for=shared.win,
deletable=False,
)
self.import_dialog.present()
def source_task_thread_func(self, _task, _obj, data, _cancellable):
"""Source import task code"""
source: Source
source, *_rest = data
# Early exit if not installed
if not source.is_installed:
logging.info("Source %s skipped, not installed", source.id)
return
logging.info("Scanning source %s", source.id)
# Initialize source iteration
iterator = iter(source)
# Get games from source
while True:
# Handle exceptions raised when iterating
try:
iteration_result = next(iterator)
except StopIteration:
break
except Exception as exception: # pylint: disable=broad-exception-caught
logging.exception(
"Exception in source %s", source.id, exc_info=exception
)
continue
# Handle the result depending on its type
if isinstance(iteration_result, Game):
game = iteration_result
additional_data = tuple()
elif isinstance(iteration_result, tuple):
game, additional_data = iteration_result
elif iteration_result is None:
continue
else:
# Warn source implementers that an invalid type was produced
# Should not happen on production code
logging.warn(
"%s produced an invalid iteration return type %s",
source.id,
type(iteration_result),
)
continue
# Register game
pipeline: Pipeline = shared.store.add_game(game, additional_data)
if pipeline is not None:
logging.info("Imported %s (%s)", game.name, game.game_id)
pipeline.connect("advanced", self.pipeline_advanced_callback)
self.game_pipelines.add(pipeline)
def update_progressbar(self):
"""Update the progressbar to show the percentage of game pipelines done"""
self.progressbar.set_fraction(self.pipelines_progress)
def source_callback(self, _obj, _result, data):
"""Callback executed when a source is fully scanned"""
source, *_rest = data
logging.debug("Import done for source %s", source.id)
self.n_source_tasks_done += 1
self.progress_changed_callback()
def pipeline_advanced_callback(self, pipeline: Pipeline):
"""Callback called when a pipeline for a game has advanced"""
if pipeline.is_done:
self.n_pipelines_done += 1
self.progress_changed_callback()
def progress_changed_callback(self):
"""
Callback called when the import process has progressed
Triggered when:
* A source finishes
* A pipeline finishes
"""
self.update_progressbar()
if self.finished:
self.import_callback()
def import_callback(self):
"""Callback called when importing has finished"""
logging.info("Import done")
self.import_dialog.close()
# TODO replace by summary if necessary
self.create_summary_toast()
# TODO create a summary of errors/warnings/tips popup (eg. SGDB, Steam libraries)
# Get the error data from shared.store.managers)
def create_summary_toast(self):
"""N games imported toast"""
toast = Adw.Toast()
toast.set_priority(Adw.ToastPriority.HIGH)
if self.n_games_added == 0:
toast.set_title(_("No new games found"))
toast.set_button_label(_("Preferences"))
toast.connect(
"button-clicked",
self.dialog_response_callback,
"open_preferences",
"import",
)
elif self.n_games_added == 1:
toast.set_title(_("1 game imported"))
elif self.n_games_added > 1:
# The variable is the number of games
toast.set_title(_("{} games imported").format(self.n_games_added))
shared.win.toast_overlay.add_toast(toast)
def open_preferences(self, page=None, expander_row=None):
shared.win.get_application().on_preferences_action(
page_name=page, expander_row=expander_row
)
def dialog_response_callback(self, _widget, response, *args):
"""Handle after-import dialogs callback"""
if response == "open_preferences":
self.open_preferences(*args)