1
0
Fork 0
mirror of synced 2024-06-02 02:34:40 +12:00

Store: Exploratory changes for GraphQL API

This commit is contained in:
loathingKernel 2023-04-10 14:03:45 +03:00
parent 8edaa1a34c
commit b43bd1081a
No known key found for this signature in database
GPG key ID: CE0C72D0B53821FD
27 changed files with 2234 additions and 854 deletions

View file

@ -6,6 +6,7 @@ from rare.widgets.side_tab import SideTabWidget
from .game_info import ShopGameInfo
from .search_results import SearchResults
from .shop_api_core import ShopApiCore
from .api.models.response import CatalogOfferModel
from .shop_widget import ShopWidget
from .wishlist import WishlistWidget, Wishlist
@ -61,7 +62,7 @@ class StoreTab(SideTabWidget):
self.shop.load()
self.wishlist.update_wishlist()
def show_game(self, data):
def show_game(self, data: CatalogOfferModel):
self.previous_index = self.currentIndex()
self.info.update_game(data)
self.setCurrentIndex(self.info_index)

View file

@ -35,4 +35,4 @@ if __name__ == "__main__":
window.setWindowTitle(f"{app.applicationName()} - Store")
window.resize(QSize(1280, 800))
window.show()
app.exec()
app.exec()

View file

@ -0,0 +1,568 @@
FEED_QUERY = '''
query feedQuery(
$locale: String!
$countryCode: String
$offset: Int
$postsPerPage: Int
$category: String
) {
TransientStream {
myTransientFeed(countryCode: $countryCode, locale: $locale) {
id
activity {
... on LinkAccountActivity {
type
created_at
platforms
}
... on SuggestedFriendsActivity {
type
created_at
platform
suggestions {
epicId
epicDisplayName
platformFullName
platformAvatar
}
}
... on IncomingInvitesActivity {
type
created_at
invites {
epicId
epicDisplayName
}
}
... on RecentPlayersActivity {
type
created_at
players {
epicId
epicDisplayName
playedGameName
}
}
}
}
}
Blog {
dieselBlogPosts: getPosts(
locale: $locale
offset: $offset
postsPerPage: $postsPerPage
category: $category
) {
blogList {
_id
author
category
content
urlPattern
slug
sticky
title
date
image
shareImage
trendingImage
url
featured
link
externalLink
}
}
}
}
'''
REVIEWS_QUERY = '''
query productReviewsQuery($sku: String!) {
OpenCritic {
productReviews(sku: $sku) {
id
name
openCriticScore
reviewCount
percentRecommended
openCriticUrl
award
topReviews {
publishedDate
externalUrl
snippet
language
score
author
ScoreFormat {
id
description
}
OutletId
outletName
displayScore
}
}
}
}
'''
MEDIA_QUERY = '''
query fetchMediaRef($mediaRefId: String!) {
Media {
getMediaRef(mediaRefId: $mediaRefId) {
accountId
outputs {
duration
url
width
height
key
contentType
}
namespace
}
}
}
'''
ADDONS_QUERY = '''
query getAddonsByNamespace(
$categories: String!
$count: Int!
$country: String!
$locale: String!
$namespace: String!
$sortBy: String!
$sortDir: String!
) {
Catalog {
catalogOffers(
namespace: $namespace
locale: $locale
params: {
category: $categories
count: $count
country: $country
sortBy: $sortBy
sortDir: $sortDir
}
) {
elements {
countriesBlacklist
customAttributes {
key
value
}
description
developer
effectiveDate
id
isFeatured
keyImages {
type
url
}
lastModifiedDate
longDescription
namespace
offerType
productSlug
releaseDate
status
technicalDetails
title
urlSlug
}
}
}
}
'''
CATALOG_QUERY = '''
query catalogQuery(
$category: String
$count: Int
$country: String!
$keywords: String
$locale: String
$namespace: String!
$sortBy: String
$sortDir: String
$start: Int
$tag: String
) {
Catalog {
catalogOffers(
namespace: $namespace
locale: $locale
params: {
count: $count
country: $country
category: $category
keywords: $keywords
sortBy: $sortBy
sortDir: $sortDir
start: $start
tag: $tag
}
) {
elements {
isFeatured
collectionOfferIds
title
id
namespace
description
keyImages {
type
url
}
seller {
id
name
}
productSlug
urlSlug
items {
id
namespace
}
customAttributes {
key
value
}
categories {
path
}
price(country: $country) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
}
lineOffers {
appliedRules {
id
endDate
}
}
}
linkedOfferId
linkedOffer {
effectiveDate
customAttributes {
key
value
}
}
}
paging {
count
total
}
}
}
}
'''
CATALOG_TAGS_QUERY = '''
query catalogTags($namespace: String!) {
Catalog {
tags(namespace: $namespace, start: 0, count: 999) {
elements {
aliases
id
name
referenceCount
status
}
}
}
}
'''
PREREQUISITES_QUERY = '''
query fetchPrerequisites($offerParams: [OfferParams]) {
Launcher {
prerequisites(offerParams: $offerParams) {
namespace
offerId
missingPrerequisiteItems
satisfiesPrerequisites
}
}
}
'''
PROMOTIONS_QUERY = '''
query promotionsQuery(
$namespace: String!
$country: String!
$locale: String!
) {
Catalog {
catalogOffers(
namespace: $namespace
locale: $locale
params: {
category: "freegames"
country: $country
sortBy: "effectiveDate"
sortDir: "asc"
}
) {
elements {
title
description
id
namespace
categories {
path
}
linkedOfferNs
linkedOfferId
keyImages {
type
url
}
productSlug
promotions {
promotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
upcomingPromotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
}
}
}
}
}
'''
OFFERS_QUERY = '''
query catalogQuery(
$productNamespace: String!
$offerId: String!
$locale: String
$country: String!
$includeSubItems: Boolean!
) {
Catalog {
catalogOffer(namespace: $productNamespace, id: $offerId, locale: $locale) {
title
id
namespace
description
effectiveDate
expiryDate
isCodeRedemptionOnly
keyImages {
type
url
}
seller {
id
name
}
productSlug
urlSlug
url
tags {
id
}
items {
id
namespace
}
customAttributes {
key
value
}
categories {
path
}
price(country: $country) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
currencyCode
currencyInfo {
decimals
}
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
}
lineOffers {
appliedRules {
id
endDate
discountSetting {
discountType
}
}
}
}
}
offerSubItems(namespace: $productNamespace, id: $offerId)
@include(if: $includeSubItems) {
namespace
id
releaseInfo {
appId
platform
}
}
}
}
'''
SEARCH_STORE_QUERY = '''
query searchStoreQuery(
$allowCountries: String
$category: String
$count: Int
$country: String!
$keywords: String
$locale: String
$namespace: String
$itemNs: String
$sortBy: String
$sortDir: String
$start: Int
$tag: String
$releaseDate: String
$withPrice: Boolean = false
$withPromotions: Boolean = false
) {
Catalog {
searchStore(
allowCountries: $allowCountries
category: $category
count: $count
country: $country
keywords: $keywords
locale: $locale
namespace: $namespace
itemNs: $itemNs
sortBy: $sortBy
sortDir: $sortDir
releaseDate: $releaseDate
start: $start
tag: $tag
) {
elements {
title
id
namespace
description
effectiveDate
keyImages {
type
url
}
seller {
id
name
}
productSlug
urlSlug
url
tags {
id
}
items {
id
namespace
}
customAttributes {
key
value
}
categories {
path
}
price(country: $country) @include(if: $withPrice) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
currencyCode
currencyInfo {
decimals
}
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
}
lineOffers {
appliedRules {
id
endDate
discountSetting {
discountType
}
}
}
}
promotions(category: $category) @include(if: $withPromotions) {
promotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
upcomingPromotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
}
}
paging {
count
total
}
}
}
}
'''

View file

@ -0,0 +1,29 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTreeView, QDialog, QVBoxLayout
from utils.json_formatter import QJsonModel
class DebugView(QTreeView):
def __init__(self, data, parent=None):
super(DebugView, self).__init__(parent=parent)
self.setColumnWidth(0, 300)
self.setWordWrap(True)
self.model = QJsonModel(self)
self.setModel(self.model)
self.setContextMenuPolicy(Qt.ActionsContextMenu)
try:
self.model.load(data)
except Exception as e:
pass
self.resizeColumnToContents(0)
class DebugDialog(QDialog):
def __init__(self, data, parent=None):
super().__init__(parent=parent)
self.resize(800, 600)
layout = QVBoxLayout(self)
view = DebugView(data, self)
layout.addWidget(view)

View file

@ -0,0 +1,15 @@
{
"name": "EGS GraphQL Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Default GraphQL Endpoint": {
"url": "http://localhost:8080/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": false
}
}
}
}

View file

@ -0,0 +1,41 @@
scalar Date
type Currency {
decimals: Int
symbol: String
}
type FormattedPrice {
originalPrice: String
discountPrice: String
intermediatePrice: String
}
type TotalPrice {
discountPrice: Int
originalPrice: Int
voucherDiscount: Int
discount: Int
currencyCode: String
currencyInfo: Currency
fmtPrice(locale: String): FormattedPrice
}
type DiscountSetting {
discountType: String
}
type AppliedRuled {
id: ID
endDate: Date
discountSetting: DiscountSetting
}
type LineOfferRes {
appliedRules: [AppliedRuled]
}
type GetPriceRes {
totalPrice: TotalPrice
lineOffers: [LineOfferRes]
}

View file

