Passed
Push — master ( 14af2f...574fad )
by Darko
10:47
created

TmdbProvider::lookupTraktId()   C

Complexity

Conditions 13
Paths 39

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 37
rs 6.6166
c 0
b 0
f 0
cc 13
nc 39
nop 3

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\ReleaseImageService;
6
use App\Services\TmdbClient;
7
8
class TmdbProvider extends AbstractTvProvider
9
{
10
    protected const MATCH_PROBABILITY = 75;
11
12
    /**
13
     * @string URL for show poster art
14
     */
15
    public string $posterUrl = '';
16
17
    /**
18
     * Custom TMDB API client
19
     */
20
    protected TmdbClient $tmdbClient;
21
22
    /**
23
     * Fetch banner from site.
24
     */
25
    public function getBanner($videoId, $siteId): bool
26
    {
27
        return false;
28
    }
29
30
    /**
31
     * Main processing director function for TMDB
32
     * Calls work query function and initiates processing.
33
     */
34
    public function processSite($groupID, $guidChar, $process, bool $local = false): void
35
    {
36
        $res = $this->getTvReleases($groupID, $guidChar, $process, parent::PROCESS_TMDB);
37
38
        $tvcount = \count($res);
39
        $lookupSetting = true;
40
41
        if ($tvcount === 0) {
42
43
            return;
44
        }
45
46
        if ($res instanceof \Traversable) {
0 ignored issues
show
introduced by
$res is always a sub-type of Traversable.
Loading history...
47
            $this->titleCache = [];
48
            $processed = 0;
49
            $matched = 0;
50
            $skipped = 0;
51
52
            foreach ($res as $row) {
53
                $processed++;
54
                $siteId = false;
55
                $this->posterUrl = '';
56
57
                // Clean the show name for better match probability
58
                $release = $this->parseInfo($row['searchname']);
59
60
                if (is_array($release) && $release['name'] !== '') {
61
                    if (in_array($release['cleanname'], $this->titleCache, false)) {
62
                        if ($this->echooutput) {
63
                            cli()->primaryOver('    → ');
64
                            cli()->alternateOver($this->truncateTitle($release['cleanname']));
65
                            cli()->primaryOver(' → ');
66
                            cli()->alternate('Skipped (previously failed)');
67
                        }
68
                        $this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']);
69
                        $skipped++;
70
71
                        continue;
72
                    }
73
74
                    // Find the Video ID if it already exists by checking the title against stored TMDB titles
75
                    $videoId = $this->getByTitle($release['cleanname'], parent::TYPE_TV, parent::SOURCE_TMDB);
76
77
                    // Force local lookup only
78
                    if ($local === true) {
79
                        $lookupSetting = false;
80
                    }
81
82
                    // If lookups are allowed lets try to get it.
83
                    if ($videoId === 0 && $lookupSetting) {
84
                        if ($this->echooutput) {
85
                            cli()->primaryOver('    → ');
86
                            cli()->headerOver($this->truncateTitle($release['cleanname']));
87
                            cli()->primaryOver(' → ');
88
                            cli()->info('Searching TMDB...');
89
                        }
90
91
                        // Get the show from TMDB
92
                        $tmdbShow = $this->getShowInfo((string) $release['cleanname']);
93
94
                        if (is_array($tmdbShow)) {
95
                            // Check if we have the TMDB ID already, if we do use that Video ID
96
                            $dupeCheck = $this->getVideoIDFromSiteID('tvdb', $tmdbShow['tvdb']);
97
                            if ($dupeCheck === false) {
98
                                $videoId = $this->add($tmdbShow);
99
                                $siteId = $tmdbShow['tmdb'];
100
                            } else {
101
                                $videoId = $dupeCheck;
102
                                // Update any missing fields and add site IDs
103
                                $this->update($videoId, $tmdbShow);
0 ignored issues
show
Bug introduced by
It seems like $videoId can also be of type true; however, parameter $videoId of App\Services\TvProcessin...actTvProvider::update() 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

103
                                $this->update(/** @scrutinizer ignore-type */ $videoId, $tmdbShow);
Loading history...
104
                                $siteId = $this->getSiteIDFromVideoID('tmdb', $videoId);
0 ignored issues
show
Bug introduced by
It seems like $videoId can also be of type true; however, parameter $videoID of App\Services\TvProcessin...:getSiteIDFromVideoID() 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

104
                                $siteId = $this->getSiteIDFromVideoID('tmdb', /** @scrutinizer ignore-type */ $videoId);
Loading history...
105
                            }
106
                        }
107
                    } else {
108
                        if ($this->echooutput && $videoId > 0) {
109
                            cli()->primaryOver('    → ');
110
                            cli()->headerOver($this->truncateTitle($release['cleanname']));
111
                            cli()->primaryOver(' → ');
112
                            cli()->info('Found in DB');
113
                        }
114
                        $siteId = $this->getSiteIDFromVideoID('tmdb', $videoId);
115
                    }
116
117
                    if (is_numeric($videoId) && $videoId > 0 && is_numeric($siteId) && $siteId > 0) {
118
                        // Now that we have valid video and tmdb ids, try to get the poster
119
                        $this->getPoster($videoId);
120
121
                        $seriesNo = preg_replace('/^S0*/i', '', $release['season']);
122
                        $episodeNo = preg_replace('/^E0*/i', '', $release['episode']);
123
                        $hasAirdate = ! empty($release['airdate']);
124
125
                        if ($episodeNo === 'all') {
126
                            // Set the video ID and leave episode 0
127
                            $this->setVideoIdFound($videoId, $row['id'], 0);
128
                            if ($this->echooutput) {
129
                                cli()->primaryOver('    → ');
130
                                cli()->headerOver($this->truncateTitle($release['cleanname']));
131
                                cli()->primaryOver(' → ');
132
                                cli()->primary('Full Season matched');
133
                            }
134
                            $matched++;
135
136
                            continue;
137
                        }
138
139
                        // Download all episodes if new show to reduce API usage
140
                        if ($this->countEpsByVideoID($videoId) === false) {
141
                            $this->getEpisodeInfo($siteId, -1, -1, '', $videoId);
142
                        }
143
144
                        // Check if we have the episode for this video ID
145
                        $episode = $this->getBySeasonEp($videoId, $seriesNo, $episodeNo, $release['airdate']);
146
147
                        if ($episode === false) {
148
                            if ($seriesNo !== '' && $episodeNo !== '') {
149
                                // Send the request for the episode to TMDB with fallback to other IDs
150
                                $tmdbEpisode = $this->getEpisodeInfo(
151
                                    $siteId,
152
                                    $seriesNo,
153
                                    $episodeNo,
154
                                    $release['airdate'],
155
                                    $videoId
156
                                );
157
158
                                if ($tmdbEpisode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tmdbEpisode of type array<string,integer|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...
159
                                    $episode = $this->addEpisode($videoId, $tmdbEpisode);
160
                                }
161
                            }
162
163
                            if ($episode === false && $hasAirdate) {
164
                                // Refresh episode cache and attempt airdate match
165
                                $this->getEpisodeInfo($siteId, -1, -1, '', $videoId);
166
                                $episode = $this->getBySeasonEp($videoId, 0, 0, $release['airdate']);
167
                            }
168
                        }
169
170
                        if ($episode !== false && is_numeric($episode) && $episode > 0) {
171
                            // Mark the releases video and episode IDs
172
                            $this->setVideoIdFound($videoId, $row['id'], $episode);
173
                            if ($this->echooutput) {
174
                                cli()->primaryOver('    → ');
175
                                cli()->headerOver($this->truncateTitle($release['cleanname']));
176
                                if ($seriesNo !== '' && $episodeNo !== '') {
177
                                    cli()->primaryOver(' S');
178
                                    cli()->warningOver(sprintf('%02d', $seriesNo));
179
                                    cli()->primaryOver('E');
180
                                    cli()->warningOver(sprintf('%02d', $episodeNo));
181
                                } elseif ($hasAirdate) {
182
                                    cli()->primaryOver(' | ');
183
                                    cli()->warningOver($release['airdate']);
184
                                }
185
                                cli()->primaryOver(' ✓ ');
186
                                cli()->primary('MATCHED (TMDB)');
187
                            }
188
                            $matched++;
189
                        } else {
190
                            // Processing failed, set the episode ID to the next processing group
191
                            $this->setVideoIdFound($videoId, $row['id'], 0);
192
                            $this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']);
193
                            if ($this->echooutput) {
194
                                cli()->primaryOver('    → ');
195
                                cli()->alternateOver($this->truncateTitle($release['cleanname']));
196
                                if ($hasAirdate) {
197
                                    cli()->primaryOver(' | ');
198
                                    cli()->warningOver($release['airdate']);
199
                                }
200
                                cli()->primaryOver(' → ');
201
                                cli()->warning('Episode not found');
202
                            }
203
                        }
204
                    } else {
205
                        // Processing failed, set the episode ID to the next processing group
206
                        $this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']);
207
                        $this->titleCache[] = $release['cleanname'] ?? null;
208
                        if ($this->echooutput) {
209
                            cli()->primaryOver('    → ');
210
                            cli()->alternateOver($this->truncateTitle($release['cleanname']));
211
                            cli()->primaryOver(' → ');
212
                            cli()->warning('Not found');
213
                        }
214
                    }
215
                } else {
216
                    // Processing failed, set the episode ID to the next processing group
217
                    $this->setVideoNotFound(parent::PROCESS_TRAKT, $row['id']);
218
                    $this->titleCache[] = $release['cleanname'] ?? null;
219
                    if ($this->echooutput) {
220
                        cli()->primaryOver('    → ');
221
                        cli()->alternateOver(mb_substr($row['searchname'], 0, 50));
222
                        cli()->primaryOver(' → ');
223
                        cli()->error('Parse failed');
224
                    }
225
                }
226
            }
227
228
            // Display summary
229
            if ($this->echooutput && $matched > 0) {
230
                echo "\n";
231
                cli()->primaryOver('  ✓ TMDB: ');
232
                cli()->primary(sprintf('%d matched, %d skipped', $matched, $skipped));
233
            }
234
        }
235
    }
236
237
    /**
238
     * Truncate title for display purposes.
239
     */
240
    protected function truncateTitle(string $title, int $maxLength = 45): string
241
    {
242
        if (mb_strlen($title) <= $maxLength) {
243
            return $title;
244
        }
245
246
        return mb_substr($title, 0, $maxLength - 3).'...';
247
    }
248
249
    /**
250
     * Calls the API to perform initial show name match to TMDB title
251
     * Returns a formatted array of show data or false if no match.
252
     *
253
     * @return array|false
254
     */
255
    public function getShowInfo(string $name): bool|array
256
    {
257
        $return = false;
258
259
        $this->tmdbClient = app(TmdbClient::class);
260
261
        if (! $this->tmdbClient->isConfigured()) {
262
            return false;
263
        }
264
265
        $response = $this->tmdbClient->searchTv($name);
266
267
        sleep(1);
268
269
        if ($response !== null && ! empty($response['results']) && is_array($response['results'])) {
270
            $return = $this->matchShowInfo($response['results'], $name);
271
        }
272
273
        return $return;
274
    }
275
276
    /**
277
     * @return array|false
278
     */
279
    private function matchShowInfo(array $shows, string $cleanName): bool|array
280
    {
281
        $return = false;
282
        $highestMatch = 0;
283
        $highest = null;
284
285
        foreach ($shows as $show) {
286
            if (! is_array($show) || ! $this->checkRequiredAttr($show, 'tmdbS')) {
287
                continue;
288
            }
289
290
            $showName = TmdbClient::getString($show, 'name');
291
            if (empty($showName)) {
292
                continue;
293
            }
294
295
            // Check for exact title match first and then terminate if found
296
            if (strtolower($showName) === strtolower($cleanName)) {
297
                $highest = $show;
298
                break;
299
            }
300
301
            // Check each show title for similarity and then find the highest similar value
302
            $matchPercent = $this->checkMatch(strtolower($showName), strtolower($cleanName), self::MATCH_PROBABILITY);
303
304
            // If new match has a higher percentage, set as new matched title
305
            if ($matchPercent > $highestMatch) {
306
                $highestMatch = $matchPercent;
307
                $highest = $show;
308
            }
309
        }
310
311
        if ($highest !== null && is_array($highest)) {
312
            $showId = TmdbClient::getInt($highest, 'id');
313
            if ($showId === 0) {
314
                return false;
315
            }
316
317
            $showAlternativeTitles = $this->tmdbClient->getTvAlternativeTitles($showId);
318
            $showExternalIds = $this->tmdbClient->getTvExternalIds($showId);
319
320
            if ($showAlternativeTitles === null || $showExternalIds === null) {
321
                return false;
322
            }
323
324
            $alternativeTitles = [];
325
            $results = TmdbClient::getArray($showAlternativeTitles, 'results');
326
            foreach ($results as $aka) {
327
                if (is_array($aka) && isset($aka['title'])) {
328
                    $alternativeTitles[] = $aka['title'];
329
                }
330
            }
331
            $highest['alternative_titles'] = $alternativeTitles;
332
333
            // Use available network info if present
334
            $networks = TmdbClient::getArray($highest, 'networks');
335
            $highest['network'] = ! empty($networks[0]['name']) ? $networks[0]['name'] : '';
336
337
            $highest['external_ids'] = $showExternalIds;
338
339
            $return = $this->formatShowInfo($highest);
340
        }
341
342
        return $return;
343
    }
344
345
    /**
346
     * Retrieves the poster art for the processed show.
347
     *
348
     * @param  int  $videoId  -- the local Video ID
349
     */
350
    public function getPoster(int $videoId): int
351
    {
352
        $ri = new ReleaseImageService;
353
354
        $hascover = 0;
355
356
        // Try to get the Poster
357
        if (! empty($this->posterUrl)) {
358
            $hascover = $ri->saveImage($videoId, $this->posterUrl, $this->imgSavePath);
359
360
            // Mark it retrieved if we saved an image
361
            if ($hascover === 1) {
362
                $this->setCoverFound($videoId);
363
            }
364
        }
365
366
        return $hascover;
367
    }
368
369
    /**
370
     * Get all external IDs for a video from the database.
371
     *
372
     * @param  int  $videoId  The local video ID
373
     * @return array Array of external IDs: ['tmdb' => X, 'tvdb' => Y, 'imdb' => Z]
374
     */
375
    protected function getAllSiteIdsFromVideoID(int $videoId): array
376
    {
377
        $result = \App\Models\Video::query()
378
            ->where('id', $videoId)
379
            ->first(['tmdb', 'tvdb', 'imdb']);
380
381
        if ($result === null) {
382
            return ['tmdb' => 0, 'tvdb' => 0, 'imdb' => 0];
383
        }
384
385
        return [
386
            'tmdb' => (int) ($result->tmdb ?? 0),
387
            'tvdb' => (int) ($result->tvdb ?? 0),
388
            'imdb' => (int) ($result->imdb ?? 0),
389
        ];
390
    }
391
392
    /**
393
     * Gets the specific episode info for the parsed release after match
394
     * Returns a formatted array of episode data or false if no match.
395
     *
396
     * @return array|false
397
     */
398
    public function getEpisodeInfo(int|string $siteId, int|string $series, int|string $episode, string $airdate = '', int $videoId = 0): bool|array
399
    {
400
        $return = false;
401
402
        if (! isset($this->tmdbClient)) {
403
            $this->tmdbClient = app(TmdbClient::class);
404
        }
405
406
        if (! $this->tmdbClient->isConfigured()) {
407
            return false;
408
        }
409
410
        // Bulk fetch all episodes for all seasons and insert
411
        if ($videoId > 0 && (int) $series === -1 && (int) $episode === -1) {
412
            $tvDetails = $this->tmdbClient->getTvShow((int) $siteId);
413
414
            if ($tvDetails === null) {
415
                return false;
416
            }
417
418
            $seasons = TmdbClient::getArray($tvDetails, 'seasons');
419
            if (empty($seasons)) {
420
                return false;
421
            }
422
423
            foreach ($seasons as $seriesInfo) {
424
                if (! is_array($seriesInfo)) {
425
                    continue;
426
                }
427
428
                $seasonNumber = TmdbClient::getInt($seriesInfo, 'season_number');
429
                if ($seasonNumber <= 0) {
430
                    continue;
431
                }
432
433
                $seriesData = $this->tmdbClient->getTvSeason((int) $siteId, $seasonNumber);
434
                sleep(1);
435
436
                if ($seriesData === null) {
437
                    continue;
438
                }
439
440
                $episodes = TmdbClient::getArray($seriesData, 'episodes');
441
                foreach ($episodes as $ep) {
442
                    if (is_array($ep) && $this->checkRequiredAttr($ep, 'tmdbE')) {
443
                        $this->addEpisode($videoId, $this->formatEpisodeInfo($ep));
444
                    }
445
                }
446
            }
447
448
            return false;
449
        }
450
451
        // Single episode lookup - try with fallback to other IDs if we have a videoId
452
        $response = null;
453
        if ($videoId > 0) {
454
            $ids = $this->getAllSiteIdsFromVideoID($videoId);
455
            // Ensure the provided siteId is used as the TMDB ID if available
456
            if ((int) $siteId > 0) {
457
                $ids['tmdb'] = (int) $siteId;
458
            }
459
            $response = $this->tmdbClient->getTvEpisodeWithFallback($ids, (int) $series, (int) $episode);
460
        } else {
461
            // Legacy behavior: just use the provided site ID
462
            $response = $this->tmdbClient->getTvEpisode((int) $siteId, (int) $series, (int) $episode);
463
        }
464
465
        sleep(1);
466
467
        // Handle Single Episode Lookups
468
        if ($response !== null && is_array($response) && $this->checkRequiredAttr($response, 'tmdbE')) {
469
            $return = $this->formatEpisodeInfo($response);
470
        }
471
472
        return $return;
473
    }
474
475
    /**
476
     * Assigns API show response values to a formatted array for insertion
477
     * Returns the formatted array.
478
     */
479
    public function formatShowInfo($show): array
480
    {
481
        if (! is_array($show)) {
482
            return [];
483
        }
484
485
        $posterPath = TmdbClient::getString($show, 'poster_path');
486
        // Prefer a reasonable default size for posters if we only have a path
487
        $this->posterUrl = ! empty($posterPath)
488
            ? 'https://image.tmdb.org/t/p/w500'.$posterPath
489
            : '';
490
491
        $imdbId = 0;
492
        $externalIds = TmdbClient::getArray($show, 'external_ids');
493
        if (! empty($externalIds['imdb_id'])) {
494
            preg_match('/tt(?P<imdbid>\d{6,7})$/i', $externalIds['imdb_id'], $imdb);
495
            $imdbId = $imdb['imdbid'] ?? 0;
496
        }
497
498
        $originCountry = TmdbClient::getArray($show, 'origin_country');
499
        $alternativeTitles = TmdbClient::getArray($show, 'alternative_titles');
500
501
        // Try to get Trakt ID by looking up via TMDB ID
502
        $traktId = 0;
503
        $tmdbId = TmdbClient::getInt($show, 'id');
504
        if ($tmdbId > 0) {
505
            $traktId = $this->lookupTraktId($tmdbId, $imdbId, TmdbClient::getInt($externalIds, 'tvdb_id'));
506
        }
507
508
        return [
509
            'type' => parent::TYPE_TV,
510
            'title' => TmdbClient::getString($show, 'name'),
511
            'summary' => TmdbClient::getString($show, 'overview'),
512
            'started' => TmdbClient::getString($show, 'first_air_date'),
513
            'publisher' => TmdbClient::getString($show, 'network'),
514
            'country' => ! empty($originCountry[0]) ? $originCountry[0] : '',
515
            'source' => parent::SOURCE_TMDB,
516
            'imdb' => $imdbId,
517
            'tvdb' => TmdbClient::getInt($externalIds, 'tvdb_id'),
518
            'trakt' => $traktId,
519
            'tvrage' => TmdbClient::getInt($externalIds, 'tvrage_id'),
520
            'tvmaze' => 0,
521
            'tmdb' => $tmdbId,
522
            'aliases' => ! empty($alternativeTitles) ? $alternativeTitles : '',
523
            'localzone' => "''",
524
        ];
525
    }
526
527
    /**
528
     * Look up Trakt ID using available external IDs.
529
     * Tries TMDB ID first, then IMDB, then TVDB.
530
     *
531
     * @param  int  $tmdbId  TMDB show ID
532
     * @param  int|string  $imdbId  IMDB ID (numeric, without 'tt' prefix)
533
     * @param  int  $tvdbId  TVDB show ID
534
     * @return int Trakt ID or 0 if not found
535
     */
536
    protected function lookupTraktId(int $tmdbId, int|string $imdbId, int $tvdbId): int
537
    {
538
        try {
539
            $traktService = app(\App\Services\TraktService::class);
540
541
            if (! $traktService->isConfigured()) {
542
                return 0;
543
            }
544
545
            // Try TMDB ID first
546
            if ($tmdbId > 0) {
547
                $ids = $traktService->lookupShowIds($tmdbId, 'tmdb');
548
                if ($ids !== null && ! empty($ids['trakt'])) {
549
                    return (int) $ids['trakt'];
550
                }
551
            }
552
553
            // Try IMDB ID
554
            if (! empty($imdbId) && $imdbId > 0) {
555
                $ids = $traktService->lookupShowIds($imdbId, 'imdb');
556
                if ($ids !== null && ! empty($ids['trakt'])) {
557
                    return (int) $ids['trakt'];
558
                }
559
            }
560
561
            // Try TVDB ID
562
            if ($tvdbId > 0) {
563
                $ids = $traktService->lookupShowIds($tvdbId, 'tvdb');
564
                if ($ids !== null && ! empty($ids['trakt'])) {
565
                    return (int) $ids['trakt'];
566
                }
567
            }
568
        } catch (\Throwable $e) {
569
            // Silently fail - Trakt ID lookup is optional enrichment
570
        }
571
572
        return 0;
573
    }
574
575
    /**
576
     * Assigns API episode response values to a formatted array for insertion
577
     * Returns the formatted array.
578
     */
579
    public function formatEpisodeInfo($episode): array
580
    {
581
        if (! is_array($episode)) {
582
            return [];
583
        }
584
585
        $seasonNumber = TmdbClient::getInt($episode, 'season_number');
586
        $episodeNumber = TmdbClient::getInt($episode, 'episode_number');
587
588
        return [
589
            'title' => TmdbClient::getString($episode, 'name'),
590
            'series' => $seasonNumber,
591
            'episode' => $episodeNumber,
592
            'se_complete' => 'S'.sprintf('%02d', $seasonNumber).'E'.sprintf('%02d', $episodeNumber),
593
            'firstaired' => TmdbClient::getString($episode, 'air_date'),
594
            'summary' => TmdbClient::getString($episode, 'overview'),
595
        ];
596
    }
597
}
598