1
0
Fork 0
mirror of synced 2024-05-04 12:42:52 +12:00

Store: Exploratory changes for GraphQL API

This commit is contained in:
loathingKernel 2023-04-10 14:03:45 +03:00
parent f6396f488a
commit 91af16b76d
No known key found for this signature in database
GPG key ID: CE0C72D0B53821FD
27 changed files with 2230 additions and 843 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
@ -69,7 +70,7 @@ class StoreTab(SideTabWidget):
def update_wishlist(self):
self.shop.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 qta_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,33 @@
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 QtRequests
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 +37,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 = QtRequests(parent=self)
self.authed_manager = QtRequests(token=token, parent=self)
self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), parent=self)
@ -39,54 +52,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 +125,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 +183,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")
@ -102,8 +103,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()
@ -124,8 +125,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
@ -137,7 +138,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()
@ -174,14 +175,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:
@ -191,7 +192,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",
@ -199,7 +200,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",
@ -230,7 +231,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))
@ -350,12 +351,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)
@ -364,14 +365,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

@ -66,7 +66,7 @@ class QtRequests(QObject):
def __post(self, item: RequestQueueItem):
request = self.__prepare_request(item)
payload = orjson.dumps(item.payload) # pylint: disable=maybe-no-member
payload = orjson.dumps(item.payload)
reply = self.manager.post(request, payload)
reply.errorOccurred.connect(self.__on_error)
self.__active_requests[reply] = item

View file

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

View file

@ -13,3 +13,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 @ git+https://github.com/derrod/legendary@96e07ff ; platform_system
orjson
vdf; platform_system == "Linux" or platform_system == "FreeBSD"
pywin32; platform_system == "Windows"