Replace the single file pattern for service-media with a list

It's a list of one element except for Lutris banners and cover-art; these now support both .jpg and .png, where the user can assign either file type without transcoding.

This, in turn, allow transparency to be used in banners and cover-art via PNG files.

We still transcode any other file types into JPEG files here.
This commit is contained in:
Daniel Johnson 2023-12-31 05:43:00 -05:00 committed by Mathieu Comandon
parent 08466f2258
commit 236f0c9999
18 changed files with 58 additions and 58 deletions

View file

@ -17,7 +17,7 @@ from lutris.gui.dialogs.delegates import DialogInstallUIDelegate
from lutris.gui.widgets.common import Label, NumberEntry, SlugEntry
from lutris.gui.widgets.notifications import send_notification
from lutris.gui.widgets.scaled_image import ScaledImage
from lutris.gui.widgets.utils import MEDIA_CACHE_INVALIDATED, get_image_file_format
from lutris.gui.widgets.utils import MEDIA_CACHE_INVALIDATED, get_image_file_extension
from lutris.runners import import_runner
from lutris.services.lutris import LutrisBanner, LutrisCoverart, LutrisIcon, download_lutris_media
from lutris.util.jobs import AsyncCall
@ -713,13 +713,23 @@ class GameDialogCommon(SavableModelessDialog, DialogInstallUIDelegate):
slug = self.slug or self.game.slug
service_media = self.service_medias[image_type]
self.game.custom_images.add(image_type)
dest_path = service_media.get_media_path(slug)
file_format = service_media.file_format
dest_paths = service_media.get_possible_media_paths(slug)
if image_path != dest_path:
if file_format == get_image_file_format(image_path):
if image_path not in dest_paths:
ext = get_image_file_extension(image_path)
dest_path = None
for candidate in dest_paths:
if os.path.isfile(candidate):
os.remove(candidate)
if candidate.casefold().endswith(ext):
dest_path = candidate
if dest_path:
shutil.copy(image_path, dest_path, follow_symlinks=True)
else:
dest_path = dest_paths[0]
file_format = {".jpg": "jpeg", ".png": "png"}[get_image_file_extension(dest_paths[0])]
# If we must transcode the image, we'll scale the image up based on
# the UI scale factor, to try to avoid blurriness. Of course this won't
# work if the user changes the scaling later, but what can you do.

View file

@ -38,21 +38,21 @@ def open_uri(uri):
system.spawn(["xdg-open", uri])
def get_image_file_format(path):
"""Returns the file format fo an image, either 'jpeg' or 'png';
we deduce this from the file extension, or if that fails the
def get_image_file_extension(path):
"""Returns the canonical file extension for an image,
either 'jpg' or 'png'; we deduce this from the file extension, or if that fails the
file's 'magic' prefix bytes."""
ext = os.path.splitext(path)[1].lower()
ext = os.path.splitext(path)[1].casefold()
if ext in [".jpg", ".jpeg"]:
return "jpeg"
return ".jpg"
if path == ".png":
return "png"
return ".png"
file_type = magic.from_file(path).lower()
file_type = magic.from_file(path).casefold()
if "jpeg image data" in file_type:
return "jpeg"
return ".jpg"
if "png image data" in file_type:
return "png"
return ".png"
return None

View file

@ -34,8 +34,7 @@ class AmazonBanner(ServiceMedia):
service = "amazon"
size = (200, 112)
dest_path = os.path.join(settings.CACHE_DIR, "amazon/banners")
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
api_field = "image"
url_pattern = "%s"

View file

@ -34,16 +34,14 @@ class LutrisBanner(ServiceMedia):
service = 'lutris'
size = BANNER_SIZE
dest_path = settings.BANNER_PATH
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg", "%s.png"]
api_field = 'banner'
class LutrisIcon(LutrisBanner):
size = ICON_SIZE
dest_path = settings.ICON_PATH
file_pattern = "lutris_%s.png"
file_format = "png"
file_patterns = ["lutris_%s.png"]
api_field = 'icon'
@property
@ -57,8 +55,7 @@ class LutrisIcon(LutrisBanner):
class LutrisCoverart(ServiceMedia):
service = 'lutris'
size = (264, 352)
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg", "%s.png"]
dest_path = settings.COVERART_PATH
api_field = 'coverart'

View file

