Compare commits
7 Commits
v2.1.1
...
RilicTheFo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0440eee5d4 | ||
|
|
436a54ba5b | ||
|
|
1aff1347e3 | ||
|
|
e4dc1253ae | ||
|
|
b378110779 | ||
|
|
5708f48db8 | ||
|
|
9618fb7fff |
@@ -207,6 +207,20 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Adw.ExpanderRow dolphin_expander_row {
|
||||||
|
title: _("Dolphin");
|
||||||
|
show-enable-switch: true;
|
||||||
|
|
||||||
|
Adw.ActionRow dolphin_cache_action_row {
|
||||||
|
title: _("Cache Location");
|
||||||
|
|
||||||
|
Button dolphin_cache_file_chooser_button {
|
||||||
|
icon-name: "folder-symbolic";
|
||||||
|
valign: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Adw.ExpanderRow itch_expander_row {
|
Adw.ExpanderRow itch_expander_row {
|
||||||
title: _("itch");
|
title: _("itch");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|||||||
@@ -55,6 +55,12 @@
|
|||||||
<key name="bottles-location" type="s">
|
<key name="bottles-location" type="s">
|
||||||
<default>"~/.var/app/com.usebottles.bottles/data/bottles/"</default>
|
<default>"~/.var/app/com.usebottles.bottles/data/bottles/"</default>
|
||||||
</key>
|
</key>
|
||||||
|
<key name="dolphin" type="b">
|
||||||
|
<default>true</default>
|
||||||
|
</key>
|
||||||
|
<key name="dolphin-cache-location" type="s">
|
||||||
|
<default>"~/.var/app/org.DolphinEmu.dolphin-emu/cache/dolphin-emu/"</default>
|
||||||
|
</key>
|
||||||
<key name="itch" type="b">
|
<key name="itch" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
</key>
|
</key>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
"--talk-name=org.freedesktop.Flatpak",
|
"--talk-name=org.freedesktop.Flatpak",
|
||||||
"--filesystem=host:ro",
|
"--filesystem=host:ro",
|
||||||
|
"--filesystem=~/.var/app/org.DolphinEmu.dolphin-emu:ro",
|
||||||
"--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro",
|
"--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro",
|
||||||
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
||||||
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
||||||
|
|||||||
98
src/importer/sources/dolphin_source.py
Normal file
98
src/importer/sources/dolphin_source.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# dolphin_source.py
|
||||||
|
#
|
||||||
|
# Copyright 2023 Rilic
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from src import shared
|
||||||
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location, LocationSubPath
|
||||||
|
from src.importer.sources.source import Source, SourceIterable
|
||||||
|
from src.utils.dolphin_cache_reader import DolphinCacheReader
|
||||||
|
|
||||||
|
|
||||||
|
class DolphinSourceIterable(SourceIterable):
|
||||||
|
source: "DolphinSource"
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
|
cache_reader = DolphinCacheReader(self.source.locations.cache["cache_file"])
|
||||||
|
games_data = cache_reader.get_games()
|
||||||
|
|
||||||
|
for game_data in games_data:
|
||||||
|
# Build game
|
||||||
|
values = {
|
||||||
|
"source": self.source.source_id,
|
||||||
|
"added": added_time,
|
||||||
|
"name": Path(game_data["file_name"]).stem,
|
||||||
|
"game_id": self.source.game_id_format.format(
|
||||||
|
game_id=game_data["game_id"]
|
||||||
|
),
|
||||||
|
"executable": self.source.executable_format.format(
|
||||||
|
rom_path=game_data["file_path"],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
game = Game(values)
|
||||||
|
|
||||||
|
image_path = Path(
|
||||||
|
self.source.locations.cache["covers"] / (game_data["game_id"] + ".png")
|
||||||
|
)
|
||||||
|
additional_data = {"local_image_path": image_path}
|
||||||
|
|
||||||
|
yield (game, additional_data)
|
||||||
|
|
||||||
|
|
||||||
|
class DolphinLocations(NamedTuple):
|
||||||
|
cache: Location
|
||||||
|
|
||||||
|
|
||||||
|
class DolphinSource(Source):
|
||||||
|
name = _("Dolphin")
|
||||||
|
source_id = "dolphin"
|
||||||
|
available_on = {"linux"}
|
||||||
|
iterable_class = DolphinSourceIterable
|
||||||
|
|
||||||
|
locations = DolphinLocations(
|
||||||
|
Location(
|
||||||
|
schema_key="dolphin-cache-location",
|
||||||
|
candidates=[
|
||||||
|
shared.flatpak_dir
|
||||||
|
/ "org.DolphinEmu.dolphin-emu"
|
||||||
|
/ "cache"
|
||||||
|
/ "dolphin-emu",
|
||||||
|
shared.home / ".cache" / "dolphin-emu",
|
||||||
|
],
|
||||||
|
paths={
|
||||||
|
"cache_file": LocationSubPath("gamelist.cache"),
|
||||||
|
"covers": LocationSubPath("GameCovers", True),
|
||||||
|
},
|
||||||
|
invalid_subtitle=Location.CACHE_INVALID_SUBTITLE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def executable_format(self):
|
||||||
|
self.locations.cache.resolve()
|
||||||
|
is_flatpak = self.locations.cache.root.is_relative_to(shared.flatpak_dir)
|
||||||
|
base = "flatpak run org.DolphinEmu.dolphin-emu" if is_flatpak else "dolphin-emu"
|
||||||
|
args = '-b -e "{rom_path}"'
|
||||||
|
return f"{base} {args}"
|
||||||
@@ -97,6 +97,7 @@ class SubSourceIterable(Iterable):
|
|||||||
"""Build a Game from a Heroic library entry"""
|
"""Build a Game from a Heroic library entry"""
|
||||||
|
|
||||||
app_name = entry["app_name"]
|
app_name = entry["app_name"]
|
||||||
|
runner = entry["runner"]
|
||||||
|
|
||||||
# Build game
|
# Build game
|
||||||
values = {
|
values = {
|
||||||
@@ -107,7 +108,7 @@ class SubSourceIterable(Iterable):
|
|||||||
"game_id": self.source.game_id_format.format(
|
"game_id": self.source.game_id_format.format(
|
||||||
service=self.service, game_id=app_name
|
service=self.service, game_id=app_name
|
||||||
),
|
),
|
||||||
"executable": self.source.executable_format.format(app_name=app_name),
|
"executable": self.source.executable_format.format(runner=runner, app_name=app_name),
|
||||||
"hidden": self.source_iterable.is_hidden(app_name),
|
"hidden": self.source_iterable.is_hidden(app_name),
|
||||||
}
|
}
|
||||||
game = Game(values)
|
game = Game(values)
|
||||||
@@ -359,7 +360,7 @@ class HeroicSource(URLExecutableSource):
|
|||||||
source_id = "heroic"
|
source_id = "heroic"
|
||||||
name = _("Heroic")
|
name = _("Heroic")
|
||||||
iterable_class = HeroicSourceIterable
|
iterable_class = HeroicSourceIterable
|
||||||
url_format = "heroic://launch/{app_name}"
|
url_format = "heroic://launch/{runner}/{app_name}"
|
||||||
available_on = {"linux", "win32"}
|
available_on = {"linux", "win32"}
|
||||||
|
|
||||||
locations = HeroicLocations(
|
locations = HeroicLocations(
|
||||||
|
|||||||
10
src/main.py
10
src/main.py
@@ -35,6 +35,7 @@ from src.details_window import DetailsWindow
|
|||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.importer.importer import Importer
|
from src.importer.importer import Importer
|
||||||
from src.importer.sources.bottles_source import BottlesSource
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
|
from src.importer.sources.dolphin_source import DolphinSource
|
||||||
from src.importer.sources.flatpak_source import FlatpakSource
|
from src.importer.sources.flatpak_source import FlatpakSource
|
||||||
from src.importer.sources.heroic_source import HeroicSource
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
from src.importer.sources.itch_source import ItchSource
|
from src.importer.sources.itch_source import ItchSource
|
||||||
@@ -45,8 +46,7 @@ from src.logging.setup import log_system_info, setup_logging
|
|||||||
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.local_cover_manager import LocalCoverManager
|
from src.store.managers.cover_manager import CoverManager
|
||||||
from src.store.managers.online_cover_manager import OnlineCoverManager
|
|
||||||
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.store import Store
|
from src.store.store import Store
|
||||||
@@ -97,9 +97,8 @@ class CartridgesApplication(Adw.Application):
|
|||||||
self.load_games_from_disk()
|
self.load_games_from_disk()
|
||||||
|
|
||||||
# Add rest of the managers for game imports
|
# Add rest of the managers for game imports
|
||||||
shared.store.add_manager(LocalCoverManager())
|
shared.store.add_manager(CoverManager())
|
||||||
shared.store.add_manager(SteamAPIManager())
|
shared.store.add_manager(SteamAPIManager())
|
||||||
shared.store.add_manager(OnlineCoverManager())
|
|
||||||
shared.store.add_manager(SGDBManager())
|
shared.store.add_manager(SGDBManager())
|
||||||
shared.store.toggle_manager_in_pipelines(FileManager, True)
|
shared.store.toggle_manager_in_pipelines(FileManager, True)
|
||||||
|
|
||||||
@@ -230,6 +229,9 @@ class CartridgesApplication(Adw.Application):
|
|||||||
if shared.schema.get_boolean("bottles"):
|
if shared.schema.get_boolean("bottles"):
|
||||||
importer.add_source(BottlesSource())
|
importer.add_source(BottlesSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("dolphin"):
|
||||||
|
importer.add_source(DolphinSource())
|
||||||
|
|
||||||
if shared.schema.get_boolean("flatpak"):
|
if shared.schema.get_boolean("flatpak"):
|
||||||
importer.add_source(FlatpakSource())
|
importer.add_source(FlatpakSource())
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
|
|||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.importer.sources.bottles_source import BottlesSource
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
|
from src.importer.sources.dolphin_source import DolphinSource
|
||||||
from src.importer.sources.flatpak_source import FlatpakSource
|
from src.importer.sources.flatpak_source import FlatpakSource
|
||||||
from src.importer.sources.heroic_source import HeroicSource
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
from src.importer.sources.itch_source import ItchSource
|
from src.importer.sources.itch_source import ItchSource
|
||||||
@@ -75,6 +76,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
bottles_data_action_row = Gtk.Template.Child()
|
bottles_data_action_row = Gtk.Template.Child()
|
||||||
bottles_data_file_chooser_button = Gtk.Template.Child()
|
bottles_data_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
|
dolphin_expander_row = Gtk.Template.Child()
|
||||||
|
dolphin_cache_action_row = Gtk.Template.Child()
|
||||||
|
dolphin_cache_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
itch_expander_row = Gtk.Template.Child()
|
itch_expander_row = Gtk.Template.Child()
|
||||||
itch_config_action_row = Gtk.Template.Child()
|
itch_config_action_row = Gtk.Template.Child()
|
||||||
itch_config_file_chooser_button = Gtk.Template.Child()
|
itch_config_file_chooser_button = Gtk.Template.Child()
|
||||||
@@ -134,6 +139,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
# Sources settings
|
# Sources settings
|
||||||
for source_class in (
|
for source_class in (
|
||||||
BottlesSource,
|
BottlesSource,
|
||||||
|
DolphinSource,
|
||||||
FlatpakSource,
|
FlatpakSource,
|
||||||
HeroicSource,
|
HeroicSource,
|
||||||
ItchSource,
|
ItchSource,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class AsyncManager(Manager):
|
|||||||
def _task_thread_func(self, _task, _source_object, data, _cancellable):
|
def _task_thread_func(self, _task, _source_object, data, _cancellable):
|
||||||
"""Task thread entry point"""
|
"""Task thread entry point"""
|
||||||
game, additional_data, *_rest = data
|
game, additional_data, *_rest = data
|
||||||
self.execute_resilient_manager_logic(game, additional_data)
|
self.run(game, additional_data)
|
||||||
|
|
||||||
def _task_callback(self, _source_object, _result, data):
|
def _task_callback(self, _source_object, _result, data):
|
||||||
"""Method run after the task is done"""
|
"""Method run after the task is done"""
|
||||||
|
|||||||
197
src/store/managers/cover_manager.py
Normal file
197
src/store/managers/cover_manager.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# local_cover_manager.py
|
||||||
|
#
|
||||||
|
# Copyright 2023 Geoffrey Coulaud
|
||||||
|
# Copyright 2023 kramo
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from gi.repository import Gio, GdkPixbuf
|
||||||
|
from requests.exceptions import HTTPError, SSLError
|
||||||
|
|
||||||
|
from src import shared
|
||||||
|
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 resize_cover, save_cover
|
||||||
|
|
||||||
|
|
||||||
|
class ImageSize(NamedTuple):
|
||||||
|
width: float = 0
|
||||||
|
height: float = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aspect_ratio(self) -> float:
|
||||||
|
return self.width / self.height
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.width}x{self.height}"
|
||||||
|
|
||||||
|
def __mul__(self, scale: float | int) -> "ImageSize":
|
||||||
|
return ImageSize(
|
||||||
|
self.width * scale,
|
||||||
|
self.height * scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __truediv__(self, divisor: float | int) -> "ImageSize":
|
||||||
|
return self * (1 / divisor)
|
||||||
|
|
||||||
|
def __add__(self, other_size: "ImageSize") -> "ImageSize":
|
||||||
|
return ImageSize(
|
||||||
|
self.width + other_size.width,
|
||||||
|
self.height + other_size.height,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __sub__(self, other_size: "ImageSize") -> "ImageSize":
|
||||||
|
return self + (other_size * -1)
|
||||||
|
|
||||||
|
def element_wise_div(self, other_size: "ImageSize") -> "ImageSize":
|
||||||
|
"""Divide every element of self by the equivalent in the other size"""
|
||||||
|
return ImageSize(
|
||||||
|
self.width / other_size.width,
|
||||||
|
self.height / other_size.height,
|
||||||
|
)
|
||||||
|
|
||||||
|
def element_wise_mul(self, other_size: "ImageSize") -> "ImageSize":
|
||||||
|
"""Multiply every element of self by the equivalent in the other size"""
|
||||||
|
return ImageSize(
|
||||||
|
self.width * other_size.width,
|
||||||
|
self.height * other_size.height,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invert(self) -> "ImageSize":
|
||||||
|
"""Invert the element of self"""
|
||||||
|
return ImageSize(1, 1).element_wise_div(self)
|
||||||
|
|
||||||
|
|
||||||
|
class CoverManager(Manager):
|
||||||
|
"""
|
||||||
|
Manager in charge of adding the cover image of the game
|
||||||
|
|
||||||
|
Order of priority is:
|
||||||
|
1. local cover
|
||||||
|
2. icon cover
|
||||||
|
3. online cover
|
||||||
|
"""
|
||||||
|
|
||||||
|
run_after = (SteamAPIManager,)
|
||||||
|
retryable_on = (HTTPError, SSLError, ConnectionError)
|
||||||
|
|
||||||
|
def download_image(self, url: str) -> Path:
|
||||||
|
image_file = Gio.File.new_tmp()[0]
|
||||||
|
path = Path(image_file.get_path())
|
||||||
|
with requests.get(url, timeout=5) as cover:
|
||||||
|
cover.raise_for_status()
|
||||||
|
path.write_bytes(cover.content)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def is_stretchable(self, source_size: ImageSize, cover_size: ImageSize) -> bool:
|
||||||
|
is_taller = source_size.aspect_ratio < cover_size.aspect_ratio
|
||||||
|
if is_taller:
|
||||||
|
return True
|
||||||
|
max_stretch = 0.12
|
||||||
|
resized_height = (1 / source_size.aspect_ratio) * cover_size.width
|
||||||
|
stretch = 1 - (resized_height / cover_size.height)
|
||||||
|
return stretch <= max_stretch
|
||||||
|
|
||||||
|
def save_composited_cover(
|
||||||
|
self,
|
||||||
|
game: Game,
|
||||||
|
image_path: Path,
|
||||||
|
scale: float = 1,
|
||||||
|
blur_size: ImageSize = ImageSize(2, 2),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Save the image composited with a background blur.
|
||||||
|
If the image is stretchable, just stretch it.
|
||||||
|
|
||||||
|
:param game: The game to save the cover for
|
||||||
|
:param path: Path where the source image is located
|
||||||
|
:param scale:
|
||||||
|
Scale of the smalled image side
|
||||||
|
compared to the corresponding side in the cover
|
||||||
|
:param blur_size: Size of the downscaled image used for the blur
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load source image
|
||||||
|
source = GdkPixbuf.Pixbuf.new_from_file(str(image_path))
|
||||||
|
source_size = ImageSize(source.get_width(), source.get_height())
|
||||||
|
cover_size = ImageSize._make(shared.image_size)
|
||||||
|
|
||||||
|
# Stretch if possible
|
||||||
|
if scale == 1 and self.is_stretchable(source_size, cover_size):
|
||||||
|
save_cover(game.game_id, resize_cover(pixbuf=source))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create the blurred cover background
|
||||||
|
# fmt: off
|
||||||
|
cover = (
|
||||||
|
source
|
||||||
|
.scale_simple(*blur_size, GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
.scale_simple(*cover_size, GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
# Scale to fit, apply scaling, then center
|
||||||
|
uniform_scale = scale * min(cover_size.element_wise_div(source_size))
|
||||||
|
source_in_cover_size = source_size * uniform_scale
|
||||||
|
source_in_cover_position = (cover_size - source_in_cover_size) / 2
|
||||||
|
|
||||||
|
# Center the scaled source image in the cover
|
||||||
|
source.composite(
|
||||||
|
cover,
|
||||||
|
*source_in_cover_position,
|
||||||
|
*source_in_cover_size,
|
||||||
|
*source_in_cover_position,
|
||||||
|
uniform_scale,
|
||||||
|
uniform_scale,
|
||||||
|
GdkPixbuf.InterpType.BILINEAR,
|
||||||
|
255,
|
||||||
|
)
|
||||||
|
save_cover(game.game_id, resize_cover(pixbuf=cover))
|
||||||
|
|
||||||
|
def main(self, game: Game, additional_data: dict) -> None:
|
||||||
|
if game.blacklisted:
|
||||||
|
return
|
||||||
|
for key in (
|
||||||
|
"local_image_path",
|
||||||
|
"local_icon_path",
|
||||||
|
"online_cover_url",
|
||||||
|
):
|
||||||
|
# Get an image path
|
||||||
|
if not (value := additional_data.get(key)):
|
||||||
|
continue
|
||||||
|
if key == "online_cover_url":
|
||||||
|
image_path = self.download_image(value)
|
||||||
|
else:
|
||||||
|
image_path = Path(value)
|
||||||
|
if not image_path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Icon cover
|
||||||
|
if key == "local_icon_path":
|
||||||
|
self.save_composited_cover(
|
||||||
|
game,
|
||||||
|
image_path,
|
||||||
|
scale=0.7,
|
||||||
|
blur_size=ImageSize(1, 2),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.save_composited_cover(game, image_path)
|
||||||
@@ -30,7 +30,7 @@ class DisplayManager(Manager):
|
|||||||
run_after = (SteamAPIManager, SGDBManager)
|
run_after = (SteamAPIManager, SGDBManager)
|
||||||
signals = {"update-ready"}
|
signals = {"update-ready"}
|
||||||
|
|
||||||
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
def main(self, game: Game, _additional_data: dict) -> None:
|
||||||
if game.get_parent():
|
if game.get_parent():
|
||||||
game.get_parent().get_parent().remove(game)
|
game.get_parent().get_parent().remove(game)
|
||||||
if game.get_parent():
|
if game.get_parent():
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class FileManager(AsyncManager):
|
|||||||
run_after = (SteamAPIManager,)
|
run_after = (SteamAPIManager,)
|
||||||
signals = {"save-ready"}
|
signals = {"save-ready"}
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
def main(self, game: Game, additional_data: dict) -> None:
|
||||||
if additional_data.get("skip_save"): # Skip saving when loading games from disk
|
if additional_data.get("skip_save"): # Skip saving when loading games from disk
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
# local_cover_manager.py
|
|
||||||
#
|
|
||||||
# Copyright 2023 Geoffrey Coulaud
|
|
||||||
# Copyright 2023 kramo
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from gi.repository import GdkPixbuf
|
|
||||||
|
|
||||||
from src import shared
|
|
||||||
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 resize_cover, save_cover
|
|
||||||
|
|
||||||
|
|
||||||
class LocalCoverManager(Manager):
|
|
||||||
"""Manager in charge of adding the local cover image of the game"""
|
|
||||||
|
|
||||||
run_after = (SteamAPIManager,)
|
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
|
||||||
if image_path := additional_data.get("local_image_path"):
|
|
||||||
if not image_path.is_file():
|
|
||||||
logging.error("Local image path is not a file: %s", image_path)
|
|
||||||
return
|
|
||||||
save_cover(game.game_id, resize_cover(image_path))
|
|
||||||
elif icon_path := additional_data.get("local_icon_path"):
|
|
||||||
cover_width, cover_height = shared.image_size
|
|
||||||
|
|
||||||
dest_width = cover_width * 0.7
|
|
||||||
dest_height = cover_width * 0.7
|
|
||||||
|
|
||||||
dest_x = cover_width * 0.15
|
|
||||||
dest_y = (cover_height - dest_height) / 2
|
|
||||||
|
|
||||||
image = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)).scale_simple(
|
|
||||||
dest_width, dest_height, GdkPixbuf.InterpType.BILINEAR
|
|
||||||
)
|
|
||||||
|
|
||||||
cover = image.scale_simple(
|
|
||||||
1, 2, GdkPixbuf.InterpType.BILINEAR
|
|
||||||
).scale_simple(cover_width, cover_height, GdkPixbuf.InterpType.BILINEAR)
|
|
||||||
|
|
||||||
image.composite(
|
|
||||||
cover,
|
|
||||||
dest_x,
|
|
||||||
dest_y,
|
|
||||||
dest_width,
|
|
||||||
dest_height,
|
|
||||||
dest_x,
|
|
||||||
dest_y,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
GdkPixbuf.InterpType.BILINEAR,
|
|
||||||
255,
|
|
||||||
)
|
|
||||||
|
|
||||||
save_cover(game.game_id, resize_cover(pixbuf=cover))
|
|
||||||
@@ -50,7 +50,7 @@ class Manager(ErrorProducer):
|
|||||||
return type(self).__name__
|
return type(self).__name__
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
def main(self, game: Game, additional_data: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Manager specific logic triggered by the run method
|
Manager specific logic triggered by the run method
|
||||||
* Implemented by final child classes
|
* Implemented by final child classes
|
||||||
@@ -59,7 +59,7 @@ class Manager(ErrorProducer):
|
|||||||
* May raise other exceptions that will be reported
|
* May raise other exceptions that will be reported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def execute_resilient_manager_logic(self, game: Game, additional_data: dict):
|
def run(self, game: Game, additional_data: dict):
|
||||||
"""Handle errors (retry, ignore or raise) that occur in the manager logic"""
|
"""Handle errors (retry, ignore or raise) that occur in the manager logic"""
|
||||||
|
|
||||||
# Keep track of the number of tries
|
# Keep track of the number of tries
|
||||||
@@ -106,7 +106,7 @@ class Manager(ErrorProducer):
|
|||||||
|
|
||||||
def try_manager_logic():
|
def try_manager_logic():
|
||||||
try:
|
try:
|
||||||
self.manager_logic(game, additional_data)
|
self.main(game, additional_data)
|
||||||
except Exception as error: # pylint: disable=broad-exception-caught
|
except Exception as error: # pylint: disable=broad-exception-caught
|
||||||
handle_error(error)
|
handle_error(error)
|
||||||
|
|
||||||
@@ -116,5 +116,5 @@ class Manager(ErrorProducer):
|
|||||||
self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]
|
self, game: Game, additional_data: dict, callback: Callable[["Manager"], Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Pass the game through the manager"""
|
"""Pass the game through the manager"""
|
||||||
self.execute_resilient_manager_logic(game, additional_data)
|
self.run(game, additional_data)
|
||||||
callback(self)
|
callback(self)
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
# online_cover_manager.py
|
|
||||||
#
|
|
||||||
# Copyright 2023 Geoffrey Coulaud
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from gi.repository import Gio, GdkPixbuf
|
|
||||||
from requests.exceptions import HTTPError, SSLError
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from src import shared
|
|
||||||
from src.game import Game
|
|
||||||
from src.store.managers.local_cover_manager import LocalCoverManager
|
|
||||||
from src.store.managers.manager import Manager
|
|
||||||
from src.utils.save_cover import resize_cover, save_cover
|
|
||||||
|
|
||||||
|
|
||||||
class OnlineCoverManager(Manager):
|
|
||||||
"""Manager that downloads game covers from URLs"""
|
|
||||||
|
|
||||||
run_after = (LocalCoverManager,)
|
|
||||||
retryable_on = (HTTPError, SSLError, ConnectionError)
|
|
||||||
|
|
||||||
def save_composited_cover(
|
|
||||||
self,
|
|
||||||
game: Game,
|
|
||||||
image_file: Gio.File,
|
|
||||||
original_width: int,
|
|
||||||
original_height: int,
|
|
||||||
target_width: int,
|
|
||||||
target_height: int,
|
|
||||||
) -> None:
|
|
||||||
"""Save the image composited with a background blur to fit the cover size"""
|
|
||||||
|
|
||||||
logging.debug(
|
|
||||||
"Compositing image for %s (%s) %dx%d -> %dx%d",
|
|
||||||
game.name,
|
|
||||||
game.game_id,
|
|
||||||
original_width,
|
|
||||||
original_height,
|
|
||||||
target_width,
|
|
||||||
target_height,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load game image
|
|
||||||
image = GdkPixbuf.Pixbuf.new_from_stream(image_file.read())
|
|
||||||
|
|
||||||
# Create background blur of the size of the cover
|
|
||||||
cover = image.scale_simple(2, 2, GdkPixbuf.InterpType.BILINEAR).scale_simple(
|
|
||||||
target_width, target_height, GdkPixbuf.InterpType.BILINEAR
|
|
||||||
)
|
|
||||||
|
|
||||||
# Center the image above the blurred background
|
|
||||||
scale = min(target_width / original_width, target_height / original_height)
|
|
||||||
left_padding = (target_width - original_width * scale) / 2
|
|
||||||
top_padding = (target_height - original_height * scale) / 2
|
|
||||||
image.composite(
|
|
||||||
cover,
|
|
||||||
# Top left of overwritten area on the destination
|
|
||||||
left_padding,
|
|
||||||
top_padding,
|
|
||||||
# Size of the overwritten area on the destination
|
|
||||||
original_width * scale,
|
|
||||||
original_height * scale,
|
|
||||||
# Offset
|
|
||||||
left_padding,
|
|
||||||
top_padding,
|
|
||||||
# Scale to apply to the resized image
|
|
||||||
scale,
|
|
||||||
scale,
|
|
||||||
# Compositing stuff
|
|
||||||
GdkPixbuf.InterpType.BILINEAR,
|
|
||||||
255,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resize and save the cover
|
|
||||||
save_cover(game.game_id, resize_cover(pixbuf=cover))
|
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
|
||||||
# Ensure that we have a cover to download
|
|
||||||
cover_url = additional_data.get("online_cover_url")
|
|
||||||
if not cover_url:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Download cover
|
|
||||||
image_file = Gio.File.new_tmp()[0]
|
|
||||||
image_path = Path(image_file.get_path())
|
|
||||||
with requests.get(cover_url, timeout=5) as cover:
|
|
||||||
cover.raise_for_status()
|
|
||||||
image_path.write_bytes(cover.content)
|
|
||||||
|
|
||||||
# Get image size
|
|
||||||
cover_width, cover_height = shared.image_size
|
|
||||||
with Image.open(image_path) as pil_image:
|
|
||||||
width, height = pil_image.size
|
|
||||||
|
|
||||||
# Composite if the image is shorter and the stretch amount is too high
|
|
||||||
aspect_ratio = width / height
|
|
||||||
target_aspect_ratio = cover_width / cover_height
|
|
||||||
is_taller = aspect_ratio < target_aspect_ratio
|
|
||||||
resized_height = height / width * cover_width
|
|
||||||
stretch = 1 - (resized_height / cover_height)
|
|
||||||
max_stretch = 0.12
|
|
||||||
if is_taller or stretch <= max_stretch:
|
|
||||||
save_cover(game.game_id, resize_cover(image_path))
|
|
||||||
else:
|
|
||||||
self.save_composited_cover(
|
|
||||||
game, image_file, width, height, cover_width, cover_height
|
|
||||||
)
|
|
||||||
@@ -24,19 +24,18 @@ from requests.exceptions import HTTPError, SSLError
|
|||||||
from src.errors.friendly_error import FriendlyError
|
from src.errors.friendly_error import FriendlyError
|
||||||
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.online_cover_manager import OnlineCoverManager
|
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||||
|
from src.store.managers.cover_manager import CoverManager
|
||||||
from src.utils.steamgriddb import SGDBAuthError, SGDBHelper
|
from src.utils.steamgriddb import SGDBAuthError, SGDBHelper
|
||||||
|
|
||||||
|
|
||||||
class SGDBManager(AsyncManager):
|
class SGDBManager(AsyncManager):
|
||||||
"""Manager in charge of downloading a game's cover from steamgriddb"""
|
"""Manager in charge of downloading a game's cover from steamgriddb"""
|
||||||
|
|
||||||
run_after = (SteamAPIManager, LocalCoverManager, OnlineCoverManager)
|
run_after = (SteamAPIManager, CoverManager)
|
||||||
retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError)
|
retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError)
|
||||||
|
|
||||||
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
def main(self, game: Game, _additional_data: dict) -> None:
|
||||||
try:
|
try:
|
||||||
sgdb = SGDBHelper()
|
sgdb = SGDBHelper()
|
||||||
sgdb.conditionaly_update_cover(game)
|
sgdb.conditionaly_update_cover(game)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from requests.exceptions import HTTPError, SSLError
|
from requests.exceptions import HTTPError, SSLError
|
||||||
|
from urllib3.exceptions import ConnectionError as Urllib3ConnectionError
|
||||||
|
|
||||||
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
|
||||||
@@ -32,7 +33,7 @@ from src.utils.steam import (
|
|||||||
class SteamAPIManager(AsyncManager):
|
class SteamAPIManager(AsyncManager):
|
||||||
"""Manager in charge of completing a game's data from the Steam API"""
|
"""Manager in charge of completing a game's data from the Steam API"""
|
||||||
|
|
||||||
retryable_on = (HTTPError, SSLError, ConnectionError)
|
retryable_on = (HTTPError, SSLError, Urllib3ConnectionError)
|
||||||
|
|
||||||
steam_api_helper: SteamAPIHelper = None
|
steam_api_helper: SteamAPIHelper = None
|
||||||
steam_rate_limiter: SteamRateLimiter = None
|
steam_rate_limiter: SteamRateLimiter = None
|
||||||
@@ -42,7 +43,7 @@ class SteamAPIManager(AsyncManager):
|
|||||||
self.steam_rate_limiter = SteamRateLimiter()
|
self.steam_rate_limiter = SteamRateLimiter()
|
||||||
self.steam_api_helper = SteamAPIHelper(self.steam_rate_limiter)
|
self.steam_api_helper = SteamAPIHelper(self.steam_rate_limiter)
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
def main(self, game: Game, additional_data: dict) -> None:
|
||||||
# Skip non-steam games
|
# Skip non-steam games
|
||||||
appid = additional_data.get("steam_appid", None)
|
appid = additional_data.get("steam_appid", None)
|
||||||
if appid is None:
|
if appid is None:
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class Store:
|
|||||||
# Connect signals
|
# Connect signals
|
||||||
for manager in self.managers.values():
|
for manager in self.managers.values():
|
||||||
for signal in manager.signals:
|
for signal in manager.signals:
|
||||||
game.connect(signal, manager.execute_resilient_manager_logic)
|
game.connect(signal, manager.run)
|
||||||
|
|
||||||
# Add the game to the store
|
# Add the game to the store
|
||||||
if not game.source in self.source_games:
|
if not game.source in self.source_games:
|
||||||
|
|||||||
146
src/utils/dolphin_cache_reader.py
Normal file
146
src/utils/dolphin_cache_reader.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""Reads the Dolphin game database, stored in a binary format"""
|
||||||
|
# Copyright 2022-2023 strycore - Lutris
|
||||||
|
# Copyright 2023 Rilic
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SUPPORTED_CACHE_VERSION = 24
|
||||||
|
|
||||||
|
|
||||||
|
def get_hex_string(string):
|
||||||
|
"""Return the hexadecimal representation of a string"""
|
||||||
|
return " ".join("{:02x}".format(c) for c in string)
|
||||||
|
|
||||||
|
|
||||||
|
def get_word_len(string):
|
||||||
|
"""Return the length of a string as specified in the Dolphin format"""
|
||||||
|
return int("0x" + "".join("{:02x}".format(c) for c in string[::-1]), 0)
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/dolphin-emu/dolphin/blob/90a994f93780ef8a7cccfc02e00576692e0f2839/Source/Core/UICommon/GameFile.h#L140
|
||||||
|
# https://github.com/dolphin-emu/dolphin/blob/90a994f93780ef8a7cccfc02e00576692e0f2839/Source/Core/UICommon/GameFile.cpp#L318
|
||||||
|
|
||||||
|
|
||||||
|
class DolphinCacheReader:
|
||||||
|
header_size = 20
|
||||||
|
structure = {
|
||||||
|
"valid": "b",
|
||||||
|
"file_path": "s",
|
||||||
|
"file_name": "s",
|
||||||
|
"file_size": 8,
|
||||||
|
"volume_size": 8,
|
||||||
|
"volume_size_type": 4,
|
||||||
|
"is_datel_disc": 1,
|
||||||
|
"is_nkit": 1,
|
||||||
|
"short_names": "a",
|
||||||
|
"long_names": "a",
|
||||||
|
"short_makers": "a",
|
||||||
|
"long_makers": "a",
|
||||||
|
"descriptions": "a",
|
||||||
|
"internal_name": "s",
|
||||||
|
"game_id": "s",
|
||||||
|
"gametdb_id": "s",
|
||||||
|
"title_id": 8,
|
||||||
|
"maker_id": "s",
|
||||||
|
"region": 4,
|
||||||
|
"country": 4,
|
||||||
|
"platform": 1,
|
||||||
|
"platform_": 3,
|
||||||
|
"blob_type": 4,
|
||||||
|
"block_size": 8,
|
||||||
|
"compression_method": "s",
|
||||||
|
"revision": 2,
|
||||||
|
"disc_number": 1,
|
||||||
|
"apploader_date": "s",
|
||||||
|
"custom_name": "s",
|
||||||
|
"custom_description": "s",
|
||||||
|
"custom_maker": "s",
|
||||||
|
"volume_banner": "i",
|
||||||
|
"custom_banner": "i",
|
||||||
|
"default_cover": "c",
|
||||||
|
"custom_cover": "c",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, cache_file: Path):
|
||||||
|
self.offset = 0
|
||||||
|
with open(cache_file, "rb") as dolphin_cache_file:
|
||||||
|
self.cache_content = dolphin_cache_file.read()
|
||||||
|
cache_version = get_word_len(self.cache_content[:4])
|
||||||
|
if cache_version != SUPPORTED_CACHE_VERSION:
|
||||||
|
logging.warning(
|
||||||
|
"Dolphin cache version expected %s but found %s",
|
||||||
|
SUPPORTED_CACHE_VERSION,
|
||||||
|
cache_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_game(self):
|
||||||
|
game = {}
|
||||||
|
for key, i in self.structure.items():
|
||||||
|
if i == "s":
|
||||||
|
game[key] = self.get_string()
|
||||||
|
elif i == "b":
|
||||||
|
game[key] = self.get_boolean()
|
||||||
|
elif i == "a":
|
||||||
|
game[key] = self.get_array()
|
||||||
|
elif i == "i":
|
||||||
|
game[key] = self.get_image()
|
||||||
|
elif i == "c":
|
||||||
|
game[key] = self.get_cover()
|
||||||
|
else:
|
||||||
|
game[key] = self.get_raw(i)
|
||||||
|
return game
|
||||||
|
|
||||||
|
def get_games(self):
|
||||||
|
self.offset += self.header_size
|
||||||
|
games = []
|
||||||
|
while self.offset < len(self.cache_content):
|
||||||
|
try:
|
||||||
|
games.append(self.get_game())
|
||||||
|
except Exception as ex:
|
||||||
|
logging.error("Failed to read Dolphin database: %s", ex)
|
||||||
|
return games
|
||||||
|
|
||||||
|
def get_boolean(self):
|
||||||
|
res = bool(get_word_len(self.cache_content[self.offset : self.offset + 1]))
|
||||||
|
self.offset += 1
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_array(self):
|
||||||
|
array_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||||
|
self.offset += 4
|
||||||
|
array = {}
|
||||||
|
for _i in range(array_len):
|
||||||
|
array_key = self.get_raw(4)
|
||||||
|
array[array_key] = self.get_string()
|
||||||
|
return array
|
||||||
|
|
||||||
|
def get_image(self):
|
||||||
|
data_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||||
|
self.offset += 4
|
||||||
|
res = self.cache_content[
|
||||||
|
self.offset : self.offset + data_len * 4
|
||||||
|
] # vector<u32>
|
||||||
|
self.offset += data_len * 4
|
||||||
|
width = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||||
|
self.offset += 4
|
||||||
|
height = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||||
|
self.offset += 4
|
||||||
|
return (width, height), res
|
||||||
|
|
||||||
|
def get_cover(self):
|
||||||
|
array_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||||
|
self.offset += 4
|
||||||
|
return self.get_raw(array_len)
|
||||||
|
|
||||||
|
def get_raw(self, word_len):
|
||||||
|
res = get_hex_string(self.cache_content[self.offset : self.offset + word_len])
|
||||||
|
self.offset += word_len
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_string(self):
|
||||||
|
word_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||||
|
self.offset += 4
|
||||||
|
string = self.cache_content[self.offset : self.offset + word_len]
|
||||||
|
self.offset += word_len
|
||||||
|
return string.decode("utf8")
|
||||||
@@ -103,11 +103,11 @@ class SGDBHelper:
|
|||||||
|
|
||||||
image_trunk = shared.covers_dir / game.game_id
|
image_trunk = shared.covers_dir / game.game_id
|
||||||
still = image_trunk.with_suffix(".tiff")
|
still = image_trunk.with_suffix(".tiff")
|
||||||
uri_kwargs = image_trunk.with_suffix(".gif")
|
animated = image_trunk.with_suffix(".gif")
|
||||||
prefer_sgdb = shared.schema.get_boolean("sgdb-prefer")
|
prefer_sgdb = shared.schema.get_boolean("sgdb-prefer")
|
||||||
|
|
||||||
# Do nothing if file present and not prefer SGDB
|
# Do nothing if file present and not prefer SGDB
|
||||||
if not prefer_sgdb and (still.is_file() or uri_kwargs.is_file()):
|
if not prefer_sgdb and (still.is_file() or animated.is_file()):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get ID for the game
|
# Get ID for the game
|
||||||
|
|||||||
Reference in New Issue
Block a user