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