Passed
Push — master ( bb114b...6dc021 )
by Alexey
03:23 queued 10s
created

GPlayApps::search()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2.0011

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 23
ccs 14
cts 15
cp 0.9333
rs 9.7333
cc 2
nc 2
nop 3
crap 2.0011
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @author   Ne-Lexa
7
 * @license  MIT
8
 *
9
 * @see      https://github.com/Ne-Lexa/google-play-scraper
10
 */
11
12
namespace Nelexa\GPlay;
13
14
use GuzzleHttp\Promise\EachPromise;
15
use GuzzleHttp\Promise\FulfilledPromise;
16
use GuzzleHttp\RequestOptions;
17
use Nelexa\HttpClient\HttpClient;
18
use Nelexa\HttpClient\Options;
19
use Psr\Http\Message\ResponseInterface;
20
use Psr\Http\Message\StreamInterface;
21
use Psr\SimpleCache\CacheInterface;
22
use function GuzzleHttp\Psr7\build_query;
23
use function GuzzleHttp\Psr7\parse_query;
24
25
/**
26
 * Contains methods for extracting information about Android applications from the Google Play store.
27
 */
28
class GPlayApps
29
{
30
    /** @var string Default request locale. */
31
    public const DEFAULT_LOCALE = 'en_US';
32
33
    /** @var string Default request country. */
34
    public const DEFAULT_COUNTRY = 'us';
35
36
    /** @var string Google Play base url. */
37
    public const GOOGLE_PLAY_URL = 'https://play.google.com';
38
39
    /** @var string Google Play apps url. */
40
    public const GOOGLE_PLAY_APPS_URL = self::GOOGLE_PLAY_URL . '/store/apps';
41
42
    /** @var int Unlimit results. */
43
    public const UNLIMIT = -1;
44
45
    /** @internal */
46
    public const REQ_PARAM_LOCALE = 'hl';
47
48
    /** @internal */
49
    public const REQ_PARAM_COUNTRY = 'gl';
50
51
    /** @internal */
52
    public const REQ_PARAM_ID = 'id';
53
54
    /** @var int Limit of parallel HTTP requests */
55
    protected $concurrency = 4;
56
57
    /** @var string Locale (language) for HTTP requests to Google Play */
58
    protected $defaultLocale;
59
60
    /** @var string Country for HTTP requests to Google Play */
61
    protected $defaultCountry;
62
63
    /**
64
     * Creates an object to retrieve data about Android applications from the Google Play store.
65
     *
66
     * @param string $locale  locale (language) for HTTP requests to Google Play
67
     *                        or {@see GPlayApps::DEFAULT_LOCALE}
68
     * @param string $country country for HTTP requests to Google Play
69
     *                        or {@see GPlayApps::DEFAULT_COUNTRY}
70
     *
71
     * @see GPlayApps::DEFAULT_LOCALE Default request locale.
72
     * @see GPlayApps::DEFAULT_COUNTRY Default request country.
73
     */
74 70
    public function __construct(
75
        string $locale = self::DEFAULT_LOCALE,
76
        string $country = self::DEFAULT_COUNTRY
77
    ) {
78
        $this
79 70
            ->setDefaultLocale($locale)
80 70
            ->setDefaultCountry($country)
81
        ;
82 70
    }
83
84
    /**
85
     * Sets caching for HTTP requests.
86
     *
87
     * @param cacheInterface|null    $cache    PSR-16 Simple Cache instance
88
     * @param \DateInterval|int|null $cacheTtl TTL cached data
89
     *
90
     * @return GPlayApps returns the current class instance to allow method chaining
91
     */
92 17
    public function setCache(?CacheInterface $cache, $cacheTtl = null): self
93
    {
94 17
        $this->getHttpClient()->setCache($cache);
95 17
        $this->setCacheTtl($cacheTtl);
96
97 17
        return $this;
98
    }
99
100
    /**
101
     * Sets cache ttl.
102
     *
103
     * @param \DateInterval|int|null $cacheTtl TTL cached data
104
     *
105
     * @return GPlayApps returns the current class instance to allow method chaining
106
     */
107 17
    public function setCacheTtl($cacheTtl): self
108
    {
109 17
        $cacheTtl = $cacheTtl ?? \DateInterval::createFromDateString('5 min');
110 17
        $this->getHttpClient()->setCacheTtl($cacheTtl);
111
112 17
        return $this;
113
    }
114
115
    /**
116
     * Returns an instance of HTTP client.
117
     *
118
     * @return HttpClient http client
119
     */
120 58
    protected function getHttpClient(): HttpClient
121
    {
122 58
        static $httpClient;
123
124 58
        if ($httpClient === null) {
125 1
            $httpClient = new HttpClient(
126
                [
127 1
                    Options::TIMEOUT => 10.0,
128 1
                    Options::HEADERS => [
129
                        'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0',
130
                    ],
131
                ]
132
            );
133
        }
134
135 58
        return $httpClient;
136
    }
137
138
    /**
139
     * Sets the limit of concurrent HTTP requests.
140
     *
141
     * @param int $concurrency maximum number of concurrent HTTP requests
142
     *
143
     * @return GPlayApps returns the current class instance to allow method chaining
144
     */
145 4
    public function setConcurrency(int $concurrency): self
146
    {
147 4
        $this->concurrency = max(1, $concurrency);
148
149 4
        return $this;
150
    }
151
152
    /**
153
     * Sets proxy for outgoing HTTP requests.
154
     *
155
     * @param string|null $proxy Proxy url, ex. socks5://127.0.0.1:9050 or https://116.90.233.2:47348
156
     *
157
     * @return GPlayApps returns the current class instance to allow method chaining
158
     *
159
     * @see https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html Description of proxy URL formats in CURL.
160
     */
161
    public function setProxy(?string $proxy): self
162
    {
163
        $this->getHttpClient()->setProxy($proxy);
164
165
        return $this;
166
    }
167
168
    /**
169
     * Returns the full detail of an application.
170
     *
171
     * For information, you must specify the application ID (android package name).
172
     * The application ID can be viewed in the Google Play store:
173
     * `https://play.google.com/store/apps/details?id=XXXXXX` , where
174
     * XXXXXX is the application id.
175
     *
176
     * Or it can be found in the APK file.
177
     * ```shell
178
     * aapt dump badging file.apk | grep package | awk '{print $2}' | sed s/name=//g | sed s/\'//g
179
     * ```
180
     *
181
     * @param string|Model\AppId $appId google play app id (Android package name)
182
     *
183
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
184
     *
185
     * @return Model\AppInfo full detail of an application or exception
186
     *
187
     * @api
188
     */
189 8
    public function getAppInfo($appId): Model\AppInfo
190
    {
191 8
        return $this->getAppsInfo([$appId])[0];
192
    }
193
194
    /**
195
     * Returns the full detail of multiple applications.
196
     *
197
     * The keys of the returned array matches to the passed array.
198
     * HTTP requests are executed in parallel.
199
     *
200
     * @param string[]|Model\AppId[] $appIds array of application ids
201
     *
202
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
203
     *
204
     * @return Model\AppInfo[] an array of detailed information for each application
205
     *
206
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
207
     *
208
     * @api
209
     */
210 26
    public function getAppsInfo(array $appIds): array
211
    {
212 26
        if (empty($appIds)) {
213 1
            return [];
214
        }
215 25
        $urls = $this->createUrlListFromAppIds($appIds);
216
217
        try {
218 23
            return $this->getHttpClient()->requestAsyncPool(
219 23
                'GET',
220
                $urls,
221
                [
222 23
                    Options::HANDLER_RESPONSE => new Scraper\AppInfoScraper(),
223
                ],
224 23
                $this->concurrency
225
            );
226 2
        } catch (\Throwable $e) {
227 2
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
228
        }
229
    }
230
231
    /**
232
     * Returns an array of URLs for application ids.
233
     *
234
     * @param string[]|Model\AppId[] $appIds array of application ids
235
     *
236
     * @return string[] an array of URL
237
     */
238 26
    final protected function createUrlListFromAppIds(array $appIds): array
239
    {
240 26
        $urls = [];
241
242 26
        foreach ($appIds as $key => $appId) {
243 26
            $urls[$key] = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry)->getFullUrl();
244
        }
245
246 24
        return $urls;
247
    }
