Completed
Push — master ( c4f023...a712a8 )
by Marcin
03:37
created

Imdb   F

Complexity

Total Complexity 54

Size/Duplication

Total Lines 425
Duplicated Lines 3.29 %

Coupling/Cohesion

Components 1
Dependencies 20

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 20
dl 14
loc 425
ccs 0
cts 296
cp 0
rs 2.6679
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 2
F getInfo() 14 82 13
A getApiTitle() 0 4 1
B getRating() 0 49 4
C searchByTitle() 0 38 8
F searchByTitleNative() 0 120 21
B searchByTitleApi() 0 37 4
A getApiTitleSearch() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Imdb often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Imdb, and based on these observations, apply Extract Interface, too.

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\Config;
28
use Imdb\Title as ApiTitle;
29
use Imdb\TitleSearch;
30
use Imdb\TitleSearch as ApiTitleSearch;
31
use KHerGe\JSON\JSON;
32
use mrcnpdlk\Xmdb\Model\Imdb\Character;
33
use mrcnpdlk\Xmdb\Model\Imdb\Image;
34
use mrcnpdlk\Xmdb\Model\Imdb\Info;
35
use mrcnpdlk\Xmdb\Model\Imdb\Person;
36
use mrcnpdlk\Xmdb\Model\Imdb\Rating;
37
use mrcnpdlk\Xmdb\Model\Imdb\Ratio;
38
use mrcnpdlk\Xmdb\Model\Imdb\Title;
39
use Sunra\PhpSimple\HtmlDomParser;
40
41
class Imdb
42
{
43
    /**
44
     * @var \mrcnpdlk\Xmdb\Client
45
     */
46
    private $oClient;
47
    /**
48
     * @var \Imdb\Config
49
     */
50
    private $oConfig;
51
    /** @noinspection PhpUndefinedClassInspection */
52
53
    /**
54
     * @var \Psr\Log\LoggerInterface
55
     */
56
    private $oLog;
57
    /**
58
     * @var \Psr\SimpleCache\CacheInterface
59
     */
60
    private $oCache;
61
62
    /**
63
     * Imdb constructor.
64
     *
65
     * @param \mrcnpdlk\Xmdb\Client $oClient
66
     *
67
     * @throws \mrcnpdlk\Xmdb\Exception
68
     */
69
    public function __construct(Client $oClient)
70
    {
71
        try {
72
            $this->oClient                = $oClient;
73
            $this->oLog                   = $oClient->getLogger();
74
            $this->oCache                 = $oClient->getAdapter()->getCache();
75
            $this->oConfig                = new Config();
76
            $this->oConfig->usecache      = null !== $this->oCache;
77
            $this->oConfig->language      = $oClient->getLang();
78
            $this->oConfig->default_agent = UserAgent::random();
79
80
81
        } catch (\Exception $e) {
82
            throw new Exception(sprintf('Cannot create Tmdb Client'), 1, $e);
83
        }
84
    }
85
86
    /**
87
     * @param string $imdbId
88
     *
89
     * @return \mrcnpdlk\Xmdb\Model\Imdb\Info
90
     * @throws \mrcnpdlk\Xmdb\Exception
91
     */
92
    public function getInfo(string $imdbId): Info
93
    {
94
        try {
95
            $searchUrl = 'http://app.imdb.com/title/maindetails?tconst=' . $imdbId;
96
97
            $oResp = $this->oClient->getAdapter()->useCache(
98
                function () use ($searchUrl) {
99
                    $oCurl = new Curl();
100
                    $oCurl->setUserAgent(UserAgent::random());
101
                    $oCurl->setHeader('Accept-Language', $this->oClient->getLang());
102
                    $oCurl->get($searchUrl);
103
104
                    if ($oCurl->error) {
105
                        throw new \RuntimeException('Curl Error! ' . Http::message($oCurl->httpStatusCode), $oCurl->error_code);
106
                    }
107
108
                    return $oCurl->response->data ?? null;
109
                },
110
                [$searchUrl, $this->oClient->getLang()],
111
                3600 * 2)
112
            ;
113
            $oData = $oResp->data ?? $oResp;
114
115
            $oApiTitle = $this->getApiTitle($imdbId);
116
117
            $oInfo              = new Info();
118
            $oInfo->id          = $oData->tconst;
119
            $oInfo->title       = $oData->title;
120
            $oInfo->year        = $oData->year;
121
            $oInfo->image       = isset($oData->image) ? new Image($oData->image->url, $oData->image->width, $oData->image->height) : null;
122
            $oInfo->releaseDate = $oData->release_date->normal ?? null;
123
            $oInfo->genres      = $oData->genres;
124
            $oInfo->rating      = $oData->rating;
125
            $oInfo->votes       = $oData->num_votes;
126
127
            $sRuntime       = $oApiTitle->runtime();
128
            $oInfo->runtime = null === $sRuntime ? null : (int)$sRuntime;
129
130
            $tmp = [];
131 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...
132
                $oPerson            = $oDir->name;
133
                $oImage             = isset($oPerson->image) ? new Image($oPerson->image->url, $oPerson->image->width,
134
                    $oPerson->image->height) : null;
135
                $oInfo->directors[] = new Person($oPerson->nconst, $oPerson->name, $oImage);
136
                $tmp[]              = $oPerson->name;
137
            }
138
            $oInfo->directorsDisplay = implode(', ', $tmp);
139
140
            $tmp = [];
141 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...
142
                $oPerson          = $oDir->name;
143
                $oImage           = isset($oPerson->image) ? new Image($oPerson->image->url, $oPerson->image->width,
144
                    $oPerson->image->height) : null;
145
                $oInfo->writers[] = new Person($oPerson->nconst, $oPerson->name, $oImage);
146
                $tmp[]            = $oPerson->name;
147
            }
148
            $oInfo->writersDisplay = implode(', ', $tmp);
149
150
            foreach ($oData->photos ?? [] as $photo) {
151
                $oInfo->photos[] = new Image($photo->image->url, $photo->image->width, $photo->image->height);
152
            }
153
154
            foreach ($oData->cast_summary ?? [] as $ch) {
155
                $oImage        = isset($ch->name->image) ? new Image($ch->name->image->url, $ch->name->image->width,
156
                    $ch->name->image->height) : null;
157
                $oPerson       = $ch->name ? new Person($ch->name->nconst, $ch->name->name, $oImage) : null;
158
                $oInfo->cast[] = new Character($ch->char, $oPerson);
159
            }
160
161
162
            $oInfo->countries = $oApiTitle->country();
163
164
165
            $oInfo->genresDisplay    = implode(', ', $oInfo->genres);
166
            $oInfo->countriesDisplay = implode(', ', $oInfo->countries);
167
168
            return $oInfo;
169
170
        } catch (\Exception $e) {
171
            throw new Exception(sprintf('Item [%s] not found, reason: %s', $imdbId, $e->getMessage()));
172
        }
