Passed
Push — master ( f86ea9...d8c162 )
by Alexey
09:45
created

AppInfoScraper   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Test Coverage

Coverage 93.9%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 31
eloc 153
c 1
b 1
f 0
dl 0
loc 310
ccs 154
cts 164
cp 0.939
rs 9.92

13 Methods

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