WIP - Work on base classes
This commit is contained in:
30
src/game2.py
Normal file
30
src/game2.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from dataclasses import dataclass, field
|
||||
from time import time
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game():
|
||||
"""Simple game class that contains the necessary fields.
|
||||
Saving, updating and removing is done by game manager classes."""
|
||||
|
||||
# State
|
||||
removed : bool = field(default=False, init=False)
|
||||
blacklisted: bool = field(default=False, init=False)
|
||||
added : int = field(default=-1, init=False)
|
||||
last_played: int = field(default=-1, init=False)
|
||||
|
||||
# Metadata
|
||||
source : str = None
|
||||
name : str = None
|
||||
game_id : str = None
|
||||
developer : str = None
|
||||
|
||||
# Launching
|
||||
executable : str = None
|
||||
|
||||
# Display
|
||||
game_cover : str = None
|
||||
hidden : bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
self.added = int(time())
|
||||
0
src/importer/__init__.py
Normal file
0
src/importer/__init__.py
Normal file
80
src/importer/importer.py
Normal file
80
src/importer/importer.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from gi.repository import Adw, Gtk, Gio
|
||||
|
||||
class Importer():
|
||||
|
||||
# Display values
|
||||
win = None
|
||||
progressbar = None
|
||||
import_statuspage = None
|
||||
import_dialog = None
|
||||
|
||||
# Importer values
|
||||
count_total = 0
|
||||
count_done = 0
|
||||
sources = list()
|
||||
|
||||
def __init__(self, win) -> None:
|
||||
self.win = win
|
||||
|
||||
def create_dialog(self):
|
||||
"""Create the import dialog"""
|
||||
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
|
||||
self.import_statuspage = Adw.StatusPage(
|
||||
title=_("Importing Games…"),
|
||||
child=self.progressbar,
|
||||
)
|
||||
self.import_dialog = Adw.Window(
|
||||
content=self.import_statuspage,
|
||||
modal=True,
|
||||
default_width=350,
|
||||
default_height=-1,
|
||||
transient_for=self.win,
|
||||
deletable=False,
|
||||
)
|
||||
self.import_dialog.present()
|
||||
|
||||
def close_dialog(self):
|
||||
"""Close the import dialog"""
|
||||
self.import_dialog.close()
|
||||
|
||||
def get_progress(self):
|
||||
"""Get the current progression as a number between 0 and 1"""
|
||||
progress = 1
|
||||
if self.total_queue > 0:
|
||||
progress = 1 - self.queue / self.total_queue
|
||||
return progress
|
||||
|
||||
def update_progressbar(self):
|
||||
"""Update the progress bar"""
|
||||
progress = self.get_progress()
|
||||
self.progressbar.set_fraction(progress)
|
||||
|
||||
def add_source(self, source):
|
||||
"""Add a source to import games from"""
|
||||
self.sources.append(source)
|
||||
|
||||
def import_games(self):
|
||||
"""Import games from the specified sources"""
|
||||
self.create_dialog()
|
||||
|
||||
# TODO make that async, you doofus
|
||||
# Every source does its job on the side, informing of the amount of work and when a game is done.
|
||||
# At the end of the task, it returns the games.
|
||||
|
||||
# Idea 1 - Work stealing queue
|
||||
# 1. Sources added to the queue
|
||||
# 2. Worker A takes source X and advances it
|
||||
# 3. Worker A puts back source X to the queue
|
||||
# 4. Worker B takes source X, that has ended
|
||||
# 5. Worker B doesn't add source X back to the queue
|
||||
|
||||
# Idea 2 - Gio.Task
|
||||
# 1. A task is created for every source
|
||||
# 2. Source X finishes
|
||||
# 3. Importer adds the games
|
||||
|
||||
for source in self.sources:
|
||||
for game in source:
|
||||
game.save()
|
||||
|
||||
self.close_dialog()
|
||||
20
src/importer/location.py
Normal file
20
src/importer/location.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location():
|
||||
"""Abstraction for a location that can be overriden by a schema key"""
|
||||
|
||||
win = None
|
||||
|
||||
default: str = None
|
||||
key: str = None
|
||||
|
||||
@cached_property
|
||||
def path(self):
|
||||
override = Path(self.win.schema.get_string(self.path_override_key))
|
||||
if override.exists():
|
||||
return override
|
||||
return self.path_default
|
||||
60
src/importer/source.py
Normal file
60
src/importer/source.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from collections.abc import Iterable, Iterator
|
||||
from enum import IntEnum, auto
|
||||
|
||||
|
||||
class SourceIterator(Iterator):
|
||||
"""Data producer for a source of games"""
|
||||
|
||||
class States(IntEnum):
|
||||
DEFAULT = auto()
|
||||
READY = auto()
|
||||
|
||||
state = States.DEFAULT
|
||||
source = None
|
||||
|
||||
def __init__(self, source) -> None:
|
||||
super().__init__()
|
||||
self.source = source
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Source(Iterable):
|
||||
"""Source of games. Can be a program location on disk with a config file that points to game for example"""
|
||||
|
||||
win = None
|
||||
name: str = "GenericSource"
|
||||
variant: str = None
|
||||
|
||||
# Format to construct the executable command for a game.
|
||||
# Available field names depend on the implementation
|
||||
executable_format: str
|
||||
|
||||
def __init__(self, win) -> None:
|
||||
super().__init__()
|
||||
self.win = win
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""Get the source's full name"""
|
||||
s = self.name
|
||||
if self.variant is not None:
|
||||
s += " (%s)" % self.variant
|
||||
return s
|
||||
|
||||
@property
|
||||
def game_id_format(self):
|
||||
"""Get the string format used to construct game IDs"""
|
||||
_format = self.name.lower()
|
||||
if self.variant is not None:
|
||||
_format += "_" + self.variant.lower()
|
||||
_format += "_{game_id}_{game_internal_id}"
|
||||
return _format
|
||||
|
||||
def __iter__(self):
|
||||
"""Get the source's iterator, to use in for loops"""
|
||||
raise NotImplementedError()
|
||||
0
src/importer/sources/__init__.py
Normal file
0
src/importer/sources/__init__.py
Normal file
115
src/importer/sources/lutris_source.py
Normal file
115
src/importer/sources/lutris_source.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from pathlib import Path
|
||||
from functools import cached_property
|
||||
from sqlite3 import connect
|
||||
|
||||
from cartridges.game2 import Game
|
||||
from cartridges.importer.source import Source, SourceIterator
|
||||
|
||||
|
||||
class LutrisSourceIterator(SourceIterator):
|
||||
|
||||
ignore_steam_games = False
|
||||
|
||||
db_connection = None
|
||||
db_cursor = None
|
||||
db_location = None
|
||||
db_request = None
|
||||
|
||||
def __init__(self, ignore_steam_games) -> None:
|
||||
super().__init__()
|
||||
self.ignore_steam_games = ignore_steam_games
|
||||
self.db_connection = None
|
||||
self.db_cursor = None
|
||||
self.db_location = self.source.location / "pga.db"
|
||||
self.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
|
||||
;
|
||||
"""
|
||||
|
||||
def __next__(self):
|
||||
"""Produce games. Behaviour depends on the state of the iterator."""
|
||||
|
||||
# Get database contents iterator
|
||||
if self.state == self.States.DEFAULT:
|
||||
self.db_connection = connect(self.db_location)
|
||||
self.db_cursor = self.db_connection.execute(self.db_request)
|
||||
self.state = self.States.READY
|
||||
|
||||
# Get next DB value
|
||||
while True:
|
||||
try:
|
||||
row = self.db_cursor.__next__()
|
||||
except StopIteration as e:
|
||||
self.db_connection.close()
|
||||
raise e
|
||||
|
||||
# Ignore steam games if requested
|
||||
if row[3] == "steam" and self.ignore_steam_games:
|
||||
continue
|
||||
|
||||
# Build basic game
|
||||
game = Game(
|
||||
name=row[1],
|
||||
hidden=row[4],
|
||||
source=self.source.full_name,
|
||||
game_id=self.source.game_id_format.format(game_id=row[2]),
|
||||
executable=self.source.executable_format.format(game_id=row[2]),
|
||||
developer=None, # TODO get developer metadata on Lutris
|
||||
)
|
||||
# TODO Add official image
|
||||
# TODO Add SGDB image
|
||||
return game
|
||||
|
||||
class LutrisSource(Source):
|
||||
|
||||
name = "Lutris"
|
||||
executable_format = "xdg-open lutris:rungameid/{game_id}"
|
||||
|
||||
location = None
|
||||
cache_location = None
|
||||
|
||||
def __iter__(self):
|
||||
return LutrisSourceIterator(source=self)
|
||||
|
||||
# TODO find a way to no duplicate this code
|
||||
# Ideas:
|
||||
# - Location class (verbose, set in __init__)
|
||||
# - Schema key override decorator ()
|
||||
|
||||
# Lutris location property
|
||||
@cached_property
|
||||
def location(self):
|
||||
ovr = Path(self.win.schema.get_string(self.location_key))
|
||||
if ovr.exists(): return ovr
|
||||
return self.location_default
|
||||
|
||||
# Lutris cache location property
|
||||
@cached_property
|
||||
def cache_location(self):
|
||||
ovr = Path(self.win.schema.get_string(self.cache_location_key))
|
||||
if ovr.exists(): return ovr
|
||||
return self.cache_location_default
|
||||
|
||||
|
||||
class LutrisNativeSource(LutrisSource):
|
||||
variant = "native"
|
||||
location_key = "lutris-location"
|
||||
location_default = Path("~/.local/share/lutris/").expanduser()
|
||||
cache_location_key = "lutris-cache-location"
|
||||
cache_location_default = location_default / "covers"
|
||||
|
||||
|
||||
class LutrisFlatpakSource(LutrisSource):
|
||||
variant = "flatpak"
|
||||
location_key = "lutris-flatpak-location"
|
||||
location_default = Path("~/.var/app/net.lutris.Lutris/data/lutris").expanduser()
|
||||
cache_location_key = "lutris-flatpak-cache-location"
|
||||
cache_location_default = location_default / "covers"
|
||||
0
src/managers/__init__.py
Normal file
0
src/managers/__init__.py
Normal file
14
src/managers/game_manager.py
Normal file
14
src/managers/game_manager.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class GameManager():
|
||||
"""Interface for systems that save, update and remove games"""
|
||||
|
||||
def add(self, game):
|
||||
"""Add a game to the manager"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self, game):
|
||||
"""Update an existing game in the manager"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove(self, game):
|
||||
"""Remove an existing game from the manager"""
|
||||
raise NotImplementedError()
|
||||
Reference in New Issue
Block a user