Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -81,6 +81,7 @@ class DetailsWindow(Adw.Window):
|
||||
image_filter = Gtk.FileFilter(name=_("Images"))
|
||||
for extension in Image.registered_extensions():
|
||||
image_filter.add_suffix(extension[1:])
|
||||
image_filter.add_suffix("svg") # Gdk.Texture supports .svg but PIL doesn't
|
||||
|
||||
file_filters = Gio.ListStore.new(Gtk.FileFilter)
|
||||
file_filters.append(image_filter)
|
||||
@@ -154,20 +155,22 @@ class DetailsWindow(Adw.Window):
|
||||
return
|
||||
|
||||
# Increment the number after the game id (eg. imported_1, imported_2)
|
||||
source_id = "imported"
|
||||
numbers = [0]
|
||||
game_id: str
|
||||
for game_id in shared.store.games:
|
||||
for game_id in shared.store.source_games.get(source_id, set()):
|
||||
prefix = "imported_"
|
||||
if not game_id.startswith(prefix):
|
||||
continue
|
||||
numbers.append(int(game_id.replace(prefix, "", 1)))
|
||||
|
||||
game_number = max(numbers) + 1
|
||||
|
||||
self.game = Game(
|
||||
{
|
||||
"game_id": f"imported_{game_number}",
|
||||
"hidden": False,
|
||||
"source": "imported",
|
||||
"source": source_id,
|
||||
"added": int(time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,11 +140,12 @@ class Game(Gtk.Box):
|
||||
def toggle_hidden(self, toast=True):
|
||||
self.hidden = not self.hidden
|
||||
self.save()
|
||||
self.update()
|
||||
|
||||
if self.win.stack.get_visible_child() == self.win.details_view:
|
||||
self.win.on_go_back_action()
|
||||
|
||||
self.update()
|
||||
|
||||
if toast:
|
||||
self.create_toast(
|
||||
# The variable is the title of the game
|
||||
|
||||
@@ -61,8 +61,8 @@ class GameCover:
|
||||
|
||||
if path:
|
||||
if path.suffix == ".gif":
|
||||
task = Gio.Task.new()
|
||||
task.run_in_thread(self.create_func(self.path))
|
||||
self.task = Gio.Task.new()
|
||||
self.task.run_in_thread(self.create_func(self.path))
|
||||
else:
|
||||
self.texture = Gdk.Texture.new_from_filename(str(path))
|
||||
|
||||
@@ -108,6 +108,8 @@ class GameCover:
|
||||
self.pictures.add(picture)
|
||||
if not self.animation:
|
||||
self.set_texture(self.texture)
|
||||
else:
|
||||
self.update_animation((self.task, self.animation))
|
||||
|
||||
def set_texture(self, texture):
|
||||
self.pictures.discard(
|
||||
@@ -131,5 +133,3 @@ class GameCover:
|
||||
self.update_animation,
|
||||
data,
|
||||
)
|
||||
else:
|
||||
data[0].return_value(False)
|
||||
|
||||
@@ -91,6 +91,8 @@ class Importer(ErrorProducer):
|
||||
def run(self):
|
||||
"""Use several Gio.Task to import games from added sources"""
|
||||
|
||||
shared.win.get_application().lookup_action("import").set_enabled(False)
|
||||
|
||||
self.create_dialog()
|
||||
|
||||
# Collect all errors and reset the cancellables for the managers
|
||||
@@ -221,6 +223,7 @@ class Importer(ErrorProducer):
|
||||
self.import_dialog.close()
|
||||
self.summary_toast = self.create_summary_toast()
|
||||
self.create_error_dialog()
|
||||
shared.win.get_application().lookup_action("import").set_enabled(True)
|
||||
|
||||
def create_error_dialog(self):
|
||||
"""Dialog containing all errors raised by importers"""
|
||||
@@ -263,7 +266,7 @@ class Importer(ErrorProducer):
|
||||
list_box = Gtk.ListBox()
|
||||
list_box.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
list_box.set_css_classes(["boxed-list"])
|
||||
list_box.set_margin_top(8)
|
||||
list_box.set_margin_top(9)
|
||||
for error in errors:
|
||||
row = Adw.ActionRow.new()
|
||||
row.set_title(error[0])
|
||||
|
||||
@@ -83,7 +83,7 @@ class BottlesSourceIterator(SourceIterator):
|
||||
class BottlesSource(URLExecutableSource):
|
||||
"""Generic Bottles source"""
|
||||
|
||||
name = "Bottles"
|
||||
name = _("Bottles")
|
||||
iterator_class = BottlesSourceIterator
|
||||
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
|
||||
available_on = {"linux"}
|
||||
|
||||
@@ -40,10 +40,11 @@ class FlatpakSourceIterator(SourceIterator):
|
||||
icon_theme.add_search_path(str(self.source.data_location["icons"]))
|
||||
|
||||
blacklist = (
|
||||
{"hu.kramo.Cartridges"}
|
||||
{"hu.kramo.Cartridges", "hu.kramo.Cartridges.Devel"}
|
||||
if shared.schema.get_boolean("flatpak-import-launchers")
|
||||
else {
|
||||
"hu.kramo.Cartridges",
|
||||
"hu.kramo.Cartridges.Devel",
|
||||
"com.valvesoftware.Steam",
|
||||
"net.lutris.Lutris",
|
||||
"com.heroicgameslauncher.hgl",
|
||||
@@ -113,7 +114,7 @@ class FlatpakSourceIterator(SourceIterator):
|
||||
class FlatpakSource(Source):
|
||||
"""Generic Flatpak source"""
|
||||
|
||||
name = "Flatpak"
|
||||
name = _("Flatpak")
|
||||
iterator_class = FlatpakSourceIterator
|
||||
executable_format = "flatpak run {flatpak_id}"
|
||||
available_on = {"linux"}
|
||||
|
||||
@@ -81,10 +81,10 @@ class HeroicSourceIterator(SourceIterator):
|
||||
runner = entry["runner"]
|
||||
service = self.sub_sources[runner]["service"]
|
||||
values = {
|
||||
"source": self.source.id,
|
||||
"source": f"{self.source.id}_{service}",
|
||||
"added": added_time,
|
||||
"name": entry["title"],
|
||||
"developer": entry["developer"],
|
||||
"developer": entry.get("developer", None),
|
||||
"game_id": self.source.game_id_format.format(
|
||||
service=service, game_id=app_name
|
||||
),
|
||||
@@ -106,14 +106,16 @@ class HeroicSourceIterator(SourceIterator):
|
||||
def generator_builder(self) -> SourceIterationResult:
|
||||
"""Generator method producing games from all the Heroic sub-sources"""
|
||||
|
||||
for sub_source in self.sub_sources.values():
|
||||
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"]):
|
||||
continue
|
||||
# Load games from JSON
|
||||
file = self.source.config_location.root.joinpath(*sub_source["path"])
|
||||
try:
|
||||
library = json.load(file.open())["library"]
|
||||
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))
|
||||
@@ -124,9 +126,11 @@ class HeroicSourceIterator(SourceIterator):
|
||||
for entry in library:
|
||||
try:
|
||||
result = self.game_from_library_entry(entry, added_time)
|
||||
except KeyError:
|
||||
except KeyError as error:
|
||||
# Skip invalid games
|
||||
logging.warning("Invalid Heroic game skipped in %s", str(file))
|
||||
logging.warning(
|
||||
"Invalid Heroic game skipped in %s", str(file), exc_info=error
|
||||
)
|
||||
continue
|
||||
yield result
|
||||
|
||||
@@ -134,7 +138,7 @@ class HeroicSourceIterator(SourceIterator):
|
||||
class HeroicSource(URLExecutableSource):
|
||||
"""Generic Heroic Games Launcher source"""
|
||||
|
||||
name = "Heroic"
|
||||
name = _("Heroic")
|
||||
iterator_class = HeroicSourceIterator
|
||||
url_format = "heroic://launch/{app_name}"
|
||||
available_on = {"linux", "win32"}
|
||||
@@ -155,4 +159,4 @@ class HeroicSource(URLExecutableSource):
|
||||
@property
|
||||
def game_id_format(self) -> str:
|
||||
"""The string format used to construct game IDs"""
|
||||
return self.name.lower() + "_{service}_{game_id}"
|
||||
return self.id + "_{service}_{game_id}"
|
||||
|
||||
@@ -79,7 +79,7 @@ class ItchSourceIterator(SourceIterator):
|
||||
|
||||
|
||||
class ItchSource(URLExecutableSource):
|
||||
name = "Itch"
|
||||
name = _("itch")
|
||||
iterator_class = ItchSourceIterator
|
||||
url_format = "itch://caves/{cave_id}/launch"
|
||||
available_on = {"linux", "win32"}
|
||||
|
||||
@@ -51,7 +51,7 @@ class LegendarySourceIterator(SourceIterator):
|
||||
data = {}
|
||||
|
||||
# Get additional metadata from file (optional)
|
||||
metadata_file = self.source.data_location["metadata"] / f"{app_name}.json"
|
||||
metadata_file = self.source.config_location["metadata"] / f"{app_name}.json"
|
||||
try:
|
||||
metadata = json.load(metadata_file.open())
|
||||
values["developer"] = metadata["metadata"]["developer"]
|
||||
@@ -67,7 +67,7 @@ class LegendarySourceIterator(SourceIterator):
|
||||
|
||||
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
||||
# Open library
|
||||
file = self.source.data_location["installed.json"]
|
||||
file = self.source.config_location["installed.json"]
|
||||
try:
|
||||
library: dict = json.load(file.open())
|
||||
except (JSONDecodeError, OSError):
|
||||
@@ -90,9 +90,9 @@ class LegendarySourceIterator(SourceIterator):
|
||||
|
||||
|
||||
class LegendarySource(Source):
|
||||
name = "Legendary"
|
||||
name = _("Legendary")
|
||||
executable_format = "legendary launch {app_name}"
|
||||
available_on = {"linux", "win32"}
|
||||
available_on = {"linux"}
|
||||
|
||||
iterator_class = LegendarySourceIterator
|
||||
config_location: Location = Location(
|
||||
|
||||
@@ -70,9 +70,9 @@ class LutrisSourceIterator(SourceIterator):
|
||||
"name": row[1],
|
||||
"source": f"{self.source.id}_{row[3]}",
|
||||
"game_id": self.source.game_id_format.format(
|
||||
game_id=row[2], game_internal_id=row[0]
|
||||
runner=row[3], game_id=row[0]
|
||||
),
|
||||
"executable": self.source.executable_format.format(game_id=row[2]),
|
||||
"executable": self.source.executable_format.format(game_id=row[0]),
|
||||
}
|
||||
game = Game(values)
|
||||
|
||||
@@ -90,7 +90,7 @@ class LutrisSourceIterator(SourceIterator):
|
||||
class LutrisSource(URLExecutableSource):
|
||||
"""Generic Lutris source"""
|
||||
|
||||
name = "Lutris"
|
||||
name = _("Lutris")
|
||||
iterator_class = LutrisSourceIterator
|
||||
url_format = "lutris:rungameid/{game_id}"
|
||||
available_on = {"linux"}
|
||||
@@ -123,4 +123,4 @@ class LutrisSource(URLExecutableSource):
|
||||
|
||||
@property
|
||||
def game_id_format(self):
|
||||
return super().game_id_format + "_{game_internal_id}"
|
||||
return self.id + "_{runner}_{game_id}"
|
||||
|
||||
@@ -110,7 +110,7 @@ class SteamSourceIterator(SourceIterator):
|
||||
|
||||
|
||||
class SteamSource(URLExecutableSource):
|
||||
name = "Steam"
|
||||
name = _("Steam")
|
||||
available_on = {"linux", "win32"}
|
||||
iterator_class = SteamSourceIterator
|
||||
url_format = "steam://rungameid/{game_id}"
|
||||
@@ -118,9 +118,9 @@ class SteamSource(URLExecutableSource):
|
||||
data_location = Location(
|
||||
schema_key="steam-location",
|
||||
candidates=(
|
||||
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
|
||||
shared.home / ".steam" / "steam",
|
||||
shared.data_dir / "Steam",
|
||||
shared.home / ".steam",
|
||||
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
|
||||
shared.programfiles32_dir / "Steam",
|
||||
),
|
||||
paths={
|
||||
|
||||
13
src/main.py
13
src/main.py
@@ -19,6 +19,7 @@
|
||||
|
||||
import json
|
||||
import lzma
|
||||
import os
|
||||
import sys
|
||||
|
||||
import gi
|
||||
@@ -50,6 +51,7 @@ from src.store.managers.online_cover_manager import OnlineCoverManager
|
||||
from src.store.managers.sgdb_manager import SGDBManager
|
||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||
from src.store.store import Store
|
||||
from src.utils.migrate_files_v1_to_v2 import migrate_files_v1_to_v2
|
||||
from src.window import CartridgesWindow
|
||||
|
||||
|
||||
@@ -65,6 +67,12 @@ class CartridgesApplication(Adw.Application):
|
||||
def do_activate(self): # pylint: disable=arguments-differ
|
||||
"""Called on app creation"""
|
||||
|
||||
setup_logging()
|
||||
log_system_info()
|
||||
|
||||
if os.name == "nt":
|
||||
migrate_files_v1_to_v2()
|
||||
|
||||
# Set fallback icon-name
|
||||
Gtk.Window.set_default_icon_name(shared.APP_ID)
|
||||
|
||||
@@ -94,7 +102,7 @@ class CartridgesApplication(Adw.Application):
|
||||
shared.store.add_manager(SteamAPIManager())
|
||||
shared.store.add_manager(OnlineCoverManager())
|
||||
shared.store.add_manager(SGDBManager())
|
||||
shared.store.enable_manager_in_pipelines(FileManager)
|
||||
shared.store.toggle_manager_in_pipelines(FileManager, True)
|
||||
|
||||
# Create actions
|
||||
self.create_actions(
|
||||
@@ -180,6 +188,7 @@ class CartridgesApplication(Adw.Application):
|
||||
# Translators: Replace this with your name for it to show up in the about window
|
||||
translator_credits=_("translator_credits"),
|
||||
debug_info=debug_str,
|
||||
debug_info_filename="cartridges.log",
|
||||
)
|
||||
about.present()
|
||||
|
||||
@@ -282,7 +291,5 @@ class CartridgesApplication(Adw.Application):
|
||||
|
||||
def main(_version):
|
||||
"""App entry point"""
|
||||
setup_logging()
|
||||
log_system_info()
|
||||
app = CartridgesApplication()
|
||||
return app.run(sys.argv)
|
||||
|
||||
@@ -220,10 +220,9 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
self.toast.dismiss()
|
||||
|
||||
def remove_all_games(self, *_args):
|
||||
for game in shared.store.games.values():
|
||||
for game in shared.store:
|
||||
if not game.removed:
|
||||
self.removed_games.add(game)
|
||||
|
||||
game.removed = True
|
||||
game.save()
|
||||
game.update()
|
||||
@@ -351,14 +350,17 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
|
||||
# Bad picked location, inform user
|
||||
else:
|
||||
if location_name == "cache":
|
||||
title = _("Invalid Directory")
|
||||
# The variable is the name of the source
|
||||
subtitle_format = _("Select the {} cache directory.")
|
||||
else:
|
||||
title = _("Invalid Directory")
|
||||
# The variable is the name of the source
|
||||
subtitle_format = _("Select the {} installation directory.")
|
||||
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,
|
||||
|
||||
@@ -34,7 +34,7 @@ state_schema = Gio.Settings.new(APP_ID + ".State")
|
||||
home = Path.home()
|
||||
data_dir = Path(GLib.get_user_data_dir())
|
||||
config_dir = Path(GLib.get_user_config_dir())
|
||||
cache_dir = Path(GLib.get_user_config_dir())
|
||||
cache_dir = Path(GLib.get_user_cache_dir())
|
||||
flatpak_dir = home / ".var" / "app"
|
||||
|
||||
games_dir = data_dir / "cartridges" / "games"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import logging
|
||||
from typing import MutableMapping, Generator, Any
|
||||
|
||||
from src import shared
|
||||
from src.game import Game
|
||||
@@ -31,24 +32,59 @@ class Store:
|
||||
managers: dict[type[Manager], Manager]
|
||||
pipeline_managers: set[Manager]
|
||||
pipelines: dict[str, Pipeline]
|
||||
games: dict[str, Game]
|
||||
source_games: MutableMapping[str, MutableMapping[str, Game]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.managers = {}
|
||||
self.pipeline_managers = set()
|
||||
self.pipelines = {}
|
||||
self.games = {}
|
||||
self.source_games = {}
|
||||
|
||||
def __contains__(self, obj: object) -> bool:
|
||||
"""Check if the game is present in the store with the `in` keyword"""
|
||||
if not isinstance(obj, Game):
|
||||
return False
|
||||
if not (source_mapping := self.source_games.get(obj.source)):
|
||||
return False
|
||||
return obj.game_id in source_mapping
|
||||
|
||||
def __iter__(self) -> Generator[Game, None, None]:
|
||||
"""Iterate through the games in the store with `for ... in`"""
|
||||
for _source_id, games_mapping in self.source_games.items():
|
||||
for _game_id, game in games_mapping.items():
|
||||
yield game
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get the number of games in the store with the `len` builtin"""
|
||||
return sum(len(source_mapping) for source_mapping in self.source_games)
|
||||
|
||||
def __getitem__(self, game_id: str) -> Game:
|
||||
"""Get a game by its id with `store["game_id_goes_here"]`"""
|
||||
for game in iter(self):
|
||||
if game.game_id == game_id:
|
||||
return game
|
||||
raise KeyError("Game not found in store")
|
||||
|
||||
def get(self, game_id: str, default: Any = None) -> Game | Any:
|
||||
"""Get a game by its ID, with a fallback if not found"""
|
||||
try:
|
||||
game = self[game_id]
|
||||
return game
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def add_manager(self, manager: Manager, in_pipeline=True):
|
||||
"""Add a manager to the store"""
|
||||
manager_type = type(manager)
|
||||
self.managers[manager_type] = manager
|
||||
if in_pipeline:
|
||||
self.enable_manager_in_pipelines(manager_type)
|
||||
self.toggle_manager_in_pipelines(manager_type, in_pipeline)
|
||||
|
||||
def enable_manager_in_pipelines(self, manager_type: type[Manager]):
|
||||
"""Make a manager run in new pipelines"""
|
||||
self.pipeline_managers.add(self.managers[manager_type])
|
||||
def toggle_manager_in_pipelines(self, manager_type: type[Manager], enable: bool):
|
||||
"""Change if a manager should run in new pipelines"""
|
||||
if enable:
|
||||
self.pipeline_managers.add(self.managers[manager_type])
|
||||
else:
|
||||
self.pipeline_managers.discard(self.managers[manager_type])
|
||||
|
||||
def cleanup_game(self, game: Game) -> None:
|
||||
"""Remove a game's files"""
|
||||
@@ -74,7 +110,7 @@ class Store:
|
||||
return None
|
||||
|
||||
# Handle game duplicates
|
||||
stored_game = self.games.get(game.game_id)
|
||||
stored_game = self.get(game.game_id)
|
||||
if not stored_game:
|
||||
# New game, do as normal
|
||||
logging.debug("New store game %s (%s)", game.name, game.game_id)
|
||||
@@ -96,11 +132,15 @@ class Store:
|
||||
for signal in manager.signals:
|
||||
game.connect(signal, manager.execute_resilient_manager_logic)
|
||||
|
||||
# Add the game to the store
|
||||
if not game.source in self.source_games:
|
||||
self.source_games[game.source] = {}
|
||||
self.source_games[game.source][game.game_id] = game
|
||||
|
||||
# Run the pipeline for the game
|
||||
if not run_pipeline:
|
||||
return None
|
||||
pipeline = Pipeline(game, additional_data, self.pipeline_managers)
|
||||
self.games[game.game_id] = game
|
||||
self.pipelines[game.game_id] = pipeline
|
||||
pipeline.advance()
|
||||
return pipeline
|
||||
|
||||
128
src/utils/migrate_files_v1_to_v2.py
Normal file
128
src/utils/migrate_files_v1_to_v2.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# migrate_files_v1_to_v2.py
|
||||
#
|
||||
# Copyright 2023 Geoffrey Coulaud
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from src import shared
|
||||
|
||||
old_data_dir = Path.home() / ".local" / "share"
|
||||
old_cartridges_data_dir = old_data_dir / "cartridges"
|
||||
migrated_file_path = old_cartridges_data_dir / ".migrated"
|
||||
old_games_dir = old_cartridges_data_dir / "games"
|
||||
old_covers_dir = old_cartridges_data_dir / "covers"
|
||||
|
||||
|
||||
def migrate_game_covers(game_path: Path):
|
||||
"""Migrate a game covers from a source game path to the current dir"""
|
||||
for suffix in (".tiff", ".gif"):
|
||||
cover_path = old_covers_dir / game_path.with_suffix(suffix).name
|
||||
if not cover_path.is_file():
|
||||
continue
|
||||
destination_cover_path = shared.covers_dir / cover_path.name
|
||||
logging.info("Moving %s -> %s", str(cover_path), str(destination_cover_path))
|
||||
cover_path.rename(destination_cover_path)
|
||||
|
||||
|
||||
def migrate_files_v1_to_v2():
|
||||
"""
|
||||
Migrate user data from the v1.X locations to the latest location.
|
||||
|
||||
Fix for commit 4a204442b5d8ba2e918f8c2605d72e483bf35efd
|
||||
where the windows directories for data, config and cache changed.
|
||||
"""
|
||||
|
||||
# Skip if there is no old dir
|
||||
# Skip if old == current
|
||||
# Skip if already migrated
|
||||
if (
|
||||
not old_data_dir.is_dir()
|
||||
or str(old_data_dir) == str(shared.data_dir)
|
||||
or migrated_file_path.is_file()
|
||||
):
|
||||
return
|
||||
|
||||
logging.info("Migrating data dir %s", str(old_data_dir))
|
||||
|
||||
# Create the current data dir if needed
|
||||
if not shared.data_dir.is_dir():
|
||||
shared.data_dir.mkdir(parents=True)
|
||||
|
||||
old_game_paths = set(old_games_dir.glob("*.json"))
|
||||
old_imported_game_paths = set(
|
||||
filter(lambda path: path.name.startswith("imported_"), old_game_paths)
|
||||
)
|
||||
old_other_game_paths = old_game_paths - old_imported_game_paths
|
||||
|
||||
# Discover current imported games
|
||||
imported_game_number = 0
|
||||
imported_execs = set()
|
||||
for game_path in shared.games_dir.glob("imported_*.json"):
|
||||
try:
|
||||
game_data = json.load(game_path.open("r"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
number = int(game_data["game_id"].replace("imported_", ""))
|
||||
imported_game_number = max(number, imported_game_number)
|
||||
imported_execs.add(game_data["executable"])
|
||||
|
||||
# Migrate imported game files
|
||||
for game_path in old_imported_game_paths:
|
||||
try:
|
||||
game_data = json.load(game_path.open("r"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
|
||||
# Don't migrate if there's a game with the same exec
|
||||
if game_data["executable"] in imported_execs:
|
||||
continue
|
||||
|
||||
# Migrate with updated index
|
||||
imported_game_number += 1
|
||||
game_id = f"imported_{imported_game_number}"
|
||||
game_data["game_id"] = game_id
|
||||
destination_game_path = shared.games_dir / f"{game_id}.json"
|
||||
logging.info(
|
||||
"Moving (updated id) %s -> %s", str(game_path), str(destination_game_path)
|
||||
)
|
||||
json.dump(
|
||||
game_data,
|
||||
destination_game_path.open("w"),
|
||||
indent=4,
|
||||
sort_keys=True,
|
||||
)
|
||||
game_path.unlink()
|
||||
migrate_game_covers(game_path)
|
||||
|
||||
# Migrate all other games
|
||||
for game_path in old_other_game_paths:
|
||||
# Do nothing if already in games dir
|
||||
destination_game_path = shared.games_dir / game_path.name
|
||||
if destination_game_path.exists():
|
||||
continue
|
||||
|
||||
# Else, migrate the game
|
||||
logging.info("Moving %s -> %s", str(game_path), str(destination_game_path))
|
||||
game_path.rename(destination_game_path)
|
||||
migrate_game_covers(game_path)
|
||||
|
||||
# Signal that this dir is migrated
|
||||
migrated_file_path.touch()
|
||||
logging.info("Migration done")
|
||||
@@ -117,7 +117,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
||||
def set_library_child(self):
|
||||
child, hidden_child = self.notice_empty, self.hidden_notice_empty
|
||||
|
||||
for game in shared.store.games.values():
|
||||
for game in shared.store:
|
||||
if game.removed or game.blacklisted:
|
||||
continue
|
||||
if game.hidden:
|
||||
|
||||
Reference in New Issue
Block a user