Passed
Pull Request — master (#13)
by Alexey
03:59
created

GPlayApps::getSearchSuggestions()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.243

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 23
ccs 7
cts 10
cp 0.7
rs 9.9
cc 3
nc 4
nop 1
crap 3.243
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\Psr7\Query;
17
use GuzzleHttp\RequestOptions;
18
use Nelexa\HttpClient\HttpClient;
19
use Nelexa\HttpClient\Options;
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\SimpleCache\CacheInterface;
22
23
/**
24
 * Contains methods for extracting information about Android applications from the Google Play store.
25
 */
26
class GPlayApps
27
{
28
    /** @var string Default request locale. */
29
    public const DEFAULT_LOCALE = 'en_US';
30
31
    /** @var string Default request country. */
32
    public const DEFAULT_COUNTRY = 'us';
33
34
    /** @var string Google Play base url. */
35
    public const GOOGLE_PLAY_URL = 'https://play.google.com';
36
37
    /** @var string Google Play apps url. */
38
    public const GOOGLE_PLAY_APPS_URL = self::GOOGLE_PLAY_URL . '/store/apps';
39
40
    /** @var int Unlimit results. */
41
    public const UNLIMIT = -1;
42
43
    /** @internal */
44
    public const REQ_PARAM_LOCALE = 'hl';
45
46
    /** @internal */
47
    public const REQ_PARAM_COUNTRY = 'gl';
48
49
    /** @internal */
50
    public const REQ_PARAM_ID = 'id';
51
52
    /** @var int Limit of parallel HTTP requests */
53
    protected $concurrency = 4;
54
55
    /** @var string Locale (language) for HTTP requests to Google Play */
56
    protected $defaultLocale;
57
58
    /** @var string Country for HTTP requests to Google Play */
59
    protected $defaultCountry;
60
61
    /**
62
     * Creates an object to retrieve data about Android applications from the Google Play store.
63
     *
64
     * @param string $locale  locale (language) for HTTP requests to Google Play
65
     *                        or {@see GPlayApps::DEFAULT_LOCALE}
66
     * @param string $country country for HTTP requests to Google Play
67
     *                        or {@see GPlayApps::DEFAULT_COUNTRY}
68
     *
69
     * @see GPlayApps::DEFAULT_LOCALE Default request locale.
70
     * @see GPlayApps::DEFAULT_COUNTRY Default request country.
71
     */
72 71
    public function __construct(
73
        string $locale = self::DEFAULT_LOCALE,
74
        string $country = self::DEFAULT_COUNTRY
75
    ) {
76
        $this
77 71
            ->setDefaultLocale($locale)
78 71
            ->setDefaultCountry($country)
79
        ;
80 71
    }
81
82
    /**
83
     * Sets caching for HTTP requests.
84
     *
85
     * @param cacheInterface|null    $cache    PSR-16 Simple Cache instance
86
     * @param \DateInterval|int|null $cacheTtl TTL cached data
87
     *
88
     * @return GPlayApps returns the current class instance to allow method chaining
89
     */
90 17
    public function setCache(?CacheInterface $cache, $cacheTtl = null): self
91
    {
92 17
        $this->getHttpClient()->setCache($cache);
93 17
        $this->setCacheTtl($cacheTtl);
94
95 17
        return $this;
96
    }
97
98
    /**
99
     * Sets cache ttl.
100
     *
101
     * @param \DateInterval|int|null $cacheTtl TTL cached data
102
     *
103
     * @return GPlayApps returns the current class instance to allow method chaining
104
     */
105 17
    public function setCacheTtl($cacheTtl): self
106
    {
107 17
        $cacheTtl = $cacheTtl ?? \DateInterval::createFromDateString('5 min');
108 17
        $this->getHttpClient()->setCacheTtl($cacheTtl);
109
110 17
        return $this;
111
    }
112
113
    /**
114
     * Returns an instance of HTTP client.
115
     *
116
     * @return HttpClient http client
117
     */
118 59
    protected function getHttpClient(): HttpClient
119
    {
120 59
        static $httpClient;
121
122 59
        if ($httpClient === null) {
123 1
            $proxy = getenv('HTTP_PROXY');
124
125 1
            if ($proxy === false) {
126 1
                $proxy = null;
127
            }
128
129 1
            $httpClient = new HttpClient(
130
                [
131 1
                    Options::TIMEOUT => 15.0,
132 1
                    Options::HEADERS => [
133
                        'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0',
134
                    ],
135 1
                    Options::PROXY => $proxy,
136
                ]
137
            );
138
        }
139
140 59
        return $httpClient;
141
    }
142
143
    /**
144
     * Sets the limit of concurrent HTTP requests.
145
     *
146
     * @param int $concurrency maximum number of concurrent HTTP requests
147
     *
148
     * @return GPlayApps returns the current class instance to allow method chaining
149
     */
150 4
    public function setConcurrency(int $concurrency): self
151
    {
152 4
        $this->concurrency = max(1, $concurrency);
153
154 4
        return $this;
155
    }
156
157
    /**
158
     * Sets proxy for outgoing HTTP requests.
159
     *
160
     * @param string|null $proxy Proxy url, ex. socks5://127.0.0.1:9050 or https://116.90.233.2:47348
161
     *
162
     * @return GPlayApps returns the current class instance to allow method chaining
163
     *
164
     * @see https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html Description of proxy URL formats in CURL.
165
     */
166
    public function setProxy(?string $proxy): self
167
    {
168
        $this->getHttpClient()->setProxy($proxy);
169
170
        return $this;
171
    }
172
173
    /**
174
     * Returns the full detail of an application.
175
     *
176
     * For information, you must specify the application ID (android package name).
177
     * The application ID can be viewed in the Google Play store:
178
     * `https://play.google.com/store/apps/details?id=XXXXXX` , where
179
     * XXXXXX is the application id.
180
     *
181
     * Or it can be found in the APK file.
182
     * ```shell
183
     * aapt dump badging file.apk | grep package | awk '{print $2}' | sed s/name=//g | sed s/\'//g
184
     * ```
185
     *
186
     * @param string|Model\AppId $appId google play app id (Android package name)
187
     *
188
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
189
     *
190
     * @return Model\AppInfo full detail of an application or exception
191
     *
192
     * @api
193
     */
194 8
    public function getAppInfo($appId): Model\AppInfo
195
    {
196 8
        return $this->getAppsInfo([$appId])[0];
197
    }
198
199
    /**
200
     * Returns the full detail of multiple applications.
201
     *
202
     * The keys of the returned array matches to the passed array.
203
     * HTTP requests are executed in parallel.
204
     *
205
     * @param string[]|Model\AppId[] $appIds array of application ids
206
     *
207
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
208
     *
209
     * @return Model\AppInfo[] an array of detailed information for each application
210
     *
211
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
212
     *
213
     * @api
214
     */
215 26
    public function getAppsInfo(array $appIds): array
216
    {
217 26
        if (empty($appIds)) {
218 1
            return [];
219
        }
220 25
        $urls = $this->createUrlListFromAppIds($appIds);
221
222
        try {
223 23
            return $this->getHttpClient()->requestAsyncPool(
224 23
                'GET',
225
                $urls,
226
                [
227 23
                    Options::HANDLER_RESPONSE => new Scraper\AppInfoScraper(),
228
                ],
229 23
                $this->concurrency
230
            );
231 2
        } catch (\Throwable $e) {
232 2
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
233
        }
234
    }
235
236
    /**
237
     * Returns an array of URLs for application ids.
238
     *
239
     * @param string[]|Model\AppId[] $appIds array of application ids
240
     *
241
     * @return string[] an array of URL
242
     */
243 26
    final protected function createUrlListFromAppIds(array $appIds): array
244
    {
245 26
        $urls = [];
246
247 26
        foreach ($appIds as $key => $appId) {
248 26
            $urls[$key] = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry)->getFullUrl();
249
        }
250
251 24
        return $urls;
252
    }
253
254
    /**
255
     * Returns the full details of an application in multiple languages.
256
     *
257
     * HTTP requests are executed in parallel.
258
     *
259
     * @param string|Model\AppId $appId   google Play app ID (Android package name)
260
     * @param string[]           $locales array of locales
261
     *
262
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
263
     *
264
     * @return array<string, Model\AppInfo> An array of detailed information for each locale.
265
     *                       The array key is the locale.
266
     *
267
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
268
     *
269
     * @api
270
     */
271 15
    public function getAppInfoForLocales($appId, array $locales): array
272
    {
273 15
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
274 14
        $requests = [];
275
276 14
        foreach ($locales as $locale) {
277 14
            $requests[$locale] = new Model\AppId($appId->getId(), $locale, $appId->getCountry());
278
        }
279
280 14
        return $this->getAppsInfo($requests);
281
    }
282
283
    /**
284
     * Returns detailed application information for all available locales.
285
     *
286
     * Information is returned only for the description loaded by the developer.
287
     * All locales with automated translation from Google Translate will be ignored.
288
     * HTTP requests are executed in parallel.
289
     *
290
     * @param string|Model\AppId $appId application ID (Android package name) as
291
     *                                  a string or {@see Model\AppId} object
292
     *
293
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
294
     *
295
     * @return array<string, Model\AppInfo> An array with detailed information about the application
296
     *                       on all available locales. The array key is the locale.
297
     *
298
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
299
     *
300
     * @api
301
     */
302 14
    public function getAppInfoForAvailableLocales($appId): array
303
    {
304 14
        $list = $this->getAppInfoForLocales($appId, Util\LocaleHelper::SUPPORTED_LOCALES);
305
306 13
        $preferredLocale = self::DEFAULT_LOCALE;
307
308 13
        foreach ($list as $app) {
309 13
            if ($app->isAutoTranslatedDescription()) {
310 12
                $preferredLocale = (string) $app->getTranslatedFromLocale();
311
                break;
312
            }
313
        }
314
315
        /**
316
         * @var Model\AppInfo[] $list
317
         */
318 13
        $list = array_filter(
319
            $list,
320 13
            static function (Model\AppInfo $app) {
321 13
                return !$app->isAutoTranslatedDescription();
322 13
            }
323
        );
324
325 13
        if (!isset($list[$preferredLocale])) {
326
            throw new \RuntimeException('No key ' . $preferredLocale);
327
        }
328 13
        $preferredApp = $list[$preferredLocale];
329 13
        $list = array_filter(
330
            $list,
331 13
            static function (Model\AppInfo $app, string $locale) use ($preferredApp, $list) {
332
                // deletes locales in which there is no translation added, but automatic translation by Google Translate is used.
333 13
                if ($preferredApp->getLocale() === $locale || !$preferredApp->equals($app)) {
334 13
                    if (($pos = strpos($locale, '_')) !== false) {
335 13
                        $rootLang = substr($locale, 0, $pos);
336 13
                        $rootLangLocale = Util\LocaleHelper::getNormalizeLocale($rootLang);
337
338
                        if (
339 13
                            $rootLangLocale !== $locale &&
340 12
                            isset($list[$rootLangLocale]) &&
341 12
                            $list[$rootLangLocale]->equals($app)
342
                        ) {
343
                            // delete duplicate data,
344
                            // for example, delete en_CA, en_IN, en_GB, en_ZA, if there is en_US and they are equals.
345 9
                            return false;
346
                        }
347
                    }
348
349 13
                    return true;
350
                }
351
352 10
                return false;
353 13
            },
354 13
            \ARRAY_FILTER_USE_BOTH
355
        );
356
357
        // sorting array keys; the first key is the preferred locale
358 13
        uksort(
359
            $list,
360 13
            static function (
361
                /** @noinspection PhpUnusedParameterInspection */
362
                string $a,
363
                string $b
364 13
            ) use ($preferredLocale) {
365 13
                return $b === $preferredLocale ? 1 : 0;
366 13
            }
367
        );
368
369 13
        return $list;
370
    }
371
372
    /**
373
     * Checks if the specified application exists in the Google Play store.
374
     *
375
     * @param string|Model\AppId $appId application ID (Android package name) as
376
     *                                  a string or {@see Model\AppId} object
377
     *
378
     * @return bool returns `true` if the application exists, or `false` if not
379
     *
380
     * @api
381
     */
382 5
    public function existsApp($appId): bool
383
    {
384 5
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
385
386
        try {
387 5
            return (bool) $this->getHttpClient()->request(
388 5
                'HEAD',
389 5
                $appId->getFullUrl(),
390
                [
391 5
                    RequestOptions::HTTP_ERRORS => false,
392 5
                    Options::HANDLER_RESPONSE => new Scraper\ExistsAppScraper(),
393
                ]
394
            );
395
        } catch (\Throwable $e) {
396
            return false;
397
        }
398
    }
399
400
    /**
401
     * Checks if the specified applications exist in the Google Play store.
402
     * HTTP requests are executed in parallel.
403
     *
404
     * @param string[]|Model\AppId[] $appIds Array of application identifiers.
405
     *                                       The keys of the returned array correspond to the transferred array.
406
     *
407
     * @throws Exception\GooglePlayException if an HTTP error other than 404 is received
408
     *
409
     * @return bool[] An array of information about the existence of each
410
     *                application in the store Google Play. The keys of the returned
411
     *                array matches to the passed array.
412
     *
413
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
414
     *
415
     * @api
416
     */
417 1
    public function existsApps(array $appIds): array
418
    {
419 1
        if (empty($appIds)) {
420
            return [];
421
        }
422 1
        $urls = $this->createUrlListFromAppIds($appIds);
423
424
        try {
425 1
            return $this->getHttpClient()->requestAsyncPool(
426 1
                'HEAD',
427
                $urls,
428
                [
429 1
                    RequestOptions::HTTP_ERRORS => false,
430 1
                    Options::HANDLER_RESPONSE => new Scraper\ExistsAppScraper(),
431
                ],
432 1
                $this->concurrency
433
            );
434
        } catch (\Throwable $e) {
435
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
436
        }
437
    }
438
439
    /**
440
     * Returns reviews of the Android app in the Google Play store.
441
     *
442
     * Getting a lot of reviews can take a lot of time.
443
     *
444
     * @param string|Model\AppId $appId application ID (Android package name) as
445
     *                                  a string or {@see Model\AppId} object
446
     * @param int                $limit Maximum number of reviews. To extract all
447
     *                                  reviews, use {@see GPlayApps::UNLIMIT}.
448
     * @param Enum\SortEnum|null $sort  Sort reviews of the application.
449
     *                                  If null, then sort by the newest reviews.
450
     *
451
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
452
     *
453
     * @return Model\Review[] app reviews
454
     *
455
     * @see Enum\SortEnum Contains all valid values for the "sort" parameter.
456
     * @see GPlayApps::UNLIMIT Limit for all available results.
457
     *
458
     * @api
459
     */
460 1
    public function getReviews($appId, int $limit = 100, ?Enum\SortEnum $sort = null): array
461
    {
462 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
463 1
        $sort = $sort ?? Enum\SortEnum::NEWEST();
464
465 1
        $allCount = 0;
466 1
        $token = null;
467 1
        $allReviews = [];
468
469 1
        $cacheTtl = $sort === Enum\SortEnum::NEWEST() ?
470 1
            \DateInterval::createFromDateString('1 min') :
471 1
            \DateInterval::createFromDateString('1 hour');
472
473
        try {
474
            do {
475 1
                $count = $limit === self::UNLIMIT ?
476
                    Scraper\PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE :
477 1
                    min(Scraper\PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE, max($limit - $allCount, 1));
478
479 1
                $request = Scraper\PlayStoreUiRequest::getReviewsRequest($appId, $count, $sort, $token);
480
481 1
                [$reviews, $token] = $this->getHttpClient()->send(
482
                    $request,
483
                    [
484 1
                        Options::CACHE_TTL => $cacheTtl,
485 1
                        Options::HANDLER_RESPONSE => new Scraper\ReviewsScraper($appId),
486
                    ]
487
                );
488 1
                $allCount += \count($reviews);
489 1
                $allReviews[] = $reviews;
490 1
            } while ($token !== null && ($limit === self::UNLIMIT || $allCount < $limit));
491
        } catch (\Throwable $e) {
492
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
493
        }
494
495 1
        return empty($allReviews) ? $allReviews : array_merge(...$allReviews);
496
    }
497
498
    /**
499
     * Returns review of the Android app in the Google Play store by review id.
500
     *
501
     * @param string|Model\AppId $appId    application ID (Android package name) as
502
     *                                     a string or {@see Model\AppId} object
503
     * @param string             $reviewId review id
504
     *
505
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
506
     *
507
     * @return Model\Review app review
508
     */
509 1
    public function getReviewById($appId, string $reviewId): Model\Review
510
    {
511 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
512
513
        try {
514
            /** @var Model\Review $review */
515 1
            $review = $this->getHttpClient()->request(
516 1
                'GET',
517 1
                self::GOOGLE_PLAY_APPS_URL . '/details',
518
                [
519 1
                    RequestOptions::QUERY => [
520 1
                        self::REQ_PARAM_ID => $appId->getId(),
521 1
                        self::REQ_PARAM_LOCALE => $appId->getLocale(),
522 1
                        self::REQ_PARAM_COUNTRY => $appId->getCountry(),
523 1
                        'reviewId' => $reviewId,
524
                    ],
525 1
                    Options::HANDLER_RESPONSE => new Scraper\AppSpecificReviewScraper($appId),
526
                ]
527
            );
528
        } catch (\Throwable $e) {
529
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
530
        }
531
532 1
        return $review;
533
    }
534
535
    /**
536
     * Returns a list of permissions for the application.
537
     *
538
     * @param string|Model\AppId $appId application ID (Android package name) as
539
     *                                  a string or {@see Model\AppId} object
540
     *
541
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
542
     *
543
     * @return Model\Permission[] an array of permissions for the application
544
     *
545
     * @api
546
     */
547 1
    public function getPermissions($appId): array
548
    {
549 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
550
551
        try {
552 1
            $request = Scraper\PlayStoreUiRequest::getPermissionsRequest($appId);
553
554
            /** @var Model\Permission[] $permissions */
555 1
            $permissions = $this->getHttpClient()->send(
556
                $request,
557
                [
558 1
                    Options::HANDLER_RESPONSE => new Scraper\PermissionScraper(),
559
                ]
560
            );
561
        } catch (\Throwable $e) {
562
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
563
        }
564
565 1
        return $permissions;
566
    }
567
568
    /**
569
     * Returns an array of application categories from the Google Play store.
570
     *
571
     * @throws Exception\GooglePlayException if HTTP error is received
572
     *
573
     * @return Model\Category[] array of application categories
574
     *
575
     * @api
576
     */
577 1
    public function getCategories(): array
578
    {
579 1
        $url = self::GOOGLE_PLAY_APPS_URL;
580
581
        try {
582
            /** @var Model\Category[] $categories */
583 1
            $categories = $this->getHttpClient()->request(
584 1
                'GET',
585
                $url,
586
                [
587 1
                    RequestOptions::QUERY => [
588 1
                        self::REQ_PARAM_LOCALE => $this->defaultLocale,
589
                    ],
590 1
                    Options::HANDLER_RESPONSE => new Scraper\CategoriesScraper(),
591
                ]
592
            );
593
        } catch (\Throwable $e) {
594
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
595
        }
596
597 1
        return $categories;
598
    }
599
600
    /**
601
     * Returns an array of application categories from the Google Play store for the specified locales.
602
     *
603
     * HTTP requests are executed in parallel.
604
     *
605
     * @param string[] $locales array of locales
606
     *
607
     * @throws Exception\GooglePlayException if HTTP error is received
608
     *
609
     * @return Model\Category[][] array of application categories by locale
610
     *
611
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
612
     *
613
     * @api
614
     */
615 2
    public function getCategoriesForLocales(array $locales): array
616
    {
617 2
        if (empty($locales)) {
618
            return [];
619
        }
620 2
        $locales = Util\LocaleHelper::getNormalizeLocales($locales);
621
622 2
        $urls = [];
623 2
        $url = self::GOOGLE_PLAY_APPS_URL;
624
625 2
        foreach ($locales as $locale) {
626 2
            $urls[$locale] = $url . '?' . http_build_query(
627
                [
628 2
                    self::REQ_PARAM_LOCALE => $locale,
629
                ]
630
            );
631
        }
632
633
        try {
634 2
            return $this->getHttpClient()->requestAsyncPool(
635 2
                'GET',
636
                $urls,
637
                [
638 2
                    Options::HANDLER_RESPONSE => new Scraper\CategoriesScraper(),
639
                ],
640 2
                $this->concurrency
641
            );
642
        } catch (\Throwable $e) {
643
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
644
        }
645
    }
646
647
    /**
648
     * Returns an array of categories from the Google Play store for all available locales.
649
     *
650
     * @throws Exception\GooglePlayException if HTTP error is received
651
     *
652
     * @return Model\Category[][] array of application categories by locale
653
     *
654
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
655
     *
656
     * @api
657
     */
658 1
    public function getCategoriesForAvailableLocales(): array
659
    {
660 1
        return $this->getCategoriesForLocales(Util\LocaleHelper::SUPPORTED_LOCALES);
661
    }
662
663
    /**
664
     * Returns information about the developer: name, icon, cover, description and website address.
665
     *
666
     * @param string|Model\Developer|Model\App $developerId developer id as
667
     *                                                      string, {@see Model\Developer}
668
     *                                                      or {@see Model\App} object
669
     *
670
     * @throws Exception\GooglePlayException if HTTP error is received
671
     *
672
     * @return Model\Developer information about the application developer
673
     *
674
     * @see GPlayApps::getDeveloperInfoForLocales() Returns information about the developer for the locale array.
675
     *
676
     * @api
677
     */
678 5
    public function getDeveloperInfo($developerId): Model\Developer
679
    {
680 5
        $developerId = Util\Caster::castToDeveloperId($developerId);
681
682 5
        if (!is_numeric($developerId)) {
683 3
            throw new Exception\GooglePlayException(
684 3
                sprintf(
685 3
                    'Developer "%s" does not have a personalized page on Google Play.',
686
                    $developerId
687
                )
688
            );
689
        }
690
691 2
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
692
693
        try {
694
            /** @var Model\Developer $developer */
695 2
            $developer = $this->getHttpClient()->request(
696 2
                'GET',
697
                $url,
698
                [
699 2
                    RequestOptions::QUERY => [
700 2
                        self::REQ_PARAM_ID => $developerId,
701 2
                        self::REQ_PARAM_LOCALE => $this->defaultLocale,
702
                    ],
703 2
                    Options::HANDLER_RESPONSE => new Scraper\DeveloperInfoScraper(),
704
                ]
705
            );
706 1
        } catch (\Throwable $e) {
707 1
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
708
        }
709
710 1
        return $developer;
711
    }
712
713
    /**
714
     * Returns information about the developer for the specified locales.
715
     *
716
     * @param string|Model\Developer|Model\App $developerId developer id as
717
     *                                                      string, {@see Model\Developer}
718
     *                                                      or {@see Model\App} object
719
     * @param string[]                         $locales     array of locales
720
     *
721
     * @throws Exception\GooglePlayException if HTTP error is received
722
     *
723
     * @return Model\Developer[] an array with information about the application developer
724
     *                           for each requested locale
725
     *
726
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
727
     * @see GPlayApps::getDeveloperInfo() Returns information about the developer: name,
728
     *     icon, cover, description and website address.
729
     *
730
     * @api
731
     */
732 1
    public function getDeveloperInfoForLocales($developerId, array $locales = []): array
733
    {
734 1
        if (empty($locales)) {
735
            return [];
736
        }
737 1
        $locales = Util\LocaleHelper::getNormalizeLocales($locales);
738
739 1
        $id = Util\Caster::castToDeveloperId($developerId);
740
741 1
        if (!is_numeric($id)) {
742
            throw new Exception\GooglePlayException(
743
                sprintf(
744
                    'Developer "%s" does not have a personalized page on Google Play.',
745
                    $id
746
                )
747
            );
748
        }
749
750 1
        $urls = [];
751 1
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
752
753 1
        foreach ($locales as $locale) {
754 1
            $urls[$locale] = $url . '?' . http_build_query(
755
                [
756 1
                    self::REQ_PARAM_ID => $id,
757 1
                    self::REQ_PARAM_LOCALE => $locale,
758
                ]
759
            );
760
        }
761
762
        try {
763 1
            return $this->getHttpClient()->requestAsyncPool(
764 1
                'GET',
765
                $urls,
766
                [
767 1
                    Options::HANDLER_RESPONSE => new Scraper\DeveloperInfoScraper(),
768
                ],
769 1
                $this->concurrency
770
            );
771
        } catch (\Throwable $e) {
772
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
773
        }
774
    }
775
776
    /**
777
     * Returns an array of applications from the Google Play store by developer id.
778
     *
779
     * @param string|Model\Developer|Model\App $developerId developer id as
780
     *                                                      string, {@see Model\Developer}
781
     *                                                      or {@see Model\App} object
782
     *
783
     * @throws Exception\GooglePlayException if HTTP error is received
784
     *
785
     * @return Model\App[] an array of applications with basic information
786
     *
787
     * @api
788
     */
789 1
    public function getDeveloperApps($developerId): array
790
    {
791 1
        $developerId = Util\Caster::castToDeveloperId($developerId);
792
793
        $query = [
794 1
            self::REQ_PARAM_ID => $developerId,
795 1
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
796 1
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
797
        ];
798
799 1
        if (is_numeric($developerId)) {
800 1
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/dev?' . http_build_query($query);
801
802
            try {
803
                /**
804
                 * @var string|null $developerUrl
805
                 */
806 1
                $developerUrl = $this->getHttpClient()->request(
807 1
                    'GET',
808
                    $developerUrl,
809
                    [
810 1
                        Options::HANDLER_RESPONSE => new Scraper\FindDevAppsUrlScraper(),
811
                    ]
812
                );
813
814 1
                if ($developerUrl === null) {
815
                    return [];
816
                }
817 1
                $developerUrl .= '&' . self::REQ_PARAM_LOCALE . '=' . urlencode($this->defaultLocale) .
818 1
                    '&' . self::REQ_PARAM_COUNTRY . '=' . urlencode($this->defaultCountry);
819
            } catch (\Throwable $e) {
820
                throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
821
            }
822
        } else {
823 1
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/developer?' . http_build_query($query);
824
        }
825
826 1
        return $this->fetchAppsFromClusterPage(
827 1
            $developerUrl,
828 1
            $this->defaultLocale,
829 1
            $this->defaultCountry,
830 1
            self::UNLIMIT
831
        );
832
    }
833
834
    /**
835
     * Returns a list of applications with basic information.
836
     *
837
     * @param string $clusterPageUrl cluster page URL
838
     * @param string $locale         locale
839
     * @param string $country        country
840
     * @param int    $limit          Maximum number of applications. To extract all
841
     *                               applications, use {@see GPlayApps::UNLIMIT}.
842
     *
843
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
844
     *
845
     * @return Model\App[] array of applications with basic information about them
846
     *
847
     * @see GPlayApps::UNLIMIT Limit for all available results.
848
     */
849 17
    protected function fetchAppsFromClusterPage(
850
        string $clusterPageUrl,
851
        string $locale,
852
        string $country,
853
        int $limit
854
    ): array {
855 17
        if ($limit < self::UNLIMIT || $limit === 0) {
856
            throw new \InvalidArgumentException(sprintf('Invalid limit: %d', $limit));
857
        }
858
859 17
        $clusterPageComponents = parse_url($clusterPageUrl);
860 17
        $query = Query::parse($clusterPageComponents['query'] ?? '');
861 17
        $query[self::REQ_PARAM_LOCALE] = $locale;
862 17
        $query[self::REQ_PARAM_COUNTRY] = $country;
863
864 17
        $clusterPageUrl = $clusterPageComponents['scheme'] . '://' .
865 17
            $clusterPageComponents['host'] .
866 17
            $clusterPageComponents['path'] .
867 17
            '?' . Query::build($query);
868
869
        try {
870 17
            [$apps, $token] = $this->getHttpClient()->request(
871 17
                'GET',
872
                $clusterPageUrl,
873
                [
874 17
                    Options::HANDLER_RESPONSE => new Scraper\ClusterAppsScraper(),
875
                ]
876
            );
877
878 17
            $allCount = \count($apps);
879 17
            $allApps = [$apps];
880
881 17
            while ($token !== null && ($limit === self::UNLIMIT || $allCount < $limit)) {
882 17
                $count = $limit === self::UNLIMIT ?
883 16
                    Scraper\PlayStoreUiRequest::LIMIT_APPS_ON_PAGE :
884 17
                    min(Scraper\PlayStoreUiRequest::LIMIT_APPS_ON_PAGE, max($limit - $allCount, 1));
885
886 17
                $request = Scraper\PlayStoreUiRequest::getAppsRequest($locale, $country, $count, $token);
887
888 17
                [$apps, $token] = $this->getHttpClient()->send(
889
                    $request,
890
                    [
891 17
                        Options::HANDLER_RESPONSE => new Scraper\PlayStoreUiAppsScraper(),
892
                    ]
893
                );
894 17
                $allCount += \count($apps);
895 17
                $allApps[] = $apps;
896
            }
897
        } catch (\Throwable $e) {
898
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
899
        }
900
901 17
        if (empty($allApps)) {
902
            return $allApps;
903
        }
904 17
        $allApps = array_merge(...$allApps);
905
906 17
        if ($limit !== self::UNLIMIT) {
907 1
            $allApps = \array_slice($allApps, 0, $limit);
908
        }
909
910 17
        return $allApps;
911
    }
912
913
    /**
914
     * Returns an array of similar applications with basic information about
915
     * them in the Google Play store.
916
     *
917
     * @param string|Model\AppId $appId application ID (Android package name)
918
     *                                  as a string or {@see Model\AppId} object
919
     * @param int                $limit The maximum number of similar applications.
920
     *                                  To extract all similar applications,
921
     *                                  use {@see GPlayApps::UNLIMIT}.
922
     *
923
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
924
     *
925
     * @return Model\App[] an array of applications with basic information about them
926
     *
927
     * @see GPlayApps::UNLIMIT Limit for all available results.
928
     *
929
     * @api
930
     */
931 1
    public function getSimilarApps($appId, int $limit = 50): array
932
    {
933 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
934
935
        try {
936
            /**
937
             * @var string|null $similarAppsUrl
938
             */
939 1
            $similarAppsUrl = $this->getHttpClient()->request(
940 1
                'GET',
941 1
                $appId->getFullUrl(),
942
                [
943 1
                    Options::HANDLER_RESPONSE => new Scraper\FindSimilarAppsUrlScraper($appId),
944
                ]
945
            );
946
947 1
            if ($similarAppsUrl === null) {
948
                return [];
949
            }
950
951 1
            return $this->fetchAppsFromClusterPage(
952 1
                $similarAppsUrl,
953 1
                $appId->getLocale(),
954 1
                $appId->getCountry(),
955
                $limit
956
            );
957
        } catch (\Throwable $e) {
958
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
959
        }
960
    }
961
962
    /**
963
     * Returns the Google Play search suggests.
964
     *
965
     * @param string $query search query
966
     *
967
     * @throws Exception\GooglePlayException if HTTP error is received
968
     *
969
     * @return string[] array containing search suggestions
970
     *
971
     * @api
972
     */
973 2
    public function getSearchSuggestions(string $query): array
974
    {
975 2
        $query = trim($query);
976
977 2
        if ($query === '') {
978
            return [];
979
        }
980
981
        try {
982 2
            $request = Scraper\PlayStoreUiRequest::getSuggestRequest($query, $this->defaultLocale, $this->defaultCountry);
983
984
            /** @var string[] $suggestions */
985 2
            $suggestions = $this->getHttpClient()->send(
986
                $request,
987
                [
988 2
                    Options::HANDLER_RESPONSE => new Scraper\SuggestScraper(),
989
                ]
990
            );
991
        } catch (\Throwable $e) {
992
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
993
        }
994
995 2
        return $suggestions;
996
    }
997
998
    /**
999
     * Returns a list of applications from the Google Play store for a search query.
1000
     *
1001
     * @param string              $query search query
1002
     * @param int                 $limit the limit on the number of search results
1003
     * @param Enum\PriceEnum|null $price price category or `null`
1004
     *
1005
     * @throws Exception\GooglePlayException if HTTP error is received
1006
     *
1007
     * @return Model\App[] an array of applications with basic information
1008
     *
1009
     * @see Enum\PriceEnum Contains all valid values for the "price" parameter.
1010
     * @see GPlayApps::UNLIMIT Limit for all available results.
1011
     *
1012
     * @api
1013
     */
1014 1
    public function search(string $query, int $limit = 50, ?Enum\PriceEnum $price = null): array
1015
    {
1016 1
        $query = trim($query);
1017
1018 1
        if (empty($query)) {
1019
            throw new \InvalidArgumentException('Search query missing');
1020
        }
1021 1
        $price = $price ?? Enum\PriceEnum::ALL();
1022
1023
        $params = [
1024 1
            'c' => 'apps',
1025 1
            'q' => $query,
1026 1
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
1027 1
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
1028 1
            'price' => $price->value(),
1029
        ];
1030 1
        $clusterPageUrl = self::GOOGLE_PLAY_URL . '/store/search?' . http_build_query($params);
1031
1032 1
        return $this->fetchAppsFromClusterPage(
1033 1
            $clusterPageUrl,
1034 1
            $this->defaultLocale,
1035 1
            $this->defaultCountry,
1036
            $limit
1037
        );
1038
    }
1039
1040
    /**
1041
     * Returns an array of applications from the Google Play store for the specified category.
1042
     *
1043
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1044
     *                                                               string, {@see Model\Category},
1045
     *                                                               {@see Enum\CategoryEnum} or
1046
     *                                                               `null` for all categories
1047
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1048
     * @param int                                          $limit    limit on the number of results
1049
     *                                                               or {@see GPlayApps::UNLIMIT}
1050
     *                                                               for no limit
1051
     *
1052
     * @return Model\App[] an array of applications with basic information
1053
     *
1054
     * @see GPlayApps::UNLIMIT Limit for all available results.
1055
     *
1056
     * @api
1057
     */
1058 5
    public function getListApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1059
    {
1060 5
        return $this->fetchAppsFromClusterPages($category, $age, null, $limit);
1061
    }
1062
1063
    /**
1064
     * Returns an array of **top apps** from the Google Play store for the specified category.
1065
     *
1066
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1067
     *                                                               string, {@see Model\Category},
1068
     *                                                               {@see Enum\CategoryEnum} or
1069
     *                                                               `null` for all categories
1070
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1071
     * @param int                                          $limit    limit on the number of results
1072
     *                                                               or {@see GPlayApps::UNLIMIT}
1073
     *                                                               for no limit
1074
     *
1075
     * @return Model\App[] an array of applications with basic information
1076
     *
1077
     * @see GPlayApps::UNLIMIT Limit for all available results.
1078
     *
1079
     * @api
1080
     */
1081 6
    public function getTopApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1082
    {
1083 6
        return $this->fetchAppsFromClusterPages($category, $age, 'top', $limit);
1084
    }
1085
1086
    /**
1087
     * Returns an array of **new apps** from the Google Play store for the specified category.
1088
     *
1089
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1090
     *                                                               string, {@see Model\Category},
1091
     *                                                               {@see Enum\CategoryEnum} or
1092
     *                                                               `null` for all categories
1093
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1094
     * @param int                                          $limit    limit on the number of results
1095
     *                                                               or {@see GPlayApps::UNLIMIT}
1096
     *                                                               for no limit
1097
     *
1098
     * @return Model\App[] an array of applications with basic information
1099
     *
1100
     * @see GPlayApps::UNLIMIT Limit for all available results.
1101
     *
1102
     * @api
1103
     */
1104 5
    public function getNewApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1105
    {
1106 5
        return $this->fetchAppsFromClusterPages($category, $age, 'new', $limit);
1107
    }
1108
1109
    /**
1110
     * @param string|Model\Category|Enum\CategoryEnum|null $category
1111
     * @param Enum\AgeEnum|null                            $age
1112
     * @param string|null                                  $path
1113
     * @param int                                          $limit
1114
     *
1115
     * @return Model\App[]
1116
     */
1117 16
    protected function fetchAppsFromClusterPages($category, ?Enum\AgeEnum $age, ?string $path, int $limit): array
1118
    {
1119 16
        if ($limit === 0 || $limit < self::UNLIMIT) {
1120
            throw new \InvalidArgumentException('Negative limit');
1121
        }
1122
1123
        $queryParams = [
1124 16
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
1125 16
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
1126
        ];
1127
1128 16
        if ($age !== null) {
1129 1
            $queryParams['age'] = $age->value();
1130
        }
1131
1132 16
        $url = self::GOOGLE_PLAY_APPS_URL;
1133
1134 16
        if ($path !== null) {
1135 11
            $url .= '/' . $path;
1136
        }
1137
1138 16
        if ($category !== null) {
1139 12
            $url .= '/category/' . Util\Caster::castToCategoryId($category);
1140
        }
1141 16
        $url .= '?' . http_build_query($queryParams);
1142
1143
        /**
1144
         * @var array $categoryClusterPages = [[
1145
         *            "name" => "Top Free Games",
1146
         *            "url" => "https://play.google.com/store/apps/store/apps/collection/cluster?clp=......"
1147
         *            ]]
1148
         */
1149 16
        $categoryClusterPages = $this->getHttpClient()->request(
1150 16
            'GET',
1151
            $url,
1152
            [
1153 16
                Options::HANDLER_RESPONSE => new Scraper\ClusterPagesFromListAppsScraper(),
1154
            ]
1155
        );
1156
1157 16
        if (empty($categoryClusterPages)) {
1158 2
            return [];
1159
        }
1160
1161 14
        $iterator = new \ArrayIterator($categoryClusterPages);
1162 14
        $results = [];
1163
1164
        do {
1165 14
            $clusterPage = $iterator->current();
1166 14
            $clusterUrl = $clusterPage['url'];
1167
1168
            try {
1169 14
                $apps = $this->fetchAppsFromClusterPage($clusterUrl, $this->defaultLocale, $this->defaultCountry, self::UNLIMIT);
1170
1171 14
                foreach ($apps as $app) {
1172 14
                    if (!isset($results[$app->getId()])) {
1173 14
                        $results[$app->getId()] = $app;
1174
                    }
1175
                }
1176
            } catch (\Throwable $e) {
1177
                // ignore exception
1178
            }
1179
1180 14
            if ($limit !== self::UNLIMIT && \count($results) >= $limit) {
1181 1
                $results = \array_slice($results, 0, $limit);
1182 1
                break;
1183
            }
1184
1185 13
            $iterator->next();
1186 13
        } while ($iterator->valid());
1187
1188 14
        return $results;
1189
    }
1190
1191
    /**
1192
     * Asynchronously saves images from googleusercontent.com and similar URLs to disk.
1193
     *
1194
     * Before use, you can set the parameters of the width-height of images.
1195
     *
1196
     * Example:
1197
     * ```php
1198
     * $gplay->saveGoogleImages(
1199
     *     $images,
1200
     *     static function (\Nelexa\GPlay\Model\GoogleImage $image): string {
1201
     *         $hash = $image->getHashUrl($hashAlgo = 'md5', $parts = 2, $partLength = 2);
1202
     *         return 'path/to/screenshots/' . $hash . '.{ext}';
1203
     *     },
1204
     *     $overwrite = false
1205
     * );
1206
     * ```
1207
     *
1208
     * @param Model\GoogleImage[] $images           array of {@see Model\GoogleImage} objects
1209
     * @param callable            $destPathCallback The function to which the
1210
     *                                              {@see Model\GoogleImage} object is
1211
     *                                              passed, and you must return the full
1212
     *                                              output. path to save this file.
1213
     * @param bool                $overwrite        overwrite files if exists
1214
     *
1215
     * @return Model\ImageInfo[] returns an array with information about saved images
1216
     *
1217
     * @see Model\GoogleImage Contains a link to the image, allows you to customize its size and download it.
1218
     * @see Model\ImageInfo Contains information about the image.
1219
     *
1220
     * @api
1221
     */
1222 2
    public function saveGoogleImages(
1223
        array $images,
1224
        callable $destPathCallback,
1225
        bool $overwrite = false
1226
    ): array {
1227
        /** @var array<string, \Nelexa\GPlay\Util\LazyStream> $mapping */
1228 2
        $mapping = [];
1229
1230 2
        foreach ($images as $image) {
1231 2
            if (!$image instanceof Model\GoogleImage) {
1232
                throw new \InvalidArgumentException(
1233
                    'An array of ' . Model\GoogleImage::class . ' objects is expected.'
1234
                );
1235
            }
1236 2
            $destPath = $destPathCallback($image);
1237 2
            $url = $image->getUrl();
1238 2
            $mapping[$url] = new Util\LazyStream($destPath, 'w+b');
1239
        }
1240
1241 2
        $httpClient = $this->getHttpClient();
1242 2
        $promises = (static function () use ($mapping, $overwrite, $httpClient) {
1243 2
            foreach ($mapping as $url => $stream) {
1244 2
                $destPath = $stream->getFilename();
1245 2
                $dynamicPath = strpos($destPath, '{url}') !== false;
1246
1247 2
                if (!$overwrite && !$dynamicPath && is_file($destPath)) {
1248
                    yield $url => new FulfilledPromise($url);
1249
                } else {
1250
                    yield $url => $httpClient
1251 2
                        ->requestAsync(
1252 2
                            'GET',
1253
                            $url,
1254
                            [
1255 2
                                RequestOptions::COOKIES => null,
1256 2
                                RequestOptions::SINK => $stream,
1257 2
                                RequestOptions::HTTP_ERRORS => true,
1258 2
                                RequestOptions::ON_HEADERS => static function (ResponseInterface $response) use (
1259 2
                                    $url,
1260 2
                                    $stream
1261
                                ): void {
1262 2
                                    Model\GoogleImage::onHeaders($response, $url, $stream);
1263 2
                                },
1264
                            ]
1265
                        )
1266 2
                        ->then(
1267 2
                            static function (
1268
                                /** @noinspection PhpUnusedParameterInspection */
1269
                                ResponseInterface $response
1270 2
                            ) use ($url) {
1271 1
                                return $url;
1272 2
                            }
1273
                        )
1274
                    ;
1275
                }
1276
            }
1277 2
        })();
1278
1279
        /**
1280
         * @var Model\ImageInfo[] $imageInfoList
1281
         */
1282 2
        $imageInfoList = [];
1283 2
        $eachPromise = (new EachPromise(
1284
            $promises,
1285
            [
1286 2
                'concurrency' => $this->concurrency,
1287 2
                'fulfilled' => static function (string $url) use (&$imageInfoList, $mapping): void {
1288 1
                    $imageInfoList[] = new Model\ImageInfo($url, $mapping[$url]->getFilename());
1289 2
                },
1290 2
                'rejected' => static function (\Throwable $reason, string $exceptionUrl) use ($mapping): void {
1291 1
                    foreach ($mapping as $destPath => $url) {
1292 1
                        if (is_file($destPath)) {
1293
                            unlink($destPath);
1294
                        }
1295
                    }
1296
1297 1
                    throw (new Exception\GooglePlayException(
1298 1
                        $reason->getMessage(),
1299 1
                        $reason->getCode(),
1300
                        $reason
1301 1
                    ))->setUrl(
1302 1
                        $exceptionUrl
1303
                    );
1304 2
                },
1305
            ]
1306 2
        ))->promise();
1307
1308 2
        if ($eachPromise !== null) {
1309 2
            $eachPromise->wait();
1310
        }
1311
1312 1
        return $imageInfoList;
1313
    }
1314
1315
    /**
1316
     * Returns the locale (language) of the requests.
1317
     *
1318
     * @return string locale (language) for HTTP requests to Google Play
1319
     */
1320 5
    public function getDefaultLocale(): string
1321
    {
1322 5
        return $this->defaultLocale;
1323
    }
1324
1325
    /**
1326
     * Sets the locale (language) of requests.
1327
     *
1328
     * @param string $defaultLocale locale (language) for HTTP requests to Google Play
1329
     *
1330
     * @return GPlayApps returns the current class instance to allow method chaining
1331
     */
1332 71
    public function setDefaultLocale(string $defaultLocale): self
1333
    {
1334 71
        $this->defaultLocale = Util\LocaleHelper::getNormalizeLocale($defaultLocale);
1335
1336 71
        return $this;
1337
    }
1338
1339
    /**
1340
     * Returns the country of the requests.
1341
     *
1342
     * @return string country for HTTP requests to Google Play
1343
     */
1344 5
    public function getDefaultCountry(): string
1345
    {
1346 5
        return $this->defaultCountry;
1347
    }
1348
1349
    /**
1350
     * Sets the country of requests.
1351
     *
1352
     * @param string $defaultCountry country for HTTP requests to Google Play
1353
     *
1354
     * @return GPlayApps returns the current class instance to allow method chaining
1355
     */
1356 71
    public function setDefaultCountry(string $defaultCountry): self
1357
    {
1358 71
        $this->defaultCountry = !empty($defaultCountry) ?
1359 71
            $defaultCountry :
1360 2
            self::DEFAULT_COUNTRY;
1361
1362 71
        return $this;
1363
    }
1364
1365
    /**
1366
     * Sets the number of seconds to wait when trying to connect to the server.
1367
     *
1368
     * @param float $connectTimeout Connection timeout in seconds, for example 3.14. Use 0 to wait indefinitely.
1369
     *
1370
     * @return GPlayApps returns the current class instance to allow method chaining
1371
     */
1372
    public function setConnectTimeout(float $connectTimeout): self
1373
    {
1374
        $this->getHttpClient()->setConnectTimeout($connectTimeout);
1375
1376
        return $this;
1377
    }
1378
1379
    /**
1380
     * Sets the timeout of the request in second.
1381
     *
1382
     * @param float $timeout Waiting timeout in seconds, for example 3.14. Use 0 to wait indefinitely.
1383
     *
1384
     * @return GPlayApps returns the current class instance to allow method chaining
1385
     */
1386
    public function setTimeout(float $timeout): self
1387
    {
1388
        $this->getHttpClient()->setTimeout($timeout);
1389
1390
        return $this;
1391
    }
1392
}
1393