1
0
Fork 0
mirror of synced 2024-06-02 10:44:40 +12:00
Rare/rare/utils/utils.py

476 lines
17 KiB
Python
Raw Normal View History

2021-02-10 23:48:25 +13:00
import json
import math
2021-02-10 23:48:25 +13:00
import os
import platform
2021-02-18 06:19:37 +13:00
import shutil
import subprocess
2021-04-14 02:56:44 +12:00
import sys
2021-02-10 23:48:25 +13:00
from logging import getLogger
from typing import Tuple, List
2021-02-10 23:48:25 +13:00
import requests
import qtawesome
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QRunnable, QSettings, Qt
from PyQt5.QtWidgets import QApplication, QStyleFactory, QStyle
from PyQt5.QtGui import QPalette, QColor, QPixmap, QImage
from requests import HTTPError
2021-11-11 07:00:15 +13:00
from legendary.models.game import Game
from .models import PathSpec
2021-05-12 03:29:35 +12:00
# Windows
if platform.system() == "Windows":
from win32com.client import Dispatch # pylint: disable=E0401
2021-05-12 03:29:35 +12:00
from rare import languages_path, resources_path, image_dir, shared
2021-05-21 23:20:58 +12:00
# Mac not supported
2021-05-12 19:19:50 +12:00
from legendary.core import LegendaryCore
2021-02-10 23:48:25 +13:00
logger = getLogger("Utils")
2021-08-29 02:01:36 +12:00
settings = QSettings("Rare", "Rare")
2021-03-19 00:45:59 +13:00
2021-02-10 23:48:25 +13:00
def download_images(signal: pyqtSignal, core: LegendaryCore):
2021-08-17 09:08:15 +12:00
if not os.path.isdir(image_dir):
os.makedirs(image_dir)
2021-02-10 23:48:25 +13:00
logger.info("Create Image dir")
# Download Images
2021-04-17 03:48:24 +12:00
games, dlcs = core.get_game_and_dlc_list()
dlc_list = []
for i in dlcs.values():
dlc_list.append(i[0])
2021-09-02 05:41:01 +12:00
no_assets = core.get_non_asset_library_items()[0]
game_list = games + dlc_list + no_assets
2021-04-17 04:56:33 +12:00
for i, game in enumerate(game_list):
2021-02-20 00:57:55 +13:00
try:
download_image(game)
except json.decoder.JSONDecodeError:
2021-08-17 09:08:15 +12:00
shutil.rmtree(f"{image_dir}/{game.app_name}")
2021-02-20 00:57:55 +13:00
download_image(game)
signal.emit(i * 100 // len(game_list))
2021-02-10 23:48:25 +13:00
2021-02-18 06:19:37 +13:00
def download_image(game, force=False):
2021-08-17 09:08:15 +12:00
if force and os.path.exists(f"{image_dir}/{game.app_name}"):
shutil.rmtree(f"{image_dir}/{game.app_name}")
if not os.path.isdir(f"{image_dir}/" + game.app_name):
os.mkdir(f"{image_dir}/" + game.app_name)
2021-02-10 23:48:25 +13:00
# to get picture updates
2021-08-17 09:08:15 +12:00
if not os.path.isfile(f"{image_dir}/{game.app_name}/image.json"):
2021-04-14 02:56:44 +12:00
json_data = {"DieselGameBoxTall": None, "DieselGameBoxLogo": None, "Thumbnail": None}
2021-02-18 06:19:37 +13:00
else:
2021-08-17 09:08:15 +12:00
json_data = json.load(open(f"{image_dir}/{game.app_name}/image.json", "r"))
2021-02-18 06:19:37 +13:00
# Download
for image in game.metadata["keyImages"]:
2021-04-14 02:56:44 +12:00
if image["type"] == "DieselGameBoxTall" or image["type"] == "DieselGameBoxLogo" or image["type"] == "Thumbnail":
if image["type"] not in json_data.keys():
json_data[image["type"]] = None
2021-02-18 06:19:37 +13:00
if json_data[image["type"]] != image["md5"] or not os.path.isfile(
2021-08-17 09:08:15 +12:00
f"{image_dir}/{game.app_name}/{image['type']}.png"):
2021-02-18 06:19:37 +13:00
# Download
json_data[image["type"]] = image["md5"]
2021-08-17 09:08:15 +12:00
# os.remove(f"{image_dir}/{game.app_name}/{image['type']}.png")
json.dump(json_data, open(f"{image_dir}/{game.app_name}/image.json", "w"))
2021-02-18 06:19:37 +13:00
logger.info(f"Download Image for Game: {game.app_title}")
url = image["url"]
resp = requests.get(url)
img = QImage()
img.loadFromData(resp.content)
img = img.scaled(200, 200 * 4 // 3, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
img.save(os.path.join(image_dir, game.app_name, image["type"] + ".png"), format="PNG")
2021-02-10 23:48:25 +13:00
color_role_map = {
0: "WindowText",
1: "Button",
2: "Light",
3: "Midlight",
4: "Dark",
5: "Mid",
6: "Text",
7: "BrightText",
8: "ButtonText",
9: "Base",
10: "Window",
11: "Shadow",
12: "Highlight",
13: "HighlightedText",
14: "Link",
15: "LinkVisited",
16: "AlternateBase",
# 17: "NoRole",
18: "ToolTipBase",
19: "ToolTipText",
20: "PlaceholderText",
# 21: "NColorRoles",
}
color_group_map = {
0: "Active",
1: "Disabled",
2: "Inactive",
}
def load_color_scheme(path: str) -> QPalette:
palette = QPalette()
scheme = QSettings(path, QSettings.IniFormat)
try:
scheme.beginGroup("ColorScheme")
for g in color_group_map:
scheme.beginGroup(color_group_map[g])
group = QPalette.ColorGroup(g)
for r in color_role_map:
role = QPalette.ColorRole(r)
color = scheme.value(color_role_map[r], None)
if color is not None:
palette.setColor(group, role, QColor(color))
else:
palette.setColor(group, role, palette.color(QPalette.Active, role))
scheme.endGroup()
scheme.endGroup()
except:
palette = None
return palette
def set_color_pallete(color_scheme: str):
if not color_scheme:
QApplication.instance().setStyle(QStyleFactory.create(QApplication.instance().property('rareDefaultQtStyle')))
QApplication.instance().setStyleSheet("")
QApplication.instance().setPalette(QApplication.instance().style().standardPalette())
return
QApplication.instance().setStyle(QStyleFactory.create("Fusion"))
custom_palette = load_color_scheme(os.path.join(resources_path, "colors", color_scheme + ".scheme"))
if custom_palette is not None:
QApplication.instance().setPalette(custom_palette)
qtawesome.set_defaults(color=custom_palette.color(QPalette.Text))
def get_color_schemes() -> List[str]:
colors = []
for file in os.listdir(os.path.join(resources_path, "colors")):
if file.endswith(".scheme") and os.path.isfile(os.path.join(resources_path, "colors", file)):
colors.append(file.replace(".scheme", ""))
return colors
def set_style_sheet(style_sheet: str):
if not style_sheet:
QApplication.instance().setStyle(QStyleFactory.create(QApplication.instance().property('rareDefaultQtStyle')))
QApplication.instance().setStyleSheet("")
return
QApplication.instance().setStyle(QStyleFactory.create("Fusion"))
stylesheet = open(os.path.join(resources_path, "stylesheets", style_sheet, "stylesheet.qss")).read()
style_resource_path = os.path.join(resources_path, "stylesheets", style_sheet, "")
if platform.system() == "Windows":
style_resource_path = style_resource_path.replace('\\', '/')
QApplication.instance().setStyleSheet(stylesheet.replace("@path@", style_resource_path))
qtawesome.set_defaults(color="white")
def get_style_sheets() -> List[str]:
styles = []
for folder in os.listdir(os.path.join(resources_path, "stylesheets")):
if os.path.isfile(os.path.join(resources_path, "stylesheets", folder, "stylesheet.qss")):
styles.append(folder)
return styles
def get_translations():
2021-02-20 00:57:55 +13:00
langs = ["en"]
for i in os.listdir(languages_path):
2021-02-20 00:57:55 +13:00
if i.endswith(".qm"):
langs.append(i.split(".")[0])
return langs
def get_latest_version():
2021-04-20 01:44:28 +12:00
try:
resp = requests.get("https://api.github.com/repos/Dummerle/Rare/releases/latest")
tag = resp.json()["tag_name"]
2021-04-20 01:44:28 +12:00
return tag
except requests.exceptions.ConnectionError:
return "0.0.0"
2021-04-12 07:02:56 +12:00
def get_size(b: int) -> str:
for i in ['', 'K', 'M', 'G', 'T', 'P', 'E']:
if b < 1024:
return f"{b:.2f}{i}B"
b /= 1024
2021-04-14 02:56:44 +12:00
def create_rare_desktop_link(type_of_link):
# Linux
if platform.system() == "Linux":
if type_of_link == "desktop":
path = os.path.expanduser("~/Desktop/")
elif type_of_link == "start_menu":
path = os.path.expanduser("~/.local/share/applications/")
else:
return
if p := os.environ.get("APPIMAGE"):
executable = p
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
2021-08-18 02:05:00 +12:00
with open(os.path.join(path, "Rare.desktop"), "w") as desktop_file:
desktop_file.write("[Desktop Entry]\n"
f"Name=Rare\n"
f"Type=Application\n"
f"Icon={os.path.join(resources_path, 'images', 'Rare.png')}\n"
f"Exec={executable}\n"
"Terminal=false\n"
"StartupWMClass=rare\n"
)
desktop_file.close()
2021-08-18 02:05:00 +12:00
os.chmod(os.path.expanduser(os.path.join(path, "Rare.desktop")), 0o755)
elif platform.system() == "Windows":
# Target of shortcut
if type_of_link == "desktop":
target_folder = os.path.expanduser('~/Desktop/')
elif type_of_link == "start_menu":
target_folder = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu")
else:
logger.warning("No valid type of link")
return
linkName = "Rare.lnk"
# Path to location of link file
pathLink = os.path.join(target_folder, linkName)
2021-09-19 02:34:43 +12:00
executable = sys.executable
executable = executable.replace("python.exe", "pythonw.exe")
logger.debug(executable)
# Add shortcut
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(pathLink)
2021-09-05 22:30:33 +12:00
shortcut.Targetpath = executable
2021-09-19 02:34:43 +12:00
if not sys.executable.endswith("Rare.exe"):
shortcut.Arguments = os.path.abspath(sys.argv[0])
# Icon
shortcut.IconLocation = os.path.join(resources_path, "images", "Rare.ico")
shortcut.save()
def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop") -> bool:
2021-04-14 02:56:44 +12:00
igame = core.get_installed_game(app_name)
2021-09-05 22:30:33 +12:00
if os.path.exists(p := os.path.join(image_dir, igame.app_name, 'Thumbnail.png')):
icon = p
elif os.path.exists(p := os.path.join(image_dir, igame.app_name, "DieselGameBoxLogo.png")):
icon = p
else:
2021-09-05 22:30:33 +12:00
icon = os.path.join(os.path.join(image_dir, igame.app_name, "DieselGameBoxTall.png"))
icon = icon.replace(".png", "")
2021-09-06 08:00:14 +12:00
2021-04-14 02:56:44 +12:00
# Linux
if platform.system() == "Linux":
2021-04-14 04:01:25 +12:00
if type_of_link == "desktop":
path = os.path.expanduser(f"~/Desktop/")
elif type_of_link == "start_menu":
path = os.path.expanduser("~/.local/share/applications/")
else:
return False
if not os.path.exists(path):
return False
if p := os.environ.get("APPIMAGE"):
executable = p
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
2021-04-14 04:01:25 +12:00
with open(f"{path}{igame.title}.desktop", "w") as desktop_file:
2021-04-14 02:56:44 +12:00
desktop_file.write("[Desktop Entry]\n"
f"Name={igame.title}\n"
f"Type=Application\n"
2021-05-12 03:29:35 +12:00
f"Icon={icon}.png\n"
f"Exec={executable} launch {app_name}\n"
2021-04-14 02:56:44 +12:00
"Terminal=false\n"
"StartupWMClass=rare-game\n"
)
2021-05-19 03:07:39 +12:00
desktop_file.close()
os.chmod(os.path.expanduser(f"{path}{igame.title}.desktop"), 0o755)
# Windows
elif platform.system() == "Windows":
2021-05-12 03:29:35 +12:00
# Target of shortcut
if type_of_link == "desktop":
2021-05-12 03:29:35 +12:00
target_folder = os.path.expanduser('~/Desktop/')
elif type_of_link == "start_menu":
2021-05-12 03:29:35 +12:00
target_folder = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu")
else:
2021-05-12 03:29:35 +12:00
logger.warning("No valid type of link")
return False
if not os.path.exists(target_folder):
return False
2021-05-12 03:29:35 +12:00
# Name of link file
2021-05-17 21:38:30 +12:00
linkName = igame.title
for c in r'<>?":|\/*':
linkName.replace(c, "")
linkName = linkName.strip() + '.lnk'
2021-05-12 03:29:35 +12:00
# Path to location of link file
pathLink = os.path.join(target_folder, linkName)
# Add shortcut
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(pathLink)
if sys.executable.endswith("Rare.exe"):
executable = sys.executable
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
shortcut.Targetpath = executable
shortcut.Arguments = f'launch {app_name}'
2021-05-12 03:29:35 +12:00
shortcut.WorkingDirectory = os.getcwd()
# Icon
if not os.path.exists(icon + ".ico"):
img = QImage()
img.load(icon + ".png")
img.save(icon + ".ico")
2021-05-12 03:29:35 +12:00
logger.info("Create Icon")
shortcut.IconLocation = os.path.join(icon + ".ico")
2021-05-12 19:19:50 +12:00
2021-05-12 03:29:35 +12:00
shortcut.save()
return True
elif platform.system() == "Darwin":
return False
2021-08-17 09:08:15 +12:00
def get_pixmap(app_name: str) -> QPixmap:
for img in ["FinalArt.png", "DieselGameBoxTall.png", "DieselGameBoxLogo.png"]:
if os.path.exists(image := os.path.join(image_dir, app_name, img)):
pixmap = QPixmap(image)
break
else:
pixmap = QPixmap()
return pixmap
def get_uninstalled_pixmap(app_name: str) -> QPixmap:
if os.path.exists(image := os.path.join(image_dir, app_name, "UninstalledArt.png")):
pixmap = QPixmap(image)
else:
pixmap = QPixmap()
return pixmap
2021-08-08 09:42:40 +12:00
def optimal_text_background(image: list) -> Tuple[int, int, int]:
"""
Finds an optimal background color for text on the image by calculating the
average color of the image and inverting it.
The image list is supposed to be a one-dimensional list of arbitrary length
containing RGB tuples, ranging from 0 to 255.
"""
# cursed, I know
average = map(lambda value: value // len(image), map(sum, zip(*image)))
inverted = map(lambda value: 255 - value, average)
return tuple(inverted)
def text_color_for_background(background: Tuple[int, int, int]) -> Tuple[int,
int,
int]:
"""
Calculates whether a black or white text color would fit better for the
given background, and returns that color. This is done by calculating the
luminance and simple comparing of bounds
"""
# see https://alienryderflex.com/hsp.html
(red, green, blue) = background
luminance = math.sqrt(
0.299 * red ** 2 +
0.587 * green ** 2 +
0.114 * blue ** 2)
if luminance < 127:
return 255, 255, 255
else:
return 0, 0, 0
class WineResolverSignals(QObject):
result_ready = pyqtSignal(str)
class WineResolver(QRunnable):
def __init__(self, path: str, app_name: str, core: LegendaryCore):
super(WineResolver, self).__init__()
self.setAutoDelete(True)
self.signals = WineResolverSignals()
self.wine_env = os.environ.copy()
self.wine_env.update(core.get_app_environment(app_name))
self.wine_env['WINEDLLOVERRIDES'] = 'winemenubuilder=d;mscoree=d;mshtml=d;'
self.wine_env['DISPLAY'] = ''
self.wine_binary = core.lgd.config.get(
app_name, 'wine_executable',
fallback=core.lgd.config.get('default', 'wine_executable', fallback='wine'))
self.winepath_binary = os.path.join(os.path.dirname(self.wine_binary), 'winepath')
self.path = PathSpec(core, app_name).cook(path)
@pyqtSlot()
def run(self):
if 'WINEPREFIX' not in self.wine_env or not os.path.exists(self.wine_env['WINEPREFIX']):
# pylint: disable=E1136
self.signals.result_ready[str].emit(str())
return
if not os.path.exists(self.wine_binary) or not os.path.exists(self.winepath_binary):
# pylint: disable=E1136
self.signals.result_ready[str].emit(str())
return
path = self.path.strip().replace('/', '\\')
# lk: if path does not exist form
cmd = [self.wine_binary, 'cmd', '/c', 'echo', path]
# lk: if path exists and needs a case sensitive interpretation form
# cmd = [self.wine_binary, 'cmd', '/c', f'cd {path} & cd']
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=self.wine_env, shell=False, text=True)
out, err = proc.communicate()
# Clean wine output
out = out.strip().strip('"')
proc = subprocess.Popen([self.winepath_binary, '-u', out],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=self.wine_env, shell=False, text=True)
out, err = proc.communicate()
real_path = os.path.realpath(out.strip())
# pylint: disable=E1136
self.signals.result_ready[str].emit(real_path)
return
class CloudResultSignal(QObject):
result_ready = pyqtSignal(list) # List[SaveGameFile]
class CloudWorker(QRunnable):
def __init__(self):
super(CloudWorker, self).__init__()
self.signals = CloudResultSignal()
self.setAutoDelete(True)
def run(self) -> None:
try:
result = shared.core.get_save_games()
except HTTPError():
result = None
self.signals.result_ready.emit(result)
2021-11-11 07:00:15 +13:00
def get_raw_save_path(game: Game):
if game.supports_cloud_saves:
return game.metadata.get("customAttributes", {}).get("CloudSaveFolder", {}).get("value")