Passed
Push — master ( 179943...1193c4 )
by Darko
09:13
created

TvProcessingPipeline::displayHeader()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 16
rs 9.9
cc 3
nc 3
nop 1
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
Bug introduced by
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
        $this->displayHeader($guidChar);
116
117
        // Get releases that need processing
118
        $releases = $this->getTvReleases($groupID, $guidChar, $processTV);
119
        $totalCount = count($releases);
120
121
        if ($totalCount === 0) {
122
            if ($this->echoOutput) {
123
                $this->colorCli->primary('  No TV releases to process');
124
            }
125
            return;
126
        }
127
128
        if ($this->echoOutput) {
129
            echo "\n";
130
            $this->colorCli->primaryOver('  Processing ');
131
            $this->colorCli->warning($totalCount);
132
            $this->colorCli->primary(' releases through pipeline');
133
            echo "\n";
134
        }
135
136
        foreach ($releases as $release) {
137
            $result = $this->processRelease($release);
138
            $this->updateStats($result);
139
        }
140
141
        $this->stats['duration'] = microtime(true) - $startTime;
142
143
        $this->displaySummary();
144
    }
145
146
    /**
147
     * Get TV releases that need processing.
148
     *
149
     * @return Collection
150
     */
151
    protected function getTvReleases(string $groupID, string $guidChar, int $processTV): Collection
152
    {
153
        $qry = Release::query()
154
            ->select([
155
                'id',
156
                'guid',
157
                'leftguid',
158
                'groups_id',
159
                'searchname',
160
                'size',
161
                'categories_id',
162
                'videos_id',
163
                'tv_episodes_id',
164
                'postdate',
165
            ])
166
            ->where(['videos_id' => 0, 'tv_episodes_id' => 0])
167
            ->where('size', '>', 1048576)
168
            ->whereBetween('categories_id', [Category::TV_ROOT, Category::TV_OTHER])
169
            ->where('categories_id', '<>', Category::TV_ANIME)
170
            ->orderByDesc('postdate')
0 ignored issues
show
Bug introduced by
'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

170
            ->orderByDesc(/** @scrutinizer ignore-type */ 'postdate')
Loading history...
171
            ->limit($this->tvqty);
172
173
        if ($groupID !== '') {
174
            $qry->where('groups_id', $groupID);
175
        }
176
177
        if ($guidChar !== '') {
178
            $qry->where('leftguid', $guidChar);
179
        }
180
181
        if ($processTV === 2) {
182
            $qry->where('isrenamed', '=', 1);
183
        }
184
185
        return $qry->get();
186
    }
187
188
    /**
189
     * Get all registered provider pipes.
190
     *
191
     * @return Collection<AbstractTvProviderPipe>
192
     */
193
    public function getPipes(): Collection
194
    {
195
        return $this->pipes;
196
    }
197
198
    /**
199
     * Get processing statistics.
200
     */
201
    public function getStats(): array
202
    {
203
        return $this->stats;
204
    }
205
206
    /**
207
     * Reset statistics.
208
     */
209
    protected function resetStats(): void
210
    {
211
        $this->stats = [
212
            'processed' => 0,
213
            'matched' => 0,
214
            'failed' => 0,
215
            'skipped' => 0,
216
            'duration' => 0.0,
217
            'providers' => [],
218
        ];
219
    }
220
221
    /**
222
     * Update statistics from a processing result.
223
     */
224
    protected function updateStats(array $result): void
225
    {
226
        $this->stats['processed']++;
227
228
        switch ($result['status'] ?? '') {
229
            case TvProcessingResult::STATUS_MATCHED:
230
                $this->stats['matched']++;
231
                $provider = $result['provider'] ?? 'unknown';
232
                $this->stats['providers'][$provider] = ($this->stats['providers'][$provider] ?? 0) + 1;
233
                break;
234
235
            case TvProcessingResult::STATUS_PARSE_FAILED:
236
            case TvProcessingResult::STATUS_NOT_FOUND:
237
                $this->stats['failed']++;
238
                break;
239
240
            case TvProcessingResult::STATUS_SKIPPED:
241
                $this->stats['skipped']++;
242
                break;
243
        }
244
    }
245
246
    /**
247
     * Display processing header.
248
     */
249
    protected function displayHeader(string $guidChar = ''): void
250
    {
251
        if (! $this->echoOutput) {
252
            return;
253
        }
254
255
        echo "\n";
256
        $this->colorCli->headerOver('▶ TV Processing');
257
        $this->colorCli->primaryOver(' → ');
258
        $this->colorCli->headerOver('PIPELINE Mode');
259
        if ($guidChar !== '') {
260
            $this->colorCli->primaryOver(' → ');
261
            $this->colorCli->warningOver('Bucket: ');
262
            $this->colorCli->header(strtoupper($guidChar));
263
        }
264
        echo "\n";
265
    }
266
267
    /**
268
     * Display processing summary.
269
     */
270
    protected function displaySummary(): void
271
    {
272
        if (! $this->echoOutput) {
273
            return;
274
        }
275
276
        echo "\n";
277
        $this->colorCli->primaryOver('✓ Pipeline Complete');
278
        $this->colorCli->primaryOver(' → ');
279
        $this->colorCli->warningOver(sprintf(
280
            '%d processed, %d matched, %d failed',
281
            $this->stats['processed'],
282
            $this->stats['matched'],
283
            $this->stats['failed']
284
        ));
285
        $this->colorCli->primaryOver(' in ');
286
        $this->colorCli->warning(sprintf('%.2fs', $this->stats['duration']));
287
        echo "\n";
288
289
        if (! empty($this->stats['providers'])) {
290
            $providerSummary = [];
291
            foreach ($this->stats['providers'] as $provider => $count) {
292
                $providerSummary[] = "$provider: $count";
293
            }
294
            $this->colorCli->primary('  Matches by provider: ' . implode(', ', $providerSummary));
295
            echo "\n";
296
        }
297
    }
298
299
    /**
300
     * Create a default pipeline with all standard providers.
301
     */
302
    public static function createDefault(bool $echoOutput = true): self
303
    {
304
        return new self([
305
            new ParseInfoPipe(),
306
            new LocalDbPipe(),
307
            new TvdbPipe(),
308
            new TvMazePipe(),
309
            new TmdbPipe(),
310
            new TraktPipe(),
311
        ], $echoOutput);
312
    }
313
}
314
315