Passed
Pull Request — master (#4)
by
unknown
07:24
created

AppInfoScraper::extractUpdatedDate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 10
cc 2
nc 2
nop 1
crap 2.0625
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
            throw new GooglePlayException('No data to create object '.Category::class);
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 = $this->extractPrice($scriptDataPrice);
86 19
        $currency = $scriptDataPrice[0][2][0][0][0][1][0][1];
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 12
            $size = null;
95
        }
96
97 19
        if (LocaleHelper::isDependOnDevice($locale, $appVersion)) {
98 8
            $appVersion = null;
99
        }
100
101 19
        if (LocaleHelper::isDependOnDevice($locale, $androidVersion)) {
102 8
            $androidVersion = null;
103 8
            $minAndroidVersion = null;
104
        } else {
105 12
            $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
            $scriptDataPrice === null ||
205 19
            $scriptDataVersion === null
0 ignored issues
show
introduced by
The condition $scriptDataVersion === null is always true.
Loading history...
206
        ) {
207
            throw (new GooglePlayException('Unable to get data for this application.'))->setUrl(
208
                $request->getUri()->__toString()
209
            );
210
        }
211
212 19
        return [$scriptDataInfo, $scriptDataRating, $scriptDataPrice, $scriptDataVersion, $scriptDataReviews];
213
    }
214
215
    /**
216
     * @param        $scriptDataInfo
217
     * @param string $locale
218
     *
219
     * @return string|null
220
     */
221 19
    private function extractTranslatedFromLocale(array $scriptDataInfo, string $locale): ?string
222
    {
223 19
        return isset($scriptDataInfo[0][19][1]) ?
224 14
            LocaleHelper::findPreferredLanguage(
225 14
                $locale,
226 14
                $scriptDataInfo[0][19][1]
227
            ) :
228 19
            null;
229
    }
230
231
    /**
232
     * @param array $scriptDataInfo
233
     *
234
     * @return string
235
     */
236 19
    private function extractDescription(array $scriptDataInfo): string
237
    {
238 19
        if (isset($scriptDataInfo[0][19][0][0][1])) {
239 14
            return ScraperUtil::html2text($scriptDataInfo[0][19][0][0][1]);
240
        }
241
242 19
        return ScraperUtil::html2text($scriptDataInfo[0][10][0][1]);
243
    }
244
245
    /**
246
     * @param $scriptDataInfo
247
     *
248
     * @return string|null
249
     */
250 19
    private function extractSummary(array $scriptDataInfo): ?string
251
    {
252 19
        return empty($scriptDataInfo[0][10][1][1]) ?
253
            null :
254 19
            ScraperUtil::html2text($scriptDataInfo[0][10][1][1]);
255
    }
256
257
    /**
258
     * @param array $scriptDataInfo
259
     *
260
     * @return Developer
261
     */
262 19
    private function extractDeveloper(array $scriptDataInfo): Developer
263
    {
264 19
        $developerPage = GPlayApps::GOOGLE_PLAY_URL . $scriptDataInfo[0][12][5][5][4][2];
265 19
        $developerId = parse_query(parse_url($developerPage, \PHP_URL_QUERY))[GPlayApps::REQ_PARAM_ID];
266 19
        $developerName = $scriptDataInfo[0][12][5][1];
267 19
        $developerEmail = $scriptDataInfo[0][12][5][2][0] ?? null;
268 19
        $developerWebsite = $scriptDataInfo[0][12][5][3][5][2] ?? null;
269 19
        $developerAddress = $scriptDataInfo[0][12][5][4][0] ?? null;
270
//        $developerInternalID = (int)$scriptDataInfo[0][12][5][0][0];
271
272 19
        return new Developer(
273 19
            Developer::newBuilder()
274 19
                ->setId($developerId)
275 19
                ->setUrl($developerPage)
276 19
                ->setName($developerName)
277 19
                ->setEmail($developerEmail)
278 19
                ->setAddress($developerAddress)
279 19
                ->setWebsite($developerWebsite)
280
        );
281
    }
282
283
    /**
284
     * @param array $data
285
     *
286
     * @return Category
287
     */
288 19
    private function extractCategory(array $data): ?Category
289
    {
290 19
        if (isset($data[0], $data[2])) {
291 19
            $genreId = (string) $data[2];
292 19
            $genreName = (string) $data[0];
293
294 19
            return new Category($genreId, $genreName);
295
        }
296
297 17
        return null;
298
    }
299
300
    /**
301
     * @param array|null $scriptDataRating
302
     *
303
     * @return HistogramRating
304
     */
305 19
    private function extractHistogramRating(?array $scriptDataRating): HistogramRating