@ -57,8 +57,7 @@ GAME_IDS = {
class BattleNetCover(ServiceMedia):
service = 'battlenet'
size = (176, 234)
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
dest_path = os.path.join(settings.CACHE_DIR, "battlenet/coverart")
api_field = 'coverart'

View file

@ -19,8 +19,7 @@ class DolphinBanner(ServiceMedia):
service = "dolphin"
source = "local"
size = (96, 32)
file_pattern = "%s.png"
file_format = "jpeg"
file_patterns = ["%s.png"]
dest_path = os.path.join(settings.CACHE_DIR, "dolphin/banners/small")

View file

@ -56,8 +56,7 @@ class EAAppGames:
class EAAppArtSmall(ServiceMedia):
service = "ea_app"
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
size = (63, 89)
dest_path = os.path.join(settings.CACHE_DIR, "ea_app/pack-art-small")
api_field = "packArtSmall"

View file

@ -33,8 +33,7 @@ BOX_ART_SIZE = (200, 267)
class DieselGameMedia(ServiceMedia):
service = "egs"
remote_size = (200, 267)
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
min_logo_x = 300
min_logo_y = 150
@ -111,8 +110,7 @@ class DieselGameBoxLogo(DieselGameMedia):
"""EGS game box"""
size = (200, 100)
remote_size = size
file_pattern = "%s.png"
file_format = "png"
file_patterns = ["%s.png"]
visible = False
dest_path = os.path.join(settings.CACHE_DIR, "egs/game_logo")
api_field = "DieselGameBoxLogo"

View file

@ -22,8 +22,7 @@ class FlathubBanner(ServiceMedia):
service = "flathub"
size = (128, 128)
dest_path = os.path.join(settings.CACHE_DIR, "flathub/banners")
file_pattern = "%s.png"
file_format = "png"
file_patterns = ["%s.png"]
url_field = 'iconDesktopUrl'
def get_media_url(self, details):

View file

@ -28,8 +28,7 @@ class GogSmallBanner(ServiceMedia):
service = "gog"
size = (100, 60)
dest_path = os.path.join(settings.CACHE_DIR, "gog/banners/small")
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
api_field = "image"
url_pattern = "https:%s_prof_game_100x60.jpg"

View file

@ -24,8 +24,7 @@ class HumbleBundleIcon(ServiceMedia):
service = "humblebundle"
size = (70, 70)
dest_path = os.path.join(settings.CACHE_DIR, "humblebundle/icons")
file_pattern = "%s.png"
file_format = "png"
file_patterns = ["%s.png"]
api_field = "icon"

View file

@ -26,8 +26,7 @@ class ItchIoCover(ServiceMedia):
service = "itchio"
size = (315, 250)
dest_path = os.path.join(settings.CACHE_DIR, "itchio/cover")
file_pattern = "%s.png"
file_format = "png"
file_patterns = ["%s.png"]
def get_media_url(self, details):
"""Extract cover from API"""

View file

@ -50,8 +50,7 @@ class OriginLauncher:
class OriginPackArtSmall(ServiceMedia):
service = "origin"
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
size = (63, 89)
dest_path = os.path.join(settings.CACHE_DIR, "origin/pack-art-small")
api_field = "packArtSmall"

View file

@ -19,8 +19,7 @@ class ScummvmBanner(ServiceMedia):
service = "scummvm"
source = "local"
size = (96, 32)
file_pattern = "%s.png"
file_format = "jpeg"
file_patterns = ["%s.png"]
dest_path = settings.CACHE_DIR

View file

@ -2,6 +2,7 @@ import json
import os
import random
import time
from typing import List
from lutris.database.services import ServiceGameCollection
from lutris.util import system
@ -18,8 +19,7 @@ class ServiceMedia:
visible = True # This media should be displayed as an option in the UI
small_size = None
dest_path = None
file_pattern = NotImplemented
file_format = NotImplemented
file_patterns = NotImplemented
api_field = NotImplemented
url_pattern = "%s"
@ -28,11 +28,21 @@ class ServiceMedia:
os.makedirs(self.dest_path)
def get_filename(self, slug):
return self.file_pattern % slug
return self.file_patterns[0] % slug
def get_media_path(self, slug):
"""Return the absolute path of a local media file"""
return os.path.join(self.dest_path, self.get_filename(slug))
candidates = self.get_possible_media_paths(slug)
if len(candidates) > 1:
for path in candidates:
if os.path.isfile(path):
return path
return candidates[0]
def get_possible_media_paths(self, slug: str) -> List[str]:
"""Return the absolute path of a local media file"""
return [os.path.join(self.dest_path, pattern % slug)
for pattern in self.file_patterns]
def exists(self, slug):
"""Whether the icon for the specified slug exists locally"""

View file

@ -25,8 +25,7 @@ class SteamBanner(ServiceMedia):
service = "steam"
size = (184, 69)
dest_path = os.path.join(settings.CACHE_DIR, "steam/banners")
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
api_field = "appid"
url_pattern = "http://cdn.akamai.steamstatic.com/steam/apps/%s/capsule_184x69.jpg"
@ -35,8 +34,7 @@ class SteamCover(ServiceMedia):
service = "steam"
size = (200, 300)
dest_path = os.path.join(settings.CACHE_DIR, "steam/covers")
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
api_field = "appid"
url_pattern = "http://cdn.steamstatic.com/steam/apps/%s/library_600x900.jpg"
@ -45,8 +43,7 @@ class SteamBannerLarge(ServiceMedia):
service = "steam"
size = (460, 215)
dest_path = os.path.join(settings.CACHE_DIR, "steam/header")
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
api_field = "appid"
url_pattern = "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg"

View file

@ -30,8 +30,7 @@ class UbisoftCover(ServiceMedia):
service = "ubisoft"
size = (160, 186)
dest_path = os.path.join(settings.CACHE_DIR, "ubisoft/covers")
file_pattern = "%s.jpg"
file_format = "jpeg"
file_patterns = ["%s.jpg"]
api_field = "id"
url_pattern = "https://ubiservices.cdn.ubi.com/%s/spaceCardAsset/boxArt_mobile.jpg?imwidth=320"

View file

@ -35,8 +35,7 @@ class XDGMedia(ServiceMedia):
source = "local"
size = (64, 64)
dest_path = os.path.join(settings.CACHE_DIR, "xdg/icons")
file_pattern = "%s.png"
file_format = "png"
file_patterns = ["%s.png"]
class XDGService(BaseService):