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\Rating; |
33
|
|
|
use mrcnpdlk\Xmdb\Model\Imdb\Title; |
34
|
|
|
use mrcnpdlk\Xmdb\Model\Ratio; |
35
|
|
|
use Sunra\PhpSimple\HtmlDomParser; |
36
|
|
|
|
37
|
|
|
class Imdb |
38
|
|
|
{ |
39
|
|
|
/** |
40
|
|
|
* @var \mrcnpdlk\Xmdb\Client |
41
|
|
|
*/ |
42
|
|
|
private $oClient; |
43
|
|
|
/** |
44
|
|
|
* @var \Imdb\Config |
45
|
|
|
*/ |
46
|
|
|
private $oConfig; |
47
|
|
|
/** @noinspection PhpUndefinedClassInspection */ |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var \Psr\Log\LoggerInterface |
51
|
|
|
*/ |
52
|
|
|
private $oLog; |
53
|
|
|
/** |
54
|
|
|
* @var \Psr\SimpleCache\CacheInterface |
55
|
|
|
*/ |
56
|
|
|
private $oCache; |
57
|
|
|
/** |
58
|
|
|
* @var \mrcnpdlk\Xmdb\ImdbCache |
59
|
|
|
*/ |
60
|
|
|
private $oImdbCache; |
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->oImdbCache = new ImdbCache($this->oCache); |
76
|
|
|
$this->oConfig = new Config(); |
77
|
|
|
$this->oConfig->usecache = null !== $this->oImdbCache; |
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): ApiTitle |
93
|
|
|
{ |
94
|
|
|
return new ApiTitle($imdbId, $this->oConfig, $this->oLog, $this->oImdbCache); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @return \Imdb\TitleSearch |
99
|
|
|
*/ |
100
|
|
|
protected function getApiTitleSearch(): TitleSearch |
101
|
|
|
{ |
102
|
|
|
return new ApiTitleSearch($this->oConfig, $this->oLog, $this->oImdbCache); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @param string $imdbId |
107
|
|
|
* |
108
|
|
|
* @return \mrcnpdlk\Xmdb\Rating |
109
|
|
|
* @throws \mrcnpdlk\Xmdb\Exception |
110
|
|
|
*/ |
111
|
|
|
public function getRating(string $imdbId): Rating |
112
|
|
|
{ |
113
|
|
|
try { |
114
|
|
|
$searchUrl = 'http://p.media-imdb.com/static-content/documents/v1/title/' |
115
|
|
|
. $imdbId |
116
|
|
|
. '/ratings%3Fjsonp=imdb.rating.run:imdb.api.title.ratings/data.json?u=' |
117
|
|
|
. $this->oClient->getImdbUser(); |
118
|
|
|
|
119
|
|
|
$oResp = $this->oClient->getAdapter()->useCache( |
120
|
|
|
function () use ($searchUrl) { |
121
|
|
|
$oCurl = new Curl(); |
122
|
|
|
$oCurl->setOpt(\CURLOPT_ENCODING, 'gzip'); |
123
|
|
|
$oCurl->setUserAgent(UserAgent::random()); |
124
|
|
|
$oCurl->setHeader('Accept-Language', $this->oClient->getLang()); |
125
|
|
|
$oCurl->get($searchUrl); |
126
|
|
|
|
127
|
|
|
if ($oCurl->error) { |
128
|
|
|
throw new \RuntimeException('Curl Error! ' . Http::message($oCurl->httpStatusCode), $oCurl->error_code); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
preg_match("/^[\w\.]*\((.*)\)$/", $oCurl->response, $output_array); |
132
|
|
|
$json = new JSON(); |
133
|
|
|
|
134
|
|
|
return $json->decode($output_array[1]); |
135
|
|
|
}, |
136
|
|
|
[$searchUrl, $this->oClient->getLang()], |
137
|
|
|
180) |
138
|
|
|
; |
139
|
|
|
|
140
|
|
|
if (!isset($oResp->resource)) { |
141
|
|
|
throw new \RuntimeException('Resource is empty'); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$oData = $oResp->resource; |
145
|
|
|
|
146
|
|
|
$oRating = new Rating(); |
147
|
|
|
$oRating->id = $imdbId; |
148
|
|
|
$oRating->title = $oData->title; |
149
|
|
|
$oRating->year = $oData->year; |
150
|
|
|
$oRating->rating = $oData->rating; |
151
|
|
|
$oRating->votes = $oData->ratingCount; |
152
|
|
|
$oRating->type = $oData->titleType; |
153
|
|
|
|
154
|
|
|
return $oRating; |
155
|
|
|
|
156
|
|
|
} catch (\Exception $e) { |
157
|
|
|
throw new Exception(sprintf('Item [%s] not found, reason: %s', $imdbId, $e->getMessage())); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Combined search by title |
163
|
|
|
* |
164
|
|
|
* @param string $title |
165
|
|
|
* @param int|null $limit |
166
|
|
|
* @param \mrcnpdlk\Xmdb\Model\Ratio|null $oRatio |
167
|
|
|
* |
168
|
|
|
* @return Title[] |
169
|
|
|
*/ |
170
|
|
|
public function searchByTitle(string $title, int $limit = null, Ratio $oRatio = null): array |
171
|
|
|
{ |
172
|
|
|
/** |
173
|
|
|
* @var Title[] $answer |
174
|
|
|
* @var Title[] $tmpList |
175
|
|
|
*/ |
176
|
|
|
$answer = []; |
177
|
|
|
$tmpList = []; |
178
|
|
|
$tNativeList = $this->searchByTitleNative($title); |
179
|
|
|
foreach ($tNativeList as $item) { |
180
|
|
|
$tmpList[$item->imdbId] = $item; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
$tApiList = $this->searchByTitleApi($title); |
184
|
|
|
foreach ($tApiList as $item) { |
185
|
|
|
if (!\array_key_exists($item->imdbId, $tmpList)) { |
186
|
|
|
$tmpList[$item->imdbId] = $item; |
187
|
|
|
} else { |
188
|
|
|
$tmpList[$item->imdbId]->isMovie = $tmpList[$item->imdbId]->isMovie ?? $item->isMovie; |
189
|
|
|
$tmpList[$item->imdbId]->type = $tmpList[$item->imdbId]->type ?? $item->type; |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
foreach ($tmpList as $item) { |
194
|
|
|
$answer[] = $item; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
if ($oRatio) { |
198
|
|
|
$oRatio->calculateRatio($answer); |
199
|
|
|
$answer = []; |
200
|
|
|
foreach ($oRatio->items as $oRatioElement) { |
201
|
|
|
$answer[] = $oRatioElement->item; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
return $limit === null ? $answer : \array_slice($answer, 0, $limit); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @param string $title |
211
|
|
|
* |
212
|
|
|
* @return Title[] |
213
|
|
|
*/ |
214
|
|
|
public function searchByTitleApi(string $title): array |
215
|
|
|
{ |
216
|
|
|
try { |
217
|
|
|
$answer = []; |
218
|
|
|
$tList = $this->getApiTitleSearch()->search($title, [ |
219
|
|
|
TitleSearch::MOVIE, |
220
|
|
|
TitleSearch::TV_SERIES, |
221
|
|
|
TitleSearch::VIDEO, |
222
|
|
|
TitleSearch::TV_MOVIE, |
223
|
|
|
]) |
224
|
|
|
; |
225
|
|
|
|
226
|
|
|
foreach ($tList as $element) { |
227
|
|
|
$oTitle = new Title(); |
228
|
|
|
$oTitle->imdbId = 'tt' . $element->imdbid(); |
229
|
|
|
$oTitle->title = $element->title(); |
230
|
|
|
$oTitle->titleOrg = $element->title(); |
231
|
|
|
$oTitle->rating = null; //set null for speedy |
232
|
|
|
$oTitle->episode = null; |
233
|
|
|
$oTitle->releaseYear = empty($element->year()) ? null : $element->year(); |
234
|
|
|
$oTitle->type = $element->movietype(); |
235
|
|
|
$oTitle->isMovie = \in_array($element->movietype(), |
236
|
|
|
[TitleSearch::MOVIE, TitleSearch::TV_MOVIE, TitleSearch::VIDEO], |
237
|
|
|
true); |
238
|
|
|
$oTitle->directors = []; |
239
|
|
|
$oTitle->directorsDisplay = implode(', ', $oTitle->directors); |
240
|
|
|
$oTitle->actors = []; |
241
|
|
|
$oTitle->actorsDisplay = implode(', ', $oTitle->actors); |
242
|
|
|
$answer[] = $oTitle; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return $answer; |
246
|
|
|
} catch (\Exception $e) { |
247
|
|
|
$this->oLog->warning(sprintf('Item [%s] not found, reason: %s', $title, $e->getMessage())); |
248
|
|
|
|
249
|
|
|
return []; |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* @param string $title |
255
|
|
|
* |
256
|
|
|
* @return Title[] |
257
|
|
|
*/ |
258
|
|
|
public function searchByTitleNative(string $title): array |
259
|
|
|
{ |
260
|
|
|
try { |
261
|
|
|
/** |
262
|
|
|
* @var Title[] $answer |
263
|
|
|
*/ |
264
|
|
|
$answer = []; |
265
|
|
|
$params = [ |
266
|
|
|
'sort' => 'moviemeter,asc', |
267
|
|
|
'view' => 'advanced', |
268
|
|
|
'title' => $title, |
269
|
|
|
]; |
270
|
|
|
$searchUrl = 'http://www.imdb.com/search/title?' . http_build_query($params); |
271
|
|
|
|
272
|
|
|
$htmlContent = $this->oClient->getAdapter()->useCache( |
273
|
|
|
function () use ($searchUrl) { |
274
|
|
|
$oCurl = new Curl(); |
275
|
|
|
$oCurl->setHeader('Accept-Language', $this->oClient->getLang()); |
276
|
|
|
|
277
|
|
|
return $oCurl->get($searchUrl); |
278
|
|
|
}, |
279
|
|
|
[$searchUrl, $this->oClient->getLang()], |
280
|
|
|
3600 * 2) |
281
|
|
|
; |
282
|
|
|
|
283
|
|
|
$html = HtmlDomParser::str_get_html($htmlContent); |
284
|
|
|
if (!$html) { |
285
|
|
|
throw new \RuntimeException('Response from IMDB malformed!'); |
286
|
|
|
} |
287
|
|
|
$listerItemNode = $html->find('div.lister-item'); |
288
|
|
|
|
289
|
|
|
foreach ($listerItemNode as $element) { |
290
|
|
|
$content = $element->find('div.lister-item-content', 0); |
291
|
|
|
if (!$content) { |
292
|
|
|
throw new \RuntimeException('Empty search result!'); |
293
|
|
|
} |
294
|
|
|
$titleNode = $content->find('h3.lister-item-header a', 0); |
295
|
|
|
if ($titleNode) { |
296
|
|
|
$foundTitle = trim($titleNode->text()); |
297
|
|
|
} else { |
298
|
|
|
throw new \RuntimeException('Title not found!'); |
299
|
|
|
} |
300
|
|
|
$yearNode = $content->find('h3.lister-item-header .lister-item-year', 0); |
301
|
|
|
if ($yearNode) { |
302
|
|
|
$foundYear = trim($yearNode->text()); |
303
|
|
|
preg_match("/^\(?([0-9\-\–\s]+)([\w\s]*)?\)?$/u", $foundYear, $yearOut); |
304
|
|
|
$foundYear = isset($yearOut[1]) ? trim($yearOut[1]) : $foundYear; |
305
|
|
|
$foundType = isset($yearOut[2]) && trim($yearOut[2]) ? trim($yearOut[2]) : null; |
306
|
|
|
} else { |
307
|
|
|
$foundYear = null; |
308
|
|
|
$foundType = null; |
309
|
|
|
} |
310
|
|
|
$episodeNode = $content->find('h3.lister-item-header a', 1); |
311
|
|
|
$foundEpisode = null; |
312
|
|
|
if ($episodeNode) { |
313
|
|
|
$foundEpisode = trim($episodeNode->text()); |
314
|
|
|
} |
315
|
|
|
$ratingNode = $content->find('div.ratings-bar div.ratings-imdb-rating', 0); |
316
|
|
|
$foundRating = null; |
317
|
|
|
if ($ratingNode) { |
318
|
|
|
$foundRating = trim($ratingNode->text()); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
$metascoreNode = $content->find('div.ratings-bar div.ratings-metascore', 0); |
322
|
|
|
$foundMetascore = null; |
323
|
|
|
if ($metascoreNode) { |
324
|
|
|
$oSpanNode = $metascoreNode->find('span', 0); |
325
|
|
|
$foundMetascore = $oSpanNode ? (int)$oSpanNode->text() : null; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
$idNode = $element->find('div.lister-top-right div.ribbonize', 0); |
329
|
|
|
$imdbId = null; |
330
|
|
|
if ($idNode) { |
331
|
|
|
$imdbId = $idNode->getAttribute('data-tconst'); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$directors = []; |
335
|
|
|
$stars = []; |
336
|
|
|
$personsNode = $content->find('p', 2); |
337
|
|
|
if ($personsNode) { |
338
|
|
|
$persons = $personsNode->find('a'); |
339
|
|
|
if (\is_array($persons)) { |
340
|
|
|
foreach ($persons as $person) { |
341
|
|
|
if (strpos($person->getAttribute('href'), 'adv_li_dr_') !== false) { |
342
|
|
|
$directors[] = $person->text(); |
343
|
|
|
} |
344
|
|
|
if (strpos($person->getAttribute('href'), 'adv_li_st_') !== false) { |
345
|
|
|
$stars[] = $person->text(); |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
if ($imdbId) { |
352
|
|
|
$oTitle = new Title(); |
353
|
|
|
$oTitle->title = $foundTitle; |
354
|
|
|
$oTitle->titleOrg = $foundTitle; |
355
|
|
|
$oTitle->imdbId = $imdbId; |
356
|
|
|
$oTitle->rating = $foundRating; |
|
|
|
|
357
|
|
|
$oTitle->metascore = $foundMetascore; |
358
|
|
|
$oTitle->episode = $foundEpisode; |
359
|
|
|
$oTitle->releaseYear = $foundYear; |
360
|
|
|
$oTitle->type = $foundType; |
361
|
|
|
$oTitle->directors = $directors; |
362
|
|
|
$oTitle->directorsDisplay = implode(', ', $oTitle->directors); |
363
|
|
|
$oTitle->actors = $stars; |
364
|
|
|
$oTitle->actorsDisplay = implode(', ', $oTitle->actors); |
365
|
|
|
|
366
|
|
|
$answer[] = $oTitle; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
return $answer; |
372
|
|
|
} catch (\Exception $e) { |
373
|
|
|
$this->oLog->warning(sprintf('Item [%s] not found, reason: %s', $title, $e->getMessage())); |
374
|
|
|
|
375
|
|
|
return []; |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
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 theid
property of an instance of theAccount
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.