🎨 Various changes

- Changed source additional data to dict
- Moved local cover saving into a manager
- Added stub for itch cover manager
This commit is contained in:
GeoffreyCoulaud
2023-06-07 15:00:42 +02:00
parent 98f02da36c
commit 5dc6ec899a
18 changed files with 106 additions and 54 deletions

View File

@@ -120,7 +120,7 @@ class Importer:
# Handle the result depending on its type
if isinstance(iteration_result, Game):
game = iteration_result
additional_data = tuple()
additional_data = {}
elif isinstance(iteration_result, tuple):
game, additional_data = iteration_result
elif iteration_result is None:

View File

@@ -1,20 +1,23 @@
from pathlib import Path
from time import time
from typing import Optional, Generator
import yaml
from src import shared
from src.game import Game
from src.importer.sources.source import LinuxSource, Source, SourceIterator
from src.importer.sources.source import (
LinuxSource,
Source,
SourceIterationResult,
SourceIterator,
)
from src.utils.decorators import replaced_by_env_path, replaced_by_path
from src.utils.save_cover import resize_cover, save_cover
class BottlesSourceIterator(SourceIterator):
source: "BottlesSource"
def generator_builder(self) -> Generator[Optional[Game], None, None]:
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games"""
data = (self.source.location / "library.yml").read_text("utf-8")
@@ -34,17 +37,16 @@ class BottlesSourceIterator(SourceIterator):
}
game = Game(values, allow_side_effects=False)
# Save official cover
# Get official cover path
bottle_path = entry["bottle"]["path"]
image_name = entry["thumbnail"].split(":")[1]
image_path = (
self.source.location / "bottles" / bottle_path / "grids" / image_name
)
if image_path.is_file():
save_cover(values["game_id"], resize_cover(image_path))
additional_data = {"local_image_path": image_path}
# Produce game
yield game
yield (game, additional_data)
class BottlesSource(Source):

View File

@@ -4,18 +4,18 @@ from hashlib import sha256
from json import JSONDecodeError
from pathlib import Path
from time import time
from typing import Optional, TypedDict, Generator
from typing import Optional, TypedDict
from src import shared
from src.game import Game
from src.importer.sources.source import (
LinuxSource,
Source,
SourceIterationResult,
SourceIterator,
WindowsSource,
)
from src.utils.decorators import replaced_by_env_path, replaced_by_path
from src.utils.save_cover import resize_cover, save_cover
class HeroicLibraryEntry(TypedDict):
@@ -50,7 +50,9 @@ class HeroicSourceIterator(SourceIterator):
},
}
def game_from_library_entry(self, entry: HeroicLibraryEntry) -> Optional[Game]:
def game_from_library_entry(
self, entry: HeroicLibraryEntry
) -> SourceIterationResult:
"""Helper method used to build a Game from a Heroic library entry"""
# Skip games that are not installed
@@ -72,20 +74,20 @@ class HeroicSourceIterator(SourceIterator):
),
"executable": self.source.executable_format.format(app_name=app_name),
}
game = Game(values, allow_side_effects=False)
# Save image from the heroic cache
# Get the image path from the heroic cache
# Filenames are derived from the URL that heroic used to get the file
uri: str = entry["art_square"]
if service == "epic":
uri += "?h=400&resize=1&w=300"
digest = sha256(uri.encode()).hexdigest()
image_path = self.source.location / "images-cache" / digest
if image_path.is_file():
save_cover(values["game_id"], resize_cover(image_path))
additional_data = {"local_image_path": image_path}
return Game(values, allow_side_effects=False)
return (game, additional_data)
def generator_builder(self) -> Generator[Optional[Game], None, None]:
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games from all the Heroic sub-sources"""
for sub_source in self.sub_sources.values():
@@ -102,12 +104,12 @@ class HeroicSourceIterator(SourceIterator):
continue
for entry in library:
try:
game = self.game_from_library_entry(entry)
result = self.game_from_library_entry(entry)
except KeyError:
# Skip invalid games
logging.warning("Invalid Heroic game skipped in %s", str(file))
continue
yield game
yield result
class HeroicSource(Source):

View File

