Skip to content

Commit

Permalink
Fixed price and discount percentage parsing for games only sold as pa…
Browse files Browse the repository at this point in the history
…rt of a bundle.

Added bundle_id field to app details parser for games sold exclusively within a bundle.
  • Loading branch information
Bilge committed Mar 10, 2024
1 parent 839338d commit d96a8c3
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 4 deletions.
36 changes: 32 additions & 4 deletions src/Scrape/AppDetailsParser.php
Expand Up @@ -59,7 +59,8 @@ private static function parseStorePage(string $html): array

// Purchase area.
[$purchaseArea, $DEBUG_primary_sub_id] = self::findPrimaryPurchaseArea($crawler, $name);
$price = $free ? 0 : self::parsePrice($purchaseArea);
$bundle_id = self::parseBundleId($purchaseArea);
$price = $free ? 0 : ($bundle_id ? self::calculateBundlePrice($purchaseArea) : self::parsePrice($purchaseArea));
$discount_price = $free ? null : self::parseDiscountPrice($purchaseArea);
$discount = $free ? 0 : self::parseDiscountPercentage($purchaseArea);

Expand Down Expand Up @@ -113,6 +114,7 @@ private static function parseStorePage(string $html): array
'mac',
'steam_deck',
'demo_id',
'bundle_id',
'DEBUG_primary_sub_id',
);
}
Expand Down Expand Up @@ -220,12 +222,17 @@ private static function parseLanguages(Crawler $crawler): array
}

/**
* @param Crawler $purchaseArea
* Parses the price in the specified purchase area.
*
* The price is the regular purchase price when not discounted, or the original price when discounted.
*
* @param Crawler $purchaseArea Purchase area.
*
* @return int|null Price if integer, 0 if app is free and null if app has no price (i.e. not for sale).
*/
private static function parsePrice(Crawler $purchaseArea): ?int
{
// Not present when discounted.
$priceElement = $purchaseArea->filter('.game_purchase_price');
$discountElement = $purchaseArea->filter('.discount_original_price');

Expand Down Expand Up @@ -253,11 +260,32 @@ private static function parseDiscountPrice(Crawler $purchaseArea): ?int

private static function parseDiscountPercentage(Crawler $purchaseArea): int
{
$element = $purchaseArea->filter('.discount_pct');
$element = $purchaseArea->filter('.discount_pct, .bundle_base_discount');

return $element->count() ? self::filterNumbers($element->text()) : 0;
}

private static function parseBundleId(Crawler $purchaseArea): ?int
{
if (!($input = $purchaseArea->filter('input[name=bundleid]'))->count()) {
return null;
}

return +$input->attr('value');
}

private static function calculateBundlePrice(Crawler $purchaseArea): int
{
$data = $purchaseArea->getNode(0)->parentNode->getAttribute('data-ds-bundle-data');
$json = json_decode($data, true, flags: JSON_THROW_ON_ERROR);

return array_reduce(
$json['m_rgItems'],
static fn (int $acc, array $item) => $acc + $item['m_nBasePriceInCents'],
0
);
}

private static function parseVrExclusive(Crawler $crawler): bool
{
return $crawler->filter('[data-featuretarget=game-notice-vr-required]')->count() > 0;
Expand Down Expand Up @@ -329,7 +357,7 @@ private static function filterDevlisherUrl(string $url): ?string
* @param Crawler $crawler Crawler instance.
* @param string $title App title.
*
* @return array (Crawler|?int)[] [
* @return (Crawler|?int)[] [
* Crawler containing the primary purchase area node if found, otherwise a crawler with no nodes.,
* Primary sub ID.,
* ]
Expand Down
18 changes: 18 additions & 0 deletions test/Functional/ScrapeAppDetailsTest.php
Expand Up @@ -87,6 +87,7 @@ public function testGame(): void
self::assertArrayNotHasKey('valve_index', $app);

self::assertNull($app['demo_id']);
self::assertNull($app['bundle_id'], 'App is not sold exclusively as a bundle.');

foreach ($app['tags'] as $tag) {
self::assertArrayHasKey('name', $tag);
Expand Down Expand Up @@ -851,6 +852,23 @@ public static function provideEaPlayRegularPurchaseAreas(): iterable
];
}

/**
* Tests that an EA Play subscrption game that can also be purchased as part of a bundle, but cannot be purchased
* separately, has its price parsed and calculated correctly.
*
* @see https://store.steampowered.com/app/2229850/Command__Conquer_Red_Alert_2_and_Yuris_Revenge/
*/
public function testEaPlayBundlePurchase(): void
{
$app = $this->porter->importOne(new Import(new ScrapeAppDetails(2229850)));

self::assertSame(39394, $app['bundle_id'], 'Game is only available in a bundle.');

self::assertSame(988, $app['discount_price']);
self::assertSame(50, $app['discount']);
self::assertSame(1988, $app['price']);
}

/**
* Tests that apps with multiple purchase areas are parsed correctly by picking the correct sub ID.
*
Expand Down

0 comments on commit d96a8c3

Please sign in to comment.