TvdbProvider   F
last analyzed

Complexity

Total Complexity 113

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 113
eloc 291
c 2
b 0
f 0
dl 0
loc 498
rs 2

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A authorizeTvdb() 0 16 5
C lookupExternalIds() 0 47 17
C getEpisodeInfo() 0 45 16
A getBanner() 0 3 1
F processSite() 0 182 48
C getShowInfo() 0 51 13
A getPoster() 0 13 3
B formatShowInfo() 0 47 8
A formatEpisodeInfo() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like TvdbProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TvdbProvider, and based on these observations, apply Extract Interface, too.

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
93
                    continue;
94
                }
95
96
                $videoId = $this->getByTitle($release['cleanname'], parent::TYPE_TV);
97
98
                if ($videoId !== 0) {
99
                    $siteId = $this->getSiteByID('tvdb', $videoId);
100
                }
101
102
                $lookupSetting = true;
103
                if ($local === true || $this->local) {
104
                    $lookupSetting = false;
105
                }
106
107
                if ($siteId === false && $lookupSetting) {
108
                    if ($this->echooutput) {
109
                        cli()->primaryOver('    → ');
110
                        cli()->headerOver($this->truncateTitle($release['cleanname']));
111
                        cli()->primaryOver(' → ');
112
                        cli()->info('Searching TVDB...');
113
                    }
114
115
                    $country = (
116
                        isset($release['country']) && \strlen($release['country']) === 2
117
                            ? (string) $release['country']
118
                            : ''
119
                    );
120
121
                    $tvdbShow = $this->getShowInfo((string) $release['cleanname']);
122
123
                    if (\is_array($tvdbShow)) {
124
                        $tvdbShow['country'] = $country;
125
                        $videoId = $this->add($tvdbShow);
126
                        $siteId = (int) $tvdbShow['tvdb'];
127
                    }
128
                } elseif ($this->echooutput && $siteId !== false) {
129
                    cli()->primaryOver('    → ');
130
                    cli()->headerOver($this->truncateTitle($release['cleanname']));
131
                    cli()->primaryOver(' → ');
132
                    cli()->info('Found in DB');
133
                }
134
135
                if ((int) $videoId > 0 && (int) $siteId > 0) {
136
                    if (! empty($tvdbShow['poster'])) {
137
                        $this->getPoster($videoId);
138
                    } elseif ($this->fanart->isConfigured()) {
139
                        $posterUrl = $this->fanart->getBestTvPoster($siteId);
140
                        if (! empty($posterUrl)) {
141
                            $this->posterUrl = $posterUrl;
142
                            $this->getPoster($videoId);
143
                        }
144
                    }
145
146
                    $seriesNo = (! empty($release['season']) ? preg_replace('/^S0*/i', '', $release['season']) : '');
147
                    $episodeNo = (! empty($release['episode']) ? preg_replace('/^E0*/i', '', $release['episode']) : '');
148
                    $hasAirdate = ! empty($release['airdate']);
149
150
                    if ($episodeNo === 'all') {
151
                        $this->setVideoIdFound($videoId, $row['id'], 0);
152
                        if ($this->echooutput) {
153
                            cli()->primaryOver('    → ');
154
                            cli()->headerOver($this->truncateTitle($release['cleanname']));
155
                            cli()->primaryOver(' → ');
156
                            cli()->primary('Full Season matched');
157
                        }
158
                        $matched++;
159
160
                        continue;
161
                    }
162
163
                    if (! $this->countEpsByVideoID($videoId)) {
164
                        $this->getEpisodeInfo($siteId, -1, -1, $videoId);
165
                    }
166
167
                    $episode = $this->getBySeasonEp($videoId, $seriesNo, $episodeNo, $release['airdate']);
168
169
                    if ($episode === false && $lookupSetting) {
170
                        if ($seriesNo !== '' && $episodeNo !== '') {
171
                            $tvdbEpisode = $this->getEpisodeInfo($siteId, (int) $seriesNo, (int) $episodeNo, $videoId);
172
                            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...
173
                                $episode = $this->addEpisode($videoId, $tvdbEpisode);
174
                            }
175
                        }
176
177
                        if ($episode === false && $hasAirdate) {
178
                            $this->getEpisodeInfo($siteId, -1, -1, $videoId);
179
                            $episode = $this->getBySeasonEp($videoId, 0, 0, $release['airdate']);
180
                        }
181
                    }
