Flatpak source initial work

This commit is contained in:
kramo
2023-06-30 19:51:44 +02:00
parent 7a1e5e0968
commit fccf302c4b
12 changed files with 160 additions and 36 deletions

View File

@@ -61,6 +61,12 @@
<key name="legendary-location" type="s"> <key name="legendary-location" type="s">
<default>"~/.config/legendary/"</default> <default>"~/.config/legendary/"</default>
</key> </key>
<key name="flatpak" type="b">
<default>false</default>
</key>
<key name="flatpak-location" type="s">
<default>"/var/lib/flatpak/exports"</default>
</key>
<key name="sgdb-key" type="s"> <key name="sgdb-key" type="s">
<default>""</default> <default>""</default>
</key> </key>

View File

@@ -16,7 +16,8 @@
"--filesystem=~/.var/app/net.lutris.Lutris/:ro", "--filesystem=~/.var/app/net.lutris.Lutris/:ro",
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
"--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro", "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro",
"--filesystem=~/.var/app/io.itch.itch/config/itch/:ro" "--filesystem=~/.var/app/io.itch.itch/config/itch/:ro",
"--filesystem=/var/lib/flatpak:ro"
], ],
"cleanup" : [ "cleanup" : [
"/include", "/include",
@@ -96,6 +97,20 @@
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1" "sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
} }
] ]
},
{
"name": "python3-pyxdg",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pyxdg\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl",
"sha256": "bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"
}
]
} }
] ]
}, },

View File