@@ -1,23 +1,23 @@
from sqlite3 import connect
from pathlib import Path
from sqlite3 import connect
from time import time
from typing import Optional, Generator
from src import shared
from src.utils.decorators import replaced_by_env_path, replaced_by_path
from src.game import Game
from src.importer.sources.source import (
Source,
SourceIterator,
LinuxSource,
Source,
SourceIterationResult,
SourceIterator,
WindowsSource,
)
from src.utils.decorators import replaced_by_env_path, replaced_by_path
class ItchSourceIterator(SourceIterator):
source: "ItchSource"
def generator_builder(self) -> Generator[Optional[Game], None, None]:
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games"""
# Query the database
@@ -48,7 +48,7 @@ class ItchSourceIterator(SourceIterator):
"game_id": self.source.game_id_format.format(game_id=row[0]),
"executable": self.source.executable_format.format(cave_id=row[4]),
}
additional_data = (row[3], row[2])
additional_data = {"itch_cover_url": row[2], "itch_still_cover_url": row[3]}
game = Game(values, allow_side_effects=False)
yield (game, additional_data)

View File

@@ -1,18 +1,21 @@
from sqlite3 import connect
from time import time
from typing import Optional, Generator
from src import shared
from src.game import Game
from src.importer.sources.source import LinuxSource, Source, SourceIterator
from src.importer.sources.source import (
LinuxSource,
Source,
SourceIterationResult,
SourceIterator,
)
from src.utils.decorators import replaced_by_path
from src.utils.save_cover import resize_cover, save_cover
class LutrisSourceIterator(SourceIterator):
source: "LutrisSource"
def generator_builder(self) -> Generator[Optional[Game], None, None]:
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games"""
# Query the database
@@ -48,13 +51,12 @@ class LutrisSourceIterator(SourceIterator):
}
game = Game(values, allow_side_effects=False)
# Save official image
# Get official image path
image_path = self.source.location / "covers" / "coverart" / f"{row[2]}.jpg"
if image_path.exists():
save_cover(values["game_id"], resize_cover(image_path))
additional_data = {"local_image_path": image_path}
# Produce game
yield game
yield (game, additional_data)
class LutrisSource(Source):

View File

@@ -3,7 +3,7 @@ from abc import abstractmethod
from collections.abc import Iterable, Iterator
from functools import wraps
from pathlib import Path
from typing import Generator, Any
from typing import Generator, Any, TypedDict
from src import shared
from src.game import Game

View File

@@ -1,18 +1,18 @@
import re
from pathlib import Path
from time import time
from typing import Iterable, Optional, Generator
from typing import Iterable
from src import shared
from src.game import Game
from src.importer.sources.source import (
LinuxSource,
Source,
SourceIterationResult,
SourceIterator,
WindowsSource,
)
from src.utils.decorators import replaced_by_env_path, replaced_by_path
from src.utils.save_cover import resize_cover, save_cover
from src.utils.steam import SteamHelper, SteamInvalidManifestError
@@ -44,7 +44,7 @@ class SteamSourceIterator(SourceIterator):
)
return manifests
def generator_builder(self) -> Generator[Optional[Game], None, None]:
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games"""
appid_cache = set()
manifests = self.get_manifests()
@@ -85,11 +85,10 @@ class SteamSourceIterator(SourceIterator):
/ "librarycache"
/ f"{appid}_library_600x900.jpg"
)
if image_path.is_file():
save_cover(game.game_id, resize_cover(image_path))
additional_data = {"local_image_path": image_path}
# Produce game
yield game
yield (game, additional_data)
class SteamSource(Source):

View File

@@ -43,6 +43,8 @@ from src.store.managers.display_manager import DisplayManager
from src.store.managers.file_manager import FileManager
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
@@ -85,7 +87,9 @@ class CartridgesApplication(Adw.Application):
self.load_games_from_disk()
# Add rest of the managers for game imports
shared.store.add_manager(LocalCoverManager())
shared.store.add_manager(SteamAPIManager())
shared.store.add_manager(ItchCoverManager())
shared.store.add_manager(SGDBManager())
shared.store.add_manager(FileManager())

View File

@@ -27,7 +27,7 @@ class AsyncManager(Manager):
self.cancellable = Gio.Cancellable()
def process_game(
self, game: Game, additional_data: tuple, callback: Callable[["Manager"], Any]
self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]
) -> None:
"""Create a task to process the game in a separate thread"""
task = Task.new(None, self.cancellable, self._task_callback, (callback,))

View File

@@ -10,7 +10,7 @@ class DisplayManager(Manager):
run_after = set((SteamAPIManager, SGDBManager))
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
def manager_logic(self, game: Game, _additional_data: dict) -> None:
# TODO decouple a game from its widget
shared.win.games[game.game_id] = game
game.update()

View File

@@ -8,5 +8,5 @@ class FileManager(AsyncManager):
run_after = set((SteamAPIManager,))
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
def manager_logic(self, game: Game, _additional_data: dict) -> None:
game.save()

View File

@@ -0,0 +1,19 @@
from urllib3.exceptions import SSLError
import requests
from requests import HTTPError
from src.game import Game
from src.store.managers.async_manager import AsyncManager
from src.store.managers.local_cover_manager import LocalCoverManager
class ItchCoverManager(AsyncManager):
"""Manager in charge of downloading the game's cover from itch.io"""
run_after = set((LocalCoverManager,))
retryable_on = set((HTTPError, SSLError))
def manager_logic(self, game: Game, additional_data: dict) -> None:
# TODO move itch cover logic here
pass

