Completed
Push — master ( 1970e3...d5c73e )
by Marcin
01:37
created

Imdb::getApiTitle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * xMDB-API
4
 *
5
 * Copyright © 2017 pudelek.org.pl
6
 *
7
 * @license MIT License (MIT)
8
 *
9
 * For the full copyright and license information, please view source file
10
 * that is bundled with this package in the file LICENSE
11
 *
12
 * @author  Marcin Pudełek <[email protected]>
13
 */
14
15
/**
16
 * Created by Marcin.
17
 * Date: 01.12.2017
18
 * Time: 22:52
19
 */
20
21
namespace mrcnpdlk\Xmdb;
22
23
24
use Campo\UserAgent;
25
use Curl\Curl;
26
use HttpLib\Http;
27
use Imdb\Cache;
28
use Imdb\Config;
29
use Imdb\TitleSearch;
30
use KHerGe\JSON\JSON;
31
use mrcnpdlk\Xmdb\Model\Imdb\Character;
32
use mrcnpdlk\Xmdb\Model\Imdb\Image;
33
use mrcnpdlk\Xmdb\Model\Imdb\Info;
34
use mrcnpdlk\Xmdb\Model\Imdb\Person;
35
use mrcnpdlk\Xmdb\Model\Imdb\Rating;
36
use mrcnpdlk\Xmdb\Model\Imdb\Title;
37
use Sunra\PhpSimple\HtmlDomParser;
38
39
class Imdb
40
{
41
42
    const ANONYMOUS_URL = 'http://anonymouse.org/cgi-bin/anon-www.cgi/';
43
44
    /**
45
     * @var \mrcnpdlk\Xmdb\Client
46
     */
47
    private $oClient;
48
    /**
49
     * @var \Imdb\Config
50
     */
51
    private $oConfig;
52
    /** @noinspection PhpUndefinedClassInspection */
53
54
    /**
55
     * @var \Psr\Log\LoggerInterface
56
     */
57
    private $oLog;
58
    /**
59
     * @var \Psr\SimpleCache\CacheInterface
60
     */
61
    private $oCache;
62
63
    /**
64
     * Imdb constructor.
65
     *
66
     * @param \mrcnpdlk\Xmdb\Client $oClient
67
     *
68
     * @throws \mrcnpdlk\Xmdb\Exception
69
     */
70
    public function __construct(Client $oClient)
71
    {
72
        try {
73
            $this->oClient                = $oClient;
74
            $this->oLog                   = $oClient->getLogger();
75
            $this->oCache                 = $oClient->getAdapter()->getCache();
76
            $this->oConfig                = new Config();
77
            $this->oConfig->usecache      = null !== $this->oCache;
78
            $this->oConfig->language      = $oClient->getLang();
79
            $this->oConfig->default_agent = UserAgent::random();
80
81
82
        } catch (\Exception $e) {
83
            throw new Exception(sprintf('Cannot create Tmdb Client'), 1, $e);
84
        }
85
    }
86
87
    /**
88
     * @param string $imdbId
89
     *
90
     * @return \Imdb\Title
91
     */
92
    protected function getApiTitle(string $imdbId)
93
    {
94
        return new \Imdb\Title($imdbId, $this->oConfig, $this->oLog, $this->oCache);
95
    }
96
97
    /**
98
     * @return \Imdb\TitleSearch
99
     */
100
    protected function getApiTitleSearch()
101
    {
102
        return new \Imdb\TitleSearch($this->oConfig, $this->oLog, $this->oCache);
103
    }
104
105
    /**
106
     * @param string $imdbId
107
     *
108
     * @return \mrcnpdlk\Xmdb\Model\Imdb\Info
109
     * @throws \mrcnpdlk\Xmdb\Exception
110
     */
111
    public function getInfo(string $imdbId): Info
112
    {
113
        try {
114
            $searchUrl = 'http://app.imdb.com/title/maindetails?tconst=' . $imdbId;
115
116
            $oResp = $this->oClient->getAdapter()->useCache(
117
                function () use ($searchUrl) {
118
                    $oCurl = new Curl();
119
                    $oCurl->setUserAgent(UserAgent::random());
120
                    $oCurl->setHeader('Accept-Language', $this->oClient->getLang());
121
                    $oCurl->get($searchUrl);
122
123
                    if ($oCurl->error) {
124
                        throw new \RuntimeException('Curl Error! ' . Http::message($oCurl->httpStatusCode), $oCurl->error_code);
125
                    }
126
127
                    return $oCurl->response->data ?? null;
128
                },
129
                [$searchUrl, $this->oClient->getLang()],
130
                3600 * 2)
131
            ;
132
            $oData = $oResp->data ?? $oResp;
133
134
            $oApiTitle = $this->getApiTitle($imdbId);
135
136
            $oInfo              = new Info();
137
            $oInfo->id          = $oData->tconst;
138
            $oInfo->title       = $oData->title;
139
            $oInfo->year        = $oData->year;
140
            $oInfo->image       = isset($oData->image) ? new Image($oData->image->url, $oData->image->width, $oData->image->height) : null;
141
            $oInfo->releaseDate = $oData->release_date->normal ?? null;
142
            $oInfo->genres      = $oData->genres;
143
            $oInfo->rating      = $oData->rating;
144
            $oInfo->votes       = $oData->num_votes;
145
            $oInfo->runtime     = $oApiTitle->runtime();
146
147
            $tmp = [];
148 View Code Duplication
            foreach ($oData->directors_summary ?? [] as $oDir) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
                $oPerson            = $oDir->name;
150
                $oImage             = isset($oPerson->image) ? new Image($oPerson->image->url, $oPerson->image->width,
151
                    $oPerson->image->height) : null;
152
                $oInfo->directors[] = new Person($oPerson->nconst, $oPerson->name, $oImage);
153
                $tmp[]              = $oPerson->name;
154
            }
155
            $oInfo->directorsDisplay = implode(', ', $tmp);
156
157
            $tmp = [];
158 View Code Duplication
            foreach ($oData->writers_summary ?? [] as $oDir) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
159
                $oPerson          = $oDir->name;
160
                $oImage           = isset($oPerson->image) ? new Image($oPerson->image->url, $oPerson->image->width,
161
                    $oPerson->image->height) : null;
162
                $oInfo->writers[] = new Person($oPerson->nconst, $oPerson->name, $oImage);
163
                $tmp[]            = $oPerson->name;
164
            }
165
            $oInfo->writersDisplay = implode(', ', $tmp);
166
167
            foreach ($oData->photos ?? [] as $photo) {
168
                $oInfo->photos[] = new Image($photo->image->url, $photo->image->width, $photo->image->height);
169
            }
170
171
            foreach ($oData->cast_summary ?? [] as $ch) {
172
                $oImage        = $ch->name->image ? new Image($ch->name->image->url, $ch->name->image->width,
173
                    $ch->name->image->height) : null;
174
                $oPerson       = $ch->name ? new Person($ch->name->nconst, $ch->name->name, $oImage) : null;
175
                $oInfo->cast[] = new Character($ch->char, $oPerson);
176
            }
177
178
179
            $oInfo->countries = $oApiTitle->country();
180
181
182
            $oInfo->genresDisplay    = implode(', ', $oInfo->genres);
183
            $oInfo->countriesDisplay = implode(', ', $oInfo->countries);
184
            
185
            return $oInfo;
186
187
        } catch (\Exception $e) {
188
            throw new Exception(sprintf('Item [%s] not found, reason: %s', $imdbId, $e->getMessage()));
189
        }
