Passed
Push — master ( 717a42...5d19db )
by Darko
11:35 queued 05:45
created

TVDB::formatShowInfo()   B

Complexity

Conditions 9
Paths 112

Size

Total Lines 48
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 37
c 1
b 0
f 0
dl 0
loc 48
rs 7.6924
cc 9
nc 112
nop 1
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: ').
0 ignored issues
show
Bug introduced by
Are you sure $this->colorCli->headerOver('Title: ') of type void can be used in concatenation? ( Ignorable by Annotation )

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

84
                        /** @scrutinizer ignore-type */ $this->colorCli->headerOver('Title: ').
Loading history...
Bug introduced by
Are you sure the usage of $this->colorCli->headerOver('Title: ') targeting Blacklight\ColorCLI::headerOver() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
85
                        $this->colorCli->warningOver($release['cleanname']).
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCli->warning...($release['cleanname']) targeting Blacklight\ColorCLI::warningOver() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
86
                        $this->colorCli->header(' already failed lookup for this site.  Skipping.', true);
0 ignored issues
show
Bug introduced by
Are you sure $this->colorCli->header(...ite. Skipping.', true) of type void can be used in concatenation? ( Ignorable by Annotation )

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

86
                        /** @scrutinizer ignore-type */ $this->colorCli->header(' already failed lookup for this site.  Skipping.', true);
Loading history...
Bug introduced by
Are you sure the usage of $this->colorCli->header(...ite. Skipping.', true) targeting Blacklight\ColorCLI::header() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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) {
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 && 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;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $tvDbId 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

304
                    $response = $this->client->series()->allEpisodes(/** @scrutinizer ignore-type */ $tvDbId);
Loading history...
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) {
0 ignored issues
show
Bug introduced by
It seems like $tvDbId 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

317
                    foreach ($this->client->series()->episodes(/** @scrutinizer ignore-type */ $tvDbId) as $episodeBaseRecord) {
Loading history...
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();
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

358
            $poster = collect(/** @scrutinizer ignore-type */ $poster)->where('type', 2)->sortByDesc('score')->first();
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $imdbId->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

373
            preg_match('/tt(?P<imdbid>\d{6,9})$/i', /** @scrutinizer ignore-type */ $imdbId->getIMDBId(), $imdb);
Loading history...
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