173
    }
174
175
    /**
176
     * @param string $imdbId
177
     *
178
     * @return \Imdb\Title
179
     */
180
    protected function getApiTitle(string $imdbId): ApiTitle
181
    {
182
        return new ApiTitle($imdbId, $this->oConfig, $this->oLog, $this->oCache);
183
    }
184
185
    /**
186
     * @param string $imdbId
187
     *
188
     * @return \mrcnpdlk\Xmdb\Model\Imdb\Rating
189
     * @throws \mrcnpdlk\Xmdb\Exception
190
     */
191
    public function getRating(string $imdbId): Rating
192
    {
193
        try {
194
            $searchUrl = 'http://p.media-imdb.com/static-content/documents/v1/title/'
195
                . $imdbId
196
                . '/ratings%3Fjsonp=imdb.rating.run:imdb.api.title.ratings/data.json?u='
197
                . $this->oClient->getImdbUser();
198
199
            $oResp = $this->oClient->getAdapter()->useCache(
200
                function () use ($searchUrl) {
201
                    $oCurl = new Curl();
202
                    $oCurl->setOpt(\CURLOPT_ENCODING, 'gzip');
203
                    $oCurl->setUserAgent(UserAgent::random());
204
                    $oCurl->setHeader('Accept-Language', $this->oClient->getLang());
205
                    $oCurl->get($searchUrl);
206
207
                    if ($oCurl->error) {
208
                        throw new \RuntimeException('Curl Error! ' . Http::message($oCurl->httpStatusCode), $oCurl->error_code);
209
                    }
210
211
                    preg_match("/^[\w\.]*\((.*)\)$/", $oCurl->response, $output_array);
212
                    $json = new JSON();
213
214
                    return $json->decode($output_array[1]);
215
                },
216
                [$searchUrl, $this->oClient->getLang()],
217
                180)
218
            ;
219
220
            if (!isset($oResp->resource)) {
221
                throw new \RuntimeException('Resource is empty');
222
            }
223
224
            $oData = $oResp->resource;
225
226
            $oRating         = new Rating();
227
            $oRating->id     = $imdbId;
228
            $oRating->title  = $oData->title;
229
            $oRating->year   = $oData->year;
230
            $oRating->rating = $oData->rating;
231
            $oRating->votes  = $oData->ratingCount;
232
            $oRating->type   = $oData->titleType;
233
234
            return $oRating;
235
236
        } catch (\Exception $e) {
237
            throw new Exception(sprintf('Item [%s] not found, reason: %s', $imdbId, $e->getMessage()));
238
        }
239
    }
