Passed
Push — master ( d8c162...9cc772 )
by Alexey
04:05 queued 12s
created

AppInfoScraper::__invoke()   C

Complexity

Conditions 8
Paths 36

Size

Total Lines 113
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 89
CRAP Score 8.0022

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 92
c 1
b 1
f 0
dl 0
loc 113
ccs 89
cts 92
cp 0.9674
rs 6.9301
cc 8
nc 36
nop 3
crap 8.0022

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * 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\Scraper;
15
16
use GuzzleHttp\Psr7\Query;
17
use Nelexa\GPlay\Exception\GooglePlayException;
18
use Nelexa\GPlay\GPlayApps;
19
use Nelexa\GPlay\HttpClient\ParseHandlerInterface;
20
use Nelexa\GPlay\Model\AppId;
21
use Nelexa\GPlay\Model\AppInfo;
22
use Nelexa\GPlay\Model\Category;
23
use Nelexa\GPlay\Model\Developer;
24
use Nelexa\GPlay\Model\GoogleImage;
25
use Nelexa\GPlay\Model\HistogramRating;
26
use Nelexa\GPlay\Model\Review;
27
use Nelexa\GPlay\Model\Video;
28
use Nelexa\GPlay\Scraper\Extractor\ReviewsExtractor;
29
use Nelexa\GPlay\Util\DateStringFormatter;
30
use Nelexa\GPlay\Util\ScraperUtil;
31
use Psr\Http\Message\RequestInterface;
32
use Psr\Http\Message\ResponseInterface;
33
34
/**
35
 * @internal
36
 */
