1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Blacklight\processing\tv; |
4
|
|
|
|
5
|
|
|
use Blacklight\ReleaseImage; |
6
|
|
|
use Tmdb\Laravel\Facades\Tmdb as TmdbClient; |
7
|
|
|
|
8
|
|
|
class TMDB extends TV |
9
|
|
|
{ |
10
|
|
|
protected const MATCH_PROBABILITY = 75; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @var string The URL for the image for poster |
14
|
|
|
*/ |
15
|
|
|
public $posterUrl; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Construct. Instantiate TMDB Class. |
19
|
|
|
* |
20
|
|
|
* @param array $options Class instances. |
21
|
|
|
* |
22
|
|
|
* @throws \Exception |
23
|
|
|
*/ |
24
|
|
|
public function __construct(array $options = []) |
25
|
|
|
{ |
26
|
|
|
parent::__construct($options); |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Fetch banner from site. |
31
|
|
|
* |
32
|
|
|
* @param $videoId |
33
|
|
|
* @param $siteID |
34
|
|
|
* |
35
|
|
|
* @return bool |
36
|
|
|
*/ |
37
|
|
|
public function getBanner($videoId, $siteID): bool |
38
|
|
|
{ |
39
|
|
|
return false; |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Main processing director function for TMDB |
44
|
|
|
* Calls work query function and initiates processing. |
45
|
|
|
* |
46
|
|
|
* @param $groupID |
47
|
|
|
* @param $guidChar |
48
|
|
|
* @param $process |
49
|
|
|
* @param bool $local |
50
|
|
|
*/ |
51
|
|
|
public function processSite($groupID, $guidChar, $process, $local = false): void |
52
|
|
|
{ |
53
|
|
|
$res = $this->getTvReleases($groupID, $guidChar, $process, parent::PROCESS_TMDB); |
54
|
|
|
|
55
|
|
|
$tvcount = \count($res); |
|
|
|
|
56
|
|
|
$lookupSetting = true; |
57
|
|
|
|
58
|
|
|
if ($this->echooutput && $tvcount > 0) { |
59
|
|
|
$this->colorCli->header('Processing TMDB lookup for '.number_format($tvcount).' release(s).', true); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
if ($res instanceof \Traversable) { |
63
|
|
|
$this->titleCache = []; |
64
|
|
|
|
65
|
|
|
foreach ($res as $row) { |
66
|
|
|
$tmdbid = false; |
67
|
|
|
|
68
|
|
|
// Clean the show name for better match probability |
69
|
|
|
$release = $this->parseInfo($row['searchname']); |
70
|
|
|
|
71
|
|
|
if (\is_array($release) && $release['name'] !== '') { |
72
|
|
|
if (\in_array($release['cleanname'], $this->titleCache, false)) { |
73
|
|
|
if ($this->echooutput) { |
74
|
|
|
$this->colorCli->headerOver('Title: '). |
|
|
|
|
75
|
|
|
$this->colorCli->warningOver($release['cleanname']). |
|
|
|
|
76
|
|
|
$this->colorCli->header(' already failed lookup for this site. Skipping.', true); |
|
|
|
|
77
|
|
|
} |
78
|
|
|
$this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']); |
79
|
|
|
continue; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
// Find the Video ID if it already exists by checking the title against stored TMDB titles |
83
|
|
|
$videoId = $this->getByTitle($release['cleanname'], parent::TYPE_TV, parent::SOURCE_TMDB); |
84
|
|
|
|
85
|
|
|
// Force local lookup only |
86
|
|
|
if ($local === true) { |
87
|
|
|
$lookupSetting = false; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
// If lookups are allowed lets try to get it. |
91
|
|
|
if ($videoId === false && $lookupSetting) { |
92
|
|
|
if ($this->echooutput) { |
93
|
|
|
$this->colorCli->primaryOver('Checking TMDB for previously failed title: '). |
|
|
|
|
94
|
|
|
$this->colorCli->headerOver($release['cleanname']). |
|
|
|
|
95
|
|
|
$this->colorCli->primary('.', true); |
|
|
|
|
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
// Get the show from TMDB |
99
|
|
|
$tmdbShow = $this->getShowInfo((string) $release['cleanname']); |
100
|
|
|
|
101
|
|
|
if (\is_array($tmdbShow)) { |
102
|
|
|
// Check if we have the TMDB ID already, if we do use that Video ID |
103
|
|
|
$dupeCheck = $this->getVideoIDFromSiteID('tvdb', $tmdbShow['tvdb']); |
104
|
|
|
if ($dupeCheck === false) { |
105
|
|
|
$videoId = $this->add($tmdbShow); |
106
|
|
|
$tmdbid = $tmdbShow['tmdb']; |
107
|
|
|
} else { |
108
|
|
|
$videoId = $dupeCheck; |
109
|
|
|
// Update any missing fields and add site IDs |
110
|
|
|
$this->update($videoId, $tmdbShow); |
111
|
|
|
$tmdbid = $this->getSiteIDFromVideoID('tmdb', $videoId); |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
} else { |
115
|
|
|
if ($this->echooutput) { |
116
|
|
|
$this->colorCli->primaryOver('Found local TMDB match for: '.$release['cleanname']); |
117
|
|
|
$this->colorCli->primary('. Attempting episode lookup!', true); |
118
|
|
|
} |
119
|
|
|
$tmdbid = $this->getSiteIDFromVideoID('tmdb', $videoId); |
|
|
|
|
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
if (is_numeric($videoId) && $videoId > 0 && is_numeric($tmdbid) && $tmdbid > 0) { |
123
|
|
|
// Now that we have valid video and tmdb ids, try to get the poster |
124
|
|
|
$this->getPoster($videoId); |
125
|
|
|
|
126
|
|
|
$seasonNo = preg_replace('/^S0*/i', '', $release['season']); |
127
|
|
|
$episodeNo = preg_replace('/^E0*/i', '', $release['episode']); |
128
|
|
|
|
129
|
|
|
if ($episodeNo === 'all') { |
130
|
|
|
// Set the video ID and leave episode 0 |
131
|
|
|
$this->setVideoIdFound($videoId, $row['id'], 0); |
132
|
|
|
$this->colorCli->primary('Found TMDB Match for Full Season!', true); |
133
|
|
|
continue; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
// Download all episodes if new show to reduce API usage |
137
|
|
|
if ($this->countEpsByVideoID($videoId) === false) { |
138
|
|
|
$this->getEpisodeInfo($tmdbid, -1, -1, '', $videoId); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
// Check if we have the episode for this video ID |
142
|
|
|
$episode = $this->getBySeasonEp($videoId, $seasonNo, $episodeNo, $release['airdate']); |
143
|
|
|
|
144
|
|
|
if ($episode === false) { |
145
|
|
|
// Send the request for the episode to TMDB |
146
|
|
|
$tmdbEpisode = $this->getEpisodeInfo( |
147
|
|
|
$tmdbid, |
148
|
|
|
$seasonNo, |
149
|
|
|
$episodeNo, |
150
|
|
|
$release['airdate'] |
151
|
|
|
); |
152
|
|
|
|
153
|
|
|
if ($tmdbEpisode) { |
154
|
|
|
$episode = $this->addEpisode($videoId, $tmdbEpisode); |
155
|
|
|
} |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
if ($episode !== false && is_numeric($episode) && $episode > 0) { |
159
|
|
|
// Mark the releases video and episode IDs |
160
|
|
|
$this->setVideoIdFound($videoId, $row['id'], $episode); |
161
|
|
|
if ($this->echooutput) { |
162
|
|
|
$this->colorCli->primary('Found TMDB Match!', true); |
163
|
|
|
} |
164
|
|
|
continue; |
165
|
|
|
} |
166
|
|
|
//Processing failed, set the episode ID to the next processing group |
167
|
|
|
$this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']); |
168
|
|
|
} else { |
169
|
|
|
//Processing failed, set the episode ID to the next processing group |
170
|
|
|
$this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']); |
171
|
|
|
$this->titleCache[] = $release['cleanname']; |
172
|
|
|
} |
173
|
|
|
} else { |
174
|
|
|
//Processing failed, set the episode ID to the next processing group |
175
|
|
|
$this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']); |
176
|
|
|
$this->titleCache[] = $release['cleanname']; |
177
|
|
|
} |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Calls the API to perform initial show name match to TMDB title |
184
|
|
|
* Returns a formatted array of show data or false if no match. |
185
|
|
|
* |
186
|
|
|
* @param $cleanName |
187
|
|
|
* |
188
|
|
|
* @return array|false |
189
|
|
|
*/ |
190
|
|
|
protected function getShowInfo($cleanName) |
191
|
|
|
{ |
192
|
|
|
$return = $response = false; |
|
|
|
|
193
|
|
|
|
194
|
|
|
$response = TmdbClient::getSearchApi()->searchTv($cleanName); |
195
|
|
|
|
196
|
|
|
sleep(1); |
197
|
|
|
|
198
|
|
|
if (\is_array($response) && ! empty($response['results'])) { |
199
|
|
|
$return = $this->matchShowInfo($response['results'], $cleanName); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
return $return; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @param array $shows |
207
|
|
|
* @param string $cleanName |
208
|
|
|
* |
209
|
|
|
* @return array|false |
210
|
|
|
*/ |
211
|
|
|
private function matchShowInfo($shows, $cleanName) |
212
|
|
|
{ |
213
|
|
|
$return = false; |
214
|
|
|
$highestMatch = 0; |
215
|
|
|
|
216
|
|
|
$show = []; |
217
|
|
|
foreach ($shows as $show) { |
218
|
|
|
if ($this->checkRequiredAttr($show, 'tmdbS')) { |
|
|
|
|
219
|
|
|
// Check for exact title match first and then terminate if found |
220
|
|
|
if (strtolower($show['name']) === strtolower($cleanName)) { |
221
|
|
|
$highest = $show; |
222
|
|
|
break; |
223
|
|
|
} |
224
|
|
|
// Check each show title for similarity and then find the highest similar value |
225
|
|
|
$matchPercent = $this->checkMatch(strtolower($show['name']), strtolower($cleanName), self::MATCH_PROBABILITY); |
226
|
|
|
|
227
|
|
|
// If new match has a higher percentage, set as new matched title |
228
|
|
|
if ($matchPercent > $highestMatch) { |
229
|
|
|
$highestMatch = $matchPercent; |
230
|
|
|
$highest = $show; |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
if (! empty($highest)) { |
235
|
|
|
$showAlternativeTitles = TmdbClient::getTvApi()->getAlternativeTitles($highest['id']); |
236
|
|
|
$showExternalIds = TmdbClient::getTvApi()->getExternalIds($highest['id']); |
237
|
|
|
|
238
|
|
|
|
239
|
|
|
if ($showAlternativeTitles !== null && \is_array($showAlternativeTitles)) { |
240
|
|
|
foreach ($showAlternativeTitles as $aka) { |
241
|
|
|
$highest['alternative_titles'][] = $aka['title']; |
242
|
|
|
} |
243
|
|
|
$highest['network'] = $show['networks'][0]['name'] ?? ''; |
244
|
|
|
$highest['external_ids'] = $showExternalIds; |
245
|
|
|
} |
246
|
|
|
$return = $this->formatShowInfo($highest); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
return $return; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Retrieves the poster art for the processed show. |
254
|
|
|
* |
255
|
|
|
* @param int $videoId -- the local Video ID |
256
|
|
|
* |
257
|
|
|
* @return int |
258
|
|
|
*/ |
259
|
|
|
public function getPoster($videoId): int |
260
|
|
|
{ |
261
|
|
|
$ri = new ReleaseImage(); |
262
|
|
|
|
263
|
|
|
$hascover = 0; |
264
|
|
|
|
265
|
|
|
// Try to get the Poster |
266
|
|
|
if (! empty($this->posterUrl)) { |
267
|
|
|
$hascover = $ri->saveImage($videoId, $this->posterUrl, $this->imgSavePath); |
268
|
|
|
|
269
|
|
|
// Mark it retrieved if we saved an image |
270
|
|
|
if ($hascover === 1) { |
271
|
|
|
$this->setCoverFound($videoId); |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
return $hascover; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Gets the specific episode info for the parsed release after match |
280
|
|
|
* Returns a formatted array of episode data or false if no match. |
281
|
|
|
* |
282
|
|
|
* @param int $tmdbid |
283
|
|
|
* @param int $season |
284
|
|
|
* @param int $episode |
285
|
|
|
* @param string $airdate |
286
|
|
|
* @param int $videoId |
287
|
|
|
* |
288
|
|
|
* @return array|false |
289
|
|
|
*/ |
290
|
|
|
protected function getEpisodeInfo($tmdbid, $season, $episode, $airdate = '', $videoId = 0) |
291
|
|
|
{ |
292
|
|
|
$return = false; |
293
|
|
|
|
294
|
|
|
try { |
295
|
|
|
$response = TmdbClient::getTvEpisodeApi()->getEpisode($tmdbid, $season, $episode); |
296
|
|
|
} catch (TmdbApiException $e) { |
|
|
|
|
297
|
|
|
return false; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
sleep(1); |
301
|
|
|
|
302
|
|
|
//Handle Single Episode Lookups |
303
|
|
|
if (\is_array($response) && $this->checkRequiredAttr($response, 'tmdbE')) { |
|
|
|
|
304
|
|
|
$return = $this->formatEpisodeInfo($response); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
return $return; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Assigns API show response values to a formatted array for insertion |
312
|
|
|
* Returns the formatted array. |
313
|
|
|
* |
314
|
|
|
* @param $show |
315
|
|
|
* |
316
|
|
|
* @return array |
317
|
|
|
*/ |
318
|
|
|
protected function formatShowInfo($show): array |
319
|
|
|
{ |
320
|
|
|
$this->posterUrl = isset($show['poster_path']) ? 'https:'.$this->helper->getUrl($show['poster_path']) : ''; |
|
|
|
|
321
|
|
|
|
322
|
|
|
if (isset($show['external_ids']['imdb_id'])) { |
323
|
|
|
preg_match('/tt(?P<imdbid>\d{6,7})$/i', $show['external_ids']['imdb_id'], $imdb); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
return [ |
327
|
|
|
'type' => parent::TYPE_TV, |
328
|
|
|
'title' => (string) $show['name'], |
329
|
|
|
'summary' => (string) $show['overview'], |
330
|
|
|
'started' => (string) $show['first_air_date'], |
331
|
|
|
'publisher' => isset($show['network']) ? (string) $show['network'] : '', |
332
|
|
|
'country' => $show['origin_country'][0] ?? '', |
333
|
|
|
'source' => parent::SOURCE_TMDB, |
334
|
|
|
'imdb' => isset($imdb['imdbid']) ? (int) $imdb['imdbid'] : 0, |
335
|
|
|
'tvdb' => isset($show['external_ids']['tvdb_id']) ? (int) $show['external_ids']['tvdb_id'] : 0, |
336
|
|
|
'trakt' => 0, |
337
|
|
|
'tvrage' => isset($show['external_ids']['tvrage_id']) ? (int) $show['external_ids']['tvrage_id'] : 0, |
338
|
|
|
'tvmaze' => 0, |
339
|
|
|
'tmdb' => (int) $show['id'], |
340
|
|
|
'aliases' => ! empty($show['alternative_titles']) ? (array) $show['alternative_titles'] : '', |
341
|
|
|
'localzone' => "''", |
342
|
|
|
]; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Assigns API episode response values to a formatted array for insertion |
347
|
|
|
* Returns the formatted array. |
348
|
|
|
* |
349
|
|
|
* @param $episode |
350
|
|
|
* |
351
|
|
|
* @return array |
352
|
|
|
*/ |
353
|
|
|
protected function formatEpisodeInfo($episode): array |
354
|
|
|
{ |
355
|
|
|
return [ |
356
|
|
|
'title' => (string) $episode['name'], |
357
|
|
|
'series' => (int) $episode['season_number'], |
358
|
|
|
'episode' => (int) $episode['episode_number'], |
359
|
|
|
'se_complete' => 'S'.sprintf('%02d', $episode['season_number']).'E'.sprintf('%02d', $episode['episode_number']), |
360
|
|
|
'firstaired' => (string) $episode['air_date'], |
361
|
|
|
'summary' => (string) $episode['overview'], |
362
|
|
|
]; |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|