From c96b64f72ed17fa39157289d5af0ff616e34388b Mon Sep 17 00:00:00 2001 From: Rilic Date: Sat, 15 Jul 2023 14:28:04 +0100 Subject: [PATCH 01/59] Implement Retroarch Importer I'm quite rusty with Python, so please let me know if there are any (or many) grave mistakes! --- data/gtk/preferences.blp | 14 +++ data/hu.kramo.Cartridges.gschema.xml.in | 8 +- flatpak/hu.kramo.Cartridges.Devel.json | 1 + src/importer/sources/retroarch_source.py | 119 +++++++++++++++++++++++ src/main.py | 4 + src/preferences.py | 6 ++ 6 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/importer/sources/retroarch_source.py diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index fbc2dee..8547e44 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -226,6 +226,20 @@ template $PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ExpanderRow retroarch_expander_row { + title: _("Retroarch"); + show-enable-switch: true; + + Adw.ActionRow retroarch_config_action_row { + title: _("Install Location"); + + Button retroarch_config_file_chooser_button { + icon-name: "folder-symbolic"; + valign: center; + } + } + } + Adw.ExpanderRow flatpak_expander_row { title: _("Flatpak"); show-enable-switch: true; diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index 4fffaad..9086a2c 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -64,6 +64,12 @@ "~/.config/legendary/" + + true + + + "~/.var/app/org.libretro.RetroArch/config/retroarch/" + true @@ -110,4 +116,4 @@ "[]" - \ No newline at end of file + diff --git a/flatpak/hu.kramo.Cartridges.Devel.json b/flatpak/hu.kramo.Cartridges.Devel.json index 1c2ecea..e74107f 100644 --- a/flatpak/hu.kramo.Cartridges.Devel.json +++ b/flatpak/hu.kramo.Cartridges.Devel.json @@ -17,6 +17,7 @@ "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro", "--filesystem=~/.var/app/io.itch.itch/config/itch/:ro", + "--filesystem=~/.var/app/org.libretro.RetroArch/config/retroarch/:ro", "--filesystem=/var/lib/flatpak:ro" ], "cleanup" : [ diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py new file mode 100644 index 0000000..b11cdcd --- /dev/null +++ b/src/importer/sources/retroarch_source.py @@ -0,0 +1,119 @@ +# retroarch_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 . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from pathlib import Path +from time import time + +import json +import os +import logging +from json import JSONDecodeError + +from src import shared +from src.game import Game +from src.importer.sources.location import Location +from src.importer.sources.source import ( + SourceIterationResult, + SourceIterator, + Source +) + + +class RetroarchSourceIterator(SourceIterator): + source: "RetroarchSource" + + def generator_builder(self) -> SourceIterationResult: + playlist_files = [] + for file in os.listdir(self.source.config_location["playlists"]): + if file.endswith('.lpl'): + playlist_files.append(file) + + playlist_items = [] + for playlist_file in playlist_files: + open_file = open(str(self.source.config_location["playlists"]) + "/" + playlist_file) + try: + playlist_json = json.load(open_file) + except (JSONDecodeError, OSError, KeyError): + logging.warning("Cannot read playlist file: %s", str(playlist_file)) + continue + + for item in playlist_json["items"]: + # Select the core. Try the content's core first, then the playlist's + # default core. + core_path = item["core_path"] + if core_path == "DETECT": + default_core = playlist_json["default_core_path"] + if default_core: + core_path = default_core + else: + logging.warning("Cannot find core for: %s", str(item["path"])) + continue + + # Build game + game_title = item["label"].split("(", 1)[0] + values = { + "source": self.source.id, + "added": int(time()), + "name": game_title, + "game_id": self.source.game_id_format.format(game_id=item["crc32"][:8]), + "executable": self.source.executable_format.format( + rom_path = item["path"], + core_path = core_path, + ) + } + + game = Game(values) + additional_data = {} + + # Get boxart + boxart_image_name = item["label"].split(".", 1)[0] + ".png" + boxart_folder_name = playlist_file.split(".", 1)[0] + image_path = self.source.config_location["thumbnails"] / boxart_folder_name / "Named_Boxarts" / boxart_image_name + additional_data = {"local_image_path": image_path} + + yield(game, additional_data) + + +class RetroarchSource(Source): + args = ' -L "{core_path}" "{rom_path}"' + + name = "Retroarch" + available_on = {"linux"} + iterator_class = RetroarchSourceIterator + executable_format = 'retroarch' + args + + config_location = Location( + schema_key="retroarch-location", + candidates=( + shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", + shared.config_dir / "retroarch", + shared.home / ".config" / "retroarch", + ), + paths={ + "playlists": (True, "playlists"), + "thumbnails": (True, "thumbnails"), + }, + ) + + # Check if installation is flatpak'd. + # TODO: There's probably a MUCH better way of doing this. + # There's *is* a URI format, but it doesn't seem to work on the flatpak + # version of Retroarch. https://github.com/libretro/RetroArch/pull/13563 + if str(shared.flatpak_dir) in str(config_location["playlists"]): + executable_format = 'flatpak run org.libretro.RetroArch' + args diff --git a/src/main.py b/src/main.py index d0afd3e..ff6ac83 100644 --- a/src/main.py +++ b/src/main.py @@ -33,6 +33,7 @@ from src import shared from src.details_window import DetailsWindow from src.game import Game from src.importer.importer import Importer +from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.bottles_source import BottlesSource from src.importer.sources.flatpak_source import FlatpakSource from src.importer.sources.heroic_source import HeroicSource @@ -230,6 +231,9 @@ class CartridgesApplication(Adw.Application): if shared.schema.get_boolean("legendary"): importer.add_source(LegendarySource()) + if shared.schema.get_boolean("retroarch"): + importer.add_source(RetroarchSource()) + importer.run() def on_remove_game_action(self, *_args): diff --git a/src/preferences.py b/src/preferences.py index f090b4a..15bfbed 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -30,6 +30,7 @@ from src.importer.sources.flatpak_source import FlatpakSource from src.importer.sources.heroic_source import HeroicSource from src.importer.sources.itch_source import ItchSource from src.importer.sources.legendary_source import LegendarySource +from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.location import UnresolvableLocationError from src.importer.sources.lutris_source import LutrisSource from src.importer.sources.source import Source @@ -82,6 +83,9 @@ class PreferencesWindow(Adw.PreferencesWindow): legendary_config_action_row = Gtk.Template.Child() legendary_config_file_chooser_button = Gtk.Template.Child() + retroarch_expander_row = Gtk.Template.Child() + retroarch_config_file_chooser_button = Gtk.Template.Child() + flatpak_expander_row = Gtk.Template.Child() flatpak_data_action_row = Gtk.Template.Child() flatpak_data_file_chooser_button = Gtk.Template.Child() @@ -138,6 +142,7 @@ class PreferencesWindow(Adw.PreferencesWindow): ItchSource, LegendarySource, LutrisSource, + RetroarchSource, SteamSource, ): source = source_class() @@ -385,3 +390,4 @@ class PreferencesWindow(Adw.PreferencesWindow): # Set the source row subtitles self.resolve_locations(source) self.update_source_action_row_paths(source) + From 9ccb315a2d32689bca9852db74b620ec90640691 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 15:50:28 +0100 Subject: [PATCH 02/59] Make Retroarch executable_format a property --- src/importer/sources/retroarch_source.py | 57 +++++++++++++----------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index b11cdcd..872d985 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -17,22 +17,18 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import json +import logging +import os +from json import JSONDecodeError from pathlib import Path from time import time -import json -import os -import logging -from json import JSONDecodeError - from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - Source -) +from src.importer.sources.source import (Source, SourceIterationResult, + SourceIterator) class RetroarchSourceIterator(SourceIterator): @@ -41,12 +37,14 @@ class RetroarchSourceIterator(SourceIterator): def generator_builder(self) -> SourceIterationResult: playlist_files = [] for file in os.listdir(self.source.config_location["playlists"]): - if file.endswith('.lpl'): + if file.endswith(".lpl"): playlist_files.append(file) playlist_items = [] for playlist_file in playlist_files: - open_file = open(str(self.source.config_location["playlists"]) + "/" + playlist_file) + open_file = open( + str(self.source.config_location["playlists"]) + "/" + playlist_file + ) try: playlist_json = json.load(open_file) except (JSONDecodeError, OSError, KeyError): @@ -71,11 +69,13 @@ class RetroarchSourceIterator(SourceIterator): "source": self.source.id, "added": int(time()), "name": game_title, - "game_id": self.source.game_id_format.format(game_id=item["crc32"][:8]), + "game_id": self.source.game_id_format.format( + game_id=item["crc32"][:8] + ), "executable": self.source.executable_format.format( - rom_path = item["path"], - core_path = core_path, - ) + rom_path=item["path"], + core_path=core_path, + ), } game = Game(values) @@ -84,10 +84,15 @@ class RetroarchSourceIterator(SourceIterator): # Get boxart boxart_image_name = item["label"].split(".", 1)[0] + ".png" boxart_folder_name = playlist_file.split(".", 1)[0] - image_path = self.source.config_location["thumbnails"] / boxart_folder_name / "Named_Boxarts" / boxart_image_name + image_path = ( + self.source.config_location["thumbnails"] + / boxart_folder_name + / "Named_Boxarts" + / boxart_image_name + ) additional_data = {"local_image_path": image_path} - yield(game, additional_data) + yield (game, additional_data) class RetroarchSource(Source): @@ -96,7 +101,6 @@ class RetroarchSource(Source): name = "Retroarch" available_on = {"linux"} iterator_class = RetroarchSourceIterator - executable_format = 'retroarch' + args config_location = Location( schema_key="retroarch-location", @@ -110,10 +114,11 @@ class RetroarchSource(Source): "thumbnails": (True, "thumbnails"), }, ) - - # Check if installation is flatpak'd. - # TODO: There's probably a MUCH better way of doing this. - # There's *is* a URI format, but it doesn't seem to work on the flatpak - # version of Retroarch. https://github.com/libretro/RetroArch/pull/13563 - if str(shared.flatpak_dir) in str(config_location["playlists"]): - executable_format = 'flatpak run org.libretro.RetroArch' + args + + @property + def executable_format(self): + self.config_location.resolve() + is_flatpak = self.config_location.root.is_relative_to(shared.flatpak_dir) + base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" + args = '-L "{core_path}" "{rom_path}"' + return f"{base} {args}" From 86a34f15964ea01e33b1462242009cec4f03da69 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 15:58:50 +0100 Subject: [PATCH 03/59] Improve playlist file opening check --- src/importer/sources/retroarch_source.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 872d985..e9a460e 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -27,8 +27,7 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import (Source, SourceIterationResult, - SourceIterator) +from src.importer.sources.source import Source, SourceIterationResult, SourceIterator class RetroarchSourceIterator(SourceIterator): @@ -40,13 +39,13 @@ class RetroarchSourceIterator(SourceIterator): if file.endswith(".lpl"): playlist_files.append(file) - playlist_items = [] for playlist_file in playlist_files: - open_file = open( - str(self.source.config_location["playlists"]) + "/" + playlist_file - ) try: - playlist_json = json.load(open_file) + with open( + self.source.config_location["playlists"] / playlist_file, + encoding="utf-8", + ) as open_file: + playlist_json = json.load(open_file) except (JSONDecodeError, OSError, KeyError): logging.warning("Cannot read playlist file: %s", str(playlist_file)) continue @@ -114,11 +113,11 @@ class RetroarchSource(Source): "thumbnails": (True, "thumbnails"), }, ) - + @property def executable_format(self): self.config_location.resolve() is_flatpak = self.config_location.root.is_relative_to(shared.flatpak_dir) base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" args = '-L "{core_path}" "{rom_path}"' - return f"{base} {args}" + return f"{base} {args}" From 45884d5c11928e858b9ea783ac360ba027565d0e Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 16:05:35 +0100 Subject: [PATCH 04/59] Add missing Retroarch action row --- src/preferences.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/preferences.py b/src/preferences.py index 15bfbed..9a3a002 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -30,9 +30,9 @@ from src.importer.sources.flatpak_source import FlatpakSource from src.importer.sources.heroic_source import HeroicSource from src.importer.sources.itch_source import ItchSource from src.importer.sources.legendary_source import LegendarySource -from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.location import UnresolvableLocationError from src.importer.sources.lutris_source import LutrisSource +from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.source import Source from src.importer.sources.steam_source import SteamSource from src.utils.create_dialog import create_dialog @@ -84,6 +84,7 @@ class PreferencesWindow(Adw.PreferencesWindow): legendary_config_file_chooser_button = Gtk.Template.Child() retroarch_expander_row = Gtk.Template.Child() + retroarch_config_action_row = Gtk.Template.Child() retroarch_config_file_chooser_button = Gtk.Template.Child() flatpak_expander_row = Gtk.Template.Child() @@ -390,4 +391,3 @@ class PreferencesWindow(Adw.PreferencesWindow): # Set the source row subtitles self.resolve_locations(source) self.update_source_action_row_paths(source) - From f9cfc311fa8adfeeee6d2f464c1ae59f4c669b4e Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 16:22:28 +0100 Subject: [PATCH 05/59] Simplify playlist file location code --- src/importer/sources/retroarch_source.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index e9a460e..ec78a52 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -34,10 +34,7 @@ class RetroarchSourceIterator(SourceIterator): source: "RetroarchSource" def generator_builder(self) -> SourceIterationResult: - playlist_files = [] - for file in os.listdir(self.source.config_location["playlists"]): - if file.endswith(".lpl"): - playlist_files.append(file) + playlist_files = self.source.config_location["playlists"].glob("*.lpl") for playlist_file in playlist_files: try: @@ -82,7 +79,7 @@ class RetroarchSourceIterator(SourceIterator): # Get boxart boxart_image_name = item["label"].split(".", 1)[0] + ".png" - boxart_folder_name = playlist_file.split(".", 1)[0] + boxart_folder_name = playlist_file.name.split(".", 1)[0] image_path = ( self.source.config_location["thumbnails"] / boxart_folder_name From 5a89f8a542b57945ba18c52b79f629b35dca8563 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 16:33:58 +0100 Subject: [PATCH 06/59] Improve core selection logic --- src/importer/sources/retroarch_source.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index ec78a52..189a1e8 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -34,6 +34,7 @@ class RetroarchSourceIterator(SourceIterator): source: "RetroarchSource" def generator_builder(self) -> SourceIterationResult: + # Get all playlist files, ending in .lpl playlist_files = self.source.config_location["playlists"].glob("*.lpl") for playlist_file in playlist_files: @@ -48,13 +49,15 @@ class RetroarchSourceIterator(SourceIterator): continue for item in playlist_json["items"]: - # Select the core. Try the content's core first, then the playlist's - # default core. - core_path = item["core_path"] - if core_path == "DETECT": - default_core = playlist_json["default_core_path"] - if default_core: - core_path = default_core + # Select the core. + # Try the content's core first, then the playlist's default core. + # If none can be used, warn the user and continue. + for core_path in ( + item["core_path"], + playlist_json["default_core_path"], + ): + if core_path != "DETECT": + break else: logging.warning("Cannot find core for: %s", str(item["path"])) continue From 9ffbcc73ae6767ab30c5db767d0231316d9163b0 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 16:47:16 +0100 Subject: [PATCH 07/59] De-indent else in core selection loop --- src/importer/sources/retroarch_source.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 189a1e8..11da224 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -58,9 +58,9 @@ class RetroarchSourceIterator(SourceIterator): ): if core_path != "DETECT": break - else: - logging.warning("Cannot find core for: %s", str(item["path"])) - continue + else: + logging.warning("Cannot find core for: %s", str(item["path"])) + continue # Build game game_title = item["label"].split("(", 1)[0] From 06de79ad54e0fdb196f4861a50aa1831733cafd8 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 16:57:11 +0100 Subject: [PATCH 08/59] Use native game label instead of truncating --- src/importer/sources/retroarch_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 11da224..e3992f6 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -63,7 +63,7 @@ class RetroarchSourceIterator(SourceIterator): continue # Build game - game_title = item["label"].split("(", 1)[0] + game_title = item["label"] values = { "source": self.source.id, "added": int(time()), From 19b0737715539a8fa080032267fd81ad4de8515a Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 18:29:42 +0100 Subject: [PATCH 09/59] Fix the boxart not being found for some games. I overthought this problem way too hard lmao --- src/importer/sources/retroarch_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index e3992f6..fe2fb11 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -81,7 +81,8 @@ class RetroarchSourceIterator(SourceIterator): additional_data = {} # Get boxart - boxart_image_name = item["label"].split(".", 1)[0] + ".png" + boxart_image_name = item["label"] + ".png" + boxart_image_name = boxart_image_name.replace("&", "_") boxart_folder_name = playlist_file.name.split(".", 1)[0] image_path = ( self.source.config_location["thumbnails"] From 6b26076b92c36d04be831fb50751835482e55c58 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 18:32:14 +0100 Subject: [PATCH 10/59] Use pre-existing variable for game name aaaaaaaa --- src/importer/sources/retroarch_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index fe2fb11..8c6e78f 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -81,7 +81,7 @@ class RetroarchSourceIterator(SourceIterator): additional_data = {} # Get boxart - boxart_image_name = item["label"] + ".png" + boxart_image_name = game_title + ".png" boxart_image_name = boxart_image_name.replace("&", "_") boxart_folder_name = playlist_file.name.split(".", 1)[0] image_path = ( From 9a7875eb87000c98f36acc9a91f12474f88e6ab8 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 22:45:27 +0100 Subject: [PATCH 11/59] Replace crc32 game_id with md5 hash from path --- src/importer/sources/retroarch_source.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 8c6e78f..1f1c656 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -17,9 +17,9 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import hashlib import json import logging -import os from json import JSONDecodeError from pathlib import Path from time import time @@ -63,14 +63,13 @@ class RetroarchSourceIterator(SourceIterator): continue # Build game - game_title = item["label"] + game_id = hashlib.md5(item["path"].encode("utf-8")).hexdigest() + values = { "source": self.source.id, "added": int(time()), - "name": game_title, - "game_id": self.source.game_id_format.format( - game_id=item["crc32"][:8] - ), + "name": item["label"], + "game_id": self.source.game_id_format.format(game_id=game_id), "executable": self.source.executable_format.format( rom_path=item["path"], core_path=core_path, @@ -81,7 +80,7 @@ class RetroarchSourceIterator(SourceIterator): additional_data = {} # Get boxart - boxart_image_name = game_title + ".png" + boxart_image_name = item["label"] + ".png" boxart_image_name = boxart_image_name.replace("&", "_") boxart_folder_name = playlist_file.name.split(".", 1)[0] image_path = ( @@ -98,7 +97,7 @@ class RetroarchSourceIterator(SourceIterator): class RetroarchSource(Source): args = ' -L "{core_path}" "{rom_path}"' - name = "Retroarch" + name = _("RetroArch") available_on = {"linux"} iterator_class = RetroarchSourceIterator From 0865d4e133a4ed611cdfceec71deb279fe9c6362 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 16 Jul 2023 22:57:14 +0100 Subject: [PATCH 12/59] Make RetroArch capitalisation consistent --- data/gtk/preferences.blp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index 8547e44..51302b1 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -227,7 +227,7 @@ template $PreferencesWindow : Adw.PreferencesWindow { } Adw.ExpanderRow retroarch_expander_row { - title: _("Retroarch"); + title: _("RetroArch"); show-enable-switch: true; Adw.ActionRow retroarch_config_action_row { From 0a2051f5c7753b69efa3a477272ef62da1d194eb Mon Sep 17 00:00:00 2001 From: Rilic Date: Mon, 17 Jul 2023 12:52:38 +0100 Subject: [PATCH 13/59] Improve error handling for unselected cores --- src/importer/sources/retroarch_source.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 1f1c656..a790ae2 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -25,6 +25,7 @@ from pathlib import Path from time import time from src import shared +from src.errors.friendly_error import FriendlyError from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import Source, SourceIterationResult, SourceIterator @@ -37,6 +38,8 @@ class RetroarchSourceIterator(SourceIterator): # Get all playlist files, ending in .lpl playlist_files = self.source.config_location["playlists"].glob("*.lpl") + bad_playlists = set() + for playlist_file in playlist_files: try: with open( @@ -56,10 +59,11 @@ class RetroarchSourceIterator(SourceIterator): item["core_path"], playlist_json["default_core_path"], ): - if core_path != "DETECT": + if core_path not in ("DETECT", ""): break else: logging.warning("Cannot find core for: %s", str(item["path"])) + bad_playlists.add(playlist_file.stem) continue # Build game @@ -93,6 +97,15 @@ class RetroarchSourceIterator(SourceIterator): yield (game, additional_data) + if bad_playlists: + raise FriendlyError( + _("No RetroArch Core Selected"), + # The variable is a newline separated list of playlists + _("The following playlists have no default core:") + + "\n\n{}\n\n".format("\n".join(bad_playlists)) + + _("Games with no core selected were not imported"), + ) + class RetroarchSource(Source): args = ' -L "{core_path}" "{rom_path}"' From faee57a42a95f423e7c97f839b147ef26032b2b4 Mon Sep 17 00:00:00 2001 From: Rilic Date: Mon, 17 Jul 2023 12:55:03 +0100 Subject: [PATCH 14/59] Clean up code and unused imports --- src/importer/sources/retroarch_source.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index a790ae2..c0ac356 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -21,7 +21,6 @@ import hashlib import json import logging from json import JSONDecodeError -from pathlib import Path from time import time from src import shared @@ -35,6 +34,8 @@ class RetroarchSourceIterator(SourceIterator): source: "RetroarchSource" def generator_builder(self) -> SourceIterationResult: + added_time = int(time()) + # Get all playlist files, ending in .lpl playlist_files = self.source.config_location["playlists"].glob("*.lpl") @@ -71,7 +72,7 @@ class RetroarchSourceIterator(SourceIterator): values = { "source": self.source.id, - "added": int(time()), + "added": added_time, "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), "executable": self.source.executable_format.format( @@ -108,8 +109,6 @@ class RetroarchSourceIterator(SourceIterator): class RetroarchSource(Source): - args = ' -L "{core_path}" "{rom_path}"' - name = _("RetroArch") available_on = {"linux"} iterator_class = RetroarchSourceIterator From 386120a50580d23820a39a0aec793b90ff75fdb9 Mon Sep 17 00:00:00 2001 From: Rilic Date: Mon, 17 Jul 2023 16:26:08 +0100 Subject: [PATCH 15/59] Code cleanup and fix cover image rename regex --- src/importer/sources/retroarch_source.py | 18 ++++++++---------- src/main.py | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index c0ac356..0e7c3a9 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -17,10 +17,11 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import hashlib import json import logging +from hashlib import md5 from json import JSONDecodeError +from re import sub from time import time from src import shared @@ -35,20 +36,18 @@ class RetroarchSourceIterator(SourceIterator): def generator_builder(self) -> SourceIterationResult: added_time = int(time()) + bad_playlists = set() # Get all playlist files, ending in .lpl playlist_files = self.source.config_location["playlists"].glob("*.lpl") - bad_playlists = set() - for playlist_file in playlist_files: try: - with open( - self.source.config_location["playlists"] / playlist_file, + with playlist_file.open( encoding="utf-8", ) as open_file: playlist_json = json.load(open_file) - except (JSONDecodeError, OSError, KeyError): + except (JSONDecodeError, OSError): logging.warning("Cannot read playlist file: %s", str(playlist_file)) continue @@ -68,7 +67,7 @@ class RetroarchSourceIterator(SourceIterator): continue # Build game - game_id = hashlib.md5(item["path"].encode("utf-8")).hexdigest() + game_id = md5(item["path"].encode("utf-8")).hexdigest() values = { "source": self.source.id, @@ -82,12 +81,11 @@ class RetroarchSourceIterator(SourceIterator): } game = Game(values) - additional_data = {} # Get boxart boxart_image_name = item["label"] + ".png" - boxart_image_name = boxart_image_name.replace("&", "_") - boxart_folder_name = playlist_file.name.split(".", 1)[0] + boxart_image_name = sub(r"[&\*\/:`<>\?\\\|]", "_", boxart_image_name) + boxart_folder_name = playlist_file.stem image_path = ( self.source.config_location["thumbnails"] / boxart_folder_name diff --git a/src/main.py b/src/main.py index ff6ac83..4280ac6 100644 --- a/src/main.py +++ b/src/main.py @@ -33,13 +33,13 @@ from src import shared from src.details_window import DetailsWindow from src.game import Game from src.importer.importer import Importer -from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.bottles_source import BottlesSource from src.importer.sources.flatpak_source import FlatpakSource from src.importer.sources.heroic_source import HeroicSource from src.importer.sources.itch_source import ItchSource from src.importer.sources.legendary_source import LegendarySource from src.importer.sources.lutris_source import LutrisSource +from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.steam_source import SteamSource from src.logging.setup import log_system_info, setup_logging from src.preferences import PreferencesWindow From fbb2ccec574f77ca8c6b00bf14ab8ec89f30f5de Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 18 Jul 2023 14:20:19 +0200 Subject: [PATCH 16/59] silence pil --- src/logging/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging/setup.py b/src/logging/setup.py index 2c0e484..e9737cd 100644 --- a/src/logging/setup.py +++ b/src/logging/setup.py @@ -73,7 +73,7 @@ def setup_logging(): "PIL": { "handlers": ["lib_console_handler", "file_handler"], "propagate": False, - "level": "NOTSET", + "level": "WARNING", }, "urllib3": { "handlers": ["lib_console_handler", "file_handler"], From 00ff29786722c469110e1dd4a840f15aae7e4b19 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 18 Jul 2023 14:23:43 +0200 Subject: [PATCH 17/59] Steam source debug info on skip --- src/importer/sources/steam_source.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 561d843..975f369 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -18,6 +18,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import logging import re from pathlib import Path from time import time @@ -25,13 +26,13 @@ from typing import Iterable from src import shared from src.game import Game +from src.importer.sources.location import Location from src.importer.sources.source import ( SourceIterationResult, SourceIterator, URLExecutableSource, ) from src.utils.steam import SteamFileHelper, SteamInvalidManifestError -from src.importer.sources.location import Location class SteamSourceIterator(SourceIterator): @@ -74,17 +75,20 @@ class SteamSourceIterator(SourceIterator): steam = SteamFileHelper() try: local_data = steam.get_manifest_data(manifest) - except (OSError, SteamInvalidManifestError): + except (OSError, SteamInvalidManifestError) as error: + logging.debug("Couldn't load appmanifest %s", manifest, exc_info=error) continue # Skip non installed games installed_mask = 4 if not int(local_data["stateflags"]) & installed_mask: + logging.debug("Skipped %s: not installed", manifest) continue # Skip duplicate appids appid = local_data["appid"] if appid in appid_cache: + logging.debug("Skipped %s: appid already seen during import", manifest) continue appid_cache.add(appid) From 15da65fccf02812511e4c53a4a1c836c67c92c68 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 05:01:17 +0200 Subject: [PATCH 18/59] WIP heroic source refactor - Fixed installed games lookup - Added support for amazon games TODO - Test (obviously) - Consider getting hidden value --- data/hu.kramo.Cartridges.gschema.xml.in | 3 + src/importer/sources/heroic_source.py | 285 +++++++++++++++++++----- 2 files changed, 229 insertions(+), 59 deletions(-) diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index edd5aec..31c6a68 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -46,6 +46,9 @@ true + + true + true diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 499803b..cedfcd7 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -22,19 +22,40 @@ import json import logging from hashlib import sha256 from json import JSONDecodeError +from pathlib import Path from time import time -from typing import Optional, TypedDict +from typing import Optional, TypedDict, Generator, Iterable +from abc import abstractmethod from src import shared from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import ( - URLExecutableSource, SourceIterationResult, SourceIterator, + URLExecutableSource, ) +def path_json_load(path: Path): + """ + Load JSON from the file at the given path + + :raises OSError: if the file can't be opened + :raises JSONDecodeError: if the file isn't valid JSON + """ + with path.open("r", encoding="utf-8") as open_file: + return json.load(open_file) + + +class InvalidLibraryFileError(Exception): + pass + + +class InvalidInstalledFileError(Exception): + pass + + class HeroicLibraryEntry(TypedDict): app_name: str installed: Optional[bool] @@ -44,49 +65,38 @@ class HeroicLibraryEntry(TypedDict): art_square: str -class HeroicSubSource(TypedDict): - service: str - path: tuple[str] +class SubSource(Iterable): + """Class representing a Heroic sub-source""" - -class HeroicSourceIterator(SourceIterator): source: "HeroicSource" + name: str + service: str + image_uri_params: str = "" + relative_library_path: Path + library_json_entries_key: str = "library" - sub_sources: dict[str, HeroicSubSource] = { - "sideload": { - "service": "sideload", - "path": ("sideload_apps", "library.json"), - }, - "legendary": { - "service": "epic", - "path": ("store_cache", "legendary_library.json"), - }, - "gog": { - "service": "gog", - "path": ("store_cache", "gog_library.json"), - }, - } + def __init__(self, source) -> None: + self.source = source - def game_from_library_entry( + @property + def library_path(self) -> Path: + return self.source.config_location.root / self.relative_library_path + + def process_library_entry( self, entry: HeroicLibraryEntry, added_time: int ) -> SourceIterationResult: - """Helper method used to build a Game from a Heroic library entry""" + """Build a Game from a Heroic library entry""" - # Skip games that are not installed - if not entry["is_installed"]: - return None + app_name = entry["app_name"] # Build game - app_name = entry["app_name"] - runner = entry["runner"] - service = self.sub_sources[runner]["service"] values = { - "source": f"{self.source.id}_{service}", + "source": f"{self.source.id}_{self.service}", "added": added_time, "name": entry["title"], "developer": entry.get("developer", None), "game_id": self.source.game_id_format.format( - service=service, game_id=app_name + service=self.service, game_id=app_name ), "executable": self.source.executable_format.format(app_name=app_name), } @@ -94,45 +104,202 @@ class HeroicSourceIterator(SourceIterator): # 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" + uri: str = entry["art_square"] + self.image_uri_params digest = sha256(uri.encode()).hexdigest() image_path = self.source.config_location.root / "images-cache" / digest additional_data = {"local_image_path": image_path} return (game, additional_data) - def generator_builder(self) -> SourceIterationResult: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: + """ + Iterate through the installed games with a generator + :raises InvalidLibraryFileError: on initial call if the library file is bad + """ + added_time = int(time()) + try: + iterator = iter( + path_json_load(self.library_path)[self.library_json_entries_key] + ) + except (OSError, JSONDecodeError, TypeError, KeyError) as error: + raise InvalidLibraryFileError( + f"Invalid {self.library_path.name}" + ) from error + for entry in iterator: + try: + yield self.process_library_entry(entry, added_time) + except KeyError as error: + logging.warning( + "Skipped invalid %s game %s", + self.name, + entry.get("app_name", "UNKNOWN"), + exc_info=error, + ) + continue + + +class StoreSubSource(SubSource): + """ + Class representing a "store" sub source iterator. + Games can be installed or not, this class does the check accordingly. + """ + + relative_installed_path: Optional[Path] + installed_app_names: set[str] + + @property + def installed_path(self) -> Path: + return self.source.config_location.root / self.relative_installed_path + + @abstractmethod + def get_installed_app_names(self) -> set[str]: + """ + Get the sub source's installed app names as a set. + + :raises InvalidInstalledFileError: if the installed file data cannot be read + Whenever possible, `__cause__` is set with the original exception + """ + + def is_installed(self, app_name: str) -> bool: + return app_name in self.installed_app_names + + def process_library_entry(self, entry, added_time): + # Skip games that are not installed + app_name = entry["app_name"] + if not self.is_installed(app_name): + logging.warning( + "Skipped %s game %s (%s): not installed", + self.service, + entry["title"], + app_name, + ) + return None + # Process entry as normal + return super().process_library_entry(entry, added_time) + + def __iter__(self): + """ + Iterate through the installed games with a generator + :raises InvalidLibraryFileError: on initial call if the library file is bad + :raises InvalidInstalledFileError: on initial call if the installed file is bad + """ + self.installed_app_names = self.get_installed_app_names() + # TODO check that this syntax works + yield from super() + + +class SideloadIterable(SubSource): + name = "sideload" + service = "sideload" + relative_library_path = Path("sideload_apps") / "library.json" + + +class LegendaryIterable(StoreSubSource): + name = "legendary" + service = "epic" + image_uri_params = "?h=400&resize=1&w=300" + relative_library_path = Path("store_cache") / "legendary_library.json" + + # TODO simplify Heroic 2.9 has been out for a while + # (uncomment value and remove the library_path property override) + # + # relative_installed_path = ( + # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" + # ) + relative_installed_path = None + + @property + def library_path(self) -> Path: + """Get the right path depending on the Heroic version""" + heroic_config_path = self.source.config_location.root + if (path := heroic_config_path / "legendaryConfig").is_dir(): + # Heroic > 2.9 + pass + elif heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak < 2.8 + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + else: + # Heroic < 2.8 + path = Path.home() / ".config" + return path / "legendary" / "installed.json" + + def get_installed_app_names(self): + try: + return set(path_json_load(self.installed_path).keys()) + except (OSError, JSONDecodeError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class GogIterable(StoreSubSource): + name = "gog" + service = "gog" + library_json_entries_key = "games" + relative_library_path = Path("store_cache") / "gog_library.json" + relative_installed_path = Path("gog_store") / "installed.json" + + def get_installed_app_names(self): + try: + return ( + app_name + for entry in path_json_load(self.installed_path)["installed"] + if (app_name := entry.get("appName")) is not None + ) + except (OSError, JSONDecodeError, KeyError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class NileIterable(StoreSubSource): + name = "nile" + service = "amazon" + relative_library_path = Path("store_cache") / "nile_library.json" + relative_installed_path = Path("nile_config") / "nile" / "installed.json" + + def get_installed_app_names(self): + try: + installed_json = path_json_load(self.installed_path) + return ( + app_name + for entry in installed_json + if (app_name := entry.get("id")) is not None + ) + except (OSError, JSONDecodeError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class HeroicSourceIterator(SourceIterator): + source: "HeroicSource" + + def __iter__(self): """Generator method producing games from all the Heroic sub-sources""" - for sub_source_name, sub_source in self.sub_sources.items(): - # Skip disabled sub-sources - if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]): + for sub_source_class in ( + SideloadIterable, + LegendaryIterable, + GogIterable, + NileIterable, + ): + sub_source = sub_source_class(self.source) + + if not shared.schema.get_boolean("heroic-import-" + sub_source.service): + logging.debug("Skipping Heroic %s: disabled", sub_source.service) continue - # Load games from JSON - file = self.source.config_location.root.joinpath(*sub_source["path"]) + try: - contents = json.load(file.open()) - key = "library" if sub_source_name == "legendary" else "games" - library = contents[key] - except (JSONDecodeError, OSError, KeyError): - # Invalid library.json file, skip it - logging.warning("Couldn't open Heroic file: %s", str(file)) - continue + sub_source_iterator = iter(sub_source) + except (InvalidLibraryFileError, InvalidInstalledFileError) as error: + logging.error( + "Skipping bad Heroic sub-source %s", + sub_source.service, + exc_info=error, + ) - added_time = int(time()) - - for entry in library: - try: - result = self.game_from_library_entry(entry, added_time) - except KeyError as error: - # Skip invalid games - logging.warning( - "Invalid Heroic game skipped in %s", str(file), exc_info=error - ) - continue - yield result + yield from sub_source_iterator class HeroicSource(URLExecutableSource): From 0601fd5ebb41d2e52a1cbc7794365bd7880a4ac9 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 05:32:47 +0200 Subject: [PATCH 19/59] Converted genexps to setcomps --- src/importer/sources/heroic_source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index cedfcd7..b3ad604 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -241,11 +241,11 @@ class GogIterable(StoreSubSource): def get_installed_app_names(self): try: - return ( + return { app_name for entry in path_json_load(self.installed_path)["installed"] if (app_name := entry.get("appName")) is not None - ) + } except (OSError, JSONDecodeError, KeyError, AttributeError) as error: raise InvalidInstalledFileError( f"Invalid {self.installed_path.name}" @@ -261,11 +261,11 @@ class NileIterable(StoreSubSource): def get_installed_app_names(self): try: installed_json = path_json_load(self.installed_path) - return ( + return { app_name for entry in installed_json if (app_name := entry.get("id")) is not None - ) + } except (OSError, JSONDecodeError, AttributeError) as error: raise InvalidInstalledFileError( f"Invalid {self.installed_path.name}" From a0bfca01d6922ceac96ea3d1e4b1cb829bedb066 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 05:33:55 +0200 Subject: [PATCH 20/59] WIP added support for heroic hidden TODO - Test all of that --- src/importer/sources/heroic_source.py | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index b3ad604..e3ba9bd 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -56,6 +56,10 @@ class InvalidInstalledFileError(Exception): pass +class InvalidStoreFileError(Exception): + pass + + class HeroicLibraryEntry(TypedDict): app_name: str installed: Optional[bool] @@ -69,14 +73,16 @@ class SubSource(Iterable): """Class representing a Heroic sub-source""" source: "HeroicSource" + source_iterator: "HeroicSourceIterator" name: str service: str image_uri_params: str = "" relative_library_path: Path library_json_entries_key: str = "library" - def __init__(self, source) -> None: + def __init__(self, source, source_iterator) -> None: self.source = source + self.source_iterator = source_iterator @property def library_path(self) -> Path: @@ -99,6 +105,7 @@ class SubSource(Iterable): service=self.service, game_id=app_name ), "executable": self.source.executable_format.format(app_name=app_name), + "hidden": self.source_iterator.is_hidden(app_name), } game = Game(values) @@ -275,16 +282,32 @@ class NileIterable(StoreSubSource): class HeroicSourceIterator(SourceIterator): source: "HeroicSource" + hidden_app_names: set[str] = set() + + def is_hidden(self, app_name: str) -> bool: + return app_name in self.hidden_app_names + def __iter__(self): """Generator method producing games from all the Heroic sub-sources""" + # Get the hidden app names + try: + store = path_json_load(self.source.config_location["store_config.json"]) + self.hidden_app_names = { + game["appName"] for game in store["games"]["hidden"] + } + except (OSError, JSONDecodeError, KeyError, TypeError) as error: + logging.error("Invalid Heroic store file", exc_info=error) + raise InvalidStoreFileError() from error + + # Get games from the sub sources for sub_source_class in ( SideloadIterable, LegendaryIterable, GogIterable, NileIterable, ): - sub_source = sub_source_class(self.source) + sub_source = sub_source_class(self.source, self) if not shared.schema.get_boolean("heroic-import-" + sub_source.service): logging.debug("Skipping Heroic %s: disabled", sub_source.service) @@ -320,6 +343,7 @@ class HeroicSource(URLExecutableSource): ), paths={ "config.json": (False, "config.json"), + "store_config.json": (False, ("store", "config.json")), }, ) From 8839db272b49daf0d7bb80cad5907b0604df2e95 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 13:00:29 +0200 Subject: [PATCH 21/59] better legendary sub-source library path detection --- src/importer/sources/heroic_source.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index e3ba9bd..b194650 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -222,12 +222,17 @@ class LegendaryIterable(StoreSubSource): if (path := heroic_config_path / "legendaryConfig").is_dir(): # Heroic > 2.9 pass - elif heroic_config_path.is_relative_to(shared.flatpak_dir): - # Heroic flatpak < 2.8 - path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" else: - # Heroic < 2.8 - path = Path.home() / ".config" + # Heroic <= 2.8 + if heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + elif shared.config_dir.is_relative_to(shared.flatpak_dir): + # Heroic native (from Cartridges flatpak) + path = Path.home() / ".config" + else: + # Heroic native (from other Cartridges installations) + path = shared.config_dir return path / "legendary" / "installed.json" def get_installed_app_names(self): From a399113ff99e71cd1bff9908eeec2e95f07e0fea Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 13:12:07 +0200 Subject: [PATCH 22/59] fixed typo --- src/importer/sources/heroic_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index b194650..5150696 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -220,7 +220,7 @@ class LegendaryIterable(StoreSubSource): """Get the right path depending on the Heroic version""" heroic_config_path = self.source.config_location.root if (path := heroic_config_path / "legendaryConfig").is_dir(): - # Heroic > 2.9 + # Heroic >= 2.9 pass else: # Heroic <= 2.8 From 2acdedf033367d0ea33d3dcf04393d0e9a8707e5 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:29:27 +0200 Subject: [PATCH 23/59] Added heroic import amazon to ui + fixes --- data/gtk/preferences.blp | 9 +++++++++ data/hu.kramo.Cartridges.gschema.xml.in | 4 ++-- src/importer/sources/heroic_source.py | 2 +- src/preferences.py | 2 ++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index fbc2dee..7aa63b9 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -174,6 +174,15 @@ template $PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ActionRow { + title: _("Import Amazon Games"); + activatable-widget: heroic_import_amazon_switch; + + Switch heroic_import_amazon_switch { + valign: center; + } + } + Adw.ActionRow { title: _("Import Sideloaded Games"); activatable-widget: heroic_import_sideload_switch; diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index 31c6a68..b0a614d 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -43,10 +43,10 @@ true - + true - + true diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 5150696..a3dce89 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -348,7 +348,7 @@ class HeroicSource(URLExecutableSource): ), paths={ "config.json": (False, "config.json"), - "store_config.json": (False, ("store", "config.json")), + "store_config.json": (False, Path("store") / "config.json"), }, ) diff --git a/src/preferences.py b/src/preferences.py index 9e9be4d..fdafd69 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -68,6 +68,7 @@ class PreferencesWindow(Adw.PreferencesWindow): heroic_config_file_chooser_button = Gtk.Template.Child() heroic_import_epic_switch = Gtk.Template.Child() heroic_import_gog_switch = Gtk.Template.Child() + heroic_import_amazon_switch = Gtk.Template.Child() heroic_import_sideload_switch = Gtk.Template.Child() bottles_expander_row = Gtk.Template.Child() @@ -181,6 +182,7 @@ class PreferencesWindow(Adw.PreferencesWindow): "lutris-import-flatpak", "heroic-import-epic", "heroic-import-gog", + "heroic-import-amazon", "heroic-import-sideload", "flatpak-import-launchers", "sgdb", From 30152cd10afda3389789830b047e566db7d8ef55 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:29:39 +0200 Subject: [PATCH 24/59] simplified SourceIterator --- src/importer/sources/bottles_source.py | 8 ++------ src/importer/sources/flatpak_source.py | 4 ++-- src/importer/sources/heroic_source.py | 4 ++-- src/importer/sources/itch_source.py | 8 ++------ src/importer/sources/legendary_source.py | 3 +-- src/importer/sources/lutris_source.py | 8 ++------ src/importer/sources/source.py | 15 +++------------ src/importer/sources/steam_source.py | 8 ++------ 8 files changed, 16 insertions(+), 42 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index d993598..f945740 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -26,17 +26,13 @@ import yaml from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource class BottlesSourceIterator(SourceIterator): source: "BottlesSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" data = self.source.data_location["library.yml"].read_text("utf-8") diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 6ec6262..4195d21 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -25,13 +25,13 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.source import Source, SourceIterator class FlatpakSourceIterator(SourceIterator): source: "FlatpakSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" added_time = int(time()) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index a3dce89..0a7038a 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -24,7 +24,7 @@ from hashlib import sha256 from json import JSONDecodeError from pathlib import Path from time import time -from typing import Optional, TypedDict, Generator, Iterable +from typing import Optional, TypedDict, Iterable from abc import abstractmethod from src import shared @@ -118,7 +118,7 @@ class SubSource(Iterable): return (game, additional_data) - def __iter__(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self): """ Iterate through the installed games with a generator :raises InvalidLibraryFileError: on initial call if the library file is bad diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 141c9f9..6659f7c 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -25,18 +25,14 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource from src.utils.sqlite import copy_db class ItchSourceIterator(SourceIterator): source: "ItchSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" # Query the database diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 22392be..2e43948 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -21,7 +21,6 @@ import json import logging from json import JSONDecodeError from time import time -from typing import Generator from src import shared from src.game import Game @@ -65,7 +64,7 @@ class LegendarySourceIterator(SourceIterator): game = Game(values) return (game, data) - def generator_builder(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self): # Open library file = self.source.config_location["installed.json"] try: diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index ec4b066..9077591 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -24,18 +24,14 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource from src.utils.sqlite import copy_db class LutrisSourceIterator(SourceIterator): source: "LutrisSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" # Query the database diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index cb9eb0c..2abfbde 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -29,25 +29,16 @@ from src.importer.sources.location import Location SourceIterationResult = None | Game | tuple[Game, tuple[Any]] -class SourceIterator(Iterator): +class SourceIterator: """Data producer for a source of games""" source: "Source" = None - generator: Generator = None def __init__(self, source: "Source") -> None: - super().__init__() self.source = source - self.generator = self.generator_builder() - - def __iter__(self) -> "SourceIterator": - return self - - def __next__(self) -> SourceIterationResult: - return next(self.generator) @abstractmethod - def generator_builder(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Method that returns a generator that produces games * Should be implemented as a generator method @@ -108,7 +99,7 @@ class Source(Iterable): if location is None: continue location.resolve() - return self.iterator_class(self) + return iter(self.iterator_class(self)) # pylint: disable=abstract-method diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 975f369..a47a90c 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -27,11 +27,7 @@ from typing import Iterable from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource from src.utils.steam import SteamFileHelper, SteamInvalidManifestError @@ -63,7 +59,7 @@ class SteamSourceIterator(SourceIterator): ) return manifests - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" appid_cache = set() manifests = self.get_manifests() From 7f576d1bd3667252e37ea7eee19494d61e33dec5 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:32:43 +0200 Subject: [PATCH 25/59] SourceIterator is actually just SourceIterable --- src/importer/sources/bottles_source.py | 6 +++--- src/importer/sources/flatpak_source.py | 6 +++--- src/importer/sources/heroic_source.py | 8 ++++---- src/importer/sources/itch_source.py | 6 +++--- src/importer/sources/legendary_source.py | 6 +++--- src/importer/sources/lutris_source.py | 6 +++--- src/importer/sources/source.py | 6 +++--- src/importer/sources/steam_source.py | 6 +++--- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index f945740..22e071b 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -26,10 +26,10 @@ import yaml from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource -class BottlesSourceIterator(SourceIterator): +class BottlesSourceIterable(SourceIterable): source: "BottlesSource" def __iter__(self): @@ -80,7 +80,7 @@ class BottlesSource(URLExecutableSource): """Generic Bottles source""" name = _("Bottles") - iterator_class = BottlesSourceIterator + iterator_class = BottlesSourceIterable url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 4195d21..2c52dab 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -25,10 +25,10 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterator +from src.importer.sources.source import Source, SourceIterable -class FlatpakSourceIterator(SourceIterator): +class FlatpakSourceIterable(SourceIterable): source: "FlatpakSource" def __iter__(self): @@ -115,7 +115,7 @@ class FlatpakSource(Source): """Generic Flatpak source""" name = _("Flatpak") - iterator_class = FlatpakSourceIterator + iterator_class = FlatpakSourceIterable executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 0a7038a..1b9ff6e 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -32,7 +32,7 @@ from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import ( SourceIterationResult, - SourceIterator, + SourceIterable, URLExecutableSource, ) @@ -73,7 +73,7 @@ class SubSource(Iterable): """Class representing a Heroic sub-source""" source: "HeroicSource" - source_iterator: "HeroicSourceIterator" + source_iterator: "HeroicSourceIterable" name: str service: str image_uri_params: str = "" @@ -284,7 +284,7 @@ class NileIterable(StoreSubSource): ) from error -class HeroicSourceIterator(SourceIterator): +class HeroicSourceIterable(SourceIterable): source: "HeroicSource" hidden_app_names: set[str] = set() @@ -334,7 +334,7 @@ class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" name = _("Heroic") - iterator_class = HeroicSourceIterator + iterator_class = HeroicSourceIterable url_format = "heroic://launch/{app_name}" available_on = {"linux", "win32"} diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 6659f7c..b6c0ad3 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -25,11 +25,11 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db -class ItchSourceIterator(SourceIterator): +class ItchSourceIterable(SourceIterable): source: "ItchSource" def __iter__(self): @@ -76,7 +76,7 @@ class ItchSourceIterator(SourceIterator): class ItchSource(URLExecutableSource): name = _("itch") - iterator_class = ItchSourceIterator + iterator_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 2e43948..72eae35 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -25,10 +25,10 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.source import Source, SourceIterationResult, SourceIterable -class LegendarySourceIterator(SourceIterator): +class LegendarySourceIterable(SourceIterable): source: "LegendarySource" def game_from_library_entry( @@ -93,7 +93,7 @@ class LegendarySource(Source): executable_format = "legendary launch {app_name}" available_on = {"linux"} - iterator_class = LegendarySourceIterator + iterator_class = LegendarySourceIterable config_location: Location = Location( schema_key="legendary-location", candidates=( diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 9077591..8696aef 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -24,11 +24,11 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db -class LutrisSourceIterator(SourceIterator): +class LutrisSourceIterable(SourceIterable): source: "LutrisSource" def __iter__(self): @@ -87,7 +87,7 @@ class LutrisSource(URLExecutableSource): """Generic Lutris source""" name = _("Lutris") - iterator_class = LutrisSourceIterator + iterator_class = LutrisSourceIterable url_format = "lutris:rungameid/{game_id}" available_on = {"linux"} diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 2abfbde..383dc1c 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -29,7 +29,7 @@ from src.importer.sources.location import Location SourceIterationResult = None | Game | tuple[Game, tuple[Any]] -class SourceIterator: +class SourceIterable(Iterable): """Data producer for a source of games""" source: "Source" = None @@ -57,7 +57,7 @@ class Source(Iterable): data_location: Optional[Location] = None cache_location: Optional[Location] = None config_location: Optional[Location] = None - iterator_class: type[SourceIterator] + iterator_class: type[SourceIterable] @property def full_name(self) -> str: @@ -89,7 +89,7 @@ class Source(Iterable): def executable_format(self) -> str: """The executable format used to construct game executables""" - def __iter__(self) -> SourceIterator: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Get an iterator for the source :raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index a47a90c..040edcb 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -27,11 +27,11 @@ from typing import Iterable from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.steam import SteamFileHelper, SteamInvalidManifestError -class SteamSourceIterator(SourceIterator): +class SteamSourceIterable(SourceIterable): source: "SteamSource" def get_manifest_dirs(self) -> Iterable[Path]: @@ -112,7 +112,7 @@ class SteamSourceIterator(SourceIterator): class SteamSource(URLExecutableSource): name = _("Steam") available_on = {"linux", "win32"} - iterator_class = SteamSourceIterator + iterator_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" data_location = Location( From 0df123975cc08bb8456daec5bf77ac1942a89c5e Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:35:07 +0200 Subject: [PATCH 26/59] Fix some syntax --- src/importer/sources/heroic_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 1b9ff6e..bb2c053 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -191,8 +191,7 @@ class StoreSubSource(SubSource): :raises InvalidInstalledFileError: on initial call if the installed file is bad """ self.installed_app_names = self.get_installed_app_names() - # TODO check that this syntax works - yield from super() + yield from super().__iter__() class SideloadIterable(SubSource): From 52b6c47c8d805ffdd022261b95865dbc5ebcd2c1 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:58:23 +0200 Subject: [PATCH 27/59] More renaming to iterable + fixes to heroic --- src/importer/sources/bottles_source.py | 2 +- src/importer/sources/flatpak_source.py | 2 +- src/importer/sources/heroic_source.py | 40 +++++++++++------------- src/importer/sources/itch_source.py | 2 +- src/importer/sources/legendary_source.py | 2 +- src/importer/sources/lutris_source.py | 2 +- src/importer/sources/source.py | 4 +-- src/importer/sources/steam_source.py | 2 +- 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 22e071b..8829023 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -80,7 +80,7 @@ class BottlesSource(URLExecutableSource): """Generic Bottles source""" name = _("Bottles") - iterator_class = BottlesSourceIterable + iterable_class = BottlesSourceIterable url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 2c52dab..2c9a4aa 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -115,7 +115,7 @@ class FlatpakSource(Source): """Generic Flatpak source""" name = _("Flatpak") - iterator_class = FlatpakSourceIterable + iterable_class = FlatpakSourceIterable executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index bb2c053..3e53787 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -69,20 +69,20 @@ class HeroicLibraryEntry(TypedDict): art_square: str -class SubSource(Iterable): +class SubSourceIterable(Iterable): """Class representing a Heroic sub-source""" source: "HeroicSource" - source_iterator: "HeroicSourceIterable" + source_iterable: "HeroicSourceIterable" name: str service: str image_uri_params: str = "" relative_library_path: Path library_json_entries_key: str = "library" - def __init__(self, source, source_iterator) -> None: + def __init__(self, source, source_iterable) -> None: self.source = source - self.source_iterator = source_iterator + self.source_iterable = source_iterable @property def library_path(self) -> Path: @@ -105,7 +105,7 @@ class SubSource(Iterable): service=self.service, game_id=app_name ), "executable": self.source.executable_format.format(app_name=app_name), - "hidden": self.source_iterator.is_hidden(app_name), + "hidden": self.source_iterable.is_hidden(app_name), } game = Game(values) @@ -120,7 +120,7 @@ class SubSource(Iterable): def __iter__(self): """ - Iterate through the installed games with a generator + Iterate through the games with a generator :raises InvalidLibraryFileError: on initial call if the library file is bad """ added_time = int(time()) @@ -145,13 +145,13 @@ class SubSource(Iterable): continue -class StoreSubSource(SubSource): +class StoreSubSourceIterable(SubSourceIterable): """ - Class representing a "store" sub source iterator. + Class representing a "store" sub source. Games can be installed or not, this class does the check accordingly. """ - relative_installed_path: Optional[Path] + relative_installed_path: Path installed_app_names: set[str] @property @@ -194,28 +194,27 @@ class StoreSubSource(SubSource): yield from super().__iter__() -class SideloadIterable(SubSource): +class SideloadIterable(SubSourceIterable): name = "sideload" service = "sideload" relative_library_path = Path("sideload_apps") / "library.json" -class LegendaryIterable(StoreSubSource): +class LegendaryIterable(StoreSubSourceIterable): name = "legendary" service = "epic" image_uri_params = "?h=400&resize=1&w=300" relative_library_path = Path("store_cache") / "legendary_library.json" # TODO simplify Heroic 2.9 has been out for a while - # (uncomment value and remove the library_path property override) + # (uncomment value and remove the installed_path property override) # # relative_installed_path = ( # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" # ) - relative_installed_path = None @property - def library_path(self) -> Path: + def installed_path(self) -> Path: """Get the right path depending on the Heroic version""" heroic_config_path = self.source.config_location.root if (path := heroic_config_path / "legendaryConfig").is_dir(): @@ -243,7 +242,7 @@ class LegendaryIterable(StoreSubSource): ) from error -class GogIterable(StoreSubSource): +class GogIterable(StoreSubSourceIterable): name = "gog" service = "gog" library_json_entries_key = "games" @@ -263,7 +262,7 @@ class GogIterable(StoreSubSource): ) from error -class NileIterable(StoreSubSource): +class NileIterable(StoreSubSourceIterable): name = "nile" service = "amazon" relative_library_path = Path("store_cache") / "nile_library.json" @@ -316,24 +315,23 @@ class HeroicSourceIterable(SourceIterable): if not shared.schema.get_boolean("heroic-import-" + sub_source.service): logging.debug("Skipping Heroic %s: disabled", sub_source.service) continue - try: - sub_source_iterator = iter(sub_source) + sub_source_iterable = iter(sub_source) + yield from sub_source_iterable except (InvalidLibraryFileError, InvalidInstalledFileError) as error: logging.error( "Skipping bad Heroic sub-source %s", sub_source.service, exc_info=error, ) - - yield from sub_source_iterator + continue class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" name = _("Heroic") - iterator_class = HeroicSourceIterable + iterable_class = HeroicSourceIterable url_format = "heroic://launch/{app_name}" available_on = {"linux", "win32"} diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index b6c0ad3..a6d8990 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -76,7 +76,7 @@ class ItchSourceIterable(SourceIterable): class ItchSource(URLExecutableSource): name = _("itch") - iterator_class = ItchSourceIterable + iterable_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 72eae35..c7e06de 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -93,7 +93,7 @@ class LegendarySource(Source): executable_format = "legendary launch {app_name}" available_on = {"linux"} - iterator_class = LegendarySourceIterable + iterable_class = LegendarySourceIterable config_location: Location = Location( schema_key="legendary-location", candidates=( diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 8696aef..7c100a8 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -87,7 +87,7 @@ class LutrisSource(URLExecutableSource): """Generic Lutris source""" name = _("Lutris") - iterator_class = LutrisSourceIterable + iterable_class = LutrisSourceIterable url_format = "lutris:rungameid/{game_id}" available_on = {"linux"} diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 383dc1c..d7ba467 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -57,7 +57,7 @@ class Source(Iterable): data_location: Optional[Location] = None cache_location: Optional[Location] = None config_location: Optional[Location] = None - iterator_class: type[SourceIterable] + iterable_class: type[SourceIterable] @property def full_name(self) -> str: @@ -99,7 +99,7 @@ class Source(Iterable): if location is None: continue location.resolve() - return iter(self.iterator_class(self)) + return iter(self.iterable_class(self)) # pylint: disable=abstract-method diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 040edcb..7e65e5b 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -112,7 +112,7 @@ class SteamSourceIterable(SourceIterable): class SteamSource(URLExecutableSource): name = _("Steam") available_on = {"linux", "win32"} - iterator_class = SteamSourceIterable + iterable_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" data_location = Location( From 190b446de5e63b6729c23f54cd69a8f8d20141ac Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 11:26:05 +0200 Subject: [PATCH 28/59] More debug messages + fix sideloaded heroic games --- src/importer/sources/heroic_source.py | 31 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 3e53787..d82b111 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -86,7 +86,9 @@ class SubSourceIterable(Iterable): @property def library_path(self) -> Path: - return self.source.config_location.root / self.relative_library_path + path = self.source.config_location.root / self.relative_library_path + logging.debug("Using Heroic %s library.json path %s", self.name, path) + return path def process_library_entry( self, entry: HeroicLibraryEntry, added_time: int @@ -156,7 +158,9 @@ class StoreSubSourceIterable(SubSourceIterable): @property def installed_path(self) -> Path: - return self.source.config_location.root / self.relative_installed_path + path = self.source.config_location.root / self.relative_installed_path + logging.debug("Using Heroic %s installed.json path %s", self.name, path) + return path @abstractmethod def get_installed_app_names(self) -> set[str]: @@ -198,6 +202,7 @@ class SideloadIterable(SubSourceIterable): name = "sideload" service = "sideload" relative_library_path = Path("sideload_apps") / "library.json" + library_json_entries_key = "games" class LegendaryIterable(StoreSubSourceIterable): @@ -206,32 +211,42 @@ class LegendaryIterable(StoreSubSourceIterable): image_uri_params = "?h=400&resize=1&w=300" relative_library_path = Path("store_cache") / "legendary_library.json" - # TODO simplify Heroic 2.9 has been out for a while - # (uncomment value and remove the installed_path property override) - # # relative_installed_path = ( # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" # ) @property def installed_path(self) -> Path: - """Get the right path depending on the Heroic version""" + """ + Get the right path depending on the Heroic version + + TODO after heroic 2.9 has been out for a while + We should use the commented out relative_installed_path + and remove this property override. + """ + heroic_config_path = self.source.config_location.root if (path := heroic_config_path / "legendaryConfig").is_dir(): # Heroic >= 2.9 - pass + logging.debug("Using Heroic >= 2.9 legendary file") else: # Heroic <= 2.8 if heroic_config_path.is_relative_to(shared.flatpak_dir): # Heroic flatpak path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + logging.debug("Using Heroic flatpak <= 2.8 legendary file") elif shared.config_dir.is_relative_to(shared.flatpak_dir): # Heroic native (from Cartridges flatpak) + logging.debug("Using Heroic native <= 2.8 legendary file") path = Path.home() / ".config" else: # Heroic native (from other Cartridges installations) + logging.debug("Using Heroic native <= 2.8 legendary file") path = shared.config_dir - return path / "legendary" / "installed.json" + + path = path / "legendary" / "installed.json" + logging.debug("Using Heroic %s installed.json path %s", self.name, path) + return path def get_installed_app_names(self): try: From bb4870e99d977d9c576ba1f3300f1b7c089128dc Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 11:33:55 +0200 Subject: [PATCH 29/59] Add debug message to local cover manager --- src/store/managers/local_cover_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/store/managers/local_cover_manager.py b/src/store/managers/local_cover_manager.py index b95c22b..0b95f30 100644 --- a/src/store/managers/local_cover_manager.py +++ b/src/store/managers/local_cover_manager.py @@ -18,6 +18,8 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import logging + from gi.repository import GdkPixbuf from src import shared @@ -35,6 +37,7 @@ class LocalCoverManager(Manager): 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"): From 82dddd1c5cc29584eb35ae33d57c29eb342ac0f1 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Thu, 20 Jul 2023 19:52:59 +0200 Subject: [PATCH 30/59] Skip missing hidden key --- src/importer/sources/heroic_source.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index d82b111..378c3b3 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -20,19 +20,19 @@ import json import logging +from abc import abstractmethod from hashlib import sha256 from json import JSONDecodeError from pathlib import Path from time import time -from typing import Optional, TypedDict, Iterable -from abc import abstractmethod +from typing import Iterable, Optional, TypedDict from src import shared from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import ( - SourceIterationResult, SourceIterable, + SourceIterationResult, URLExecutableSource, ) @@ -311,9 +311,10 @@ class HeroicSourceIterable(SourceIterable): # Get the hidden app names try: store = path_json_load(self.source.config_location["store_config.json"]) - self.hidden_app_names = { - game["appName"] for game in store["games"]["hidden"] - } + if "hidden" in store["games"].keys(): + self.hidden_app_names = { + game["appName"] for game in store["games"]["hidden"] + } except (OSError, JSONDecodeError, KeyError, TypeError) as error: logging.error("Invalid Heroic store file", exc_info=error) raise InvalidStoreFileError() from error From 270fa2092cb3d532083c11c394d9fa4f7ea60bd7 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 20:56:28 +0200 Subject: [PATCH 31/59] Fixed heroic location candidates priority --- src/importer/sources/heroic_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 378c3b3..32a8d5e 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -354,9 +354,9 @@ class HeroicSource(URLExecutableSource): config_location = Location( schema_key="heroic-location", candidates=( - shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", shared.config_dir / "heroic", shared.home / ".config" / "heroic", + shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", shared.appdata_dir / "heroic", ), paths={ From 45877209342eab5d1b1436c8313e95227d1bd9b6 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 21:06:04 +0200 Subject: [PATCH 32/59] using cached_property for sub-source paths --- src/importer/sources/heroic_source.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 32a8d5e..8673aaa 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -26,6 +26,7 @@ from json import JSONDecodeError from pathlib import Path from time import time from typing import Iterable, Optional, TypedDict +from functools import cached_property from src import shared from src.game import Game @@ -84,7 +85,7 @@ class SubSourceIterable(Iterable): self.source = source self.source_iterable = source_iterable - @property + @cached_property def library_path(self) -> Path: path = self.source.config_location.root / self.relative_library_path logging.debug("Using Heroic %s library.json path %s", self.name, path) @@ -156,7 +157,7 @@ class StoreSubSourceIterable(SubSourceIterable): relative_installed_path: Path installed_app_names: set[str] - @property + @cached_property def installed_path(self) -> Path: path = self.source.config_location.root / self.relative_installed_path logging.debug("Using Heroic %s installed.json path %s", self.name, path) @@ -215,7 +216,7 @@ class LegendaryIterable(StoreSubSourceIterable): # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" # ) - @property + @cached_property def installed_path(self) -> Path: """ Get the right path depending on the Heroic version From da777d3605968ac24b68e8873d72fc8df37b75a6 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 21:06:30 +0200 Subject: [PATCH 33/59] Permission for heroic flatpak's legendary files --- flatpak/hu.kramo.Cartridges.Devel.json | 1 + 1 file changed, 1 insertion(+) diff --git a/flatpak/hu.kramo.Cartridges.Devel.json b/flatpak/hu.kramo.Cartridges.Devel.json index 1c2ecea..d0dc5b9 100644 --- a/flatpak/hu.kramo.Cartridges.Devel.json +++ b/flatpak/hu.kramo.Cartridges.Devel.json @@ -15,6 +15,7 @@ "--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro", "--filesystem=~/.var/app/net.lutris.Lutris/:ro", "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", + "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/legendary/:ro", "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro", "--filesystem=~/.var/app/io.itch.itch/config/itch/:ro", "--filesystem=/var/lib/flatpak:ro" From b1992a9466ded8f77b657156195b75785ed9d73f Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Fri, 21 Jul 2023 14:39:07 +0200 Subject: [PATCH 34/59] Fix heroic legendary path detection --- src/importer/sources/heroic_source.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 8673aaa..acd16f5 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -227,23 +227,18 @@ class LegendaryIterable(StoreSubSourceIterable): """ heroic_config_path = self.source.config_location.root + # Heroic >= 2.9 if (path := heroic_config_path / "legendaryConfig").is_dir(): - # Heroic >= 2.9 logging.debug("Using Heroic >= 2.9 legendary file") + # Heroic <= 2.8 + elif heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + logging.debug("Using Heroic flatpak <= 2.8 legendary file") else: - # Heroic <= 2.8 - if heroic_config_path.is_relative_to(shared.flatpak_dir): - # Heroic flatpak - path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" - logging.debug("Using Heroic flatpak <= 2.8 legendary file") - elif shared.config_dir.is_relative_to(shared.flatpak_dir): - # Heroic native (from Cartridges flatpak) - logging.debug("Using Heroic native <= 2.8 legendary file") - path = Path.home() / ".config" - else: - # Heroic native (from other Cartridges installations) - logging.debug("Using Heroic native <= 2.8 legendary file") - path = shared.config_dir + # Heroic native + logging.debug("Using Heroic native <= 2.8 legendary file") + path = Path.home() / ".config" path = path / "legendary" / "installed.json" logging.debug("Using Heroic %s installed.json path %s", self.name, path) From fbf076660d6eaabb2b2b2c0bb69956c738037486 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sat, 22 Jul 2023 00:04:02 +0200 Subject: [PATCH 35/59] Better heroic store file parsing --- src/importer/sources/heroic_source.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index acd16f5..73461e8 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -307,11 +307,14 @@ class HeroicSourceIterable(SourceIterable): # Get the hidden app names try: store = path_json_load(self.source.config_location["store_config.json"]) - if "hidden" in store["games"].keys(): - self.hidden_app_names = { - game["appName"] for game in store["games"]["hidden"] - } - except (OSError, JSONDecodeError, KeyError, TypeError) as error: + self.hidden_app_names = { + app_name + for game in store["games"]["hidden"] + if (app_name := game.get("appName")) is not None + } + except KeyError: + logging.warning('No ["games"]["hidden"] key in Heroic store file') + except (OSError, JSONDecodeError, TypeError) as error: logging.error("Invalid Heroic store file", exc_info=error) raise InvalidStoreFileError() from error From 7bcb113a3377353b80a127d6dde5b015f3a2e4db Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sat, 22 Jul 2023 00:06:16 +0200 Subject: [PATCH 36/59] extracted get_hidden_app_names to a method --- src/importer/sources/heroic_source.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 73461e8..d169129 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -301,10 +301,12 @@ class HeroicSourceIterable(SourceIterable): def is_hidden(self, app_name: str) -> bool: return app_name in self.hidden_app_names - def __iter__(self): - """Generator method producing games from all the Heroic sub-sources""" + def get_hidden_app_names(self) -> set[str]: + """Get the hidden app names from store/config.json + + :raises InvalidStoreFileError: if the store is invalid for some reason + """ - # Get the hidden app names try: store = path_json_load(self.source.config_location["store_config.json"]) self.hidden_app_names = { @@ -318,6 +320,11 @@ class HeroicSourceIterable(SourceIterable): logging.error("Invalid Heroic store file", exc_info=error) raise InvalidStoreFileError() from error + def __iter__(self): + """Generator method producing games from all the Heroic sub-sources""" + + self.get_hidden_app_names() + # Get games from the sub sources for sub_source_class in ( SideloadIterable, From 8eca19d9a19a2d586e5faa75c7b1828ba8ff23b6 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 23 Jul 2023 17:11:14 +0100 Subject: [PATCH 37/59] Changes - Add Windows support - Add Steam RetroArch support - Add support for custom playlist and thumbnail directories --- src/importer/sources/retroarch_source.py | 64 ++++++++++++++++++++---- src/shared.py.in | 1 + 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 0e7c3a9..0960368 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -19,9 +19,10 @@ import json import logging +import re from hashlib import md5 from json import JSONDecodeError -from re import sub +from pathlib import Path from time import time from src import shared @@ -29,19 +30,39 @@ from src.errors.friendly_error import FriendlyError from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.steam_source import SteamSource class RetroarchSourceIterator(SourceIterator): source: "RetroarchSource" + def get_config_value(self, key: str, config_data: str): + for item in re.findall(f'{key}\\s*=\\s*"(.*)"\n', config_data, re.IGNORECASE): + logging.debug(str(item)) + return item + + raise KeyError(f"Key not found in RetroArch config: {key}") + def generator_builder(self) -> SourceIterationResult: added_time = int(time()) bad_playlists = set() + config_file = self.source.config_location["config"] + with config_file.open(encoding="utf-8") as open_file: + config_data = open_file.read() + + playlist_folder = Path( + self.get_config_value("playlist_directory", config_data) + ).expanduser() + thumbnail_folder = Path( + self.get_config_value("thumbnails_directory", config_data) + ).expanduser() + # Get all playlist files, ending in .lpl - playlist_files = self.source.config_location["playlists"].glob("*.lpl") + playlist_files = playlist_folder.glob("*.lpl") for playlist_file in playlist_files: + logging.debug(playlist_file) try: with playlist_file.open( encoding="utf-8", @@ -84,10 +105,10 @@ class RetroarchSourceIterator(SourceIterator): # Get boxart boxart_image_name = item["label"] + ".png" - boxart_image_name = sub(r"[&\*\/:`<>\?\\\|]", "_", boxart_image_name) + boxart_image_name = re.sub(r"[&\*\/:`<>\?\\\|]", "_", boxart_image_name) boxart_folder_name = playlist_file.stem image_path = ( - self.source.config_location["thumbnails"] + thumbnail_folder / boxart_folder_name / "Named_Boxarts" / boxart_image_name @@ -107,20 +128,45 @@ class RetroarchSourceIterator(SourceIterator): class RetroarchSource(Source): + def __init__(self) -> None: + super().__init__() + # Find steam location + libraryfolders = SteamSource().data_location["libraryfolders.vdf"] + library_path = "" + + with open(libraryfolders, "r", encoding="utf-8") as open_file: + for line in open_file: + if '"path"' in line: + library_path = re.findall( + '"path"\\s+"(.*)"\n', line, re.IGNORECASE + )[0] + elif "1118310" in line: + break + + if library_path: + self.config_location.candidates.append( + Path(f"{library_path}/steamapps/common/RetroArch") + ) + name = _("RetroArch") - available_on = {"linux"} + available_on = {"linux", "windows"} iterator_class = RetroarchSourceIterator config_location = Location( schema_key="retroarch-location", - candidates=( + candidates=[ shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", shared.config_dir / "retroarch", shared.home / ".config" / "retroarch", - ), + Path("C:\\RetroArch-Win64"), + Path("C:\\RetroArch-Win32"), + shared.local_appdata_dir + / "Packages" + / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" + / "LocalState", + ], paths={ - "playlists": (True, "playlists"), - "thumbnails": (True, "thumbnails"), + "config": (False, "retroarch.cfg"), }, ) diff --git a/src/shared.py.in b/src/shared.py.in index 92fc574..91e7b6d 100644 --- a/src/shared.py.in +++ b/src/shared.py.in @@ -41,6 +41,7 @@ games_dir = data_dir / "cartridges" / "games" covers_dir = data_dir / "cartridges" / "covers" appdata_dir = Path(os.getenv("appdata") or "C:\\Users\\Default\\AppData\\Roaming") +local_appdata_dir = Path(os.getenv("csidl_local_appdata") or "C:\\Users\\Default\\AppData\\Local") programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)") scale_factor = max( From 311ed3b09c872929adebf184de46f2879a2be655 Mon Sep 17 00:00:00 2001 From: Rilic Date: Sun, 23 Jul 2023 17:21:24 +0100 Subject: [PATCH 38/59] Refactoring and error checks --- src/importer/sources/retroarch_source.py | 49 ++++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 0960368..6481e90 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -28,7 +28,7 @@ from time import time from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, UnresolvableLocationError from src.importer.sources.source import Source, SourceIterationResult, SourceIterator from src.importer.sources.steam_source import SteamSource @@ -128,26 +128,6 @@ class RetroarchSourceIterator(SourceIterator): class RetroarchSource(Source): - def __init__(self) -> None: - super().__init__() - # Find steam location - libraryfolders = SteamSource().data_location["libraryfolders.vdf"] - library_path = "" - - with open(libraryfolders, "r", encoding="utf-8") as open_file: - for line in open_file: - if '"path"' in line: - library_path = re.findall( - '"path"\\s+"(.*)"\n', line, re.IGNORECASE - )[0] - elif "1118310" in line: - break - - if library_path: - self.config_location.candidates.append( - Path(f"{library_path}/steamapps/common/RetroArch") - ) - name = _("RetroArch") available_on = {"linux", "windows"} iterator_class = RetroarchSourceIterator @@ -177,3 +157,30 @@ class RetroarchSource(Source): base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" args = '-L "{core_path}" "{rom_path}"' return f"{base} {args}" + + def __init__(self) -> None: + super().__init__() + + try: + self.config_location.candidates.append(self.get_steam_location()) + except (OSError, KeyError, UnresolvableLocationError): + pass + + def get_steam_location(self) -> str: + # Find steam location + libraryfolders = SteamSource().data_location["libraryfolders.vdf"] + library_path = "" + + with open(libraryfolders, "r", encoding="utf-8") as open_file: + for line in open_file: + if '"path"' in line: + library_path = re.findall( + '"path"\\s+"(.*)"\n', line, re.IGNORECASE + )[0] + elif "1118310" in line: + break + + if library_path: + return Path(f"{library_path}/steamapps/common/RetroArch") + + raise ValueError("No Steam RetroArch installed.") From b8cd1fd74147036dd1f4b97fdc452eadb4869da3 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 25 Jul 2023 20:29:01 +0200 Subject: [PATCH 39/59] Translations update from Hosted Weblate (#152) * Translated using Weblate (Italian) Currently translated at 100.0% (121 of 121 strings) Co-authored-by: Giasko Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/it/ Translation: Cartridges/Cartridges * Translated using Weblate (Czech) Currently translated at 100.0% (121 of 121 strings) Added translation using Weblate (Czech) Co-authored-by: Hosted Weblate Co-authored-by: foo expert Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/cs/ Translation: Cartridges/Cartridges * Translated using Weblate (Polish) Currently translated at 100.0% (121 of 121 strings) Co-authored-by: Michaks Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/pl/ Translation: Cartridges/Cartridges * Translated using Weblate (French) Currently translated at 100.0% (121 of 121 strings) Co-authored-by: rene-coty Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/fr/ Translation: Cartridges/Cartridges * Update translation files Updated by "Squash Git commits" hook in Weblate. Translation: Cartridges/Cartridges Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ --------- Co-authored-by: Giasko Co-authored-by: foo expert Co-authored-by: Michaks Co-authored-by: rene-coty --- po/LINGUAS | 1 + po/cs.po | 553 +++++++++++++++++++++++++++++++++++++++++++++++++++++ po/fr.po | 10 +- po/it.po | 9 +- po/pl.po | 35 ++-- 5 files changed, 582 insertions(+), 26 deletions(-) create mode 100644 po/cs.po diff --git a/po/LINGUAS b/po/LINGUAS index 88aad50..799d587 100644 --- a/po/LINGUAS +++ b/po/LINGUAS @@ -19,3 +19,4 @@ pl sv tr el +cs diff --git a/po/cs.po b/po/cs.po new file mode 100644 index 0000000..8863bde --- /dev/null +++ b/po/cs.po @@ -0,0 +1,553 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR kramo +# This file is distributed under the same license as the Cartridges package. +# foo expert , 2023. +msgid "" +msgstr "" +"Project-Id-Version: Cartridges\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"PO-Revision-Date: 2023-07-24 13:05+0000\n" +"Last-Translator: foo expert \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Weblate 5.0-dev\n" + +#: data/hu.kramo.Cartridges.desktop.in:3 +#: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 +#: src/main.py:162 +msgid "Cartridges" +msgstr "Kazety" + +#: data/hu.kramo.Cartridges.desktop.in:4 +msgid "Game Launcher" +msgstr "Spouštěč her" + +#: data/hu.kramo.Cartridges.desktop.in:5 +#: data/hu.kramo.Cartridges.metainfo.xml.in:7 +msgid "Launch all your games" +msgstr "Spusťte všechny vaše hry" + +#: data/hu.kramo.Cartridges.desktop.in:11 +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgstr "hraní;spouštěč;steam;lutris;heroic;láhve;itch;" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:9 +msgid "" +"Cartridges is a simple game launcher for all of your games. It has support " +"for importing games from Steam, Lutris, Heroic and more with no login " +"necessary. You can sort and hide games or download cover art from " +"SteamGridDB." +msgstr "" +"Kazety jsou jednoduchý spouštěč pro všechny vaše hry. Podporuje importovaní " +"her ze služeb Steam, Lutris, Heroic a dalších bez nutnosti přihlášení. Hry " +"můžete třídit a skrývat nebo stahovat obálky ze služby SteamGridDB." + +#: data/hu.kramo.Cartridges.metainfo.xml.in:30 +msgid "Library" +msgstr "Knihovna" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 +msgid "Edit Game Details" +msgstr "Upravit podrobnosti o hře" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 +msgid "Game Details" +msgstr "Podrobnosti o hře" + +#: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 +#: src/details_window.py:239 +msgid "Preferences" +msgstr "Předvolby" + +#: data/gtk/details-window.blp:25 +msgid "Cancel" +msgstr "Zrušit" + +#: data/gtk/details-window.blp:57 +msgid "New Cover" +msgstr "Nový obal" + +#: data/gtk/details-window.blp:75 +msgid "Delete Cover" +msgstr "Odstranit obal" + +#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 +#: data/gtk/game.blp:80 +msgid "Title" +msgstr "Název" + +#: data/gtk/details-window.blp:102 +msgid "The title of the game" +msgstr "Název hry" + +#: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 +msgid "Developer" +msgstr "Vývojář" + +#: data/gtk/details-window.blp:113 +msgid "The developer or publisher (optional)" +msgstr "Vývojář nebo vydavatel (nepovinné)" + +#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155 +msgid "Executable" +msgstr "Spustitelný soubor" + +#: data/gtk/details-window.blp:124 +msgid "File to open or command to run when launching the game" +msgstr "Soubor nebo příkaz pro spuštění hry" + +#: data/gtk/details-window.blp:130 +msgid "More Info" +msgstr "Více informací" + +#: data/gtk/game.blp:102 data/gtk/game.blp:121 data/gtk/window.blp:195 +msgid "Edit" +msgstr "Upravit" + +#: data/gtk/game.blp:107 src/window.py:171 +msgid "Hide" +msgstr "Skrýt" + +#: data/gtk/game.blp:112 data/gtk/game.blp:131 data/gtk/preferences.blp:56 +#: data/gtk/window.blp:209 +msgid "Remove" +msgstr "Odstranit" + +#: data/gtk/game.blp:126 src/window.py:173 +msgid "Unhide" +msgstr "Odkrýt" + +#: data/gtk/help-overlay.blp:11 data/gtk/preferences.blp:9 +msgid "General" +msgstr "Obecné" + +#: data/gtk/help-overlay.blp:14 +msgid "Quit" +msgstr "Ukončit" + +#: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257 +#: data/gtk/window.blp:323 +msgid "Search" +msgstr "Vyhledávání" + +#: data/gtk/help-overlay.blp:24 +msgid "Show preferences" +msgstr "Zobrazit předvolby" + +#: data/gtk/help-overlay.blp:29 +msgid "Shortcuts" +msgstr "Zkratky" + +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +msgid "Undo" +msgstr "Zpět" + +#: data/gtk/help-overlay.blp:39 +msgid "Open menu" +msgstr "Otevřít nabídku" + +#: data/gtk/help-overlay.blp:45 +msgid "Games" +msgstr "Hry" + +#: data/gtk/help-overlay.blp:48 +msgid "Add new game" +msgstr "Přidat novou hru" + +#: data/gtk/help-overlay.blp:53 +msgid "Import games" +msgstr "Importovat hry" + +#: data/gtk/help-overlay.blp:58 +msgid "Show hidden games" +msgstr "Zobrazit skryté hry" + +#: data/gtk/help-overlay.blp:63 +msgid "Remove game" +msgstr "Odstranit hru" + +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +msgid "Behavior" +msgstr "Chování" + +#: data/gtk/preferences.blp:16 +msgid "Exit After Launching Games" +msgstr "Ukončit po spuštění her" + +#: data/gtk/preferences.blp:25 +msgid "Cover Image Launches Game" +msgstr "Obrázek na obálce spouští hru" + +#: data/gtk/preferences.blp:26 +msgid "Swaps the behavior of the cover image and the play button" +msgstr "Vymění chování obrázku na obálce a tlačítka pro přehrávání" + +#: data/gtk/preferences.blp:36 src/details_window.py:81 +msgid "Images" +msgstr "Obrázky" + +#: data/gtk/preferences.blp:39 +msgid "High Quality Images" +msgstr "Vysoce kvalitní obrázky" + +#: data/gtk/preferences.blp:40 +msgid "Save game covers losslessly at the cost of storage" +msgstr "Ukládat obaly her bezztrátově na úkor většího místa na disku" + +#: data/gtk/preferences.blp:50 +msgid "Danger Zone" +msgstr "Nebezpečná zóna" + +#: data/gtk/preferences.blp:53 +msgid "Remove All Games" +msgstr "Odstranit všechny hry" + +#: data/gtk/preferences.blp:85 data/gtk/window.blp:27 data/gtk/window.blp:442 +msgid "Import" +msgstr "Import" + +#: data/gtk/preferences.blp:89 +msgid "Sources" +msgstr "Zdroje" + +#: data/gtk/preferences.blp:92 +msgid "Steam" +msgstr "Steam" + +#: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 +#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 +#: data/gtk/preferences.blp:234 +msgid "Install Location" +msgstr "Umístění instalace" + +#: data/gtk/preferences.blp:106 +msgid "Lutris" +msgstr "Lutris" + +#: data/gtk/preferences.blp:119 +msgid "Cache Location" +msgstr "Umístění dočasných souborů" + +#: data/gtk/preferences.blp:128 +msgid "Import Steam Games" +msgstr "Importovat Steam hry" + +#: data/gtk/preferences.blp:137 +msgid "Import Flatpak Games" +msgstr "Importovat Flatpak hry" + +#: data/gtk/preferences.blp:147 +msgid "Heroic" +msgstr "Heroic" + +#: data/gtk/preferences.blp:160 +msgid "Import Epic Games" +msgstr "Importovat Epic Games hry" + +#: data/gtk/preferences.blp:169 +msgid "Import GOG Games" +msgstr "Importovat GOG hry" + +#: data/gtk/preferences.blp:178 +msgid "Import Sideloaded Games" +msgstr "Importovat ručně načtené hry" + +#: data/gtk/preferences.blp:188 +msgid "Bottles" +msgstr "Láhve" + +#: data/gtk/preferences.blp:202 +msgid "itch" +msgstr "itch" + +#: data/gtk/preferences.blp:216 +msgid "Legendary" +msgstr "Legendary" + +#: data/gtk/preferences.blp:230 +msgid "Flatpak" +msgstr "Flatpak" + +#: data/gtk/preferences.blp:243 +msgid "Import Game Launchers" +msgstr "Importovat spouštěče her" + +#: data/gtk/preferences.blp:256 +msgid "SteamGridDB" +msgstr "SteamGridDB" + +#: data/gtk/preferences.blp:260 +msgid "Authentication" +msgstr "Ověření" + +#: data/gtk/preferences.blp:263 +msgid "API Key" +msgstr "Klíč API" + +#: data/gtk/preferences.blp:271 +msgid "Use SteamGridDB" +msgstr "Používat SteamGridDB" + +#: data/gtk/preferences.blp:272 +msgid "Download images when adding or importing games" +msgstr "Stahovat obrázky při přidávání nebo importování her" + +#: data/gtk/preferences.blp:281 +msgid "Prefer Over Official Images" +msgstr "Upřednostnit před oficiálními obrázky" + +#: data/gtk/preferences.blp:290 +msgid "Prefer Animated Images" +msgstr "Upřednostnit animované obrázky" + +#: data/gtk/window.blp:6 data/gtk/window.blp:14 +msgid "No Games Found" +msgstr "Nebyly nalezeny žádné hry" + +#: data/gtk/window.blp:7 data/gtk/window.blp:15 +msgid "Try a different search." +msgstr "Zkuste hledat něco jiného." + +#: data/gtk/window.blp:21 +msgid "No Games" +msgstr "Žádné hry" + +#: data/gtk/window.blp:22 +msgid "Use the + button to add games." +msgstr "Tlačítkem + můžete přidávat hry." + +#: data/gtk/window.blp:40 +msgid "No Hidden Games" +msgstr "Žádné skryté hry" + +#: data/gtk/window.blp:41 +msgid "Games you hide will appear here." +msgstr "Hry, které skryjete, se zobrazí zde." + +#: data/gtk/window.blp:64 data/gtk/window.blp:304 +msgid "Back" +msgstr "Zpět" + +#: data/gtk/window.blp:121 +msgid "Game Title" +msgstr "Název hry" + +#: data/gtk/window.blp:176 +msgid "Play" +msgstr "Hrát" + +#: data/gtk/window.blp:243 data/gtk/window.blp:435 +msgid "Add Game" +msgstr "Přidat hru" + +#: data/gtk/window.blp:250 data/gtk/window.blp:316 +msgid "Main Menu" +msgstr "Hlavní nabídka" + +#: data/gtk/window.blp:311 +msgid "Hidden Games" +msgstr "Skryté hry" + +#: data/gtk/window.blp:374 +msgid "Sort" +msgstr "Třídit" + +#: data/gtk/window.blp:377 +msgid "A-Z" +msgstr "A-Ž" + +#: data/gtk/window.blp:383 +msgid "Z-A" +msgstr "Ž-A" + +#: data/gtk/window.blp:389 +msgid "Newest" +msgstr "Nejnovější" + +#: data/gtk/window.blp:395 +msgid "Oldest" +msgstr "Nejstarší" + +#: data/gtk/window.blp:401 +msgid "Last Played" +msgstr "Naposledy hráno" + +#: data/gtk/window.blp:408 +msgid "Show Hidden" +msgstr "Zobrazit Skryté" + +#: data/gtk/window.blp:421 +msgid "Keyboard Shortcuts" +msgstr "Klávesové zkratky" + +#: data/gtk/window.blp:426 +msgid "About Cartridges" +msgstr "O Kazetách" + +#. Translators: Replace this with your name for it to show up in the about window +#: src/main.py:180 +msgid "translator_credits" +msgstr "ooo.i.love.foo" + +#. The variable is the date when the game was added +#: src/window.py:194 +msgid "Added: {}" +msgstr "Přidáno: {}" + +#: src/window.py:197 +msgid "Never" +msgstr "Nikdy" + +#. The variable is the date when the game was last played +#: src/window.py:201 +msgid "Last played: {}" +msgstr "Naposledy hráno: {}" + +#: src/details_window.py:72 +msgid "Apply" +msgstr "Použít" + +#: src/details_window.py:78 +msgid "Add New Game" +msgstr "Přidat novou hru" + +#: src/details_window.py:79 +msgid "Confirm" +msgstr "Potvrdit" + +#. Translate this string as you would translate "file" +#: src/details_window.py:92 +msgid "file.txt" +msgstr "soubor.txt" + +#. As in software +#: src/details_window.py:94 +msgid "program" +msgstr "program" + +#. Translate this string as you would translate "path to {}" +#: src/details_window.py:99 src/details_window.py:101 +msgid "C:\\path\\to\\{}" +msgstr "C:\\cesta\\k\\{}" + +#. Translate this string as you would translate "path to {}" +#: src/details_window.py:105 src/details_window.py:107 +msgid "/path/to/{}" +msgstr "/cesta/k/{}" + +#: src/details_window.py:112 +msgid "" +"To launch the executable \"{}\", use the command:\n" +"\n" +"\"{}\"\n" +"\n" +"To open the file \"{}\" with the default application, use:\n" +"\n" +"{} \"{}\"\n" +"\n" +"If the path contains spaces, make sure to wrap it in double quotes!" +msgstr "" +"Chcete-li spustit spustitelný soubor \"{}\", použijte příkaz:\n" +"\n" +"\"{}\"\n" +"\n" +"Chcete-li otevřít soubor \"{}\" pomocí výchozí aplikace, použijte příkaz:\n" +"\n" +"{} \"{}\"\n" +"\n" +"Pokud cesta obsahuje mezery, nezapomeňte ji zabalit do dvojitých uvozovek!" + +#: src/details_window.py:147 src/details_window.py:153 +msgid "Couldn't Add Game" +msgstr "Nelze přidat hru" + +#: src/details_window.py:147 src/details_window.py:181 +msgid "Game title cannot be empty." +msgstr "Název hry nemůže být prázdný." + +#: src/details_window.py:153 src/details_window.py:189 +msgid "Executable cannot be empty." +msgstr "Spustitelný soubor nemůže být prázdný." + +#: src/details_window.py:180 src/details_window.py:188 +msgid "Couldn't Apply Preferences" +msgstr "Nelze použít předvolby" + +#. The variable is the title of the game +#: src/game.py:138 +msgid "{} launched" +msgstr "{} spuštěno" + +#. The variable is the title of the game +#: src/game.py:152 +msgid "{} hidden" +msgstr "{} skryto" + +#: src/game.py:152 +msgid "{} unhidden" +msgstr "{} odkryto" + +#: src/game.py:169 +msgid "{} removed" +msgstr "{} odstraněno" + +#: src/preferences.py:111 +msgid "All games removed" +msgstr "Všechny hry odstraněny" + +#: src/preferences.py:159 +msgid "" +"An API key is required to use SteamGridDB. You can generate one {}here{}." +msgstr "" +"K používání služby SteamGridDB je vyžadován klíč API. Můžete si ho " +"vygenerovat {}zde{}." + +#: src/preferences.py:284 +msgid "Installation Not Found" +msgstr "Instalace nebyla nalezena" + +#: src/preferences.py:286 +msgid "Select a valid directory." +msgstr "Vyberte platný adresář." + +#: src/preferences.py:348 +msgid "Invalid Directory" +msgstr "Neplatný adresář" + +#. The variable is the name of the source +#: src/preferences.py:352 +msgid "Select the {} cache directory." +msgstr "Vyberte adresář {} mezipaměti." + +#. The variable is the name of the source +#: src/preferences.py:355 +msgid "Select the {} configuration directory." +msgstr "Vyberte konfigurační adresář {}." + +#. The variable is the name of the source +#: src/preferences.py:358 +msgid "Select the {} data directory." +msgstr "Vyberte datový adresář {}." + +#: src/preferences.py:364 +msgid "Set Location" +msgstr "Nastavit umístění" + +#: src/utils/create_dialog.py:25 +msgid "Dismiss" +msgstr "Zahodit" + +#: src/store/managers/sgdb_manager.py:47 +msgid "Couldn't Authenticate SteamGridDB" +msgstr "Nelze ověřit SteamGridDB" + +#: src/store/managers/sgdb_manager.py:48 +msgid "Verify your API key in preferences" +msgstr "Ověřte váš klíč API v předvolbách" diff --git a/po/fr.po b/po/fr.po index d2bce77..6596b30 100644 --- a/po/fr.po +++ b/po/fr.po @@ -12,8 +12,8 @@ msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-07-05 14:36+0200\n" -"PO-Revision-Date: 2023-07-05 13:13+0000\n" -"Last-Translator: Geoffrey Coulaud \n" +"PO-Revision-Date: 2023-07-24 13:05+0000\n" +"Last-Translator: rene-coty \n" "Language-Team: French \n" "Language: fr\n" @@ -27,7 +27,7 @@ msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 #: src/main.py:162 msgid "Cartridges" -msgstr "Cartridges" +msgstr "Cartouches" #: data/hu.kramo.Cartridges.desktop.in:4 msgid "Game Launcher" @@ -49,7 +49,7 @@ msgid "" "necessary. You can sort and hide games or download cover art from " "SteamGridDB." msgstr "" -"Cartridges est un lanceur de jeux simple pour tous vos jeux. Il prend en " +"Cartouches est un lanceur de jeux simple pour tous vos jeux. Il prend en " "charge l’importation des jeux depuis Steam, Lutris, Heroic et d’autres " "encore, sans nécessiter de connexion. Vous pouvez trier et masquer les jeux " "ou télécharger la pochette depuis SteamGridDB." @@ -398,7 +398,7 @@ msgstr "Raccourcis clavier" #: data/gtk/window.blp:426 msgid "About Cartridges" -msgstr "À propos de Cartridges" +msgstr "À propos de Cartouches" #. Translators: Replace this with your name for it to show up in the about window #: src/main.py:180 diff --git a/po/it.po b/po/it.po index 09e83ae..a2786e7 100644 --- a/po/it.po +++ b/po/it.po @@ -4,13 +4,14 @@ # Alessandro Iepure , 2023. # albanobattistella , 2023. # kramo , 2023. +# Giasko , 2023. msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-07-05 14:36+0200\n" -"PO-Revision-Date: 2023-07-08 14:52+0000\n" -"Last-Translator: Alessandro Iepure \n" +"PO-Revision-Date: 2023-07-21 12:16+0000\n" +"Last-Translator: Giasko \n" "Language-Team: Italian \n" "Language: it\n" @@ -132,7 +133,7 @@ msgstr "Generale" #: data/gtk/help-overlay.blp:14 msgid "Quit" -msgstr "Chiudi" +msgstr "Esci" #: data/gtk/help-overlay.blp:19 data/gtk/window.blp:217 data/gtk/window.blp:257 #: data/gtk/window.blp:323 @@ -273,7 +274,7 @@ msgstr "itch" #: data/gtk/preferences.blp:216 msgid "Legendary" -msgstr "Leggendario" +msgstr "Legendary" #: data/gtk/preferences.blp:230 msgid "Flatpak" diff --git a/po/pl.po b/po/pl.po index c22be3a..a5211e0 100644 --- a/po/pl.po +++ b/po/pl.po @@ -4,13 +4,14 @@ # Artur Wróblewski , 2023. # Kshyso , 2023. # Eryk Michalak , 2023. +# Michaks , 2023. msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-07-05 14:36+0200\n" -"PO-Revision-Date: 2023-07-14 15:51+0000\n" -"Last-Translator: Eryk Michalak \n" +"PO-Revision-Date: 2023-07-24 13:05+0000\n" +"Last-Translator: Michaks \n" "Language-Team: Polish \n" "Language: pl\n" @@ -25,7 +26,7 @@ msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 #: src/main.py:162 msgid "Cartridges" -msgstr "Cartridges" +msgstr "Kartridże" #: data/hu.kramo.Cartridges.desktop.in:4 msgid "Game Launcher" @@ -57,16 +58,16 @@ msgstr "Biblioteka" #: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 msgid "Edit Game Details" -msgstr "Edytuj detale gry" +msgstr "Edycja szczegółów gry" #: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 msgid "Game Details" -msgstr "Detale gry" +msgstr "Szczegóły gry" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 #: src/details_window.py:239 msgid "Preferences" -msgstr "Ustawienia" +msgstr "Preferencje" #: data/gtk/details-window.blp:25 msgid "Cancel" @@ -74,11 +75,11 @@ msgstr "Anuluj" #: data/gtk/details-window.blp:57 msgid "New Cover" -msgstr "Nowa Okładka" +msgstr "Nowa okładka" #: data/gtk/details-window.blp:75 msgid "Delete Cover" -msgstr "Usuń Okładkę" +msgstr "Usuń osłonę" #: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 #: data/gtk/game.blp:80 @@ -87,7 +88,7 @@ msgstr "Tytuł" #: data/gtk/details-window.blp:102 msgid "The title of the game" -msgstr "Tytuł gry" +msgstr "Tytuł Gry" #: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 msgid "Developer" @@ -150,7 +151,7 @@ msgstr "Skróty" #: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 msgid "Undo" -msgstr "Cofnij" +msgstr "Wróć" #: data/gtk/help-overlay.blp:39 msgid "Open menu" @@ -162,7 +163,7 @@ msgstr "Gry" #: data/gtk/help-overlay.blp:48 msgid "Add new game" -msgstr "Dodaj nową grę" +msgstr "Dodaj nową gre" #: data/gtk/help-overlay.blp:53 msgid "Import games" @@ -186,7 +187,7 @@ msgstr "Wyjdź po uruchomieniu gry" #: data/gtk/preferences.blp:25 msgid "Cover Image Launches Game" -msgstr "Obraz okładki startera gier" +msgstr "Obraz okładki uruchamia grę" #: data/gtk/preferences.blp:26 msgid "Swaps the behavior of the cover image and the play button" @@ -337,7 +338,7 @@ msgstr "Gry, które ukryjesz, pojawią się tutaj." #: data/gtk/window.blp:64 data/gtk/window.blp:304 msgid "Back" -msgstr "Cofnij" +msgstr "Powrót" #: data/gtk/window.blp:121 msgid "Game Title" @@ -345,7 +346,7 @@ msgstr "Tytuł gry" #: data/gtk/window.blp:176 msgid "Play" -msgstr "Uruchom" +msgstr "Graj" #: data/gtk/window.blp:243 data/gtk/window.blp:435 msgid "Add Game" @@ -393,7 +394,7 @@ msgstr "Skróty klawiaturowe" #: data/gtk/window.blp:426 msgid "About Cartridges" -msgstr "O Cartridges" +msgstr "O Kartridżach" #. Translators: Replace this with your name for it to show up in the about window #: src/main.py:180 @@ -420,7 +421,7 @@ msgstr "Zastosuj" #: src/details_window.py:78 msgid "Add New Game" -msgstr "Dodaj nową grę" +msgstr "Dodaj nową Grę" #: src/details_window.py:79 msgid "Confirm" @@ -542,7 +543,7 @@ msgstr "Wybierz katalog z danymi {}." #: src/preferences.py:364 msgid "Set Location" -msgstr "Ustaw lokacje" +msgstr "Ustaw położenie" #: src/utils/create_dialog.py:25 msgid "Dismiss" From ca73023bc9008836f6fdd78f0a68a7ab16befaee Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Tue, 25 Jul 2023 20:32:46 +0200 Subject: [PATCH 40/59] v2.1 --- data/hu.kramo.Cartridges.metainfo.xml.in | 5 +++-- meson.build | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/hu.kramo.Cartridges.metainfo.xml.in b/data/hu.kramo.Cartridges.metainfo.xml.in index e0eba2d..24cb18b 100644 --- a/data/hu.kramo.Cartridges.metainfo.xml.in +++ b/data/hu.kramo.Cartridges.metainfo.xml.in @@ -44,10 +44,11 @@ - +
    -
  • Fixes an issue with Steam mods not importing properly
  • +
  • Added support for Amazon Games in the Heroic importer
  • +
  • Translations since 2.0