182
183
                    if ($episode !== false && is_numeric($episode) && $episode > 0) {
184
                        $this->setVideoIdFound($videoId, $row['id'], $episode);
185
                        if ($this->echooutput) {
186
                            cli()->primaryOver('    → ');
187
                            cli()->headerOver($this->truncateTitle($release['cleanname']));
188
                            if ($seriesNo !== '' && $episodeNo !== '') {
189
                                cli()->primaryOver(' S');
190
                                cli()->warningOver(sprintf('%02d', $seriesNo));
191
                                cli()->primaryOver('E');
192
                                cli()->warningOver(sprintf('%02d', $episodeNo));
193
                            } elseif ($hasAirdate) {
194
                                cli()->primaryOver(' | ');
195
                                cli()->warningOver($release['airdate']);
196
                            }
197
                            cli()->primaryOver(' ✓ ');
198
                            cli()->primary('MATCHED (TVDB)');
199
                        }
200
                        $matched++;
201
                    } else {
202
                        $this->setVideoIdFound($videoId, $row['id'], 0);
203
                        $this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']);
204
                        if ($this->echooutput) {
205
                            cli()->primaryOver('    → ');
206
                            cli()->alternateOver($this->truncateTitle($release['cleanname']));
207
                            if ($hasAirdate) {
208
                                cli()->primaryOver(' | ');
209
                                cli()->warningOver($release['airdate']);
210
                            }
211
                            cli()->primaryOver(' → ');
212
                            cli()->warning('Episode not found');
213
                        }
214
                    }
215
                } else {
216
                    $this->setVideoNotFound(parent::PROCESS_TVMAZE, $row['id']);
217
                    $this->titleCache[] = $release['cleanname'] ?? null;
218
                    if ($this->echooutput) {
219
                        cli()->primaryOver('    → ');
220
                        cli()->alternateOver($this->truncateTitle($release['cleanname']));
221
                        cli()->primaryOver(' → ');
222
                        cli()->warning('Not found');
223
                    }
224
                }
225
            } else {
226
                $this->setVideoNotFound(parent::FAILED_PARSE, $row['id']);
227
                $this->titleCache[] = $release['cleanname'] ?? null;
228
                if ($this->echooutput) {
229
                    cli()->error(sprintf(
230
                        '  ✗ [%d/%d] Parse failed: %s',
231
                        $processed,
232
                        $tvCount,
233
                        mb_substr($row['searchname'], 0, 50)
234
                    ));
235
                }
236
            }
237
        }
238
239
        if ($this->echooutput && $matched > 0) {
240
            echo "\n";
241
            cli()->primaryOver('  ✓ TVDB: ');
242
            cli()->primary(sprintf('%d matched, %d skipped', $matched, $skipped));
243
        }
244
    }
245
246
    public function getBanner($videoID, $siteId): bool
247
    {
248
        return false;
249
    }
250
251
    /**
252
     * @throws UnauthorizedException
253
     * @throws ParseException
254
     * @throws ExceptionInterface
255
     */
256
    public function getShowInfo(string $name): bool|array
257
    {
258
        $return = $response = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
259
        $highestMatch = 0;
260
        try {
261
            $response = $this->client->search()->search($name, ['type' => 'series']);
262
        } catch (ResourceNotFoundException $e) {
263
            $response = false;
264
            cli()->error('Show not found on TVDB');
265
        } catch (UnauthorizedException $e) {
266
            try {
267
                $this->authorizeTvdb();
268
            } catch (UnauthorizedException $error) {
269
                cli()->error('Not authorized to access TVDB');
270
            }
271
        }
272
273
        sleep(1);
274
275
        if (\is_array($response)) {
276
            foreach ($response as $show) {
277
                if ($this->checkRequiredAttr($show, 'tvdbS')) {
278
                    if (strtolower($show->name) === strtolower($name)) {
279
                        $highest = $show;
280
                        break;
281
                    }
282
283
                    $matchPercent = $this->checkMatch(strtolower($show->name), strtolower($name), self::MATCH_PROBABILITY);
284
285
                    if ($matchPercent > $highestMatch) {
286
                        $highestMatch = $matchPercent;
287
                        $highest = $show;
288
                    }
289
290
                    if (! empty($show->aliases)) {
291
                        foreach ($show->aliases as $akaIndex => $akaName) {
292
                            $aliasPercent = $this->checkMatch(strtolower($akaName), strtolower($name), self::MATCH_PROBABILITY);
293
                            if ($aliasPercent > $highestMatch) {
294
                                $highestMatch = $aliasPercent;
295
                                $highest = $show;
296
                            }
297
                        }
298
                    }
299
                }
300
            }
301
            if (! empty($highest)) {
302
                $return = $this->formatShowInfo($highest);
303
            }
304
        }
305
306
        return $return;
307
    }
