Passed
Branch master (2bc959)
by Alexey
02:57
created

AppInfoScraper::extractHistogramRating()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 8
ccs 7
cts 7
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
/** @noinspection MultiAssignmentUsageInspection */
4
declare(strict_types=1);
5
6
/**
7
 * @author   Ne-Lexa
8
 * @license  MIT
9
 *
10
 * @see      https://github.com/Ne-Lexa/google-play-scraper
11
 */
12
13
namespace Nelexa\GPlay\Scraper;
14
15
use Nelexa\GPlay\Exception\GooglePlayException;
16
use Nelexa\GPlay\GPlayApps;
17
use Nelexa\GPlay\Model\AppId;
18
use Nelexa\GPlay\Model\AppInfo;
19
use Nelexa\GPlay\Model\Category;
20
use Nelexa\GPlay\Model\Developer;
21
use Nelexa\GPlay\Model\GoogleImage;
22
use Nelexa\GPlay\Model\HistogramRating;
23
use Nelexa\GPlay\Model\Review;
24
use Nelexa\GPlay\Model\Video;
25
use Nelexa\GPlay\Scraper\Extractor\ReviewsExtractor;
26
use Nelexa\GPlay\Util\DateStringFormatter;
27
use Nelexa\GPlay\Util\LocaleHelper;
28
use Nelexa\GPlay\Util\ScraperUtil;
29
use Nelexa\HttpClient\ResponseHandlerInterface;
30
use Psr\Http\Message\RequestInterface;
31
use Psr\Http\Message\ResponseInterface;
32
use function GuzzleHttp\Psr7\parse_query;
33
34
/**
35
 * @internal
36
 */