248
249
    /**
250
     * Returns the full details of an application in multiple languages.
251
     *
252
     * HTTP requests are executed in parallel.
253
     *
254
     * @param string|Model\AppId $appId   google Play app ID (Android package name)
255
     * @param string[]           $locales array of locales
256
     *
257
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
258
     *
259
     * @return Model\AppInfo[] An array of detailed information for each locale.
260
     *                         The array key is the locale.
261
     *
262
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
263
     *
264
     * @api
265
     */
266 15
    public function getAppInfoForLocales($appId, array $locales): array
267
    {
268 15
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
269 14
        $requests = [];
270
271 14
        foreach ($locales as $locale) {
272 14
            $requests[$locale] = new Model\AppId($appId->getId(), $locale, $appId->getCountry());
273
        }
274
275 14
        return $this->getAppsInfo($requests);
276
    }
277
278
    /**
279
     * Returns detailed application information for all available locales.
280
     *
281
     * Information is returned only for the description loaded by the developer.
282
     * All locales with automated translation from Google Translate will be ignored.
283
     * HTTP requests are executed in parallel.
284
     *
285
     * @param string|Model\AppId $appId application ID (Android package name) as
286
     *                                  a string or {@see Model\AppId} object
287
     *
288
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
289
     *
290
     * @return Model\AppInfo[] An array with detailed information about the application
291
     *                         on all available locales. The array key is the locale.
292
     *
293
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
294
     *
295
     * @api
296
     */
297 14
    public function getAppInfoForAvailableLocales($appId): array
298
    {
299 14
        $list = $this->getAppInfoForLocales($appId, Util\LocaleHelper::SUPPORTED_LOCALES);
300
301 13
        $preferredLocale = self::DEFAULT_LOCALE;
302
303 13
        foreach ($list as $app) {
304 13
            if ($app->isAutoTranslatedDescription()) {
305 12
                $preferredLocale = (string) $app->getTranslatedFromLocale();
306
                break;
307
            }
308
        }
309
310
        /**
311
         * @var Model\AppInfo[] $list
312
         */
313 13
        $list = array_filter(
314
            $list,
315 13
            static function (Model\AppInfo $app) {
316 13
                return !$app->isAutoTranslatedDescription();
317 13
            }
318
        );
319
320 13
        if (!isset($list[$preferredLocale])) {
321
            throw new \RuntimeException('No key ' . $preferredLocale);
322
        }
323 13
        $preferredApp = $list[$preferredLocale];
324 13
        $list = array_filter(
325
            $list,
326 13
            static function (Model\AppInfo $app, string $locale) use ($preferredApp, $list) {
327
                // deletes locales in which there is no translation added, but automatic translation by Google Translate is used.
328 13
                if ($preferredApp->getLocale() === $locale || !$preferredApp->equals($app)) {
329 13
                    if (($pos = strpos($locale, '_')) !== false) {
330 13
                        $rootLang = substr($locale, 0, $pos);
331 13
                        $rootLangLocale = Util\LocaleHelper::getNormalizeLocale($rootLang);
332
333
                        if (
334 13
                            $rootLangLocale !== $locale &&
335 12
                            isset($list[$rootLangLocale]) &&
336 12
                            $list[$rootLangLocale]->equals($app)
337
                        ) {
338
                            // delete duplicate data,
339
                            // for example, delete en_CA, en_IN, en_GB, en_ZA, if there is en_US and they are equals.
340 9
                            return false;
341
                        }
342
                    }
343
344 13
                    return true;
345
                }
346
347 7
                return false;
348 13
            },
349 13
            \ARRAY_FILTER_USE_BOTH
350
        );
351
352
        // sorting array keys; the first key is the preferred locale
353 13
        uksort(
354
            $list,
355 13
            static function (
356
                /** @noinspection PhpUnusedParameterInspection */
357
                string $a,
358
                string $b
359 13
            ) use ($preferredLocale) {
360 13
                return $b === $preferredLocale ? 1 : 0;
361 13
            }
362
        );
363
364 13
        return $list;
365
    }