@ -0,0 +1,164 @@
import logging
from dataclasses import dataclass, field
from typing import List, Dict, Any, Type, Optional
logger = logging.getLogger("DieselModels")
# lk: Typing overloads for unimplemented types
DieselSocialLinks = Dict
@dataclass
class DieselSystemDetailItem:
p_type: Optional[str] = None
minimum: Optional[str] = None
recommended: Optional[str] = None
title: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem":
d = src.copy()
tmp = cls(
p_type=d.pop("_type", ""),
minimum=d.pop("minimum", ""),
recommended=d.pop("recommended", ""),
title=d.pop("title", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselSystemDetail:
p_type: Optional[str] = None
details: Optional[List[DieselSystemDetailItem]] = None
system_type: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail":
d = src.copy()
_details = d.pop("details", [])
details = [] if _details else None
for item in _details:
detail = DieselSystemDetailItem.from_dict(item)
details.append(detail)
tmp = cls(
p_type=d.pop("_type", ""),
details=details,
system_type=d.pop("systemType", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselSystemDetails:
p_type: Optional[str] = None
languages: Optional[List[str]] = None
rating: Optional[Dict] = None
systems: Optional[List[DieselSystemDetail]] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails":
d = src.copy()
_systems = d.pop("systems", [])
systems = [] if _systems else None
for item in _systems:
system = DieselSystemDetail.from_dict(item)
systems.append(system)
tmp = cls(
p_type=d.pop("_type", ""),
languages=d.pop("languages", []),
rating=d.pop("rating", {}),
systems=systems,
)
tmp.unmapped = d
return tmp
@dataclass
class DieselProductAbout:
p_type: Optional[str] = None
desciption: Optional[str] = None
developer_attribution: Optional[str] = None
publisher_attribution: Optional[str] = None
short_description: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout":
d = src.copy()
tmp = cls(
p_type=d.pop("_type", ""),
desciption=d.pop("description", ""),
developer_attribution=d.pop("developerAttribution", ""),
publisher_attribution=d.pop("publisherAttribution", ""),
short_description=d.pop("shortDescription", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselProductDetail:
p_type: Optional[str] = None
about: Optional[DieselProductAbout] = None
requirements: Optional[DieselSystemDetails] = None
social_links: Optional[DieselSocialLinks] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail":
d = src.copy()
about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None
requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None
tmp = cls(
p_type=d.pop("_type", ""),
about=about,
requirements=requirements,
social_links=d.pop("socialLinks", {}),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselProduct:
p_id: Optional[str] = None
p_images_: Optional[List[str]] = None
p_locale: Optional[str] = None
p_slug: Optional[str] = None
p_title: Optional[str] = None
p_url_pattern: Optional[str] = None
namespace: Optional[str] = None
pages: Optional[List["DieselProduct"]] = None
data: Optional[DieselProductDetail] = None
product_name: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct":
d = src.copy()
_pages = d.pop("pages", [])
pages = [] if _pages else None
for item in _pages:
page = DieselProduct.from_dict(item)
pages.append(page)
data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None
tmp = cls(
p_id=d.pop("_id", ""),
p_images_=d.pop("_images_", []),
p_locale=d.pop("_locale", ""),
p_slug=d.pop("_slug", ""),
p_title=d.pop("_title", ""),
p_url_pattern=d.pop("_urlPattern", ""),
namespace=d.pop("namespace", ""),
pages=pages,
data=data,
product_name=d.pop("productName", ""),
)
tmp.unmapped = d
return tmp

View file

@ -0,0 +1,80 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import List
@dataclass
class SearchDateRange:
start_date: datetime = datetime(year=1990, month=1, day=1, tzinfo=timezone.utc)
end_date: datetime = datetime.utcnow()
def __str__(self):
def fmt_date(date: datetime) -> str:
# lk: The formatting accepted by the GraphQL API is either '%Y-%m-%dT%H:%M:%S.000Z' or '%Y-%m-%dT'
return datetime.strftime(date, '%Y-%m-%dT%H:%M:%S.000Z')
return f"[{fmt_date(self.start_date)},{fmt_date(self.end_date)}]"
@dataclass
class SearchStoreQuery:
country: str = "US"
category: str = "games/edition/base|bundles/games|editors|software/edition/base"
count: int = 30
keywords: str = ""
language: str = "en"
namespace: str = ""
with_mapping: bool = True
item_ns: str = ""
sort_by: str = "releaseDate"
sort_dir: str = "DESC"
start: int = 0
tag: List[str] = ""
release_date: SearchDateRange = field(default_factory=SearchDateRange)
with_price: bool = True
with_promotions: bool = True
price_range: str = ""
free_game: bool = None
on_sale: bool = None
effective_date: SearchDateRange = field(default_factory=SearchDateRange)
def __post_init__(self):
self.locale = f"{self.language}-{self.country}"
def to_dict(self):
payload = {
"allowCountries": self.country,
"category": self.category,
"count": self.count,
"country": self.country,
"keywords": self.keywords,
"locale": self.locale,
"namespace": self.namespace,
"withMapping": self.with_mapping,
"itemNs": self.item_ns,
"sortBy": self.sort_by,
"sortDir": self.sort_dir,
"start": self.start,
"tag": self.tag,
"releaseDate": str(self.release_date),
"withPrice": self.with_price,
"withPromotions": self.with_promotions,
"priceRange": self.price_range,
"freeGame": self.free_game,
"onSale": self.on_sale,
"effectiveDate": str(self.effective_date),
}
# payload.pop("withPromotions")
payload.pop("onSale")
if self.price_range == "free":
payload["freeGame"] = True
payload.pop("priceRange")
elif self.price_range.startswith("<price>"):
payload["priceRange"] = self.price_range.replace("<price>", "")
if self.on_sale:
payload["onSale"] = True
if self.price_range:
payload["effectiveDate"] = self.effective_date
else:
payload.pop("priceRange")
return payload

View file

@ -0,0 +1,589 @@
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import List, Dict, Any, Type, Optional
logger = logging.getLogger("StoreApiModels")
# lk: Typing overloads for unimplemented types
DieselSocialLinks = Dict
CatalogNamespaceModel = Dict
CategoryModel = Dict
CustomAttributeModel = Dict
ItemModel = Dict
SellerModel = Dict
OfferMappingModel = Dict
TagModel = Dict
PromotionsModel = Dict
def parse_date(date: str):
return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc)
@dataclass
class DieselSystemDetailItem:
p_type: Optional[str] = None
minimum: Optional[str] = None
recommended: Optional[str] = None
title: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem":
d = src.copy()
tmp = cls(
p_type=d.pop("_type", ""),
minimum=d.pop("minimum", ""),
recommended=d.pop("recommended", ""),
title=d.pop("title", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselSystemDetail:
p_type: Optional[str] = None
details: Optional[List[DieselSystemDetailItem]] = None
system_type: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail":
d = src.copy()
_details = d.pop("details", [])
details = [] if _details else None
for item in _details:
detail = DieselSystemDetailItem.from_dict(item)
details.append(detail)
tmp = cls(
p_type=d.pop("_type", ""),
details=details,
system_type=d.pop("systemType", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselSystemDetails:
p_type: Optional[str] = None
languages: Optional[List[str]] = None
rating: Optional[Dict] = None
systems: Optional[List[DieselSystemDetail]] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails":
d = src.copy()
_systems = d.pop("systems", [])
systems = [] if _systems else None
for item in _systems:
system = DieselSystemDetail.from_dict(item)
systems.append(system)
tmp = cls(
p_type=d.pop("_type", ""),
languages=d.pop("languages", []),
rating=d.pop("rating", {}),
systems=systems,
)
tmp.unmapped = d
return tmp
@dataclass
class DieselProductAbout:
p_type: Optional[str] = None
desciption: Optional[str] = None
developer_attribution: Optional[str] = None
publisher_attribution: Optional[str] = None
short_description: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout":
d = src.copy()
tmp = cls(
p_type=d.pop("_type", ""),
desciption=d.pop("description", ""),
developer_attribution=d.pop("developerAttribution", ""),
publisher_attribution=d.pop("publisherAttribution", ""),
short_description=d.pop("shortDescription", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselProductDetail:
p_type: Optional[str] = None
about: Optional[DieselProductAbout] = None
requirements: Optional[DieselSystemDetails] = None
social_links: Optional[DieselSocialLinks] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail":
d = src.copy()
about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None
requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None
tmp = cls(
p_type=d.pop("_type", ""),
about=about,
requirements=requirements,
social_links=d.pop("socialLinks", {}),
)
tmp.unmapped = d
return tmp
@dataclass
class DieselProduct:
p_id: Optional[str] = None
p_images_: Optional[List[str]] = None
p_locale: Optional[str] = None
p_slug: Optional[str] = None
p_title: Optional[str] = None
p_url_pattern: Optional[str] = None
namespace: Optional[str] = None
pages: Optional[List["DieselProduct"]] = None
data: Optional[DieselProductDetail] = None
product_name: Optional[str] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct":
d = src.copy()
_pages = d.pop("pages", [])
pages = [] if _pages else None
for item in _pages:
page = DieselProduct.from_dict(item)
pages.append(page)
data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None
tmp = cls(
p_id=d.pop("_id", ""),
p_images_=d.pop("_images_", []),
p_locale=d.pop("_locale", ""),
p_slug=d.pop("_slug", ""),
p_title=d.pop("_title", ""),
p_url_pattern=d.pop("_urlPattern", ""),
namespace=d.pop("namespace", ""),
pages=pages,
data=data,
product_name=d.pop("productName", ""),
)
tmp.unmapped = d
return tmp
@dataclass
class ImageUrlModel:
type: Optional[str] = None
url: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
tmp: Dict[str, Any] = {}
tmp.update({})
if self.type is not None:
tmp["type"] = self.type
if self.url is not None:
tmp["url"] = self.url
return tmp
@classmethod
def from_dict(cls: Type["ImageUrlModel"], src: Dict[str, Any]) -> "ImageUrlModel":
d = src.copy()
type = d.pop("type", None)
url = d.pop("url", None)
tmp = cls(
type=type,
url=url,
)
return tmp
@dataclass
class KeyImagesModel:
key_images: Optional[List[ImageUrlModel]] = None
def __getitem__(self, item):
return self.key_images[item]
def __bool__(self):
return bool(self.key_images)
def to_list(self) -> List[Dict[str, Any]]:
items: Optional[List[Dict[str, Any]]] = None
if self.key_images is not None:
items = []
for image_url in self.key_images:
item = image_url.to_dict()
items.append(item)
return items
@classmethod
def from_list(cls: Type["KeyImagesModel"], src: List[Dict]):
d = src.copy()
key_images = []
for item in d:
image_url = ImageUrlModel.from_dict(item)
key_images.append(image_url)
tmp = cls(key_images)
return tmp
def available_tall(self) -> List[ImageUrlModel]:
tall_types = [
"DieselStoreFrontTall",
"OfferImageTall",
"Thumbnail",
"ProductLogo",
"DieselGameBoxLogo",
]
tall_images = filter(lambda img: img.type in tall_types, self.key_images)
tall_images = sorted(tall_images, key=lambda x: tall_types.index(x.type))
return tall_images
def available_wide(self) -> List[ImageUrlModel]:
wide_types = ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]
wide_images = filter(lambda img: img.type in wide_types, self.key_images)
wide_images = sorted(wide_images, key=lambda x: wide_types.index(x.type))
return wide_images
def for_dimensions(self, w: int, h: int) -> ImageUrlModel:
try:
if w > h:
model = self.available_wide()[0]
else:
model = self.available_tall()[0]
_ = model.url
except Exception as e:
logger.error(e)
logger.error(self.to_list())
else:
return model
TotalPriceModel = Dict
FmtPriceModel = Dict
LineOffersModel = Dict
@dataclass
class PriceModel:
total_price: Optional[TotalPriceModel] = None
fmt_price: Optional[FmtPriceModel] = None
line_offers: Optional[LineOffersModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["PriceModel"], src: Dict[str, Any]) -> "PriceModel":
d = src.copy()
tmp = cls(
total_price=d.pop("totalPrice", {}),
fmt_price=d.pop("fmtPrice", {}),
line_offers=d.pop("lineOffers", {}),
)
tmp.unmapped = d
return tmp
@dataclass
class CatalogOfferModel:
catalog_ns: Optional[CatalogNamespaceModel] = None
categories: Optional[List[CategoryModel]] = None
custom_attributes: Optional[List[CustomAttributeModel]] = None
description: Optional[str] = None
effective_date: Optional[datetime] = None
expiry_date: Optional[datetime] = None
id: Optional[str] = None
is_code_redemption_only: Optional[bool] = None
items: Optional[List[ItemModel]] = None
key_images: Optional[KeyImagesModel] = None
namespace: Optional[str] = None
offer_mappings: Optional[List[OfferMappingModel]] = None
offer_type: Optional[str] = None
price: Optional[PriceModel] = None
product_slug: Optional[str] = None
promotions: Optional[PromotionsModel] = None
seller: Optional[SellerModel] = None
status: Optional[str] = None
tags: Optional[List[TagModel]] = None
title: Optional[str] = None
url: Optional[str] = None
url_slug: Optional[str] = None
viewable_date: Optional[datetime] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["CatalogOfferModel"], src: Dict[str, Any]) -> "CatalogOfferModel":
d = src.copy()
effective_date = parse_date(x) if (x := d.pop("effectiveDate", "")) else None
expiry_date = parse_date(x) if (x := d.pop("expiryDate", "")) else None
key_images = KeyImagesModel.from_list(d.pop("keyImages", []))
price = PriceModel.from_dict(x) if (x := d.pop("price", {})) else None
viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None
tmp = cls(
catalog_ns=d.pop("catalogNs", {}),
categories=d.pop("categories", []),
custom_attributes=d.pop("customAttributes", []),
description=d.pop("description", ""),
effective_date=effective_date,
expiry_date=expiry_date,
id=d.pop("id", ""),
is_code_redemption_only=d.pop("isCodeRedemptionOnly", None),
items=d.pop("items", []),
key_images=key_images,
namespace=d.pop("namespace", ""),
offer_mappings=d.pop("offerMappings", []),
offer_type=d.pop("offerType", ""),
price=price,
product_slug=d.pop("productSlug", ""),
promotions=d.pop("promotions", {}),
seller=d.pop("seller", {}),
status=d.pop("status", ""),
tags=d.pop("tags", []),
title=d.pop("title", ""),
url=d.pop("url", ""),
url_slug=d.pop("urlSlug", ""),
viewable_date=viewable_date,
)
tmp.unmapped = d
return tmp
@dataclass
class WishlistItemModel:
created: Optional[datetime] = None
id: Optional[str] = None
namespace: Optional[str] = None
is_first_time: Optional[bool] = None
offer_id: Optional[str] = None
order: Optional[Any] = None
updated: Optional[datetime] = None
offer: Optional[CatalogOfferModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["WishlistItemModel"], src: Dict[str, Any]) -> "WishlistItemModel":
d = src.copy()
created = parse_date(x) if (x := d.pop("created", "")) else None
offer = CatalogOfferModel.from_dict(x) if (x := d.pop("offer", {})) else None
updated = parse_date(x) if (x := d.pop("updated", "")) else None
tmp = cls(
created=created,
id=d.pop("id", ""),
namespace=d.pop("namespace", ""),
is_first_time=d.pop("isFirstTime", None),
offer_id=d.pop("offerId", ""),
order=d.pop("order", ""),
updated=updated,
offer=offer,
)
tmp.unmapped = d
return tmp
@dataclass
class PagingModel:
count: Optional[int] = None
total: Optional[int] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["PagingModel"], src: Dict[str, Any]) -> "PagingModel":
d = src.copy()
count = d.pop("count", None)
total = d.pop("total", None)
tmp = cls(
count=count,
total=total,
)
tmp.unmapped = d
return tmp
@dataclass
class SearchStoreModel:
elements: Optional[List[CatalogOfferModel]] = None
paging: Optional[PagingModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["SearchStoreModel"], src: Dict[str, Any]) -> "SearchStoreModel":
d = src.copy()
_elements = d.pop("elements", [])
elements = [] if _elements else None
for item in _elements:
elem = CatalogOfferModel.from_dict(item)
elements.append(elem)
paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None
tmp = cls(
elements=elements,
paging=paging,
)
tmp.unmapped = d
return tmp
@dataclass
class CatalogModel:
search_store: Optional[SearchStoreModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["CatalogModel"], src: Dict[str, Any]) -> "CatalogModel":
d = src.copy()
search_store = SearchStoreModel.from_dict(x) if (x := d.pop("searchStore", {})) else None
tmp = cls(
search_store=search_store,
)
tmp.unmapped = d
return tmp
@dataclass
class WishlistItemsModel:
elements: Optional[List[WishlistItemModel]] = None
paging: Optional[PagingModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["WishlistItemsModel"], src: Dict[str, Any]) -> "WishlistItemsModel":
d = src.copy()
_elements = d.pop("elements", [])
elements = [] if _elements else None
for item in _elements:
elem = WishlistItemModel.from_dict(item)
elements.append(elem)
paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None
tmp = cls(
elements=elements,
paging=paging,
)
tmp.unmapped = d
return tmp
@dataclass
class RemoveFromWishlistModel:
success: Optional[bool] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["RemoveFromWishlistModel"], src: Dict[str, Any]) -> "RemoveFromWishlistModel":
d = src.copy()
tmp = cls(
success=d.pop("success", None),
)
tmp.unmapped = d
return tmp
@dataclass
class AddToWishlistModel:
wishlist_item: Optional[WishlistItemModel] = None
success: Optional[bool] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["AddToWishlistModel"], src: Dict[str, Any]) -> "AddToWishlistModel":
d = src.copy()
wishlist_item = WishlistItemModel.from_dict(x) if (x := d.pop("wishlistItem", {})) else None
tmp = cls(
wishlist_item=wishlist_item,
success=d.pop("success", None),
)
tmp.unmapped = d
return tmp
@dataclass
class WishlistModel:
wishlist_items: Optional[WishlistItemsModel] = None
remove_from_wishlist: Optional[RemoveFromWishlistModel] = None
add_to_wishlist: Optional[AddToWishlistModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["WishlistModel"], src: Dict[str, Any]) -> "WishlistModel":
d = src.copy()
wishlist_items = WishlistItemsModel.from_dict(x) if (x := d.pop("wishlistItems", {})) else None
remove_from_wishlist = (
RemoveFromWishlistModel.from_dict(x) if (x := d.pop("removeFromWishlist", {})) else None
)
add_to_wishlist = AddToWishlistModel.from_dict(x) if (x := d.pop("addToWishlist", {})) else None
tmp = cls(
wishlist_items=wishlist_items,
remove_from_wishlist=remove_from_wishlist,
add_to_wishlist=add_to_wishlist,
)
tmp.unmapped = d
return tmp
ProductModel = Dict
@dataclass
class DataModel:
product: Optional[ProductModel] = None
catalog: Optional[CatalogModel] = None
wishlist: Optional[WishlistModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["DataModel"], src: Dict[str, Any]) -> "DataModel":
d = src.copy()
catalog = CatalogModel.from_dict(x) if (x := d.pop("Catalog", {})) else None
wishlist = WishlistModel.from_dict(x) if (x := d.pop("Wishlist", {})) else None
tmp = cls(product=d.pop("Product", {}), catalog=catalog, wishlist=wishlist)
tmp.unmapped = d
return tmp
@dataclass
class ErrorModel:
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["ErrorModel"], src: Dict[str, Any]) -> "ErrorModel":
d = src.copy()
tmp = cls()
tmp.unmapped = d
return tmp
@dataclass
class ExtensionsModel:
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsModel":
d = src.copy()
tmp = cls()
tmp.unmapped = d
return tmp
@dataclass
class ResponseModel:
data: Optional[DataModel] = None
errors: Optional[List[ErrorModel]] = None
extensions: Optional[ExtensionsModel] = None
unmapped: Dict[str, Any] = field(default_factory=dict)
@classmethod
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)
extensions = ExtensionsModel.from_dict(x) if (x := d.pop("extensions", {})) else None
tmp = cls(data=data, errors=errors, extensions=extensions)
tmp.unmapped = d
return tmp

View file

@ -0,0 +1,5 @@
from datetime import datetime, timezone
def parse_date(date: str):
return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc)

View file

@ -44,8 +44,187 @@ class Constants(QObject):
]
game_query = """
query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, $keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, $sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, $effectiveDate: String) {
__Image = '''
type
url
alt
'''
__StorePageMapping = '''
cmsSlug
offerId
prePurchaseOfferId
'''
__PageSandboxModel = '''
pageSlug
pageType
productId
sandboxId
createdDate
updatedDate
deletedDate
mappings {
%s
}
''' % (__StorePageMapping)
__CatalogNamespace = '''
parent
displayName
store
home: mappings(pageType: "productHome") {
%s
}
addons: mappings(pageType: "addon--cms-hybrid") {
%s
}
offers: mappings(pageType: "offer") {
%s
}
''' % (__PageSandboxModel, __PageSandboxModel, __PageSandboxModel)
__CatalogItem = '''
id
namespace
'''
__GetPriceRes = '''
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
currencyCode
currencyInfo {
decimals
symbol
}
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
}
lineOffers {
appliedRules {
id
endDate
discountSetting {
discountType
}
}
}
'''
__Promotions = '''
promotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
upcomingPromotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
'''
__CatalogOffer = '''
title
id
namespace
offerType
expiryDate
status
isCodeRedemptionOnly
description
effectiveDate
keyImages {
%(image)s
}
currentPrice
seller {
id
name
}
productSlug
urlSlug
url
tags {
id
name
groupName
}
items {
%(catalog_item)s
}
customAttributes {
key
value
}
categories {
path
}
catalogNs @include(if: $withMapping) {
%(catalog_namespace)s
}
offerMappings @include(if: $withMapping) {
%(page_sandbox_model)s
}
price(country: $country) @include(if: $withPrice) {
%(get_price_res)s
}
promotions(category: $category) @include(if: $withPromotions) {
%(promotions)s
}
''' % {
"image": __Image,
"catalog_item": __CatalogItem,
"catalog_namespace": __CatalogNamespace,
"page_sandbox_model": __PageSandboxModel,
"get_price_res": __GetPriceRes,
"promotions": __Promotions,
}
__Pagination = '''
count
total
'''
SEARCH_STORE_QUERY = '''
query searchStoreQuery(
$allowCountries: String
$category: String
$count: Int
$country: String!
$keywords: String
$locale: String
$namespace: String
$withMapping: Boolean = false
$itemNs: String
$sortBy: String
$sortDir: String
$start: Int
$tag: String
$releaseDate: String
$withPrice: Boolean = false
$withPromotions: Boolean = false
$priceRange: String
$freeGame: Boolean
$onSale: Boolean
$effectiveDate: String
) {
Catalog {
searchStore(
allowCountries: $allowCountries
@ -67,452 +246,209 @@ query searchStoreQuery($allowCountries: String, $category: String, $count: Int,
effectiveDate: $effectiveDate
) {
elements {
title
id
namespace
description
effectiveDate
keyImages {
type
url
}
currentPrice
seller {
id
name
}
productSlug
urlSlug
url
tags {
id
}
items {
id
namespace
}
customAttributes {
key
value
}
categories {
path
}
catalogNs @include(if: $withMapping) {
mappings(pageType: "productHome") {
pageSlug
pageType
}
}
offerMappings @include(if: $withMapping) {
pageSlug
pageType
}
price(country: $country) @include(if: $withPrice) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
currencyCode
currencyInfo {
decimals
}
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
}
lineOffers {
appliedRules {
id
endDate
discountSetting {
discountType
}
}
}
}
promotions(category: $category) @include(if: $withPromotions) {
promotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
upcomingPromotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
}
%s
}
paging {
count
total
%s
}
}
}
}
"""
''' % (__CatalogOffer, __Pagination)
search_query = """
query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, $keywords: String, $locale: String, $namespace: String, $itemNs: String, $sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, $effectiveDate: String) {
Catalog {
searchStore(
allowCountries: $allowCountries
category: $category
count: $count
country: $country
keywords: $keywords
locale: $locale
namespace: $namespace
itemNs: $itemNs
sortBy: $sortBy
sortDir: $sortDir
releaseDate: $releaseDate
start: $start
tag: $tag
priceRange: $priceRange
freeGame: $freeGame
onSale: $onSale
effectiveDate: $effectiveDate
) {
elements {
title
id
namespace
description
effectiveDate
keyImages {
type
url
}
currentPrice
seller {
id
name
}
productSlug
urlSlug
url
tags {
id
}
items {
id
namespace
}
customAttributes {
key
value
}
categories {
path
}
catalogNs {
mappings(pageType: "productHome") {
pageSlug
pageType
}
}
offerMappings {
pageSlug
pageType
}
price(country: $country) @include(if: $withPrice) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
currencyCode
currencyInfo {
decimals
}
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
}
lineOffers {
appliedRules {
id
endDate
discountSetting {
discountType
}
}
}
}
promotions(category: $category) @include(if: $withPromotions) {
promotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
upcomingPromotionalOffers {
promotionalOffers {
startDate
endDate
discountSetting {
discountType
discountPercentage
}
}
}
}
}
paging {
count
total
}
}
}
__WISHLIST_ITEM = '''
id
order
created
offerId
updated
namespace
isFirstTime
offer(locale: $locale) {
%s
}
"""
''' % __CatalogOffer
wishlist_query = """
query wishlistQuery($country:String!, $locale:String) {
WISHLIST_QUERY = '''
query wishlistQuery(
$country: String!
$locale: String
$category: String
$withMapping: Boolean = false
$withPrice: Boolean = false
$withPromotions: Boolean = false
) {
Wishlist {
wishlistItems {
elements {
id
order
created
offerId
updated
namespace
offer(locale: $locale) {
productSlug
urlSlug
title
id
namespace
offerType
expiryDate
status
isCodeRedemptionOnly
description
effectiveDate
keyImages {
type
url
}
seller {
id
name
}
productSlug
urlSlug
items {
id
namespace
}
customAttributes {
key
value
}
catalogNs {
mappings(pageType: "productHome") {
pageSlug
pageType
%s
}
}
}
}
''' % __WISHLIST_ITEM
WISHLIST_ADD_QUERY = '''
mutation addWishlistMutation(
$namespace: String!
$offerId: String!
$country: String!
$locale: String
$category: String
$withMapping: Boolean = false
$withPrice: Boolean = false
$withPromotions: Boolean = false
) {
Wishlist {
addToWishlist(
namespace: $namespace
offerId: $offerId
) {
wishlistItem {
%s
}
success
}
}
}
''' % __WISHLIST_ITEM
WISHLIST_REMOVE_QUERY = '''
mutation removeFromWishlistMutation(
$namespace: String!
$offerId: String!
$operation: RemoveOperation!
) {
Wishlist {
removeFromWishlist(
namespace: $namespace
offerId: $offerId
operation: $operation
) {
success
}
}
}
'''
COUPONS_QUERY = '''
query getCoupons(
$currencyCountry: String!
$identityId: String!
$locale: String
) {
CodeRedemption {
coupons(
currencyCountry: $currencyCountry
identityId: $identityId
includeSalesEventInfo: true
) {
code
codeStatus
codeType
consumptionMetadata {
amountDisplay {
amount
currency
placement
symbol
}
minSalesPriceDisplay {
amount
currency
placement
symbol
}
}
endDate
namespace
salesEvent(locale: $locale) {
eventName
eventSlug
voucherImages {
type
url
}
voucherLink
}
startDate
}
}
}
'''
STORE_CONFIG_QUERY = '''
query getStoreConfig(
$includeCriticReviews: Boolean = false
$locale: String!
$sandboxId: String!
$templateId: String
) {
Product {
sandbox(sandboxId: $sandboxId) {
configuration(locale: $locale, templateId: $templateId) {
... on StoreConfiguration {
configs {
shortDescription
criticReviews @include(if: $includeCriticReviews) {
openCritic
}
}
offerMappings {
pageSlug
pageType
}
categories {
path
}
price(country: $country) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
socialLinks {
platform
url
}
supportedAudio
supportedText
tags(locale: $locale) {
id
name
groupName
}
technicalRequirements {
macos {
minimum
recommended
title
}
currencyCode
currencyInfo {
decimals
symbol
windows {
minimum
recommended
title
}
}
lineOffers {
appliedRules {
id
endDate
}
}
... on HomeConfiguration {
configs {
keyImages {
... on KeyImage {
type
url
alt
}
}
longDescription
}
}
}
}
}
}
"""
add_to_wishlist_query = """
mutation addWishlistMutation($namespace: String!, $offerId: String!, $country:String!, $locale:String) {
Wishlist {
addToWishlist(namespace: $namespace, offerId: $offerId) {
wishlistItem {
id,
order,
created,
offerId,
updated,
namespace,
isFirstTime
offer {
productSlug
urlSlug
title
id
namespace
offerType
expiryDate
status
isCodeRedemptionOnly
description
effectiveDate
keyImages {
type
url
}
seller {
id
name
}
productSlug
urlSlug
items {
id
namespace
}
customAttributes {
key
value
}
catalogNs {
mappings(pageType: "productHome") {
pageSlug
pageType
}
}
offerMappings {
pageSlug
pageType
}
categories {
path
}
price(country: $country) {
totalPrice {
discountPrice
originalPrice
voucherDiscount
discount
fmtPrice(locale: $locale) {
originalPrice
discountPrice
intermediatePrice
}
currencyCode
currencyInfo {
decimals
symbol
}
}
lineOffers {
appliedRules {
id
endDate
}
}
}
}
}
success
}
}
}
"""
remove_from_wishlist_query = """
mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {
Wishlist {
removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {
success
}
}
}
"""
coupon_query = """
query getCoupons($currencyCountry: String!, $identityId: String!, $locale: String) {
CodeRedemption {
coupons(currencyCountry: $currencyCountry, identityId: $identityId, includeSalesEventInfo: true) {
code
codeStatus
codeType
consumptionMetadata {
amountDisplay {
amount
currency
placement
symbol
}
minSalesPriceDisplay {
amount
currency
placement
symbol
}
}
endDate
namespace
salesEvent(locale: $locale) {
eventName
eventSlug
voucherImages {
type
url
}
voucherLink
}
startDate
}
}
}
"""
'''
# if __name__ == "__main__":
# from sgqlc import introspection, codegen
#
# coupon = codegen.operation.parse_graphql(coupon_query)
# codegen.schema.
# print(coupon.)
def compress_query(query: str) -> str:
return query.replace(" ", "").replace("\n", " ")
game_query = compress_query(SEARCH_STORE_QUERY)
search_query = compress_query(SEARCH_STORE_QUERY)
wishlist_query = compress_query(WISHLIST_QUERY)
wishlist_add_query = compress_query(WISHLIST_ADD_QUERY)
wishlist_remove_query = compress_query(WISHLIST_REMOVE_QUERY)
coupons_query = compress_query(COUPONS_QUERY)
store_config_query = compress_query(STORE_CONFIG_QUERY)
if __name__ == "__main__":
print(SEARCH_STORE_QUERY)

View file

@ -1,4 +1,6 @@
import logging
from pprint import pprint
from typing import List
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QFont, QDesktopServices, QFontMetrics
@ -10,13 +12,14 @@ from PyQt5.QtWidgets import (
QSizePolicy,
)
from rare.components.tabs.store.shop_models import ShopGame
from rare.components.tabs.store.api.models.response import CatalogOfferModel, DieselProduct, DieselProductDetail
from rare.shared import LegendaryCoreSingleton
from rare.shared.image_manager import ImageSize
from rare.ui.components.tabs.store.shop_game_info import Ui_ShopInfo
from rare.utils.misc import icon
from rare.widgets.side_tab import SideTabWidget, SideTabContents
from rare.widgets.elide_label import ElideLabel
from .api.debug import DebugDialog
from .image_widget import ShopImageWidget
logger = logging.getLogger("ShopInfo")
@ -37,45 +40,46 @@ class ShopGameInfo(QWidget, SideTabContents):
self.image.setFixedSize(ImageSize.Normal)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
self.game: ShopGame = None
self.offer: CatalogOfferModel = None
self.data: dict = {}
self.ui.wishlist_button.clicked.connect(self.add_to_wishlist)
self.ui.wishlist_button.setVisible(True)
self.in_wishlist = False
self.wishlist = []
self.requirements_tabs: SideTabWidget = SideTabWidget(parent=self.ui.requirements_group)
self.requirements_tabs: SideTabWidget = SideTabWidget(parent=self.ui.requirements_frame)
self.requirements_tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.ui.requirements_layout.addWidget(self.requirements_tabs)
self.setDisabled(True)
def handle_wishlist_update(self, data):
if data and data[0] == "error":
def handle_wishlist_update(self, wishlist: List[CatalogOfferModel]):
if wishlist and wishlist[0] == "error":
return
self.wishlist = [i["offer"]["title"] for i in data]
if self.title_str in self.wishlist:
self.wishlist = [game.id for game in wishlist]
if self.id_str in self.wishlist:
self.in_wishlist = True
self.ui.wishlist_button.setVisible(True)
self.ui.wishlist_button.setText(self.tr("Remove from Wishlist"))
else:
self.in_wishlist = False
self.ui.wishlist_button.setVisible(False)
def update_game(self, data: dict):
self.set_title.emit(data["title"])
self.ui.title.setText(data["title"])
self.title_str = data["title"]
self.id_str = data["id"]
def update_game(self, offer: CatalogOfferModel):
debug = DebugDialog(offer.__dict__, None)
debug.exec()
self.set_title.emit(offer.title)
self.ui.title.setText(offer.title)
self.title_str = offer.title
self.id_str = offer.id
self.api_core.get_wishlist(self.handle_wishlist_update)
# lk: delete tabs in inverse order because indices are updated on deletion
# lk: delete tabs in reverse order because indices are updated on deletion
while self.requirements_tabs.count():
self.requirements_tabs.widget(0).deleteLater()
self.requirements_tabs.removeTab(0)
self.requirements_tabs.clear()
slug = data["productSlug"]
slug = offer.product_slug
if not slug:
for mapping in data["offerMappings"]:
for mapping in offer.offer_mappings:
if mapping["pageType"] == "productHome":
slug = mapping["pageSlug"]
break
@ -86,7 +90,7 @@ class ShopGameInfo(QWidget, SideTabContents):
slug = slug.replace("/home", "")
self.slug = slug
if data["namespace"] in self.installed:
if offer.namespace in self.installed:
self.ui.open_store_button.setText(self.tr("Show Game on Epic Page"))
self.ui.owned_label.setVisible(True)
else:
@ -94,39 +98,44 @@ class ShopGameInfo(QWidget, SideTabContents):
self.ui.owned_label.setVisible(False)
self.ui.price.setText(self.tr("Loading"))
self.ui.wishlist_button.setVisible(False)
# self.title.setText(self.tr("Loading"))
# self.image.setPixmap(QPixmap())
self.data = data
is_bundle = False
for i in data["categories"]:
for i in offer.categories:
if "bundles" in i.get("path", ""):
is_bundle = True
# init API request
if slug:
self.api_core.get_game(slug, is_bundle, self.data_received)
self.api_core.get_game(offer.product_slug, is_bundle, self.data_received)
# else:
# self.data_received({})
self.offer = offer
def add_to_wishlist(self):
if not self.in_wishlist:
return
# self.api_core.add_to_wishlist(self.game.namespace, self.game.offer_id,
# lambda success: self.wishlist_button.setText(self.tr("Remove from wishlist"))
# if success else self.wishlist_button.setText("Something goes wrong"))
self.api_core.add_to_wishlist(
self.offer.namespace,
self.offer.id,
lambda success: self.ui.wishlist_button.setText(self.tr("Remove from wishlist"))
if success
else self.ui.wishlist_button.setText("Something went wrong")
)
else:
self.api_core.remove_from_wishlist(
self.game.namespace,
self.game.offer_id,
lambda success: self.ui.wishlist_button.setVisible(False)
self.offer.namespace,
self.offer.id,
lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist"))
if success
else self.ui.wishlist_button.setText("Something goes wrong"),
else self.ui.wishlist_button.setText("Something went wrong"),
)
def data_received(self, game):
def data_received(self, product: DieselProduct):
try:
self.game = ShopGame.from_json(game, self.data)
if product.pages:
product_data: DieselProductDetail = product.pages[0].data
else:
product_data: DieselProductDetail = product.data
except Exception as e:
raise e
logger.error(str(e))
@ -150,17 +159,19 @@ class ShopGameInfo(QWidget, SideTabContents):
# self.title.setText(self.game.title)
self.ui.price.setFont(QFont())
if self.game.price == "0" or self.game.price == 0:
price = self.offer.price.total_price["fmtPrice"]["originalPrice"]
discount_price = self.offer.price.total_price["fmtPrice"]["discountPrice"]
if price == "0" or price == 0:
self.ui.price.setText(self.tr("Free"))
else:
self.ui.price.setText(self.game.price)
if self.game.price != self.game.discount_price:
self.ui.price.setText(price)
if price != discount_price:
font = QFont()
font.setStrikeOut(True)
self.ui.price.setFont(font)
self.ui.discount_price.setText(
self.game.discount_price
if self.game.discount_price != "0"
discount_price
if discount_price != "0"
else self.tr("Free")
)
self.ui.discount_price.setVisible(True)
@ -171,8 +182,9 @@ class ShopGameInfo(QWidget, SideTabContents):
bold_font.setBold(True)
fm = QFontMetrics(self.font())
if self.game.reqs:
for system in self.game.reqs:
requirements = product_data.requirements
if requirements and requirements.systems:
for system in requirements.systems:
req_widget = QWidget(self.requirements_tabs)
req_layout = QGridLayout(req_widget)
req_widget.layout().setAlignment(Qt.AlignTop)
@ -185,53 +197,57 @@ class ShopGameInfo(QWidget, SideTabContents):
req_layout.addWidget(rec_label, 0, 2)
req_layout.setColumnStretch(1, 2)
req_layout.setColumnStretch(2, 2)
for i, (key, value) in enumerate(self.game.reqs.get(system, {}).items()):
req_layout.addWidget(QLabel(key, parent=req_widget), i + 1, 0)
min_label = ElideLabel(value[0], parent=req_widget)
for i, detail in enumerate(system.details):
req_layout.addWidget(QLabel(detail.title, parent=req_widget), i + 1, 0)
min_label = ElideLabel(detail.minimum, parent=req_widget)
req_layout.addWidget(min_label, i + 1, 1)
rec_label = ElideLabel(value[1], parent=req_widget)
rec_label = ElideLabel(detail.recommended, parent=req_widget)
req_layout.addWidget(rec_label, i + 1, 2)
self.requirements_tabs.addTab(req_widget, system)
# self.req_group_box.layout().addWidget(req_tabs)
# self.req_group_box.layout().setAlignment(Qt.AlignTop)
# else:
# self.req_group_box.layout().addWidget(
# QLabel(self.tr("Could not get requirements"))
# )
self.requirements_tabs.setEnabled(True)
if self.game.image_urls.front_tall:
img_url = self.game.image_urls.front_tall
elif self.game.image_urls.offer_image_tall:
img_url = self.game.image_urls.offer_image_tall
elif self.game.image_urls.product_logo:
img_url = self.game.image_urls.product_logo
self.requirements_tabs.addTab(req_widget, system.system_type)
# self.req_group_box.layout().addWidget(req_tabs)
# self.req_group_box.layout().setAlignment(Qt.AlignTop)
# else:
# self.req_group_box.layout().addWidget(
# QLabel(self.tr("Could not get requirements"))
# )
self.ui.requirements_frame.setVisible(True)
else:
img_url = ""
self.image.fetchPixmap(img_url)
self.ui.requirements_frame.setVisible(False)
key_images = self.offer.key_images
img_url = key_images.for_dimensions(self.image.size().width(), self.image.size().height())
self.image.fetchPixmap(img_url.url)
# self.image_stack.setCurrentIndex(0)
try:
if isinstance(self.game.developer, list):
self.ui.dev.setText(", ".join(self.game.developer))
else:
self.ui.dev.setText(self.game.developer)
except KeyError:
pass
self.ui.tags.setText(", ".join(self.game.tags))
about = product_data.about
self.ui.description_label.setText(about.desciption)
self.ui.dev.setText(about.developer_attribution)
# try:
# if isinstance(aboudeveloper, list):
# self.ui.dev.setText(", ".join(self.game.developer))
# else:
# self.ui.dev.setText(self.game.developer)
# except KeyError:
# pass
tags = product_data.unmapped["meta"].get("tags", [])
self.ui.tags.setText(", ".join(tags))
# clear Layout
for b in self.ui.social_group.findChildren(SocialButton, options=Qt.FindDirectChildrenOnly):
self.ui.social_layout.removeWidget(b)
b.deleteLater()
links = product_data.social_links
link_count = 0
for name, url in self.game.links:
if name.lower() == "homepage":
for name, url in links.items():
if name == "_type":
continue
name = name.replace("link", "").lower()
if name == "homepage":
icn = icon("mdi.web", "fa.search", scale_factor=1.5)
else:
try:
icn = icon(f"mdi.{name.lower()}", f"fa.{name.lower()}", scale_factor=1.5)
icn = icon(f"mdi.{name}", f"fa.{name}", scale_factor=1.5)
except Exception as e:
logger.error(str(e))
continue
@ -244,10 +260,10 @@ class ShopGameInfo(QWidget, SideTabContents):
self.setEnabled(True)
def add_wishlist_items(self, wishlist):
wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"]
for game in wishlist:
self.wishlist.append(game["offer"]["title"])
# def add_wishlist_items(self, wishlist: List[CatalogGameModel]):
# wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"]
# for game in wishlist:
# self.wishlist.append(game["offer"]["title"])
def button_clicked(self):
return

View file

@ -3,42 +3,44 @@ import logging
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QPushButton
from orjson import orjson
from rare.components.tabs.store.shop_models import ImageUrlModel
from rare.components.tabs.store.api.models.response import CatalogOfferModel
from rare.shared.image_manager import ImageSize
from rare.utils.misc import icon
from rare.utils.qt_requests import QtRequestManager
from .api.debug import DebugDialog
from .image_widget import ShopImageWidget
logger = logging.getLogger("GameWidgets")
class GameWidget(ShopImageWidget):
show_info = pyqtSignal(dict)
show_info = pyqtSignal(CatalogOfferModel)
def __init__(self, manager: QtRequestManager, json_info=None, parent=None):
def __init__(self, manager: QtRequestManager, catalog_game: CatalogOfferModel = None, parent=None):
super(GameWidget, self).__init__(manager, parent=parent)
self.setFixedSize(ImageSize.Wide)
self.ui.setupUi(self)
self.json_info = json_info
if json_info:
self.init_ui(json_info)
self.catalog_game = catalog_game
if catalog_game:
self.init_ui(catalog_game)
def init_ui(self, json_info):
if not json_info:
def init_ui(self, game: CatalogOfferModel):
if not game:
self.ui.title_label.setText(self.tr("An error occurred"))
return
self.ui.title_label.setText(json_info.get("title"))
for attr in json_info["customAttributes"]:
self.ui.title_label.setText(game.title)
for attr in game.custom_attributes:
if attr["key"] == "developerName":
developer = attr["value"]
break
else:
developer = json_info["seller"]["name"]
developer = game.seller["name"]
self.ui.developer_label.setText(developer)
price = json_info["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
discount_price = json_info["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
price = game.price.total_price["fmtPrice"]["originalPrice"]
discount_price = game.price.total_price["fmtPrice"]["discountPrice"]
self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}')
if price != discount_price:
font = self.ui.price_label.font()
@ -48,43 +50,48 @@ class GameWidget(ShopImageWidget):
else:
self.ui.discount_label.setVisible(False)
for c in r'<>?":|\/*':
json_info["title"] = json_info["title"].replace(c, "")
key_images = game.key_images
self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url)
for img in json_info["keyImages"]:
if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo",]:
if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game":
continue
self.fetchPixmap(img["url"])
break
else:
logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
# for img in json_info["keyImages"]:
# if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]:
# if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game":
# continue
# self.fetchPixmap(img["url"])
# break
# else:
# logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
def mousePressEvent(self, a0: QMouseEvent) -> None:
if a0.button() == Qt.LeftButton:
a0.accept()
self.show_info.emit(self.json_info)
self.show_info.emit(self.catalog_game)
if a0.button() == Qt.RightButton:
a0.accept()
print(self.catalog_game.__dict__)
dialog = DebugDialog(self.catalog_game.__dict__, self)
dialog.show()
class WishlistWidget(ShopImageWidget):
open_game = pyqtSignal(dict)
delete_from_wishlist = pyqtSignal(dict)
open_game = pyqtSignal(CatalogOfferModel)
delete_from_wishlist = pyqtSignal(CatalogOfferModel)
def __init__(self, manager: QtRequestManager, game: dict, parent=None):
def __init__(self, manager: QtRequestManager, catalog_game: CatalogOfferModel, parent=None):
super(WishlistWidget, self).__init__(manager, parent=parent)
self.setFixedSize(ImageSize.Wide)
self.ui.setupUi(self)
self.game = game
for attr in game["customAttributes"]:
self.game = catalog_game
for attr in catalog_game.custom_attributes:
if attr["key"] == "developerName":
developer = attr["value"]
break
else:
developer = game["seller"]["name"]
original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
developer = catalog_game.seller["name"]
original_price = catalog_game.price.total_price["fmtPrice"]["originalPrice"]
discount_price = catalog_game.price.total_price["fmtPrice"]["discountPrice"]
self.ui.title_label.setText(game.get("title"))
self.ui.title_label.setText(catalog_game.title)
self.ui.developer_label.setText(developer)
self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}')
if original_price != discount_price:
@ -94,11 +101,10 @@ class WishlistWidget(ShopImageWidget):
self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}')
else:
self.ui.discount_label.setVisible(False)
image_model = ImageUrlModel.from_json(game["keyImages"])
url = image_model.front_wide
if not url:
url = image_model.offer_image_wide
self.fetchPixmap(url)
key_images = catalog_game.key_images
self.fetchPixmap(
key_images.for_dimensions(self.width(), self.height()).url
)
self.delete_button = QPushButton(self)
self.delete_button.setIcon(icon("mdi.delete", color="white"))
@ -113,5 +119,7 @@ class WishlistWidget(ShopImageWidget):
a0.accept()
self.open_game.emit(self.game)
# right
elif a0.button() == Qt.RightButton:
pass # self.showMenu(e)
if a0.button() == Qt.RightButton:
a0.accept()
dialog = DebugDialog(self.game.__dict__, self)
dialog.show()

View file

@ -13,9 +13,12 @@ from rare.widgets.flow_layout import FlowLayout
from rare.widgets.side_tab import SideTabContents
from .image_widget import ShopImageWidget
from .api.debug import DebugDialog
from .api.models.response import CatalogOfferModel
class SearchResults(QScrollArea, SideTabContents):
show_info = pyqtSignal(dict)
show_info = pyqtSignal(CatalogOfferModel)
def __init__(self, api_core, parent=None):
super(SearchResults, self).__init__(parent=parent)
@ -59,22 +62,20 @@ class SearchResults(QScrollArea, SideTabContents):
class SearchResultItem(ShopImageWidget):
show_info = pyqtSignal(dict)
show_info = pyqtSignal(CatalogOfferModel)
def __init__(self, manager: QtRequestManager, result: dict, parent=None):
def __init__(self, manager: QtRequestManager, catalog_game: CatalogOfferModel, parent=None):
super(SearchResultItem, self).__init__(manager, parent=parent)
self.setFixedSize(ImageSize.Normal)
self.ui.setupUi(self)
for img in result["keyImages"]:
if img["type"] in ["DieselStoreFrontTall", "OfferImageTall", "Thumbnail", "ProductLogo"]:
self.fetchPixmap(img["url"])
break
else:
print("No image found")
self.ui.title_label.setText(result["title"])
price = result["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
discount_price = result["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
key_images = catalog_game.key_images
self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url)
self.ui.title_label.setText(catalog_game.title)
price = catalog_game.price.total_price["fmtPrice"]["originalPrice"]
discount_price = catalog_game.price.total_price["fmtPrice"]["discountPrice"]
self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}')
if price != discount_price:
font = self.ui.price_label.font()
@ -84,9 +85,14 @@ class SearchResultItem(ShopImageWidget):
else:
self.ui.discount_label.setVisible(False)
self.res = result
self.catalog_game = catalog_game
def mousePressEvent(self, a0: QMouseEvent) -> None:
if a0.button() == Qt.LeftButton:
a0.accept()
self.show_info.emit(self.res)
self.show_info.emit(self.catalog_game)
if a0.button() == Qt.RightButton:
a0.accept()
dialog = DebugDialog(self.catalog_game.__dict__, self)
dialog.show()

View file

@ -1,21 +1,32 @@
from logging import getLogger
from typing import List, Callable
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtWidgets import QApplication
from rare.components.tabs.store.api.debug import DebugDialog
from rare.components.tabs.store.constants import (
wishlist_query,
search_query,
add_to_wishlist_query,
remove_from_wishlist_query,
wishlist_add_query,
wishlist_remove_query,
)
from rare.components.tabs.store.shop_models import BrowseModel
from rare.utils.paths import cache_dir
from rare.utils.qt_requests import QtRequestManager
from .api.models.query import SearchStoreQuery
from .api.models.response import (
DieselProduct,
ResponseModel,
CatalogOfferModel,
)
logger = getLogger("ShopAPICore")
graphql_url = "https://graphql.epicgames.com/graphql"
DEBUG: Callable[[], bool] = lambda: "--debug" in QApplication.arguments()
class ShopApiCore(QObject):
update_wishlist = pyqtSignal()
@ -25,6 +36,7 @@ class ShopApiCore(QObject):
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 = QtRequestManager(parent=self)
self.authed_manager = QtRequestManager(token=token, parent=self)
self.cached_manager = QtRequestManager(cache=str(cache_dir().joinpath("store")), parent=self)
@ -39,54 +51,67 @@ class ShopApiCore(QObject):
"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, handle_func), params=params)
def _handle_free_games(self, data, handle_func):
@staticmethod
def __handle_free_games(data, handle_func):
try:
results: dict = data["data"]["Catalog"]["searchStore"]["elements"]
except KeyError:
response = ResponseModel.from_dict(data)
results: List[CatalogOfferModel] = response.data.catalog.search_store.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 DEBUG():
raise e
logger.error(f"Free games Api request failed: {e}")
handle_func(["error", e])
return
handle_func(results)
def get_wishlist(self, handle_func):
self.authed_manager.post(
graphql_url,
lambda data: self._handle_wishlist(data, handle_func),
lambda data: self.__handle_wishlist(data, handle_func),
{
"query": wishlist_query,
"variables": {
"country": self.country_code,
"locale": self.locale,
"withPrice": True,
},
},
)
def _handle_wishlist(self, data, handle_func):
@staticmethod
def __handle_wishlist(data, handle_func):
try:
results: list = data["data"]["Wishlist"]["wishlistItems"]["elements"]
except KeyError:
response = ResponseModel.from_dict(data)
if response.errors:
logger.error(response.errors)
handle_func(response.data.wishlist.wishlist_items.elements)
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 DEBUG():
raise e
logger.error(f"Free games Api request failed: {e}")
handle_func(["error", e])
return
handle_func(results)
def search_game(self, name, handle_func):
def search_game(self, name, handler):
payload = {
"query": search_query,
"variables": {
"category": "games/edition/base|bundles/games|editors|software/edition/base",
"count": 10,
"count": 20,
"country": self.country_code,
"keywords": name,
"locale": self.locale,
@ -99,42 +124,56 @@ class ShopApiCore(QObject):
},
}
self.manager.post(graphql_url, lambda data: self._handle_search(data, handle_func), payload)
self.manager.post(graphql_url, lambda data: self.__handle_search(data, handler), payload)
def _handle_search(self, data, handle_func):
@staticmethod
def __handle_search(data, handler):
try:
handle_func(data["data"]["Catalog"]["searchStore"]["elements"])
response = ResponseModel.from_dict(data)
handler(response.data.catalog.search_store.elements)
except KeyError as e:
logger.error(str(e))
handle_func([])
if DEBUG():
raise e
handler([])
except Exception as e:
logger.error(f"Search Api request failed: {e}")
handle_func([])
if DEBUG():
raise e
handler([])
return
def browse_games(self, browse_model: BrowseModel, handle_func):
def browse_games(self, browse_model: SearchStoreQuery, handle_func):
if self.browse_active:
self.next_browse_request = (browse_model, handle_func)
return
self.browse_active = True
payload = {
"query": search_query,
"variables": browse_model.__dict__
"variables": browse_model.to_dict()
}
self.manager.post(graphql_url, lambda data: self._handle_browse_games(data, handle_func), payload)
debug = DebugDialog(payload["variables"], None)
debug.exec()
self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, handle_func), payload)
def _handle_browse_games(self, data, handle_func):
def __handle_browse_games(self, data, handle_func):
debug = DebugDialog(data, None)
debug.exec()
self.browse_active = False
if data is None:
data = {}
if not self.next_browse_request:
try:
handle_func(data["data"]["Catalog"]["searchStore"]["elements"])
response = ResponseModel.from_dict(data)
handle_func(response.data.catalog.search_store.elements)
except 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
@ -143,62 +182,72 @@ class ShopApiCore(QObject):
self.next_browse_request = tuple(())
def get_game(self, slug: str, is_bundle: bool, handle_func):
url = f"https://store-content.ak.epicgames.com/api/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}"
self.manager.get(url, lambda data: self._handle_get_game(data, handle_func))
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))
def _handle_get_game(self, data, handle_func):
@staticmethod
def __handle_get_game(data, handle_func):
debug = DebugDialog(data, None)
debug.exec()
try:
handle_func(data)
product = DieselProduct.from_dict(data)
handle_func(product)
except Exception as e:
raise e
if DEBUG():
raise e
logger.error(str(e))
# handle_func({})
# needs a captcha
def add_to_wishlist(self, namespace, offer_id, handle_func: callable):
payload = {
"query": wishlist_add_query,
"variables": {
"offerId": offer_id,
"namespace": namespace,
"country": self.country_code,
"locale": self.locale,
},
"query": add_to_wishlist_query,
}
self.authed_manager.post(graphql_url, lambda data: self._handle_add_to_wishlist(data, handle_func), payload)
def _handle_add_to_wishlist(self, data, handle_func):
debug = DebugDialog(data, None)
debug.exec()
try:
data = data["data"]["Wishlist"]["addToWishlist"]
if data["success"]:
handle_func(True)
else:
handle_func(False)
response = ResponseModel.from_dict(data)
data = response.data.wishlist.add_to_wishlist
handle_func(data.success)
except Exception as e:
if DEBUG():
raise e
logger.error(str(e))
handle_func(False)
self.update_wishlist.emit()
def remove_from_wishlist(self, namespace, offer_id, handle_func: callable):
payload = {
"query": wishlist_remove_query,
"variables": {
"offerId": offer_id,
"namespace": namespace,
"operation": "REMOVE",
},
"query": remove_from_wishlist_query,
}
self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func),
payload)
def _handle_remove_from_wishlist(self, data, handle_func):
debug = DebugDialog(data, None)
debug.exec()
try:
data = data["data"]["Wishlist"]["removeFromWishlist"]
if data["success"]:
handle_func(True)
else:
handle_func(False)
response = ResponseModel.from_dict(data)
data = response.data.wishlist.remove_from_wishlist
handle_func(data.success)
except Exception as e:
if DEBUG():
raise e
logger.error(str(e))
handle_func(False)
self.update_wishlist.emit()

