🎨 Black codestyle
This commit is contained in:
@@ -17,25 +17,38 @@ from pathlib import Path
|
|||||||
from os import PathLike
|
from os import PathLike
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
def replaced_by_path(path: PathLike): # Decorator builder
|
|
||||||
|
def replaced_by_path(path: PathLike): # Decorator builder
|
||||||
"""Replace the method's returned path with the override if the override exists on disk"""
|
"""Replace the method's returned path with the override if the override exists on disk"""
|
||||||
def decorator(original_function): # Built decorator (closure)
|
|
||||||
|
def decorator(original_function): # Built decorator (closure)
|
||||||
@wraps(original_function)
|
@wraps(original_function)
|
||||||
def wrapper(*args, **kwargs): # func's override
|
def wrapper(*args, **kwargs): # func's override
|
||||||
p = Path(path).expanduser()
|
p = Path(path).expanduser()
|
||||||
if p.exists(): return p
|
if p.exists():
|
||||||
else: return original_function(*args, **kwargs)
|
return p
|
||||||
|
else:
|
||||||
|
return original_function(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def replaced_by_schema_key(key: str): # Decorator builder
|
|
||||||
|
def replaced_by_schema_key(key: str): # Decorator builder
|
||||||
"""Replace the method's returned path with the path pointed by the key if it exists on disk"""
|
"""Replace the method's returned path with the path pointed by the key if it exists on disk"""
|
||||||
def decorator(original_function): # Built decorator (closure)
|
|
||||||
|
def decorator(original_function): # Built decorator (closure)
|
||||||
@wraps(original_function)
|
@wraps(original_function)
|
||||||
def wrapper(*args, **kwargs): # func's override
|
def wrapper(*args, **kwargs): # func's override
|
||||||
schema = args[0].win.schema
|
schema = args[0].win.schema
|
||||||
try: override = schema.get_string(key)
|
try:
|
||||||
except Exception: return original_function(*args, **kwargs)
|
override = schema.get_string(key)
|
||||||
else: return replaced_by_path(override)(*args, **kwargs)
|
except Exception:
|
||||||
|
return original_function(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return replaced_by_path(override)(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
|
||||||
|
return decorator
|
||||||
|
|||||||
@@ -1,21 +1,45 @@
|
|||||||
|
from threading import Thread, Lock
|
||||||
from gi.repository import Adw, Gtk, Gio
|
from gi.repository import Adw, Gtk, Gio
|
||||||
|
|
||||||
class Importer():
|
from .game import Game
|
||||||
|
from .steamgriddb import SGDBSave
|
||||||
|
|
||||||
# Display values
|
|
||||||
|
class Importer:
|
||||||
win = None
|
win = None
|
||||||
progressbar = None
|
progressbar = None
|
||||||
import_statuspage = None
|
import_statuspage = None
|
||||||
import_dialog = None
|
import_dialog = None
|
||||||
|
sources = None
|
||||||
|
|
||||||
# Importer values
|
progress_lock = None
|
||||||
count_total = 0
|
counts = None
|
||||||
count_done = 0
|
|
||||||
sources = list()
|
games_lock = None
|
||||||
|
games = None
|
||||||
|
|
||||||
def __init__(self, win) -> None:
|
def __init__(self, win) -> None:
|
||||||
|
self.games = set()
|
||||||
|
self.sources = list()
|
||||||
|
self.counts = dict()
|
||||||
|
self.games_lock = Lock()
|
||||||
|
self.progress_lock = Lock()
|
||||||
self.win = win
|
self.win = win
|
||||||
|
|
||||||
|
@property
|
||||||
|
def progress(self):
|
||||||
|
# Compute overall values
|
||||||
|
done = 0
|
||||||
|
total = 0
|
||||||
|
for source in self.sources:
|
||||||
|
done += self.counts[source.id]["done"]
|
||||||
|
total += self.counts[source.id]["total"]
|
||||||
|
# Compute progress
|
||||||
|
progress = 1
|
||||||
|
if total > 0:
|
||||||
|
progress = 1 - done / total
|
||||||
|
return progress
|
||||||
|
|
||||||
def create_dialog(self):
|
def create_dialog(self):
|
||||||
"""Create the import dialog"""
|
"""Create the import dialog"""
|
||||||
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
|
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
|
||||||
@@ -37,44 +61,83 @@ class Importer():
|
|||||||
"""Close the import dialog"""
|
"""Close the import dialog"""
|
||||||
self.import_dialog.close()
|
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):
|
def update_progressbar(self):
|
||||||
"""Update the progress bar"""
|
"""Update the progress bar"""
|
||||||
progress = self.get_progress()
|
progress = self.progress()
|
||||||
self.progressbar.set_fraction(progress)
|
self.progressbar.set_fraction(progress)
|
||||||
|
|
||||||
def add_source(self, source):
|
def add_source(self, source):
|
||||||
"""Add a source to import games from"""
|
"""Add a source to import games from"""
|
||||||
self.sources.append(source)
|
self.sources.append(source)
|
||||||
|
self.counts[source.id] = {"done": 0, "total": 0}
|
||||||
|
|
||||||
def import_games(self):
|
def import_games(self):
|
||||||
"""Import games from the specified sources"""
|
"""Import games from the specified sources"""
|
||||||
|
|
||||||
self.create_dialog()
|
self.create_dialog()
|
||||||
|
|
||||||
# TODO make that async, you doofus
|
# Scan all sources
|
||||||
# Every source does its job on the side, informing of the amount of work and when a game is done.
|
threads = []
|
||||||
# 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 source in self.sources:
|
||||||
for game in source:
|
t = Thread(
|
||||||
game.save()
|
None,
|
||||||
|
self.__import_from_source,
|
||||||
|
args=tuple(
|
||||||
|
source,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
threads.append(t)
|
||||||
|
t.start()
|
||||||
|
|
||||||
self.close_dialog()
|
# Wait for all of them to finish
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
self.close_dialog()
|
||||||
|
|
||||||
|
def __import_from_source(self, *args, **kwargs):
|
||||||
|
"""Source import thread entry point"""
|
||||||
|
# TODO just get Game objects from the sources
|
||||||
|
source, *rest = args
|
||||||
|
|
||||||
|
iterator = source.__iter__()
|
||||||
|
for game_values in iterator:
|
||||||
|
game = Game(self.win, game_values)
|
||||||
|
|
||||||
|
self.games_lock.acquire()
|
||||||
|
self.games.add(game)
|
||||||
|
self.games_lock.release()
|
||||||
|
|
||||||
|
self.progress_lock.acquire()
|
||||||
|
self.counts[source.id]["total"] = len(iterator)
|
||||||
|
if not game.blacklisted:
|
||||||
|
self.counts[source.id]["done"] += 1
|
||||||
|
self.update_progressbar()
|
||||||
|
self.progress_lock.release()
|
||||||
|
|
||||||
|
# TODO remove after not needed
|
||||||
|
def save_game(self, values=None, cover_path=None):
|
||||||
|
if values:
|
||||||
|
game = Game(self.win, values)
|
||||||
|
|
||||||
|
if save_cover:
|
||||||
|
save_cover(self.win, game.game_id, resize_cover(self.win, cover_path))
|
||||||
|
|
||||||
|
self.games.add(game)
|
||||||
|
|
||||||
|
self.games_no += 1
|
||||||
|
if game.blacklisted:
|
||||||
|
self.games_no -= 1
|
||||||
|
|
||||||
|
self.queue -= 1
|
||||||
|
self.update_progressbar()
|
||||||
|
|
||||||
|
if self.queue == 0 and not self.blocker:
|
||||||
|
if self.games:
|
||||||
|
self.total_queue = len(self.games)
|
||||||
|
self.queue = len(self.games)
|
||||||
|
self.import_statuspage.set_title(_("Importing Covers…"))
|
||||||
|
self.update_progressbar()
|
||||||
|
SGDBSave(self.win, self.games, self)
|
||||||
|
else:
|
||||||
|
self.done()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SourceIterator(Iterator):
|
|||||||
class States(IntEnum):
|
class States(IntEnum):
|
||||||
DEFAULT = auto()
|
DEFAULT = auto()
|
||||||
READY = auto()
|
READY = auto()
|
||||||
|
|
||||||
state = States.DEFAULT
|
state = States.DEFAULT
|
||||||
source = None
|
source = None
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class SourceIterator(Iterator):
|
|||||||
class Source(Iterable):
|
class Source(Iterable):
|
||||||
"""Source of games. E.g an installed app with a config file that lists game directories"""
|
"""Source of games. E.g an installed app with a config file that lists game directories"""
|
||||||
|
|
||||||
win = None # TODO maybe not depend on that ?
|
win = None # TODO maybe not depend on that ?
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
variant: str
|
variant: str
|
||||||
@@ -42,17 +42,25 @@ class Source(Iterable):
|
|||||||
"""The source's full name"""
|
"""The source's full name"""
|
||||||
s = self.name
|
s = self.name
|
||||||
if self.variant is not None:
|
if self.variant is not None:
|
||||||
s += " (%s)" % self.variant
|
s += f" ({self.variant})"
|
||||||
|
return s
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
"""The source's identifier"""
|
||||||
|
s = self.name.lower()
|
||||||
|
if self.variant is not None:
|
||||||
|
s += f"_{self.variant.lower()}"
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def game_id_format(self):
|
def game_id_format(self):
|
||||||
"""The string format used to construct game IDs"""
|
"""The string format used to construct game IDs"""
|
||||||
_format = self.name.lower()
|
f = self.name.lower()
|
||||||
if self.variant is not None:
|
if self.variant is not None:
|
||||||
_format += "_" + self.variant.lower()
|
f += f"_{self.variant.lower()}"
|
||||||
_format += "_{game_id}_{game_internal_id}"
|
f += "_{game_id}"
|
||||||
return _format
|
return f
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -63,4 +71,4 @@ class Source(Iterable):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Get the source's iterator, to use in for loops"""
|
"""Get the source's iterator, to use in for loops"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from sqlite3 import connect
|
from sqlite3 import connect
|
||||||
|
|
||||||
from src.game2 import Game
|
from src.utils.save_cover import resize_cover, save_cover
|
||||||
from src.importer.source import Source, SourceIterator
|
from src.importer.source import Source, SourceIterator
|
||||||
from src.importer.decorators import replaced_by_schema_key, replaced_by_path
|
from src.importer.decorators import replaced_by_schema_key, replaced_by_path
|
||||||
|
|
||||||
|
|
||||||
class LutrisSourceIterator(SourceIterator):
|
class LutrisSourceIterator(SourceIterator):
|
||||||
|
ignore_steam_games = False # TODO get that value
|
||||||
ignore_steam_games = False
|
|
||||||
|
|
||||||
db_connection = None
|
db_connection = None
|
||||||
db_cursor = None
|
db_cursor = None
|
||||||
db_location = None
|
db_location = None
|
||||||
db_request = None
|
db_request = None
|
||||||
|
|
||||||
def __init__(self, ignore_steam_games) -> None:
|
def __init__(self, ignore_steam_games):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ignore_steam_games = ignore_steam_games
|
self.ignore_steam_games = ignore_steam_games
|
||||||
self.db_connection = None
|
self.db_connection = None
|
||||||
@@ -43,10 +42,10 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
self.db_cursor = self.db_connection.execute(self.db_request)
|
self.db_cursor = self.db_connection.execute(self.db_request)
|
||||||
self.state = self.States.READY
|
self.state = self.States.READY
|
||||||
|
|
||||||
# Get next DB value
|
|
||||||
while True:
|
while True:
|
||||||
|
# Get next DB value
|
||||||
try:
|
try:
|
||||||
row = self.db_cursor.__next__()
|
row = self.db_cursor.__next__()
|
||||||
except StopIteration as e:
|
except StopIteration as e:
|
||||||
self.db_connection.close()
|
self.db_connection.close()
|
||||||
raise e
|
raise e
|
||||||
@@ -54,29 +53,41 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
# Ignore steam games if requested
|
# Ignore steam games if requested
|
||||||
if row[3] == "steam" and self.ignore_steam_games:
|
if row[3] == "steam" and self.ignore_steam_games:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build basic game
|
# Build basic game
|
||||||
values = {
|
values = {
|
||||||
"name" : row[1],
|
"hidden": row[4],
|
||||||
"hidden" : row[4],
|
"name": row[1],
|
||||||
"source" : self.source.full_name,
|
"source": f"{self.source.id}_{row[3]}",
|
||||||
"game_id" : self.source.game_id_format.format(game_id=row[2]),
|
"game_id": self.source.game_id_format.format(
|
||||||
|
game_id=row[2], game_internal_id=row[0]
|
||||||
|
),
|
||||||
"executable": self.source.executable_format.format(game_id=row[2]),
|
"executable": self.source.executable_format.format(game_id=row[2]),
|
||||||
"developer" : None, # TODO get developer metadata on Lutris
|
"developer": None, # TODO get developer metadata on Lutris
|
||||||
}
|
}
|
||||||
# TODO Add official image
|
|
||||||
# TODO Add SGDB image
|
# Save official image
|
||||||
|
image_path = self.source.cache_location / "coverart" / f"{row[2]}.jpg"
|
||||||
|
if image_path.exists():
|
||||||
|
resized = resize_cover(self.win, image_path)
|
||||||
|
save_cover(self.win, values["game_id"], resized)
|
||||||
|
|
||||||
|
# Save SGDB
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
class LutrisSource(Source):
|
class LutrisSource(Source):
|
||||||
|
|
||||||
name = "Lutris"
|
name = "Lutris"
|
||||||
executable_format = "xdg-open lutris:rungameid/{game_id}"
|
executable_format = "xdg-open lutris:rungameid/{game_id}"
|
||||||
|
|
||||||
location = None
|
location = None
|
||||||
cache_location = None
|
cache_location = None
|
||||||
|
|
||||||
def __init__(self, win) -> None:
|
@property
|
||||||
|
def game_id_format(self):
|
||||||
|
return super().game_id_format + "_{game_internal_id}"
|
||||||
|
|
||||||
|
def __init__(self, win):
|
||||||
super().__init__(win)
|
super().__init__(win)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
@@ -116,4 +127,4 @@ class LutrisFlatpakSource(LutrisSource):
|
|||||||
@replaced_by_schema_key("lutris-flatpak-cache-location")
|
@replaced_by_schema_key("lutris-flatpak-cache-location")
|
||||||
@replaced_by_path("~/.var/app/net.lutris.Lutris/data/lutris/covers")
|
@replaced_by_path("~/.var/app/net.lutris.Lutris/data/lutris/covers")
|
||||||
def cache_location(self):
|
def cache_location(self):
|
||||||
raise FileNotFoundError()
|
raise FileNotFoundError()
|
||||||
|
|||||||
Reference in New Issue
Block a user