37
class AppInfoScraper implements ResponseHandlerInterface
38
{
39
    /**
40
     * @param RequestInterface  $request
41
     * @param ResponseInterface $response
42
     *
43
     * @throws GooglePlayException
44
     *
45
     * @return AppInfo
46
     */
47 19
    public function __invoke(RequestInterface $request, ResponseInterface $response): AppInfo
48
    {
49 19
        $query = parse_query($request->getUri()->getQuery());
50
51 19
        $id = $query[GPlayApps::REQ_PARAM_ID];
52 19
        $locale = $query[GPlayApps::REQ_PARAM_LOCALE] ?? GPlayApps::DEFAULT_LOCALE;
53 19
        $country = $query[GPlayApps::REQ_PARAM_COUNTRY] ?? GPlayApps::DEFAULT_COUNTRY;
54
55
        /** @var array|null $scriptDataRating */
56
        [
57
            $scriptDataInfo,
58
            $scriptDataRating,
59
            $scriptDataPrice,
60
            $scriptDataVersion,
61
            $scriptDataReviews,
62 19
        ] = $this->getScriptData($request, $response);
63
64 19
        $name = $scriptDataInfo[0][0][0];
65 19
        $description = $this->extractDescription($scriptDataInfo);
66 19
        $translatedFromLocale = $this->extractTranslatedFromLocale($scriptDataInfo, $locale);
67 19
        $developer = $this->extractDeveloper($scriptDataInfo);
68 19
        if (isset($scriptDataInfo[0][12][13][0][0])) {
69 19
            $category = $this->extractCategory($scriptDataInfo[0][12][13][0]);
70
        }
71
        elseif (!empty($data[0][12][13][25])){
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data seems to never exist and therefore empty should always be true.
Loading history...
72
            $genreId = (string) $data[0][12][13][25];
73
            $genreName = ucwords(strtolower(str_replace(['_', 'AND'],[' ', '&'], $genreId)));
74
            $category = new Category($genreId, $genreName);
75
        }
76
        else{
77
            $category = null;
78
        }
79 19
        $summary = $this->extractSummary($scriptDataInfo);
80 19
        $installs = $scriptDataInfo[0][12][9][2] ?? 0;
81 19
        $score = (float) ($scriptDataRating[0][6][0][1] ?? 0);
82 19
        $numberVoters = (int) ($scriptDataRating[0][6][2][1] ?? 0);
83 19
        $numberReviews = (int) ($scriptDataRating[0][6][3][1] ?? 0);
84 19
        $histogramRating = $this->extractHistogramRating($scriptDataRating);
85 19
        $price = $scriptDataPrice !== null ? $this->extractPrice($scriptDataPrice) : 0.0;
86 19
        $currency = $scriptDataPrice[0][2][0][0][0][1][0][1] ?? 'USD';
87 19
        $priceText = $scriptDataPrice[0][2][0][0][0][1][0][2] ?? null;
88 19
        $offersIAPCost = $scriptDataInfo[0][12][12][0] ?? null;
89 19
        $containsAds = (bool) ($scriptDataInfo[0][12][14][0] ?? false);
90
91 19
        [$size, $appVersion, $androidVersion] = $scriptDataVersion;
92
93 19
        if (LocaleHelper::isDependOnDevice($locale, $size)) {
94 10
            $size = null;
95
        }
96
97 19
        if (LocaleHelper::isDependOnDevice($locale, $appVersion)) {
98 7
            $appVersion = null;
99
        }
100
101 19
        if (LocaleHelper::isDependOnDevice($locale, $androidVersion)) {
102 7
            $androidVersion = null;
103 7
            $minAndroidVersion = null;
104
        } else {
105 15
            $minAndroidVersion = preg_replace('~.*?(\d+(\.\d+)*).*~', '$1', $androidVersion);
106
        }
107
108 19
        $editorsChoice = !empty($scriptDataInfo[0][12][15][1][1]);
109 19
        $privacyPoliceUrl = $scriptDataInfo[0][12][7][2] ?? '';
110 19
        $categoryFamily = $this->extractCategory($scriptDataInfo[0][12][13][1] ?? []);
111 19
        $icon = $this->extractIcon($scriptDataInfo);
112 19
        $cover = $this->extractCover($scriptDataInfo);
113 19
        $screenshots = $this->extractScreenshots($scriptDataInfo);
114 19
        $video = $this->extractVideo($scriptDataInfo);
115 19
        $contentRating = $scriptDataInfo[0][12][4][0] ?? '';
116 19
        $released = $this->extractReleaseDate($scriptDataInfo, $locale);
117 19
        $updated = $this->extractUpdatedDate($scriptDataInfo);
118 19
        $recentChanges = $this->extractRecentChanges($scriptDataInfo);
119 19
        $reviews = $this->extractReviews(new AppId($id, $locale, $country), $scriptDataReviews);
120
121 19
        return AppInfo::newBuilder()
122 19
            ->setId($id)
123 19
            ->setLocale($locale)
124 19
            ->setCountry($country)
125 19
            ->setName($name)
126 19
            ->setDescription($description)
127 19
            ->setTranslatedFromLocale($translatedFromLocale)
128 19
            ->setSummary($summary)
129 19
            ->setIcon($icon)
130 19
            ->setCover($cover)
131 19
            ->setScreenshots($screenshots)
132 19
            ->setDeveloper($developer)
133 19
            ->setCategory($category)
134 19
            ->setCategoryFamily($categoryFamily)
135 19
            ->setVideo($video)
136 19
            ->setRecentChanges($recentChanges)
137 19
            ->setEditorsChoice($editorsChoice)
138 19
            ->setPrivacyPoliceUrl($privacyPoliceUrl)
139 19
            ->setInstalls($installs)
140 19
            ->setScore($score)
141 19
            ->setRecentChanges($recentChanges)
142 19
            ->setEditorsChoice($editorsChoice)
143 19
            ->setPrivacyPoliceUrl($privacyPoliceUrl)
144 19
            ->setInstalls($installs)
145 19
            ->setScore($score)
146 19
            ->setNumberVoters($numberVoters)
147 19
            ->setHistogramRating($histogramRating)
148 19
            ->setPrice($price)
149 19
            ->setCurrency($currency)
150 19
            ->setPriceText($priceText)
151 19
            ->setOffersIAPCost($offersIAPCost)
152 19
            ->setContainsAds($containsAds)
153 19
            ->setSize($size)
154 19
            ->setAppVersion($appVersion)
155 19
            ->setAndroidVersion($androidVersion)
156 19
            ->setMinAndroidVersion($minAndroidVersion)
157 19
            ->setContentRating($contentRating)
158 19
            ->setReleased($released)
159 19
            ->setUpdated($updated)
160 19
            ->setNumberReviews($numberReviews)
161 19
            ->setReviews($reviews)
162 19
            ->buildDetailInfo()
163
        ;
164
    }
165
166
    /**
167
     * @param RequestInterface  $request
168
     * @param ResponseInterface $response
169
     *
170
     * @throws GooglePlayException
171
     *
172
     * @return array
173
     */
174 19
    private function getScriptData(RequestInterface $request, ResponseInterface $response): array
175
    {
176 19
        $scriptData = ScraperUtil::extractScriptData($response->getBody()->getContents());
177
178 19
        $scriptDataInfo = null;
179 19
        $scriptDataRating = null;
180 19
        $scriptDataPrice = null;
181 19
        $scriptDataVersion = null;
182 19
        $scriptDataReviews = [];
183
184 19
        foreach ($scriptData as $key => $scriptValue) {
185 19
            if (isset($scriptValue[0][12][5][5][4][2])) { // ds:5
186 19
                $scriptDataInfo = $scriptValue;
187 19
            } elseif (isset($scriptValue[0][2][0][0][0][1][0][0])) { // ds:3
188 19
                $scriptDataPrice = $scriptValue;
189 19
            } elseif (isset($scriptValue[0][0][0])
190 19
                && \is_string($scriptValue[0][0][0])
191 19
                && strpos($scriptValue[0][0][0], 'gp:') === 0) { // ds:15
192 19
                $scriptDataReviews = $scriptValue;
193 19
            } elseif (isset($scriptValue[0][6][3][1])) { // ds:7
194 19
                $scriptDataRating = $scriptValue;
195 19
            } elseif (isset($scriptValue[0])
196 19
                && \is_string($scriptValue[0])
197 19
                && \count($scriptValue) === 3) { // ds:8
198 19
                $scriptDataVersion = $scriptValue;
199
            }
200
        }
201
202
        if (
203 19
            $scriptDataInfo === null ||
204 19
            $scriptDataVersion === null
0 ignored issues
show
introduced by
The condition $scriptDataVersion === null is always true.
Loading history...
205
        ) {
206
            throw (new GooglePlayException('Unable to get data for this application.'))->setUrl(
207
                $request->getUri()->__toString()
208
            );
209
        }
210
211 19
        return [$scriptDataInfo, $scriptDataRating, $scriptDataPrice, $scriptDataVersion, $scriptDataReviews];
212
    }
213
214
    /**
215
     * @param        $scriptDataInfo
216
     * @param string $locale
217
     *
218
     * @return string|null
219
     */
220 19
    private function extractTranslatedFromLocale(array $scriptDataInfo, string $locale): ?string
221
    {
222 19
        return isset($scriptDataInfo[0][19][1]) ?
223 14
            LocaleHelper::findPreferredLanguage(
224 14
                $locale,
225 14
                $scriptDataInfo[0][19][1]
226
            ) :
227 19
            null;
228
    }
229
230
    /**
231
     * @param array $scriptDataInfo
232
     *
233
     * @return string
234
     */
235 19
    private function extractDescription(array $scriptDataInfo): string
236
    {
237 19
        if (isset($scriptDataInfo[0][19][0][0][1])) {
238 14
            return ScraperUtil::html2text($scriptDataInfo[0][19][0][0][1]);
239
        }
240
241 19
        return ScraperUtil::html2text($scriptDataInfo[0][10][0][1]);
242
    }
243
244
    /**
245
     * @param $scriptDataInfo
246
     *
247
     * @return string|null
248
     */
249 19
    private function extractSummary(array $scriptDataInfo): ?string
250
    {
251 19
        return empty($scriptDataInfo[0][10][1][1]) ?
252
            null :
253 19
            ScraperUtil::html2text($scriptDataInfo[0][10][1][1]);
254
    }
255
256
    /**
257
     * @param array $scriptDataInfo
258
     *
259
     * @return Developer
260
     */
261 19
    private function extractDeveloper(array $scriptDataInfo): Developer
262
    {
263 19
        $developerPage = GPlayApps::GOOGLE_PLAY_URL . $scriptDataInfo[0][12][5][5][4][2];
264 19
        $developerId = parse_query(parse_url($developerPage, \PHP_URL_QUERY))[GPlayApps::REQ_PARAM_ID];
265 19
        $developerName = $scriptDataInfo[0][12][5][1];
266 19
        $developerEmail = $scriptDataInfo[0][12][5][2][0] ?? null;
267 19
        $developerWebsite = $scriptDataInfo[0][12][5][3][5][2] ?? null;
268 19
        $developerAddress = $scriptDataInfo[0][12][5][4][0] ?? null;
269
//        $developerInternalID = (int)$scriptDataInfo[0][12][5][0][0];
270
271 19
        return new Developer(
272 19
            Developer::newBuilder()
273 19
                ->setId($developerId)
274 19
                ->setUrl($developerPage)
275 19
                ->setName($developerName)
276 19
                ->setEmail($developerEmail)
277 19
                ->setAddress($developerAddress)
278 19
                ->setWebsite($developerWebsite)
279
        );
280
    }
281
282
    /**
283
     * @param array $data
284
     *
285
     * @return Category
286
     */
287 19
    private function extractCategory(array $data): ?Category
288
    {
289 19
        if (isset($data[0], $data[2])) {
290 19
            $genreId = (string) $data[2];
291 19
            $genreName = (string) $data[0];
292
293 19
            return new Category($genreId, $genreName);
294
        }
295
296 19
        return null;
297
    }
298
299
    /**
300
     * @param array|null $scriptDataRating
301
     *
302
     * @return HistogramRating
303
     */
304 19
    private function extractHistogramRating(?array $scriptDataRating): HistogramRating
305
    {
306 19
        return new HistogramRating(
307 19
            $scriptDataRating[0][6][1][5][1] ?? 0,
308 19
            $scriptDataRating[0][6][1][4][1] ?? 0,
309 19
            $scriptDataRating[0][6][1][3][1] ?? 0,
310 19
            $scriptDataRating[0][6][1][2][1] ?? 0,
311 19
            $scriptDataRating[0][6][1][1][1] ?? 0
312
        );
313
    }
314
315
    /**
316
     * @param array $scriptDataPrice
317
     *
318
     * @return float
319
     */
320 19
    protected function extractPrice(array $scriptDataPrice): ?float
321
    {
322 19
        return isset($scriptDataPrice[0][2][0][0][0][1][0][0]) ?
323 19
            (float) ($scriptDataPrice[0][2][0][0][0][1][0][0] / 1000000) :
324 19
            0.0;
325
    }
326
327
    /**
328
     * @param array $scriptDataInfo
329
     *
330
     * @return GoogleImage|null
331
     */
332 19
    protected function extractIcon(array $scriptDataInfo): ?GoogleImage
333
    {
334 19
        return empty($scriptDataInfo[0][12][1][3][2]) ?
335
            null :
336 19
            new GoogleImage($scriptDataInfo[0][12][1][3][2]);
337
    }
338
339
    /**
340
     * @param array $scriptDataInfo
341
     *
342
     * @return GoogleImage|null
343
     */
344 19
    protected function extractCover(array $scriptDataInfo): ?GoogleImage
345
    {
346 19
        return empty($scriptDataInfo[0][12][2][3][2]) ?
347
            null :
348 19
            new GoogleImage($scriptDataInfo[0][12][2][3][2]);
349
    }
350
351
    /**
352
     * @param array $scriptDataInfo
353
     *
354
     * @return GoogleImage[]
355
     */
356 19
    private function extractScreenshots(array $scriptDataInfo): array
357
    {
358 19
        return !empty($scriptDataInfo[0][12][0]) ? array_map(
359
            static function (array $v) {
360 19
                return new GoogleImage($v[3][2]);
361 19
            },
362 19
            $scriptDataInfo[0][12][0]
363 19
        ) : [];
364
    }
365
366
    /**
367
     * @param array $scriptDataInfo
368
     *
369
     * @return Video|null
370
     */
371 19
    private function extractVideo(array $scriptDataInfo): ?Video
372
    {
373
        if (
374 19
            isset($scriptDataInfo[0][12][3][0][3][2]) &&
375 19
            $scriptDataInfo[0][12][3][0][3][2] !== null &&
376 19
            $scriptDataInfo[0][12][3][1][3][2] !== null
377
        ) {
378 8
            $videoThumb = (string) $scriptDataInfo[0][12][3][1][3][2];
379 8
            $videoUrl = (string) $scriptDataInfo[0][12][3][0][3][2];
380
381 8
            return new Video($videoThumb, $videoUrl);
382
        }
383
384 12
        return null;
385
    }
386
387
    /**
388
     * @param array  $scriptDataInfo
389
     * @param string $locale
390
     *
391
     * @return \DateTimeInterface|null
392
     */
393 19
    private function extractReleaseDate(array $scriptDataInfo, string $locale): ?\DateTimeInterface
394
    {
395 19
        if (isset($scriptDataInfo[0][12][36])) {
396 18
            return DateStringFormatter::formatted($locale, $scriptDataInfo[0][12][36]);
397
        }
398
399 1
        return null;
400
    }
401
402
    /**
403
     * @param array $scriptDataInfo
404
     *
405
     * @return \DateTimeInterface|null
406
     */
407 19
    private function extractUpdatedDate(array $scriptDataInfo): ?\DateTimeInterface
408
    {
409 19
        if (isset($scriptDataInfo[0][12][8][0])) {
410 19
            return DateStringFormatter::unixTimeToDateTime($scriptDataInfo[0][12][8][0]);
411
        }
412
413
        return null;
414
    }
415
416
    /**
417
     * @param $scriptDataInfo
418
     *
419
     * @return string|null
420
     */
421 19
    protected function extractRecentChanges($scriptDataInfo): ?string
422
    {
423 19
        return empty($scriptDataInfo[0][12][6][1]) ?
424 1
            null :
425 19
            ScraperUtil::html2text($scriptDataInfo[0][12][6][1]);
426
    }
427
428
    /**
429
     * @param AppId $appId
430
     * @param array $scriptDataReviews
431
     * @param int   $limit
432
     *
433
     * @return Review[]
434
     */
435 19
    private function extractReviews(AppId $appId, array $scriptDataReviews, int $limit = 4): array
436
    {
437 19
        if (empty($scriptDataReviews[0])) {
438 12
            return [];
439
        }
440
441 19
        return ReviewsExtractor::extractReviews(
442 19
            $appId,
443 19
            \array_slice($scriptDataReviews[0], 0, $limit)
444
        );
445
    }
446
}
447