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