308
309
    public function getPoster(int $videoId): int
310
    {
311
        $ri = new ReleaseImageService;
312
        $hasCover = 0;
313
314
        if (! empty($this->posterUrl)) {
315
            $hasCover = $ri->saveImage($videoId, $this->posterUrl, $this->imgSavePath);
316
            if ($hasCover === 1) {
317
                $this->setCoverFound($videoId);
318
            }
319
        }
320
321
        return $hasCover;
322
    }
323
324
    /**
325
     * @throws ParseException
326
     * @throws UnauthorizedException
327
     * @throws ExceptionInterface
328
     */
329
    public function getEpisodeInfo(int|string $siteId, int|string $series, int|string $episode, int $videoId = 0): bool|array
330
    {
331
        $return = $response = false;
332
333
        if (! $this->local) {
334
            if ($videoId > 0) {
335
                try {
336
                    $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

336
                    $response = $this->client->series()->allEpisodes(/** @scrutinizer ignore-type */ $siteId);
Loading history...
337
                } catch (ResourceNotFoundException $error) {
338
                    return false;
339
                } catch (UnauthorizedException $error) {
340
                    try {
341
                        $this->authorizeTvdb();
342
                    } catch (UnauthorizedException $error) {
343
                        cli()->error('Not authorized to access TVDB');
344
                    }
345
                }
346
            } else {
347
                try {
348
                    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

348
                    foreach ($this->client->series()->episodes(/** @scrutinizer ignore-type */ $siteId) as $episodeBaseRecord) {
Loading history...
349
                        if ($episodeBaseRecord->seasonNumber === $series && $episodeBaseRecord->number === $episode) {
350
                            $response = $episodeBaseRecord;
351
                        }
352
                    }
353
                } catch (ResourceNotFoundException $error) {
354
                    return false;
355
                }
356
            }
357
358
            sleep(1);
359
360
            if (\is_object($response)) {
361
                if ($this->checkRequiredAttr($response, 'tvdbE')) {
362
                    $return = $this->formatEpisodeInfo($response);
363
                }
364
            } elseif ($videoId > 0 && \is_array($response)) {
365
                foreach ($response as $singleEpisode) {
366
                    if ($this->checkRequiredAttr($singleEpisode, 'tvdbE')) {
367
                        $this->addEpisode($videoId, $this->formatEpisodeInfo($singleEpisode));
368
                    }
369
                }
370
            }
371
        }
372
373
        return $return;
374
    }
375
376
    /**
377
     * @throws ExceptionInterface
378
     * @throws ParseException
379
     */
380
    public function formatShowInfo($show): array
381
    {
382
        try {
383
            $poster = $this->client->series()->artworks($show->tvdb_id);
384
            $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

384
            $poster = collect(/** @scrutinizer ignore-type */ $poster)->where('type', 2)->sortByDesc('score')->first();
Loading history...
385
            $this->posterUrl = ! empty($poster->image) ? $poster->image : '';
386
        } catch (ResourceNotFoundException $e) {
387
            cli()->error('Poster image not found on TVDB');
388
        } catch (UnauthorizedException $error) {
389
            try {
390
                $this->authorizeTvdb();
391
            } catch (UnauthorizedException $error) {
392
                cli()->error('Not authorized to access TVDB');
393
            }
394
        }
395
396
        $imdbId = 0;
397
        $imdbIdObj = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $imdbIdObj is dead and can be removed.
Loading history...
398
        try {
399
            $imdbIdObj = $this->client->series()->extended($show->tvdb_id);
400
            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

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