37
class AppInfoScraper implements ParseHandlerInterface
38
{
39
    /**
40
     * @param RequestInterface  $request
41
     * @param ResponseInterface $response
42
     * @param array             $options
43
     *
44
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
45
     *
46
     * @return AppInfo
47
     */
48 10
    public function __invoke(RequestInterface $request, ResponseInterface $response, array &$options = []): AppInfo
49
    {
50 10
        $scriptData = ScraperUtil::extractScriptData($response->getBody()->getContents());
51
52 10
        $appInfo = null;
53 10
        $editorsChoice = false;
54
55 10
        foreach ($scriptData as $data) {
56 10
            if (isset($data[1][2][72][0][1])) {
57 10
                $appInfo = $data[1][2];
58 10
            } elseif (isset($data[1][2][136][0][1][0])) {
59
                $editorsChoice = (bool) $data[1][2][136][0][1][0];
60
            }
61
        }
62
63 10
        if (!\is_array($appInfo)) {
64
            throw (new GooglePlayException('Unable to get data for this application.'))->setUrl(
65
                $request->getUri()->__toString()
66
            );
67
        }
68
69 10
        $query = Query::parse($request->getUri()->getQuery());
70 10
        $id = $query[GPlayApps::REQ_PARAM_ID];
71 10
        $locale = $query[GPlayApps::REQ_PARAM_LOCALE] ?? GPlayApps::DEFAULT_LOCALE;
72 10
        $country = $query[GPlayApps::REQ_PARAM_COUNTRY] ?? GPlayApps::DEFAULT_COUNTRY;
73
74 10
        $name = $appInfo[0][0];
75 10
        $description = ScraperUtil::html2text($appInfo[72][0][1]);
76 10
        $developer = $this->extractDeveloper($appInfo);
77
78 10
        $category = $this->extractCategory($appInfo[79][0][0] ?? []);
79 10
        $summary = $this->extractSummary($appInfo);
80 10
        $installsText = $appInfo[13][0] ?? null;
81 10
        $installs = $appInfo[13][2] ?? 0;
82 10
        $score = (float) ($appInfo[51][0][1] ?? 0);
83 10
        $numberVoters = (int) ($appInfo[51][2][1] ?? 0);
84 10
        $numberReviews = (int) ($appInfo[51][3][1] ?? 0);
85 10
        $histogramRating = null;
86 10
        if (isset($appInfo[51][1])) {
87 10
            $histogramRating = $this->extractHistogramRating($appInfo[51][1]);
88
        }
89
90 10
        $scriptDataPrice = $appInfo[57][0][0][0][0][1] ?? null;
91 10
        $price = $scriptDataPrice !== null ? $this->extractPrice($scriptDataPrice) : 0.0;
92 10
        $currency = $scriptDataPrice[0][1] ?? 'USD';
93 10
        $priceText = $scriptDataPrice[0][2] ?? null;
94
95 10
        $offersIAPCost = $appInfo[19][0] ?? null;
96 10
        $containsAds = (bool) ($appInfo[48][0] ?? false);
97
98 10
        $androidVersion = $appInfo[140][1][1][0][0][1] ?? null;
99 10
        $appVersion = $appInfo[140][0][0][0] ?? null;
100
101 10
        if ($androidVersion !== null) {
102 4
            $minAndroidVersion = preg_replace('~.*?(\d+(\.\d+)*).*~', '$1', $androidVersion);
103
        } else {
104 7
            $minAndroidVersion = null;
105
        }
106
107 10
        $privacyPoliceUrl = $appInfo[99][0][5][2] ?? '';
108 10
        $categoryFamily = $this->extractCategory($appInfo[118][0][0][0] ?? []);
109 10
        $icon = $this->extractIcon($appInfo);
110 10
        $cover = $this->extractCover($appInfo);
111 10
        $screenshots = $this->extractScreenshots($appInfo);
112 10
        $video = $this->extractVideo($appInfo);
113 10
        $contentRating = $appInfo[111][1] ?? '';
114 10
        $released = $this->convertDate($appInfo[10][1][0] ?? null);
115 10
        $updated = $this->convertDate($appInfo[145][0][1][0] ?? null);
116 10
        $recentChanges = $this->extractRecentChanges($appInfo);
117
118 10
        $reviews = $this->extractReviews(new AppId($id, $locale, $country), $scriptData);
119
120 10
        return AppInfo::newBuilder()
121 10
            ->setId($id)
122 10
            ->setLocale($locale)
123 10
            ->setCountry($country)
124 10
            ->setName($name)
125 10
            ->setDescription($description)
126 10
            ->setSummary($summary)
127 10
            ->setIcon($icon)
128 10
            ->setCover($cover)
129 10
            ->setScreenshots($screenshots)
130 10
            ->setDeveloper($developer)
131 10
            ->setCategory($category)
132 10
            ->setCategoryFamily($categoryFamily)
133 10
            ->setVideo($video)
134 10
            ->setRecentChanges($recentChanges)
135 10
            ->setEditorsChoice($editorsChoice)
136 10
            ->setPrivacyPoliceUrl($privacyPoliceUrl)
137 10
            ->setInstalls($installs)
138 10
            ->setInstallsText($installsText)
139 10
            ->setScore($score)
140 10
            ->setRecentChanges($recentChanges)
141 10
            ->setEditorsChoice($editorsChoice)
142 10
            ->setPrivacyPoliceUrl($privacyPoliceUrl)
143 10
            ->setInstalls($installs)
144 10
            ->setScore($score)
145 10
            ->setNumberVoters($numberVoters)
146 10
            ->setHistogramRating($histogramRating)
147 10
            ->setPrice($price)
148 10
            ->setCurrency($currency)
149 10
            ->setPriceText($priceText)
150 10
            ->setOffersIAPCost($offersIAPCost)
151 10
            ->setContainsAds($containsAds)
152 10
            ->setAppVersion($appVersion)
153 10
            ->setAndroidVersion($androidVersion)
154 10
            ->setMinAndroidVersion($minAndroidVersion)
155 10
            ->setContentRating($contentRating)
156 10
            ->setReleased($released)
157 10
            ->setUpdated($updated)
158 10
            ->setNumberReviews($numberReviews)
159 10
            ->setReviews($reviews)
160 10
            ->buildDetailInfo()
161
        ;
162
    }
163
164
    /**
165
     * @param array $appInfo
166
     *
167
     * @return string|null
168
     */
169 10
    private function extractSummary(array $appInfo): ?string
170
    {
171 10
        return empty($appInfo[73][0][1])
172
            ? null
173 10
            : ScraperUtil::html2text(str_replace("\n", ' ', $appInfo[73][0][1]));
174
    }
175
176
    /**
177
     * @param array $appInfo
178
     *
179
     * @return Developer
180
     */
181 10
    private function extractDeveloper(array $appInfo): Developer
182
    {
183 10
        $developerPage = GPlayApps::GOOGLE_PLAY_URL . $appInfo[68][1][4][2];
184 10
        $developerId = Query::parse(parse_url($developerPage, \PHP_URL_QUERY))[GPlayApps::REQ_PARAM_ID];
185 10
        $developerName = $appInfo[68][0];
186 10
        $developerEmail = $appInfo[69][1][0] ?? null;
187 10
        $developerWebsite = $appInfo[69][0][5][2] ?? null;
188 10
        $developerAddress = $appInfo[69][2][0] ?? null;
189
190 10
        return new Developer(
191 10
            Developer::newBuilder()
192 10
                ->setId($developerId)
193 10
                ->setUrl($developerPage)
194 10
                ->setName($developerName)
195 10
                ->setEmail($developerEmail)
196 10
                ->setAddress($developerAddress)
197 10
                ->setWebsite($developerWebsite)
198
        );
199
    }
200
201
    /**
202
     * @param array $data
203
     *
204
     * @return Category|null
205
     */
206 10
    private function extractCategory(array $data): ?Category
207
    {
208 10
        if (isset($data[1][4][2], $data[0], $data[2])) {
209 10
            $categorySlug = (string) $data[2];
210 10
            $categoryName = (string) $data[0];
211
212 10
            return new Category($categorySlug, $categoryName);
213
        }
214
215 9
        return null;
216
    }
217
218
    /**
219
     * @param array|null $data
220
     *
221
     * @return HistogramRating
222
     */
223 10
    private function extractHistogramRating(array $data): HistogramRating
224
    {
225 10
        return new HistogramRating(
226 10
            $data[1][1] ?? 0,
227 10
            $data[2][1] ?? 0,
228 10
            $data[3][1] ?? 0,
229 10
            $data[4][1] ?? 0,
230 10
            $data[5][1] ?? 0
231
        );
232
    }
233
234
    /**
235
     * @param array $scriptDataPrice
236
     *
237
     * @return float
238
     */
239 10
    protected function extractPrice(array $scriptDataPrice): ?float
240
    {
241 10
        return isset($scriptDataPrice[0][0])
242 10
            ? (float) ($scriptDataPrice[0][0] / 1000000)
243 10
            : 0.0;
244
    }
245
246
    /**
247
     * @param array $data
248
     *
249
     * @return GoogleImage|null
250
     */
251 10
    protected function extractIcon(array $data): ?GoogleImage
252
    {
253 10
        return empty($data[95][0][3][2])
254
            ? null
255 10
            : new GoogleImage($data[95][0][3][2]);
256
    }
257
258
    /**
259
     * @param array $data
260
     *
261
     * @return GoogleImage|null
262
     */
263 10
    protected function extractCover(array $data): ?GoogleImage
264
    {
265 10
        return empty($data[96][0][3][2])
266
            ? null
267 10
            : new GoogleImage($data[96][0][3][2]);
268
    }
269
270
    /**
271
     * @param array $data
272
     *
273
     * @return GoogleImage[]
274
     */
275 10
    private function extractScreenshots(array $data): array
276
    {
277 10
        return !empty($data[78][0][0][3][2]) ? array_map(
278 10
            static function (array $v) {
279 10
                return new GoogleImage($v[3][2]);
280
            },
281 10
            $data[78][0]
282 10
        ) : [];
283
    }
284
285
    /**
286
     * @param array $data
287
     *
288
     * @return Video|null
289
     */
290 10
    private function extractVideo(array $data): ?Video
291
    {
292 10
        if (isset($data[100][0][0][4], $data[100][0][1][3][3])) {
293
            $videoThumb = (string) $data[100][0][1][3][2];
294
            $youtubeId = (string) $data[100][0][4];
295
            $youtubeId = str_replace('yt:', '', strtok($youtubeId, '?'));
296
            $videoUrl = 'https://www.youtube.com/embed/' . $youtubeId . '?ps=play&vq=large&rel=0&autohide=1&showinfo=0';
297
298
            return new Video($videoThumb, $videoUrl);
299
        }
300
301 10
        return null;
302
    }
303
304
    /**
305
     * @param ?int $timestamp
306
     *
307
     * @return \DateTimeInterface|null
308
     */
309 10
    private function convertDate(?int $timestamp): ?\DateTimeInterface
310
    {
311 10
        if ($timestamp !== null) {
312 10
            return DateStringFormatter::unixTimeToDateTime($timestamp);
313
        }
314
315 2
        return null;
316
    }
317
318
    /**
319
     * @param array $data
320
     *
321
     * @return string|null
322
     */
323 10
    protected function extractRecentChanges(array $data): ?string
324
    {
325 10
        return empty($data[144][1][1])
326 2
            ? null
327 10
            : ScraperUtil::html2text($data[144][1][1]);
328
    }
329
330
    /**
331
     * @param AppId $appId
332
     * @param array $data
333
     * @param array $scripData
334
     *
335
     * @return Review[]
336
     */
337 10
    private function extractReviews(AppId $appId, array $scripData): array
338
    {
339 10
        $data = null;
340 10
        foreach ($scripData as $value) {
341
            if (
342 10
                isset($value[0][0][0])
343 10
                && \is_string($value[0][0][0])
344 10
                && preg_match('~^[0-9a-f]{8}-[0-9a-f]{4}-~', $value[0][0][0])
345
            ) {
346 10
                $data = $value[0];
347 10
                break;
348
            }
349
        }
350
351 10
        if ($data === null) {
352 1
            return [];
353
        }
354
355 10
        return ReviewsExtractor::extractReviews($appId, $data);
356
    }
357
}
358