190
    }
191
192
    /**
193
     * @param string $imdbId
194
     *
195
     * @return \mrcnpdlk\Xmdb\Model\Imdb\Rating
196
     * @throws \mrcnpdlk\Xmdb\Exception
197
     */
198
    public function getRating(string $imdbId): Rating
199
    {
200
        try {
201
            $searchUrl = 'http://p.media-imdb.com/static-content/documents/v1/title/'
202
                . $imdbId
203
                . '/ratings%3Fjsonp=imdb.rating.run:imdb.api.title.ratings/data.json?u='
204
                . $this->oClient->getImdbUser();
205
206
            $oResp = $this->oClient->getAdapter()->useCache(
207
                function () use ($searchUrl) {
208
                    $oCurl = new Curl();
209
                    $oCurl->setOpt(\CURLOPT_ENCODING, 'gzip');
210
                    $oCurl->setUserAgent(UserAgent::random());
211
                    $oCurl->setHeader('Accept-Language', $this->oClient->getLang());
212
                    $oCurl->get($searchUrl);
213
214
                    if ($oCurl->error) {
215
                        throw new \RuntimeException('Curl Error! ' . Http::message($oCurl->httpStatusCode), $oCurl->error_code);
216
                    }
217
218
                    preg_match("/^[\w\.]*\((.*)\)$/", $oCurl->response, $output_array);
219
                    $json = new JSON();
220
221
                    return $json->decode($output_array[1]);
222
                },
223
                [$searchUrl, $this->oClient->getLang()],
224
                180)
225
            ;
226
227
            if (!isset($oResp->resource)) {
228
                throw new \RuntimeException('Resource is empty');
229
            }
230
231
            $oData = $oResp->resource;
232
233
            $oRating         = new Rating();
234
            $oRating->id     = $imdbId;
235
            $oRating->title  = $oData->title;
236
            $oRating->year   = $oData->year;
237
            $oRating->rating = $oData->rating;
238
            $oRating->votes  = $oData->ratingCount;
239
            $oRating->type   = $oData->titleType;
240
241
            return $oRating;
242
243
        } catch (\Exception $e) {
244
            throw new Exception(sprintf('Item [%s] not found, reason: %s', $imdbId, $e->getMessage()));
245
        }
246
    }
