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) { |
|
|
|
|
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) { |
|
|
|
|
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; |
|
|
|
|
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
|
|
|
|
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.