diff --git a/.gitignore b/.gitignore index 99daec5..906fc1b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ build/ dist/ __pycache__ -config.ini \ No newline at end of file +config.ini +file_version_info.txt +lamo-watcher.exe \ No newline at end of file diff --git a/README.md b/README.md index 4a3ca3e..d292e15 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,29 @@ -## Lost Ark Market Launcher 1.0.1 +## Lost Ark Market Launcher 1.2.1.1 This app does the authentication to the Lost Ark Marketplace Online project and looks if the latest version of the launcher is the one currently present. If it finds a new version, it will proceed to download the new version and launch it. If the latest version is already present, it proceeds launching it. -### Dependencies +### App Dependencies - Pyside6 (Qt for Python) `pip install PySide6` - requests `pip install requests` - Simpleaudio `pip install simpleaudio` +### Compilation Dependencies +- PyInstaller `pip install PyInstaller` +- pyinstaller-versionfile `pip install pyinstaller-versionfile` + ### Assets Audio files from [MixKit](https://mixkit.co/) ### Changelog +### 1.2.1.1 +- Update forms +- Update config strategy +- Add file metadata +- Add compile script + ### 1.0.1 - Add extra audio cues - Fix subprocess no console diff --git a/compile.py b/compile.py new file mode 100644 index 0000000..2256efe --- /dev/null +++ b/compile.py @@ -0,0 +1,15 @@ +import PyInstaller.__main__ + +from modules.config import Config + +import pyinstaller_versionfile + +pyinstaller_versionfile.create_versionfile_from_input_file( + output_file="file_version_info.txt", + input_file="metadata.yml", + version=Config().version +) + +PyInstaller.__main__.run([ + 'compile.spec', +]) \ No newline at end of file diff --git a/release.spec b/compile.spec similarity index 93% rename from release.spec rename to compile.spec index 2ad3a08..8e07b7f 100644 --- a/release.spec +++ b/compile.spec @@ -28,7 +28,7 @@ exe = EXE( a.zipfiles, a.datas, [], - name='LostArkMarketLauncher', + name='lamo-launcher', debug=False, bootloader_ignore_signals=False, strip=False, @@ -42,4 +42,5 @@ exe = EXE( codesign_identity=None, entitlements_file=None, icon='assets\\icons\\favicon.ico', + version='file_version_info.txt' ) diff --git a/file_version_info.txt b/file_version_info.txt new file mode 100644 index 0000000..e39bd57 --- /dev/null +++ b/file_version_info.txt @@ -0,0 +1,44 @@ +# UTF-8 +# +# For more details about fixed file info 'ffi' see: +# http://msdn.microsoft.com/en-us/library/ms646997.aspx + +VSVersionInfo( + ffi=FixedFileInfo( + # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) + # Set not needed items to zero 0. Must always contain 4 elements. + filevers=(1,2,1,1), + prodvers=(1,2,1,1), + # Contains a bitmask that specifies the valid bits 'flags'r + mask=0x3f, + # Contains a bitmask that specifies the Boolean attributes of the file. + flags=0x0, + # The operating system for which this file was designed. + # 0x4 - NT and there is no need to change it. + OS=0x40004, + # The general type of file. + # 0x1 - the file is an application. + fileType=0x1, + # The function of the file. + # 0x0 - the function is not defined for this fileType + subtype=0x0, + # Creation date and time stamp. + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'040904B0', + [StringStruct(u'CompanyName', u'Lost Ark Market Online'), + StringStruct(u'FileDescription', u'Lost Ark Market Online Launcher App'), + StringStruct(u'FileVersion', u'1.2.1.1'), + StringStruct(u'InternalName', u'Launcher App'), + StringStruct(u'LegalCopyright', u'© Lost Ark Market Online'), + StringStruct(u'OriginalFilename', u'lamo-launcher.exe'), + StringStruct(u'ProductName', u'Lost Ark Market Online Launcher App'), + StringStruct(u'ProductVersion', u'1.2.1.1')]) + ]), + VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) + ] +) \ No newline at end of file diff --git a/index.py b/index.py index 8c975b5..520a636 100644 --- a/index.py +++ b/index.py @@ -4,31 +4,20 @@ import subprocess from PySide6.QtWidgets import QApplication +from PySide6.QtGui import QIcon from modules.app_version import get_app_version -from modules.config import update_latest_app_version, get_tokens, needs_update, update_current_app_version -from modules.errors import NoTokenError +from modules.config import Config from modules.sound import playDownloadComplete, playError, playSuccess, playUpdateDetected from ui.download.download import LostArkMarketLauncherDownload from ui.login_form.login_form import LostArkMarketLoginForm -from modules.auth import refresh_token - - class LostArkMarketOnlineLauncher(QApplication): - refresh_token = None - id_token = None - uid = None - def __init__(self, *args, **kwargs): QApplication.__init__(self, *args, **kwargs) - try: - self.id_token, self.refresh_token, self.uid = get_tokens() - self.id_token, self.refresh_token, self.uid = refresh_token( - self.refresh_token) + + if(Config().refresh_token): self.check_config() - except NoTokenError: - traceback.print_exc() - playUpdateDetected() + else: self.login_form = LostArkMarketLoginForm() self.login_form.login_success.connect(self.login_success) self.login_form.login_error.connect(self.login_error) @@ -38,17 +27,15 @@ def login_error(self): def login_success(self): playSuccess() - self.login_form.ui.hide() + self.login_form.close() self.check_config() def check_config(self): - self.latest_app_version = get_app_version() - update_latest_app_version(self.latest_app_version) - if needs_update() == True: + Config().check_watcher_version() + if Config().needs_update == True: playUpdateDetected() self.download = LostArkMarketLauncherDownload({ - "url": f'https://github.com/gogodr/LostArk-Market-Watcher/releases/download/{self.latest_app_version}/LostArkMarketWatcher.exe', - "version": self.latest_app_version + "url": f'https://github.com/gogodr/LostArk-Market-Watcher/releases/download/{Config().watcher_version}/{Config().watcher_file}.exe' }) self.download.launch.connect(self.launch_watcher) self.download.finished_download.connect(self.finished_download) @@ -57,16 +44,22 @@ def check_config(self): def finished_download(self): playDownloadComplete() - update_current_app_version(self.latest_app_version) + if(Config().run_after_download): + self.download.close() + self.launch_watcher() def launch_watcher(self): playSuccess() si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.call("LostArkMarketWatcher.exe") + subprocess.call(f"{Config().watcher_file}.exe") sys.exit() if __name__ == "__main__": + Config() app = LostArkMarketOnlineLauncher([]) + icon = QIcon(os.path.abspath(os.path.join(os.path.dirname(__file__), + "assets/icons/favicon.png"))) + app.setWindowIcon(icon) sys.exit(app.exec()) diff --git a/metadata.yml b/metadata.yml new file mode 100644 index 0000000..5240137 --- /dev/null +++ b/metadata.yml @@ -0,0 +1,7 @@ +Version: 1.1.0 +CompanyName: Lost Ark Market Online +FileDescription: Lost Ark Market Online Launcher App +InternalName: Launcher App +LegalCopyright: © Lost Ark Market Online +OriginalFilename: lamo-launcher.exe +ProductName: Lost Ark Market Online Launcher App \ No newline at end of file diff --git a/modules/auth.py b/modules/auth.py index a853150..8dd58c0 100644 --- a/modules/auth.py +++ b/modules/auth.py @@ -1,5 +1,5 @@ import requests -from modules.config import update_token +from modules.config import Config from modules.errors import LoginError, NoTokenError api_key = 'AIzaSyBMTA0A2fy-dh4jWidbAtYseC7ZZssnsmk' @@ -14,12 +14,11 @@ def login(email, password): tokens = res.json() if 'error' in tokens: raise LoginError() - update_token({ + Config().update_token({ "id_token": tokens['idToken'], "refresh_token": tokens['refreshToken'], "uid": tokens['localId'], }) - return tokens['idToken'], tokens['refreshToken'], tokens['localId'] def refresh_token(token): @@ -30,9 +29,8 @@ def refresh_token(token): tokens = res.json() if 'error' in tokens: raise NoTokenError() - update_token({ - "id_token": tokens['id_token'], - "refresh_token": tokens['refresh_token'], - "uid": tokens['user_id'], + Config().update_token({ + "id_token": tokens['idToken'], + "refresh_token": tokens['refreshToken'], + "uid": tokens['localId'], }) - return tokens['id_token'], tokens['refresh_token'], tokens['user_id'] diff --git a/modules/common/singleton.py b/modules/common/singleton.py new file mode 100644 index 0000000..6ca871d --- /dev/null +++ b/modules/common/singleton.py @@ -0,0 +1,12 @@ +import threading + +class Singleton(type): + _instances = {} + _lock = threading.Lock() + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + with cls._lock: + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] \ No newline at end of file diff --git a/modules/config.py b/modules/config.py index 396827a..e745eb7 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,61 +1,97 @@ import configparser -from modules.errors import NoTokenError - - -def update_token(token): - config = configparser.ConfigParser() - config.read("config.ini") - - if config.has_section("Token") == False: - config.add_section("Token") - config.set("Token", "refresh_token", token["refresh_token"]) - config.set("Token", "id_token", token["id_token"]) - config.set("Token", "uid", token["uid"]) - else: - if 'refresh_token' in token: - config.set("Token", "refresh_token", token["refresh_token"]) - if 'id_token' in token: - config.set("Token", "id_token", token["id_token"]) - if 'uid' in token: - config.set("Token", "uid", token["uid"]) - - with open("config.ini", "w") as configfile: - config.write(configfile) - - -def get_tokens(): - config = configparser.ConfigParser() - config.read("config.ini") - if config.has_section("Token") == False: - raise NoTokenError() - else: - return config.get("Token", "id_token"), config.get("Token", "refresh_token"), config.get("Token", "uid") - - -def update_latest_app_version(app_version): - config = configparser.ConfigParser() - config.read("config.ini") - - if config.has_section("Launcher") == False: - config.add_section("Launcher") - config.set("Launcher", "current_app_version", 'None') - config.set("Launcher", "latest_app_version", app_version) - else: - config.set("Launcher", "latest_app_version", app_version) - - with open("config.ini", "w") as configfile: - config.write(configfile) - - -def update_current_app_version(app_version): - config = configparser.ConfigParser() - config.read("config.ini") - config.set("Launcher", "current_app_version", app_version) - with open("config.ini", "w") as configfile: - config.write(configfile) - - -def needs_update(): - config = configparser.ConfigParser() - config.read("config.ini") - return config.get("Launcher", "current_app_version") != config.get("Launcher", "latest_app_version") +import os +import pefile +import requests +from packaging import version +from modules.common.singleton import Singleton + + +class Config(metaclass=Singleton): + version = "1.2.1.1" + debug = False + id_token: str = None + refresh_token: str = None + uid: str = None + play_audio: bool = True + save_log: bool = True + run_after_download: bool = True + needs_update = False + watcher_version: str = None + watcher_file: str = None + + def __init__(self) -> None: + self._config = configparser.ConfigParser() + self._config.read("config.ini") + self.load_config() + + def load_config(self): + if self._config.has_section("Token"): + self.id_token = self._config.get("Token", "id_token") + self.refresh_token = self._config.get("Token", "refresh_token") + self.uid = self._config.get("Token", "uid") + + if self._config.has_section("Launcher"): + self.run_after_download = self._config.get( + "Launcher", "run_after_download") == 'True' + self.debug = self._config.get( + "Launcher", "debug") == 'True' + + def update_config_file(self): + if self._config.has_section("Token") == False: + self._config.add_section("Token") + + self._config.set("Token", "refresh_token", self.refresh_token) + self._config.set("Token", "id_token", self.id_token) + self._config.set("Token", "uid", self.uid) + + if self._config.has_section("Launcher") == False: + self._config.add_section("Launcher") + + self.set_or_remove_config_option( + "Launcher", "run_after_download", self.run_after_download + ) + self.set_or_remove_config_option( + "Launcher", "debug", self.debug + ) + with open("config.ini", "w") as configfile: + self._config.write(configfile) + + def update_token(self, token): + self.id_token = token["id_token"] + self.refresh_token = token["refresh_token"] + self.uid = token["uid"] + self.update_config_file() + + def set_or_remove_config_option(self, section, option, value): + if value is None: + self._config.remove_option(section, option) + else: + self._config.set(section, option, str(value)) + + def check_watcher_version(self): + res = requests.get( + f'https://firestore.googleapis.com/v1/projects/lostarkmarket-79ddf/databases/(default)/documents/app-info/market-watcher') + res = res.json() + self.watcher_file = res["fields"]["file"]["stringValue"] + self.watcher_version = res["fields"]["version"]["stringValue"] + + if os.path.isfile(f"{self.watcher_file}.exe") == False: + self.needs_update = True + else: + pe = pefile.PE(f"{self.watcher_file}.exe") + + def LOWORD(dword): + return dword & 0x0000ffff + + def HIWORD(dword): + return dword >> 16 + + ms = pe.VS_FIXEDFILEINFO[0].ProductVersionMS + ls = pe.VS_FIXEDFILEINFO[0].ProductVersionLS + + curr_version = version.parse( + f"{HIWORD(ms)}.{LOWORD(ms)}.{HIWORD(ls)}.{LOWORD(ls)}") + latest_version = version.parse(self.watcher_version) + + if latest_version > curr_version: + self.needs_update = True diff --git a/ui/common/uiloader.py b/ui/common/uiloader.py new file mode 100644 index 0000000..4eea9ee --- /dev/null +++ b/ui/common/uiloader.py @@ -0,0 +1,16 @@ +from PySide6.QtUiTools import QUiLoader + +class UiLoader(QUiLoader): + def __init__(self, base_instance): + QUiLoader.__init__(self, base_instance) + self.base_instance = base_instance + + def createWidget(self, class_name, parent=None, name=''): + if parent is None and self.base_instance: + return self.base_instance + else: + # create a new widget for child widgets + widget = QUiLoader.createWidget(self, class_name, parent, name) + if self.base_instance: + setattr(self.base_instance, name, widget) + return widget \ No newline at end of file diff --git a/ui/download/download.py b/ui/download/download.py index 751e56c..7c4e091 100644 --- a/ui/download/download.py +++ b/ui/download/download.py @@ -3,10 +3,11 @@ from PySide6.QtWidgets import QMainWindow from PySide6.QtCore import QFile, Qt, Signal -from PySide6.QtUiTools import QUiLoader +from modules.config import Config from ui.common.downloadprogressbar import DownloadProgressBar from ui.common.draggablewindow import DraggableWindow +from ui.common.uiloader import UiLoader import ui.download.resources @@ -16,44 +17,42 @@ class LostArkMarketLauncherDownload(QMainWindow): def __init__(self, data): super(LostArkMarketLauncherDownload, self).__init__() - self.data = data self.load_ui() + self.setWindowTitle("Download - Lost Ark Market Online") + self.data = data + self.show() def load_ui(self): - loader = QUiLoader() + loader = UiLoader(self) loader.registerCustomWidget(DownloadProgressBar) loader.registerCustomWidget(DraggableWindow) ui_file = QFile(os.path.join(os.path.dirname( __file__), "../../assets/ui/download.ui")) ui_file.open(QFile.ReadOnly) - self.ui = loader.load(ui_file, self) - self.ui.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) - self.ui.lblTitle.setText( - f'New version of the Lost Ark Market Watcher Found: v{self.data["version"]}') - self.ui.btnClose.clicked.connect(self.close) - self.ui.btnSkip.clicked.connect(self.close) - self.ui.btnDownload.clicked.connect(self.download) - self.ui.pbDownload.finished.connect(self.download_done) - - self.ui.show() + widget = loader.load(ui_file) + widget.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + widget.lblTitle.setText( + f'New version of the Lost Ark Market Watcher Found: v{Config().watcher_version}') + widget.btnClose.clicked.connect(self.close) + widget.btnSkip.clicked.connect(self.close) + widget.btnDownload.clicked.connect(self.download) + widget.pbDownload.finished.connect(self.download_done) ui_file.close() - - def close(self): - os._exit(1) + return widget def download(self): - self.ui.btnDownload.setEnabled(False) - self.ui.btnSkip.setEnabled(False) - self.ui.pbDownload.download_file( - self.data['url'], 'LostArkMarketWatcher.exe') + self.btnDownload.setEnabled(False) + self.btnSkip.setEnabled(False) + self.pbDownload.download_file( + self.data['url'], f'{Config().watcher_file}.exe') def download_done(self): - self.ui.btnDownload.clicked.disconnect(self.download) - self.ui.btnDownload.setText('Launch') - self.ui.btnDownload.setEnabled(True) - self.ui.btnDownload.clicked.connect(self.launch_app) + self.btnDownload.clicked.disconnect(self.download) + self.btnDownload.setText('Launch') + self.btnDownload.setEnabled(True) + self.btnDownload.clicked.connect(self.launch_app) self.finished_download.emit() def launch_app(self): - self.ui.close() self.launch.emit() + self.close() diff --git a/ui/login_form/login_form.py b/ui/login_form/login_form.py index c246dc6..718c086 100644 --- a/ui/login_form/login_form.py +++ b/ui/login_form/login_form.py @@ -2,9 +2,9 @@ import traceback from pathlib import Path from PySide6.QtWidgets import QMainWindow, QLineEdit -from PySide6.QtUiTools import QUiLoader from PySide6.QtCore import QFile, Qt, Signal from ui.common.draggablewindow import DraggableWindow +from ui.common.uiloader import UiLoader import ui.login_form.resources from modules.auth import login @@ -16,34 +16,30 @@ class LostArkMarketLoginForm(QMainWindow): login_error = Signal() def __init__(self): - print('Init Login Form') super(LostArkMarketLoginForm, self).__init__() self.load_ui() + self.setWindowTitle("Login - Lost Ark Market Online") + self.show() def load_ui(self): - print('Login Form: Load UI') - loader = QUiLoader() + loader = UiLoader(self) ui_file = QFile(os.path.join(os.path.dirname( __file__), "../../assets/ui/login_form.ui")) ui_file.open(QFile.ReadOnly) loader.registerCustomWidget(DraggableWindow) - self.ui = loader.load(ui_file, self) - self.ui.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) - self.ui.btnLogin.clicked.connect(self.login) - self.ui.btnClose.clicked.connect(self.close) - self.ui.txtPassword.setEchoMode(QLineEdit.Password) - self.ui.show() + widget = loader.load(ui_file) + widget.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + widget.btnLogin.clicked.connect(self.login) + widget.btnClose.clicked.connect(self.close) + widget.txtPassword.setEchoMode(QLineEdit.Password) ui_file.close() def login(self): - self.ui.btnLogin.setEnabled(False) + self.btnLogin.setEnabled(False) try: - login(self.ui.txtEmail.text(), self.ui.txtPassword.text()) + login(self.txtEmail.text(), self.txtPassword.text()) self.login_success.emit() except: traceback.print_exc() - self.ui.btnLogin.setEnabled(True) + self.btnLogin.setEnabled(True) self.login_error.emit() - - def close(self): - os._exit(1)