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

TvMazePipe::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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