Issues (867)

app/Services/TvProcessing/TvProcessingPipeline.php (3 issues)

1
<?php
2
3
namespace App\Services\TvProcessing;
4
5
use App\Models\Category;
6
use App\Models\Release;
7
use App\Models\Settings;
8
use App\Services\TvProcessing\Pipes\AbstractTvProviderPipe;
9
use App\Services\TvProcessing\Pipes\LocalDbPipe;
10
use App\Services\TvProcessing\Pipes\ParseInfoPipe;
11
use App\Services\TvProcessing\Pipes\TmdbPipe;
12
use App\Services\TvProcessing\Pipes\TraktPipe;
13
use App\Services\TvProcessing\Pipes\TvdbPipe;
14
use App\Services\TvProcessing\Pipes\TvMazePipe;
15
use Blacklight\ColorCLI;
16
use Illuminate\Pipeline\Pipeline;
17
use Illuminate\Support\Collection;
18
19
/**
20
 * Pipeline-based TV processing service using Laravel Pipeline.
21
 *
22
 * This service uses Laravel's Pipeline to orchestrate multiple TV data providers
23
 * to process TV releases and match them against video metadata from various sources.
24
 */
25
class TvProcessingPipeline
26
{
27
    /**
28
     * @var Collection<AbstractTvProviderPipe>
29
     */
30
    protected Collection $pipes;
31
32
    protected int $tvqty;
33
    protected bool $echoOutput;
34
    protected ColorCLI $colorCli;
35
36
    protected array $stats = [
37
        'processed' => 0,
38
        'matched' => 0,
39
        'failed' => 0,
40
        'skipped' => 0,
41
        'duration' => 0.0,
42
        'providers' => [],
43
    ];
44
45
    /**
46
     * @param iterable<AbstractTvProviderPipe> $pipes
47
     */
48
    public function __construct(iterable $pipes = [], bool $echoOutput = true)
49
    {
50
        $this->pipes = collect($pipes)
0 ignored issues
show
It seems like $pipes can also be of type array; however, parameter $value of collect() does only seem to accept Illuminate\Contracts\Support\Arrayable, 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

50
        $this->pipes = collect(/** @scrutinizer ignore-type */ $pipes)
Loading history...
51
            ->sortBy(fn (AbstractTvProviderPipe $p) => $p->getPriority());
52
53
        $this->tvqty = Settings::settingValue('maxrageprocessed') !== ''
54
            ? (int) Settings::settingValue('maxrageprocessed')
55
            : 75;
56
57
        $this->echoOutput = $echoOutput;
58
        $this->colorCli = new ColorCLI();
59
    }
60
61
    /**
62
     * Add a provider pipe to the pipeline.
63
     */
64
    public function addPipe(AbstractTvProviderPipe $pipe): self
65
    {
66
        $this->pipes->push($pipe);
67
        $this->pipes = $this->pipes->sortBy(fn (AbstractTvProviderPipe $p) => $p->getPriority());
68
69
        return $this;
70
    }
71
72
    /**
73
     * Process a single release through the pipeline.
74
     *
75
     * @param array|object $release Release data
76
     * @param bool $debug Whether to include debug information
77
     * @return array Processing result
78
     */
79
    public function processRelease(array|object $release, bool $debug = false): array
80
    {
81
        $context = TvReleaseContext::fromRelease($release);
82
        $passable = new TvProcessingPassable($context, $debug);
83
84
        // Set echo output on all pipes
85
        foreach ($this->pipes as $pipe) {
86
            $pipe->setEchoOutput($this->echoOutput);
87
        }
88
89
        /** @var TvProcessingPassable $result */
90
        $result = app(Pipeline::class)
91
            ->send($passable)
92
            ->through($this->pipes->values()->all())
93
            ->thenReturn();
94
95
        return $result->toArray();
96
    }
97
98
    /**
99
     * Process all TV releases matching the criteria.
100
     *
101
     * @param string $groupID Group ID to process
102
     * @param string $guidChar GUID character to process
103
     * @param int|string|null $processTV Processing setting (0/1/2 or '' to read from settings)
104
     */
105
    public function process(string $groupID = '', string $guidChar = '', int|string|null $processTV = ''): void
106
    {
107
        $processTV = (int) (is_numeric($processTV) ? $processTV : Settings::settingValue('lookuptv'));
108
        if ($processTV <= 0) {
109
            return;
110
        }
111
112
        $this->resetStats();
113
        $startTime = microtime(true);
114
115
        // Get releases that need processing
116
        $releases = $this->getTvReleases($groupID, $guidChar, $processTV);
117
        $totalCount = count($releases);
118
119
        if ($totalCount === 0) {
120
            if ($this->echoOutput) {
121
                $this->colorCli->header('No TV releases to process.');
122
            }
123
            return;
124
        }
125
126
        if ($this->echoOutput) {
127
            $this->colorCli->header('Processing '.$totalCount.' TV release(s).');
128
        }
129
130
        foreach ($releases as $release) {
131
            $result = $this->processRelease($release);
132
            $this->updateStats($result);
133
        }
134
135
        $this->stats['duration'] = microtime(true) - $startTime;
136
137
        $this->displaySummary();
138
    }
139
140
    /**
141
     * Get TV releases that need processing.
142
     *
143
     * @return Collection
144
     */
145
    protected function getTvReleases(string $groupID, string $guidChar, int $processTV): Collection
146
    {
147
        $qry = Release::query()
148
            ->select([
149
                'id',
150
                'guid',
151
                'leftguid',
152
                'groups_id',
153
                'searchname',
154
                'size',
155
                'categories_id',
156
                'videos_id',
157
                'tv_episodes_id',
158
                'postdate',
159
            ])
160
            ->where(['videos_id' => 0, 'tv_episodes_id' => 0])
161
            ->where('size', '>', 1048576)
162
            ->whereBetween('categories_id', [Category::TV_ROOT, Category::TV_OTHER])
163
            ->where('categories_id', '<>', Category::TV_ANIME)
164
            ->orderByDesc('postdate')
0 ignored issues
show
'postdate' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderByDesc(). ( Ignorable by Annotation )

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

164
            ->orderByDesc(/** @scrutinizer ignore-type */ 'postdate')
Loading history...
165
            ->limit($this->tvqty);
166
167
        if ($groupID !== '') {
168
            $qry->where('groups_id', $groupID);
169
        }
170
171
        if ($guidChar !== '') {
172
            $qry->where('leftguid', $guidChar);
173
        }
174
175
        if ($processTV === 2) {
176
            $qry->where('isrenamed', '=', 1);
177
        }
178
179
        return $qry->get();
180
    }
181
182
    /**
183
     * Get all registered provider pipes.
184
     *
185
     * @return Collection<AbstractTvProviderPipe>
186
     */
187
    public function getPipes(): Collection
188
    {
189
        return $this->pipes;
190
    }
191
192
    /**
193
     * Get processing statistics.
194
     */
195
    public function getStats(): array
196
    {
197
        return $this->stats;
198
    }
199
200
    /**
201
     * Reset statistics.
202
     */
203
    protected function resetStats(): void
204
    {
205
        $this->stats = [
206
            'processed' => 0,
207
            'matched' => 0,
208
            'failed' => 0,
209
            'skipped' => 0,
210
            'duration' => 0.0,
211
            'providers' => [],
212
        ];
213
    }
214
215
    /**
216
     * Update statistics from a processing result.
217
     */
218
    protected function updateStats(array $result): void
219
    {
220
        $this->stats['processed']++;
221
222
        switch ($result['status'] ?? '') {
223
            case TvProcessingResult::STATUS_MATCHED:
224
                $this->stats['matched']++;
225
                $provider = $result['provider'] ?? 'unknown';
226
                $this->stats['providers'][$provider] = ($this->stats['providers'][$provider] ?? 0) + 1;
227
                break;
228
229
            case TvProcessingResult::STATUS_PARSE_FAILED:
230
            case TvProcessingResult::STATUS_NOT_FOUND:
231
                $this->stats['failed']++;
232
                break;
233
234
            case TvProcessingResult::STATUS_SKIPPED:
235
                $this->stats['skipped']++;
236
                break;
237
        }
238
    }
239
240
    /**
241
     * Display processing header.
242
     */
243
    protected function displayHeader(string $guidChar = ''): void
0 ignored issues
show
The parameter $guidChar is not used and could be removed. ( Ignorable by Annotation )

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

243
    protected function displayHeader(/** @scrutinizer ignore-unused */ string $guidChar = ''): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
244
    {
245
        // Header is now shown in process() after we know the release count
246
    }
247
248
    /**
249
     * Display processing summary.
250
     */
251
    protected function displaySummary(): void
252
    {
253
        if (! $this->echoOutput) {
254
            return;
255
        }
256
257
        $this->colorCli->header(sprintf(
258
            'TV processing complete: %d processed, %d matched, %d failed (%.2fs)',
259
            $this->stats['processed'],
260
            $this->stats['matched'],
261
            $this->stats['failed'],
262
            $this->stats['duration']
263
        ));
264
265
        if (! empty($this->stats['providers'])) {
266
            $providerSummary = [];
267
            foreach ($this->stats['providers'] as $provider => $count) {
268
                $providerSummary[] = "$provider: $count";
269
            }
270
            $this->colorCli->primary('Matches by provider: '.implode(', ', $providerSummary));
271
        }
272
    }
273
274
    /**
275
     * Create a default pipeline with all standard providers.
276
     */
277
    public static function createDefault(bool $echoOutput = true): self
278
    {
279
        return new self([
280
            new ParseInfoPipe(),
281
            new LocalDbPipe(),
282
            new TvdbPipe(),
283
            new TvMazePipe(),
284
            new TmdbPipe(),
285
            new TraktPipe(),
286
        ], $echoOutput);
287
    }
288
}
289
290