Add itch import

This commit is contained in:
kramo
2023-04-03 18:17:53 +02:00
parent a4c28449a7
commit f18894bf10
10 changed files with 226 additions and 39 deletions

View File

@@ -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 .itch_parser import itch_parser
from .lutris_parser import lutris_parser
from .preferences import PreferencesWindow
from .save_game import save_game
@@ -206,6 +207,9 @@ class CartridgesApplication(Adw.Application):
if self.win.schema.get_boolean("bottles"):
bottles_parser(self.win)
if self.win.schema.get_boolean("itch"):
itch_parser(self.win)
self.win.importer.blocker = False
if self.win.importer.import_dialog.is_visible and self.win.importer.queue == 0:

View File

@@ -24,9 +24,10 @@ cartridges_sources = [
'game.py',
'utils/importer.py',
'utils/steam_parser.py',
'utils/lutris_parser.py',
'utils/heroic_parser.py',
'utils/bottles_parser.py',
'utils/lutris_parser.py',
'utils/itch_parser.py',
'utils/get_games.py',
'utils/save_game.py',
'utils/save_cover.py',

View File

@@ -110,6 +110,9 @@ class PreferencesWindow(Adw.PreferencesWindow):
bottles_expander_row = Gtk.Template.Child()
bottles_file_chooser_button = Gtk.Template.Child()
itch_expander_row = Gtk.Template.Child()
itch_file_chooser_button = Gtk.Template.Child()
def __init__(self, parent_widget, **kwargs):
super().__init__(**kwargs)
self.schema = parent_widget.schema
@@ -281,6 +284,18 @@ class PreferencesWindow(Adw.PreferencesWindow):
if os.name == "nt":
self.sources_group.remove(self.bottles_expander_row)
# itch
ImportPreferences(
self,
"itch",
"itch",
"itch-location",
[Path("db") / "butler.db"],
self.itch_expander_row,
self.itch_file_chooser_button,
True,
)
def choose_folder(self, _widget, function):
self.file_chooser.select_folder(self.parent_widget, None, function, None)

View File

@@ -51,8 +51,8 @@ class Importer:
self.import_dialog.present()
def save_cover(self, game_id, cover_path):
save_cover(self.parent_widget, game_id, cover_path)
def save_cover(self, game_id, cover_path=None, pixbuf=None):
save_cover(self.parent_widget, game_id, cover_path, pixbuf)
def save_game(self, values=None):
if values:

161
src/utils/itch_parser.py Normal file
View File

@@ -0,0 +1,161 @@
# itch_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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import urllib.request
from pathlib import Path
from shutil import copyfile
from sqlite3 import connect
from time import time
from gi.repository import GdkPixbuf, Gio
def get_game(task, current_time, parent_widget, row, importer):
values = {}
values["game_id"] = f"itch_{row[0]}"
if (
values["game_id"] in parent_widget.games
and not parent_widget.games[values["game_id"]].removed
):
task.return_value(None)
return
values["added"] = current_time
values["executable"] = ["xdg-open", f"itch://caves/{row[4]}/launch"]
values["hidden"] = False
values["last_played"] = 0
values["name"] = row[1]
values["source"] = "itch"
if row[3] or row[2]:
tmp_file = Gio.File.new_tmp(None)[0]
try:
with urllib.request.urlopen(row[3] or row[2], timeout=5) as open_file:
Path(tmp_file.get_path()).write_bytes(open_file.read())
except urllib.error.URLError:
task.return_value(values)
return
cover_pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
tmp_file.read(), 2, 2, False
).scale_simple(400, 600, GdkPixbuf.InterpType.BILINEAR)
itch_pixbuf = GdkPixbuf.Pixbuf.new_from_stream(tmp_file.read())
itch_pixbuf = itch_pixbuf.scale_simple(
400,
itch_pixbuf.get_height() * (400 / itch_pixbuf.get_width()),
GdkPixbuf.InterpType.BILINEAR,
)
itch_pixbuf.composite(
cover_pixbuf,
0,
(600 - itch_pixbuf.get_height()) / 2,
itch_pixbuf.get_width(),
itch_pixbuf.get_height(),
0,
(600 - itch_pixbuf.get_height()) / 2,
1.0,
1.0,
GdkPixbuf.InterpType.BILINEAR,
255,
)
importer.save_cover(values["game_id"], pixbuf=cover_pixbuf)
task.return_value(values)
def get_games_async(parent_widget, rows, importer):
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(current_time, parent_widget, row):
def wrapper(task, *_unused):
get_game(
task,
current_time,
parent_widget,
row,
importer,
)
return wrapper
def update_games(_task, result):
final_values = result.propagate_value()[1]
# No need for an if statement as final_value would be None for games we don't want to save
importer.save_game(final_values)
for row in rows:
task = Gio.Task.new(None, None, update_games)
task.run_in_thread(create_func(current_time, parent_widget, row))
def itch_parser(parent_widget):
schema = parent_widget.schema
database_path = (
Path(schema.get_string("itch-location")) / "db" / "butler.db"
).expanduser()
if not database_path.is_file():
if Path("~/.var/app/io.itch.itch/config/itch/").expanduser().exists():
schema.set_string("itch-location", "~/.var/app/io.itch.itch/config/itch/")
elif (parent_widget.config_dir / "itch").exists():
schema.set_string("itch-location", str(parent_widget.config_dir / "itch"))
else:
return
database_path = (
Path(schema.get_string("itch-location")) / "db" / "butler.db"
).expanduser()
db_cache_dir = parent_widget.cache_dir / "cartridges" / "itch"
db_cache_dir.mkdir(parents=True, exist_ok=True)
# Copy the file because sqlite3 doesn't like databases in /run/user/
database_tmp_path = db_cache_dir / "butler.db"
copyfile(database_path, database_tmp_path)
db_request = """
SELECT
games.id,
games.title,
games.cover_url,
games.still_cover_url,
caves.id
FROM
'caves'
INNER JOIN
'games'
ON
caves.game_id = games.id
;
"""
connection = connect(database_tmp_path)
cursor = connection.execute(db_request)
rows = cursor.fetchall()
connection.close()
database_tmp_path.unlink(missing_ok=True)
importer = parent_widget.importer
importer.total_queue += len(rows)
importer.queue += len(rows)
get_games_async(parent_widget, rows, importer)

