Compare commits

...

4 Commits

Author SHA1 Message Date
Balló György
4331b0d8c0 Clear temporary files after conversion
convert_cover() now always return a temporary file, which needs to be
removed after the cover is saved. Without this, these files would remain on
the system until reboot. Also remove the downloaded temporary files after
save.
2024-11-02 20:07:38 +01:00
Balló György
117055bf64 Handle more error types when opening/saving images with PIL
Decoding/encoding errors sometimes raise an OSError or ValueError.
2024-11-02 18:24:11 +01:00
Balló György
fa86a25870 Make sure that image can be opened
Checking for file extension does not ensure that the image file can be
actually opened. Check if it can be loaded into a pixbuf, and convert it if
necessary.
2024-11-02 15:46:38 +01:00
Balló György
31c2a1dfee Save covers directly into covers dir
Instead of saving the pixbuf of the new cover into a temporary file and
then copy into covers dir, save it directly to there. Without this, a lot
of temporary files are created on import, which remain on the system even
after the application is closed.
2024-11-02 10:13:12 +01:00
5 changed files with 76 additions and 36 deletions

View File

@@ -68,7 +68,8 @@ class DetailsDialog(Adw.Dialog):
# Make it so only one dialog can be open at a time
self.__class__.is_open = True
self.connect("closed", lambda *_: self.set_is_open(False))
self.tmp_cover_path = None
self.connect("closed", self.on_closed)
self.game: Optional[Game] = game
self.game_cover: GameCover = GameCover({self.cover})
@@ -160,11 +161,20 @@ class DetailsDialog(Adw.Dialog):
self.set_focus(self.name)
def delete_pixbuf(self, *_args: Any) -> None:
if self.tmp_cover_path:
self.tmp_cover_path.unlink(missing_ok=True)
self.game_cover.new_cover()
self.cover_button_delete_revealer.set_reveal_child(False)
self.cover_changed = True
def on_closed(self, *args):
if self.tmp_cover_path:
self.tmp_cover_path.unlink(missing_ok=True)
self.set_is_open(False)
def apply_preferences(self, *_args: Any) -> None:
final_name = self.name.get_text()
final_developer = self.developer.get_text()
@@ -240,6 +250,7 @@ class DetailsDialog(Adw.Dialog):
save_cover(
self.game.game_id,
self.game_cover.path,
self.game_cover.pixbuf,
)
shared.store.add_game(self.game, {}, run_pipeline=False)
@@ -295,26 +306,29 @@ class DetailsDialog(Adw.Dialog):
return
def thread_func() -> None:
new_path = None
is_animated = False
try:
with Image.open(path) as image:
if getattr(image, "is_animated", False):
new_path = convert_cover(path)
except UnidentifiedImageError:
is_animated = True
except (UnidentifiedImageError, OSError, ValueError):
pass
if not new_path:
new_path = convert_cover(
if is_animated:
if self.tmp_cover_path:
self.tmp_cover_path.unlink(missing_ok=True)
self.tmp_cover_path = convert_cover(path)
self.game_cover.new_cover(self.tmp_cover_path)
else:
self.game_cover.new_cover(
pixbuf=shared.store.managers[CoverManager].composite_cover(
Path(path)
)
)
if new_path:
self.game_cover.new_cover(new_path)
self.cover_button_delete_revealer.set_reveal_child(True)
self.cover_changed = True
self.cover_button_delete_revealer.set_reveal_child(True)
self.cover_changed = True
self.toggle_loading()

View File

@@ -45,12 +45,22 @@ class GameCover:
self.pictures = pictures
self.new_cover(path)
def new_cover(self, path: Optional[Path] = None) -> None:
def new_cover(
self,
path: Optional[Path] = None,
pixbuf: Optional[GdkPixbuf.Pixbuf] = None
) -> None:
self.animation = None
self.texture = None
self.blurred = None
self.luminance = None
self.path = path
self.pixbuf = pixbuf
if pixbuf:
self.texture = Gdk.Texture.new_for_pixbuf(pixbuf)
self.set_texture(self.texture)
return
if path:
if path.suffix == ".gif":

View File

@@ -19,10 +19,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
from typing import NamedTuple
from typing import NamedTuple, Optional
import requests
from gi.repository import GdkPixbuf, Gio
from gi.repository import GdkPixbuf, Gio, GLib
from requests.exceptions import HTTPError, SSLError
from cartridges import shared
@@ -128,9 +128,21 @@ class CoverManager(Manager):
"""
# Load source image
source = GdkPixbuf.Pixbuf.new_from_file(
str(convert_cover(image_path, resize=False))
)
try:
source = GdkPixbuf.Pixbuf.new_from_file(
str(image_path)
)
except GLib.Error:
tmp_cover_path = convert_cover(image_path, resize=False)
if tmp_cover_path:
source = GdkPixbuf.Pixbuf.new_from_file(
str(tmp_cover_path)
)
tmp_cover_path.unlink(missing_ok=True)
else:
return None
source_size = ImageSize(source.get_width(), source.get_height())
cover_size = ImageSize._make(shared.image_size)
@@ -192,7 +204,8 @@ class CoverManager(Manager):
save_cover(
game.game_id,
convert_cover(
pixbuf=self.composite_cover(image_path, **composite_kwargs)
),
pixbuf=self.composite_cover(image_path, **composite_kwargs),
)
if key == "online_cover_url":
image_path.unlink(missing_ok=True)

View File

@@ -30,24 +30,11 @@ from cartridges import shared
def convert_cover(
cover_path: Optional[Path] = None,
pixbuf: Optional[GdkPixbuf.Pixbuf] = None,
resize: bool = True,
) -> Optional[Path]:
if not cover_path and not pixbuf:
return None
pixbuf_extensions = set()
for pixbuf_format in GdkPixbuf.Pixbuf.get_formats():
for pixbuf_extension in pixbuf_format.get_extensions():
pixbuf_extensions.add(pixbuf_extension)
if not resize and cover_path and cover_path.suffix.lower()[1:] in pixbuf_extensions:
return cover_path
if pixbuf:
cover_path = Path(Gio.File.new_tmp("XXXXXX.tiff")[0].get_path())
pixbuf.savev(str(cover_path), "tiff", ["compression"], ["1"])
try:
with Image.open(cover_path) as image:
if getattr(image, "is_animated", False):
@@ -76,7 +63,7 @@ def convert_cover(
if shared.schema.get_boolean("high-quality-images")
else shared.TIFF_COMPRESSION,
)
except UnidentifiedImageError:
except (UnidentifiedImageError, OSError, ValueError):
try:
Gdk.Texture.new_from_filename(str(cover_path)).save_to_tiff(
tmp_path := Gio.File.new_tmp("XXXXXX.tiff")[0].get_path()
@@ -88,7 +75,11 @@ def convert_cover(
return tmp_path
def save_cover(game_id: str, cover_path: Path) -> None:
def save_cover(
game_id: str,
cover_path: Optional[Path] = None,
pixbuf: Optional[GdkPixbuf.Pixbuf] = None,
) -> None:
shared.covers_dir.mkdir(parents=True, exist_ok=True)
animated_path = shared.covers_dir / f"{game_id}.gif"
@@ -98,7 +89,15 @@ def save_cover(game_id: str, cover_path: Path) -> None:
animated_path.unlink(missing_ok=True)
static_path.unlink(missing_ok=True)
if not cover_path:
if not cover_path and not pixbuf:
return
if pixbuf:
pixbuf.savev(str(static_path), "tiff", ["compression"], ["1"])
if game_id in shared.win.game_covers:
shared.win.game_covers[game_id].new_cover(static_path)
return
copyfile(

View File

@@ -134,7 +134,11 @@ class SgdbHelper:
tmp_file = Gio.File.new_tmp()[0]
tmp_file_path = tmp_file.get_path()
Path(tmp_file_path).write_bytes(response.content)
save_cover(game.game_id, convert_cover(tmp_file_path))
tmp_cover_path = convert_cover(tmp_file_path)
if tmp_cover_path:
save_cover(game.game_id, tmp_cover_path)
tmp_cover_path.unlink(missing_ok=True)
tmp_file_path.unlink(missing_ok=True)
except SgdbAuthError as error:
# Let caller handle auth errors
raise error