Added Itch source

- Added call stack to unretryable errors in managers
- Added existing itch cover downloading code
- Fixed importer not closing if no source enabled

TODO
- Tidying the itch cover downloading code
- If possible, make save_cover and resize_cover work in AsyncManager-s
This commit is contained in:
GeoffreyCoulaud
2023-06-07 15:33:00 +02:00
parent 5dc6ec899a
commit 9ebd7cf7ee
5 changed files with 69 additions and 19 deletions

View File

@@ -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
"""

View File

@@ -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:

View File

@@ -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):

View File

@@ -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))

View File

@@ -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]