366
367
    /**
368
     * Checks if the specified application exists in the Google Play store.
369
     *
370
     * @param string|Model\AppId $appId application ID (Android package name) as
371
     *                                  a string or {@see Model\AppId} object
372
     *
373
     * @return bool returns `true` if the application exists, or `false` if not
374
     *
375
     * @api
376
     */
377 5
    public function existsApp($appId): bool
378
    {
379 5
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
380
381
        try {
382 5
            return (bool) $this->getHttpClient()->request(
383 5
                'HEAD',
384 5
                $appId->getFullUrl(),
385
                [
386 5
                    RequestOptions::HTTP_ERRORS => false,
387 5
                    Options::HANDLER_RESPONSE => new Scraper\ExistsAppScraper(),
388
                ]
389
            );
390
        } catch (\Throwable $e) {
391
            return false;
392
        }
393
    }
394
395
    /**
396
     * Checks if the specified applications exist in the Google Play store.
397
     * HTTP requests are executed in parallel.
398
     *
399
     * @param string[]|Model\AppId[] $appIds Array of application identifiers.
400
     *                                       The keys of the returned array correspond to the transferred array.
401
     *
402
     * @throws Exception\GooglePlayException if an HTTP error other than 404 is received
403
     *
404
     * @return bool[] An array of information about the existence of each
405
     *                application in the store Google Play. The keys of the returned
406
     *                array matches to the passed array.
407
     *
408
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
409
     *
410
     * @api
411
     */
412 1
    public function existsApps(array $appIds): array
413
    {
414 1
        if (empty($appIds)) {
415
            return [];
416
        }
417 1
        $urls = $this->createUrlListFromAppIds($appIds);
418
419
        try {
420 1
            return $this->getHttpClient()->requestAsyncPool(
421 1
                'HEAD',
422
                $urls,
423
                [
424 1
                    RequestOptions::HTTP_ERRORS => false,
425 1
                    Options::HANDLER_RESPONSE => new Scraper\ExistsAppScraper(),
426
                ],
427 1
                $this->concurrency
428
            );
429
        } catch (\Throwable $e) {
430
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
431
        }
432
    }
433
434
    /**
435
     * Returns reviews of the Android app in the Google Play store.
436
     *
437
     * Getting a lot of reviews can take a lot of time.
438
     *
439
     * @param string|Model\AppId $appId application ID (Android package name) as
440
     *                                  a string or {@see Model\AppId} object
441
     * @param int                $limit Maximum number of reviews. To extract all
442
     *                                  reviews, use {@see GPlayApps::UNLIMIT}.
443
     * @param Enum\SortEnum|null $sort  Sort reviews of the application.
444
     *                                  If null, then sort by the newest reviews.
445
     *
446
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
447
     *
448
     * @return Model\Review[] app reviews
449
     *
450
     * @see Enum\SortEnum Contains all valid values for the "sort" parameter.
451
     * @see GPlayApps::UNLIMIT Limit for all available results.
452
     *
453
     * @api
454
     */
455 1
    public function getReviews($appId, int $limit = 100, ?Enum\SortEnum $sort = null): array
456
    {
457 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
458 1
        $sort = $sort ?? Enum\SortEnum::NEWEST();
459
460 1
        $allCount = 0;
461 1
        $token = null;
462 1
        $allReviews = [];
463
464 1
        $cacheTtl = $sort === Enum\SortEnum::NEWEST() ?
465 1
            \DateInterval::createFromDateString('1 min') :
466 1
            \DateInterval::createFromDateString('1 hour');
467
468
        try {
469
            do {
470 1
                $count = $limit === self::UNLIMIT ?
471
                    Scraper\PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE :
472 1
                    min(Scraper\PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE, max($limit - $allCount, 1));
473
474 1
                $request = Scraper\PlayStoreUiRequest::getReviewsRequest($appId, $count, $sort, $token);
475
476 1
                [$reviews, $token] = $this->getHttpClient()->send(
477
                    $request,
478
                    [
479 1
                        Options::CACHE_TTL => $cacheTtl,
480 1
                        Options::HANDLER_RESPONSE => new Scraper\ReviewsScraper($appId),
481
                    ]
482
                );
483 1
                $allCount += \count($reviews);
484 1
                $allReviews[] = $reviews;
485 1
            } while ($token !== null && ($limit === self::UNLIMIT || $allCount < $limit));
486
        } catch (\Throwable $e) {
487
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
488
        }
489
490 1
        return empty($allReviews) ? $allReviews : array_merge(...$allReviews);
491
    }
492
493
    /**
494
     * Returns review of the Android app in the Google Play store by review id.
495
     *
496
     * @param string|Model\AppId $appId    application ID (Android package name) as
497
     *                                     a string or {@see Model\AppId} object
498
     * @param string             $reviewId review id
499
     *
500
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
501
     *
502
     * @return Model\Review app review
503
     */
504 1
    public function getReviewById($appId, string $reviewId): Model\Review
505
    {
506 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
507
508
        try {
509
            /** @var Model\Review $review */
510 1
            $review = $this->getHttpClient()->request(
511 1
                'GET',
512 1
                self::GOOGLE_PLAY_APPS_URL . '/details',
513
                [
514 1
                    RequestOptions::QUERY => [
515 1
                        self::REQ_PARAM_ID => $appId->getId(),
516 1
                        self::REQ_PARAM_LOCALE => $appId->getLocale(),
517 1
                        self::REQ_PARAM_COUNTRY => $appId->getCountry(),
518 1
                        'reviewId' => $reviewId,
519
                    ],
520 1
                    Options::HANDLER_RESPONSE => new Scraper\AppSpecificReviewScraper($appId),
521
                ]
522
            );
523
        } catch (\Throwable $e) {
524
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
525
        }
526
527 1
        return $review;
528
    }