View file

@ -1,195 +0,0 @@
import datetime
from dataclasses import dataclass
from typing import List, Dict
import epicstore_api.queries as egs_query
class ImageUrlModel:
def __init__(
self,
front_tall: str = "",
offer_image_tall: str = "",
thumbnail: str = "",
front_wide: str = "",
offer_image_wide: str = "",
product_logo: str = "",
):
self.front_tall = front_tall
self.offer_image_tall = offer_image_tall
self.thumbnail = thumbnail
self.front_wide = front_wide
self.offer_image_wide = offer_image_wide
self.product_logo = product_logo
@classmethod
def from_json(cls, json_data: list):
tmp = cls()
for item in json_data:
if item["type"] == "Thumbnail":
tmp.thumbnail = item["url"]
elif item["type"] == "DieselStoreFrontTall":
tmp.front_tall = item["url"]
elif item["type"] == "DieselStoreFrontWide":
tmp.front_wide = item["url"]
elif item["type"] == "OfferImageTall":
tmp.offer_image_tall = item["url"]
elif item["type"] == "OfferImageWide":
tmp.offer_image_wide = item["url"]
elif item["type"] == "ProductLogo":
tmp.product_logo = item["url"]
return tmp
class ShopGame:
# TODO: Copyrights etc
def __init__(
self,
title: str = "",
id: str = "",
image_urls: ImageUrlModel = None,
social_links: Dict = None,
langs: Dict = None,
reqs: Dict = None,
publisher: str = "",
developer: str = "",
original_price: str = "",
discount_price: str = "",
tags: List = None,
namespace: str = "",
offer_id: str = "",
):
self.title = title
self.id = id
self.image_urls = image_urls
self.links = []
if social_links:
for item in social_links:
if item.startswith("link"):
self.links.append(
tuple((item.replace("link", ""), social_links[item]))
)
else:
self.links = []
self.languages = langs if langs is not None else {}
self.reqs = reqs if reqs is not None else {}
self.publisher = publisher
self.developer = developer
self.price = original_price
self.discount_price = discount_price
self.tags = tags if tags is not None else []
self.namespace = namespace
self.offer_id = offer_id
@classmethod
def from_json(cls, api_data: dict, search_data: dict):
if isinstance(api_data, list):
for product in api_data:
if product["_title"] == "home":
api_data = product
break
if "pages" in api_data.keys():
for page in api_data["pages"]:
if page["_slug"] == "home":
api_data = page
break
tmp = cls()
if search_data:
tmp.title = search_data.get("title", "Fail")
tmp.id = search_data.get("id")
tmp.image_urls = ImageUrlModel.from_json(search_data["keyImages"])
if not tmp.developer:
for i in search_data["customAttributes"]:
if i["key"] == "developerName":
tmp.developer = i["value"]
tmp.price = search_data["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
tmp.discount_price = search_data["price"]["totalPrice"]["fmtPrice"][
"discountPrice"
]
tmp.namespace = search_data["namespace"]
tmp.offer_id = search_data["id"]
if api_data:
links = api_data["data"]["socialLinks"]
tmp.links = []
for item in links:
if item.startswith("link"):
tmp.links.append(tuple((item.replace("link", ""), links[item])))
tmp.available_voice_langs = api_data["data"]["requirements"].get(
"languages", "Failed"
)
tmp.reqs = {}
for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])):
try:
tmp.reqs[system["systemType"]] = {}
except KeyError:
continue
for req in system["details"]:
try:
tmp.reqs[system["systemType"]][req["title"]] = (
req["minimum"],
req["recommended"],
)
except KeyError:
pass
tmp.publisher = api_data["data"]["meta"].get("publisher", "")
tmp.developer = api_data["data"]["meta"].get("developer", "")
tmp.tags = [
i.replace("_", " ").capitalize()
for i in api_data["data"]["meta"].get("tags", [])
]
return tmp
@dataclass
class BrowseModel:
category: str = "games/edition/base|bundles/games|editors|software/edition/base"
count: int = 30
language_code: str = "en"
country_code: str = "US"
keywords: str = ""
sortDir: str = "DESC"
start: int = 0
tag: str = ""
withMapping: bool = True
withPrice: bool = True
date: str = (
f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%H:%M:%S')}.420Z]"
)
price: str = ""
onSale: bool = False
def __post_init__(self):
self.locale = f"{self.language_code}-{self.country_code}"
@property
def __dict__(self):
payload = {
"count": self.count,
"category": self.category,
"allowCountries": self.country_code,
"namespace": "",
"sortBy": "releaseDate",
"sortDir": self.sortDir,
"start": self.start,
"keywords": self.keywords,
"tag": self.tag,
"priceRange": self.price,
"releaseDate": self.date,
"withPrice": self.withPrice,
"locale": self.locale,
"country": self.country_code,
}
if self.price == "free":
payload["freeGame"] = True
payload.pop("priceRange")
elif self.price.startswith("<price>"):
payload["priceRange"] = self.price.replace("<price>", "")
if self.onSale:
payload["onSale"] = True
if self.price:
payload["effectiveDate"] = self.date
else:
payload.pop("priceRange")
return payload

