1
0
Fork 0
mirror of synced 2024-06-29 11:40:37 +12:00

Merge pull request #242 from loathingKernel/fixups

Nuitka and another round of fixes for Windows
This commit is contained in:
Dummerle 2022-09-11 23:39:58 +02:00 committed by GitHub
commit 4ef755fa2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 398 additions and 144 deletions

44
.github/workflows/checks.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: "Checks"
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'rare/**'
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- main
paths:
- 'rare/**'
jobs:
pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install Test Dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
- name: Install Target Dependencies
run: |
pip3 install -r requirements.txt
pip3 install -r requirements-presence.txt
- name: Analysis with pylint
run: |
pylint -E rare --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5

View file

@ -1,30 +1,12 @@
name: "Test" name: "Release Tests"
on: on:
workflow_dispatch: workflow_dispatch:
jobs:
pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
pip install -r requirements.txt
pip install pypresence
- name: Analysing the code with pylint
run: |
pylint -E rare --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5
jobs:
deb-package: deb-package:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -46,7 +28,8 @@ jobs:
makedeb -d makedeb -d
mv *.deb Rare.deb mv *.deb Rare.deb
- uses: actions/upload-artifact@v2 - name: Upload to Artifacts
uses: actions/upload-artifact@v2
with: with:
name: Rare.deb name: Rare.deb
path: build/Rare.deb path: build/Rare.deb
@ -72,11 +55,55 @@ jobs:
appimage-builder --skip-test appimage-builder --skip-test
mv Rare-*.AppImage Rare.AppImage mv Rare-*.AppImage Rare.AppImage
- uses: actions/upload-artifact@v2 - name: Upload to Artifacts
uses: actions/upload-artifact@v2
with: with:
name: Rare.AppImage name: Rare.AppImage
path: Rare.AppImage path: Rare.AppImage
nuitka:
runs-on: "windows-latest"
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install build dependencies
run: pip3 install nuitka ordered-set
- name: Install target dependencies
run: |
pip3 install -r requirements.txt
pip3 install -r requirements-presence.txt
- name: Version
id: version
run: |
git fetch --prune --unshallow
echo "::set-output name=tag_offset::$(git describe --long --tags)"
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Build
run: >-
python -m nuitka
--assume-yes-for-downloads
--follow-imports --prefer-source-code --mingw64 --lto=no --jobs=2 --static-libpython=no --standalone
--enable-plugin=anti-bloat --enable-plugin=pyqt5 --show-anti-bloat-changes --nofollow-import-to="*.tests"
--nofollow-import-to="*.distutils" --include-package-data=qtawesome
--include-data-dir=rare\resources\images=rare\resources\images
--include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
--windows-icon-from-ico=rare\resources\images\Rare.ico
--windows-company-name=Rare --windows-product-name=Rare --windows-file-description=rare.exe
--windows-file-version=0.0.0.0
--windows-product-version=0.0.0.0
--windows-disable-console
rare
- name: Upload to Artifacts
uses: actions/upload-artifact@v2
with:
name: Rare-Windows-${{ steps.version.outputs.tag_offset }}
path: rare.dist
cx_freeze: cx_freeze:
runs-on: "windows-latest" runs-on: "windows-latest"
steps: steps:
@ -85,19 +112,28 @@ jobs:
submodules: true submodules: true
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: '3.9'
- name: Dependencies - name: Install Build Dependencies
run: pip3 install -r requirements.txt
- name: cx_freeze
run: pip3 install --upgrade cx_freeze wheel run: pip3 install --upgrade cx_freeze wheel
- name: pypresence - name: Install Target Dependencies
run: pip3 install pypresence run: |
pip3 install -r requirements.txt
pip3 install -r requirements-presence.txt
- name: Version
id: version
run: |
git fetch --prune --unshallow
echo "::set-output name=tag_offset::$(git describe --long --tags)"
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Build - name: Build
run: python freeze.py bdist_msi run: |
python freeze.py bdist_msi
mv dist/*.msi dist/Rare-Windows.msi
- uses: actions/upload-artifact@v2 - name: Upload to Artifacts
uses: actions/upload-artifact@v2
with: with:
name: Rare-Windows.msi name: Rare-Windows-${{ steps.version.outputs.tag_offset }}.msi
path: dist/*.msi path: dist/*.msi
mac_os: mac_os:
@ -120,8 +156,12 @@ jobs:
run: mv rare/__main__.py __main__.py run: mv rare/__main__.py __main__.py
- name: run pyinstaller - name: run pyinstaller
run: | run: >-
pyinstaller -F --name Rare --add-data "rare/resources/languages/*:rare/resources/languages" --add-data "rare/resources/images/*:rare/resources/images/" --windowed --icon rare/resources/images/Rare.icns --hidden-import=legendary __main__.py pyinstaller -F --name Rare
--add-data "rare/resources/languages/*:rare/resources/languages"
--add-data "rare/resources/images/*:rare/resources/images/"
--windowed --icon rare/resources/images/Rare.icns
--hidden-import=legendary __main__.py
- name: create dmg - name: create dmg
run: | run: |

View file

@ -1,9 +1,12 @@
name: New Release
name: "Release"
on: on:
release: release:
types: [ published ] types: [ published ]
jobs: jobs:
pypi-deploy: pypi-deploy:
if: "!github.event.release.prerelease" if: "!github.event.release.prerelease"
@ -48,7 +51,7 @@ jobs:
makedeb -d makedeb -d
mv *.deb Rare.deb mv *.deb Rare.deb
- name: Upload files to GitHub - name: Upload to Releases
uses: svenstaro/upload-release-action@2.2.1 uses: svenstaro/upload-release-action@2.2.1
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
@ -79,7 +82,7 @@ jobs:
mv Rare-*.AppImage Rare.AppImage mv Rare-*.AppImage Rare.AppImage
mv Rare-*.AppImage.zsync Rare.AppImage.zsync mv Rare-*.AppImage.zsync Rare.AppImage.zsync
- name: Upload AppImage to GitHub - name: Upload to Releases
uses: svenstaro/upload-release-action@2.2.1 uses: svenstaro/upload-release-action@2.2.1
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
@ -96,6 +99,49 @@ jobs:
tag: ${{ github.ref }} tag: ${{ github.ref }}
overwrite: true overwrite: true
nuitka:
runs-on: "windows-latest"
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install build dependencies
run: pip3 install nuitka ordered-set
- name: Install target dependencies
run: |
pip3 install -r requirements.txt
pip3 install -r requirements-presence.txt
- name: Build
run: >-
python -m nuitka
--assume-yes-for-downloads
--follow-imports --prefer-source-code --mingw64 --lto=no --jobs=2 --static-libpython=no --standalone
--enable-plugin=anti-bloat --enable-plugin=pyqt5 --show-anti-bloat-changes --nofollow-import-to="*.tests"
--nofollow-import-to="*.distutils" --include-package-data=qtawesome
--include-data-dir=rare\resources\images=rare\resources\images
--include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
--windows-icon-from-ico=rare\resources\images\Rare.ico
--windows-company-name=Rare --windows-product-name=Rare --windows-file-description=rare.exe
--windows-file-version=${{ github.event.release.tag_name }}
--windows-product-version=${{ github.event.release.tag_name }}
--windows-disable-console
rare
- name: Compress
run: |
python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
- name: Upload to Releases
uses: svenstaro/upload-release-action@2.2.1
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: Rare-Windows.zip
asset_name: Rare-Windows-${{ github.event.release.tag_name }}.zip
tag: ${{ github.ref }}
overwrite: true
cx_freeze: cx_freeze:
runs-on: "windows-latest" runs-on: "windows-latest"
steps: steps:
@ -104,24 +150,27 @@ jobs:
submodules: true submodules: true
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.8' python-version: '3.9'
- name: Dependencies - name: Install Build Dependencies
run: pip3 install -r requirements.txt
- name: cx_freeze
run: pip3 install --upgrade cx_freeze wheel run: pip3 install --upgrade cx_freeze wheel
- name: Install Target Dependencies
run: |
pip3 install -r requirements.txt
pip3 install -r requirements-presence.txt
- name: Build - name: Build
run: python freeze.py bdist_msi run: |
python freeze.py bdist_msi
mv dist/*.msi dist/Rare-Windows.msi
- name: Rename File - name: Upload to Releases
run: mv dist/*.msi dist/Rare.msi
- name: Upload to GitHub
uses: svenstaro/upload-release-action@2.2.1 uses: svenstaro/upload-release-action@2.2.1
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/Rare.msi file: dist/Rare-Windows.msi
asset_name: Rare-${{ github.event.release.tag_name }}.msi asset_name: Rare-Windows-${{ github.event.release.tag_name }}.msi
tag: ${{ github.ref }} tag: ${{ github.ref }}
overwrite: true overwrite: true
mac_os: mac_os:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
@ -142,8 +191,12 @@ jobs:
run: mv rare/__main__.py __main__.py run: mv rare/__main__.py __main__.py
- name: run pyinstaller - name: run pyinstaller
run: | run: >-
pyinstaller -F --name Rare --add-data "rare/resources/languages/*:rare/resources/languages" --add-data "rare/resources/images/*:rare/resources/images/" --windowed --icon rare/resources/images/Rare.icns --hidden-import=legendary __main__.py pyinstaller -F --name Rare
--add-data "rare/resources/languages/*:rare/resources/languages"
--add-data "rare/resources/images/*:rare/resources/images/"
--windowed --icon rare/resources/images/Rare.icns
--hidden-import=legendary __main__.py
- name: create dmg - name: create dmg
run: | run: |

1
misc/nuitka_rare.bat Normal file
View file

@ -0,0 +1 @@
python -m nuitka --assume-yes-for-downloads --follow-imports --prefer-source-code --mingw64 --lto=no --jobs=2 --static-libpython=no --standalone --enable-plugin=anti-bloat --enable-plugin=pyqt5 --show-anti-bloat-changes --nofollow-import-to="*.tests" --nofollow-import-to="*.distutils" --include-package-data=qtawesome --include-data-dir=rare\resources\images=rare\resources\images --include-data-files=rare\resources\languages=rare\resources\languages="*.qm" --windows-icon-from-ico=rare\resources\images\Rare.ico --windows-company-name=Rare --windows-product-name=Rare --windows-file-description=rare.exe --windows-file-version=1.9.0 --windows-product-version=1.9.0 --windows-disable-console rare

5
misc/pip_upgrade_venv.py Normal file
View file

@ -0,0 +1,5 @@
import pkg_resources
from subprocess import call
for dist in pkg_resources.working_set:
call("python -m pip install --upgrade " + dist.project_name, shell=True)

View file

@ -33,24 +33,36 @@ pywebview = [
legendary-gl = "^0.20.28" legendary-gl = "^0.20.28"
typing-extensions = "^4.3.0" typing-extensions = "^4.3.0"
[tool.poetry.scripts] [tool.poetry.scripts]
start = "rare.__main__:main" start = "rare.__main__:main"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
Nuitka = "^1.0.6" Nuitka = "^1.0.6"
pylint = "^2.15.0" pylint = "^2.15.0"
black = "^22.6.0" black = "^22.6.0"
#[build-system] #[build-system]
#requires = ["setuptools>=42"z, "wheel", "nuitka", "toml"] #requires = ["setuptools>=42", "wheel", "nuitka", "toml"]
#build-backend = "nuitka.distutils.Build" #build-backend = "nuitka.distutils.Build"
[nuitka] [nuitka]
show-scons = true assume-yes-for-downloads = true
enable-plugin = ["pyqt5", "anti-bloat"] follow-imports = true
prefer-source-code = true
mingw64 = true
lto = true
static-libpython = false
standalone = true
show-scons = false
enable-plugin = ["anti-bloat", "pyqt5"]
show-anti-bloat-changes = true show-anti-bloat-changes = true
nofollow-import-to = ["*.tests", "*.distutils"] nofollow-import-to = ["*.tests", "*.distutils"]
include-package-data = "qtawesome"
include-data-dir = "rare/resources/images=rare/resources/images"
include-data-files = "rare/resources/languages=rare/resources/laguanges=*.qm"
windows-icon-from-ico = "rare/resources/images/Rare.ico"
windows-company-name = "Rare"
windows-product-name = "Rare"
windows-file-version = "1.9.0"
windows-product-version = "1.9.0"
windows-disable-console = true

View file

@ -117,4 +117,12 @@ if __name__ == "__main__":
if "__compiled__" not in globals(): if "__compiled__" not in globals():
sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
# If we are on Windows, and we are in a "compiled" GUI application form
# stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'`
# errors, redirect both of them to devnull
if os.name == "nt" and (getattr(sys, "frozen", False) or ("__compiled__" in globals())):
f = open(os.devnull, 'w')
sys.stdout = f
sys.stderr = f
main() main()

View file

@ -20,7 +20,7 @@ from rare.shared import (
ArgumentsSingleton, ArgumentsSingleton,
) )
from rare.shared.rare_core import RareCore from rare.shared.rare_core import RareCore
from rare.utils import legendary_utils, config_helper, paths from rare.utils import config_helper, paths
from rare.widgets.rare_app import RareApp from rare.widgets.rare_app import RareApp
logger = logging.getLogger("Rare") logger = logging.getLogger("Rare")
@ -60,23 +60,23 @@ class App(RareApp):
self.load_translator(lang) self.load_translator(lang)
# set Application name for settings # set Application name for settings
self.mainwindow: Optional[MainWindow] = None self.main_window: Optional[MainWindow] = None
self.launch_dialog: Optional[LaunchDialog] = None self.launch_dialog: Optional[LaunchDialog] = None
self.timer = QTimer() self.timer: Optional[QTimer] = None
# launch app # launch app
self.launch_dialog = LaunchDialog(parent=None) self.launch_dialog = LaunchDialog(parent=None)
self.launch_dialog.quit_app.connect(self.launch_dialog.close) self.launch_dialog.quit_app.connect(self.launch_dialog.close)
self.launch_dialog.quit_app.connect(lambda ec: exit(ec)) self.launch_dialog.quit_app.connect(lambda x: sys.exit(x))
self.launch_dialog.start_app.connect(self.start_app) self.launch_dialog.start_app.connect(self.start_app)
self.launch_dialog.start_app.connect(self.launch_dialog.close) self.launch_dialog.start_app.connect(self.launch_dialog.close)
self.launch_dialog.login() self.launch_dialog.login()
def poke_timer(self):
dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]) dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1])
dt_now = datetime.utcnow() dt_now = datetime.utcnow()
td = abs(dt_exp - dt_now) td = abs(dt_exp - dt_now)
self.timer.timeout.connect(self.re_login)
self.timer.start(int(td.total_seconds() - 60) * 1000) self.timer.start(int(td.total_seconds() - 60) * 1000)
def re_login(self): def re_login(self):
@ -86,36 +86,18 @@ class App(RareApp):
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
self.timer.start(3000) # try again if no connection self.timer.start(3000) # try again if no connection
return return
dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]) self.poke_timer()
dt_now = datetime.utcnow()
td = abs(dt_exp - dt_now)
self.timer.start(int(td.total_seconds() - 60) * 1000)
def start_app(self): def start_app(self):
for igame in self.core.get_installed_list(): self.timer = QTimer()
if not os.path.exists(igame.install_path): self.timer.timeout.connect(self.re_login)
# lk; since install_path is lost anyway, set keep_files to True self.poke_timer()
# lk: to avoid spamming the log with "file not found" errors
legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True)
logger.info(f"Uninstalled {igame.title}, because no game files exist")
continue
# lk: games that don't have an override and can't find their executable due to case sensitivity
# lk: will still erroneously require verification. This might need to be removed completely
# lk: or be decoupled from the verification requirement
if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""):
igame_executable = override_exe
else:
igame_executable = igame.executable
if not os.path.exists(os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))):
igame.needs_verification = True
self.core.lgd.set_installed_game(igame.app_name, igame)
logger.info(f"{igame.title} needs verification")
self.mainwindow = MainWindow() self.main_window = MainWindow()
self.mainwindow.exit_app.connect(self.exit_app) self.main_window.exit_app.connect(self.exit_app)
if not self.args.silent: if not self.args.silent:
self.mainwindow.show() self.main_window.show()
if self.args.test_start: if self.args.test_start:
self.exit_app(0) self.exit_app(0)
@ -127,9 +109,9 @@ class App(RareApp):
self.timer.stop() self.timer.stop()
self.timer.deleteLater() self.timer.deleteLater()
self.timer = None self.timer = None
if self.mainwindow is not None: if self.main_window is not None:
self.mainwindow.close() self.main_window.close()
self.mainwindow = None self.main_window = None
self.rare_core.deleteLater() self.rare_core.deleteLater()
del self.rare_core del self.rare_core
self.processEvents() self.processEvents()

View file

@ -1,22 +1,26 @@
import os
import platform import platform
from logging import getLogger from logging import getLogger
from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings, pyqtSlot
from PyQt5.QtWidgets import QDialog, QApplication from PyQt5.QtWidgets import QDialog, QApplication
from legendary.models.game import Game
from requests.exceptions import ConnectionError, HTTPError from requests.exceptions import ConnectionError, HTTPError
from rare.components.dialogs.login import LoginDialog from rare.components.dialogs.login import LoginDialog
from rare.models.apiresults import ApiResults from rare.models.apiresults import ApiResults
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton, ImageManagerSingleton from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton, ImageManagerSingleton
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
from rare.utils import legendary_utils
from rare.utils.misc import CloudWorker from rare.utils.misc import CloudWorker
from rare.widgets.elide_label import ElideLabel
logger = getLogger("LaunchDialog") logger = getLogger("LaunchDialog")
class LaunchWorker(QRunnable): class LaunchWorker(QRunnable):
class Signals(QObject): class Signals(QObject):
progress = pyqtSignal(int) progress = pyqtSignal(int, str)
result = pyqtSignal(object, str) result = pyqtSignal(object, str)
finished = pyqtSignal() finished = pyqtSignal()
@ -26,16 +30,41 @@ class LaunchWorker(QRunnable):
self.signals = LaunchWorker.Signals() self.signals = LaunchWorker.Signals()
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
def run(self): def run_real(self):
pass pass
def run(self):
self.run_real()
self.signals.deleteLater()
class ImageWorker(LaunchWorker): class ImageWorker(LaunchWorker):
# FIXME: this is a middle-ground solution for concurrent downloads
class DownloadSlot(QObject):
def __init__(self, signals: LaunchWorker.Signals):
super(ImageWorker.DownloadSlot, self).__init__()
self.signals = signals
self.counter = 0
self.length = 0
@pyqtSlot(object)
def counter_inc(self, game: Game):
self.signals.progress.emit(
int(self.counter / self.length * 75),
self.tr("Downloading image for <b>{}</b>").format(game.app_title)
)
self.counter += 1
def __init__(self): def __init__(self):
super(ImageWorker, self).__init__() super(ImageWorker, self).__init__()
# FIXME: this is a middle-ground solution for concurrent downloads
self.dl_slot = ImageWorker.DownloadSlot(self.signals)
self.image_manager = ImageManagerSingleton() self.image_manager = ImageManagerSingleton()
def run(self): def tr(self, t) -> str:
return QApplication.instance().translate(self.__class__.__name__, t)
def run_real(self):
# Download Images # Download Images
games, dlcs = self.core.get_game_and_dlc_list(update_assets=True, skip_ue=False) games, dlcs = self.core.get_game_and_dlc_list(update_assets=True, skip_ue=False)
self.signals.result.emit((games, dlcs), "gamelist") self.signals.result.emit((games, dlcs), "gamelist")
@ -47,12 +76,46 @@ class ImageWorker(LaunchWorker):
game_list = games + dlc_list + na_games + na_dlc_list game_list = games + dlc_list + na_games + na_dlc_list
self.dl_slot.length = len(game_list)
for i, game in enumerate(game_list): for i, game in enumerate(game_list):
if game.app_title == "Unreal Engine": if game.app_title == "Unreal Engine":
game.app_title += f" {game.app_name.split('_')[-1]}" game.app_title += f" {game.app_name.split('_')[-1]}"
self.core.lgd.set_game_meta(game.app_name, game) self.core.lgd.set_game_meta(game.app_name, game)
self.image_manager.download_image_blocking(game) # self.image_manager.download_image_blocking(game)
self.signals.progress.emit(int(i / len(game_list) * 100)) self.image_manager.download_image(game, self.dl_slot.counter_inc, priority=0)
# FIXME: this is a middle-ground solution for concurrent downloads
while self.dl_slot.counter < len(game_list):
QApplication.instance().processEvents()
self.dl_slot.deleteLater()
igame_list = self.core.get_installed_list(include_dlc=True)
# FIXME: incorporate installed game status checking here for now, still slow
for i, igame in enumerate(igame_list):
self.signals.progress.emit(
int(i / len(igame_list) * 25) + 75,
self.tr("Validating install for <b>{}</b>").format(igame.title)
)
if not os.path.exists(igame.install_path):
# lk; since install_path is lost anyway, set keep_files to True
# lk: to avoid spamming the log with "file not found" errors
legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True)
logger.info(f"Uninstalled {igame.title}, because no game files exist")
continue
# lk: games that don't have an override and can't find their executable due to case sensitivity
# lk: will still erroneously require verification. This might need to be removed completely
# lk: or be decoupled from the verification requirement
if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""):
igame_executable = override_exe
else:
igame_executable = igame.executable
if not os.path.exists(os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))):
igame.needs_verification = True
self.core.lgd.set_installed_game(igame.app_name, igame)
logger.info(f"{igame.title} needs verification")
# FIXME: end
self.signals.progress.emit(100, self.tr("Launching Rare"))
self.signals.finished.emit() self.signals.finished.emit()
@ -61,7 +124,7 @@ class ApiRequestWorker(LaunchWorker):
super(ApiRequestWorker, self).__init__() super(ApiRequestWorker, self).__init__()
self.settings = QSettings() self.settings = QSettings()
def run(self) -> None: def run_real(self) -> None:
if self.settings.value("mac_meta", platform.system() == "Darwin", bool): if self.settings.value("mac_meta", platform.system() == "Darwin", bool):
try: try:
result = self.core.get_game_and_dlc_list(update_assets=False, platform="Mac") result = self.core.get_game_and_dlc_list(update_assets=False, platform="Mac")
@ -102,12 +165,15 @@ class LaunchDialog(QDialog):
self.ui = Ui_LaunchDialog() self.ui = Ui_LaunchDialog()
self.ui.setupUi(self) self.ui.setupUi(self)
self.progress_info = ElideLabel(parent=self)
self.layout().addWidget(self.progress_info)
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.args = ArgumentsSingleton() self.args = ArgumentsSingleton()
self.thread_pool = QThreadPool().globalInstance() self.thread_pool = QThreadPool().globalInstance()
self.api_results = ApiResults() self.api_results = ApiResults()
self.login_dialog = LoginDialog(core=self.core, parent=self) self.login_dialog = LoginDialog(core=self.core, parent=parent)
def login(self): def login(self):
do_launch = True do_launch = True
@ -146,12 +212,12 @@ class LaunchDialog(QDialog):
self.thread_pool.start(api_worker) self.thread_pool.start(api_worker)
def launch(self): def launch(self):
self.progress_info.setText(self.tr("Preparing Rare"))
if not self.args.offline: if not self.args.offline:
self.ui.image_info.setText(self.tr("Downloading Images"))
image_worker = ImageWorker() image_worker = ImageWorker()
image_worker.signals.result.connect(self.handle_api_worker_result) image_worker.signals.result.connect(self.handle_api_worker_result)
image_worker.signals.progress.connect(self.update_image_progbar) image_worker.signals.progress.connect(self.update_progress)
# lk: start the api requests worker after the manifests have been downloaded # lk: start the api requests worker after the manifests have been downloaded
# lk: to avoid force updating the assets twice and causing inconsistencies # lk: to avoid force updating the assets twice and causing inconsistencies
image_worker.signals.finished.connect(self.start_api_requests) image_worker.signals.finished.connect(self.start_api_requests)
@ -206,14 +272,15 @@ class LaunchDialog(QDialog):
if self.api_results: if self.api_results:
self.finish() self.finish()
def update_image_progbar(self, i: int): @pyqtSlot(int, str)
self.ui.image_prog_bar.setValue(i) def update_progress(self, i: int, m: str):
self.ui.progress_bar.setValue(i)
self.progress_info.setText(m)
def finish(self): def finish(self):
self.completed += 1 self.completed += 1
if self.completed == 2: if self.completed == 2:
logger.info("App starting") logger.info("App starting")
self.ui.image_info.setText(self.tr("Starting..."))
ApiResultsSingleton(self.api_results) ApiResultsSingleton(self.api_results)
self.completed += 1 self.completed += 1
self.start_app.emit() self.start_app.emit()

View file

@ -63,8 +63,8 @@ class GameInfo(QWidget, Ui_GameInfo):
self.lbl_grade.setVisible(False) self.lbl_grade.setVisible(False)
self.grade.setVisible(False) self.grade.setVisible(False)
else: else:
self.steam_worker = SteamWorker(self.core) self.steam_worker: SteamWorker = SteamWorker(self.core)
self.steam_worker.signals.rating_signal.connect(self.grade.setText) self.steam_worker.signals.rating.connect(self.grade.setText)
self.steam_worker.setAutoDelete(False) self.steam_worker.setAutoDelete(False)
self.game_actions_stack.setCurrentIndex(0) self.game_actions_stack.setCurrentIndex(0)

View file

@ -135,11 +135,15 @@ class MoveGamePopUp(QWidget):
return True, dir_selected, str() return True, dir_selected, str()
def update_game(self, app_name): def update_game(self, app_name):
igame = self.core.get_installed_game(app_name, False) igame = self.core.get_installed_game(app_name, skip_sync=True)
if igame is None: if igame is None:
return return
self.install_path = igame.install_path self.install_path = igame.install_path
# FIXME: Make edit_func lighter instead of blocking signals
self.move_path_edit.line_edit.blockSignals(True)
self.move_path_edit.setText(igame.install_path) self.move_path_edit.setText(igame.install_path)
# FIXME: Make edit_func lighter instead of blocking signals
self.move_path_edit.line_edit.blockSignals(False)
self.warn_overwriting.setText( self.warn_overwriting.setText(
self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem) self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem)
) )

View file

@ -87,7 +87,7 @@ class UninstalledInfo(QWidget, Ui_GameInfo):
self.install_button.clicked.connect(self.install_game) self.install_button.clicked.connect(self.install_game)
if platform.system() != "Windows": if platform.system() != "Windows":
self.steam_worker = SteamWorker(self.core) self.steam_worker = SteamWorker(self.core)
self.steam_worker.signals.rating_signal.connect(self.grade.setText) self.steam_worker.signals.rating.connect(self.grade.setText)
self.steam_worker.setAutoDelete(False) self.steam_worker.setAutoDelete(False)
else: else:

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import hashlib import hashlib
import json import json
import pickle import pickle
import zlib import zlib
# from concurrent import futures
from logging import getLogger from logging import getLogger
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -30,6 +29,8 @@ from rare.lgndr.core import LegendaryCore
from rare.models.signals import GlobalSignals from rare.models.signals import GlobalSignals
from rare.utils.paths import image_dir, resources_path from rare.utils.paths import image_dir, resources_path
# from requests_futures.sessions import FuturesSession
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -86,8 +87,8 @@ class ImageSize:
class ImageManager(QObject): class ImageManager(QObject):
class Worker(QRunnable): class Worker(QRunnable):
class Signals(QObject): class Signals(QObject):
# str: app_name # object: Game
completed = pyqtSignal(str) completed = pyqtSignal(object)
def __init__(self, func: Callable, updates: List, json_data: Dict, game: Game): def __init__(self, func: Callable, updates: List, json_data: Dict, game: Game):
super(ImageManager.Worker, self).__init__() super(ImageManager.Worker, self).__init__()
@ -101,7 +102,7 @@ class ImageManager(QObject):
def run(self): def run(self):
self.func(self.updates, self.json_data, self.game) self.func(self.updates, self.json_data, self.game)
logger.debug(f" Emitting singal for game {self.game.app_name} - {self.game.app_title}") logger.debug(f" Emitting singal for game {self.game.app_name} - {self.game.app_title}")
self.signals.completed.emit(self.game.app_name) self.signals.completed.emit(self.game)
def __init__(self, signals: GlobalSignals, core: LegendaryCore): def __init__(self, signals: GlobalSignals, core: LegendaryCore):
# lk: the ordering in __img_types matters for the order of fallbacks # lk: the ordering in __img_types matters for the order of fallbacks
@ -179,7 +180,7 @@ class ImageManager(QObject):
return updates, json_data return updates, json_data
def __download(self, updates, json_data, game) -> bool: def __download(self, updates, json_data, game, use_async: bool = False) -> bool:
# Decompress existing image.cache # Decompress existing image.cache
if not self.__img_cache(game.app_name).is_file(): if not self.__img_cache(game.app_name).is_file():
cache_data = dict(zip(self.__img_types, [None] * len(self.__img_types))) cache_data = dict(zip(self.__img_types, [None] * len(self.__img_types)))
@ -193,6 +194,22 @@ class ImageManager(QObject):
if cache_data[image["type"]] is None or json_data[image["type"]] != image["md5"] if cache_data[image["type"]] is None or json_data[image["type"]] != image["md5"]
] ]
# Download
# # lk: Keep this so I don't have to go looking for it again,
# # lk: it might be useful in the future.
# if use_async and len(updates) > 1:
# session = FuturesSession(max_workers=len(self.__img_types))
# image_requests = []
# for image in updates:
# logger.info(f"Downloading {image['type']} for {game.app_title}")
# json_data[image["type"]] = image["md5"]
# payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
# req = session.get(image["url"], params=payload)
# req.image_type = image["type"]
# image_requests.append(req)
# for req in futures.as_completed(image_requests):
# cache_data[req.image_type] = req.result().content
# else:
for image in updates: for image in updates:
logger.info(f"Downloading {image['type']} for {game.app_title}") logger.info(f"Downloading {image['type']} for {game.app_title}")
json_data[image["type"]] = image["md5"] json_data[image["type"]] = image["md5"]
@ -291,18 +308,18 @@ class ImageManager(QObject):
return data return data
def download_image( def download_image(
self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False self, game: Game, load_callback: Callable[[Game], None], priority: int, force: bool = False
) -> None: ) -> None:
updates, json_data = self.__prepare_download(game, force) updates, json_data = self.__prepare_download(game, force)
if not updates: if not updates:
load_callback() load_callback(game)
return return
if updates and game.app_name not in self.__worker_app_names: if updates and game.app_name not in self.__worker_app_names:
image_worker = ImageManager.Worker(self.__download, updates, json_data, game) image_worker = ImageManager.Worker(self.__download, updates, json_data, game)
self.__worker_app_names.append(game.app_name) self.__worker_app_names.append(game.app_name)
image_worker.signals.completed.connect(load_callback) image_worker.signals.completed.connect(load_callback)
image_worker.signals.completed.connect(lambda app_name: self.__worker_app_names.remove(app_name)) image_worker.signals.completed.connect(lambda g: self.__worker_app_names.remove(g.app_name))
self.threadpool.start(image_worker, priority) self.threadpool.start(image_worker, priority)
def download_image_blocking(self, game: Game, force: bool = False) -> None: def download_image_blocking(self, game: Game, force: bool = False) -> None:
@ -310,7 +327,7 @@ class ImageManager(QObject):
if not updates: if not updates:
return return
if updates: if updates:
self.__download(updates, json_data, game) self.__download(updates, json_data, game, use_async=True)
def __get_cover( def __get_cover(
self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool = True self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool = True

View file

@ -1,32 +1,36 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'launch_dialog.ui' # Form implementation generated from reading ui file 'rare/ui/components/dialogs/launch_dialog.ui'
# #
# Created by: PyQt5 UI code generator 5.15.4 # Created by: PyQt5 UI code generator 5.15.7
# #
# WARNING: Any manual changes made to this file will be lost when pyuic5 is # WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing. # run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_LaunchDialog(object): class Ui_LaunchDialog(object):
def setupUi(self, LaunchDialog): def setupUi(self, LaunchDialog):
LaunchDialog.setObjectName("LaunchDialog") LaunchDialog.setObjectName("LaunchDialog")
LaunchDialog.resize(400, 168) LaunchDialog.resize(400, 160)
self.verticalLayout = QtWidgets.QVBoxLayout(LaunchDialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
self.verticalLayout.setObjectName("verticalLayout") sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(LaunchDialog.sizePolicy().hasHeightForWidth())
LaunchDialog.setSizePolicy(sizePolicy)
LaunchDialog.setMinimumSize(QtCore.QSize(400, 160))
LaunchDialog.setMaximumSize(QtCore.QSize(400, 160))
self.launch_dialog_layout = QtWidgets.QVBoxLayout(LaunchDialog)
self.launch_dialog_layout.setObjectName("launch_dialog_layout")
self.title_label = QtWidgets.QLabel(LaunchDialog) self.title_label = QtWidgets.QLabel(LaunchDialog)
self.title_label.setObjectName("title_label") self.title_label.setObjectName("title_label")
self.verticalLayout.addWidget(self.title_label) self.launch_dialog_layout.addWidget(self.title_label)
self.image_prog_bar = QtWidgets.QProgressBar(LaunchDialog) self.progress_bar = QtWidgets.QProgressBar(LaunchDialog)
self.image_prog_bar.setProperty("value", 0) self.progress_bar.setProperty("value", 0)
self.image_prog_bar.setObjectName("image_prog_bar") self.progress_bar.setObjectName("progress_bar")
self.verticalLayout.addWidget(self.image_prog_bar) self.launch_dialog_layout.addWidget(self.progress_bar)
self.image_info = QtWidgets.QLabel(LaunchDialog)
self.image_info.setObjectName("image_info")
self.verticalLayout.addWidget(self.image_info)
self.retranslateUi(LaunchDialog) self.retranslateUi(LaunchDialog)
QtCore.QMetaObject.connectSlotsByName(LaunchDialog) QtCore.QMetaObject.connectSlotsByName(LaunchDialog)
@ -35,7 +39,6 @@ class Ui_LaunchDialog(object):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching Rare")) LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching Rare"))
self.title_label.setText(_translate("LaunchDialog", "<h2>Launching Rare</h2>")) self.title_label.setText(_translate("LaunchDialog", "<h2>Launching Rare</h2>"))
self.image_info.setText(_translate("LaunchDialog", "Downloading images"))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -7,13 +7,31 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>400</width>
<height>168</height> <height>160</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>160</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>160</height>
</size>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Launching Rare</string> <string>Launching Rare</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="launch_dialog_layout">
<item> <item>
<widget class="QLabel" name="title_label"> <widget class="QLabel" name="title_label">
<property name="text"> <property name="text">
@ -22,19 +40,12 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QProgressBar" name="image_prog_bar"> <widget class="QProgressBar" name="progress_bar">
<property name="value"> <property name="value">
<number>0</number> <number>0</number>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="image_info">
<property name="text">
<string>Downloading images</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View file

@ -16,7 +16,7 @@ url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
class SteamWorker(QRunnable): class SteamWorker(QRunnable):
class Signals(QObject): class Signals(QObject):
rating_signal = pyqtSignal(str) rating = pyqtSignal(str)
app_name: str = "" app_name: str = ""
@ -39,7 +39,7 @@ class SteamWorker(QRunnable):
self.app_name = app_name self.app_name = app_name
def run(self) -> None: def run(self) -> None:
self.signals.rating_signal.emit( self.signals.rating.emit(
self.ratings.get(get_rating(self.app_name), self.ratings["fail"]) self.ratings.get(get_rating(self.app_name), self.ratings["fail"])
) )

7
requirements-dev.txt Normal file
View file

@ -0,0 +1,7 @@
build
wheel
black
toml
nuitka
ordered-set
PyQt5-stubs

View file

@ -47,5 +47,5 @@ setuptools.setup(
python_requires=">=3.8", python_requires=">=3.8",
entry_points=dict(console_scripts=["rare=rare.__main__:main"]), entry_points=dict(console_scripts=["rare=rare.__main__:main"]),
install_requires=requirements, install_requires=requirements,
extras_require=optional_reqs extras_require=optional_reqs,
) )