diff --git a/src/importer/importer.py b/src/importer/importer.py index fdbee81..d824b1a 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -61,10 +61,6 @@ class Importer: 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,)) @@ -72,6 +68,8 @@ class Importer: task.set_task_data((source,)) task.run_in_thread(self.source_task_thread_func) + self.progress_changed_callback() + def create_dialog(self): """Create the import dialog""" self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12) @@ -164,6 +162,7 @@ class Importer: Callback called when the import process has progressed Triggered when: + * All sources have been started * A source finishes * A pipeline finishes """ diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 4ffda7b..5682f03 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -45,6 +45,7 @@ class ItchSourceIterator(SourceIterator): "version": shared.SPEC_VERSION, "added": int(time()), "source": self.source.id, + "name": row[1], "game_id": self.source.game_id_format.format(game_id=row[0]), "executable": self.source.executable_format.format(cave_id=row[4]), } @@ -65,6 +66,7 @@ class ItchLinuxSource(ItchSource, LinuxSource): variant = "linux" executable_format = "xdg-open itch://caves/{cave_id}/launch" + @property @ItchSource.replaced_by_schema_key() @replaced_by_path("~/.var/app/io.itch.itch/config/itch/") @replaced_by_env_path("XDG_DATA_HOME", "itch/") @@ -77,6 +79,7 @@ class ItchWindowsSource(ItchSource, WindowsSource): variant = "windows" executable_format = "start itch://caves/{cave_id}/launch" + @property @ItchSource.replaced_by_schema_key() @replaced_by_env_path("appdata", "itch/") def location(self) -> Path: diff --git a/src/main.py b/src/main.py index 1ffa240..834e204 100644 --- a/src/main.py +++ b/src/main.py @@ -36,15 +36,16 @@ from src.game import Game from src.importer.importer import Importer from src.importer.sources.bottles_source import BottlesLinuxSource from src.importer.sources.heroic_source import HeroicLinuxSource, HeroicWindowsSource +from src.importer.sources.itch_source import ItchLinuxSource, ItchWindowsSource from src.importer.sources.lutris_source import LutrisLinuxSource from src.importer.sources.steam_source import SteamLinuxSource, SteamWindowsSource from src.preferences import PreferencesWindow from src.store.managers.display_manager import DisplayManager from src.store.managers.file_manager import FileManager +from src.store.managers.itch_cover_manager import ItchCoverManager +from src.store.managers.local_cover_manager import LocalCoverManager from src.store.managers.sgdb_manager import SGDBManager from src.store.managers.steam_api_manager import SteamAPIManager -from src.store.managers.local_cover_manager import LocalCoverManager -from src.store.managers.itch_cover_manager import ItchCoverManager from src.store.store import Store from src.window import CartridgesWindow @@ -198,6 +199,9 @@ class CartridgesApplication(Adw.Application): importer.add_source(HeroicWindowsSource()) if shared.schema.get_boolean("bottles"): importer.add_source(BottlesLinuxSource()) + if shared.schema.get_boolean("itch"): + importer.add_source(ItchLinuxSource()) + importer.add_source(ItchWindowsSource()) importer.run() def on_remove_game_action(self, *_args): diff --git a/src/store/managers/itch_cover_manager.py b/src/store/managers/itch_cover_manager.py index 39b7928..cc8b783 100644 --- a/src/store/managers/itch_cover_manager.py +++ b/src/store/managers/itch_cover_manager.py @@ -1,11 +1,15 @@ -from urllib3.exceptions import SSLError +from pathlib import Path import requests +from gi.repository import GdkPixbuf, Gio from requests import HTTPError +from urllib3.exceptions import SSLError +from src import shared from src.game import Game from src.store.managers.async_manager import AsyncManager from src.store.managers.local_cover_manager import LocalCoverManager +from src.utils.save_cover import resize_cover, save_cover class ItchCoverManager(AsyncManager): @@ -15,5 +19,43 @@ class ItchCoverManager(AsyncManager): retryable_on = set((HTTPError, SSLError)) def manager_logic(self, game: Game, additional_data: dict) -> None: - # TODO move itch cover logic here - pass + # Get the first matching cover url + base_cover_url: str = additional_data.get("itch_cover_url", None) + still_cover_url: str = additional_data.get("itch_still_cover_url", None) + cover_url = still_cover_url or base_cover_url + if not cover_url: + return + + # Download cover + tmp_file = Gio.File.new_tmp()[0] + with requests.get(cover_url, timeout=5) as cover: + cover.raise_for_status() + Path(tmp_file.get_path()).write_bytes(cover.content) + + # TODO comment the following blocks of code + game_cover = GdkPixbuf.Pixbuf.new_from_stream_at_scale( + tmp_file.read(), 2, 2, False + ).scale_simple(*shared.image_size, GdkPixbuf.InterpType.BILINEAR) + + itch_pixbuf = GdkPixbuf.Pixbuf.new_from_stream(tmp_file.read()) + itch_pixbuf = itch_pixbuf.scale_simple( + shared.image_size[0], + itch_pixbuf.get_height() * (shared.image_size[0] / itch_pixbuf.get_width()), + GdkPixbuf.InterpType.BILINEAR, + ) + itch_pixbuf.composite( + game_cover, + 0, + (shared.image_size[1] - itch_pixbuf.get_height()) / 2, + itch_pixbuf.get_width(), + itch_pixbuf.get_height(), + 0, + (shared.image_size[1] - itch_pixbuf.get_height()) / 2, + 1.0, + 1.0, + GdkPixbuf.InterpType.BILINEAR, + 255, + ) + + # Resize and save the cover + save_cover(game.game_id, resize_cover(pixbuf=game_cover)) diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index 732b868..6d82f61 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -65,32 +65,34 @@ class Manager: try: self.manager_logic(game, additional_data) except Exception as error: + logging_args = ( + type(error).__name__, + self.name, + f"{game.name} ({game.game_id})", + ) if error in self.continue_on: # Handle skippable errors (skip silently) return elif error in self.retryable_on: if try_index < self.max_tries: # Handle retryable errors - logging_format = "Retrying %s in %s for %s" + logging.error("Retrying %s in %s for %s", *logging_args) sleep(self.retry_delay) self.execute_resilient_manager_logic( game, additional_data, try_index + 1 ) else: # Handle being out of retries - logging_format = "Out of retries dues to %s in %s for %s" + logging.error( + "Out of retries dues to %s in %s for %s", *logging_args + ) self.report_error(error) else: # Handle unretryable errors - logging_format = "Unretryable %s in %s for %s" + logging.error( + "Unretryable %s in %s for %s", *logging_args, exc_info=error + ) self.report_error(error) - # Finally log errors - logging.error( - logging_format, - type(error).__name__, - self.name, - f"{game.name} ({game.game_id})", - ) def process_game( self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]