Completed
Push — master ( 45165d...5e44c4 )
by Alexey
04:08 queued 11s
created

GPlayApps::fetchAppsFromClusterPage()   B

Complexity

Conditions 10
Paths 25

Size

Total Lines 52
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 10.3638

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 52
ccs 22
cts 26
cp 0.8462
rs 7.6666
cc 10
nc 25
nop 4
crap 10.3638

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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