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)
+