View file

@ -11,17 +11,18 @@ from PyQt5.QtWidgets import (
QHBoxLayout,
QWidget, QSizePolicy, QStackedLayout,
)
from legendary.core import LegendaryCore
from rare.ui.components.tabs.store.store import Ui_ShopWidget
from rare.utils.extra_widgets import ButtonLineEdit
from rare.widgets.flow_layout import FlowLayout
from rare.widgets.side_tab import SideTabContents
from .api.models.query import SearchStoreQuery
from .api.models.response import CatalogOfferModel, WishlistItemModel
from .constants import Constants
from .game_widgets import GameWidget
from .image_widget import WaitingSpinner
from .shop_api_core import ShopApiCore
from .shop_models import BrowseModel
logger = logging.getLogger("Shop")
@ -98,8 +99,8 @@ class ShopWidget(QWidget, SideTabContents):
def update_wishlist(self):
self.api_core.get_wishlist(self.add_wishlist_items)
def add_wishlist_items(self, wishlist):
for w in self.discounts_flow.findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly):
def add_wishlist_items(self, wishlist: List[WishlistItemModel]):
for w in self.discounts_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
self.discounts_flow.layout().removeWidget(w)
w.deleteLater()
@ -120,8 +121,8 @@ class ShopWidget(QWidget, SideTabContents):
if not game:
continue
try:
if game["offer"]["price"]["totalPrice"]["discount"] > 0:
w = GameWidget(self.api_core.cached_manager, game["offer"])
if game.offer.price.total_price["discount"] > 0:
w = GameWidget(self.api_core.cached_manager, game.offer)
w.show_info.connect(self.show_game)
self.discounts_flow.layout().addWidget(w)
discounts += 1
@ -133,7 +134,7 @@ class ShopWidget(QWidget, SideTabContents):
# FIXME: FlowLayout doesn't update on adding widget
self.discounts_flow.layout().update()
def add_free_games(self, free_games: list):
def add_free_games(self, free_games: List[CatalogOfferModel]):
for w in self.ui.free_container.layout().findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly):
self.ui.free_container.layout().removeWidget(w)
w.deleteLater()
@ -170,14 +171,14 @@ class ShopWidget(QWidget, SideTabContents):
for game in free_games:
try:
if (
game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] == "0"
and game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
!= game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
game.price.total_price["fmtPrice"]["discountPrice"] == "0"
and game.price.total_price["fmtPrice"]["originalPrice"]
!= game.price.total_price["fmtPrice"]["discountPrice"]
):
free_games_now.append(game)
continue
if game["title"] == "Mystery Game":
if game.title == "Mystery Game":
coming_free_games.append(game)
continue
except KeyError as e:
@ -187,7 +188,7 @@ class ShopWidget(QWidget, SideTabContents):
# parse datetime to check if game is next week or now
try:
start_date = datetime.datetime.strptime(
game["promotions"]["upcomingPromotionalOffers"][0][
game.promotions["upcomingPromotionalOffers"][0][
"promotionalOffers"
][0]["startDate"],
"%Y-%m-%dT%H:%M:%S.%fZ",
@ -195,7 +196,7 @@ class ShopWidget(QWidget, SideTabContents):
except Exception:
try:
start_date = datetime.datetime.strptime(
game["promotions"]["promotionalOffers"][0][
game.promotions["promotionalOffers"][0][
"promotionalOffers"
][0]["startDate"],
"%Y-%m-%dT%H:%M:%S.%fZ",
@ -226,7 +227,7 @@ class ShopWidget(QWidget, SideTabContents):
# free games next week
for free_game in coming_free_games:
w = GameWidget(self.api_core.cached_manager, free_game)
if free_game["title"] != "Mystery Game":
if free_game.title != "Mystery Game":
w.show_info.connect(self.show_game)
self.free_games_next.layout().addWidget(w)
# self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300))
@ -346,12 +347,12 @@ class ShopWidget(QWidget, SideTabContents):
self.games_layout.setCurrentWidget(self.games_spinner)
browse_model = BrowseModel(
language_code=self.core.language_code,
country_code=self.core.country_code,
browse_model = SearchStoreQuery(
language=self.core.language_code,
country=self.core.country_code,
count=20,
price=self.price,
onSale=self.ui.on_discount.isChecked(),
price_range=self.price,
on_sale=self.ui.on_discount.isChecked(),
)
browse_model.tag = "|".join(self.tags)
@ -360,14 +361,14 @@ class ShopWidget(QWidget, SideTabContents):
self.api_core.browse_games(browse_model, self.show_games)
def show_games(self, data):
for w in self.games_flow.layout().findChildren(GameWidget, options=Qt.FindDirectChildrenOnly):
for w in self.games_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
self.games_flow.layout().removeWidget(w)
w.deleteLater()
if data:
for game in data:
w = GameWidget(self.api_core.cached_manager, game)
w.show_info.connect(self.show_game.emit)
w.show_info.connect(self.show_game)
self.games_flow.layout().addWidget(w)
else:
self.games_flow.layout().addWidget(

View file

@ -1,3 +1,5 @@
from typing import List
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QMessageBox, QWidget
@ -7,10 +9,11 @@ from rare.widgets.side_tab import SideTabContents
from rare.widgets.flow_layout import FlowLayout
from .shop_api_core import ShopApiCore
from .game_widgets import WishlistWidget
from .api.models.response import WishlistItemModel, CatalogOfferModel
class Wishlist(QWidget, SideTabContents):
show_game_info = pyqtSignal(dict)
show_game_info = pyqtSignal(CatalogOfferModel)
update_wishlist_signal = pyqtSignal()
def __init__(self, api_core: ShopApiCore, parent=None):
@ -38,10 +41,10 @@ class Wishlist(QWidget, SideTabContents):
self.setEnabled(False)
self.api_core.get_wishlist(self.set_wishlist)
def delete_from_wishlist(self, game):
def delete_from_wishlist(self, game: CatalogOfferModel):
self.api_core.remove_from_wishlist(
game["namespace"],
game["id"],
game.namespace,
game.id,
lambda success: self.update_wishlist()
if success
else QMessageBox.warning(
@ -73,27 +76,26 @@ class Wishlist(QWidget, SideTabContents):
self.ui.list_container.layout().removeWidget(w)
if sort == 0:
func = lambda x: x.game["title"]
func = lambda x: x.game.title
reverse = self.ui.reverse.isChecked()
elif sort == 1:
func = lambda x: x.game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
func = lambda x: x.game.price.total_price["fmtPrice"]["discountPrice"]
reverse = self.ui.reverse.isChecked()
elif sort == 2:
func = lambda x: x.game["seller"]["name"]
func = lambda x: x.game.seller["name"]
reverse = self.ui.reverse.isChecked()
elif sort == 3:
func = lambda x: 1 - (x.game["price"]["totalPrice"]["discountPrice"] / x.game["price"]["totalPrice"]["originalPrice"])
func = lambda x: 1 - (x.game.price.total_price["discountPrice"] / x.game.price.total_price["originalPrice"])
reverse = not self.ui.reverse.isChecked()
else:
func = lambda x: x.game["title"]
func = lambda x: x.game.title
reverse = self.ui.reverse.isChecked()
widgets = sorted(widgets, key=func, reverse=reverse)
for w in widgets:
self.ui.list_container.layout().addWidget(w)
def set_wishlist(self, wishlist=None, sort=0):
def set_wishlist(self, wishlist: List[WishlistItemModel] = None, sort=0):
if wishlist and wishlist[0] == "error":
return
@ -111,8 +113,8 @@ class Wishlist(QWidget, SideTabContents):
self.ui.no_games_label.setVisible(False)
for game in wishlist:
w = WishlistWidget(self.api_core.cached_manager, game["offer"], self.ui.list_container)
w.open_game.connect(self.show_game_info.emit)
w = WishlistWidget(self.api_core.cached_manager, game.offer, self.ui.list_container)
w.open_game.connect(self.show_game_info)
w.delete_from_wishlist.connect(self.delete_from_wishlist)
self.widgets.append(w)
self.list_layout.addWidget(w)

View file

@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ShopInfo(object):
def setupUi(self, ShopInfo):
ShopInfo.setObjectName("ShopInfo")
ShopInfo.resize(747, 442)
ShopInfo.resize(443, 347)
ShopInfo.setWindowTitle("ShopGameInfo")
self.main_layout = QtWidgets.QHBoxLayout(ShopInfo)
self.main_layout.setObjectName("main_layout")
@ -146,19 +146,43 @@ class Ui_ShopInfo(object):
self.button_layout.addWidget(self.wishlist_button)
self.info_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.buttons_widget)
self.right_layout.addLayout(self.info_layout)
self.requirements_group = QtWidgets.QFrame(ShopInfo)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.requirements_frame = QtWidgets.QFrame(ShopInfo)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.requirements_group.sizePolicy().hasHeightForWidth())
self.requirements_group.setSizePolicy(sizePolicy)
self.requirements_group.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.requirements_group.setFrameShadow(QtWidgets.QFrame.Sunken)
self.requirements_group.setObjectName("requirements_group")
self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_group)
sizePolicy.setHeightForWidth(self.requirements_frame.sizePolicy().hasHeightForWidth())
self.requirements_frame.setSizePolicy(sizePolicy)
self.requirements_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.requirements_frame.setFrameShadow(QtWidgets.QFrame.Sunken)
self.requirements_frame.setObjectName("requirements_frame")
self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_frame)
self.requirements_layout.setContentsMargins(0, 0, 0, 0)
self.requirements_layout.setObjectName("requirements_layout")
self.right_layout.addWidget(self.requirements_group)
self.right_layout.addWidget(self.requirements_frame)
self.description_group = QtWidgets.QGroupBox(ShopInfo)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.description_group.sizePolicy().hasHeightForWidth())
self.description_group.setSizePolicy(sizePolicy)
self.description_group.setFlat(False)
self.description_group.setObjectName("description_group")
self.description_layout = QtWidgets.QVBoxLayout(self.description_group)
self.description_layout.setObjectName("description_layout")
self.description_label = QtWidgets.QLabel(self.description_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.description_label.sizePolicy().hasHeightForWidth())
self.description_label.setSizePolicy(sizePolicy)
self.description_label.setText("error")
self.description_label.setTextFormat(QtCore.Qt.MarkdownText)
self.description_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.description_label.setWordWrap(True)
self.description_label.setOpenExternalLinks(True)
self.description_label.setObjectName("description_label")
self.description_layout.addWidget(self.description_label)
self.right_layout.addWidget(self.description_group)
self.main_layout.addLayout(self.right_layout)
self.main_layout.setStretch(1, 1)
@ -176,6 +200,7 @@ class Ui_ShopInfo(object):
self.actions_label.setText(_translate("ShopInfo", "Actions"))
self.open_store_button.setText(_translate("ShopInfo", "Buy in Epic Games Store"))
self.wishlist_button.setText(_translate("ShopInfo", "Add to wishlist"))
self.description_group.setTitle(_translate("ShopInfo", "Description"))
if __name__ == "__main__":

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>747</width>
<height>442</height>
<width>443</width>
<height>347</height>
</rect>
</property>
<property name="windowTitle">
@ -309,9 +309,9 @@
</layout>
</item>
<item>
<widget class="QFrame" name="requirements_group">
<widget class="QFrame" name="requirements_frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@ -338,6 +338,49 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="description_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Description</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="description_layout">
<item>
<widget class="QLabel" name="description_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">error</string>
</property>
<property name="textFormat">
<enum>Qt::MarkdownText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>

View file

@ -1,11 +1,11 @@
import json
from dataclasses import dataclass, field
from email.message import Message
from logging import getLogger
from typing import Callable, Dict, TypeVar, List, Tuple
from typing import Union
from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QJsonParseError, QJsonDocument, QUrlQuery, pyqtSlot
import orjson
from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QUrlQuery, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkDiskCache
logger = getLogger("QtRequests")
@ -67,7 +67,7 @@ class QtRequestManager(QObject):
def __post(self, item: RequestQueueItem):
request = self.__prepare_request(item)
payload = json.dumps(item.payload).encode("utf-8")
payload = orjson.dumps(item.payload)
reply = self.manager.post(request, payload)
reply.errorOccurred.connect(self.__on_error)
self.__active_requests[reply] = item
@ -118,7 +118,7 @@ class QtRequestManager(QObject):
def __on_finished(self, reply: QNetworkReply):
item = self.__active_requests.pop(reply, None)
if item is None:
logger.error("QNetworkReply: {} without associated item", reply)
logger.error("QNetworkReply: %s without associated item", reply.url().toString())
reply.deleteLater()
return
if reply.error():
@ -128,13 +128,7 @@ class QtRequestManager(QObject):
maintype, subtype = mimetype.split("/")
bin_data = reply.readAll().data()
if mimetype == "application/json":
error = QJsonParseError()
json_data = QJsonDocument.fromJson(bin_data, error)
if not error.error:
data = json.loads(json_data.toJson().data().decode())
else:
logger.error(error.errorString())
data = None
data = orjson.loads(bin_data)
elif maintype == "image":
data = bin_data
else:

View file

@ -6,3 +6,4 @@ nuitka
ordered-set
PyQt5-stubs
qstylizer

View file

@ -11,3 +11,4 @@ pythonnet>=3.0.0rc4; platform_system == "Windows"
cefpython3; platform_system == "Windows"
pywebview[cef]; platform_system == "Windows"
pypresence

View file

@ -7,3 +7,4 @@ legendary-gl>=0.20.33
orjson
vdf; platform_system != "Windows"
pywin32; platform_system == "Windows"