247
248
    /**
249
     * Combined search by title
250
     *
251
     * @param string $title
252
     *
253
     * @return Title[]
254
     */
255
    public function searchByTitle(string $title): array
256
    {
257
        /**
258
         * @var Title[] $answer
259
         * @var Title[] $tmpList
260
         */
261
        $answer      = [];
262
        $tmpList     = [];
263
        $tNativeList = $this->searchByTitleNative($title);
264
        foreach ($tNativeList as $item) {
265
            $tmpList[$item->imdbId] = $item;
266
        }
267
268
        $tApiList = $this->searchByTitleApi($title);
269
        foreach ($tApiList as $item) {
270
            if (!\array_key_exists($item->imdbId, $tmpList)) {
271
                $tmpList[$item->imdbId] = $item;
272
            } else {
273
                $tmpList[$item->imdbId]->isMovie = $tmpList[$item->imdbId]->isMovie ?? $item->isMovie;
274
            }
275
        }
276
277
        foreach ($tmpList as $item) {
278
            $answer[] = $item;
279
        }
280
281
        return $answer;
282
    }
283
284
    /**
285
     * @param string $title
286
     *
287
     * @return Title[]
288
     */
289
    public function searchByTitleApi(string $title): array
290
    {
291
        try {
292
            $answer = [];
293
            $tList  = $this->getApiTitleSearch()->search($title, [
294
                TitleSearch::MOVIE,
295
                TitleSearch::TV_SERIES,
296
                TitleSearch::VIDEO,
297
                TitleSearch::TV_MOVIE,
298
            ])
299
            ;
300
301
            foreach ($tList as $element) {
302
                $oTitle                  = new Title();
303
                $oTitle->imdbId          = 'tt' . $element->imdbid();
304
                $oTitle->title           = $element->title();
305
                $oTitle->rating          = null; //set null for speedy
306
                $oTitle->episode         = null;
307
                $oTitle->year            = empty($element->year()) ? null : $element->year();
308
                $oTitle->type            = $element->movietype();
309
                $oTitle->isMovie         = \in_array($element->movietype(), [TitleSearch::MOVIE, TitleSearch::TV_MOVIE, TitleSearch::VIDEO],
310
                    true);
311
                $oTitle->director        = [];
312
                $oTitle->directorDisplay = implode(', ', $oTitle->director);
313
                $oTitle->star            = [];
314
                $oTitle->starDisplay     = implode(', ', $oTitle->star);
315
                $answer[]                = $oTitle;
316
            }
317
318
            return $answer;
319
        } catch (\Exception $e) {
320
            $this->oLog->warning(sprintf('Item [%s] not found, reason: %s', $title, $e->getMessage()));
321
322
            return [];
323
        }
324
    }
325
326
    /**
327
     * @param string $title
328
     *
329
     * @return Title[]
330
     */
331
    public function searchByTitleNative(string $title): array
