✨ 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:
@@ -61,10 +61,6 @@ class Importer:
|
|||||||
|
|
||||||
self.create_dialog()
|
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:
|
for source in self.sources:
|
||||||
logging.debug("Importing games from source %s", source.id)
|
logging.debug("Importing games from source %s", source.id)
|
||||||
task = Task.new(None, None, self.source_callback, (source,))
|
task = Task.new(None, None, self.source_callback, (source,))
|
||||||
@@ -72,6 +68,8 @@ class Importer:
|
|||||||
task.set_task_data((source,))
|
task.set_task_data((source,))
|
||||||
task.run_in_thread(self.source_task_thread_func)
|
task.run_in_thread(self.source_task_thread_func)
|
||||||
|
|
||||||
|
self.progress_changed_callback()
|
||||||
|
|
||||||
def create_dialog(self):
|
def create_dialog(self):
|
||||||
"""Create the import dialog"""
|
"""Create the import dialog"""
|
||||||
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
|
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
|
||||||
@@ -164,6 +162,7 @@ class Importer:
|
|||||||
Callback called when the import process has progressed
|
Callback called when the import process has progressed
|
||||||
|
|
||||||
Triggered when:
|
Triggered when:
|
||||||
|
* All sources have been started
|
||||||
* A source finishes
|
* A source finishes
|
||||||
* A pipeline finishes
|
* A pipeline finishes
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class ItchSourceIterator(SourceIterator):
|
|||||||
"version": shared.SPEC_VERSION,
|
"version": shared.SPEC_VERSION,
|
||||||
"added": int(time()),
|
"added": int(time()),
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
|
"name": row[1],
|
||||||
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
||||||
"executable": self.source.executable_format.format(cave_id=row[4]),
|
"executable": self.source.executable_format.format(cave_id=row[4]),
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ class ItchLinuxSource(ItchSource, LinuxSource):
|
|||||||
variant = "linux"
|
variant = "linux"
|
||||||
executable_format = "xdg-open itch://caves/{cave_id}/launch"
|
executable_format = "xdg-open itch://caves/{cave_id}/launch"
|
||||||
|
|
||||||
|
@property
|
||||||
@ItchSource.replaced_by_schema_key()
|
@ItchSource.replaced_by_schema_key()
|
||||||
@replaced_by_path("~/.var/app/io.itch.itch/config/itch/")
|
@replaced_by_path("~/.var/app/io.itch.itch/config/itch/")
|
||||||
@replaced_by_env_path("XDG_DATA_HOME", "itch/")
|
@replaced_by_env_path("XDG_DATA_HOME", "itch/")
|
||||||
@@ -77,6 +79,7 @@ class ItchWindowsSource(ItchSource, WindowsSource):
|
|||||||
variant = "windows"
|
variant = "windows"
|
||||||
executable_format = "start itch://caves/{cave_id}/launch"
|
executable_format = "start itch://caves/{cave_id}/launch"
|
||||||
|
|
||||||
|
@property
|
||||||
@ItchSource.replaced_by_schema_key()
|
@ItchSource.replaced_by_schema_key()
|
||||||
@replaced_by_env_path("appdata", "itch/")
|
@replaced_by_env_path("appdata", "itch/")
|
||||||
def location(self) -> Path:
|
def location(self) -> Path:
|
||||||
|
|||||||
@@ -36,15 +36,16 @@ from src.game import Game
|
|||||||
from src.importer.importer import Importer
|
from src.importer.importer import Importer
|
||||||
from src.importer.sources.bottles_source import BottlesLinuxSource
|
from src.importer.sources.bottles_source import BottlesLinuxSource
|
||||||
from src.importer.sources.heroic_source import HeroicLinuxSource, HeroicWindowsSource
|
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.lutris_source import LutrisLinuxSource
|
||||||
from src.importer.sources.steam_source import SteamLinuxSource, SteamWindowsSource
|
from src.importer.sources.steam_source import SteamLinuxSource, SteamWindowsSource
|
||||||
from src.preferences import PreferencesWindow
|
from src.preferences import PreferencesWindow
|
||||||
from src.store.managers.display_manager import DisplayManager
|
from src.store.managers.display_manager import DisplayManager
|
||||||
from src.store.managers.file_manager import FileManager
|
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.sgdb_manager import SGDBManager
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
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.store.store import Store
|
||||||
from src.window import CartridgesWindow
|
from src.window import CartridgesWindow
|
||||||
|
|
||||||
@@ -198,6 +199,9 @@ class CartridgesApplication(Adw.Application):
|
|||||||
importer.add_source(HeroicWindowsSource())
|
importer.add_source(HeroicWindowsSource())
|
||||||
if shared.schema.get_boolean("bottles"):
|
if shared.schema.get_boolean("bottles"):
|
||||||
importer.add_source(BottlesLinuxSource())
|
importer.add_source(BottlesLinuxSource())
|
||||||
|
if shared.schema.get_boolean("itch"):
|
||||||
|
importer.add_source(ItchLinuxSource())
|
||||||
|
importer.add_source(ItchWindowsSource())
|
||||||
importer.run()
|
importer.run()
|
||||||
|
|
||||||
def on_remove_game_action(self, *_args):
|
def on_remove_game_action(self, *_args):
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from urllib3.exceptions import SSLError
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from gi.repository import GdkPixbuf, Gio
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
from urllib3.exceptions import SSLError
|
||||||
|
|
||||||
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.managers.async_manager import AsyncManager
|
from src.store.managers.async_manager import AsyncManager
|
||||||
from src.store.managers.local_cover_manager import LocalCoverManager
|
from src.store.managers.local_cover_manager import LocalCoverManager
|
||||||
|
from src.utils.save_cover import resize_cover, save_cover
|
||||||
|
|
||||||
|
|
||||||
class ItchCoverManager(AsyncManager):
|
class ItchCoverManager(AsyncManager):
|
||||||
@@ -15,5 +19,43 @@ class ItchCoverManager(AsyncManager):
|
|||||||
retryable_on = set((HTTPError, SSLError))
|
retryable_on = set((HTTPError, SSLError))
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
||||||
# TODO move itch cover logic here
|
# Get the first matching cover url
|
||||||
pass
|
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))
|
||||||
|
|||||||
@@ -65,32 +65,34 @@ class Manager:
|
|||||||
try:
|
try:
|
||||||
self.manager_logic(game, additional_data)
|
self.manager_logic(game, additional_data)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
logging_args = (
|
||||||
|
type(error).__name__,
|
||||||
|
self.name,
|
||||||
|
f"{game.name} ({game.game_id})",
|
||||||
|
)
|
||||||
if error in self.continue_on:
|
if error in self.continue_on:
|
||||||
# Handle skippable errors (skip silently)
|
# Handle skippable errors (skip silently)
|
||||||
return
|
return
|
||||||
elif error in self.retryable_on:
|
elif error in self.retryable_on:
|
||||||
if try_index < self.max_tries:
|
if try_index < self.max_tries:
|
||||||
# Handle retryable errors
|
# 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)
|
sleep(self.retry_delay)
|
||||||
self.execute_resilient_manager_logic(
|
self.execute_resilient_manager_logic(
|
||||||
game, additional_data, try_index + 1
|
game, additional_data, try_index + 1
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Handle being out of retries
|
# 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)
|
self.report_error(error)
|
||||||
else:
|
else:
|
||||||
# Handle unretryable errors
|
# 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)
|
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(
|
def process_game(
|
||||||
self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]
|
self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]
|
||||||
|
|||||||
Reference in New Issue
Block a user