TvdbProvider::lookupExternalIds()   C
last analyzed

Complexity

Conditions 17
Paths 88

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
dl 0
loc 46
rs 5.2166
c 1
b 0
f 0
cc 17
nc 88
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Services\TvProcessing\Providers;
4
5
use App\Services\FanartTvService;
6
use App\Services\ReleaseImageService;
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 TvdbProvider -- functions used to post process releases against TVDB.
15
 */
16
class TvdbProvider extends AbstractTvProvider
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 FanartTvService $fanart;
38
39
    private mixed $fanartapikey;
40
41
    /**
42
     * TvdbProvider constructor.
43
     *
44
     * @throws \Exception
45
     */
46
    public function __construct()
47
    {
48
        parent::__construct();
49
        $this->client = new TheTVDbAPI;
50
        $this->local = false;
51
        $this->authorizeTvdb();
52
53
        $this->fanartapikey = config('nntmux_api.fanarttv_api_key');
54
        $this->fanart = new FanartTvService($this->fanartapikey);
55
    }
56
57
    /**
58
     * Main processing director function for scrapers
59
     * Calls work query function and initiates processing.
60
     */
61
    public function processSite($groupID, $guidChar, $process, bool $local = false): void
62
    {
63
        $res = $this->getTvReleases($groupID, $guidChar, $process, parent::PROCESS_TVDB);
64
65
        $tvCount = \count($res);
66
67
        if ($tvCount === 0) {
68
            return;
69
        }
70
71
        $this->titleCache = [];
72
        $processed = 0;
73
        $matched = 0;
74
        $skipped = 0;
75
76
        foreach ($res as $row) {
77
            $processed++;
78
            $siteId = false;
79
            $this->posterUrl = '';
80
81
            $release = $this->parseInfo($row['searchname']);
82
            if (\is_array($release) && $release['name'] !== '') {
83
                if (\in_array($release['cleanname'], $this->titleCache, false)) {
84
                    if ($this->echooutput) {
85
                        cli()->primaryOver('    → ');
86
                        cli()->alternateOver($this->truncateTitle($release['cleanname']));
87
                        cli()->primaryOver(' → ');
88
                        cli()->alternate('Skipped (previously failed)');
89
                    }
90
                    $this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']);
91
                    $skipped++;
92
                    continue;
93
                }
94
95
                $videoId = $this->getByTitle($release['cleanname'], parent::TYPE_TV);
96
97
                if ($videoId !== 0) {
98
                    $siteId = $this->getSiteByID('tvdb', $videoId);
99
                }
100
101
                $lookupSetting = true;
102
                if ($local === true || $this->local) {
103
                    $lookupSetting = false;
104
                }
105
106
                if ($siteId === false && $lookupSetting) {
107
                    if ($this->echooutput) {
108
                        cli()->primaryOver('    → ');
109
                        cli()->headerOver($this->truncateTitle($release['cleanname']));
110
                        cli()->primaryOver(' → ');
111
                        cli()->info('Searching TVDB...');
112
                    }
113
114
                    $country = (
115
                        isset($release['country']) && \strlen($release['country']) === 2
116
                            ? (string) $release['country']
117
                            : ''
118
                    );
119
120
                    $tvdbShow = $this->getShowInfo((string) $release['cleanname']);
121
122
                    if (\is_array($tvdbShow)) {
123
                        $tvdbShow['country'] = $country;
124
                        $videoId = $this->add($tvdbShow);
125
                        $siteId = (int) $tvdbShow['tvdb'];
126
                    }
127
                } elseif ($this->echooutput && $siteId !== false) {
128
                    cli()->primaryOver('    → ');
129
                    cli()->headerOver($this->truncateTitle($release['cleanname']));
130
                    cli()->primaryOver(' → ');
131
                    cli()->info('Found in DB');
132
                }
133
134
                if ((int) $videoId > 0 && (int) $siteId > 0) {
135
                    if (! empty($tvdbShow['poster'])) {
136
                        $this->getPoster($videoId);
137
                    } elseif ($this->fanart->isConfigured()) {
138
                        $posterUrl = $this->fanart->getBestTvPoster($siteId);
139
                        if (! empty($posterUrl)) {
140
                            $this->posterUrl = $posterUrl;
141
                            $this->getPoster($videoId);
142
                        }
143
                    }
144
145
                    $seriesNo = (! empty($release['season']) ? preg_replace('/^S0*/i', '', $release['season']) : '');
146
                    $episodeNo = (! empty($release['episode']) ? preg_replace('/^E0*/i', '', $release['episode']) : '');
147
                    $hasAirdate = ! empty($release['airdate']);
148
149
                    if ($episodeNo === 'all') {
150
                        $this->setVideoIdFound($videoId, $row['id'], 0);
151
                        if ($this->echooutput) {
152
                            cli()->primaryOver('    → ');
153
                            cli()->headerOver($this->truncateTitle($release['cleanname']));
154
                            cli()->primaryOver(' → ');
155
                            cli()->primary('Full Season matched');
156
                        }
157
                        $matched++;
158
                        continue;
159
                    }
160
161
                    if (! $this->countEpsByVideoID($videoId)) {
162
                        $this->getEpisodeInfo($siteId, -1, -1, $videoId);
163
                    }
164
165
                    $episode = $this->getBySeasonEp($videoId, $seriesNo, $episodeNo, $release['airdate']);
166
167
                    if ($episode === false && $lookupSetting) {
168
                        if ($seriesNo !== '' && $episodeNo !== '') {
169
                            $tvdbEpisode = $this->getEpisodeInfo($siteId, (int) $seriesNo, (int) $episodeNo, $videoId);
170
                            if ($tvdbEpisode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tvdbEpisode of type array<string,integer|mixed|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
171
                                $episode = $this->addEpisode($videoId, $tvdbEpisode);
172
                            }
173
                        }
174
175
                        if ($episode === false && $hasAirdate) {
176
                            $this->getEpisodeInfo($siteId, -1, -1, $videoId);
177
                            $episode = $this->getBySeasonEp($videoId, 0, 0, $release['airdate']);
178
                        }
179
                    }
180
181
                    if ($episode !== false && is_numeric($episode) && $episode > 0) {
182
                        $this->setVideoIdFound($videoId, $row['id'], $episode);
183
                        if ($this->echooutput) {
184
                            cli()->primaryOver('    → ');
185
                            cli()->headerOver($this->truncateTitle($release['cleanname']));
186
                            if ($seriesNo !== '' && $episodeNo !== '') {
187
                                cli()->primaryOver(' S');
188
                                cli()->warningOver(sprintf('%02d', $seriesNo));
189
                                cli()->primaryOver('E');
190
                                cli()->warningOver(sprintf('%02d', $episodeNo));
191
                            } elseif ($hasAirdate) {
192
                                cli()->primaryOver(' | ');
193
                                cli()->warningOver($release['airdate']);
194
                            }
195
                            cli()->primaryOver(' ✓ ');
196
                            cli()->primary('MATCHED (TVDB)');
197
                        }
198
                        $matched++;
199
                    } else {
200
                        $this->setVideoIdFound($videoId, $row['id'], 0);
201
                        $this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']);
202
                        if ($this->echooutput) {
203
                            cli()->primaryOver('    → ');
204
                            cli()->alternateOver($this->truncateTitle($release['cleanname']));
205
                            if ($hasAirdate) {
206
                                cli()->primaryOver(' | ');
207
                                cli()->warningOver($release['airdate']);
208
                            }
209
                            cli()->primaryOver(' → ');
210
                            cli()->warning('Episode not found');
211
                        }
212
                    }
213
                } else {
214
                    $this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']);
215
                    $this->titleCache[] = $release['cleanname'] ?? null;
216
                    if ($this->echooutput) {
217
                        cli()->primaryOver('    → ');
218
                        cli()->alternateOver($this->truncateTitle($release['cleanname']));
219
                        cli()->primaryOver(' → ');
220
                        cli()->warning('Not found');
221
                    }
222
                }
223
            } else {
224
                $this->setVideoNotFound(parent::FAILED_PARSE, $row['id']);
225
                $this->titleCache[] = $release['cleanname'] ?? null;
226
                if ($this->echooutput) {
227
                    cli()->error(sprintf(
228
                        '  ✗ [%d/%d] Parse failed: %s',
229
                        $processed,
230
                        $tvCount,
231
                        mb_substr($row['searchname'], 0, 50)
232
                    ));
233
                }
234
            }
235
        }
236
237
        if ($this->echooutput && $matched > 0) {
238
            echo "\n";
239
            cli()->primaryOver('  ✓ TVDB: ');
240
            cli()->primary(sprintf('%d matched, %d skipped', $matched, $skipped));
241
        }
242
    }