529
530
    /**
531
     * Returns a list of permissions for the application.
532
     *
533
     * @param string|Model\AppId $appId application ID (Android package name) as
534
     *                                  a string or {@see Model\AppId} object
535
     *
536
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
537
     *
538
     * @return Model\Permission[] an array of permissions for the application
539
     *
540
     * @api
541
     */
542 1
    public function getPermissions($appId): array
543
    {
544 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
545
546
        try {
547 1
            $request = Scraper\PlayStoreUiRequest::getPermissionsRequest($appId);
548
549
            /** @var Model\Permission[] $permissions */
550 1
            $permissions = $this->getHttpClient()->send(
551
                $request,
552
                [
553 1
                    Options::HANDLER_RESPONSE => new Scraper\PermissionScraper(),
554
                ]
555
            );
556
        } catch (\Throwable $e) {
557
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
558
        }
559
560 1
        return $permissions;
561
    }
562
563
    /**
564
     * Returns an array of application categories from the Google Play store.
565
     *
566
     * @throws Exception\GooglePlayException if HTTP error is received
567
     *
568
     * @return Model\Category[] array of application categories
569
     *
570
     * @api
571
     */
572 1
    public function getCategories(): array
573
    {
574 1
        $url = self::GOOGLE_PLAY_APPS_URL;
575
576
        try {
577
            /** @var Model\Category[] $categories */
578 1
            $categories = $this->getHttpClient()->request(
579 1
                'GET',
580
                $url,
581
                [
582 1
                    RequestOptions::QUERY => [
583 1
                        self::REQ_PARAM_LOCALE => $this->defaultLocale,
584
                    ],
585 1
                    Options::HANDLER_RESPONSE => new Scraper\CategoriesScraper(),
586
                ]
587
            );
588
        } catch (\Throwable $e) {
589
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
590
        }
591
592 1
        return $categories;
593
    }
594
595
    /**
596
     * Returns an array of application categories from the Google Play store for the specified locales.
597
     *
598
     * HTTP requests are executed in parallel.
599
     *
600
     * @param string[] $locales array of locales
601
     *
602
     * @throws Exception\GooglePlayException if HTTP error is received
603
     *
604
     * @return Model\Category[][] array of application categories by locale
605
     *
606
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
607
     *
608
     * @api
609
     */
610 2
    public function getCategoriesForLocales(array $locales): array
611
    {
612 2
        if (empty($locales)) {
613
            return [];
614
        }
615 2
        $locales = Util\LocaleHelper::getNormalizeLocales($locales);
616
617 2
        $urls = [];
618 2
        $url = self::GOOGLE_PLAY_APPS_URL;
619
620 2
        foreach ($locales as $locale) {
621 2
            $urls[$locale] = $url . '?' . http_build_query(
622
                [
623 2
                    self::REQ_PARAM_LOCALE => $locale,
624
                ]
625
            );
626
        }
627
628
        try {
629 2
            return $this->getHttpClient()->requestAsyncPool(
630 2
                'GET',
631
                $urls,
632
                [
633 2
                    Options::HANDLER_RESPONSE => new Scraper\CategoriesScraper(),
634
                ],
635 2
                $this->concurrency
636
            );
637
        } catch (\Throwable $e) {
638
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
639
        }
640
    }
641
642
    /**
643
     * Returns an array of categories from the Google Play store for all available locales.
644
     *
645
     * @throws Exception\GooglePlayException if HTTP error is received
646
     *
647
     * @return Model\Category[][] array of application categories by locale
648
     *
649
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
650
     *
651
     * @api
652
     */
653 1
    public function getCategoriesForAvailableLocales(): array
654
    {
655 1
        return $this->getCategoriesForLocales(Util\LocaleHelper::SUPPORTED_LOCALES);
656
    }
657
658
    /**
659
     * Returns information about the developer: name, icon, cover, description and website address.
660
     *
661
     * @param string|Model\Developer|Model\App $developerId developer id as
662
     *                                                      string, {@see Model\Developer}
663
     *                                                      or {@see Model\App} object
664
     *
665
     * @throws Exception\GooglePlayException if HTTP error is received
666
     *
667
     * @return Model\Developer information about the application developer
668
     *
669
     * @see GPlayApps::getDeveloperInfoForLocales() Returns information about the developer for the locale array.
670
     *
671
     * @api
672
     */
673 5
    public function getDeveloperInfo($developerId): Model\Developer
674
    {
675 5
        $developerId = Util\Caster::castToDeveloperId($developerId);
676
677 5
        if (!is_numeric($developerId)) {
678 3
            throw new Exception\GooglePlayException(
679 3
                sprintf(
680 3
                    'Developer "%s" does not have a personalized page on Google Play.',
681
                    $developerId
682
                )
683
            );
684
        }
685
686 2
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
687
688
        try {
689
            /** @var Model\Developer $developer */
690 2
            $developer = $this->getHttpClient()->request(
691 2
                'GET',
692
                $url,
693
                [
694 2
                    RequestOptions::QUERY => [
695 2
                        self::REQ_PARAM_ID => $developerId,
696 2
                        self::REQ_PARAM_LOCALE => $this->defaultLocale,
697
                    ],
698 2
                    Options::HANDLER_RESPONSE => new Scraper\DeveloperInfoScraper(),
699
                ]
700
            );
701 1
        } catch (\Throwable $e) {
702 1
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
703
        }
704
705 1
        return $developer;
706
    }
707
708
    /**
709
     * Returns information about the developer for the specified locales.
710
     *
711
     * @param string|Model\Developer|Model\App $developerId developer id as
712
     *                                                      string, {@see Model\Developer}
713
     *                                                      or {@see Model\App} object
714
     * @param string[]                         $locales     array of locales
715
     *
716
     * @throws Exception\GooglePlayException if HTTP error is received
717
     *
718
     * @return Model\Developer[] an array with information about the application developer
719
     *                           for each requested locale
720
     *
721
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
722
     * @see GPlayApps::getDeveloperInfo() Returns information about the developer: name,
723
     *     icon, cover, description and website address.
724
     *
725
     * @api
726
     */