diff --git a/meson.build b/meson.build index 94a902a..2b2ea32 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cartridges', - version: '2.0.6', + version: '2.1', meson_version: '>= 0.59.0', default_options: [ 'warning_level=2', 'werror=false', ], ) From 0b577d2480198561d0eca8f4c3e569c04b7b4435 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Tue, 25 Jul 2023 20:34:35 +0200 Subject: [PATCH 41/59] Update translations --- po/cartridges.pot | 72 +++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/po/cartridges.pot b/po/cartridges.pot index 1aa2a79..6943828 100644 --- a/po/cartridges.pot +++ b/po/cartridges.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,7 +19,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "" @@ -33,7 +33,7 @@ msgid "Launch all your games" msgstr "" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -57,7 +57,7 @@ msgid "Game Details" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "" @@ -140,7 +140,7 @@ msgstr "" msgid "Shortcuts" msgstr "" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "" @@ -168,7 +168,7 @@ msgstr "" msgid "Remove game" msgstr "" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "" @@ -217,9 +217,9 @@ msgid "Steam" msgstr "" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "" @@ -252,54 +252,58 @@ msgid "Import GOG Games" msgstr "" #: data/gtk/preferences.blp:178 +msgid "Import Amazon Games" +msgstr "" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "" @@ -388,7 +392,7 @@ msgid "About Cartridges" msgstr "" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "" @@ -455,15 +459,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "" -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "" -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "" @@ -485,43 +489,43 @@ msgstr "" msgid "{} removed" msgstr "" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "" -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "" #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "" #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "" -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "" From f3dcdbf0d2df0b37f12c90d716e482b08443b421 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 26 Jul 2023 03:53:17 +0200 Subject: [PATCH 42/59] Using a named tuple to store source locations --- src/importer/sources/bottles_source.py | 35 +++++++++------- src/importer/sources/flatpak_source.py | 31 ++++++++------ src/importer/sources/heroic_source.py | 45 ++++++++++++-------- src/importer/sources/itch_source.py | 27 +++++++----- src/importer/sources/legendary_source.py | 33 +++++++++------ src/importer/sources/lutris_source.py | 53 ++++++++++++++---------- src/importer/sources/source.py | 8 ++-- src/importer/sources/steam_source.py | 36 +++++++++------- src/preferences.py | 22 ++++++---- 9 files changed, 172 insertions(+), 118 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 8829023..42eebaa 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -20,6 +20,7 @@ from pathlib import Path from time import time +from typing import NamedTuple import yaml @@ -35,7 +36,7 @@ class BottlesSourceIterable(SourceIterable): def __iter__(self): """Generator method producing games""" - data = self.source.data_location["library.yml"].read_text("utf-8") + data = self.source.locations.data["library.yml"].read_text("utf-8") library: dict = yaml.safe_load(data) added_time = int(time()) @@ -58,11 +59,11 @@ class BottlesSourceIterable(SourceIterable): # as Cartridges can't access directories picked via Bottles' file picker portal bottles_location = Path( yaml.safe_load( - self.source.data_location["data.yml"].read_text("utf-8") + self.source.locations.data["data.yml"].read_text("utf-8") )["custom_bottles_path"] ) except (FileNotFoundError, KeyError): - bottles_location = self.source.data_location.root / "bottles" + bottles_location = self.source.locations.data.root / "bottles" bottle_path = entry["bottle"]["path"] @@ -76,6 +77,10 @@ class BottlesSourceIterable(SourceIterable): yield (game, additional_data) +class BottlesLocations(NamedTuple): + data: Location + + class BottlesSource(URLExecutableSource): """Generic Bottles source""" @@ -84,15 +89,17 @@ class BottlesSource(URLExecutableSource): url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} - data_location = Location( - schema_key="bottles-location", - candidates=( - shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", - shared.data_dir / "bottles/", - shared.home / ".local" / "share" / "bottles", - ), - paths={ - "library.yml": (False, "library.yml"), - "data.yml": (False, "data.yml"), - }, + locations = BottlesLocations( + Location( + schema_key="bottles-location", + candidates=( + shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", + shared.data_dir / "bottles/", + shared.home / ".local" / "share" / "bottles", + ), + paths={ + "library.yml": (False, "library.yml"), + "data.yml": (False, "data.yml"), + }, + ) ) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 2c9a4aa..f29028c 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -19,6 +19,7 @@ from pathlib import Path from time import time +from typing import NamedTuple from gi.repository import GLib, Gtk @@ -37,7 +38,7 @@ class FlatpakSourceIterable(SourceIterable): added_time = int(time()) icon_theme = Gtk.IconTheme.new() - icon_theme.add_search_path(str(self.source.data_location["icons"])) + icon_theme.add_search_path(str(self.source.locations.data["icons"])) blacklist = ( {"hu.kramo.Cartridges", "hu.kramo.Cartridges.Devel"} @@ -53,7 +54,7 @@ class FlatpakSourceIterable(SourceIterable): } ) - for entry in (self.source.data_location["applications"]).iterdir(): + for entry in (self.source.locations.data["applications"]).iterdir(): if entry.suffix != ".desktop": continue @@ -111,6 +112,10 @@ class FlatpakSourceIterable(SourceIterable): yield (game, additional_data) +class FlatpakLocations(NamedTuple): + data: Location + + class FlatpakSource(Source): """Generic Flatpak source""" @@ -119,14 +124,16 @@ class FlatpakSource(Source): executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} - data_location = Location( - schema_key="flatpak-location", - candidates=( - "/var/lib/flatpak/", - shared.data_dir / "flatpak", - ), - paths={ - "applications": (True, "exports/share/applications"), - "icons": (True, "exports/share/icons"), - }, + locations = FlatpakLocations( + Location( + schema_key="flatpak-location", + candidates=( + "/var/lib/flatpak/", + shared.data_dir / "flatpak", + ), + paths={ + "applications": (True, "exports/share/applications"), + "icons": (True, "exports/share/icons"), + }, + ) ) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index d169129..1c28f14 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -25,7 +25,7 @@ from hashlib import sha256 from json import JSONDecodeError from pathlib import Path from time import time -from typing import Iterable, Optional, TypedDict +from typing import Iterable, NamedTuple, Optional, TypedDict from functools import cached_property from src import shared @@ -87,7 +87,7 @@ class SubSourceIterable(Iterable): @cached_property def library_path(self) -> Path: - path = self.source.config_location.root / self.relative_library_path + path = self.source.locations.config.root / self.relative_library_path logging.debug("Using Heroic %s library.json path %s", self.name, path) return path @@ -116,7 +116,7 @@ class SubSourceIterable(Iterable): # Filenames are derived from the URL that heroic used to get the file uri: str = entry["art_square"] + self.image_uri_params digest = sha256(uri.encode()).hexdigest() - image_path = self.source.config_location.root / "images-cache" / digest + image_path = self.source.locations.config.root / "images-cache" / digest additional_data = {"local_image_path": image_path} return (game, additional_data) @@ -159,7 +159,7 @@ class StoreSubSourceIterable(SubSourceIterable): @cached_property def installed_path(self) -> Path: - path = self.source.config_location.root / self.relative_installed_path + path = self.source.locations.config.root / self.relative_installed_path logging.debug("Using Heroic %s installed.json path %s", self.name, path) return path @@ -226,7 +226,7 @@ class LegendaryIterable(StoreSubSourceIterable): and remove this property override. """ - heroic_config_path = self.source.config_location.root + heroic_config_path = self.source.locations.config.root # Heroic >= 2.9 if (path := heroic_config_path / "legendaryConfig").is_dir(): logging.debug("Using Heroic >= 2.9 legendary file") @@ -308,7 +308,7 @@ class HeroicSourceIterable(SourceIterable): """ try: - store = path_json_load(self.source.config_location["store_config.json"]) + store = path_json_load(self.source.locations.config["store_config.json"]) self.hidden_app_names = { app_name for game in store["games"]["hidden"] @@ -349,6 +349,10 @@ class HeroicSourceIterable(SourceIterable): continue +class HeroicLocations(NamedTuple): + config: Location + + class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" @@ -357,18 +361,23 @@ class HeroicSource(URLExecutableSource): url_format = "heroic://launch/{app_name}" available_on = {"linux", "win32"} - config_location = Location( - schema_key="heroic-location", - candidates=( - shared.config_dir / "heroic", - shared.home / ".config" / "heroic", - shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", - shared.appdata_dir / "heroic", - ), - paths={ - "config.json": (False, "config.json"), - "store_config.json": (False, Path("store") / "config.json"), - }, + locations = HeroicLocations( + Location( + schema_key="heroic-location", + candidates=( + shared.config_dir / "heroic", + shared.home / ".config" / "heroic", + shared.flatpak_dir + / "com.heroicgameslauncher.hgl" + / "config" + / "heroic", + shared.appdata_dir / "heroic", + ), + paths={ + "config.json": (False, "config.json"), + "store_config.json": (False, Path("store") / "config.json"), + }, + ) ) @property diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index a6d8990..dc4d4f9 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -21,6 +21,7 @@ from shutil import rmtree from sqlite3 import connect from time import time +from typing import NamedTuple from src import shared from src.game import Game @@ -51,7 +52,7 @@ class ItchSourceIterable(SourceIterable): caves.game_id = games.id ; """ - db_path = copy_db(self.source.config_location["butler.db"]) + db_path = copy_db(self.source.locations.config["butler.db"]) connection = connect(db_path) cursor = connection.execute(db_request) @@ -74,19 +75,25 @@ class ItchSourceIterable(SourceIterable): rmtree(str(db_path.parent)) +class ItchLocations(NamedTuple): + config: Location + + class ItchSource(URLExecutableSource): name = _("itch") iterable_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} - config_location = Location( - schema_key="itch-location", - candidates=( - shared.flatpak_dir / "io.itch.itch" / "config" / "itch", - shared.config_dir / "itch", - shared.home / ".config" / "itch", - shared.appdata_dir / "itch", - ), - paths={"butler.db": (False, "db/butler.db")}, + locations = ItchLocations( + Location( + schema_key="itch-location", + candidates=( + shared.flatpak_dir / "io.itch.itch" / "config" / "itch", + shared.config_dir / "itch", + shared.home / ".config" / "itch", + shared.appdata_dir / "itch", + ), + paths={"butler.db": (False, "db/butler.db")}, + ) ) diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index c7e06de..529bd8c 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -21,6 +21,7 @@ import json import logging from json import JSONDecodeError from time import time +from typing import NamedTuple from src import shared from src.game import Game @@ -50,7 +51,7 @@ class LegendarySourceIterable(SourceIterable): data = {} # Get additional metadata from file (optional) - metadata_file = self.source.config_location["metadata"] / f"{app_name}.json" + metadata_file = self.source.locations.config["metadata"] / f"{app_name}.json" try: metadata = json.load(metadata_file.open()) values["developer"] = metadata["metadata"]["developer"] @@ -66,7 +67,7 @@ class LegendarySourceIterable(SourceIterable): def __iter__(self): # Open library - file = self.source.config_location["installed.json"] + file = self.source.locations.config["installed.json"] try: library: dict = json.load(file.open()) except (JSONDecodeError, OSError): @@ -88,20 +89,26 @@ class LegendarySourceIterable(SourceIterable): yield result +class LegendaryLocations(NamedTuple): + config: Location + + class LegendarySource(Source): name = _("Legendary") executable_format = "legendary launch {app_name}" available_on = {"linux"} - iterable_class = LegendarySourceIterable - config_location: Location = Location( - schema_key="legendary-location", - candidates=( - shared.config_dir / "legendary", - shared.home / ".config" / "legendary", - ), - paths={ - "installed.json": (False, "installed.json"), - "metadata": (True, "metadata"), - }, + + locations = LegendaryLocations( + Location( + schema_key="legendary-location", + candidates=( + shared.config_dir / "legendary", + shared.home / ".config" / "legendary", + ), + paths={ + "installed.json": (False, "installed.json"), + "metadata": (True, "metadata"), + }, + ) ) diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 7c100a8..5a26fc1 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -20,6 +20,7 @@ from shutil import rmtree from sqlite3 import connect from time import time +from typing import NamedTuple from src import shared from src.game import Game @@ -51,7 +52,7 @@ class LutrisSourceIterable(SourceIterable): "import_steam": shared.schema.get_boolean("lutris-import-steam"), "import_flatpak": shared.schema.get_boolean("lutris-import-flatpak"), } - db_path = copy_db(self.source.data_location["pga.db"]) + db_path = copy_db(self.source.locations.config["pga.db"]) connection = connect(db_path) cursor = connection.execute(request, params) @@ -73,7 +74,7 @@ class LutrisSourceIterable(SourceIterable): game = Game(values) # Get official image path - image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg" + image_path = self.source.locations.cache["coverart"] / f"{row[2]}.jpg" additional_data = {"local_image_path": image_path} # Produce game @@ -83,6 +84,11 @@ class LutrisSourceIterable(SourceIterable): rmtree(str(db_path.parent)) +class LutrisLocations(NamedTuple): + config: Location + cache: Location + + class LutrisSource(URLExecutableSource): """Generic Lutris source""" @@ -91,30 +97,31 @@ class LutrisSource(URLExecutableSource): url_format = "lutris:rungameid/{game_id}" available_on = {"linux"} - # FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local... + # FIXME possible bug: config picks ~/.var... and cache picks ~/.local... - data_location = Location( - schema_key="lutris-location", - candidates=( - shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", - shared.data_dir / "lutris", - shared.home / ".local" / "share" / "lutris", + locations = LutrisLocations( + Location( + schema_key="lutris-location", + candidates=( + shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", + shared.data_dir / "lutris", + shared.home / ".local" / "share" / "lutris", + ), + paths={ + "pga.db": (False, "pga.db"), + }, ), - paths={ - "pga.db": (False, "pga.db"), - }, - ) - - cache_location = Location( - schema_key="lutris-cache-location", - candidates=( - shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", - shared.cache_dir / "lutris", - shared.home / ".cache" / "lutris", + Location( + schema_key="lutris-cache-location", + candidates=( + shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", + shared.cache_dir / "lutris", + shared.home / ".cache" / "lutris", + ), + paths={ + "coverart": (True, "coverart"), + }, ), - paths={ - "coverart": (True, "coverart"), - }, ) @property diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index d7ba467..164a792 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -19,8 +19,8 @@ import sys from abc import abstractmethod -from collections.abc import Iterable, Iterator -from typing import Any, Generator, Optional +from collections.abc import Iterable +from typing import Any, Generator, Optional, Collection from src.game import Game from src.importer.sources.location import Location @@ -54,10 +54,8 @@ class Source(Iterable): name: str variant: str = None available_on: set[str] = set() - data_location: Optional[Location] = None - cache_location: Optional[Location] = None - config_location: Optional[Location] = None iterable_class: type[SourceIterable] + locations: Collection[Location] @property def full_name(self) -> str: diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 7e65e5b..e4e9cfb 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -22,7 +22,7 @@ import logging import re from pathlib import Path from time import time -from typing import Iterable +from typing import Iterable, NamedTuple from src import shared from src.game import Game @@ -36,7 +36,7 @@ class SteamSourceIterable(SourceIterable): def get_manifest_dirs(self) -> Iterable[Path]: """Get dirs that contain steam app manifests""" - libraryfolders_path = self.source.data_location["libraryfolders.vdf"] + libraryfolders_path = self.source.locations.data["libraryfolders.vdf"] with open(libraryfolders_path, "r", encoding="utf-8") as file: contents = file.read() return [ @@ -100,7 +100,7 @@ class SteamSourceIterable(SourceIterable): # Add official cover image image_path = ( - self.source.data_location["librarycache"] + self.source.locations.data["librarycache"] / f"{appid}_library_600x900.jpg" ) additional_data = {"local_image_path": image_path, "steam_appid": appid} @@ -109,22 +109,28 @@ class SteamSourceIterable(SourceIterable): yield (game, additional_data) +class SteamLocations(NamedTuple): + data: Location + + class SteamSource(URLExecutableSource): name = _("Steam") available_on = {"linux", "win32"} iterable_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" - data_location = Location( - schema_key="steam-location", - candidates=( - shared.home / ".steam" / "steam", - shared.data_dir / "Steam", - shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", - shared.programfiles32_dir / "Steam", - ), - paths={ - "libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"), - "librarycache": (True, "appcache/librarycache"), - }, + locations = SteamLocations( + Location( + schema_key="steam-location", + candidates=( + shared.home / ".steam" / "steam", + shared.data_dir / "Steam", + shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", + shared.programfiles32_dir / "Steam", + ), + paths={ + "libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"), + "librarycache": (True, "appcache/librarycache"), + }, + ) ) diff --git a/src/preferences.py b/src/preferences.py index fdafd69..7300523 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -262,19 +262,19 @@ class PreferencesWindow(Adw.PreferencesWindow): subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path)) action_row.set_subtitle(subtitle) - def resolve_locations(self, source): + def resolve_locations(self, source: Source): """Resolve locations and add a warning if location cannot be found""" def clear_warning_selection(_widget, label): label.select_region(-1, -1) - for location_name in ("data", "config", "cache"): + for location_name, location in source.locations._asdict().items(): action_row = getattr(self, f"{source.id}_{location_name}_action_row", None) if not action_row: continue try: - getattr(source, f"{location_name}_location", None).resolve() + location.resolve() except UnresolvableLocationError: popover = Gtk.Popover( @@ -325,10 +325,14 @@ class PreferencesWindow(Adw.PreferencesWindow): return # Good picked location - location = getattr(source, f"{location_name}_location") + location = getattr(source.locations, location_name) if location.check_candidate(path): # Set the schema - infix = "-cache" if location_name == "cache" else "" + match location_name: + case "config" | "data": + infix = "" + case _: + infix = f"-{location_name}" key = f"{source.id}{infix}-location" value = str(path) shared.schema.set_string(key, value) @@ -381,10 +385,12 @@ class PreferencesWindow(Adw.PreferencesWindow): ) # Connect dir picker buttons - for location in ("data", "config", "cache"): - button = getattr(self, f"{source.id}_{location}_file_chooser_button", None) + for location_name in source.locations._asdict(): + button = getattr( + self, f"{source.id}_{location_name}_file_chooser_button", None + ) if button is not None: - button.connect("clicked", self.choose_folder, set_dir, location) + button.connect("clicked", self.choose_folder, set_dir, location_name) # Set the source row subtitles self.resolve_locations(source) From 0677eae0a24d674518472940c283c821f904cc5a Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 26 Jul 2023 03:55:23 +0200 Subject: [PATCH 43/59] Removed unused import --- src/importer/sources/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 164a792..57b45d7 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -20,7 +20,7 @@ import sys from abc import abstractmethod from collections.abc import Iterable -from typing import Any, Generator, Optional, Collection +from typing import Any, Generator, Collection from src.game import Game from src.importer.sources.location import Location From 04d0e9e90e1646d0621429d592c876cefce1d0f0 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 26 Jul 2023 04:43:10 +0200 Subject: [PATCH 44/59] Clarified location sub paths --- src/importer/sources/bottles_source.py | 6 +++--- src/importer/sources/flatpak_source.py | 6 +++--- src/importer/sources/heroic_source.py | 6 +++--- src/importer/sources/itch_source.py | 6 ++++-- src/importer/sources/legendary_source.py | 6 +++--- src/importer/sources/location.py | 25 ++++++++++++++---------- src/importer/sources/lutris_source.py | 4 ++-- src/importer/sources/steam_source.py | 6 +++--- 8 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 42eebaa..5983c27 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -26,7 +26,7 @@ import yaml from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import SourceIterable, URLExecutableSource @@ -98,8 +98,8 @@ class BottlesSource(URLExecutableSource): shared.home / ".local" / "share" / "bottles", ), paths={ - "library.yml": (False, "library.yml"), - "data.yml": (False, "data.yml"), + "library.yml": LocationSubPath("library.yml"), + "data.yml": LocationSubPath("data.yml"), }, ) ) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index f29028c..ec8e2a5 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -25,7 +25,7 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import Source, SourceIterable @@ -132,8 +132,8 @@ class FlatpakSource(Source): shared.data_dir / "flatpak", ), paths={ - "applications": (True, "exports/share/applications"), - "icons": (True, "exports/share/icons"), + "applications": LocationSubPath("exports/share/applications", True), + "icons": LocationSubPath("exports/share/icons", True), }, ) ) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 1c28f14..2986aad 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -30,7 +30,7 @@ from functools import cached_property from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import ( SourceIterable, SourceIterationResult, @@ -374,8 +374,8 @@ class HeroicSource(URLExecutableSource): shared.appdata_dir / "heroic", ), paths={ - "config.json": (False, "config.json"), - "store_config.json": (False, Path("store") / "config.json"), + "config.json": LocationSubPath("config.json"), + "store_config.json": LocationSubPath("store/config.json"), }, ) ) diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index dc4d4f9..19839cd 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -25,7 +25,7 @@ from typing import NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db @@ -94,6 +94,8 @@ class ItchSource(URLExecutableSource): shared.home / ".config" / "itch", shared.appdata_dir / "itch", ), - paths={"butler.db": (False, "db/butler.db")}, + paths={ + "butler.db": LocationSubPath("db/butler.db"), + }, ) ) diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 529bd8c..e802f51 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -25,7 +25,7 @@ from typing import NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import Source, SourceIterationResult, SourceIterable @@ -107,8 +107,8 @@ class LegendarySource(Source): shared.home / ".config" / "legendary", ), paths={ - "installed.json": (False, "installed.json"), - "metadata": (True, "metadata"), + "installed.json": LocationSubPath("installed.json"), + "metadata": LocationSubPath("metadata", True), }, ) ) diff --git a/src/importer/sources/location.py b/src/importer/sources/location.py index 8374a20..a884ba0 100644 --- a/src/importer/sources/location.py +++ b/src/importer/sources/location.py @@ -1,13 +1,18 @@ import logging from pathlib import Path -from typing import Callable, Mapping, Iterable +from typing import Mapping, Iterable, NamedTuple from os import PathLike from src import shared PathSegment = str | PathLike | Path PathSegments = Iterable[PathSegment] -Candidate = PathSegments | Callable[[], PathSegments] +Candidate = PathSegments + + +class LocationSubPath(NamedTuple): + segment: PathSegment + is_directory: bool = False class UnresolvableLocationError(Exception): @@ -26,14 +31,14 @@ class Location: schema_key: str candidates: Iterable[Candidate] - paths: Mapping[str, tuple[bool, PathSegments]] + paths: Mapping[str, LocationSubPath] root: Path = None def __init__( self, schema_key: str, candidates: Iterable[Candidate], - paths: Mapping[str, tuple[bool, PathSegments]], + paths: Mapping[str, LocationSubPath], ) -> None: super().__init__() self.schema_key = schema_key @@ -42,13 +47,13 @@ class Location: def check_candidate(self, candidate: Path) -> bool: """Check if a candidate root has the necessary files and directories""" - for type_is_dir, subpath in self.paths.values(): - subpath = Path(candidate) / Path(subpath) - if type_is_dir: - if not subpath.is_dir(): + for segment, is_directory in self.paths.values(): + path = Path(candidate) / segment + if is_directory: + if not path.is_dir(): return False else: - if not subpath.is_file(): + if not path.is_file(): return False return True @@ -81,4 +86,4 @@ class Location: def __getitem__(self, key: str): """Get the computed path from its key for the location""" self.resolve() - return self.root / self.paths[key][1] + return self.root / self.paths[key].segment diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 5a26fc1..627f896 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -24,7 +24,7 @@ from typing import NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db @@ -119,7 +119,7 @@ class LutrisSource(URLExecutableSource): shared.home / ".cache" / "lutris", ), paths={ - "coverart": (True, "coverart"), + "coverart": LocationSubPath("coverart", True), }, ), ) diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index e4e9cfb..39e3c49 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -26,7 +26,7 @@ from typing import Iterable, NamedTuple from src import shared from src.game import Game -from src.importer.sources.location import Location +from src.importer.sources.location import Location, LocationSubPath from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.steam import SteamFileHelper, SteamInvalidManifestError @@ -129,8 +129,8 @@ class SteamSource(URLExecutableSource): shared.programfiles32_dir / "Steam", ), paths={ - "libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"), - "librarycache": (True, "appcache/librarycache"), + "libraryfolders.vdf": LocationSubPath("steamapps/libraryfolders.vdf"), + "librarycache": LocationSubPath("appcache/librarycache", True), }, ) ) From fa8a15addf7c000f8b368ff9fc154782ac12fd01 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 26 Jul 2023 15:27:28 +0200 Subject: [PATCH 45/59] Moved invalid location message to location --- src/importer/sources/bottles_source.py | 1 + src/importer/sources/flatpak_source.py | 1 + src/importer/sources/heroic_source.py | 1 + src/importer/sources/itch_source.py | 1 + src/importer/sources/legendary_source.py | 1 + src/importer/sources/location.py | 11 +++++++++++ src/importer/sources/lutris_source.py | 2 ++ src/importer/sources/steam_source.py | 1 + src/preferences.py | 12 +----------- 9 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 5983c27..5b48078 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -101,5 +101,6 @@ class BottlesSource(URLExecutableSource): "library.yml": LocationSubPath("library.yml"), "data.yml": LocationSubPath("data.yml"), }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, ) ) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index ec8e2a5..b78815c 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -135,5 +135,6 @@ class FlatpakSource(Source): "applications": LocationSubPath("exports/share/applications", True), "icons": LocationSubPath("exports/share/icons", True), }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, ) ) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 2986aad..605ba56 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -377,6 +377,7 @@ class HeroicSource(URLExecutableSource): "config.json": LocationSubPath("config.json"), "store_config.json": LocationSubPath("store/config.json"), }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, ) ) diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 19839cd..8302e91 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -97,5 +97,6 @@ class ItchSource(URLExecutableSource): paths={ "butler.db": LocationSubPath("db/butler.db"), }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, ) ) diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index e802f51..48f0cac 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -110,5 +110,6 @@ class LegendarySource(Source): "installed.json": LocationSubPath("installed.json"), "metadata": LocationSubPath("metadata", True), }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, ) ) diff --git a/src/importer/sources/location.py b/src/importer/sources/location.py index a884ba0..55684b4 100644 --- a/src/importer/sources/location.py +++ b/src/importer/sources/location.py @@ -29,9 +29,18 @@ class Location: * When resolved, the schema is updated with the picked chosen """ + # The variable is the name of the source + CACHE_INVALID_SUBTITLE = _("Select the {} cache directory.") + # The variable is the name of the source + CONFIG_INVALID_SUBTITLE = _("Select the {} configuration directory.") + # The variable is the name of the source + DATA_INVALID_SUBTITLE = _("Select the {} data directory.") + schema_key: str candidates: Iterable[Candidate] paths: Mapping[str, LocationSubPath] + invalid_subtitle: str + root: Path = None def __init__( @@ -39,11 +48,13 @@ class Location: schema_key: str, candidates: Iterable[Candidate], paths: Mapping[str, LocationSubPath], + invalid_subtitle: str, ) -> None: super().__init__() self.schema_key = schema_key self.candidates = candidates self.paths = paths + self.invalid_subtitle = invalid_subtitle def check_candidate(self, candidate: Path) -> bool: """Check if a candidate root has the necessary files and directories""" diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 627f896..9afc977 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -110,6 +110,7 @@ class LutrisSource(URLExecutableSource): paths={ "pga.db": (False, "pga.db"), }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, ), Location( schema_key="lutris-cache-location", @@ -121,6 +122,7 @@ class LutrisSource(URLExecutableSource): paths={ "coverart": LocationSubPath("coverart", True), }, + invalid_subtitle=Location.CACHE_INVALID_SUBTITLE, ), ) diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 39e3c49..f1c34ba 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -132,5 +132,6 @@ class SteamSource(URLExecutableSource): "libraryfolders.vdf": LocationSubPath("steamapps/libraryfolders.vdf"), "librarycache": LocationSubPath("appcache/librarycache", True), }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, ) ) diff --git a/src/preferences.py b/src/preferences.py index 7300523..53bc098 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -351,20 +351,10 @@ class PreferencesWindow(Adw.PreferencesWindow): # Bad picked location, inform user else: title = _("Invalid Directory") - match location_name: - case "cache": - # The variable is the name of the source - subtitle_format = _("Select the {} cache directory.") - case "config": - # The variable is the name of the source - subtitle_format = _("Select the {} configuration directory.") - case "data": - # The variable is the name of the source - subtitle_format = _("Select the {} data directory.") dialog = create_dialog( self, title, - subtitle_format.format(source.name), + location.invalid_subtitle.format(source.name), "choose_folder", _("Set Location"), ) From d3a6eaa4d1ddd22d087b34dc413c2a5162b7de50 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 27 Jul 2023 04:34:33 +0200 Subject: [PATCH 46/59] Fix source id being translated - Additional fix, lutris source db subpath --- src/importer/importer.py | 14 +++++------ src/importer/sources/bottles_source.py | 3 ++- src/importer/sources/flatpak_source.py | 3 ++- src/importer/sources/heroic_source.py | 5 ++-- src/importer/sources/itch_source.py | 3 ++- src/importer/sources/legendary_source.py | 3 ++- src/importer/sources/lutris_source.py | 7 +++--- src/importer/sources/source.py | 11 ++------- src/importer/sources/steam_source.py | 3 ++- src/preferences.py | 30 ++++++++++++++---------- 10 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/importer/importer.py b/src/importer/importer.py index 8b30c6b..756ea3b 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -105,7 +105,7 @@ class Importer(ErrorProducer): manager.reset_cancellable() for source in self.sources: - logging.debug("Importing games from source %s", source.id) + logging.debug("Importing games from source %s", source.source_id) task = Task.new(None, None, self.source_callback, (source,)) self.n_source_tasks_created += 1 task.set_task_data((source,)) @@ -138,16 +138,16 @@ class Importer(ErrorProducer): # Early exit if not available or not installed if not source.is_available: - logging.info("Source %s skipped, not available", source.id) + logging.info("Source %s skipped, not available", source.source_id) return try: iterator = iter(source) except UnresolvableLocationError: - logging.info("Source %s skipped, bad location", source.id) + logging.info("Source %s skipped, bad location", source.source_id) return # Get games from source - logging.info("Scanning source %s", source.id) + logging.info("Scanning source %s", source.source_id) while True: # Handle exceptions raised when iterating try: @@ -155,7 +155,7 @@ class Importer(ErrorProducer): except StopIteration: break except Exception as error: # pylint: disable=broad-exception-caught - logging.exception("%s in %s", type(error).__name__, source.id) + logging.exception("%s in %s", type(error).__name__, source.source_id) self.report_error(error) continue @@ -172,7 +172,7 @@ class Importer(ErrorProducer): # Should not happen on production code logging.warning( "%s produced an invalid iteration return type %s", - source.id, + source.source_id, type(iteration_result), ) continue @@ -194,7 +194,7 @@ class Importer(ErrorProducer): def source_callback(self, _obj, _result, data): """Callback executed when a source is fully scanned""" source, *_rest = data - logging.debug("Import done for source %s", source.id) + logging.debug("Import done for source %s", source.source_id) self.n_source_tasks_done += 1 self.progress_changed_callback() diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 5b48078..041247b 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -43,7 +43,7 @@ class BottlesSourceIterable(SourceIterable): for entry in library.values(): # Build game values = { - "source": self.source.id, + "source": self.source.source_id, "added": added_time, "name": entry["name"], "game_id": self.source.game_id_format.format(game_id=entry["id"]), @@ -84,6 +84,7 @@ class BottlesLocations(NamedTuple): class BottlesSource(URLExecutableSource): """Generic Bottles source""" + source_id = "bottles" name = _("Bottles") iterable_class = BottlesSourceIterable url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index b78815c..ee4ebbe 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -77,7 +77,7 @@ class FlatpakSourceIterable(SourceIterable): continue values = { - "source": self.source.id, + "source": self.source.source_id, "added": added_time, "name": name, "game_id": self.source.game_id_format.format(game_id=flatpak_id), @@ -119,6 +119,7 @@ class FlatpakLocations(NamedTuple): class FlatpakSource(Source): """Generic Flatpak source""" + source_id = "flatpak" name = _("Flatpak") iterable_class = FlatpakSourceIterable executable_format = "flatpak run {flatpak_id}" diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 605ba56..33b7f90 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -100,7 +100,7 @@ class SubSourceIterable(Iterable): # Build game values = { - "source": f"{self.source.id}_{self.service}", + "source": f"{self.source.source_id}_{self.service}", "added": added_time, "name": entry["title"], "developer": entry.get("developer", None), @@ -356,6 +356,7 @@ class HeroicLocations(NamedTuple): class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" + source_id = "heroic" name = _("Heroic") iterable_class = HeroicSourceIterable url_format = "heroic://launch/{app_name}" @@ -384,4 +385,4 @@ class HeroicSource(URLExecutableSource): @property def game_id_format(self) -> str: """The string format used to construct game IDs""" - return self.id + "_{service}_{game_id}" + return self.source_id + "_{service}_{game_id}" diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 8302e91..36e02e0 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -62,7 +62,7 @@ class ItchSourceIterable(SourceIterable): for row in cursor: values = { "added": added_time, - "source": self.source.id, + "source": self.source.source_id, "name": row[1], "game_id": self.source.game_id_format.format(game_id=row[0]), "executable": self.source.executable_format.format(cave_id=row[4]), @@ -80,6 +80,7 @@ class ItchLocations(NamedTuple): class ItchSource(URLExecutableSource): + source_id = "itch" name = _("itch") iterable_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 48f0cac..03bbdd2 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -43,7 +43,7 @@ class LegendarySourceIterable(SourceIterable): app_name = entry["app_name"] values = { "added": added_time, - "source": self.source.id, + "source": self.source.source_id, "name": entry["title"], "game_id": self.source.game_id_format.format(game_id=app_name), "executable": self.source.executable_format.format(app_name=app_name), @@ -94,6 +94,7 @@ class LegendaryLocations(NamedTuple): class LegendarySource(Source): + source_id = "legendary" name = _("Legendary") executable_format = "legendary launch {app_name}" available_on = {"linux"} diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 9afc977..023d3be 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -65,7 +65,7 @@ class LutrisSourceIterable(SourceIterable): "added": added_time, "hidden": row[4], "name": row[1], - "source": f"{self.source.id}_{row[3]}", + "source": f"{self.source.source_id}_{row[3]}", "game_id": self.source.game_id_format.format( runner=row[3], game_id=row[0] ), @@ -92,6 +92,7 @@ class LutrisLocations(NamedTuple): class LutrisSource(URLExecutableSource): """Generic Lutris source""" + source_id = "lutris" name = _("Lutris") iterable_class = LutrisSourceIterable url_format = "lutris:rungameid/{game_id}" @@ -108,7 +109,7 @@ class LutrisSource(URLExecutableSource): shared.home / ".local" / "share" / "lutris", ), paths={ - "pga.db": (False, "pga.db"), + "pga.db": LocationSubPath("pga.db"), }, invalid_subtitle=Location.DATA_INVALID_SUBTITLE, ), @@ -128,4 +129,4 @@ class LutrisSource(URLExecutableSource): @property def game_id_format(self): - return self.id + "_{runner}_{game_id}" + return self.source_id + "_{runner}_{game_id}" diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 57b45d7..26b2a84 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -51,6 +51,7 @@ class SourceIterable(Iterable): class Source(Iterable): """Source of games. E.g an installed app with a config file that lists game directories""" + source_id: str name: str variant: str = None available_on: set[str] = set() @@ -65,18 +66,10 @@ class Source(Iterable): full_name_ += f" ({self.variant})" return full_name_ - @property - def id(self) -> str: # pylint: disable=invalid-name - """The source's identifier""" - id_ = self.name.lower() - if self.variant is not None: - id_ += f"_{self.variant.lower()}" - return id_ - @property def game_id_format(self) -> str: """The string format used to construct game IDs""" - return self.id + "_{game_id}" + return self.source_id + "_{game_id}" @property def is_available(self): diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index f1c34ba..904fb0b 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -92,7 +92,7 @@ class SteamSourceIterable(SourceIterable): values = { "added": added_time, "name": local_data["name"], - "source": self.source.id, + "source": self.source.source_id, "game_id": self.source.game_id_format.format(game_id=appid), "executable": self.source.executable_format.format(game_id=appid), } @@ -114,6 +114,7 @@ class SteamLocations(NamedTuple): class SteamSource(URLExecutableSource): + source_id = "steam" name = _("Steam") available_on = {"linux", "win32"} iterable_class = SteamSourceIterable diff --git a/src/preferences.py b/src/preferences.py index 53bc098..1a16123 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -143,7 +143,7 @@ class PreferencesWindow(Adw.PreferencesWindow): ): source = source_class() if not source.is_available: - expander_row = getattr(self, f"{source.id}_expander_row") + expander_row = getattr(self, f"{source.source_id}_expander_row") expander_row.set_visible(False) else: self.init_source_row(source) @@ -250,12 +250,14 @@ class PreferencesWindow(Adw.PreferencesWindow): """Set the dir subtitle for a source's action rows""" for location in ("data", "config", "cache"): # Get the action row to subtitle - action_row = getattr(self, f"{source.id}_{location}_action_row", None) + action_row = getattr( + self, f"{source.source_id}_{location}_action_row", None + ) if not action_row: continue infix = "-cache" if location == "cache" else "" - key = f"{source.id}{infix}-location" + key = f"{source.source_id}{infix}-location" path = Path(shared.schema.get_string(key)).expanduser() # Remove the path prefix if picked via Flatpak portal @@ -269,7 +271,9 @@ class PreferencesWindow(Adw.PreferencesWindow): label.select_region(-1, -1) for location_name, location in source.locations._asdict().items(): - action_row = getattr(self, f"{source.id}_{location_name}_action_row", None) + action_row = getattr( + self, f"{source.source_id}_{location_name}_action_row", None + ) if not action_row: continue @@ -311,7 +315,7 @@ class PreferencesWindow(Adw.PreferencesWindow): menu_button.add_css_class("warning") action_row.add_prefix(menu_button) - self.warning_menu_buttons[source.id] = menu_button + self.warning_menu_buttons[source.source_id] = menu_button def init_source_row(self, source: Source): """Initialize a preference row for a source class""" @@ -333,18 +337,18 @@ class PreferencesWindow(Adw.PreferencesWindow): infix = "" case _: infix = f"-{location_name}" - key = f"{source.id}{infix}-location" + key = f"{source.source_id}{infix}-location" value = str(path) shared.schema.set_string(key, value) # Update the row self.update_source_action_row_paths(source) - if self.warning_menu_buttons.get(source.id): + if self.warning_menu_buttons.get(source.source_id): action_row = getattr( - self, f"{source.id}_{location_name}_action_row", None + self, f"{source.source_id}_{location_name}_action_row", None ) - action_row.remove(self.warning_menu_buttons[source.id]) - self.warning_menu_buttons.pop(source.id) + action_row.remove(self.warning_menu_buttons[source.source_id]) + self.warning_menu_buttons.pop(source.source_id) logging.debug("User-set value for schema key %s: %s", key, value) @@ -366,9 +370,9 @@ class PreferencesWindow(Adw.PreferencesWindow): dialog.connect("response", on_response) # Bind expander row activation to source being enabled - expander_row = getattr(self, f"{source.id}_expander_row") + expander_row = getattr(self, f"{source.source_id}_expander_row") shared.schema.bind( - source.id, + source.source_id, expander_row, "enable-expansion", Gio.SettingsBindFlags.DEFAULT, @@ -377,7 +381,7 @@ class PreferencesWindow(Adw.PreferencesWindow): # Connect dir picker buttons for location_name in source.locations._asdict(): button = getattr( - self, f"{source.id}_{location_name}_file_chooser_button", None + self, f"{source.source_id}_{location_name}_file_chooser_button", None ) if button is not None: button.connect("clicked", self.choose_folder, set_dir, location_name) From e554cf86785987e03506aabb8c0854f24e79ea41 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 27 Jul 2023 11:10:45 +0200 Subject: [PATCH 47/59] Translations update from Hosted Weblate (#155) * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges * Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cartridges/cartridges/ Translation: Cartridges/Cartridges --- po/ar.po | 76 +++++++++++++++++++++++++-------------------- po/cs.po | 76 +++++++++++++++++++++++++-------------------- po/de.po | 74 ++++++++++++++++++++++++-------------------- po/el.po | 76 +++++++++++++++++++++++++-------------------- po/es.po | 80 +++++++++++++++++++++++++---------------------- po/fa.po | 74 ++++++++++++++++++++++++-------------------- po/fi.po | 74 ++++++++++++++++++++++++-------------------- po/fr.po | 76 +++++++++++++++++++++++++-------------------- po/hu.po | 76 +++++++++++++++++++++++++-------------------- po/it.po | 76 +++++++++++++++++++++++++-------------------- po/ko.po | 74 ++++++++++++++++++++++++-------------------- po/nb_NO.po | 74 ++++++++++++++++++++++++-------------------- po/nl.po | 76 +++++++++++++++++++++++++-------------------- po/pl.po | 76 +++++++++++++++++++++++++-------------------- po/pt.po | 76 +++++++++++++++++++++++++-------------------- po/pt_BR.po | 76 +++++++++++++++++++++++++-------------------- po/ro.po | 74 ++++++++++++++++++++++++-------------------- po/ru.po | 80 +++++++++++++++++++++++++---------------------- po/sv.po | 76 +++++++++++++++++++++++++-------------------- po/ta.po | 89 ++++++++++++++++++++++++++++------------------------- po/tr.po | 76 +++++++++++++++++++++++++-------------------- po/uk.po | 80 +++++++++++++++++++++++++---------------------- 22 files changed, 923 insertions(+), 762 deletions(-) diff --git a/po/ar.po b/po/ar.po index 73185ab..506fe72 100644 --- a/po/ar.po +++ b/po/ar.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-09 07:59+0000\n" "Last-Translator: Ali Aljishi \n" "Language-Team: Arabic " @@ -470,15 +478,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "تعذَّرت إضافة اللعبة" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "لا يجوز كون عنوان اللعبة فارغًا." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "لا يجوز كون ملفِّ التنفيذ فارغًا." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "تعذَّر تطبيق التفضيلات" @@ -500,44 +508,44 @@ msgstr "أٌظهرت {}" msgid "{} removed" msgstr "أزيلت {}" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "أُزيلت كلُّ الألعاب" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "تحتاج مفتاح واجهة برمجة حال ما أردت استخدام SteamGridDB، {}هنا تولِّده{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "لم يُعثر على التثبيت" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "حدِّد مجلَّدًا صالحًا." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "مجلَّد غير صالح" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "حدِّد مجلَّد ذاكرة {} المؤقتة." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "حدِّد مجلَّد ضبط {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "حدِّد مجلَّد بيانات {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "عيِّن الموضع" diff --git a/po/cs.po b/po/cs.po index 8863bde..83da395 100644 --- a/po/cs.po +++ b/po/cs.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-24 13:05+0000\n" "Last-Translator: foo expert \n" "Language-Team: Czech \n" "Language-Team: German \n" "Language-Team: Greek \n" +"Last-Translator: Óscar Fernández Díaz \n" "Language-Team: Spanish \n" "Language: es\n" @@ -23,7 +23,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Cartuchos" @@ -37,7 +37,9 @@ msgid "Launch all your games" msgstr "Lance todos sus juegos" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gaming;launcher;steam;lutris;heroic;bottles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -65,7 +67,7 @@ msgid "Game Details" msgstr "Detalles del juego" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Preferencias" @@ -148,7 +150,7 @@ msgstr "Mostrar preferencias" msgid "Shortcuts" msgstr "Atajos" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Deshacer" @@ -176,7 +178,7 @@ msgstr "Mostrar juegos ocultos" msgid "Remove game" msgstr "Eliminar juego" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Comportamiento" @@ -226,9 +228,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Ruta de instalación" @@ -261,54 +263,60 @@ msgid "Import GOG Games" msgstr "Importar juegos de GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Importar juegos de Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Importar juegos descargados" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Legendario" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Importar lanzadores de juegos" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Autenticación" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Clave API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Usar SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Descargar las imágenes al añadir o importar juegos" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Preferir las imágenes oficiales" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Prefiero las imágenes animadas" @@ -397,7 +405,7 @@ msgid "About Cartridges" msgstr "Acerca de Cartuchos" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "Óscar Fernández Díaz " @@ -473,15 +481,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "No se puede añadir el juego" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "El título del juego no puede estar vacío." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "El ejecutable no puede estar vacío." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "No se pudieron aplicar las preferencias" @@ -503,45 +511,45 @@ msgstr "{} visible" msgid "{} removed" msgstr "{} eliminado" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Todos los juegos eliminados" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Se necesita una clave API para utilizar SteamGridDB. Puedes generar una {}" "aquí{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Instalación no encontrada" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Selecciona un directorio válido." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Directorio incorrecto" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Seleccione el directorio de la caché {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Seleccione el directorio de configuración {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Seleccione el directorio de datos {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Escoger la ubicación" diff --git a/po/fa.po b/po/fa.po index da0494e..3ee2a79 100644 --- a/po/fa.po +++ b/po/fa.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-04-22 10:48+0000\n" "Last-Translator: سید حسین موسوی فرد \n" "Language-Team: Persian \n" "Language-Team: Finnish \n" "Language-Team: French \n" "Language-Team: Hungarian \n" "Language-Team: Italian \n" "Language-Team: Korean \n" "Language-Team: Norwegian Bokmål " @@ -491,15 +497,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Kunne ikke legge til spill" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Spillnavnet kan ikke være tomt." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Kjørbar fil må angis." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Kunne ikke ta i bruk endringer" @@ -523,54 +529,54 @@ msgstr "{} synlig" msgid "{} removed" msgstr "{} fjernet" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Alle spill fjernet" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "En API-nøkkel kreves for å bruke SteamGridDB. Du kan generere en {}her{}." -#: src/preferences.py:284 +#: src/preferences.py:285 #, fuzzy #| msgid "Installation Not Found" msgid "Installation Not Found" msgstr "Fant ikke installasjonen" -#: src/preferences.py:286 +#: src/preferences.py:287 #, fuzzy #| msgid "Select the {} data directory." msgid "Select a valid directory." msgstr "Velg {}-datamappen." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 #, fuzzy #| msgid "Select the {} data directory." msgid "Select the {} cache directory." msgstr "Velg {}-datamappen." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 #, fuzzy #| msgid "Select the {} configuration directory." msgid "Select the {} configuration directory." msgstr "Velg {}-oppsettsmappen." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 #, fuzzy #| msgid "Select the {} data directory." msgid "Select the {} data directory." msgstr "Velg {}-datamappen." -#: src/preferences.py:364 +#: src/preferences.py:365 #, fuzzy #| msgid "Set Steam Location" msgid "Set Location" diff --git a/po/nl.po b/po/nl.po index b85ed43..3d8b8f6 100644 --- a/po/nl.po +++ b/po/nl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-08 14:52+0000\n" "Last-Translator: Philip Goto \n" "Language-Team: Dutch \n" "Language-Team: Polish \n" "Language-Team: Portuguese \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: Romanian \n" "Language-Team: Russian =2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 5.0-dev\n" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Картриджи" @@ -36,7 +36,9 @@ msgid "Launch all your games" msgstr "Запустите все свои игры" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "gaming;launcher;steam;lutris;heroic;bottles;itch;игры;стим;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -64,7 +66,7 @@ msgid "Game Details" msgstr "Подробности об игре" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Параметры" @@ -147,7 +149,7 @@ msgstr "Показать параметры" msgid "Shortcuts" msgstr "Комбинации клавиш" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Вернуть" @@ -175,7 +177,7 @@ msgstr "Показать скрытые игры" msgid "Remove game" msgstr "Удалить игру" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Поведение" @@ -224,9 +226,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Место установки" @@ -259,54 +261,60 @@ msgid "Import GOG Games" msgstr "Импорт игр GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Импорт игр Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Импорт сторонних игр" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Legendary" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Импорт средств запуска игр" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Аутентификация" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "API-ключ" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Использовать SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Загрузка изображений при добавлении или импорте игр" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Отдавать предпочтение официальным изображениям" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Отдавать предпочтение анимированным изображениям" @@ -395,7 +403,7 @@ msgid "About Cartridges" msgstr "О приложении" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "Ser82-png" @@ -471,15 +479,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Не удалось добавить игру" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Название игры не может быть пустым." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Исполняемый файл не может быть пустым." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Не удалось применить параметры" @@ -501,45 +509,45 @@ msgstr "{} - не скрыта" msgid "{} removed" msgstr "{} удалена" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Все игры удалены" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Для использования SteamGridDB требуется ключ API. Вы можете сгенерировать " "его {}здесь{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Установка не найдена" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Выберите действующий каталог." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Неверный каталог" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Выберите каталог кэша {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Выберите каталог конфигурации {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Выберите каталог данных {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Установить расположение" diff --git a/po/sv.po b/po/sv.po index d6f1122..1adaa49 100644 --- a/po/sv.po +++ b/po/sv.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-08 14:52+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" "Language-Team: Tamil " @@ -473,15 +480,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "விளையாட்டைச் சேர்க்க முடியவில்லை" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "விளையாட்டு தலைப்பு காலியாக இருக்கக்கூடாது." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "இயங்கக்கூடியது காலியாக இருக்க முடியாது." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "விருப்பங்களைப் பயன்படுத்த முடியவில்லை" @@ -503,45 +510,43 @@ msgstr "{} மறைக்கப்படாதது" msgid "{} removed" msgstr "{} அகற்றப்பட்டது" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "அனைத்து விளையாட்டுகளும் அகற்றப்பட்டன" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." -msgstr "" -"SteamGridDB ஐப் பயன்படுத்த API விசை தேவை. நீங்கள் ஒன்றை {}இங்கே{} " -"உருவாக்கலாம்." +msgstr "SteamGridDB ஐப் பயன்படுத்த API விசை தேவை. நீங்கள் ஒன்றை {}இங்கே{} உருவாக்கலாம்." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "நிறுவல் கிடைக்கவில்லை" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "சரியான கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "தவறான கோப்பகம்" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "{} கேச் கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "{} கட்டமைப்பு கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "{} தரவு கோப்பகத்தைத் தேர்ந்தெடுக்கவும்." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "இருப்பிடத்தை அமைக்கவும்" diff --git a/po/tr.po b/po/tr.po index e7acdd1..121fd78 100644 --- a/po/tr.po +++ b/po/tr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-15 22:51+0000\n" "Last-Translator: Sabri Ünal \n" "Language-Team: Turkish " @@ -471,15 +479,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Oyun Eklenemedi" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Oyun başlığı boş olamaz." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Çalıştırılabilir boş olamaz." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Tercihler Uygulanamadı" @@ -501,45 +509,45 @@ msgstr "{} görünür" msgid "{} removed" msgstr "{} kaldırıldı" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Tüm oyunlar kaldırıldı" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "SteamGridDBʼyi kullanmak için API anahtarı gereklidir. {}Buradan{} bir tane " "oluşturabilirsiniz." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Kurulum Bulunamadı" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Geçerli bir dizin seçin." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Geçersiz Dizin" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "{} önbellek dizinini seç." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "{} yapılandırma dizinini seç." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "{} veri dizinini seç." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Konum Ayarla" diff --git a/po/uk.po b/po/uk.po index 2ac3ec9..034db8d 100644 --- a/po/uk.po +++ b/po/uk.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-05 14:36+0200\n" +"POT-Creation-Date: 2023-07-25 20:33+0200\n" "PO-Revision-Date: 2023-07-08 14:52+0000\n" "Last-Translator: Dan \n" "Language-Team: Ukrainian =2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 5.0-dev\n" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:162 +#: src/main.py:170 msgid "Cartridges" msgstr "Картриджі" @@ -38,7 +38,9 @@ msgid "Launch all your games" msgstr "Запустіть усі свої ігри" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +#, fuzzy +#| msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;" +msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" msgstr "ігри;лаунчер;steam;lutris;heroic;bottles;itch;" #: data/hu.kramo.Cartridges.metainfo.xml.in:9 @@ -66,7 +68,7 @@ msgid "Game Details" msgstr "Подробиці гри" #: data/hu.kramo.Cartridges.metainfo.xml.in:42 data/gtk/window.blp:416 -#: src/details_window.py:239 +#: src/details_window.py:241 msgid "Preferences" msgstr "Параметри" @@ -149,7 +151,7 @@ msgstr "Показати параметри" msgid "Shortcuts" msgstr "Ярлики" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:112 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 msgid "Undo" msgstr "Відмінити" @@ -177,7 +179,7 @@ msgstr "Показати приховані ігри" msgid "Remove game" msgstr "Видалити гру" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:268 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 msgid "Behavior" msgstr "Поведінка" @@ -226,9 +228,9 @@ msgid "Steam" msgstr "Steam" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 -#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:192 -#: data/gtk/preferences.blp:206 data/gtk/preferences.blp:220 -#: data/gtk/preferences.blp:234 +#: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 +#: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 +#: data/gtk/preferences.blp:243 msgid "Install Location" msgstr "Місце встановлення" @@ -261,54 +263,60 @@ msgid "Import GOG Games" msgstr "Імпорт ігор GOG" #: data/gtk/preferences.blp:178 +#, fuzzy +#| msgid "Import Steam Games" +msgid "Import Amazon Games" +msgstr "Імпорт ігор Steam" + +#: data/gtk/preferences.blp:187 msgid "Import Sideloaded Games" msgstr "Імпорт сторонніх ігор" -#: data/gtk/preferences.blp:188 +#: data/gtk/preferences.blp:197 msgid "Bottles" msgstr "Bottles" -#: data/gtk/preferences.blp:202 +#: data/gtk/preferences.blp:211 msgid "itch" msgstr "itch" -#: data/gtk/preferences.blp:216 +#: data/gtk/preferences.blp:225 msgid "Legendary" msgstr "Легендарний" -#: data/gtk/preferences.blp:230 +#: data/gtk/preferences.blp:239 msgid "Flatpak" msgstr "Flatpak" -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:252 msgid "Import Game Launchers" msgstr "Імпортувати ігрові лаунчери" -#: data/gtk/preferences.blp:256 +#: data/gtk/preferences.blp:265 msgid "SteamGridDB" msgstr "SteamGridDB" -#: data/gtk/preferences.blp:260 +#: data/gtk/preferences.blp:269 msgid "Authentication" msgstr "Аутентифікація" -#: data/gtk/preferences.blp:263 +#: data/gtk/preferences.blp:272 msgid "API Key" msgstr "Ключ API" -#: data/gtk/preferences.blp:271 +#: data/gtk/preferences.blp:280 msgid "Use SteamGridDB" msgstr "Використовувати SteamGridDB" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:281 msgid "Download images when adding or importing games" msgstr "Завантаження зображень під час додавання або імпорту ігор" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:290 msgid "Prefer Over Official Images" msgstr "Надавати перевагу офіційним зображенням" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:299 msgid "Prefer Animated Images" msgstr "Надавати перевагу анімованим зображенням" @@ -397,7 +405,7 @@ msgid "About Cartridges" msgstr "Про Картриджі" #. Translators: Replace this with your name for it to show up in the about window -#: src/main.py:180 +#: src/main.py:188 msgid "translator_credits" msgstr "kefir2105" @@ -474,15 +482,15 @@ msgstr "" msgid "Couldn't Add Game" msgstr "Не вдалося додати гру" -#: src/details_window.py:147 src/details_window.py:181 +#: src/details_window.py:147 src/details_window.py:183 msgid "Game title cannot be empty." msgstr "Назва гри не може бути порожньою." -#: src/details_window.py:153 src/details_window.py:189 +#: src/details_window.py:153 src/details_window.py:191 msgid "Executable cannot be empty." msgstr "Виконуваний файл не може бути порожнім." -#: src/details_window.py:180 src/details_window.py:188 +#: src/details_window.py:182 src/details_window.py:190 msgid "Couldn't Apply Preferences" msgstr "Не вдалося застосувати параметри" @@ -504,45 +512,45 @@ msgstr "{} показано" msgid "{} removed" msgstr "{} видалено" -#: src/preferences.py:111 +#: src/preferences.py:112 msgid "All games removed" msgstr "Всі ігри видалено" -#: src/preferences.py:159 +#: src/preferences.py:160 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" "Для використання SteamGridDB потрібен ключ API. Ви можете згенерувати його {}" "тут{}." -#: src/preferences.py:284 +#: src/preferences.py:285 msgid "Installation Not Found" msgstr "Встановлення не знайдено" -#: src/preferences.py:286 +#: src/preferences.py:287 msgid "Select a valid directory." msgstr "Виберіть правильний каталог." -#: src/preferences.py:348 +#: src/preferences.py:349 msgid "Invalid Directory" msgstr "Неправильний каталог" #. The variable is the name of the source -#: src/preferences.py:352 +#: src/preferences.py:353 msgid "Select the {} cache directory." msgstr "Виберіть каталог кешу {}." #. The variable is the name of the source -#: src/preferences.py:355 +#: src/preferences.py:356 msgid "Select the {} configuration directory." msgstr "Виберіть каталог конфігурації {}." #. The variable is the name of the source -#: src/preferences.py:358 +#: src/preferences.py:359 msgid "Select the {} data directory." msgstr "Виберіть каталог даних {}." -#: src/preferences.py:364 +#: src/preferences.py:365 msgid "Set Location" msgstr "Встановити місцезнаходження" From 459e13ad0584709503a14bfc6e22971dfde59759 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Thu, 27 Jul 2023 11:13:07 +0200 Subject: [PATCH 48/59] v2.1.1 --- data/hu.kramo.Cartridges.metainfo.xml.in | 8 ++++++++ meson.build | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/data/hu.kramo.Cartridges.metainfo.xml.in b/data/hu.kramo.Cartridges.metainfo.xml.in index 24cb18b..12ce985 100644 --- a/data/hu.kramo.Cartridges.metainfo.xml.in +++ b/data/hu.kramo.Cartridges.metainfo.xml.in @@ -44,6 +44,14 @@ + + +
    +
  • Fixes an issue with translations
  • +
  • Translations since 2.1
  • +
