1
0
Fork 0
mirror of synced 2024-05-19 20:12:49 +12:00
Rare/rare/utils/utils.py

484 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 qtawesome
import requests
2021-12-10 09:59:07 +13:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QRunnable, QSettings, Qt, QFile, QDir
from PyQt5.QtGui import QPalette, QColor, QPixmap, QImage
from PyQt5.QtWidgets import QApplication, QStyleFactory
from requests.exceptions 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":
# noinspection PyUnresolvedReferences
from win32com.client import Dispatch # pylint: disable=E0401
2021-05-12 03:29:35 +12:00
2021-12-10 09:59:07 +13:00
from rare import image_dir, shared, resources_path
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(progress: pyqtSignal, results: 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
games, dlcs = core.get_game_and_dlc_list(True)
results.emit((games, dlcs), "gamelist")
2021-04-17 03:48:24 +12:00
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]
results.emit(no_assets, "no_assets")
2021-09-02 05:41:01 +12:00
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)
progress.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"))
2021-12-10 09:59:07 +13:00
custom_palette = load_color_scheme(f":/schemes/{color_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 = []
2021-12-10 09:59:07 +13:00
for file in QDir(":/schemes"):
if file.endswith(".scheme"):
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"))
2021-12-10 09:59:07 +13:00
file = QFile(f":/stylesheets/{style_sheet}")
file.open(QFile.ReadOnly)
stylesheet = file.readAll().data().decode("utf-8")
QApplication.instance().setStyleSheet(stylesheet)
qtawesome.set_defaults(color="white")
def get_style_sheets() -> List[str]:
styles = []
2021-12-10 09:59:07 +13:00
for file in QDir(":/stylesheets/"):
styles.append(file)
return styles
def get_translations():
2021-02-20 00:57:55 +13:00
langs = ["en"]
2021-12-11 08:05:51 +13:00
for i in os.listdir(os.path.join(resources_path, "languages")):
if i.endswith(".qm") and not i.startswith("qt_"):
2021-02-20 00:57:55 +13:00
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:
pm = get_pixmap(app_name)
grey_image = pm.toImage().convertToFormat(QImage.Format_Grayscale8)
return QPixmap.fromImage(grey_image)
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")
def get_default_platform(app_name):
if platform.system() != "Darwin" or app_name not in shared.api_results.mac_games:
return "Windows"
else:
2021-12-10 09:59:07 +13:00
return "Mac"