727 1
    public function getDeveloperInfoForLocales($developerId, array $locales = []): array
728
    {
729 1
        if (empty($locales)) {
730
            return [];
731
        }
732 1
        $locales = Util\LocaleHelper::getNormalizeLocales($locales);
733
734 1
        $id = Util\Caster::castToDeveloperId($developerId);
735
736 1
        if (!is_numeric($id)) {
737
            throw new Exception\GooglePlayException(
738
                sprintf(
739
                    'Developer "%s" does not have a personalized page on Google Play.',
740
                    $id
741
                )
742
            );
743
        }
744
745 1
        $urls = [];
746 1
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
747
748 1
        foreach ($locales as $locale) {
749 1
            $urls[$locale] = $url . '?' . http_build_query(
750
                [
751 1
                    self::REQ_PARAM_ID => $id,
752 1
                    self::REQ_PARAM_LOCALE => $locale,
753
                ]
754
            );
755
        }
756
757
        try {
758 1
            return $this->getHttpClient()->requestAsyncPool(
759 1
                'GET',
760
                $urls,
761
                [
762 1
                    Options::HANDLER_RESPONSE => new Scraper\DeveloperInfoScraper(),
763
                ],
764 1
                $this->concurrency
765
            );
766
        } catch (\Throwable $e) {
767
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
768
        }
769
    }
770
771
    /**
772
     * Returns an array of applications from the Google Play store by developer id.
773
     *
774
     * @param string|Model\Developer|Model\App $developerId developer id as
775
     *                                                      string, {@see Model\Developer}
776
     *                                                      or {@see Model\App} object
777
     *
778
     * @throws Exception\GooglePlayException if HTTP error is received
779
     *
780
     * @return Model\App[] an array of applications with basic information
781
     *
782
     * @api
783
     */
784 1
    public function getDeveloperApps($developerId): array
785
    {
786 1
        $developerId = Util\Caster::castToDeveloperId($developerId);
787
788
        $query = [
789 1
            self::REQ_PARAM_ID => $developerId,
790 1
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
791 1
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
792
        ];
793
794 1
        if (is_numeric($developerId)) {
795 1
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/dev?' . http_build_query($query);
796
797
            try {
798
                /**
799
                 * @var string|null $developerUrl
800
                 */
801 1
                $developerUrl = $this->getHttpClient()->request(
802 1
                    'GET',
803
                    $developerUrl,
804
                    [
805 1
                        Options::HANDLER_RESPONSE => new Scraper\FindDevAppsUrlScraper(),
806
                    ]
807
                );
808
809 1
                if ($developerUrl === null) {
810
                    return [];
811
                }
812 1
                $developerUrl .= '&' . self::REQ_PARAM_LOCALE . '=' . urlencode($this->defaultLocale) .
813 1
                    '&' . self::REQ_PARAM_COUNTRY . '=' . urlencode($this->defaultCountry);
814
            } catch (\Throwable $e) {
815
                throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
816
            }
817
        } else {
818 1
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/developer?' . http_build_query($query);
819
        }
820
821 1
        return $this->fetchAppsFromClusterPage(
822 1
            $developerUrl,
823 1
            $this->defaultLocale,
824 1
            $this->defaultCountry,
825 1
            self::UNLIMIT
826
        );
827
    }
828
829
    /**
830
     * Returns a list of applications with basic information.
831
     *
832
     * @param string $clusterPageUrl cluster page URL
833
     * @param string $locale         locale
834
     * @param string $country        country
835
     * @param int    $limit          Maximum number of applications. To extract all
836
     *                               applications, use {@see GPlayApps::UNLIMIT}.
837
     *
838
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
839
     *
840
     * @return Model\App[] array of applications with basic information about them
841
     *
842
     * @see GPlayApps::UNLIMIT Limit for all available results.
843
     */
844 17
    protected function fetchAppsFromClusterPage(
845
        string $clusterPageUrl,
846
        string $locale,
847
        string $country,
848
        int $limit
849
    ): array {
850 17
        if ($limit < self::UNLIMIT || $limit === 0) {
851
            throw new \InvalidArgumentException(sprintf('Invalid limit: %d', $limit));
852
        }
853
854 17
        $clusterPageComponents = parse_url($clusterPageUrl);
855 17
        $query = parse_query($clusterPageComponents['query'] ?? '');
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Psr7\parse_query() has been deprecated: parse_query will be removed in guzzlehttp/psr7:2.0. Use Query::parse instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

855
        $query = /** @scrutinizer ignore-deprecated */ parse_query($clusterPageComponents['query'] ?? '');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
856 17
        $query[self::REQ_PARAM_LOCALE] = $locale;
857 17
        $query[self::REQ_PARAM_COUNTRY] = $country;
858
859 17
        $clusterPageUrl = $clusterPageComponents['scheme'] . '://' .
860 17
            $clusterPageComponents['host'] .
861 17
            $clusterPageComponents['path'] .
862 17
            '?' . build_query($query);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Psr7\build_query() has been deprecated: build_query will be removed in guzzlehttp/psr7:2.0. Use Query::build instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

862
            '?' . /** @scrutinizer ignore-deprecated */ build_query($query);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
863
864
        try {
865 17
            [$apps, $token] = $this->getHttpClient()->request(
866 17
                'GET',
867
                $clusterPageUrl,
868
                [
869 17
                    Options::HANDLER_RESPONSE => new Scraper\ClusterAppsScraper(),
870
                ]
871
            );
872
873 17
            $allCount = \count($apps);
874 17
            $allApps = [$apps];
875
876 17
            while ($token !== null && ($limit === self::UNLIMIT || $allCount < $limit)) {
877 17
                $count = $limit === self::UNLIMIT ?
878 16
                    Scraper\PlayStoreUiRequest::LIMIT_APPS_ON_PAGE :
879 17
                    min(Scraper\PlayStoreUiRequest::LIMIT_APPS_ON_PAGE, max($limit - $allCount, 1));
880
881 17
                $request = Scraper\PlayStoreUiRequest::getAppsRequest($locale, $country, $count, $token);
882
883 17
                [$apps, $token] = $this->getHttpClient()->send(
884
                    $request,
885
                    [
886 17
                        Options::HANDLER_RESPONSE => new Scraper\PlayStoreUiAppsScraper(),
887
                    ]
888
                );
889 17
                $allCount += \count($apps);
890 17
                $allApps[] = $apps;
891
            }
892
        } catch (\Throwable $e) {
893
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
894
        }
895
896 17
        if (empty($allApps)) {
897
            return $allApps;
898
        }
899 17
        $allApps = array_merge(...$allApps);
900
901 17
        if ($limit !== self::UNLIMIT) {
902 1
            $allApps = \array_slice($allApps, 0, $limit);
903
        }
904
905 17
        return $allApps;
906
    }