332
    {
333
        try {
334
            /**
335
             * @var Title[] $answer
336
             */
337
            $answer = [];
338
            $searchUrl
339
                    = 'http://www.imdb.com/search/title' .
340
                '?at=0&sort=num_votes&title_type=feature,tv_movie,tv_series,tv_episode,tv_special,mini_series,documentary,short,video&title='
341
                . $title;
342
343
            $htmlContent = $this->oClient->getAdapter()->useCache(
344
                function () use ($searchUrl) {
345
                    $oCurl = new Curl();
346
                    $oCurl->setHeader('Accept-Language', $this->oClient->getLang());
347
348
                    return $oCurl->get($searchUrl);
349
                },
350
                [$searchUrl, $this->oClient->getLang()],
351
                3600 * 2)
352
            ;
353
354
            $html = HtmlDomParser::str_get_html($htmlContent);
355
            if (!$html) {
356
                throw new \RuntimeException('Response from IMDB malformed!');
357
            }
358
            $listerItemNode = $html->find('div.lister-item');
359
360
            foreach ($listerItemNode as $element) {
361
                $content = $element->find('div.lister-item-content', 0);
362
                if (!$content) {
363
                    throw new \RuntimeException('Empty search result!');
364
                }
365
                $titleNode = $content->find('h3.lister-item-header a', 0);
366
                if ($titleNode) {
367
                    $foundTitle = trim($titleNode->text());
368
                } else {
369
                    throw new \RuntimeException('Title not found!');
370
                }
371
                $yearNode = $content->find('h3.lister-item-header .lister-item-year', 0);
372
                if ($yearNode) {
373
                    $foundYear = trim($yearNode->text());
374
                    preg_match("/^\(?([0-9\-\–\s]+)([\w\s]*)?\)?$/u", $foundYear, $yearOut);
375
                    $foundYear = isset($yearOut[1]) ? trim($yearOut[1]) : $foundYear;
376
                    $foundType = isset($yearOut[2]) && trim($yearOut[2]) ? trim($yearOut[2]) : null;
377
                } else {
378
                    $foundYear = null;
379
                    $foundType = null;
380
                }
381
                $episodeNode  = $content->find('h3.lister-item-header a', 1);
382
                $foundEpisode = null;
383
                if ($episodeNode) {
384
                    $foundEpisode = trim($episodeNode->text());
385
                }
386
                $ratingNode  = $content->find('div.ratings-bar div.ratings-imdb-rating', 0);
387
                $foundRating = null;
388
                if ($ratingNode) {
389
                    $foundRating = trim($ratingNode->text());
390
                }
391
392
                $metascoreNode  = $content->find('div.ratings-bar div.ratings-metascore', 0);
393
                $foundMetascore = null;
394
                if ($metascoreNode) {
395
                    $oSpanNode      = $metascoreNode->find('span', 0);
396
                    $foundMetascore = $oSpanNode ? (int)$oSpanNode->text() : null;
397
                }
398
399
                $idNode = $element->find('div.lister-top-right div.ribbonize', 0);
400
                $imdbId = null;
401
                if ($idNode) {
402
                    $imdbId = $idNode->getAttribute('data-tconst');
403
                }
404
405
                $directors   = [];
406
                $stars       = [];
407
                $personsNode = $content->find('p', 2);
408
                if ($personsNode) {
409
                    $persons = $personsNode->find('a');
410
                    if (\is_array($persons)) {
411
                        foreach ($persons as $person) {
412
                            if (strpos($person->getAttribute('href'), 'adv_li_dr_') !== false) {
413
                                $directors[] = $person->text();
414
                            }
415
                            if (strpos($person->getAttribute('href'), 'adv_li_st_') !== false) {
416
                                $stars[] = $person->text();
417
                            }
418
                        }
419
                    }
420
                }
421
422
                if ($imdbId) {
423
                    $oTitle                  = new Title();
424
                    $oTitle->title           = $foundTitle;
425
                    $oTitle->imdbId          = $imdbId;
426
                    $oTitle->rating          = $foundRating;
0 ignored issues
show
Documentation Bug introduced by
It seems like $foundRating can also be of type string. However, the property $rating is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
427
                    $oTitle->metascore       = $foundMetascore;
428
                    $oTitle->episode         = $foundEpisode;
429
                    $oTitle->year            = $foundYear;
430
                    $oTitle->type            = $foundType;
431
                    $oTitle->director        = $directors;
432
                    $oTitle->directorDisplay = implode(', ', $oTitle->director);
433
                    $oTitle->star            = $stars;
434
                    $oTitle->starDisplay     = implode(', ', $oTitle->star);
435
436
                    $answer[] = $oTitle;
437
                }
438
439
            }
440
441
            return $answer;
442
        } catch (\Exception $e) {
443
            $this->oLog->warning(sprintf('Item [%s] not found, reason: %s', $title, $e->getMessage()));
444
445
            return [];
446
        }
447
    }
448
}
449