@@ -41,13 +41,13 @@ class BottlesSourceIterator(SourceIterator):
data = self.source.data_location["library.yml"].read_text("utf-8") data = self.source.data_location["library.yml"].read_text("utf-8")
library: dict = yaml.safe_load(data) library: dict = yaml.safe_load(data)
added_time = int(time())
for entry in library.values(): for entry in library.values():
# Build game # Build game
values = { values = {
"version": shared.SPEC_VERSION,
"source": self.source.id, "source": self.source.id,
"added": int(time()), "added": added_time,
"name": entry["name"], "name": entry["name"],
"game_id": self.source.game_id_format.format(game_id=entry["id"]), "game_id": self.source.game_id_format.format(game_id=entry["id"]),
"executable": self.source.executable_format.format( "executable": self.source.executable_format.format(

View File

@@ -0,0 +1,94 @@
# flatpak_source.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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import re
from pathlib import Path
from time import time
from xdg import IconTheme
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
class FlatpakSourceIterator(SourceIterator):
source: "FlatpakSource"
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games"""
added_time = int(time())
IconTheme.icondirs.append("/var/lib/flatpak/exports/share/icons")
for entry in (self.source.data_location["applications"]).iterdir():
flatpak_id = entry.stem
with entry.open("r", encoding="utf-8") as open_file:
string = open_file.read()
desktop_values = {"Name": None, "Icon": None}
for key in desktop_values:
if regex := re.findall(f"{key}=(.*)\n", string):
desktop_values[key] = regex[0]
if not desktop_values["Name"]:
continue
values = {
"source": self.source.id,
"added": added_time,
"name": desktop_values["Name"],
"game_id": self.source.game_id_format.format(game_id=flatpak_id),
"executable": self.source.executable_format.format(
flatpak_id=flatpak_id
),
}
game = Game(values, allow_side_effects=False)
additional_data = {}
if icon_name := desktop_values["Icon"]:
if icon_path := IconTheme.getIconPath(icon_name):
additional_data = {"local_image_path": Path(icon_path)}
# Produce game
yield (game, additional_data)
class FlatpakSource(Source):
"""Generic Flatpak source"""
name = "Flatpak"
iterator_class = FlatpakSourceIterator
executable_format = "flatpak run {flatpak_id}"
available_on = set(("linux",))
data_location = Location(
schema_key="flatpak-location",
candidates=(
"/var/lib/flatpak/exports/",
shared.data_dir / "flatpak" / "exports",
),
paths={
"applications": (True, "share/applications"),
},
)

View File

@@ -68,7 +68,7 @@ class HeroicSourceIterator(SourceIterator):
} }
def game_from_library_entry( def game_from_library_entry(
self, entry: HeroicLibraryEntry self, entry: HeroicLibraryEntry, added_time: int
) -> SourceIterationResult: ) -> SourceIterationResult:
"""Helper method used to build a Game from a Heroic library entry""" """Helper method used to build a Game from a Heroic library entry"""
@@ -81,9 +81,8 @@ class HeroicSourceIterator(SourceIterator):
runner = entry["runner"] runner = entry["runner"]
service = self.sub_sources[runner]["service"] service = self.sub_sources[runner]["service"]
values = { values = {
"version": shared.SPEC_VERSION,
"source": self.source.id, "source": self.source.id,
"added": int(time()), "added": added_time,
"name": entry["title"], "name": entry["title"],
"developer": entry["developer"], "developer": entry["developer"],
"game_id": self.source.game_id_format.format( "game_id": self.source.game_id_format.format(
@@ -119,9 +118,12 @@ class HeroicSourceIterator(SourceIterator):
# Invalid library.json file, skip it # Invalid library.json file, skip it
logging.warning("Couldn't open Heroic file: %s", str(file)) logging.warning("Couldn't open Heroic file: %s", str(file))
continue continue
added_time = int(time())
for entry in library: for entry in library:
try: try:
result = self.game_from_library_entry(entry) result = self.game_from_library_entry(entry, added_time)
except KeyError: except KeyError:
# Skip invalid games # Skip invalid games
logging.warning("Invalid Heroic game skipped in %s", str(file)) logging.warning("Invalid Heroic game skipped in %s", str(file))
@@ -130,7 +132,7 @@ class HeroicSourceIterator(SourceIterator):
class HeroicSource(URLExecutableSource): class HeroicSource(URLExecutableSource):
"""Generic heroic games launcher source""" """Generic Heroic Games Launcher source"""
name = "Heroic" name = "Heroic"
iterator_class = HeroicSourceIterator iterator_class = HeroicSourceIterator
@@ -141,9 +143,8 @@ class HeroicSource(URLExecutableSource):
schema_key="heroic-location", schema_key="heroic-location",
candidates=( candidates=(
"~/.var/app/com.heroicgameslauncher.hgl/config/heroic/", "~/.var/app/com.heroicgameslauncher.hgl/config/heroic/",
shared.config_dir / "heroic/", shared.config_dir / "heroic",
"~/.config/heroic/", shared.appdata_dir / "heroic",
shared.appdata_dir / "heroic/",
), ),
paths={ paths={
"config.json": (False, "config.json"), "config.json": (False, "config.json"),

View File

@@ -59,11 +59,12 @@ class ItchSourceIterator(SourceIterator):
connection = connect(db_path) connection = connect(db_path)
cursor = connection.execute(db_request) cursor = connection.execute(db_request)
added_time = int(time())
# Create games from the db results # Create games from the db results
for row in cursor: for row in cursor:
values = { values = {
"version": shared.SPEC_VERSION, "added": added_time,
"added": int(time()),
"source": self.source.id, "source": self.source.id,
"name": row[1], "name": row[1],
"game_id": self.source.game_id_format.format(game_id=row[0]), "game_id": self.source.game_id_format.format(game_id=row[0]),
@@ -87,9 +88,8 @@ class ItchSource(URLExecutableSource):
schema_key="itch-location", schema_key="itch-location",
candidates=( candidates=(
"~/.var/app/io.itch.itch/config/itch/", "~/.var/app/io.itch.itch/config/itch/",
shared.config_dir / "itch/", shared.config_dir / "itch",
"~/.config/itch/", shared.appdata_dir / "itch",
shared.appdata_dir / "itch/",
), ),
paths={"butler.db": (False, "db/butler.db")}, paths={"butler.db": (False, "db/butler.db")},
) )

View File

@@ -32,7 +32,9 @@ from src.importer.sources.source import Source, SourceIterationResult, SourceIte
class LegendarySourceIterator(SourceIterator): class LegendarySourceIterator(SourceIterator):
source: "LegendarySource" source: "LegendarySource"
def game_from_library_entry(self, entry: dict) -> SourceIterationResult: def game_from_library_entry(
self, entry: dict, added_time: int
) -> SourceIterationResult:
# Skip non-games # Skip non-games
if entry["is_dlc"]: if entry["is_dlc"]:
return None return None
@@ -40,8 +42,7 @@ class LegendarySourceIterator(SourceIterator):
# Build game # Build game
app_name = entry["app_name"] app_name = entry["app_name"]
values = { values = {
"version": shared.SPEC_VERSION, "added": added_time,
"added": int(time()),
"source": self.source.id, "source": self.source.id,
"name": entry["title"], "name": entry["title"],
"game_id": self.source.game_id_format.format(game_id=app_name), "game_id": self.source.game_id_format.format(game_id=app_name),
@@ -72,10 +73,13 @@ class LegendarySourceIterator(SourceIterator):
except (JSONDecodeError, OSError): except (JSONDecodeError, OSError):
logging.warning("Couldn't open Legendary file: %s", str(file)) logging.warning("Couldn't open Legendary file: %s", str(file))
return return
added_time = int(time())
# Generate games from library # Generate games from library
for entry in library.values(): for entry in library.values():
try: try:
result = self.game_from_library_entry(entry) result = self.game_from_library_entry(entry, added_time)
except KeyError as error: except KeyError as error:
# Skip invalid games # Skip invalid games
logging.warning( logging.warning(
@@ -93,10 +97,7 @@ class LegendarySource(Source):
iterator_class = LegendarySourceIterator iterator_class = LegendarySourceIterator
data_location: Location = Location( data_location: Location = Location(
schema_key="legendary-location", schema_key="legendary-location",
candidates=( candidates=(shared.config_dir / "legendary",),
shared.config_dir / "legendary/",
"~/.config/legendary",
),
paths={ paths={
"installed.json": (False, "installed.json"), "installed.json": (False, "installed.json"),
"metadata": (True, "metadata"), "metadata": (True, "metadata"),

View File

@@ -55,12 +55,13 @@ class LutrisSourceIterator(SourceIterator):
connection = connect(db_path) connection = connect(db_path)
cursor = connection.execute(request, params) cursor = connection.execute(request, params)
added_time = int(time())
# Create games from the DB results # Create games from the DB results
for row in cursor: for row in cursor:
# Create game # Create game
values = { values = {
"version": shared.SPEC_VERSION, "added": added_time,
"added": int(time()),
"hidden": row[4], "hidden": row[4],
"name": row[1], "name": row[1],
"source": f"{self.source.id}_{row[3]}", "source": f"{self.source.id}_{row[3]}",
@@ -83,7 +84,7 @@ class LutrisSourceIterator(SourceIterator):
class LutrisSource(URLExecutableSource): class LutrisSource(URLExecutableSource):
"""Generic lutris source""" """Generic Lutris source"""
name = "Lutris" name = "Lutris"
iterator_class = LutrisSourceIterator iterator_class = LutrisSourceIterator
@@ -96,8 +97,7 @@ class LutrisSource(URLExecutableSource):
schema_key="lutris-location", schema_key="lutris-location",
candidates=( candidates=(
"~/.var/app/net.lutris.Lutris/data/lutris/", "~/.var/app/net.lutris.Lutris/data/lutris/",
shared.data_dir / "lutris/", shared.data_dir / "lutris",
"~/.local/share/lutris/",
), ),
paths={ paths={
"pga.db": (False, "pga.db"), "pga.db": (False, "pga.db"),
@@ -108,8 +108,7 @@ class LutrisSource(URLExecutableSource):
schema_key="lutris-cache-location", schema_key="lutris-cache-location",
candidates=( candidates=(
"~/.var/app/net.lutris.Lutris/cache/lutris/", "~/.var/app/net.lutris.Lutris/cache/lutris/",
shared.cache_dir / "lutris/", shared.cache_dir / "lutris",
"~/.cache/lutris",
), ),
paths={ paths={
"coverart": (True, "coverart"), "coverart": (True, "coverart"),

View File

@@ -87,7 +87,7 @@ class Source(Iterable):
@property @property
def game_id_format(self) -> str: def game_id_format(self) -> str:
"""The string format used to construct game IDs""" """The string format used to construct game IDs"""
return self.name.lower() + "_{game_id}" return self.id + "_{game_id}"
@property @property
def is_available(self): def is_available(self):

View File

@@ -66,6 +66,9 @@ class SteamSourceIterator(SourceIterator):
"""Generator method producing games""" """Generator method producing games"""
appid_cache = set() appid_cache = set()
manifests = self.get_manifests() manifests = self.get_manifests()
added_time = int(time())
for manifest in manifests: for manifest in manifests:
# Get metadata from manifest # Get metadata from manifest
steam = SteamFileHelper() steam = SteamFileHelper()
@@ -87,8 +90,7 @@ class SteamSourceIterator(SourceIterator):
# Build game from local data # Build game from local data
values = { values = {
"version": shared.SPEC_VERSION, "added": added_time,
"added": int(time()),
"name": local_data["name"], "name": local_data["name"],
"source": self.source.id, "source": self.source.id,
"game_id": self.source.game_id_format.format(game_id=appid), "game_id": self.source.game_id_format.format(game_id=appid),
@@ -117,8 +119,8 @@ class SteamSource(URLExecutableSource):
schema_key="steam-location", schema_key="steam-location",
candidates=( candidates=(
"~/.var/app/com.valvesoftware.Steam/data/Steam/", "~/.var/app/com.valvesoftware.Steam/data/Steam/",
shared.data_dir / "Steam/", shared.data_dir / "Steam",
"~/.steam/", "~/.steam",
shared.programfiles32_dir / "Steam", shared.programfiles32_dir / "Steam",
), ),
paths={ paths={

View File

@@ -34,6 +34,7 @@ from src.details_window import DetailsWindow
from src.game import Game from src.game import Game
from src.importer.importer import Importer from src.importer.importer import Importer
from src.importer.sources.bottles_source import BottlesSource 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.heroic_source import HeroicSource
from src.importer.sources.itch_source import ItchSource from src.importer.sources.itch_source import ItchSource
from src.importer.sources.legendary_source import LegendarySource from src.importer.sources.legendary_source import LegendarySource
@@ -222,6 +223,9 @@ class CartridgesApplication(Adw.Application):
if shared.schema.get_boolean("bottles"): if shared.schema.get_boolean("bottles"):
importer.add_source(BottlesSource()) importer.add_source(BottlesSource())
if not shared.schema.get_boolean("flatpak"):
importer.add_source(FlatpakSource())
if shared.schema.get_boolean("itch"): if shared.schema.get_boolean("itch"):
importer.add_source(ItchSource()) importer.add_source(ItchSource())

View File

@@ -26,6 +26,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
from src import shared from src import shared
from src.importer.sources.bottles_source import BottlesSource 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.heroic_source import HeroicSource
from src.importer.sources.itch_source import ItchSource from src.importer.sources.itch_source import ItchSource
from src.importer.sources.legendary_source import LegendarySource from src.importer.sources.legendary_source import LegendarySource
@@ -124,6 +125,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
# Sources settings # Sources settings
for source_class in ( for source_class in (
BottlesSource, BottlesSource,
FlatpakSource,
HeroicSource, HeroicSource,
ItchSource, ItchSource,
LegendarySource, LegendarySource,