From 780fd0a09ff356c37394e916dc1d4e99b38ac058 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:22:45 +0200 Subject: [PATCH] Added Lutris import --- data/gtk/preferences.blp | 24 +++++ data/hu.kramo.Cartridges.gschema.xml | 9 ++ hu.kramo.Cartridges.json | 3 + src/main.py | 4 + src/meson.build | 1 + src/preferences.py | 43 ++++++++ src/utils/bottles_parser.py | 4 +- src/utils/heroic_parser.py | 4 +- src/utils/lutris_parser.py | 149 +++++++++++++++++++++++++++ src/utils/save_cover.py | 3 +- src/utils/save_game.py | 3 +- src/utils/steam_parser.py | 4 +- 12 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 src/utils/lutris_parser.py diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index bdd4c70..85dfa1e 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -108,6 +108,30 @@ template PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ExpanderRow lutris_expander_row { + title: _("Lutris"); + show-enable-switch: true; + + Adw.ActionRow { + title: _("Lutris Install Location"); + subtitle: _("Directory to use when importing games"); + + Button lutris_file_chooser_button { + icon-name: "folder-symbolic"; + valign: center; + } + } + Adw.ActionRow { + title: _("Lutris Cache Location"); + subtitle: _("Directory to use when importing game covers"); + + Button lutris_cache_file_chooser_button { + icon-name: "folder-symbolic"; + valign: center; + } + } + } + Adw.ExpanderRow heroic_expander_row { title: _("Heroic"); show-enable-switch: true; diff --git a/data/hu.kramo.Cartridges.gschema.xml b/data/hu.kramo.Cartridges.gschema.xml index b0304d0..23d1ae6 100644 --- a/data/hu.kramo.Cartridges.gschema.xml +++ b/data/hu.kramo.Cartridges.gschema.xml @@ -19,6 +19,15 @@ [] + + true + + + "~/.var/app/net.lutris.Lutris/data/lutris/" + + + "~/.var/app/net.lutris.Lutris/cache/lutris" + true diff --git a/hu.kramo.Cartridges.json b/hu.kramo.Cartridges.json index 9c6624a..6dc752a 100644 --- a/hu.kramo.Cartridges.json +++ b/hu.kramo.Cartridges.json @@ -13,9 +13,12 @@ "--talk-name=org.gtk.vfs.*", "--filesystem=xdg-run/gvfsd", "--filesystem=~/.steam/steam/:ro", + "--filesystem=xdg-data/lutris/:ro", + "--filesystem=xdg-cache/lutris/:ro", "--filesystem=xdg-config/heroic/:ro", "--filesystem=xdg-data/bottles/:ro", "--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.usebottles.bottles/data/bottles/:ro" ], diff --git a/src/main.py b/src/main.py index 6f0baac..b23675a 100644 --- a/src/main.py +++ b/src/main.py @@ -33,6 +33,7 @@ from .create_details_window import create_details_window from .get_games import get_games from .heroic_parser import heroic_parser from .importer import Importer +from .lutris_parser import lutris_parser from .preferences import PreferencesWindow from .save_game import save_game from .steam_parser import steam_parser @@ -168,6 +169,9 @@ class CartridgesApplication(Adw.Application): if self.win.schema.get_boolean("steam"): steam_parser(self.win) + if self.win.schema.get_boolean("lutris"): + lutris_parser(self.win) + if self.win.schema.get_boolean("heroic"): heroic_parser(self.win) diff --git a/src/meson.build b/src/meson.build index f2cd13c..5bbaae6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -26,6 +26,7 @@ cartridges_sources = [ 'utils/steam_parser.py', 'utils/heroic_parser.py', 'utils/bottles_parser.py', + 'utils/lutris_parser.py', 'utils/get_games.py', 'utils/save_game.py', 'utils/save_cover.py', diff --git a/src/preferences.py b/src/preferences.py index 20a9841..8af5263 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -98,6 +98,10 @@ class PreferencesWindow(Adw.PreferencesWindow): steam_clear_button_revealer = Gtk.Template.Child() steam_clear_button = Gtk.Template.Child() + lutris_expander_row = Gtk.Template.Child() + lutris_file_chooser_button = Gtk.Template.Child() + lutris_cache_file_chooser_button = Gtk.Template.Child() + heroic_expander_row = Gtk.Template.Child() heroic_file_chooser_button = Gtk.Template.Child() heroic_epic_switch = Gtk.Template.Child() @@ -191,6 +195,45 @@ class PreferencesWindow(Adw.PreferencesWindow): ) self.steam_clear_button.connect("clicked", clear_steam_dirs) + # Lutris + ImportPreferences( + self, + "lutris", + "Lutris", + "lutris-location", + ["pga.db"], + self.lutris_expander_row, + self.lutris_file_chooser_button, + ) + + def set_cache_dir(_source, result, _unused): + try: + path = self.file_chooser.select_folder_finish(result).get_path() + + def response(widget, response): + if response == "choose_folder": + self.choose_folder(widget, set_cache_dir) + + if not os.path.exists(os.path.join(path, "coverart")): + create_dialog( + self.parent_widget, + _("Cache Not Found"), + _("Select the Lutris cache directory."), + "choose_folder", + _("Set Location"), + ).connect("response", response) + else: + self.schema.set_string( + "lutris-cache-location", + path, + ) + except GLib.GError: + pass + + self.lutris_cache_file_chooser_button.connect( + "clicked", self.choose_folder, set_cache_dir + ) + # Heroic ImportPreferences( self, diff --git a/src/utils/bottles_parser.py b/src/utils/bottles_parser.py index 9beee8d..35bccb4 100644 --- a/src/utils/bottles_parser.py +++ b/src/utils/bottles_parser.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import os -import time +from time import time import yaml @@ -53,7 +53,7 @@ def bottles_parser(parent_widget): return bottles_dir = os.path.expanduser(schema.get_string("bottles-location")) - current_time = int(time.time()) + current_time = int(time()) with open(os.path.join(bottles_dir, "library.yml"), "r") as open_file: data = open_file.read() diff --git a/src/utils/heroic_parser.py b/src/utils/heroic_parser.py index 6671e55..e78998a 100644 --- a/src/utils/heroic_parser.py +++ b/src/utils/heroic_parser.py @@ -20,7 +20,7 @@ import hashlib import json import os -import time +from time import time def heroic_parser(parent_widget): @@ -60,7 +60,7 @@ def heroic_parser(parent_widget): return heroic_dir = os.path.expanduser(schema.get_string("heroic-location")) - current_time = int(time.time()) + current_time = int(time()) importer = parent_widget.importer diff --git a/src/utils/lutris_parser.py b/src/utils/lutris_parser.py new file mode 100644 index 0000000..beb58b9 --- /dev/null +++ b/src/utils/lutris_parser.py @@ -0,0 +1,149 @@ +# lutris_parser.py +# +# Copyright 2022-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 os +import shutil +from sqlite3 import connect +from time import time + + +def lutris_parser(parent_widget): + + schema = parent_widget.schema + + database_path = os.path.join( + os.path.expanduser(schema.get_string("lutris-location")), "pga.db" + ) + if not os.path.isfile(database_path): + if os.path.exists( + os.path.expanduser("~/.var/app/net.lutris.Lutris/data/lutris/") + ): + schema.set_string( + "lutris-location", "~/.var/app/net.lutris.Lutris/data/lutris/" + ) + elif os.path.exists( + os.path.join( + os.getenv("XDG_DATA_HOME") + or os.path.expanduser(os.path.join("~", ".local", "share")), + "lutris", + ) + ): + schema.set_string( + "lutris-location", + os.path.join( + os.getenv("XDG_DATA_HOME") + or os.path.expanduser(os.path.join("~", ".local", "share")), + "lutris", + ), + ) + else: + return + + cache_dir = os.path.expanduser(schema.get_string("lutris-cache-location")) + if not os.path.exists(cache_dir): + if os.path.exists( + os.path.expanduser("~/.var/app/net.lutris.Lutris/cache/lutris/") + ): + schema.set_string( + "lutris-cache-location", "~/.var/app/net.lutris.Lutris/cache/lutris/" + ) + elif os.path.exists( + os.path.join( + os.getenv("XDG_CACHE_HOME") + or os.path.expanduser(os.path.join("~", ".cache")), + "lutris", + ) + ): + schema.set_string( + "lutris-cache-location", + os.path.join( + os.getenv("XDG_CACHE_HOME") + or os.path.expanduser(os.path.join("~", ".cache")), + "lutris", + ), + ) + else: + return + + database_path = os.path.join( + os.path.expanduser(schema.get_string("lutris-location")), "pga.db" + ) + cache_dir = os.path.expanduser(schema.get_string("lutris-cache-location")) + + db_cache_dir = os.path.join( + os.getenv("XDG_CACHE_HOME") or os.path.expanduser(os.path.join("~", ".cache")), + "cartridges", + "lutris", + ) + os.makedirs(db_cache_dir, exist_ok=True) + + shutil.copyfile(database_path, os.path.join(db_cache_dir, "pga.db")) + + db_request = """ + SELECT + id, name, slug, runner, hidden + FROM + 'games' + WHERE + name IS NOT NULL + AND slug IS NOT NULL + AND configPath IS NOT NULL + AND installed IS TRUE + ; + """ + + connection = connect(os.path.join(db_cache_dir, "pga.db")) + cursor = connection.execute(db_request) + rows = cursor.fetchall() + connection.close() + + if schema.get_boolean("steam"): + rows = [row for row in rows if not row[3] == "steam"] + + current_time = int(time()) + + importer = parent_widget.importer + importer.total_queue += len(rows) + importer.queue += len(rows) + + for row in rows: + values = {} + + values["game_id"] = f"lutris_{row[3]}_{row[0]}" + + if ( + values["game_id"] in parent_widget.games + and not parent_widget.games[values["game_id"]].removed + ): + importer.save_game() + continue + + values["added"] = current_time + values["executable"] = ["xdg-open", f"lutris:rungameid/{row[0]}"] + values["hidden"] = row[4] == 1 + values["last_played"] = 0 + values["name"] = row[1] + values["source"] = f"lutris_{row[3]}" + + if os.path.isfile(os.path.join(cache_dir, "coverart", f"{row[2]}.jpg")): + importer.save_cover( + values["game_id"], os.path.join(cache_dir, "coverart", f"{row[2]}.jpg") + ) + + importer.save_game(values) diff --git a/src/utils/save_cover.py b/src/utils/save_cover.py index 9f868ad..a4c9711 100644 --- a/src/utils/save_cover.py +++ b/src/utils/save_cover.py @@ -30,8 +30,7 @@ def save_cover(parent_widget, game_id, cover_path, pixbuf=None): "covers", ) - if not os.path.exists(covers_dir): - os.makedirs(covers_dir) + os.makedirs(covers_dir, exist_ok=True) if pixbuf is None: pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(cover_path, 400, 600, False) diff --git a/src/utils/save_game.py b/src/utils/save_game.py index b93b28d..4ecda93 100644 --- a/src/utils/save_game.py +++ b/src/utils/save_game.py @@ -29,8 +29,7 @@ def save_game(game): "games", ) - if not os.path.exists(games_dir): - os.makedirs(games_dir) + os.makedirs(games_dir, exist_ok=True) with open(os.path.join(games_dir, f'{game["game_id"]}.json'), "w") as open_file: open_file.write(json.dumps(game, indent=4, sort_keys=True)) diff --git a/src/utils/steam_parser.py b/src/utils/steam_parser.py index e10f954..23fcf37 100644 --- a/src/utils/steam_parser.py +++ b/src/utils/steam_parser.py @@ -20,8 +20,8 @@ import json import os import re -import time import urllib.request +from time import time from gi.repository import Gio, GLib @@ -113,7 +113,7 @@ def get_game( def get_games_async(parent_widget, appmanifests, steam_dir, importer): datatypes = ["appid", "name"] - current_time = int(time.time()) + current_time = int(time()) # Wrap the function in another one as Gio.Task.run_in_thread does not allow for passing args def create_func(datatypes, current_time, parent_widget, appmanifest, steam_dir):