diff --git a/.github/workflows/job_cx-freeze-msi.yml b/.github/workflows/job_cx-freeze-msi.yml index 29226d0cb..c55309ef6 100644 --- a/.github/workflows/job_cx-freeze-msi.yml +++ b/.github/workflows/job_cx-freeze-msi.yml @@ -19,9 +19,9 @@ jobs: python-version: '3.12' check-latest: true architecture: x64 - - name: Install Build Dependencies + - name: Install build dependencies run: pip3 install --upgrade cx_freeze wheel - - name: Install Target Dependencies + - name: Install target dependencies run: | pip3 install -r requirements.txt pip3 install -r requirements-presence.txt @@ -34,4 +34,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: Rare-${{ inputs.version }}.msi - path: Rare.msi \ No newline at end of file + path: Rare.msi + diff --git a/.github/workflows/job_cx-freeze-zip.yml b/.github/workflows/job_cx-freeze-zip.yml index 5bd79b499..de56341db 100644 --- a/.github/workflows/job_cx-freeze-zip.yml +++ b/.github/workflows/job_cx-freeze-zip.yml @@ -20,7 +20,7 @@ jobs: check-latest: true architecture: x64 - name: Install build dependencies - run: pip3 install cx_freeze + run: pip3 install --upgrade cx_freeze wheel - name: Install target dependencies run: | pip3 install -r requirements.txt @@ -37,3 +37,4 @@ jobs: with: name: Rare-portable-windows-${{ inputs.version }}.zip path: Rare.zip + diff --git a/.github/workflows/job_nuitka-linux.yml b/.github/workflows/job_nuitka-linux.yml index 12e112c69..c9acfa7bf 100644 --- a/.github/workflows/job_nuitka-linux.yml +++ b/.github/workflows/job_nuitka-linux.yml @@ -29,6 +29,7 @@ jobs: run: >- python -m nuitka --assume-yes-for-downloads + --clang --lto=no --jobs=4 --static-libpython=no diff --git a/.github/workflows/job_nuitka-win.yml b/.github/workflows/job_nuitka-win.yml index 44b60f2e9..278fb062d 100644 --- a/.github/workflows/job_nuitka-win.yml +++ b/.github/workflows/job_nuitka-win.yml @@ -29,7 +29,7 @@ jobs: run: >- python -m nuitka --assume-yes-for-downloads - --msvc=latest + --mingw64 --lto=no --jobs=4 --static-libpython=no diff --git a/rare/components/tabs/store/api/models/response.py b/rare/components/tabs/store/api/models/response.py index fe789f52d..037698208 100644 --- a/rare/components/tabs/store/api/models/response.py +++ b/rare/components/tabs/store/api/models/response.py @@ -408,12 +408,23 @@ def from_dict(cls: Type["DataModel"], src: Dict[str, Any]) -> "DataModel": @dataclass class ErrorModel: + message: str = None + correlationId: str = None + serviceResponse: str = None unmapped: Dict[str, Any] = field(default_factory=dict) + def __str__(self): + return f"{self.correlationId} - {self.message}" + @classmethod def from_dict(cls: Type["ErrorModel"], src: Dict[str, Any]) -> "ErrorModel": d = src.copy() - return cls(unmapped=d) + return cls( + message=d.pop("message", ""), + correlationId= d.pop("correlationId", ""), + serviceResponse=d.pop("serviceResponse", ""), + unmapped=d + ) @dataclass @@ -429,7 +440,7 @@ def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsM @dataclass class ResponseModel: data: DataModel = None - errors: List[ErrorModel] = None + errors: Tuple[ErrorModel, ...] = None extensions: ExtensionsModel = None unmapped: Dict[str, Any] = field(default_factory=dict) @@ -438,9 +449,6 @@ def from_dict(cls: Type["ResponseModel"], src: Dict[str, Any]) -> "ResponseModel d = src.copy() data = DataModel.from_dict(x) if (x := d.pop("data", {})) else None _errors = d.pop("errors", []) - errors = [] if _errors else None - for item in _errors: - error = ErrorModel.from_dict(item) - errors.append(error) + errors = tuple(map(ErrorModel.from_dict, _errors)) extensions = ExtensionsModel.from_dict(x) if (x := d.pop("extensions", {})) else None return cls(data=data, errors=errors, extensions=extensions, unmapped=d) diff --git a/rare/components/tabs/store/store_api.py b/rare/components/tabs/store/store_api.py index a53eecbd7..7158f9acb 100644 --- a/rare/components/tabs/store/store_api.py +++ b/rare/components/tabs/store/store_api.py @@ -1,5 +1,5 @@ from logging import getLogger -from typing import Callable, Tuple +from typing import Callable, Tuple, Any from PySide6.QtCore import Signal, QObject from PySide6.QtWidgets import QApplication @@ -35,7 +35,6 @@ def __init__(self, token, language: str, country: str, installed): self.language_code: str = language self.country_code: str = country self.locale = f"{self.language_code}-{self.country_code}" - self.locale = "en-US" self.manager = QtRequests(parent=self) self.authed_manager = QtRequests(token=token, parent=self) self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), parent=self) @@ -45,38 +44,34 @@ def __init__(self, token, language: str, country: str, installed): self.browse_active = False self.next_browse_request = tuple(()) - def get_free(self, handle_func: callable): + def get_free(self, callback: callable): url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions" params = { "locale": self.locale, "country": self.country_code, "allowCountries": self.country_code, } - self.manager.get(url, lambda data: self.__handle_free_games(data, handle_func), params=params) + self.manager.get(url, lambda data: self.__handle_free_games(data, callback), params=params) @staticmethod - def __handle_free_games(data, handle_func): + def __handle_free_games(data, callback): try: response = ResponseModel.from_dict(data) - results: Tuple[CatalogOfferModel, ...] = response.data.catalog.searchStore.elements - handle_func(results) - except KeyError as e: - if DEBUG(): - raise e - logger.error("Free games Api request failed") - handle_func(["error", "Key error"]) - return - except Exception as e: + if response.errors: + for error in response.errors: + logger.error(error) + elements = response.data.catalog.searchStore.elements + except (Exception, AttributeError, KeyError) as e: if DEBUG(): raise e - logger.error(f"Free games Api request failed: {e}") - handle_func(["error", e]) - return + elements = False + logger.error("Free games request failed with: %s", e) + callback(elements) - def get_wishlist(self, handle_func): + def get_wishlist(self, callback): self.authed_manager.post( graphql_url, - lambda data: self.__handle_wishlist(data, handle_func), + lambda data: self.__handle_wishlist(data, callback), { "query": wishlist_query, "variables": { @@ -88,26 +83,21 @@ def get_wishlist(self, handle_func): ) @staticmethod - def __handle_wishlist(data, handle_func): + def __handle_wishlist(data, callback): try: response = ResponseModel.from_dict(data) if response.errors: - logger.error(response.errors) - handle_func(response.data.wishlist.wishlistItems.elements) - except KeyError as e: + for error in response.errors: + logger.error(error) + elements = response.data.wishlist.wishlistItems.elements + except (Exception, AttributeError, KeyError) as e: if DEBUG(): raise e - logger.error("Free games API request failed") - handle_func(["error", "Key error"]) - return - except Exception as e: - if DEBUG(): - raise e - logger.error(f"Free games API request failed") - handle_func(["error", e]) - return + elements = False + logger.error("Wishlist request failed with: %s", e) + callback(elements) - def search_game(self, name, handler): + def search_game(self, name, callback): payload = { "query": search_query, "variables": { @@ -125,60 +115,56 @@ def search_game(self, name, handler): }, } - self.manager.post(graphql_url, lambda data: self.__handle_search(data, handler), payload) + self.manager.post(graphql_url, lambda data: self.__handle_search(data, callback), payload) @staticmethod - def __handle_search(data, handler): + def __handle_search(data, callback): try: response = ResponseModel.from_dict(data) - handler(response.data.catalog.searchStore.elements) - except KeyError as e: - if DEBUG(): - raise e - logger.error(str(e)) - handler([]) - except Exception as e: + if response.errors: + for error in response.errors: + logger.error(error) + elements = response.data.catalog.searchStore.elements + except (Exception, AttributeError, KeyError) as e: if DEBUG(): raise e - logger.error(f"Search Api request failed: {e}") - handler([]) - return + elements = False + logger.error("Search request failed with: %s", e) + callback(elements) - def browse_games(self, browse_model: SearchStoreQuery, handle_func): + def browse_games(self, browse_model: SearchStoreQuery, callback): if self.browse_active: - self.next_browse_request = (browse_model, handle_func) + self.next_browse_request = (browse_model, callback) return self.browse_active = True payload = { "query": search_query, "variables": browse_model.to_dict() } - self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, handle_func), payload) + self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, callback), payload) - def __handle_browse_games(self, data, handle_func): + def __handle_browse_games(self, data, callback): self.browse_active = False if data is None: data = {} if not self.next_browse_request: try: response = ResponseModel.from_dict(data) - handle_func(response.data.catalog.searchStore.elements) - except KeyError as e: + if response.errors: + for error in response.errors: + logger.error(error) + elements = response.data.catalog.searchStore.elements + except (Exception, AttributeError, KeyError) as e: if DEBUG(): raise e - logger.error(str(e)) - handle_func([]) - except Exception as e: - if DEBUG(): - raise e - logger.error(f"Browse games Api request failed: {e}") - handle_func([]) - return + elements = False + logger.error("Browse request failed with: %s", e) + callback(elements) else: self.browse_games(*self.next_browse_request) # pylint: disable=E1120 self.next_browse_request = tuple(()) - # def get_game_config_graphql(self, namespace: str, handle_func): + # def get_game_config_graphql(self, namespace: str, callback): # payload = { # "query": config_query, # "variables": { @@ -192,24 +178,24 @@ def __make_graphql_query(self): def __make_api_query(self): pass - def get_game_config_cms(self, slug: str, is_bundle: bool, handle_func): + def get_game_config_cms(self, slug: str, is_bundle: bool, callback): url = "https://store-content.ak.epicgames.com/api" url += f"/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" - self.manager.get(url, lambda data: self.__handle_get_game(data, handle_func)) + self.manager.get(url, lambda data: self.__handle_get_game(data, callback)) @staticmethod - def __handle_get_game(data, handle_func): + def __handle_get_game(data, callback): try: product = DieselProduct.from_dict(data) - handle_func(product) + callback(product) except Exception as e: if DEBUG(): raise e logger.error(str(e)) - # handle_func({}) + # callback({}) # needs a captcha - def add_to_wishlist(self, namespace, offer_id, handle_func: callable): + def add_to_wishlist(self, namespace, offer_id, callback: callable): payload = { "query": wishlist_add_query, "variables": { @@ -219,21 +205,24 @@ def add_to_wishlist(self, namespace, offer_id, handle_func: callable): "locale": self.locale, }, } - self.authed_manager.post(graphql_url, lambda data: self._handle_add_to_wishlist(data, handle_func), payload) + self.authed_manager.post(graphql_url, lambda data: self._handle_add_to_wishlist(data, callback), payload) - def _handle_add_to_wishlist(self, data, handle_func): + def _handle_add_to_wishlist(self, data, callback): try: response = ResponseModel.from_dict(data) - data = response.data.wishlist.addToWishlist - handle_func(data.success) + if response.errors: + for error in response.errors: + logger.error(error) + success = response.data.wishlist.addToWishlist.success except Exception as e: if DEBUG(): raise e - logger.error(str(e)) - handle_func(False) + logger.error("Add to wishlist request failed with: %s", e) + success = False + callback(success) self.update_wishlist.emit() - def remove_from_wishlist(self, namespace, offer_id, handle_func: callable): + def remove_from_wishlist(self, namespace, offer_id, callback: callable): payload = { "query": wishlist_remove_query, "variables": { @@ -242,17 +231,20 @@ def remove_from_wishlist(self, namespace, offer_id, handle_func: callable): "operation": "REMOVE", }, } - self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func), + self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, callback), payload) - def _handle_remove_from_wishlist(self, data, handle_func): + def _handle_remove_from_wishlist(self, data, callback): try: response = ResponseModel.from_dict(data) - data = response.data.wishlist.removeFromWishlist - handle_func(data.success) + if response.errors: + for error in response.errors: + logger.error(error) + success = response.data.wishlist.removeFromWishlist.success except Exception as e: if DEBUG(): raise e - logger.error(str(e)) - handle_func(False) + logger.error("Remove from wishlist request failed with: %s", e) + success = False + callback(success) self.update_wishlist.emit() diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index 7e9f9f4a2..d39abc3f7 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -59,7 +59,7 @@ class WishlistFilter(IntEnum): class WishlistWidget(QWidget, SideTabContents): show_details = Signal(CatalogOfferModel) - update_wishlist_signal = Signal() + update_wishlist = Signal() def __init__(self, api: StoreAPI, parent=None): super(WishlistWidget, self).__init__(parent=parent) @@ -91,7 +91,7 @@ def __init__(self, api: StoreAPI, parent=None): self.ui.order_combo.currentIndexChanged.connect(self.order_wishlist) self.ui.reload_button.setIcon(qta_icon("fa.refresh", color="white")) - self.ui.reload_button.clicked.connect(self.update_wishlist) + self.ui.reload_button.clicked.connect(self.__update_widget) self.ui.reverse_check.stateChanged.connect( lambda: self.order_wishlist(self.ui.order_combo.currentIndex()) @@ -100,10 +100,10 @@ def __init__(self, api: StoreAPI, parent=None): self.setEnabled(False) def showEvent(self, a0: QShowEvent) -> None: - self.update_wishlist() + self.__update_widget() return super().showEvent(a0) - def update_wishlist(self): + def __update_widget(self): self.setEnabled(False) self.api.get_wishlist(self.set_wishlist) @@ -111,13 +111,13 @@ def delete_from_wishlist(self, game: CatalogOfferModel): self.api.remove_from_wishlist( game.namespace, game.id, - lambda success: self.update_wishlist() + lambda success: self.__update_widget() if success else QMessageBox.warning( self, "Error", self.tr("Could not remove game from wishlist") ), ) - self.update_wishlist_signal.emit() + self.update_wishlist.emit() @Slot(int) def filter_wishlist(self, index: int = int(WishlistFilter.NONE)): diff --git a/rare/models/game.py b/rare/models/game.py index a2c35d9f4..6c4e28ba7 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -152,7 +152,7 @@ def __load_metadata_json() -> Dict: metadata = {} file = os.path.join(data_dir(), "game_meta.json") try: - with open(file, "r", encoding="utf-8") as f: + with open(file, "r") as f: metadata = json.load(f) except FileNotFoundError: logger.info("%s does not exist", file) @@ -175,7 +175,7 @@ def __save_metadata(self): metadata: Dict = self.__load_metadata_json() # pylint: disable=unsupported-assignment-operation metadata[self.app_name] = vars(self.metadata) - with open(os.path.join(data_dir(), "game_meta.json"), "w+", encoding="utf-8") as file: + with open(os.path.join(data_dir(), "game_meta.json"), "w+") as file: json.dump(metadata, file, indent=2) def update_game(self): diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py index 8ab0e8468..40db6b212 100644 --- a/rare/shared/image_manager.py +++ b/rare/shared/image_manager.py @@ -237,7 +237,7 @@ def __download(self, updates: List, json_data: Dict, game: Game, use_async: bool json_data["size"] = {"w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()} # write image.json - with open(self.__img_json(game.app_name), "w", encoding="utf-8") as file: + with open(self.__img_json(game.app_name), "w") as file: json.dump(json_data, file) return bool(updates) diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index a9b3a710d..87a430e7c 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -156,7 +156,7 @@ def core(self, init: bool = False) -> LegendaryCore: else: path = os.path.expanduser('~/.config/legendary') logger.info("Creating config in path: %s", config_path) - with open(os.path.join(path, "config.ini"), "w", encoding="utf-8") as config_file: + with open(os.path.join(path, "config.ini"), "w") as config_file: config_file.write("[Legendary]") self.__core = LegendaryCore() diff --git a/rare/shared/wrappers.py b/rare/shared/wrappers.py index d6eaca9ee..d99b40d2b 100644 --- a/rare/shared/wrappers.py +++ b/rare/shared/wrappers.py @@ -19,7 +19,7 @@ def __init__(self): self.__file = os.path.join(config_dir(), "wrappers.json") self.__wrappers_dict = {} try: - with open(self.__file, "r", encoding="utf-8") as f: + with open(self.__file) as f: self.__wrappers_dict = json.load(f) except FileNotFoundError: logger.info("%s does not exist", self.__file) @@ -114,7 +114,7 @@ def __save_wrappers(self): self.__wrappers_dict["wrappers"] = self.__wrappers self.__wrappers_dict["applists"] = self.__applists - with open(os.path.join(self.__file), "w+", encoding="utf-8") as f: + with open(os.path.join(self.__file), "w+") as f: json.dump(self.__wrappers_dict, f, default=lambda o: vars(o), indent=2)