View File

@@ -21,7 +21,7 @@
from gi.repository import GdkPixbuf, Gio
def save_cover(parent_widget, game_id, cover_path, pixbuf=None):
def save_cover(parent_widget, game_id, cover_path=None, pixbuf=None):
covers_dir = parent_widget.data_dir / "cartridges" / "covers"
covers_dir.mkdir(parents=True, exist_ok=True)

View File

@@ -24,7 +24,7 @@ import urllib.request
from pathlib import Path
from time import time
from gi.repository import Gio, GLib
from gi.repository import Gio
def update_values_from_data(content, values):
@@ -70,25 +70,6 @@ def get_game(
values["added"] = current_time
values["last_played"] = 0
url = f'https://store.steampowered.com/api/appdetails?appids={values["appid"]}'
# On Linux the request is made through gvfs so the app can run without network permissions
if os.name == "nt":
try:
with urllib.request.urlopen(url, timeout=10) as open_file:
content = open_file.read().decode("utf-8")
except urllib.error.URLError:
content = None
else:
open_file = Gio.File.new_for_uri(url)
try:
content = open_file.load_contents()[1]
except GLib.GError:
content = None
if content:
values = update_values_from_data(content, values)
if (
steam_dir
/ "appcache"
@@ -105,8 +86,18 @@ def get_game(
),
)
try:
with urllib.request.urlopen(
f'https://store.steampowered.com/api/appdetails?appids={values["appid"]}',
timeout=5,
) as open_file:
content = open_file.read().decode("utf-8")
except urllib.error.URLError:
task.return_value(values)
return
values = update_values_from_data(content, values)
task.return_value(values)
return
def get_games_async(parent_widget, appmanifests, steam_dir, importer):
@@ -129,19 +120,12 @@ def get_games_async(parent_widget, appmanifests, steam_dir, importer):
return wrapper
def update_games(_task, result):
try:
final_values = result.propagate_value()[1]
# No need for an if statement as final_value would be None for games we don't want to save
importer.save_game(final_values)
except GLib.GError: # Handle the exception for the timeout
importer.save_game()
final_values = result.propagate_value()[1]
# No need for an if statement as final_value would be None for games we don't want to save
importer.save_game(final_values)
for appmanifest in appmanifests:
cancellable = Gio.Cancellable.new()
GLib.timeout_add_seconds(5, cancellable.cancel)
task = Gio.Task.new(None, cancellable, update_games)
task.set_return_on_cancel(True)
task = Gio.Task.new(None, None, update_games)
task.run_in_thread(
create_func(datatypes, current_time, parent_widget, appmanifest, steam_dir)
)