Passed
Branch develop (61f351)
by Alexey
01:52
created

AppDetailScraper::extractCategory()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 4
nc 2
nop 1
1
<?php
2
/** @noinspection MultiAssignmentUsageInspection */
3
declare(strict_types=1);
4
5
namespace Nelexa\GPlay\Scraper;
6
7
use Nelexa\GPlay\Exception\GooglePlayException;
8
use Nelexa\GPlay\GPlayApps;
9
use Nelexa\GPlay\Http\ResponseHandlerInterface;
10
use Nelexa\GPlay\Model\AppDetail;
11
use Nelexa\GPlay\Model\Category;
12
use Nelexa\GPlay\Model\Developer;
13
use Nelexa\GPlay\Model\GoogleImage;
14
use Nelexa\GPlay\Model\HistogramRating;
15
use Nelexa\GPlay\Model\Review;
16
use Nelexa\GPlay\Model\Video;
17
use Nelexa\GPlay\Request\RequestApp;
18
use Nelexa\GPlay\Scraper\Extractor\ReviewsExtractor;
19
use Nelexa\GPlay\Util\DateStringFormatter;
20
use Nelexa\GPlay\Util\LocaleHelper;
21
use Nelexa\GPlay\Util\ScraperUtil;
22
use Psr\Http\Message\RequestInterface;
23
use Psr\Http\Message\ResponseInterface;
24
use function GuzzleHttp\Psr7\parse_query;
25
26
class AppDetailScraper implements ResponseHandlerInterface
27
{
28
    /**
29
     * @param RequestInterface $request
30
     * @param ResponseInterface $response
31
     * @return AppDetail
32
     * @throws GooglePlayException
33
     */
34
    public function __invoke(RequestInterface $request, ResponseInterface $response): AppDetail
35
    {
36
        $query = parse_query($request->getUri()->getQuery());
37
        $appId = $query[GPlayApps::REQ_PARAM_ID];
38
        $locale = $query[GPlayApps::REQ_PARAM_LOCALE] ?? GPlayApps::DEFAULT_LOCALE;
39
        $country = $query[GPlayApps::REQ_PARAM_COUNTRY] ?? GPlayApps::DEFAULT_COUNTRY;
40
        $requestApp = new RequestApp($appId, $locale, $country);
41
42
        $scriptData = ScraperUtil::extractScriptData($response->getBody()->getContents());
43
44
        $scriptDataInfo = null;
45
        $scriptDataRating = null;
46
        $scriptDataPrice = null;
47
        $scriptDataVersion = null;
48
        $scriptDataReviews = [];
49
50
        foreach ($scriptData as $key => $scriptValue) {
51
            if (isset($scriptValue[0][12][5][5][4][2])) { // ds:5
52
                $scriptDataInfo = $scriptValue;
53
            } elseif (isset($scriptValue[0][2][0][0][0][1][0][0])) { // ds:3
54
                $scriptDataPrice = $scriptValue;
55
            } elseif (isset($scriptValue[0][0][0])
56
                && is_string($scriptValue[0][0][0])
57
                && strpos($scriptValue[0][0][0], 'gp:') === 0) { // ds:15
58
                $scriptDataReviews = $scriptValue;
59
            } elseif (isset($scriptValue[0][6][3][1])) { // ds:7
60
                $scriptDataRating = $scriptValue;
61
            } elseif (isset($scriptValue[0])
62
                && is_string($scriptValue[0])
63
                && count($scriptValue) === 3) { // ds:8
64
                $scriptDataVersion = $scriptValue;
65
            }
66
        }
67
68
        if (
69
            $scriptDataInfo === null ||
70
            $scriptDataRating === null ||
71
            $scriptDataPrice === null ||
72
            $scriptDataVersion === null
0 ignored issues
show
introduced by
The condition $scriptDataVersion === null is always true.
Loading history...
73
        ) {
74
            throw (new GooglePlayException('Unable to get data for this application.'))->setUrl($request->getUri()->__toString());
75
        }
76
77
        $name = $scriptDataInfo[0][0][0];
78
        $descriptionHTML = $scriptDataInfo[0][10][0][1];
79
        $description = ScraperUtil::html2text($descriptionHTML);
80
81
        $developer = $this->extractDeveloper($scriptDataInfo);
82
        $category = $this->extractCategory($scriptDataInfo[0][12][13][0]);
83
84
        $summary = empty($scriptDataInfo[0][10][1][1]) ?
85
            null :
86
            ScraperUtil::html2text($scriptDataInfo[0][10][1][1]);
87
88
        $installs = $scriptDataInfo[0][12][9][2] ?? 0;
89
        $score = (float)($scriptDataRating[0][6][0][1] ?? 0);
90
        $numberVoters = (int)($scriptDataRating[0][6][2][1] ?? 0);
91
        $reviewsCount = (int)($scriptDataRating[0][6][3][1] ?? 0);
92
        $histogram = $scriptDataRating[0][6][1] ?? null;
93
94
        $histogramRating = new HistogramRating(
95
            $histogram[5][1] ?? 0,
96
            $histogram[4][1] ?? 0,
97
            $histogram[3][1] ?? 0,
98
            $histogram[2][1] ?? 0,
99
            $histogram[1][1] ?? 0
100
        );
101
102
        $price = isset($scriptDataPrice[0][2][0][0][0][1][0][0]) ?
103
            (float)($scriptDataPrice[0][2][0][0][0][1][0][0] / 1000000) :
104
            0;
105
        $currency = $scriptDataPrice[0][2][0][0][0][1][0][1];
106
        $priceText = $scriptDataPrice[0][2][0][0][0][1][0][2] ?: 'Free';
107
        $offersIAPCost = $scriptDataInfo[0][12][12][0] ?? null;
108
        $adSupported = (bool)$scriptDataInfo[0][12][14][0];
109
110
        [$size, $appVersion, $androidVersion] = $scriptDataVersion;
111
        if (LocaleHelper::isDependOnDevice($locale, $size)) {
112
            $size = null;
113
        }
114
        if (LocaleHelper::isDependOnDevice($locale, $appVersion)) {
115
            $appVersion = null;
116
        }
117
        if (LocaleHelper::isDependOnDevice($locale, $androidVersion)) {
118
            $androidVersion = null;
119
            $minAndroidVersion = null;
120
        } else {
121
            $minAndroidVersion = preg_replace('~.*?(\d+(\.\d+)*).*~', '$1', $androidVersion);
122
        }
123
124
        $editorsChoice = !empty($scriptDataInfo[0][12][15][1][1]);
125
        $privacyPoliceUrl = $scriptDataInfo[0][12][7][2];
126
127
        $categoryFamily = $this->extractCategory($scriptDataInfo[0][12][13][1] ?? []);
128
129
        $icon = empty($scriptDataInfo[0][12][1][3][2]) ?
130
            null :
131
            new GoogleImage($scriptDataInfo[0][12][1][3][2]);
132
133
        $headerImage = empty($scriptDataInfo[0][12][2][3][2]) ?
134
            null :
135
            new GoogleImage($scriptDataInfo[0][12][2][3][2]);
136
137
        $screenshots = $this->extractScreenshots($scriptDataInfo);
138
        $video = $this->extractVideo($scriptDataInfo);
139
140
        $contentRating = $scriptDataInfo[0][12][4][0];
141
        $released = $this->extractReleaseDate($scriptDataInfo, $locale);
142
        $updated = $this->extractUpdatedDate($scriptDataInfo);
143
144
        $recentChanges = empty($scriptDataInfo[0][12][6][1]) ?
145
            null :
146
            ScraperUtil::html2text($scriptDataInfo[0][12][6][1]);
147
148
        $translatedFromLanguage = null;
149
        $translatedDescription = null;
150
        if (isset($scriptDataInfo[0][19][1])) {
151
            $translatedFromLanguage = LocaleHelper::findPreferredLanguage(
152
                $locale,
153
                $scriptDataInfo[0][19][1]
154
            );
155
            $translatedDescription = ScraperUtil::html2text($scriptDataInfo[0][19][0][0][1]);
156
        }
157
158
        $reviews = $this->extractReviews($requestApp, $scriptDataReviews);
159
160
        return new AppDetail(
161
            AppDetail::newBuilder()
162
                ->setId($appId)
163
                ->setUrl($requestApp->getUrl())
164
                ->setLocale($locale)
165
                ->setName($name)
166
                ->setDescription($description)
167
                ->setTranslated($translatedDescription, $translatedFromLanguage)
168
                ->setSummary($summary)
169
                ->setIcon($icon)
170
                ->setHeaderImage($headerImage)
171
                ->setScreenshots($screenshots)
172
                ->setDeveloper($developer)
173
                ->setCategory($category)
174
                ->setCategoryFamily($categoryFamily)
175
                ->setVideo($video)
176
                ->setRecentChanges($recentChanges)
177
                ->setEditorsChoice($editorsChoice)
178
                ->setPrivacyPoliceUrl($privacyPoliceUrl)
179
                ->setInstalls($installs)
180
                ->setScore($score)
181
                ->setRecentChanges($recentChanges)
182
                ->setEditorsChoice($editorsChoice)
183
                ->setPrivacyPoliceUrl($privacyPoliceUrl)
184
                ->setInstalls($installs)
185
                ->setScore($score)
186
                ->setNumberVoters($numberVoters)
187
                ->setHistogramRating($histogramRating)
188
                ->setPrice($price)
189
                ->setCurrency($currency)
190
                ->setPriceText($priceText)
191
                ->setOffersIAPCost($offersIAPCost)
192
                ->setAdSupported($adSupported)
193
                ->setAppSize($size)
194
                ->setAppVersion($appVersion)
195
                ->setAndroidVersion($androidVersion)
196
                ->setMinAndroidVersion($minAndroidVersion)
197
                ->setContentRating($contentRating)
198
                ->setReleased($released)
199
                ->setUpdated($updated)
200
                ->setReviewsCount($reviewsCount)
201
                ->setReviews($reviews)
202
        );
203
    }
204
205
    /**
206
     * @param array $scriptDataInfo
207
     * @return Developer
208
     */
209
    private function extractDeveloper(array $scriptDataInfo): Developer
210
    {
211
        $developerPage = GPlayApps::GOOGLE_PLAY_URL . $scriptDataInfo[0][12][5][5][4][2];
212
        $developerId = parse_query(parse_url($developerPage, PHP_URL_QUERY))[GPlayApps::REQ_PARAM_ID];
213
        $developerName = $scriptDataInfo[0][12][5][1];
214
        $developerEmail = $scriptDataInfo[0][12][5][2][0];
215
        $developerWebsite = $scriptDataInfo[0][12][5][3][5][2];
216
        $developerAddress = $scriptDataInfo[0][12][5][4][0];
217
//        $developerInternalID = (int)$scriptDataInfo[0][12][5][0][0];
218
219
        return new Developer(
220
            Developer::newBuilder()
221
                ->setId($developerId)
222
                ->setUrl($developerPage)
223
                ->setName($developerName)
224
                ->setEmail($developerEmail)
225
                ->setAddress($developerAddress)
226
                ->setWebsite($developerWebsite)
227
        );
228
    }
229
230
    /**
231
     * @param array $data
232
     * @return Category|null
233
     */
234
    private function extractCategory(array $data): ?Category
235
    {
236
        if (isset($data[0]) && $data[0] !== null && $data[2] !== null) {
237
            $genreId = (string)$data[2];
238
            $genreName = (string)$data[0];
239
            return new Category($genreId, $genreName);
240
        }
241
        return null;
242
    }
243
244
    /**
245
     * @param array $scriptDataInfo
246
     * @return GoogleImage[]
247
     */
248
    private function extractScreenshots(array $scriptDataInfo): array
249
    {
250
        return !empty($scriptDataInfo[0][12][0]) ? array_map(static function (array $v) {
251
            return new GoogleImage($v[3][2]);
252
        }, $scriptDataInfo[0][12][0]) : [];
253
    }
254
255
    /**
256
     * @param array $scriptDataInfo
257
     * @return Video|null
258
     */
259
    private function extractVideo(array $scriptDataInfo): ?Video
260
    {
261
        if (
262
            isset($scriptDataInfo[0][12][3][0][3][2]) &&
263
            $scriptDataInfo[0][12][3][0][3][2] !== null &&
264
            $scriptDataInfo[0][12][3][1][3][2] !== null
265
        ) {
266
            $videoThumb = (string)$scriptDataInfo[0][12][3][1][3][2];
267
            $videoUrl = (string)$scriptDataInfo[0][12][3][0][3][2];
268
269
            return new Video($videoThumb, $videoUrl);
270
        }
271
        return null;
272
    }
273
274
    /**
275
     * @param array $scriptDataInfo
276
     * @param string $locale
277
     * @return \DateTimeInterface|null
278
     */
279
    private function extractReleaseDate(array $scriptDataInfo, string $locale): ?\DateTimeInterface
280
    {
281
        if (isset($scriptDataInfo[0][12][36])) {
282
            return DateStringFormatter::formatted($locale, $scriptDataInfo[0][12][36]);
283
        }
284
        return null;
285
    }
286
287
    /**
288
     * @param array $scriptDataInfo
289
     * @return \DateTimeInterface|null
290
     */
291
    private function extractUpdatedDate(array $scriptDataInfo): ?\DateTimeInterface
292
    {
293
        if (isset($scriptDataInfo[0][12][8][0])) {
294
            return DateStringFormatter::unixTimeToDateTime($scriptDataInfo[0][12][8][0]);
295
        }
296
        return null;
297
    }
298
299
    /**
300
     * @param RequestApp $requestApp
301
     * @param array $scriptDataReviews
302
     * @param int $limit
303
     * @return Review[]
304
     */
305
    private function extractReviews(RequestApp $requestApp, array $scriptDataReviews, int $limit = 4): array
306
    {
307
        if (empty($scriptDataReviews[0])) {
308
            return [];
309
        }
310
311
        return ReviewsExtractor::extractReviews(
312
            $requestApp,
313
            array_slice($scriptDataReviews[0], 0, $limit)
314
        );
315
    }
316
}
317