240
241
    /**
242
     * Combined search by title
243
     *
244
     * @param string                               $title
245
     * @param int|null                             $limit
246
     * @param \mrcnpdlk\Xmdb\Model\Imdb\Ratio|null $oRatio
247
     *
248
     * @return Title[]
249
     */
250
    public function searchByTitle(string $title, int $limit = null, Ratio $oRatio = null): array
251
    {
252
        /**
253
         * @var Title[] $answer
254
         * @var Title[] $tmpList
255
         */
256
        $answer      = [];
257
        $tmpList     = [];
258
        $tNativeList = $this->searchByTitleNative($title);
259
        foreach ($tNativeList as $item) {
260
            $tmpList[$item->imdbId] = $item;
261
        }
262
263
        $tApiList = $this->searchByTitleApi($title);
264
        foreach ($tApiList as $item) {
265
            if (!\array_key_exists($item->imdbId, $tmpList)) {
266
                $tmpList[$item->imdbId] = $item;
267
            } else {
268
                $tmpList[$item->imdbId]->isMovie = $tmpList[$item->imdbId]->isMovie ?? $item->isMovie;
269
                $tmpList[$item->imdbId]->type    = $tmpList[$item->imdbId]->type ?? $item->type;
270
            }
271
        }
272
273
        foreach ($tmpList as $item) {
274
            $answer[] = $item;
275
        }
276
277
        if ($oRatio) {
278
            $oRatio->calculateRatio($answer);
279
            $answer = [];
280
            foreach ($oRatio->items as $oRatioElement) {
281
                $answer[] = $oRatioElement->item;
282
            }
283
284
        }
285
286
        return $limit === null ? $answer : \array_slice($answer, 0, $limit);
287
    }
288
289
    /**
290
     * @param string $title
291
     *
292
     * @return Title[]
293
     */
294
    public function searchByTitleNative(string $title): array