View File

@@ -0,0 +1,23 @@
from pathlib import Path
from src.game import Game
from src.store.managers.manager import Manager
from src.store.managers.steam_api_manager import SteamAPIManager
from src.utils.save_cover import save_cover, resize_cover
class LocalCoverManager(Manager):
"""Manager in charge of adding the local cover image of the game"""
run_after = set((SteamAPIManager,))
def manager_logic(self, game: Game, additional_data: dict) -> None:
# Ensure that the cover path is in the additional data
try:
image_path: Path = additional_data["local_image_path"]
except KeyError:
return
if not image_path.is_file():
return
# Save the image
save_cover(game.game_id, resize_cover(image_path))

View File

@@ -49,7 +49,7 @@ class Manager:
return errors
@abstractmethod
def manager_logic(self, game: Game, additional_data: tuple) -> None:
def manager_logic(self, game: Game, additional_data: dict) -> None:
"""
Manager specific logic triggered by the run method
* Implemented by final child classes
@@ -59,7 +59,7 @@ class Manager:
"""
def execute_resilient_manager_logic(
self, game: Game, additional_data: tuple, try_index: int = 0
self, game: Game, additional_data: dict, try_index: int = 0
) -> None:
"""Execute the manager logic and handle its errors by reporting them or retrying"""
try:
@@ -93,7 +93,7 @@ class Manager:
)
def process_game(
self, game: Game, additional_data: tuple, callback: Callable[["Manager"], Any]
self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]
) -> None:
"""Pass the game through the manager"""
self.execute_resilient_manager_logic(game, additional_data)

View File

@@ -2,6 +2,8 @@ from urllib3.exceptions import SSLError
from src.game import Game
from src.store.managers.async_manager import AsyncManager
from src.store.managers.itch_cover_manager import ItchCoverManager
from src.store.managers.local_cover_manager import LocalCoverManager
from src.store.managers.steam_api_manager import SteamAPIManager
from src.utils.steamgriddb import HTTPError, SGDBAuthError, SGDBHelper
@@ -9,10 +11,10 @@ from src.utils.steamgriddb import HTTPError, SGDBAuthError, SGDBHelper
class SGDBManager(AsyncManager):
"""Manager in charge of downloading a game's cover from steamgriddb"""
run_after = set((SteamAPIManager,))
run_after = set((SteamAPIManager, LocalCoverManager, ItchCoverManager))
retryable_on = set((HTTPError, SSLError))
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
def manager_logic(self, game: Game, _additional_data: dict) -> None:
try:
sgdb = SGDBHelper()
sgdb.conditionaly_update_cover(game)

View File

@@ -15,11 +15,10 @@ class SteamAPIManager(AsyncManager):
retryable_on = set((HTTPError, SSLError))
def manager_logic(self, game: Game, _additional_data: tuple) -> None:
def manager_logic(self, game: Game, _additional_data: dict) -> None:
# Skip non-steam games
if not game.source.startswith("steam_"):
return
# Get online metadata
appid = str(game.game_id).split("_")[-1]
steam = SteamHelper()

View File

@@ -11,14 +11,14 @@ class Pipeline(GObject.Object):
"""Class representing a set of managers for a game"""
game: Game
additional_data: tuple
additional_data: dict
waiting: set[Manager]
running: set[Manager]
done: set[Manager]
def __init__(
self, game: Game, additional_data: tuple, managers: Iterable[Manager]
self, game: Game, additional_data: dict, managers: Iterable[Manager]
) -> None:
super().__init__()
self.game = game

View File

@@ -21,7 +21,7 @@ class Store:
self.managers.add(manager)
def add_game(
self, game: Game, additional_data: tuple, replace=False
self, game: Game, additional_data: dict, replace=False
) -> Pipeline | None:
"""Add a game to the app if not already there