TvdbPipe::fetchFanartPoster()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 14
rs 10
cc 4
nc 6
nop 2
1
<?php
2
3
namespace App\Services\TvProcessing\Pipes;
4
5
use App\Services\FanartTvService;
6
use App\Services\TvProcessing\Providers\TvdbProvider;
7
use App\Services\TvProcessing\TvProcessingPassable;
8
use App\Services\TvProcessing\TvProcessingResult;
9
10
/**
11
 * Pipe for TVDB API lookups.
12
 */
13
class TvdbPipe extends AbstractTvProviderPipe
14
{
15
    // Video type constants (matching Videos class protected constants)
16
    private const TYPE_TV = 0;
17
18
    protected int $priority = 20;
19
20
    private ?TvdbProvider $tvdb = null;
21
22
    private ?FanartTvService $fanart = null;
23
24
    public function getName(): string
25
    {
26
        return 'TVDB';
27
    }
28
29
    public function getStatusCode(): int
30
    {
31
        return 0; // PROCESS_TVDB
32
    }
33
34
    /**
35
     * Get or create the TVDB instance.
36
     */
37
    private function getTvdb(): TvdbProvider
38
    {
39
        if ($this->tvdb === null) {
40
            $this->tvdb = new TvdbProvider;
41
        }
42
43
        return $this->tvdb;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->tvdb could return the type null which is incompatible with the type-hinted return App\Services\TvProcessing\Providers\TvdbProvider. Consider adding an additional type-check to rule them out.
Loading history...
44
    }
45
46
    protected function process(TvProcessingPassable $passable): TvProcessingResult
47
    {
48
        $parsedInfo = $passable->getParsedInfo();
49
        $context = $passable->context;
50
51
        if ($parsedInfo === null || empty($parsedInfo['cleanname'])) {
52
            return TvProcessingResult::notFound($this->getName());
53
        }
54
55
        $cleanName = $parsedInfo['cleanname'];
56
57
        // Check if we've already failed this title
58
        if ($this->isInTitleCache($cleanName)) {
59
            $this->outputSkipped($cleanName);
60
61
            return TvProcessingResult::skipped('previously failed', $this->getName());
62
        }
63
64
        $tvdb = $this->getTvdb();
65
        $siteId = false;
66
        $posterUrl = '';
67
68
        // Find the Video ID if it already exists by checking the title
69
        $videoId = $tvdb->getByTitle($cleanName, self::TYPE_TV);
70
71
        // If not found and cleanName contains a year in parentheses, try without the year
72
        if ($videoId === 0 && preg_match('/^(.+?)\s*\(\d{4}\)$/', $cleanName, $yearMatch)) {
73
            $nameWithoutYear = trim($yearMatch[1]);
74
            $videoId = $tvdb->getByTitle($nameWithoutYear, self::TYPE_TV);
75
        }
76
77
        if ($videoId !== 0) {
78
            $siteId = $tvdb->getSiteByID('tvdb', $videoId);
79
            // If show exists in local DB but doesn't have a TVDB ID, use the existing video
80
            // and process episode matching without trying to search TVDB API
81
            if ($siteId === false || $siteId === 0) {
82
                // Show exists in our DB (likely from another source like TMDB)
83
                // Skip TVDB API search and proceed to episode matching
84
                $this->outputFoundInDb($cleanName);
85
86
                return $this->processEpisodeForExistingVideo($passable, $tvdb, $videoId, $parsedInfo);
87
            }
88
        }
89
90
        // Check if we have a valid country
91
        $country = (
92
            isset($parsedInfo['country']) && strlen($parsedInfo['country']) === 2
93
                ? (string) $parsedInfo['country']
94
                : ''
95
        );
96
97
        if ($siteId === false || $siteId === 0) {
98
            // Not in local DB, search TVDB
99
            $this->outputSearching($cleanName);
100
101
            $tvdbShow = $tvdb->getShowInfo((string) $cleanName);
102
103
            // If not found and cleanName contains a year in parentheses, try without the year
104
            if ($tvdbShow === false && preg_match('/^(.+?)\s*\(\d{4}\)$/', $cleanName, $yearMatch)) {
0 ignored issues
show
introduced by
The condition $tvdbShow === false is always false.
Loading history...
105
                $nameWithoutYear = trim($yearMatch[1]);
106
                $tvdbShow = $tvdb->getShowInfo($nameWithoutYear);
107
            }
108
109
            if (is_array($tvdbShow)) {
0 ignored issues
show
introduced by
The condition is_array($tvdbShow) is always true.
Loading history...
110
                $tvdbShow['country'] = $country;
111
                $videoId = $tvdb->add($tvdbShow);
112
                $siteId = (int) $tvdbShow['tvdb'];
113
                $posterUrl = $tvdbShow['poster'] ?? '';
114
            }
115
        } else {
116
            $this->outputFoundInDb($cleanName);
117
        }
118
119
        if ((int) $videoId === 0 || (int) $siteId === 0) {
120
            // Show not found
121
            $this->addToTitleCache($cleanName);
122
            $this->outputNotFound($cleanName);
123
124
            return TvProcessingResult::notFound($this->getName(), ['title' => $cleanName]);
125
        }
126
127
        // Fetch poster if available
128
        if (! empty($posterUrl)) {
129
            $tvdb->getPoster($videoId);
130
        } else {
131
            $this->fetchFanartPoster($videoId, $siteId);
132
        }
133
134
        // Process episode
135
        $seriesNo = ! empty($parsedInfo['season']) ? preg_replace('/^S0*/i', '', (string) $parsedInfo['season']) : '';
136
        $episodeNo = ! empty($parsedInfo['episode']) ? preg_replace('/^E0*/i', '', (string) $parsedInfo['episode']) : '';
137
        $hasAirdate = ! empty($parsedInfo['airdate']);
138
139
        if ($episodeNo === 'all') {
140
            // Full season release
141
            $tvdb->setVideoIdFound($videoId, $context->releaseId, 0);
142
            $this->outputFullSeason($cleanName);
143
144
            return TvProcessingResult::matched($videoId, 0, $this->getName(), ['full_season' => true]);
145
        }
146
147
        // Download all episodes if new show to reduce API/bandwidth usage
148
        if (! $tvdb->countEpsByVideoID($videoId)) {
149
            $tvdb->getEpisodeInfo($siteId, -1, -1, $videoId);
150
        }
151
152
        // Check if we have the episode for this video ID
153
        $episode = $tvdb->getBySeasonEp($videoId, $seriesNo, $episodeNo, $parsedInfo['airdate'] ?? '');
154
155
        if ($episode === false) {
156
            if ($seriesNo !== '' && $episodeNo !== '') {
157
                // Try to get episode from TVDB
158
                $tvdbEpisode = $tvdb->getEpisodeInfo($siteId, (int) $seriesNo, (int) $episodeNo, $videoId);
159
160
                if ($tvdbEpisode) {
0 ignored issues
show
introduced by
$tvdbEpisode is a non-empty array, thus is always true.
Loading history...
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...
161
                    $episode = $tvdb->addEpisode($videoId, $tvdbEpisode);
162
                }
163
            }
164
165
            if ($episode === false && $hasAirdate) {
166
                // Refresh episode cache and attempt airdate match
167
                $tvdb->getEpisodeInfo($siteId, -1, -1, $videoId);
168
                $episode = $tvdb->getBySeasonEp($videoId, 0, 0, $parsedInfo['airdate']);
169
            }
170
        }
171
172
        if ($episode !== false && is_numeric($episode) && $episode > 0) {
173
            // Success!
174
            $tvdb->setVideoIdFound($videoId, $context->releaseId, $episode);
175
            $this->outputMatch(
176
                $cleanName,
177
                $seriesNo !== '' ? (int) $seriesNo : null,
178
                $episodeNo !== '' ? (int) $episodeNo : null,
179
                $hasAirdate ? $parsedInfo['airdate'] : null
180
            );
181
182
            return TvProcessingResult::matched($videoId, (int) $episode, $this->getName());
183
        }
184
185
        // Episode not found
186
        $tvdb->setVideoIdFound($videoId, $context->releaseId, 0);
187
188
        if ($this->echoOutput) {
189
            cli()->primaryOver('    → ');
190
            cli()->alternateOver($this->truncateTitle($cleanName));
191
            if ($hasAirdate) {
192
                cli()->primaryOver(' | ');
193
                cli()->warningOver($parsedInfo['airdate']);
194
            }
195
            cli()->primaryOver(' → ');
196
            cli()->warning('Episode not found');
197
        }
198
199
        return TvProcessingResult::notFound($this->getName(), [
200
            'video_id' => $videoId,
201
            'episode_not_found' => true,
202
        ]);
203
    }
204
205
    /**
206
     * Fetch poster from Fanart.tv.
207
     */
208
    private function fetchFanartPoster(int $videoId, int $siteId): void
209
    {
210
        if ($this->fanart === null) {
211
            $this->fanart = new FanartTvService;
212
        }
213
214
        if (! $this->fanart->isConfigured()) {
0 ignored issues
show
Bug introduced by
The method isConfigured() does not exist on null. ( Ignorable by Annotation )

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

214
        if (! $this->fanart->/** @scrutinizer ignore-call */ isConfigured()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
215
            return;
216
        }
217
218
        $posterUrl = $this->fanart->getBestTvPoster($siteId);
219
        if (! empty($posterUrl)) {
220
            $this->getTvdb()->posterUrl = $posterUrl;
221
            $this->getTvdb()->getPoster($videoId);
222
        }
223
    }
224
225
    /**
226
     * Output full season match message.
227
     */
228
    private function outputFullSeason(string $title): void
229
    {
230
        if (! $this->echoOutput) {
231
            return;
232
        }
233
234
        cli()->primaryOver('    → ');
235
        cli()->headerOver($this->truncateTitle($title));
236
        cli()->primaryOver(' → ');
237
        cli()->primary('Full Season matched');
238
    }
239
240
    /**
241
     * Process episode matching for a video that already exists in local DB.
242
     * This is used when the show was added from another source (e.g., TMDB) and doesn't have a TVDB ID.
243
     */
244
    private function processEpisodeForExistingVideo(
245
        TvProcessingPassable $passable,
246
        TvdbProvider $tvdb,
247
        int $videoId,
248
        array $parsedInfo
249
    ): TvProcessingResult {
250
        $context = $passable->context;
251
        $cleanName = $parsedInfo['cleanname'];
252
253
        $seriesNo = ! empty($parsedInfo['season']) ? preg_replace('/^S0*/i', '', (string) $parsedInfo['season']) : '';
254
        $episodeNo = ! empty($parsedInfo['episode']) ? preg_replace('/^E0*/i', '', (string) $parsedInfo['episode']) : '';
255
        $hasAirdate = ! empty($parsedInfo['airdate']);
256
257
        if ($episodeNo === 'all') {
258
            // Full season release
259
            $tvdb->setVideoIdFound($videoId, $context->releaseId, 0);
260
            $this->outputFullSeason($cleanName);
261
262
            return TvProcessingResult::matched($videoId, 0, $this->getName(), ['full_season' => true]);
263
        }
264
265
        // Try to find episode in local DB
266
        $episode = $tvdb->getBySeasonEp($videoId, $seriesNo, $episodeNo, $parsedInfo['airdate'] ?? '');
267
268
        if ($episode !== false && is_numeric($episode) && $episode > 0) {
269
            $tvdb->setVideoIdFound($videoId, $context->releaseId, $episode);
270
            $this->outputMatch(
271
                $cleanName,
272
                $seriesNo !== '' ? (int) $seriesNo : null,
273
                $episodeNo !== '' ? (int) $episodeNo : null,
274
                $hasAirdate ? $parsedInfo['airdate'] : null
275
            );
276
277
            return TvProcessingResult::matched($videoId, (int) $episode, $this->getName());
278
        }
279
280
        // Episode not found in local DB - mark video but episode not matched
281
        $tvdb->setVideoIdFound($videoId, $context->releaseId, 0);
282
283
        if ($this->echoOutput) {
284
            cli()->primaryOver('    → ');
285
            cli()->alternateOver($this->truncateTitle($cleanName));
286
            if ($hasAirdate) {
287
                cli()->primaryOver(' | ');
288
                cli()->warningOver($parsedInfo['airdate']);
289
            }
290
            cli()->primaryOver(' → ');
291
            cli()->warning('Episode not in local DB');
292
        }
293
294
        return TvProcessingResult::notFound($this->getName(), [
295
            'video_id' => $videoId,
296
            'episode_not_found' => true,
297
        ]);
298
    }
299
}
300