1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Blacklight\processing\tv; |
4
|
|
|
|
5
|
|
|
use Blacklight\libraries\FanartTV; |
6
|
|
|
use Blacklight\ReleaseImage; |
7
|
|
|
use CanIHaveSomeCoffee\TheTVDbAPI\Exception\ParseException; |
8
|
|
|
use CanIHaveSomeCoffee\TheTVDbAPI\Exception\ResourceNotFoundException; |
9
|
|
|
use CanIHaveSomeCoffee\TheTVDbAPI\Exception\UnauthorizedException; |
10
|
|
|
use CanIHaveSomeCoffee\TheTVDbAPI\TheTVDbAPI; |
11
|
|
|
use Symfony\Component\Serializer\Exception\ExceptionInterface; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Class TVDB -- functions used to post process releases against TVDB. |
15
|
|
|
*/ |
16
|
|
|
class TVDB extends TV |
17
|
|
|
{ |
18
|
|
|
private const MATCH_PROBABILITY = 75; |
19
|
|
|
|
20
|
|
|
public TheTVDbAPI $client; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var string Authorization token for TVDB v2 API |
24
|
|
|
*/ |
25
|
|
|
public string $token = ''; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @string URL for show poster art |
29
|
|
|
*/ |
30
|
|
|
public string $posterUrl = ''; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @bool Do a local lookup only if server is down |
34
|
|
|
*/ |
35
|
|
|
private bool $local; |
36
|
|
|
|
37
|
|
|
private FanartTV $fanart; |
38
|
|
|
|
39
|
|
|
private mixed $fanartapikey; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* TVDB constructor. |
43
|
|
|
* |
44
|
|
|
* |
45
|
|
|
* @throws \Exception |
46
|
|
|
*/ |
47
|
|
|
public function __construct() |
48
|
|
|
{ |
49
|
|
|
parent::__construct(); |
50
|
|
|
$this->client = new TheTVDbAPI(); |
51
|
|
|
$this->local = false; |
52
|
|
|
$this->authorizeTvdb(); |
53
|
|
|
|
54
|
|
|
$this->fanartapikey = config('nntmux_api.fanarttv_api_key'); |
55
|
|
|
if ($this->fanartapikey !== null) { |
56
|
|
|
$this->fanart = new FanartTV($this->fanartapikey); |
57
|
|
|
} |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Main processing director function for scrapers |
62
|
|
|
* Calls work query function and initiates processing. |
63
|
|
|
*/ |
64
|
|
|
public function processSite($groupID, $guidChar, $process, bool $local = false): void |
65
|
|
|
{ |
66
|
|
|
$res = $this->getTvReleases($groupID, $guidChar, $process, parent::PROCESS_TVDB); |
67
|
|
|
|
68
|
|
|
$tvCount = \count($res); |
69
|
|
|
|
70
|
|
|
if ($this->echooutput && $tvCount > 0) { |
71
|
|
|
$this->colorCli->header('Processing TVDB lookup for '.number_format($tvCount).' release(s).', true); |
72
|
|
|
} |
73
|
|
|
$this->titleCache = []; |
74
|
|
|
|
75
|
|
|
foreach ($res as $row) { |
76
|
|
|
$tvDbId = false; |
77
|
|
|
$this->posterUrl = ''; |
78
|
|
|
|
79
|
|
|
// Clean the show name for better match probability |
80
|
|
|
$release = $this->parseInfo($row['searchname']); |
81
|
|
|
if (\is_array($release) && $release['name'] !== '') { |
82
|
|
|
if (\in_array($release['cleanname'], $this->titleCache, false)) { |
83
|
|
|
if ($this->echooutput) { |
84
|
|
|
$this->colorCli->headerOver('Title: '). |
|
|
|
|
85
|
|
|
$this->colorCli->warningOver($release['cleanname']). |
|
|
|
|
86
|
|
|
$this->colorCli->header(' already failed lookup for this site. Skipping.', true); |
|
|
|
|
87
|
|
|
} |
88
|
|
|
$this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']); |
89
|
|
|
|
90
|
|
|
continue; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// Find the Video ID if it already exists by checking the title. |
94
|
|
|
$videoId = $this->getByTitle($release['cleanname'], parent::TYPE_TV); |
95
|
|
|
|
96
|
|
|
if ($videoId !== 0) { |
97
|
|
|
$tvDbId = $this->getSiteByID('tvdb', $videoId); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// Force local lookup only |
101
|
|
|
$lookupSetting = true; |
102
|
|
|
if ($local === true || $this->local) { |
103
|
|
|
$lookupSetting = false; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
if ($tvDbId === false && $lookupSetting) { |
107
|
|
|
// If it doesn't exist locally and lookups are allowed lets try to get it. |
108
|
|
|
if ($this->echooutput) { |
109
|
|
|
$this->colorCli->climate()->error('Video ID for '.$release['cleanname'].' not found in local db, checking web.'); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
// Check if we have a valid country and set it in the array |
113
|
|
|
$country = ( |
114
|
|
|
isset($release['country']) && \strlen($release['country']) === 2 |
115
|
|
|
? (string) $release['country'] |
116
|
|
|
: '' |
117
|
|
|
); |
118
|
|
|
|
119
|
|
|
// Get the show from TVDB |
120
|
|
|
$tvdbShow = $this->getShowInfo((string) $release['cleanname']); |
121
|
|
|
|
122
|
|
|
if (\is_array($tvdbShow)) { |
123
|
|
|
$tvdbShow['country'] = $country; |
124
|
|
|
$videoId = $this->add($tvdbShow); |
125
|
|
|
$tvDbId = (int) $tvdbShow['tvdb']; |
126
|
|
|
} |
127
|
|
|
} elseif ($this->echooutput && $tvDbId !== false) { |
128
|
|
|
$this->colorCli->climate()->info('Video ID for '.$release['cleanname'].' found in local db, attempting episode match.'); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
if ((int) $videoId > 0 && (int) $tvDbId > 0) { |
132
|
|
|
if (! empty($tvdbShow['poster'])) { // Use TVDB poster if available |
133
|
|
|
$this->getPoster($videoId); |
134
|
|
|
} else { // Check Fanart.tv for poster |
135
|
|
|
$poster = $this->fanart->getTVFanArt($tvDbId); |
136
|
|
|
if ($poster) { |
137
|
|
|
$this->posterUrl = collect($poster['tvposter'])->sortByDesc('likes')[0]['url']; |
138
|
|
|
$this->getPoster($videoId); |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
$seasonNo = (! empty($release['season']) ? preg_replace('/^S0*/i', '', $release['season']) : ''); |
143
|
|
|
$episodeNo = (! empty($release['episode']) ? preg_replace('/^E0*/i', '', $release['episode']) : ''); |
144
|
|
|
|
145
|
|
|
if ($episodeNo === 'all') { |
146
|
|
|
// Set the video ID and leave episode 0 |
147
|
|
|
$this->setVideoIdFound($videoId, $row['id'], 0); |
148
|
|
|
$this->colorCli->climate()->info('Found TVDB Match for Full Season!'); |
149
|
|
|
|
150
|
|
|
continue; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
// Download all episodes if new show to reduce API/bandwidth usage |
154
|
|
|
if (! $this->countEpsByVideoID($videoId)) { |
155
|
|
|
$this->getEpisodeInfo($tvDbId, -1, -1, $videoId); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
// Check if we have the episode for this video ID |
159
|
|
|
$episode = $this->getBySeasonEp($videoId, $seasonNo, $episodeNo, $release['airdate']); |
160
|
|
|
|
161
|
|
|
if ($episode === false && $lookupSetting) { |
162
|
|
|
// Send the request for the episode to TVDB |
163
|
|
|
$tvdbEpisode = $this->getEpisodeInfo( |
164
|
|
|
$tvDbId, |
165
|
|
|
(int) $seasonNo, |
166
|
|
|
(int) $episodeNo, |
167
|
|
|
$videoId |
168
|
|
|
); |
169
|
|
|
|
170
|
|
|
if ($tvdbEpisode) { |
|
|
|
|
171
|
|
|
$episode = $this->addEpisode($videoId, $tvdbEpisode); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
if ($episode !== false && is_numeric($episode) && $episode > 0) { |
176
|
|
|
// Mark the releases video and episode IDs |
177
|
|
|
$this->setVideoIdFound($videoId, $row['id'], $episode); |
178
|
|
|
if ($this->echooutput) { |
179
|
|
|
$this->colorCli->climate()->info('Found TVDB Match!'); |
180
|
|
|
} |
181
|
|
|
} else { |
182
|
|
|
//Processing failed, set the episode ID to the next processing group |
183
|
|
|
$this->setVideoIdFound($videoId, $row['id'], 0); |
184
|
|
|
$this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']); |
185
|
|
|
} |
186
|
|
|
} else { |
187
|
|
|
//Processing failed, set the episode ID to the next processing group |
188
|
|
|
$this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']); |
189
|
|
|
$this->titleCache[] = $release['cleanname'] ?? null; |
190
|
|
|
} |
191
|
|
|
} else { |
192
|
|
|
//Parsing failed, take it out of the queue for examination |
193
|
|
|
$this->setVideoNotFound(parent::FAILED_PARSE, $row['id']); |
194
|
|
|
$this->titleCache[] = $release['cleanname'] ?? null; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Placeholder for Videos getBanner. |
201
|
|
|
*/ |
202
|
|
|
protected function getBanner($videoID, $siteId): bool |
203
|
|
|
{ |
204
|
|
|
return false; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Calls the API to perform initial show name match to TVDB title |
209
|
|
|
* Returns a formatted array of show data or false if no match. |
210
|
|
|
* |
211
|
|
|
* |
212
|
|
|
* @throws UnauthorizedException |
213
|
|
|
* @throws ParseException |
214
|
|
|
* @throws ExceptionInterface |
215
|
|
|
*/ |
216
|
|
|
protected function getShowInfo(string $name): bool|array |
217
|
|
|
{ |
218
|
|
|
$return = $response = false; |
|
|
|
|
219
|
|
|
$highestMatch = 0; |
220
|
|
|
try { |
221
|
|
|
$response = $this->client->search()->search($name, ['type' => 'series']); |
222
|
|
|
} catch (ResourceNotFoundException $e) { |
223
|
|
|
$response = false; |
224
|
|
|
$this->colorCli->climate()->error('Show not found on TVDB'); |
225
|
|
|
} catch (UnauthorizedException $e) { |
226
|
|
|
try { |
227
|
|
|
$this->authorizeTvdb(); |
228
|
|
|
} catch (UnauthorizedException $error) { |
229
|
|
|
$this->colorCli->climate()->error('Not authorized to access TVDB'); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
sleep(1); |
234
|
|
|
|
235
|
|
|
if (\is_array($response)) { |
236
|
|
|
foreach ($response as $show) { |
237
|
|
|
if ($this->checkRequiredAttr($show, 'tvdbS')) { |
238
|
|
|
// Check for exact title match first and then terminate if found |
239
|
|
|
if (strtolower($show->name) === strtolower($name)) { |
240
|
|
|
$highest = $show; |
241
|
|
|
break; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
// Check each show title for similarity and then find the highest similar value |
245
|
|
|
$matchPercent = $this->checkMatch(strtolower($show->name), strtolower($name), self::MATCH_PROBABILITY); |
246
|
|
|
|
247
|
|
|
// If new match has a higher percentage, set as new matched title |
248
|
|
|
if ($matchPercent > $highestMatch) { |
249
|
|
|
$highestMatch = $matchPercent; |
250
|
|
|
$highest = $show; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
// Check for show aliases and try match those too |
254
|
|
|
if (! empty($show->aliases)) { |
255
|
|
|
foreach ($show->aliases as $key => $name) { |
256
|
|
|
$matchPercent = $this->checkMatch(strtolower($name), strtolower($name), $matchPercent); |
257
|
|
|
if ($matchPercent > $highestMatch) { |
258
|
|
|
$highestMatch = $matchPercent; |
259
|
|
|
$highest = $show; |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
if (! empty($highest)) { |
266
|
|
|
$return = $this->formatShowInfo($highest); |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
return $return; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Retrieves the poster art for the processed show. |
275
|
|
|
* |
276
|
|
|
* @param int $videoId -- the local Video ID |
277
|
|
|
*/ |
278
|
|
|
public function getPoster(int $videoId): int |
279
|
|
|
{ |
280
|
|
|
$ri = new ReleaseImage(); |
281
|
|
|
|
282
|
|
|
// Try to get the Poster |
283
|
|
|
$hasCover = $ri->saveImage($videoId, $this->posterUrl, $this->imgSavePath); |
284
|
|
|
// Mark it retrieved if we saved an image |
285
|
|
|
if ($hasCover === 1) { |
286
|
|
|
$this->setCoverFound($videoId); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
return $hasCover; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* @throws ParseException |
294
|
|
|
* @throws UnauthorizedException |
295
|
|
|
* @throws ExceptionInterface |
296
|
|
|
*/ |
297
|
|
|
protected function getEpisodeInfo(int|string $tvDbId, int|string $season, int|string $episode, int $videoId = 0): bool|array |
298
|
|
|
{ |
299
|
|
|
$return = $response = false; |
300
|
|
|
|
301
|
|
|
if (! $this->local) { |
302
|
|
|
if ($videoId > 0) { |
303
|
|
|
try { |
304
|
|
|
$response = $this->client->series()->allEpisodes($tvDbId); |
|
|
|
|
305
|
|
|
} catch (ResourceNotFoundException $error) { |
306
|
|
|
return false; |
307
|
|
|
} catch (UnauthorizedException $error) { |
308
|
|
|
|
309
|
|
|
try { |
310
|
|
|
$this->authorizeTvdb(); |
311
|
|
|
} catch (UnauthorizedException $error) { |
312
|
|
|
$this->colorCli->climate()->error('Not authorized to access TVDB'); |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
} else { |
316
|
|
|
try { |
317
|
|
|
foreach ($this->client->series()->episodes($tvDbId) as $episodeBaseRecord) { |
|
|
|
|
318
|
|
|
if ($episodeBaseRecord->seasonNumber === $season && $episodeBaseRecord->number === $episode) { |
319
|
|
|
$response = $episodeBaseRecord; |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
} catch (ResourceNotFoundException $error) { |
323
|
|
|
return false; |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
sleep(1); |
328
|
|
|
|
329
|
|
|
if (\is_object($response)) { |
330
|
|
|
if ($this->checkRequiredAttr($response, 'tvdbE')) { |
331
|
|
|
$return = $this->formatEpisodeInfo($response); |
332
|
|
|
} |
333
|
|
|
} elseif ($videoId > 0 && \is_array($response)) { |
334
|
|
|
foreach ($response as $singleEpisode) { |
335
|
|
|
if ($this->checkRequiredAttr($singleEpisode, 'tvdbE')) { |
336
|
|
|
$this->addEpisode($videoId, $this->formatEpisodeInfo($singleEpisode)); |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
return $return; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Assigns API show response values to a formatted array for insertion |
347
|
|
|
* Returns the formatted array. |
348
|
|
|
* |
349
|
|
|
* @throws ExceptionInterface |
350
|
|
|
* @throws ParseException |
351
|
|
|
* @throws UnauthorizedException |
352
|
|
|
*/ |
353
|
|
|
protected function formatShowInfo($show): array |
354
|
|
|
{ |
355
|
|
|
try { |
356
|
|
|
$poster = $this->client->series()->artworks($show->tvdb_id); |
357
|
|
|
// Grab the image with the highest score where type == 2 |
358
|
|
|
$poster = collect($poster)->where('type', 2)->sortByDesc('score')->first(); |
|
|
|
|
359
|
|
|
$this->posterUrl = ! empty($poster->image) ? $poster->image : ''; |
360
|
|
|
} catch (ResourceNotFoundException $e) { |
361
|
|
|
$this->colorCli->climate()->error('Poster image not found on TVDB'); |
362
|
|
|
} catch (UnauthorizedException $error) { |
363
|
|
|
|
364
|
|
|
try { |
365
|
|
|
$this->authorizeTvdb(); |
366
|
|
|
} catch (UnauthorizedException $error) { |
367
|
|
|
$this->colorCli->climate()->error('Not authorized to access TVDB'); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
try { |
372
|
|
|
$imdbId = $this->client->series()->extended($show->tvdb_id); |
373
|
|
|
preg_match('/tt(?P<imdbid>\d{6,9})$/i', $imdbId->getIMDBId(), $imdb); |
|
|
|
|
374
|
|
|
} catch (ResourceNotFoundException $e) { |
375
|
|
|
$this->colorCli->climate()->error('Show ImdbId not found on TVDB'); |
376
|
|
|
} catch (UnauthorizedException $error) { |
377
|
|
|
|
378
|
|
|
try { |
379
|
|
|
$this->authorizeTvdb(); |
380
|
|
|
} catch (UnauthorizedException $error) { |
381
|
|
|
$this->colorCli->climate()->error('Not authorized to access TVDB'); |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
return [ |
386
|
|
|
'type' => parent::TYPE_TV, |
387
|
|
|
'title' => (string) $show->name, |
388
|
|
|
'summary' => (string) $show->overview, |
389
|
|
|
'started' => $show->first_air_time, |
390
|
|
|
'publisher' => $imdbId->originalNetwork->name ?? '', |
391
|
|
|
'poster' => $this->posterUrl, |
392
|
|
|
'source' => parent::SOURCE_TVDB, |
393
|
|
|
'imdb' => (int) ($imdb['imdbid'] ?? 0), |
394
|
|
|
'tvdb' => (int) $show->tvdb_id, |
395
|
|
|
'trakt' => 0, |
396
|
|
|
'tvrage' => 0, |
397
|
|
|
'tvmaze' => 0, |
398
|
|
|
'tmdb' => 0, |
399
|
|
|
'aliases' => ! empty($show->aliases) ? $show->aliases : '', |
400
|
|
|
'localzone' => "''", |
401
|
|
|
]; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* Assigns API episode response values to a formatted array for insertion |
406
|
|
|
* Returns the formatted array. |
407
|
|
|
*/ |
408
|
|
|
protected function formatEpisodeInfo($episode): array |
409
|
|
|
{ |
410
|
|
|
return [ |
411
|
|
|
'title' => (string) $episode->name, |
412
|
|
|
'series' => (int) $episode->seasonNumber, |
413
|
|
|
'episode' => (int) $episode->number, |
414
|
|
|
'se_complete' => 'S'.sprintf('%02d', $episode->seasonNumber).'E'.sprintf('%02d', $episode->number), |
415
|
|
|
'firstaired' => $episode->aired, |
416
|
|
|
'summary' => (string) $episode->overview, |
417
|
|
|
]; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
protected function authorizeTvdb(): void |
421
|
|
|
{ |
422
|
|
|
// Check if we can get the time for API status |
423
|
|
|
// If we can't then we set local to true |
424
|
|
|
$this->token = ''; |
425
|
|
|
// Check if we have the tvdb api key and user pin |
426
|
|
|
if (config('tvdb.api_key') === null || config('tvdb.user_pin') === null) { |
427
|
|
|
$this->colorCli->warning('TVDB API key or user pin not set. Running in local mode only!', true); |
428
|
|
|
$this->local = true; |
429
|
|
|
} else { |
430
|
|
|
try { |
431
|
|
|
$this->token = $this->client->authentication()->login(config('tvdb.api_key'), config('tvdb.user_pin')); |
432
|
|
|
} catch (UnauthorizedException $error) { |
433
|
|
|
$this->colorCli->warning('Could not reach TVDB API. Running in local mode only!', true); |
434
|
|
|
$this->local = true; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
if ($this->token !== '') { |
438
|
|
|
$this->client->setToken($this->token); |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|