907
908
    /**
909
     * Returns an array of similar applications with basic information about
910
     * them in the Google Play store.
911
     *
912
     * @param string|Model\AppId $appId application ID (Android package name)
913
     *                                  as a string or {@see Model\AppId} object
914
     * @param int                $limit The maximum number of similar applications.
915
     *                                  To extract all similar applications,
916
     *                                  use {@see GPlayApps::UNLIMIT}.
917
     *
918
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
919
     *
920
     * @return Model\App[] an array of applications with basic information about them
921
     *
922
     * @see GPlayApps::UNLIMIT Limit for all available results.
923
     *
924
     * @api
925
     */
926 1
    public function getSimilarApps($appId, int $limit = 50): array
927
    {
928 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
929
930
        try {
931
            /**
932
             * @var string|null $similarAppsUrl
933
             */
934 1
            $similarAppsUrl = $this->getHttpClient()->request(
935 1
                'GET',
936 1
                $appId->getFullUrl(),
937
                [
938 1
                    Options::HANDLER_RESPONSE => new Scraper\FindSimilarAppsUrlScraper($appId),
939
                ]
940
            );
941
942 1
            if ($similarAppsUrl === null) {
943
                return [];
944
            }
945
946 1
            return $this->fetchAppsFromClusterPage(
947 1
                $similarAppsUrl,
948 1
                $appId->getLocale(),
949 1
                $appId->getCountry(),
950
                $limit
951
            );
952
        } catch (\Throwable $e) {
953
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
954
        }
955
    }
956
957
    /**
958
     * Returns the Google Play search suggests.
959
     *
960
     * @param string $query search query
961
     *
962
     * @throws Exception\GooglePlayException if HTTP error is received
963
     *
964
     * @return string[] array containing search suggestions
965
     *
966
     * @api
967
     */
968 1
    public function getSearchSuggestions(string $query): array
969
    {
970 1
        $query = trim($query);
971
972 1
        if ($query === '') {
973
            return [];
974
        }
975
976 1
        $url = 'https://market.android.com/suggest/SuggRequest';
977
978
        try {
979
            /** @var string[] $suggestions */
980 1
            $suggestions = $this->getHttpClient()->request(
981 1
                'GET',
982
                $url,
983
                [
984 1
                    RequestOptions::QUERY => [
985 1
                        'json' => 1,
986 1
                        'c' => 3,
987 1
                        'query' => $query,
988 1
                        self::REQ_PARAM_LOCALE => $this->defaultLocale,
989 1
                        self::REQ_PARAM_COUNTRY => $this->defaultCountry,
990
                    ],
991 1
                    Options::HANDLER_RESPONSE => new Scraper\SuggestScraper(),
992
                ]
993
            );
994
        } catch (\Throwable $e) {
995
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
996
        }
997
998 1
        return $suggestions;
999
    }
1000
1001
    /**
1002
     * Returns a list of applications from the Google Play store for a search query.
1003
     *
1004
     * @param string              $query search query
1005
     * @param int                 $limit the limit on the number of search results
1006
     * @param Enum\PriceEnum|null $price price category or `null`
1007
     *
1008
     * @throws Exception\GooglePlayException if HTTP error is received
1009
     *
1010
     * @return Model\App[] an array of applications with basic information
1011
     *
1012
     * @see Enum\PriceEnum Contains all valid values for the "price" parameter.
1013
     * @see GPlayApps::UNLIMIT Limit for all available results.
1014
     *
1015
     * @api
1016
     */
1017 1
    public function search(string $query, int $limit = 50, ?Enum\PriceEnum $price = null): array
1018
    {
1019 1
        $query = trim($query);
1020
1021 1
        if (empty($query)) {
1022
            throw new \InvalidArgumentException('Search query missing');
1023
        }
1024 1
        $price = $price ?? Enum\PriceEnum::ALL();
1025
1026
        $params = [
1027 1
            'c' => 'apps',
1028 1
            'q' => $query,
1029 1
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
1030 1
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
1031 1
            'price' => $price->value(),
1032
        ];
1033 1
        $clusterPageUrl = self::GOOGLE_PLAY_URL . '/store/search?' . http_build_query($params);
1034
1035 1
        return $this->fetchAppsFromClusterPage(
1036 1
            $clusterPageUrl,
1037 1
            $this->defaultLocale,
1038 1
            $this->defaultCountry,
1039
            $limit
1040
        );
1041
    }
1042
1043
    /**
1044
     * Returns an array of applications from the Google Play store for the specified category.
1045
     *
1046
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1047
     *                                                               string, {@see Model\Category},
1048
     *                                                               {@see Enum\CategoryEnum} or
1049
     *                                                               `null` for all categories
1050
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1051
     * @param int                                          $limit    limit on the number of results
1052
     *                                                               or {@see GPlayApps::UNLIMIT}
1053
     *                                                               for no limit
1054
     *
1055
     * @return Model\App[] an array of applications with basic information
1056
     *
1057
     * @see GPlayApps::UNLIMIT Limit for all available results.
1058
     *
1059
     * @api
1060
     */
1061 5
    public function getListApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1062
    {
1063 5
        return $this->fetchAppsFromClusterPages($category, $age, null, $limit);
1064
    }
