🎨 Made OnlineCoverManager more general
- Does compositing of image with a blurred background - Stretches the original image if it's not too much - Handles images that are too wide and images that are too tall - Removed ItchCoverManager
This commit is contained in:
@@ -50,7 +50,7 @@ class ItchSourceIterator(SourceIterator):
|
|||||||
"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]),
|
||||||
}
|
}
|
||||||
additional_data = {"itch_cover_url": row[2], "itch_still_cover_url": row[3]}
|
additional_data = {"online_cover_url": row[3] or row[2]}
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values, allow_side_effects=False)
|
||||||
yield (game, additional_data)
|
yield (game, additional_data)
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ from src.importer.sources.itch_source import ItchSource
|
|||||||
from src.importer.sources.legendary_source import LegendarySource
|
from src.importer.sources.legendary_source import LegendarySource
|
||||||
from src.importer.sources.lutris_source import LutrisSource
|
from src.importer.sources.lutris_source import LutrisSource
|
||||||
from src.importer.sources.steam_source import SteamSource
|
from src.importer.sources.steam_source import SteamSource
|
||||||
from src.logging.setup import setup_logging, log_system_info
|
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.itch_cover_manager import ItchCoverManager
|
|
||||||
from src.store.managers.local_cover_manager import LocalCoverManager
|
from src.store.managers.local_cover_manager import LocalCoverManager
|
||||||
|
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
|
||||||
@@ -89,7 +89,7 @@ class CartridgesApplication(Adw.Application):
|
|||||||
# 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(LocalCoverManager())
|
||||||
shared.store.add_manager(SteamAPIManager())
|
shared.store.add_manager(SteamAPIManager())
|
||||||
shared.store.add_manager(ItchCoverManager())
|
shared.store.add_manager(OnlineCoverManager())
|
||||||
shared.store.add_manager(SGDBManager())
|
shared.store.add_manager(SGDBManager())
|
||||||
shared.store.add_manager(FileManager())
|
shared.store.add_manager(FileManager())
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from gi.repository import GdkPixbuf, Gio
|
|
||||||
from requests.exceptions import HTTPError, SSLError
|
|
||||||
|
|
||||||
from src import shared # pylint: disable=no-name-in-module
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Remove by generalizing OnlineCoverManager
|
|
||||||
|
|
||||||
|
|
||||||
class ItchCoverManager(Manager):
|
|
||||||
"""Manager in charge of downloading the game's cover from itch.io"""
|
|
||||||
|
|
||||||
run_after = (LocalCoverManager,)
|
|
||||||
retryable_on = (HTTPError, SSLError)
|
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Create background blur
|
|
||||||
game_cover = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
|
|
||||||
tmp_file.read(), 2, 2, False
|
|
||||||
).scale_simple(*shared.image_size, GdkPixbuf.InterpType.BILINEAR)
|
|
||||||
|
|
||||||
# Resize square image
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Composite
|
|
||||||
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))
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from gi.repository import Gio
|
from gi.repository import Gio, GdkPixbuf
|
||||||
from requests.exceptions import HTTPError, SSLError
|
from requests.exceptions import HTTPError, SSLError
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.managers.local_cover_manager import LocalCoverManager
|
from src.store.managers.local_cover_manager import LocalCoverManager
|
||||||
from src.store.managers.manager import Manager
|
from src.store.managers.manager import Manager
|
||||||
@@ -16,15 +19,89 @@ class OnlineCoverManager(Manager):
|
|||||||
run_after = (LocalCoverManager,)
|
run_after = (LocalCoverManager,)
|
||||||
retryable_on = (HTTPError, SSLError)
|
retryable_on = (HTTPError, SSLError)
|
||||||
|
|
||||||
|
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:
|
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
||||||
# Ensure that we have a cover to download
|
# Ensure that we have a cover to download
|
||||||
cover_url = additional_data.get("online_cover_url", None)
|
cover_url = additional_data.get("online_cover_url")
|
||||||
if not cover_url:
|
if not cover_url:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Download cover
|
# Download cover
|
||||||
tmp_file = Gio.File.new_tmp()[0]
|
image_file = Gio.File.new_tmp()[0]
|
||||||
|
image_path = Path(image_file.get_path())
|
||||||
with requests.get(cover_url, timeout=5) as cover:
|
with requests.get(cover_url, timeout=5) as cover:
|
||||||
cover.raise_for_status()
|
cover.raise_for_status()
|
||||||
Path(tmp_file.get_path()).write_bytes(cover.content)
|
image_path.write_bytes(cover.content)
|
||||||
# Resize and save
|
|
||||||
save_cover(game.game_id, resize_cover(tmp_file.get_path()))
|
# Get image size
|
||||||
|
cover_width, cover_height = shared.image_size
|
||||||
|
with Image.open(image_path) as pil_image:
|
||||||
|
width, height = pil_image.size
|
||||||
|
|
||||||
|
# Composite the image if its aspect ratio differs too much
|
||||||
|
# (allow the side that is smaller to be stretched by a small percentage)
|
||||||
|
max_diff_proportion = 0.12
|
||||||
|
scale = min(cover_width / width, cover_height / height)
|
||||||
|
width_diff = (cover_width - (width * scale)) / cover_width
|
||||||
|
height_diff = (cover_height - (height * scale)) / cover_height
|
||||||
|
diff = width_diff + height_diff
|
||||||
|
if diff < max_diff_proportion:
|
||||||
|
save_cover(game.game_id, resize_cover(image_path))
|
||||||
|
else:
|
||||||
|
self.save_composited_cover(
|
||||||
|
game, image_file, width, height, cover_width, cover_height
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from requests.exceptions import HTTPError, SSLError
|
|||||||
|
|
||||||
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.itch_cover_manager import ItchCoverManager
|
from src.store.managers.online_cover_manager import OnlineCoverManager
|
||||||
from src.store.managers.local_cover_manager import LocalCoverManager
|
from src.store.managers.local_cover_manager import LocalCoverManager
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||||
from src.utils.steamgriddb import SGDBAuthError, SGDBHelper
|
from src.utils.steamgriddb import SGDBAuthError, SGDBHelper
|
||||||
@@ -13,7 +13,7 @@ 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, ItchCoverManager)
|
run_after = (SteamAPIManager, LocalCoverManager, OnlineCoverManager)
|
||||||
retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError)
|
retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError)
|
||||||
|
|
||||||
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user