+
+
    diff --git a/meson.build b/meson.build index 2b2ea32..ac33e44 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cartridges', - version: '2.1', + version: '2.1.1', meson_version: '>= 0.59.0', default_options: [ 'warning_level=2', 'werror=false', ], ) From e4dc1253ae63baaef12cdf2c7485d9c94a00a16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Thu, 27 Jul 2023 18:38:34 +0200 Subject: [PATCH 49/59] Specify runner in heroic protocol (#163) * improv: specify runner in heroic protocol to avoid appName collisions * Update styling Co-authored-by: kramo <93832451+kra-mo@users.noreply.github.com> --------- Co-authored-by: kramo <93832451+kra-mo@users.noreply.github.com> --- src/importer/sources/heroic_source.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 33b7f90..c06a266 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -97,6 +97,7 @@ class SubSourceIterable(Iterable): """Build a Game from a Heroic library entry""" app_name = entry["app_name"] + runner = entry["runner"] # Build game values = { @@ -107,7 +108,7 @@ class SubSourceIterable(Iterable): "game_id": self.source.game_id_format.format( 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), } game = Game(values) @@ -359,7 +360,7 @@ class HeroicSource(URLExecutableSource): source_id = "heroic" name = _("Heroic") iterable_class = HeroicSourceIterable - url_format = "heroic://launch/{app_name}" + url_format = "heroic://launch/{runner}/{app_name}" available_on = {"linux", "win32"} locations = HeroicLocations( From 1aff1347e3d8a319e028c7ce160b0b02a81aa1f6 Mon Sep 17 00:00:00 2001 From: Geoffrey Coulaud Date: Mon, 31 Jul 2023 18:44:18 +0200 Subject: [PATCH 50/59] Managers refactor (#164) The main reason for this is the compositing of both local and online covers with the same logic. It was a problem raised in #146 with some covers getting stretched. Changes: - Renamed and simplified managers methods - Created a generic `cover manager` - Added more retryable errors to `steam api manager` - Removed `local cover manager` and `online cover manager` - Reduced dependency on `PIL` --- src/main.py | 6 +- src/store/managers/async_manager.py | 2 +- src/store/managers/cover_manager.py | 197 +++++++++++++++++++++ src/store/managers/display_manager.py | 2 +- src/store/managers/file_manager.py | 2 +- src/store/managers/local_cover_manager.py | 74 -------- src/store/managers/manager.py | 8 +- src/store/managers/online_cover_manager.py | 126 ------------- src/store/managers/sgdb_manager.py | 7 +- src/store/managers/steam_api_manager.py | 5 +- src/store/store.py | 2 +- src/utils/steamgriddb.py | 4 +- 12 files changed, 215 insertions(+), 220 deletions(-) create mode 100644 src/store/managers/cover_manager.py delete mode 100644 src/store/managers/local_cover_manager.py delete mode 100644 src/store/managers/online_cover_manager.py diff --git a/src/main.py b/src/main.py index 949029d..1bad1ac 100644 --- a/src/main.py +++ b/src/main.py @@ -45,8 +45,7 @@ from src.logging.setup import log_system_info, setup_logging from src.preferences import PreferencesWindow from src.store.managers.display_manager import DisplayManager from src.store.managers.file_manager import FileManager -from src.store.managers.local_cover_manager import LocalCoverManager -from src.store.managers.online_cover_manager import OnlineCoverManager +from src.store.managers.cover_manager import CoverManager from src.store.managers.sgdb_manager import SGDBManager from src.store.managers.steam_api_manager import SteamAPIManager from src.store.store import Store @@ -97,9 +96,8 @@ 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(CoverManager()) shared.store.add_manager(SteamAPIManager()) - shared.store.add_manager(OnlineCoverManager()) shared.store.add_manager(SGDBManager()) shared.store.toggle_manager_in_pipelines(FileManager, True) diff --git a/src/store/managers/async_manager.py b/src/store/managers/async_manager.py index 153ce82..c7f2ff8 100644 --- a/src/store/managers/async_manager.py +++ b/src/store/managers/async_manager.py @@ -56,7 +56,7 @@ class AsyncManager(Manager): def _task_thread_func(self, _task, _source_object, data, _cancellable): """Task thread entry point""" 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): """Method run after the task is done""" diff --git a/src/store/managers/cover_manager.py b/src/store/managers/cover_manager.py new file mode 100644 index 0000000..4496e80 --- /dev/null +++ b/src/store/managers/cover_manager.py @@ -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 . +# +# 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) diff --git a/src/store/managers/display_manager.py b/src/store/managers/display_manager.py index 7d3a8f0..a5005a4 100644 --- a/src/store/managers/display_manager.py +++ b/src/store/managers/display_manager.py @@ -30,7 +30,7 @@ class DisplayManager(Manager): run_after = (SteamAPIManager, SGDBManager) 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(): game.get_parent().get_parent().remove(game) if game.get_parent(): diff --git a/src/store/managers/file_manager.py b/src/store/managers/file_manager.py index 4caa3b4..8eee69b 100644 --- a/src/store/managers/file_manager.py +++ b/src/store/managers/file_manager.py @@ -31,7 +31,7 @@ class FileManager(AsyncManager): run_after = (SteamAPIManager,) 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 return diff --git a/src/store/managers/local_cover_manager.py b/src/store/managers/local_cover_manager.py deleted file mode 100644 index 0b95f30..0000000 --- a/src/store/managers/local_cover_manager.py +++ /dev/null @@ -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 . -# -# 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)) diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index b1aadf6..4ef50d0 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -50,7 +50,7 @@ class Manager(ErrorProducer): return type(self).__name__ @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 * Implemented by final child classes @@ -59,7 +59,7 @@ class Manager(ErrorProducer): * 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""" # Keep track of the number of tries @@ -106,7 +106,7 @@ class Manager(ErrorProducer): def try_manager_logic(): try: - self.manager_logic(game, additional_data) + self.main(game, additional_data) except Exception as error: # pylint: disable=broad-exception-caught handle_error(error) @@ -116,5 +116,5 @@ class Manager(ErrorProducer): 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) + self.run(game, additional_data) callback(self) diff --git a/src/store/managers/online_cover_manager.py b/src/store/managers/online_cover_manager.py deleted file mode 100644 index bc6ea1e..0000000 --- a/src/store/managers/online_cover_manager.py +++ /dev/null @@ -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 . -# -# 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 - ) diff --git a/src/store/managers/sgdb_manager.py b/src/store/managers/sgdb_manager.py index 142495f..5c002cb 100644 --- a/src/store/managers/sgdb_manager.py +++ b/src/store/managers/sgdb_manager.py @@ -24,19 +24,18 @@ from requests.exceptions import HTTPError, SSLError from src.errors.friendly_error import FriendlyError from src.game import Game 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.cover_manager import CoverManager from src.utils.steamgriddb import SGDBAuthError, SGDBHelper class SGDBManager(AsyncManager): """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) - def manager_logic(self, game: Game, _additional_data: dict) -> None: + def main(self, game: Game, _additional_data: dict) -> None: try: sgdb = SGDBHelper() sgdb.conditionaly_update_cover(game) diff --git a/src/store/managers/steam_api_manager.py b/src/store/managers/steam_api_manager.py index 19b38af..1a62ddd 100644 --- a/src/store/managers/steam_api_manager.py +++ b/src/store/managers/steam_api_manager.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from requests.exceptions import HTTPError, SSLError +from urllib3.exceptions import ConnectionError as Urllib3ConnectionError from src.game import Game from src.store.managers.async_manager import AsyncManager @@ -32,7 +33,7 @@ from src.utils.steam import ( class SteamAPIManager(AsyncManager): """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_rate_limiter: SteamRateLimiter = None @@ -42,7 +43,7 @@ class SteamAPIManager(AsyncManager): self.steam_rate_limiter = SteamRateLimiter() 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 appid = additional_data.get("steam_appid", None) if appid is None: diff --git a/src/store/store.py b/src/store/store.py index 9da9ef0..231bbdc 100644 --- a/src/store/store.py +++ b/src/store/store.py @@ -130,7 +130,7 @@ class Store: # Connect signals for manager in self.managers.values(): for signal in manager.signals: - game.connect(signal, manager.execute_resilient_manager_logic) + game.connect(signal, manager.run) # Add the game to the store if not game.source in self.source_games: diff --git a/src/utils/steamgriddb.py b/src/utils/steamgriddb.py index 9cd9c3c..57dda3e 100644 --- a/src/utils/steamgriddb.py +++ b/src/utils/steamgriddb.py @@ -103,11 +103,11 @@ class SGDBHelper: image_trunk = shared.covers_dir / game.game_id 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") # 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 # Get ID for the game From 7598f1ea713a54da24144f37d94fb317ec026dd9 Mon Sep 17 00:00:00 2001 From: Rilic Date: Tue, 1 Aug 2023 11:30:39 +0100 Subject: [PATCH 51/59] Convert RetroArch importer to new format --- src/importer/sources/retroarch_source.py | 67 ++++++++++++++---------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 6481e90..ccdb467 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -24,16 +24,21 @@ from hashlib import md5 from json import JSONDecodeError from pathlib import Path from time import time +from typing import NamedTuple from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game -from src.importer.sources.location import Location, UnresolvableLocationError -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.location import ( + Location, + LocationSubPath, + UnresolvableLocationError, +) +from src.importer.sources.source import Source, SourceIterable from src.importer.sources.steam_source import SteamSource -class RetroarchSourceIterator(SourceIterator): +class RetroarchSourceIterable(SourceIterable): source: "RetroarchSource" def get_config_value(self, key: str, config_data: str): @@ -43,11 +48,11 @@ class RetroarchSourceIterator(SourceIterator): raise KeyError(f"Key not found in RetroArch config: {key}") - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): added_time = int(time()) bad_playlists = set() - config_file = self.source.config_location["config"] + config_file = self.source.locations.config["retroarch.cfg"] with config_file.open(encoding="utf-8") as open_file: config_data = open_file.read() @@ -91,7 +96,7 @@ class RetroarchSourceIterator(SourceIterator): game_id = md5(item["path"].encode("utf-8")).hexdigest() values = { - "source": self.source.id, + "source": self.source.source_id, "added": added_time, "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), @@ -127,33 +132,41 @@ class RetroarchSourceIterator(SourceIterator): ) +class RetroarchLocations(NamedTuple): + config: Location + + class RetroarchSource(Source): name = _("RetroArch") + source_id = "retroarch" available_on = {"linux", "windows"} - iterator_class = RetroarchSourceIterator + iterable_class = RetroarchSourceIterable - config_location = Location( - schema_key="retroarch-location", - candidates=[ - shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", - shared.config_dir / "retroarch", - shared.home / ".config" / "retroarch", - Path("C:\\RetroArch-Win64"), - Path("C:\\RetroArch-Win32"), - shared.local_appdata_dir - / "Packages" - / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" - / "LocalState", - ], - paths={ - "config": (False, "retroarch.cfg"), - }, + locations = RetroarchLocations( + Location( + schema_key="retroarch-location", + candidates=[ + shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", + shared.config_dir / "retroarch", + shared.home / ".config" / "retroarch", + Path("C:\\RetroArch-Win64"), + Path("C:\\RetroArch-Win32"), + shared.local_appdata_dir + / "Packages" + / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" + / "LocalState", + ], + paths={ + "retroarch.cfg": LocationSubPath("retroarch.cfg"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) ) @property def executable_format(self): - self.config_location.resolve() - is_flatpak = self.config_location.root.is_relative_to(shared.flatpak_dir) + self.locations.config.resolve() + is_flatpak = self.locations.config.root.is_relative_to(shared.flatpak_dir) base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" args = '-L "{core_path}" "{rom_path}"' return f"{base} {args}" @@ -162,13 +175,13 @@ class RetroarchSource(Source): super().__init__() try: - self.config_location.candidates.append(self.get_steam_location()) + self.locations.config.candidates.append(self.get_steam_location()) except (OSError, KeyError, UnresolvableLocationError): pass def get_steam_location(self) -> str: # Find steam location - libraryfolders = SteamSource().data_location["libraryfolders.vdf"] + libraryfolders = SteamSource().locations.data["libraryfolders.vdf"] library_path = "" with open(libraryfolders, "r", encoding="utf-8") as open_file: From 8de7226a2f044c64674557ba8bd000964e59250f Mon Sep 17 00:00:00 2001 From: Rilic Date: Tue, 1 Aug 2023 11:56:15 +0100 Subject: [PATCH 52/59] Change "windows" to "win32" --- src/importer/sources/retroarch_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index ccdb467..a64c906 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -139,7 +139,7 @@ class RetroarchLocations(NamedTuple): class RetroarchSource(Source): name = _("RetroArch") source_id = "retroarch" - available_on = {"linux", "windows"} + available_on = {"linux", "win32"} iterable_class = RetroarchSourceIterable locations = RetroarchLocations( From 2e844b2d06e220e75522a7b956a8867e871d3f37 Mon Sep 17 00:00:00 2001 From: Rilic Date: Thu, 3 Aug 2023 19:27:55 +0100 Subject: [PATCH 53/59] Improve RetroArch Steam support And remove Windows support. For now. --- src/importer/sources/retroarch_source.py | 39 ++++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index a64c906..578d9b9 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -43,6 +43,9 @@ class RetroarchSourceIterable(SourceIterable): def get_config_value(self, key: str, config_data: str): for item in re.findall(f'{key}\\s*=\\s*"(.*)"\n', config_data, re.IGNORECASE): + if item.startswith(":"): + item = item.replace(":", str(self.source.locations.config.root)) + logging.debug(str(item)) return item @@ -139,7 +142,7 @@ class RetroarchLocations(NamedTuple): class RetroarchSource(Source): name = _("RetroArch") source_id = "retroarch" - available_on = {"linux", "win32"} + available_on = {"linux"} iterable_class = RetroarchSourceIterable locations = RetroarchLocations( @@ -149,12 +152,14 @@ class RetroarchSource(Source): shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", shared.config_dir / "retroarch", shared.home / ".config" / "retroarch", - Path("C:\\RetroArch-Win64"), - Path("C:\\RetroArch-Win32"), - shared.local_appdata_dir - / "Packages" - / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" - / "LocalState", + # TODO: Windows support, waiting for executable path setting improvement + # Path("C:\\RetroArch-Win64"), + # Path("C:\\RetroArch-Win32"), + # TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563) + # shared.local_appdata_dir + # / "Packages" + # / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" + # / "LocalState", ], paths={ "retroarch.cfg": LocationSubPath("retroarch.cfg"), @@ -182,18 +187,20 @@ class RetroarchSource(Source): def get_steam_location(self) -> str: # Find steam location libraryfolders = SteamSource().locations.data["libraryfolders.vdf"] - library_path = "" - + parse_apps = False with open(libraryfolders, "r", encoding="utf-8") as open_file: + # Search each line for a library path and store it each time a new one is found. for line in open_file: if '"path"' in line: library_path = re.findall( '"path"\\s+"(.*)"\n', line, re.IGNORECASE )[0] - elif "1118310" in line: - break - - if library_path: - return Path(f"{library_path}/steamapps/common/RetroArch") - - raise ValueError("No Steam RetroArch installed.") + elif '"apps"' in line: + parse_apps = True + elif parse_apps and "}" in line: + parse_apps = False + # Stop searching, as the library path directly above the appid has been found. + elif parse_apps and '"1118310"' in line: + return Path(f"{library_path}/steamapps/common/RetroArch") + else: + logging.debug("Steam RetroArch not installed.") From f43d8ff90729d7a9322b8c4947e61b4f26f3617b Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sun, 6 Aug 2023 16:24:34 +0200 Subject: [PATCH 54/59] Fix for RetroArch Steam not found --- src/importer/sources/retroarch_source.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 578d9b9..96b16e1 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -178,13 +178,23 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() - try: self.locations.config.candidates.append(self.get_steam_location()) except (OSError, KeyError, UnresolvableLocationError): - pass + logging.debug("Steam isn't installed") + except ValueError as error: + logging.debug("RetroArch Steam location candiate not found", exc_info=error) def get_steam_location(self) -> str: + """ + Get the RetroArch installed via Steam location + + :raise UnresolvableLocationError: if steam isn't installed + :raise KeyError: if there is no libraryfolders.vdf subpath + :raise OSError: if libraryfolders.vdf can't be opened + :raise ValueError: if RetroArch isn't installed through Steam + """ + # Find steam location libraryfolders = SteamSource().locations.data["libraryfolders.vdf"] parse_apps = False @@ -202,5 +212,5 @@ class RetroarchSource(Source): # Stop searching, as the library path directly above the appid has been found. elif parse_apps and '"1118310"' in line: return Path(f"{library_path}/steamapps/common/RetroArch") - else: - logging.debug("Steam RetroArch not installed.") + # Not found + raise ValueError("RetroArch not found in Steam library") From 70d8d91b5345433556d2cb7f772a509dc397c368 Mon Sep 17 00:00:00 2001 From: kramo Date: Tue, 8 Aug 2023 13:44:27 +0200 Subject: [PATCH 55/59] Use Libadwaita widgets for details window --- data/gtk/details-window.blp | 42 +++++++++++-------------------------- src/details_window.py | 10 ++++----- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/data/gtk/details-window.blp b/data/gtk/details-window.blp index ecddb13..d84b4f8 100644 --- a/data/gtk/details-window.blp +++ b/data/gtk/details-window.blp @@ -2,7 +2,7 @@ using Gtk 4.0; using Adw 1; template $DetailsWindow : Adw.Window { - default-width: 500; + default-width: 480; // Same as Nautilus' properties window default-height: -1; modal: true; @@ -97,34 +97,20 @@ template $DetailsWindow : Adw.Window { } } - Adw.PreferencesGroup title_group { - title: _("Title"); - description: _("The title of the game"); - - Entry name { - accessibility { - label: _("Title"); - } + Adw.PreferencesGroup { + Adw.EntryRow name { + title: _("Title"); + } + Adw.EntryRow developer { + title: _("Developer (optional)"); } } + Adw.PreferencesGroup { + Adw.EntryRow executable { + title: _("Executable"); - Adw.PreferencesGroup developer_group { - title: _("Developer"); - description: _("The developer or publisher (optional)"); - - Entry developer { - accessibility { - label: _("Developer"); - } - } - } - - Adw.PreferencesGroup exec_group { - title: _("Executable"); - description: _("File to open or command to run when launching the game"); - - [header-suffix] - Gtk.MenuButton exec_info_button { + [suffix] + Gtk.MenuButton exec_info_button { valign: center; icon-name: "help-about-symbolic"; tooltip-text: _("More Info"); @@ -150,10 +136,6 @@ template $DetailsWindow : Adw.Window { ] } - Entry executable { - accessibility { - label: _("Executable"); - } } } } diff --git a/src/details_window.py b/src/details_window.py index 33553fa..c437d53 100644 --- a/src/details_window.py +++ b/src/details_window.py @@ -64,7 +64,7 @@ class DetailsWindow(Adw.Window): self.set_transient_for(self.win) if self.game: - self.set_title(_("Edit Game Details")) + self.set_title(_("Game Details")) self.name.set_text(self.game.name) if self.game.developer: self.developer.set_text(self.game.developer) @@ -76,7 +76,7 @@ class DetailsWindow(Adw.Window): self.cover_button_delete_revealer.set_reveal_child(True) else: self.set_title(_("Add New Game")) - self.apply_button.set_label(_("Confirm")) + self.apply_button.set_label(_("Add")) image_filter = Gtk.FileFilter(name=_("Images")) for extension in Image.registered_extensions(): @@ -123,9 +123,9 @@ class DetailsWindow(Adw.Window): self.cover_button_edit.connect("clicked", self.choose_cover) self.apply_button.connect("clicked", self.apply_preferences) - self.name.connect("activate", self.focus_executable) - self.developer.connect("activate", self.focus_executable) - self.executable.connect("activate", self.apply_preferences) + self.name.connect("entry-activated", self.focus_executable) + self.developer.connect("entry-activated", self.focus_executable) + self.executable.connect("entry-activated", self.apply_preferences) self.set_focus(self.name) self.present() From fe07f4f5711bf3e54d41a4b064633d1a288c878b Mon Sep 17 00:00:00 2001 From: kramo Date: Sun, 13 Aug 2023 12:23:02 +0200 Subject: [PATCH 56/59] Add Rilic to about window --- src/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index 1f2c0ac..b4b7d9b 100644 --- a/src/main.py +++ b/src/main.py @@ -44,9 +44,9 @@ from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.steam_source import SteamSource from src.logging.setup import log_system_info, setup_logging from src.preferences import PreferencesWindow +from src.store.managers.cover_manager import CoverManager from src.store.managers.display_manager import DisplayManager from src.store.managers.file_manager import FileManager -from src.store.managers.cover_manager import CoverManager from src.store.managers.sgdb_manager import SGDBManager from src.store.managers.steam_api_manager import SteamAPIManager from src.store.store import Store @@ -173,9 +173,10 @@ class CartridgesApplication(Adw.Application): developers=[ "kramo https://kramo.hu", "Geoffrey Coulaud https://geoffrey-coulaud.fr", + "Rilic https://github.com/RilicTheFox", "Arcitec https://github.com/Arcitec", - "Domenico https://github.com/Domefemia", "Paweł Lidwin https://github.com/imLinguin", + "Domenico https://github.com/Domefemia", "Rafael Mardojai CM https://mardojai.com", ], designers=("kramo https://kramo.hu",), From fefa9d27bdd36e293dbefbfd719fb3f86820db39 Mon Sep 17 00:00:00 2001 From: kramo Date: Sun, 13 Aug 2023 12:30:45 +0200 Subject: [PATCH 57/59] Update translations --- po/POTFILES | 1 + po/cartridges.pot | 104 +++++++++++++++++++++------------------------- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/po/POTFILES b/po/POTFILES index 6b25bb0..d48bf15 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -16,4 +16,5 @@ src/preferences.py src/utils/create_dialog.py src/importer/sources/source.py +src/importer/sources/location.py src/store/managers/sgdb_manager.py \ No newline at end of file diff --git a/po/cartridges.pot b/po/cartridges.pot index 6943828..f554e1f 100644 --- a/po/cartridges.pot +++ b/po/cartridges.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-25 20:33+0200\n" +"POT-Creation-Date: 2023-08-13 12:29+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,7 +19,7 @@ msgstr "" #: data/hu.kramo.Cartridges.desktop.in:3 #: data/hu.kramo.Cartridges.metainfo.xml.in:6 data/gtk/window.blp:47 -#: src/main.py:170 +#: src/main.py:169 msgid "Cartridges" msgstr "" @@ -48,11 +48,12 @@ msgstr "" msgid "Library" msgstr "" -#: data/hu.kramo.Cartridges.metainfo.xml.in:34 src/details_window.py:67 +#: data/hu.kramo.Cartridges.metainfo.xml.in:34 msgid "Edit Game Details" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:38 data/gtk/window.blp:71 +#: src/details_window.py:67 msgid "Game Details" msgstr "" @@ -73,32 +74,19 @@ msgstr "" msgid "Delete Cover" msgstr "" -#: data/gtk/details-window.blp:101 data/gtk/details-window.blp:106 -#: data/gtk/game.blp:80 +#: data/gtk/details-window.blp:102 data/gtk/game.blp:80 msgid "Title" msgstr "" -#: data/gtk/details-window.blp:102 -msgid "The title of the game" +#: data/gtk/details-window.blp:105 +msgid "Developer (optional)" msgstr "" -#: data/gtk/details-window.blp:112 data/gtk/details-window.blp:117 -msgid "Developer" -msgstr "" - -#: data/gtk/details-window.blp:113 -msgid "The developer or publisher (optional)" -msgstr "" - -#: data/gtk/details-window.blp:123 data/gtk/details-window.blp:155 +#: data/gtk/details-window.blp:110 msgid "Executable" msgstr "" -#: data/gtk/details-window.blp:124 -msgid "File to open or command to run when launching the game" -msgstr "" - -#: data/gtk/details-window.blp:130 +#: data/gtk/details-window.blp:116 msgid "More Info" msgstr "" @@ -140,7 +128,7 @@ msgstr "" msgid "Shortcuts" msgstr "" -#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:113 +#: data/gtk/help-overlay.blp:34 src/game.py:102 src/preferences.py:118 msgid "Undo" msgstr "" @@ -168,7 +156,7 @@ msgstr "" msgid "Remove game" msgstr "" -#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:277 +#: data/gtk/preferences.blp:13 data/gtk/preferences.blp:291 msgid "Behavior" msgstr "" @@ -219,7 +207,7 @@ msgstr "" #: data/gtk/preferences.blp:96 data/gtk/preferences.blp:110 #: data/gtk/preferences.blp:151 data/gtk/preferences.blp:201 #: data/gtk/preferences.blp:215 data/gtk/preferences.blp:229 -#: data/gtk/preferences.blp:243 +#: data/gtk/preferences.blp:243 data/gtk/preferences.blp:257 msgid "Install Location" msgstr "" @@ -272,38 +260,42 @@ msgid "Legendary" msgstr "" #: data/gtk/preferences.blp:239 +msgid "RetroArch" +msgstr "" + +#: data/gtk/preferences.blp:253 msgid "Flatpak" msgstr "" -#: data/gtk/preferences.blp:252 +#: data/gtk/preferences.blp:266 msgid "Import Game Launchers" msgstr "" -#: data/gtk/preferences.blp:265 +#: data/gtk/preferences.blp:279 msgid "SteamGridDB" msgstr "" -#: data/gtk/preferences.blp:269 +#: data/gtk/preferences.blp:283 msgid "Authentication" msgstr "" -#: data/gtk/preferences.blp:272 +#: data/gtk/preferences.blp:286 msgid "API Key" msgstr "" -#: data/gtk/preferences.blp:280 +#: data/gtk/preferences.blp:294 msgid "Use SteamGridDB" msgstr "" -#: data/gtk/preferences.blp:281 +#: data/gtk/preferences.blp:295 msgid "Download images when adding or importing games" msgstr "" -#: data/gtk/preferences.blp:290 +#: data/gtk/preferences.blp:304 msgid "Prefer Over Official Images" msgstr "" -#: data/gtk/preferences.blp:299 +#: data/gtk/preferences.blp:313 msgid "Prefer Animated Images" msgstr "" @@ -419,7 +411,7 @@ msgid "Add New Game" msgstr "" #: src/details_window.py:79 -msgid "Confirm" +msgid "Add" msgstr "" #. Translate this string as you would translate "file" @@ -489,43 +481,28 @@ msgstr "" msgid "{} removed" msgstr "" -#: src/preferences.py:112 +#: src/preferences.py:117 msgid "All games removed" msgstr "" -#: src/preferences.py:160 +#: src/preferences.py:166 msgid "" "An API key is required to use SteamGridDB. You can generate one {}here{}." msgstr "" -#: src/preferences.py:285 +#: src/preferences.py:295 msgid "Installation Not Found" msgstr "" -#: src/preferences.py:287 +#: src/preferences.py:297 msgid "Select a valid directory." msgstr "" -#: src/preferences.py:349 +#: src/preferences.py:363 msgid "Invalid Directory" msgstr "" -#. The variable is the name of the source -#: src/preferences.py:353 -msgid "Select the {} cache directory." -msgstr "" - -#. The variable is the name of the source -#: src/preferences.py:356 -msgid "Select the {} configuration directory." -msgstr "" - -#. The variable is the name of the source -#: src/preferences.py:359 -msgid "Select the {} data directory." -msgstr "" - -#: src/preferences.py:365 +#: src/preferences.py:369 msgid "Set Location" msgstr "" @@ -533,10 +510,25 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/store/managers/sgdb_manager.py:47 +#. The variable is the name of the source +#: src/importer/sources/location.py:33 +msgid "Select the {} cache directory." +msgstr "" + +#. The variable is the name of the source +#: src/importer/sources/location.py:35 +msgid "Select the {} configuration directory." +msgstr "" + +#. The variable is the name of the source +#: src/importer/sources/location.py:37 +msgid "Select the {} data directory." +msgstr "" + +#: src/store/managers/sgdb_manager.py:46 msgid "Couldn't Authenticate SteamGridDB" msgstr "" -#: src/store/managers/sgdb_manager.py:48 +#: src/store/managers/sgdb_manager.py:47 msgid "Verify your API key in preferences" msgstr "" From 0098669ab66dfadd24b0e1c69318a1e6304c5160 Mon Sep 17 00:00:00 2001 From: kramo Date: Sun, 13 Aug 2023 12:31:44 +0200 Subject: [PATCH 58/59] Update Rilic's link --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index b4b7d9b..958aa20 100644 --- a/src/main.py +++ b/src/main.py @@ -173,7 +173,7 @@ class CartridgesApplication(Adw.Application): developers=[ "kramo https://kramo.hu", "Geoffrey Coulaud https://geoffrey-coulaud.fr", - "Rilic https://github.com/RilicTheFox", + "Rilic https://rilic.red", "Arcitec https://github.com/Arcitec", "Paweł Lidwin https://github.com/imLinguin", "Domenico https://github.com/Domefemia", From 78199267b3b8e1042a092039c084cc2d3cf48f0a Mon Sep 17 00:00:00 2001 From: kramo Date: Sun, 13 Aug 2023 12:49:35 +0200 Subject: [PATCH 59/59] Add RertoArch to keywords --- data/hu.kramo.Cartridges.desktop.in | 2 +- po/cartridges.pot | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/hu.kramo.Cartridges.desktop.in b/data/hu.kramo.Cartridges.desktop.in index 7341def..0c0c75a 100644 --- a/data/hu.kramo.Cartridges.desktop.in +++ b/data/hu.kramo.Cartridges.desktop.in @@ -7,5 +7,5 @@ Icon=@APP_ID@ Terminal=false Type=Application Categories=GNOME;GTK;Game; -Keywords=gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary; +Keywords=gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;retroarch; StartupNotify=true diff --git a/po/cartridges.pot b/po/cartridges.pot index f554e1f..2308909 100644 --- a/po/cartridges.pot +++ b/po/cartridges.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cartridges\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-08-13 12:29+0200\n" +"POT-Creation-Date: 2023-08-13 12:49+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -33,7 +33,8 @@ msgid "Launch all your games" msgstr "" #: data/hu.kramo.Cartridges.desktop.in:11 -msgid "gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;" +msgid "" +"gaming;launcher;steam;lutris;heroic;bottles;itch;flatpak;legendary;retroarch;" msgstr "" #: data/hu.kramo.Cartridges.metainfo.xml.in:9