295
    {
296
        try {
297
            /**
298
             * @var Title[] $answer
299
             */
300
            $answer    = [];
301
            $params    = [
302
                'sort'  => 'moviemeter,asc',
303
                'view'  => 'advanced',
304
                'title' => $title,
305
            ];
306
            $searchUrl = 'http://www.imdb.com/search/title?' . http_build_query($params);
307
308
            $htmlContent = $this->oClient->getAdapter()->useCache(
309
                function () use ($searchUrl) {
310
                    $oCurl = new Curl();
311
                    $oCurl->setHeader('Accept-Language', $this->oClient->getLang());
312
313
                    return $oCurl->get($searchUrl);
314
                },
315
                [$searchUrl, $this->oClient->getLang()],
316
                3600 * 2)
317
            ;
318
319
            $html = HtmlDomParser::str_get_html($htmlContent);
320
            if (!$html) {
321
                throw new \RuntimeException('Response from IMDB malformed!');
322
            }
323
            $listerItemNode = $html->find('div.lister-item');
324
325
            foreach ($listerItemNode as $element) {
326
                $content = $element->find('div.lister-item-content', 0);
327
                if (!$content) {
328
                    throw new \RuntimeException('Empty search result!');
329
                }
330
                $titleNode = $content->find('h3.lister-item-header a', 0);
331
                if ($titleNode) {
332
                    $foundTitle = trim($titleNode->text());
333
                } else {
334
                    throw new \RuntimeException('Title not found!');
335
                }
336
                $yearNode = $content->find('h3.lister-item-header .lister-item-year', 0);
337
                if ($yearNode) {
338
                    $foundYear = trim($yearNode->text());
339
                    preg_match("/^\(?([0-9\-\–\s]+)([\w\s]*)?\)?$/u", $foundYear, $yearOut);
340
                    $foundYear = isset($yearOut[1]) ? trim($yearOut[1]) : $foundYear;
341
                    $foundType = isset($yearOut[2]) && trim($yearOut[2]) ? trim($yearOut[2]) : null;
342
                } else {
343
                    $foundYear = null;
344
                    $foundType = null;
345
                }
346
                $episodeNode  = $content->find('h3.lister-item-header a', 1);
347
                $foundEpisode = null;
348
                if ($episodeNode) {
349
                    $foundEpisode = trim($episodeNode->text());
350
                }
351
                $ratingNode  = $content->find('div.ratings-bar div.ratings-imdb-rating', 0);
352
                $foundRating = null;
353
                if ($ratingNode) {
354
                    $foundRating = trim($ratingNode->text());
355
                }
356
357
                $metascoreNode  = $content->find('div.ratings-bar div.ratings-metascore', 0);
358
                $foundMetascore = null;
359
                if ($metascoreNode) {
360
                    $oSpanNode      = $metascoreNode->find('span', 0);
361
                    $foundMetascore = $oSpanNode ? (int)$oSpanNode->text() : null;
362
                }
363
364
                $idNode = $element->find('div.lister-top-right div.ribbonize', 0);
365
                $imdbId = null;
366
                if ($idNode) {
367
                    $imdbId = $idNode->getAttribute('data-tconst');
368
                }
369
370
                $directors   = [];
371
                $stars       = [];
372
                $personsNode = $content->find('p', 2);
373
                if ($personsNode) {
374
                    $persons = $personsNode->find('a');
375
                    if (\is_array($persons)) {
376
                        foreach ($persons as $person) {
377
                            if (strpos($person->getAttribute('href'), 'adv_li_dr_') !== false) {
378
                                $directors[] = $person->text();
379
                            }
380
                            if (strpos($person->getAttribute('href'), 'adv_li_st_') !== false) {
381
                                $stars[] = $person->text();
382
                            }
383
                        }
384
                    }
385
                }
386
387
                if ($imdbId) {
388
                    $oTitle                  = new Title();
389
                    $oTitle->title           = $foundTitle;
390
                    $oTitle->titleOrg        = $foundTitle;
391
                    $oTitle->imdbId          = $imdbId;
392
                    $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...
393
                    $oTitle->metascore       = $foundMetascore;
394
                    $oTitle->episode         = $foundEpisode;
395
                    $oTitle->year            = $foundYear;
396
                    $oTitle->type            = $foundType;
397
                    $oTitle->director        = $directors;
398
                    $oTitle->directorDisplay = implode(', ', $oTitle->director);
399
                    $oTitle->star            = $stars;
400
                    $oTitle->starDisplay     = implode(', ', $oTitle->star);
401
402
                    $answer[] = $oTitle;
403
                }
404
405
            }
406
407
            return $answer;
408
        } catch (\Exception $e) {
409
            $this->oLog->warning(sprintf('Item [%s] not found, reason: %s', $title, $e->getMessage()));
410
411
            return [];
412
        }
413
    }
414
415
    /**
416
     * @param string $title
417
     *
418
     * @return Title[]
419
     */
420
    public function searchByTitleApi(string $title): array
421
    {
422
        try {
423
            $answer = [];
424
            $tList  = $this->getApiTitleSearch()->search($title, [
425
                TitleSearch::MOVIE,
426
                TitleSearch::TV_SERIES,
427
                TitleSearch::VIDEO,
428
                TitleSearch::TV_MOVIE,
429
            ])
430
            ;
431
432
            foreach ($tList as $element) {
433
                $oTitle                  = new Title();
434
                $oTitle->imdbId          = 'tt' . $element->imdbid();
435
                $oTitle->title           = $element->title();
436
                $oTitle->titleOrg        = $element->title();
437
                $oTitle->rating          = null; //set null for speedy
438
                $oTitle->episode         = null;
439
                $oTitle->year            = empty($element->year()) ? null : $element->year();
440
                $oTitle->type            = $element->movietype();
441
                $oTitle->isMovie         = \in_array($element->movietype(), [TitleSearch::MOVIE, TitleSearch::TV_MOVIE, TitleSearch::VIDEO],
442
                    true);
443
                $oTitle->director        = [];
444
                $oTitle->directorDisplay = implode(', ', $oTitle->director);
445
                $oTitle->star            = [];
446
                $oTitle->starDisplay     = implode(', ', $oTitle->star);
447
                $answer[]                = $oTitle;
448
            }
449
450
            return $answer;
451
        } catch (\Exception $e) {
452
            $this->oLog->warning(sprintf('Item [%s] not found, reason: %s', $title, $e->getMessage()));
453
454
            return [];
455
        }
456
    }
457
458
    /**
459
     * @return \Imdb\TitleSearch
460
     */
461
    protected function getApiTitleSearch(): TitleSearch
462
    {
463
        return new ApiTitleSearch($this->oConfig, $this->oLog, $this->oCache);
464
    }
465
}
466