1065
1066
    /**
1067
     * Returns an array of **top apps** from the Google Play store for the specified category.
1068
     *
1069
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1070
     *                                                               string, {@see Model\Category},
1071
     *                                                               {@see Enum\CategoryEnum} or
1072
     *                                                               `null` for all categories
1073
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1074
     * @param int                                          $limit    limit on the number of results
1075
     *                                                               or {@see GPlayApps::UNLIMIT}
1076
     *                                                               for no limit
1077
     *
1078
     * @return Model\App[] an array of applications with basic information
1079
     *
1080
     * @see GPlayApps::UNLIMIT Limit for all available results.
1081
     *
1082
     * @api
1083
     */
1084 6
    public function getTopApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1085
    {
1086 6
        return $this->fetchAppsFromClusterPages($category, $age, 'top', $limit);
1087
    }
1088
1089
    /**
1090
     * Returns an array of **new apps** from the Google Play store for the specified category.
1091
     *
1092
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1093
     *                                                               string, {@see Model\Category},
1094
     *                                                               {@see Enum\CategoryEnum} or
1095
     *                                                               `null` for all categories
1096
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1097
     * @param int                                          $limit    limit on the number of results
1098
     *                                                               or {@see GPlayApps::UNLIMIT}
1099
     *                                                               for no limit
1100
     *
1101
     * @return Model\App[] an array of applications with basic information
1102
     *
1103
     * @see GPlayApps::UNLIMIT Limit for all available results.
1104
     *
1105
     * @api
1106
     */
1107 5
    public function getNewApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1108
    {
1109 5
        return $this->fetchAppsFromClusterPages($category, $age, 'new', $limit);
1110
    }
1111
1112
    /**
1113
     * @param string|Model\Category|Enum\CategoryEnum|null $category
1114
     * @param Enum\AgeEnum|null                            $age
1115
     * @param string|null                                  $path
1116
     * @param int                                          $limit
1117
     *
1118
     * @return Model\App[]
1119
     */
1120 16
    protected function fetchAppsFromClusterPages($category, ?Enum\AgeEnum $age, ?string $path, int $limit): array
1121
    {
1122 16
        if ($limit === 0 || $limit < self::UNLIMIT) {
1123
            throw new \InvalidArgumentException('Negative limit');
1124
        }
1125
1126
        $queryParams = [
1127 16
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
1128 16
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
1129
        ];
1130
1131 16
        if ($age !== null) {
1132 1
            $queryParams['age'] = $age->value();
1133
        }
1134
1135 16
        $url = self::GOOGLE_PLAY_APPS_URL;
1136
1137 16
        if ($path !== null) {
1138 11
            $url .= '/' . $path;
1139
        }
1140
1141 16
        if ($category !== null) {
1142 12
            $url .= '/category/' . Util\Caster::castToCategoryId($category);
1143
        }
1144 16
        $url .= '?' . http_build_query($queryParams);
1145
1146
        /**
1147
         * @var array $categoryClusterPages = [[
1148
         *            "name" => "Top Free Games",
1149
         *            "url" => "https://play.google.com/store/apps/store/apps/collection/cluster?clp=......"
1150
         *            ]]
1151
         */
1152 16
        $categoryClusterPages = $this->getHttpClient()->request(
1153 16
            'GET',
1154
            $url,
1155
            [
1156 16
                Options::HANDLER_RESPONSE => new Scraper\ClusterPagesFromListAppsScraper(),
1157
            ]
1158
        );
1159
1160 16
        if (empty($categoryClusterPages)) {
1161 2
            return [];
1162
        }
1163
1164 14
        $iterator = new \ArrayIterator($categoryClusterPages);
1165 14
        $results = [];
1166
1167
        do {
1168 14
            $clusterPage = $iterator->current();
1169 14
            $clusterUrl = $clusterPage['url'];
1170
1171
            try {
1172 14
                $apps = $this->fetchAppsFromClusterPage($clusterUrl, $this->defaultLocale, $this->defaultCountry, self::UNLIMIT);
1173
1174 14
                foreach ($apps as $app) {
1175 14
                    if (!isset($results[$app->getId()])) {
1176 14
                        $results[$app->getId()] = $app;
1177
                    }
1178
                }
1179
            } catch (\Throwable $e) {
1180
                // ignore exception
1181
            }
1182
1183 14
            if ($limit !== self::UNLIMIT && \count($results) >= $limit) {
1184 1
                $results = \array_slice($results, 0, $limit);
1185 1
                break;
1186
            }
1187
1188 13
            $iterator->next();
1189 13
        } while ($iterator->valid());
1190
1191 14
        return $results;
1192
    }
1193
1194
    /**
1195
     * Asynchronously saves images from googleusercontent.com and similar URLs to disk.
1196
     *
1197
     * Before use, you can set the parameters of the width-height of images.
1198
     *
1199
     * Example:
1200
     * ```php
1201
     * $gplay->saveGoogleImages(
1202
     *     $images,
1203
     *     static function (\Nelexa\GPlay\Model\GoogleImage $image): string {
1204
     *         $hash = $image->getHashUrl($hashAlgo = 'md5', $parts = 2, $partLength = 2);
1205
     *         return 'path/to/screenshots/' . $hash . '.{ext}';
1206
     *     },
1207
     *     $overwrite = false
1208
     * );
1209
     * ```
1210
     *
1211
     * @param Model\GoogleImage[] $images           array of {@see Model\GoogleImage} objects
1212
     * @param callable            $destPathCallback The function to which the
1213
     *                                              {@see Model\GoogleImage} object is
1214
     *                                              passed and you must return the full
1215
     *                                              output. path to save this file.
1216
     * @param bool                $overwrite        overwrite files if exists
1217
     *
1218
     * @return Model\ImageInfo[] returns an array with information about saved images
1219
     *
1220
     * @see Model\GoogleImage Contains a link to the image, allows you to customize its size and download it.
1221
     * @see Model\ImageInfo Contains information about the image.
1222
     *
1223
     * @api
1224
     */
