Passed
Push — master ( 8b334f...d95312 )
by Darko
11:24
created

TvdbPipe::fetchFanartPoster()   A

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

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