243
244
    public function getBanner($videoID, $siteId): bool
245
    {
246
        return false;
247
    }
248
249
    /**
250
     * @throws UnauthorizedException
251
     * @throws ParseException
252
     * @throws ExceptionInterface
253
     */
254
    public function getShowInfo(string $name): bool|array
255
    {
256
        $return = $response = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
257
        $highestMatch = 0;
258
        try {
259
            $response = $this->client->search()->search($name, ['type' => 'series']);
260
        } catch (ResourceNotFoundException $e) {
261
            $response = false;
262
            cli()->error('Show not found on TVDB');
263
        } catch (UnauthorizedException $e) {
264
            try {
265
                $this->authorizeTvdb();
266
            } catch (UnauthorizedException $error) {
267
                cli()->error('Not authorized to access TVDB');
268
            }
269
        }
270
271
        sleep(1);
272
273
        if (\is_array($response)) {
274
            foreach ($response as $show) {
275
                if ($this->checkRequiredAttr($show, 'tvdbS')) {
276
                    if (strtolower($show->name) === strtolower($name)) {
277
                        $highest = $show;
278
                        break;
279
                    }
280
281
                    $matchPercent = $this->checkMatch(strtolower($show->name), strtolower($name), self::MATCH_PROBABILITY);
282
283
                    if ($matchPercent > $highestMatch) {
284
                        $highestMatch = $matchPercent;
285
                        $highest = $show;
286
                    }
287
288
                    if (! empty($show->aliases)) {
289
                        foreach ($show->aliases as $akaIndex => $akaName) {
290
                            $aliasPercent = $this->checkMatch(strtolower($akaName), strtolower($name), self::MATCH_PROBABILITY);
291
                            if ($aliasPercent > $highestMatch) {
292
                                $highestMatch = $aliasPercent;
293
                                $highest = $show;
294
                            }
295
                        }
296
                    }
297
                }
298
            }
299
            if (! empty($highest)) {
300
                $return = $this->formatShowInfo($highest);
301
            }
302
        }
303
304
        return $return;
305
    }
