From 24dd082681492645f1a2643e5cd41e8cd18c786a Mon Sep 17 00:00:00 2001 From: kramo Date: Thu, 2 Feb 2023 18:29:09 +0100 Subject: [PATCH] Implement Bottles parser --- data/hu.kramo.Cartridges.gschema.xml | 3 + hu.kramo.Cartridges.json | 4 +- python3-PyYAML.json | 14 +++++ src/main.py | 87 ++++++++++++++------------- src/meson.build | 1 + src/utils/bottles_parser.py | 90 ++++++++++++++++++++++++++++ src/utils/heroic_parser.py | 3 + src/utils/steam_parser.py | 3 +- src/window.blp | 23 ++++--- 9 files changed, 179 insertions(+), 49 deletions(-) create mode 100644 python3-PyYAML.json create mode 100644 src/utils/bottles_parser.py diff --git a/data/hu.kramo.Cartridges.gschema.xml b/data/hu.kramo.Cartridges.gschema.xml index 3d112f4..b35be61 100644 --- a/data/hu.kramo.Cartridges.gschema.xml +++ b/data/hu.kramo.Cartridges.gschema.xml @@ -18,6 +18,9 @@ "~/.var/app/com.heroicgameslauncher.hgl/config/heroic/" + + + "~/.var/app/com.usebottles.bottles/data/bottles/" diff --git a/hu.kramo.Cartridges.json b/hu.kramo.Cartridges.json index 51185ab..8cd34f1 100644 --- a/hu.kramo.Cartridges.json +++ b/hu.kramo.Cartridges.json @@ -11,7 +11,8 @@ "--socket=wayland", "--talk-name=org.freedesktop.Flatpak", "--filesystem=~/.steam/steam/", - "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/" + "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/", + "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/" ], "cleanup" : [ "/include", @@ -25,6 +26,7 @@ "*.a" ], "modules" : [ + "python3-PyYAML.json", { "name" : "cartridges", "builddir" : true, diff --git a/python3-PyYAML.json b/python3-PyYAML.json new file mode 100644 index 0000000..8393db4 --- /dev/null +++ b/python3-PyYAML.json @@ -0,0 +1,14 @@ +{ + "name": "python3-PyYAML", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"PyYAML>=6.0\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz", + "sha256": "68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2" + } + ] +} \ No newline at end of file diff --git a/src/main.py b/src/main.py index c08e061..32eaedf 100644 --- a/src/main.py +++ b/src/main.py @@ -31,6 +31,7 @@ from .save_games import save_games from .run_command import run_command from .steam_parser import steam_parser from .heroic_parser import heroic_parser +from .bottles_parser import bottles_parser from .create_details_window import create_details_window class CartridgesApplication(Adw.Application): @@ -41,6 +42,7 @@ class CartridgesApplication(Adw.Application): self.create_action("preferences", self.on_preferences_action) self.create_action("steam_import", self.on_steam_import_action) self.create_action("heroic_import", self.on_heroic_import_action) + self.create_action("bottles_import", self.on_bottles_import_action) self.create_action("launch_game", self.on_launch_game_action) self.create_action("hide_game", self.on_hide_game_action) self.create_action("edit_details", self.on_edit_details_action) @@ -50,26 +52,26 @@ class CartridgesApplication(Adw.Application): def do_activate(self): # Create the main window - win = self.props.active_window - if not win: - win = CartridgesWindow(application=self) + self.win = self.props.active_window + if not self.win: + self.win = CartridgesWindow(application=self) - win.present() + self.win.present() # Create actions for the main window - self.create_action("show_hidden", win.on_show_hidden_action, None, win) - self.create_action("go_back", win.on_go_back_action, ["Left"], win) - self.create_action("go_to_parent", win.on_go_to_parent_action, ["Up"], win) - self.create_action("toggle_search", win.on_toggle_search_action, ["f"], win) - self.create_action("escape", win.on_escape_action, ["Escape"], win) - self.create_action("undo_remove", win.on_undo_remove_action, ["z"], win) - win.sort = Gio.SimpleAction.new_stateful("sort_by", GLib.VariantType.new("s"), GLib.Variant("s", "a-z")) - win.add_action(win.sort) - win.sort.connect("activate", win.on_sort_action) - win.on_sort_action(win.sort, win.schema.get_value("sort-mode")) + self.create_action("show_hidden", self.win.on_show_hidden_action, None, self.win) + self.create_action("go_back", self.win.on_go_back_action, ["Left"], self.win) + self.create_action("go_to_parent", self.win.on_go_to_parent_action, ["Up"], self.win) + self.create_action("toggle_search", self.win.on_toggle_search_action, ["f"], self.win) + self.create_action("escape", self.win.on_escape_action, ["Escape"], self.win) + self.create_action("undo_remove", self.win.on_undo_remove_action, ["z"], self.win) + self.win.sort = Gio.SimpleAction.new_stateful("sort_by", GLib.VariantType.new("s"), GLib.Variant("s", "a-z")) + self.win.add_action(self.win.sort) + self.win.sort.connect("activate", self.win.on_sort_action) + self.win.on_sort_action(self.win.sort, self.win.schema.get_value("sort-mode")) def on_about_action(self, widget, callback=None): - about = Adw.AboutWindow(transient_for=self.props.active_window, + about = Adw.AboutWindow(transient_for=self.win, application_name="Cartridges", application_icon="hu.kramo.Cartridges", developer_name="kramo", @@ -83,62 +85,67 @@ class CartridgesApplication(Adw.Application): about.present() def on_preferences_action(self, widget, callback=None): - PreferencesWindow(self.props.active_window).present() + PreferencesWindow(self.win).present() def on_steam_import_action(self, widget, callback=None): - games = steam_parser(self.props.active_window, self.on_steam_import_action) + games = steam_parser(self.win, self.on_steam_import_action) save_games(games) - self.props.active_window.update_games(games.keys()) + self.win.update_games(games.keys()) def on_heroic_import_action(self, widget, callback=None): - games = heroic_parser(self.props.active_window, self.on_heroic_import_action) + games = heroic_parser(self.win, self.on_heroic_import_action) save_games(games) - self.props.active_window.update_games(games.keys()) + self.win.update_games(games.keys()) + + def on_bottles_import_action(self, widget, callback=None): + games = bottles_parser(self.win, self.on_bottles_import_action) + save_games(games) + self.win.update_games(games.keys()) def on_launch_game_action(self, widget, callback=None): # Launch the game and update the last played value - self.props.active_window.games[self.props.active_window.active_game_id]["last_played"] = int(time.time()) - save_games({self.props.active_window.active_game_id : self.props.active_window.games[self.props.active_window.active_game_id]}) - self.props.active_window.update_games([self.props.active_window.active_game_id]) - run_command(self.props.active_window, self.props.active_window.games[self.props.active_window.active_game_id]["executable"]) + self.win.games[self.win.active_game_id]["last_played"] = int(time.time()) + save_games({self.win.active_game_id : self.win.games[self.win.active_game_id]}) + self.win.update_games([self.win.active_game_id]) + run_command(self.win, self.win.games[self.win.active_game_id]["executable"]) - if self.props.active_window.stack.get_visible_child() == self.props.active_window.overview: - self.props.active_window.show_overview(None, self.props.active_window.active_game_id) + if self.win.stack.get_visible_child() == self.win.overview: + self.win.show_overview(None, self.win.active_game_id) def on_hide_game_action(self, widget, callback=None): - if self.props.active_window.stack.get_visible_child() == self.props.active_window.overview: - self.props.active_window.on_go_back_action(None, None) - toggle_hidden(self.props.active_window.active_game_id) - self.props.active_window.update_games([self.props.active_window.active_game_id]) + if self.win.stack.get_visible_child() == self.win.overview: + self.win.on_go_back_action(None, None) + toggle_hidden(self.win.active_game_id) + self.win.update_games([self.win.active_game_id]) def on_edit_details_action(self, widget, callback=None): - create_details_window(self.props.active_window, self.props.active_window.active_game_id) + create_details_window(self.win, self.win.active_game_id) def on_add_game_action(self, widget, callback=None): - create_details_window(self.props.active_window) + create_details_window(self.win) def on_remove_game_action(self, widget, callback=None): # Add "removed=True" to the game properties so it can be deleted on next init - game_id = self.props.active_window.active_game_id + game_id = self.win.active_game_id open_file = open(os.path.join(os.path.join(os.environ.get("XDG_DATA_HOME"), "cartridges", "games", game_id + ".json")), "r") data = json.loads(open_file.read()) open_file.close() data["removed"] = True save_games({game_id : data}) - self.props.active_window.update_games([game_id]) - if self.props.active_window.stack.get_visible_child() == self.props.active_window.overview: - self.props.active_window.on_go_back_action(None, None) + self.win.update_games([game_id]) + if self.win.stack.get_visible_child() == self.win.overview: + self.win.on_go_back_action(None, None) # Create toast for undoing the remove action - toast = Adw.Toast.new(self.props.active_window.games[game_id]["name"] + " " + (_("removed"))) + toast = Adw.Toast.new(self.win.games[game_id]["name"] + " " + (_("removed"))) toast.set_button_label(_("Undo")) - toast.connect("button-clicked", self.props.active_window.on_undo_remove_action, game_id) + toast.connect("button-clicked", self.win.on_undo_remove_action, game_id) toast.set_priority(Adw.ToastPriority.HIGH) - self.props.active_window.toasts[game_id] = toast - self.props.active_window.toast_overlay.add_toast(toast) + self.win.toasts[game_id] = toast + self.win.toast_overlay.add_toast(toast) def on_quit_action(self, widget, callback=None): self.quit() diff --git a/src/meson.build b/src/meson.build index c49ca9a..be9faba 100644 --- a/src/meson.build +++ b/src/meson.build @@ -45,6 +45,7 @@ cartridges_sources = [ 'game.py', 'utils/steam_parser.py', 'utils/heroic_parser.py', + 'utils/bottles_parser.py', 'utils/run_command.py', 'utils/get_games.py', 'utils/get_cover.py', diff --git a/src/utils/bottles_parser.py b/src/utils/bottles_parser.py new file mode 100644 index 0000000..73c8f00 --- /dev/null +++ b/src/utils/bottles_parser.py @@ -0,0 +1,90 @@ +# bottles_parser.py +# +# Copyright 2022 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 + +def bottles_parser(parent_widget, action): + import os, yaml, time + + from gi.repository import Gtk, GLib + + from .create_dialog import create_dialog + from .save_cover import save_cover + + schema = parent_widget.schema + bottles_dir = os.path.expanduser(os.path.join(schema.get_string("bottles-location"))) + + def bottles_not_found(): + filechooser = Gtk.FileDialog.new() + + def set_bottles_dir(source, result, _): + try: + schema.set_string("bottles-location", filechooser.select_folder_finish(result).get_path()) + bottles_dir = bottles_dir = os.path.join(schema.get_string("bottles-location")) + action(None, None) + except GLib.GError: + return + + def choose_folder(widget): + filechooser.select_folder(parent_widget, None, set_bottles_dir, None) + + def response(widget, response): + if response == "choose_folder": + choose_folder(widget) + + create_dialog(parent_widget, _("Couldn't Import Games"), _("Bottles directory cannot be found."), "choose_folder", _("Set Bottles Location")).connect("response", response) + + if os.path.isfile(os.path.join(bottles_dir, "library.yml")): + pass + else: + bottles_not_found() + return {} + + datatypes = ["path", "id", "name", "thumbnail"] + bottles_games = {} + current_time = int(time.time()) + + open_file = open(os.path.join(bottles_dir, "library.yml"), "r") + data = open_file.read() + open_file.close() + + library = yaml.load(data, Loader=yaml.Loader) + + for game in library: + game = library[game] + values = {} + + values["game_id"] = "bottles_" + game["id"] + values["name"] = game["name"] + values["executable"] = "xdg-open bottles:run/" + game["bottle"]["name"] + "/" + game["name"] + values["hidden"] = False + values["source"] = "bottles" + values["added"] = current_time + values["last_played"] = 0 + + if game["thumbnail"]: + values["pixbuf_options"] = save_cover(values, parent_widget, os.path.join(bottles_dir, "bottles", game["bottle"]["path"], "grids", game["thumbnail"].replace("grid:", ""))) + + bottles_games[values["game_id"]] = values + + if len(bottles_games) == 0: + create_dialog(parent_widget, _("No Games Found"), _("No new games found in Bottles library.")) + elif len(bottles_games) == 1: + create_dialog(parent_widget, _("bottles Games Imported"), _("Successfully imported 1 game.")) + elif len(bottles_games) > 1: + create_dialog(parent_widget, _("bottles Games Imported"), _("Successfully imported") + " " + str(len(bottles_games)) + " " + _("games.")) + return bottles_games diff --git a/src/utils/heroic_parser.py b/src/utils/heroic_parser.py index 73e6e62..ec44fc2 100644 --- a/src/utils/heroic_parser.py +++ b/src/utils/heroic_parser.py @@ -99,6 +99,7 @@ def heroic_parser(parent_widget, action): values["pixbuf_options"] = save_cover(values, parent_widget, image_path) break + heroic_games[values["game_id"]] = values # Import GOG games @@ -138,6 +139,7 @@ def heroic_parser(parent_widget, action): values["source"] = "heroic_gog" values["added"] = current_time values["last_played"] = 0 + heroic_games[values["game_id"]] = values # Import sideloaded games @@ -168,6 +170,7 @@ def heroic_parser(parent_widget, action): hashlib.sha256(item["art_square"].encode()).hexdigest()) if os.path.exists(image_path): values["pixbuf_options"] = save_cover(values, parent_widget, image_path) + heroic_games[values["game_id"]] = values if len(heroic_games) == 0: diff --git a/src/utils/steam_parser.py b/src/utils/steam_parser.py index e59f3ad..88300fb 100644 --- a/src/utils/steam_parser.py +++ b/src/utils/steam_parser.py @@ -77,7 +77,7 @@ def steam_parser(parent_widget, action): open_file.close() for datatype in datatypes: value = re.findall("\"" + datatype + "\"\t\t\"(.*)\"\n", data) - values [datatype] = value[0] + values[datatype] = value[0] values["game_id"] = "steam_" + values["appid"] @@ -92,6 +92,7 @@ def steam_parser(parent_widget, action): if os.path.isfile(os.path.join(steam_dir, "appcache", "librarycache", values["appid"] + "_library_600x900.jpg")): values["pixbuf_options"] = save_cover(values, parent_widget, os.path.join(steam_dir, "appcache", "librarycache", values["appid"] + "_library_600x900.jpg")) + steam_games[values["game_id"]] = values if len(steam_games) == 0: diff --git a/src/window.blp b/src/window.blp index 47240c1..1af916b 100644 --- a/src/window.blp +++ b/src/window.blp @@ -360,15 +360,24 @@ menu add_games { label: _("Add Game"); action: "app.add_game"; } + } + section { + submenu { + label: _("Import from"); + item { + label: _("Steam"); + action: "app.steam_import"; + } - item { - label: _("Import From Steam"); - action: "app.steam_import"; - } + item { + label: _("Heroic"); + action: "app.heroic_import"; + } - item { - label: _("Import From Heroic"); - action: "app.heroic_import"; + item { + label: _("Bottles"); + action: "app.bottles_import"; + } } } }