306
    {
307 19
        return new HistogramRating(
308 19
            $scriptDataRating[0][6][1][5][1] ?? 0,
309 19
            $scriptDataRating[0][6][1][4][1] ?? 0,
310 19
            $scriptDataRating[0][6][1][3][1] ?? 0,
311 19
            $scriptDataRating[0][6][1][2][1] ?? 0,
312 19
            $scriptDataRating[0][6][1][1][1] ?? 0
313
        );
314
    }
315
316
    /**
317
     * @param $scriptDataPrice
318
     *
319
     * @return float
320
     */
321 19
    protected function extractPrice(array $scriptDataPrice): ?float
322
    {
323 19
        return isset($scriptDataPrice[0][2][0][0][0][1][0][0]) ?
324 19
            (float) ($scriptDataPrice[0][2][0][0][0][1][0][0] / 1000000) :
325 19
            0.0;
326
    }
327
328
    /**
329
     * @param array $scriptDataInfo
330
     *
331
     * @return GoogleImage|null
332
     */
333 19
    protected function extractIcon(array $scriptDataInfo): ?GoogleImage
334
    {
335 19
        return empty($scriptDataInfo[0][12][1][3][2]) ?
336
            null :
337 19
            new GoogleImage($scriptDataInfo[0][12][1][3][2]);
338
    }
339
340
    /**
341
     * @param array $scriptDataInfo
342
     *
343
     * @return GoogleImage|null
344
     */
345 19
    protected function extractCover(array $scriptDataInfo): ?GoogleImage
346
    {
347 19
        return empty($scriptDataInfo[0][12][2][3][2]) ?
348
            null :
349 19
            new GoogleImage($scriptDataInfo[0][12][2][3][2]);
350
    }
351
352
    /**
353
     * @param array $scriptDataInfo
354
     *
355
     * @return GoogleImage[]
356
     */
357 19
    private function extractScreenshots(array $scriptDataInfo): array
358
    {
359 19
        return !empty($scriptDataInfo[0][12][0]) ? array_map(
360
            static function (array $v) {
361 19
                return new GoogleImage($v[3][2]);
362 19
            },
363 19
            $scriptDataInfo[0][12][0]
364 19
        ) : [];
365
    }
366
367
    /**
368
     * @param array $scriptDataInfo
369
     *
370
     * @return Video|null
371
     */
372 19
    private function extractVideo(array $scriptDataInfo): ?Video
373
    {
374
        if (
375 19
            isset($scriptDataInfo[0][12][3][0][3][2]) &&
376 19
            $scriptDataInfo[0][12][3][0][3][2] !== null &&
377 19
            $scriptDataInfo[0][12][3][1][3][2] !== null
378
        ) {
379 8
            $videoThumb = (string) $scriptDataInfo[0][12][3][1][3][2];
380 8
            $videoUrl = (string) $scriptDataInfo[0][12][3][0][3][2];
381
382 8
            return new Video($videoThumb, $videoUrl);
383
        }
384
385 12
        return null;
386
    }
387
388
    /**
389
     * @param array  $scriptDataInfo
390
     * @param string $locale
391
     *
392
     * @return \DateTimeInterface|null
393
     */
394 19
    private function extractReleaseDate(array $scriptDataInfo, string $locale): ?\DateTimeInterface
395
    {
396 19
        if (isset($scriptDataInfo[0][12][36])) {
397 18
            return DateStringFormatter::formatted($locale, $scriptDataInfo[0][12][36]);
398
        }
399
400 1
        return null;
401
    }
402
403
    /**
404
     * @param array $scriptDataInfo
405
     *
406
     * @return \DateTimeInterface|null
407
     */
408 19
    private function extractUpdatedDate(array $scriptDataInfo): ?\DateTimeInterface
409
    {
410 19
        if (isset($scriptDataInfo[0][12][8][0])) {
411 19
            return DateStringFormatter::unixTimeToDateTime($scriptDataInfo[0][12][8][0]);
412
        }
413
414
        return null;
415
    }
416
417
    /**
418
     * @param $scriptDataInfo
419
     *
420
     * @return string|null
421
     */
422 19
    protected function extractRecentChanges($scriptDataInfo): ?string
423
    {
424 19
        return empty($scriptDataInfo[0][12][6][1]) ?
425 1
            null :
426 19
            ScraperUtil::html2text($scriptDataInfo[0][12][6][1]);
427
    }
428
429
    /**
430
     * @param AppId $appId
431
     * @param array $scriptDataReviews
432
     * @param int   $limit
433
     *
434
     * @return Review[]
435
     */
436 19
    private function extractReviews(AppId $appId, array $scriptDataReviews, int $limit = 4): array
437
    {
438 19
        if (empty($scriptDataReviews[0])) {
439 12
            return [];
440
        }
441
442 19
        return ReviewsExtractor::extractReviews(
443 19
            $appId,
444 19
            \array_slice($scriptDataReviews[0], 0, $limit)
445
        );
446
    }
447
}
448