306
307
    public function getPoster(int $videoId): int
308
    {
309
        $ri = new ReleaseImageService;
310
        $hasCover = 0;
311
312
        if (! empty($this->posterUrl)) {
313
            $hasCover = $ri->saveImage($videoId, $this->posterUrl, $this->imgSavePath);
314
            if ($hasCover === 1) {
315
                $this->setCoverFound($videoId);
316
            }
317
        }
318
319
        return $hasCover;
320
    }
321
322
    /**
323
     * @throws ParseException
324
     * @throws UnauthorizedException
325
     * @throws ExceptionInterface
326
     */
327
    public function getEpisodeInfo(int|string $siteId, int|string $series, int|string $episode, int $videoId = 0): bool|array
328
    {
329
        $return = $response = false;
330
331
        if (! $this->local) {
332
            if ($videoId > 0) {
333
                try {
334
                    $response = $this->client->series()->allEpisodes($siteId);
0 ignored issues
show
Bug introduced by
It seems like $siteId can also be of type string; however, parameter $id of CanIHaveSomeCoffee\TheTV...iesRoute::allEpisodes() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

334
                    $response = $this->client->series()->allEpisodes(/** @scrutinizer ignore-type */ $siteId);
Loading history...
335
                } catch (ResourceNotFoundException $error) {
336
                    return false;
337
                } catch (UnauthorizedException $error) {
338
                    try {
339
                        $this->authorizeTvdb();
340
                    } catch (UnauthorizedException $error) {
341
                        cli()->error('Not authorized to access TVDB');
342
                    }
343
                }
344
            } else {
345
                try {
346
                    foreach ($this->client->series()->episodes($siteId) as $episodeBaseRecord) {
0 ignored issues
show
Bug introduced by
It seems like $siteId can also be of type string; however, parameter $id of CanIHaveSomeCoffee\TheTV...SeriesRoute::episodes() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

346
                    foreach ($this->client->series()->episodes(/** @scrutinizer ignore-type */ $siteId) as $episodeBaseRecord) {
Loading history...
347
                        if ($episodeBaseRecord->seasonNumber === $series && $episodeBaseRecord->number === $episode) {
348
                            $response = $episodeBaseRecord;
349
                        }
350
                    }
351
                } catch (ResourceNotFoundException $error) {
352
                    return false;
353
                }
354
            }
355
356
            sleep(1);
357
358
            if (\is_object($response)) {
359
                if ($this->checkRequiredAttr($response, 'tvdbE')) {
360
                    $return = $this->formatEpisodeInfo($response);
361
                }
362
            } elseif ($videoId > 0 && \is_array($response)) {
363
                foreach ($response as $singleEpisode) {
364
                    if ($this->checkRequiredAttr($singleEpisode, 'tvdbE')) {
365
                        $this->addEpisode($videoId, $this->formatEpisodeInfo($singleEpisode));
366
                    }
367
                }
368
            }
369
        }
370
371
        return $return;
372
    }
373
374
    /**
375
     * @throws ExceptionInterface
376
     * @throws ParseException
377
     */
378
    public function formatShowInfo($show): array
379
    {
380
        try {
381
            $poster = $this->client->series()->artworks($show->tvdb_id);
382
            $poster = collect($poster)->where('type', 2)->sortByDesc('score')->first();
0 ignored issues
show
Bug introduced by
$poster of type array|array<mixed,mixed> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

382
            $poster = collect(/** @scrutinizer ignore-type */ $poster)->where('type', 2)->sortByDesc('score')->first();
Loading history...
383
            $this->posterUrl = ! empty($poster->image) ? $poster->image : '';
384
        } catch (ResourceNotFoundException $e) {
385
            cli()->error('Poster image not found on TVDB');
386
        } catch (UnauthorizedException $error) {
387
            try {
388
                $this->authorizeTvdb();
389
            } catch (UnauthorizedException $error) {
390
                cli()->error('Not authorized to access TVDB');
391
            }
392
        }
393
394
        $imdbId = 0;
395
        $imdbIdObj = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $imdbIdObj is dead and can be removed.
Loading history...
396
        try {
397
            $imdbIdObj = $this->client->series()->extended($show->tvdb_id);
398
            preg_match('/tt(?P<imdbid>\d{6,9})$/i', $imdbIdObj->getIMDBId(), $imdb);
0 ignored issues
show
Bug introduced by
It seems like $imdbIdObj->getIMDBId() can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

398
            preg_match('/tt(?P<imdbid>\d{6,9})$/i', /** @scrutinizer ignore-type */ $imdbIdObj->getIMDBId(), $imdb);
Loading history...
399
            $imdbId = $imdb['imdbid'] ?? 0;
400
        } catch (ResourceNotFoundException $e) {
401
            cli()->error('Show ImdbId not found on TVDB');
402
        } catch (\Exception) {
403
            cli()->error('Error on TVDB, aborting');
404
        }
405
406
        // Look up TMDB and Trakt IDs using available external IDs
407
        $externalIds = $this->lookupExternalIds($show->tvdb_id, $imdbId);
408
409
        return [
410
            'type' => parent::TYPE_TV,
411
            'title' => $show->name,
412
            'summary' => $show->overview,
413
            'started' => $show->first_air_time,
414
            'publisher' => $imdbIdObj->originalNetwork->name ?? '',
415
            'poster' => $this->posterUrl,
416
            'source' => parent::SOURCE_TVDB,
417
            'imdb' => $imdbId,
418
            'tvdb' => $show->tvdb_id,
419
            'trakt' => $externalIds['trakt'],
420
            'tvrage' => 0,
421
            'tvmaze' => 0,
422
            'tmdb' => $externalIds['tmdb'],
423
            'aliases' => ! empty($show->aliases) ? $show->aliases : '',
424
            'localzone' => "''",
425
        ];
426
    }
427
428
    /**
429
     * Look up TMDB and Trakt IDs using TVDB ID and IMDB ID.
430
     *
431
     * @param  int  $tvdbId  TVDB show ID
432
     * @param  int|string  $imdbId  IMDB ID (numeric, without 'tt' prefix)
433
     * @return array ['tmdb' => int, 'trakt' => int]
434
     */
435
    protected function lookupExternalIds(int $tvdbId, int|string $imdbId): array
436
    {
437
        $result = ['tmdb' => 0, 'trakt' => 0];
438
439
        try {
440
            // Try to get TMDB ID and other IDs via TMDB's find endpoint
441
            $tmdbClient = app(\App\Services\TmdbClient::class);
442
            if ($tmdbClient->isConfigured() && $tvdbId > 0) {
443
                $tmdbIds = $tmdbClient->lookupTvShowIds($tvdbId, 'tvdb');
444
                if ($tmdbIds !== null) {
445
                    $result['tmdb'] = $tmdbIds['tmdb'] ?? 0;
446
                }
447
            }
448
449
            // Try to get Trakt ID via Trakt's search endpoint
450
            $traktService = app(\App\Services\TraktService::class);
451
            if ($traktService->isConfigured()) {
452
                // Try TVDB ID first
453
                if ($tvdbId > 0) {
454
                    $traktIds = $traktService->lookupShowIds($tvdbId, 'tvdb');
455
                    if ($traktIds !== null && ! empty($traktIds['trakt'])) {
456
                        $result['trakt'] = (int) $traktIds['trakt'];
457
                        // Also get TMDB if we didn't find it above
458
                        if ($result['tmdb'] === 0 && ! empty($traktIds['tmdb'])) {
459
                            $result['tmdb'] = (int) $traktIds['tmdb'];
460
                        }
461
                        return $result;
462
                    }
463
                }
464
465
                // Try IMDB ID as fallback
466
                if (! empty($imdbId) && $imdbId > 0) {
467
                    $traktIds = $traktService->lookupShowIds($imdbId, 'imdb');
468
                    if ($traktIds !== null && ! empty($traktIds['trakt'])) {
469
                        $result['trakt'] = (int) $traktIds['trakt'];
470
                        if ($result['tmdb'] === 0 && ! empty($traktIds['tmdb'])) {
471
                            $result['tmdb'] = (int) $traktIds['tmdb'];
472
                        }
473
                    }
474
                }
475
            }
476
        } catch (\Throwable $e) {
477
            // Silently fail - external ID lookup is optional enrichment
478
        }
479
480
        return $result;
481
    }
482
483
    public function formatEpisodeInfo($episode): array
484
    {
485
        return [
486
            'title' => (string) $episode->name,
487
            'series' => (int) $episode->seasonNumber,
488
            'episode' => (int) $episode->number,
489
            'se_complete' => 'S'.sprintf('%02d', $episode->seasonNumber).'E'.sprintf('%02d', $episode->number),
490
            'firstaired' => $episode->aired,
491
            'summary' => (string) $episode->overview,
492
        ];
493
    }
494
495
    protected function authorizeTvdb(): void
496
    {
497
        $this->token = '';
498
        if (config('tvdb.api_key') === null || config('tvdb.user_pin') === null) {
499
            cli()->warning('TVDB API key or user pin not set. Running in local mode only!', true);
500
            $this->local = true;
501
        } else {
502
            try {
503
                $this->token = $this->client->authentication()->login(config('tvdb.api_key'), config('tvdb.user_pin'));
504
            } catch (UnauthorizedException $error) {
505
                cli()->warning('Could not reach TVDB API. Running in local mode only!', true);
506
                $this->local = true;
507
            }
508
509
            if ($this->token !== '') {
510
                $this->client->setToken($this->token);
511
            }
512
        }
513
    }
514
}
515
516