1225 2
    public function saveGoogleImages(
1226
        array $images,
1227
        callable $destPathCallback,
1228
        bool $overwrite = false
1229
    ): array {
1230
        /** @var array<string, StreamInterface> $mapping */
1231 2
        $mapping = [];
1232
1233 2
        foreach ($images as $image) {
1234 2
            if (!$image instanceof Model\GoogleImage) {
1235
                throw new \InvalidArgumentException(
1236
                    'An array of ' . Model\GoogleImage::class . ' objects is expected.'
1237
                );
1238
            }
1239 2
            $destPath = $destPathCallback($image);
1240 2
            $url = $image->getUrl();
1241 2
            $mapping[$url] = new Util\LazyStream($destPath, 'w+b');
1242
        }
1243
1244 2
        $httpClient = $this->getHttpClient();
1245 2
        $promises = (static function () use ($mapping, $overwrite, $httpClient) {
1246 2
            foreach ($mapping as $url => $stream) {
1247 2
                $destPath = $stream->getFilename();
1248 2
                $dynamicPath = strpos($destPath, '{url}') !== false;
1249
1250 2
                if (!$overwrite && !$dynamicPath && is_file($destPath)) {
1251
                    yield $url => new FulfilledPromise($url);
1252
                } else {
1253
                    yield $url => $httpClient
1254 2
                        ->requestAsync(
1255 2
                            'GET',
1256
                            $url,
1257
                            [
1258 2
                                RequestOptions::COOKIES => null,
1259 2
                                RequestOptions::SINK => $stream,
1260 2
                                RequestOptions::HTTP_ERRORS => true,
1261 2
                                RequestOptions::ON_HEADERS => static function (ResponseInterface $response) use (
1262 2
                                    $url,
1263 2
                                    $stream
1264
                                ): void {
1265 2
                                    Model\GoogleImage::onHeaders($response, $url, $stream);
1266 2
                                },
1267
                            ]
1268
                        )
1269 2
                        ->then(
1270 2
                            static function (
1271
                                /** @noinspection PhpUnusedParameterInspection */
1272
                                ResponseInterface $response
1273 2
                            ) use ($url) {
1274 1
                                return $url;
1275 2
                            }
1276
                        )
1277
                    ;
1278
                }
1279
            }
1280 2
        })();
1281
1282
        /**
1283
         * @var Model\ImageInfo[] $imageInfoList
1284
         */
1285 2
        $imageInfoList = [];
1286 2
        (new EachPromise(
1287
            $promises,
1288
            [
1289 2
                'concurrency' => $this->concurrency,
1290 2
                'fulfilled' => static function (string $url) use (&$imageInfoList, $mapping): void {
1291 1
                    $imageInfoList[] = new Model\ImageInfo($url, $mapping[$url]->getFilename());
1292 2
                },
1293 2
                'rejected' => static function (\Throwable $reason, string $exceptionUrl) use ($mapping): void {
1294 1
                    foreach ($mapping as $destPath => $url) {
1295 1
                        if (is_file($destPath)) {
1296
                            unlink($destPath);
1297
                        }
1298
                    }
1299
1300 1
                    throw (new Exception\GooglePlayException(
1301 1
                        $reason->getMessage(),
1302 1
                        $reason->getCode(),
1303
                        $reason
1304 1
                    ))->setUrl(
1305 1
                        $exceptionUrl
1306
                    );
1307 2
                },
1308
            ]
1309 2
        ))->promise()->wait();
1310
1311 1
        return $imageInfoList;
1312
    }
1313
1314
    /**
1315
     * Returns the locale (language) of the requests.
1316
     *
1317
     * @return string locale (language) for HTTP requests to Google Play
1318
     */
1319 5
    public function getDefaultLocale(): string
1320
    {
1321 5
        return $this->defaultLocale;
1322
    }
1323
1324
    /**
1325
     * Sets the locale (language) of requests.
1326
     *
1327
     * @param string $defaultLocale locale (language) for HTTP requests to Google Play
1328
     *
1329
     * @return GPlayApps returns the current class instance to allow method chaining
1330
     */
1331 70
    public function setDefaultLocale(string $defaultLocale): self
1332
    {
1333 70
        $this->defaultLocale = Util\LocaleHelper::getNormalizeLocale($defaultLocale);
1334
1335 70
        return $this;
1336
    }
1337
1338
    /**
1339
     * Returns the country of the requests.
1340
     *
1341
     * @return string country for HTTP requests to Google Play
1342
     */
1343 5
    public function getDefaultCountry(): string
1344
    {
1345 5
        return $this->defaultCountry;
1346
    }
1347
1348
    /**
1349
     * Sets the country of requests.
1350
     *
1351
     * @param string $defaultCountry country for HTTP requests to Google Play
1352
     *
1353
     * @return GPlayApps returns the current class instance to allow method chaining
1354
     */
1355 70
    public function setDefaultCountry(string $defaultCountry): self
1356
    {
1357 70
        $this->defaultCountry = !empty($defaultCountry) ?
1358 70
            $defaultCountry :
1359 2
            self::DEFAULT_COUNTRY;
1360
1361 70
        return $this;
1362
    }
1363
1364
    /**
1365
     * Sets the number of seconds to wait when trying to connect to the server.
1366
     *
1367
     * @param float $connectTimeout Connection timeout in seconds, for example 3.14. Use 0 to wait indefinitely.
1368
     *
1369
     * @return GPlayApps returns the current class instance to allow method chaining
1370
     */
1371
    public function setConnectTimeout(float $connectTimeout): self
1372
    {
1373
        $this->getHttpClient()->setConnectTimeout($connectTimeout);
1374
1375
        return $this;
1376
    }
1377
1378
    /**
1379
     * Sets the timeout of the request in second.
1380
     *
1381
     * @param float $timeout Waiting timeout in seconds, for example 3.14. Use 0 to wait indefinitely.
1382
     *
1383
     * @return GPlayApps returns the current class instance to allow method chaining
1384
     */
1385
    public function setTimeout(float $timeout): self
1386
    {
1387
        $this->getHttpClient()->setTimeout($timeout);
1388
1389
        return $this;
1390
    }
1391
}
1392