Issues (867)

app/Services/TvProcessor.php (15 issues)

1
<?php
2
3
namespace App\Services;
4
5
use App\Models\Release;
6
use App\Models\Settings;
7
use App\Services\TvProcessing\Providers\LocalDbProvider;
8
use App\Services\TvProcessing\Providers\TmdbProvider;
9
use App\Services\TvProcessing\Providers\TraktProvider;
10
use App\Services\TvProcessing\Providers\TvdbProvider;
11
use App\Services\TvProcessing\Providers\TvMazeProvider;
12
use Blacklight\ColorCLI;
13
14
class TvProcessor
15
{
16
    // Processing modes
17
    public const MODE_PIPELINE = 'pipeline';  // Sequential processing (efficient, reduces API calls)
18
19
    public const MODE_PARALLEL = 'parallel';  // Parallel processing (faster, more API calls)
20
21
    private bool $echooutput;
22
23
    private ColorCLI $colorCli;
24
25
    /**
26
     * @var array<int, array{name: string, factory: callable, status: int}>
27
     */
28
    private array $providers;
29
30
    private array $stats = [
31
        'mode' => self::MODE_PIPELINE,
32
        'totalDuration' => 0.0,
33
        'providers' => [],
34
    ];
35
36
    public function __construct(bool $echooutput)
37
    {
38
        $this->echooutput = $echooutput;
39
        $this->colorCli = new ColorCLI;
40
        $this->providers = $this->buildProviderPipeline();
41
    }
42
43
    /**
44
     * Process all TV related releases across supported providers.
45
     *
46
     * @param  string  $groupID  Group ID to process
47
     * @param  string  $guidChar  GUID character to process
48
     * @param  int|string|null  $processTV  0/1/2 or '' to read from settings
49
     * @param  string  $mode  Processing mode: 'pipeline' (sequential) or 'parallel' (simultaneous)
50
     */
51
    public function process(string $groupID = '', string $guidChar = '', int|string|null $processTV = '', string $mode = self::MODE_PIPELINE): void
52
    {
53
        $processTV = (int) (is_numeric($processTV) ? $processTV : Settings::settingValue('lookuptv'));
54
        if ($processTV <= 0) {
55
            return;
56
        }
57
58
        if ($mode === self::MODE_PARALLEL) {
59
            $this->processParallel($groupID, $guidChar, $processTV);
60
        } else {
61
            $this->processPipeline($groupID, $guidChar, $processTV);
62
        }
63
    }
64
65
    /**
66
     * Retrieve statistics from the most recent run.
67
     */
68
    public function getStats(): array
69
    {
70
        return $this->stats;
71
    }
72
73
    /**
74
     * Process releases through providers in parallel (all providers process all releases).
75
     * This is faster but uses more API calls. Compatible with Forking class.
76
     */
77
    private function processParallel(string $groupID, string $guidChar, int $processTV): void
78
    {
79
        $this->resetStats(self::MODE_PARALLEL);
80
        $this->displayModeHeader(self::MODE_PARALLEL, $guidChar);
81
        [$totalTime, $processedAny] = $this->runProviders(self::MODE_PARALLEL, $groupID, $guidChar, $processTV);
82
83
        if ($processedAny) {
84
            $this->displaySummaryParallel($totalTime);
85
        }
86
    }
87
88
    /**
89
     * Process releases through providers in pipeline (sequential, each processes failures from previous).
90
     * This is more efficient and reduces API calls significantly.
91
     */
92
    private function processPipeline(string $groupID, string $guidChar, int $processTV): void
93
    {
94
        $this->resetStats(self::MODE_PIPELINE);
95
        $this->displayModeHeader(self::MODE_PIPELINE, $guidChar);
96
        [, $processedAny] = $this->runProviders(self::MODE_PIPELINE, $groupID, $guidChar, $processTV);
97
98
        if ($processedAny) {
99
            $this->displaySummary();
100
        }
101
    }
102
103
    private function resetStats(string $mode): void
104
    {
105
        $this->stats = [
106
            'mode' => $mode,
107
            'totalDuration' => 0.0,
108
            'providers' => [],
109
        ];
110
111
        foreach ($this->providers as $provider) {
112
            $this->stats['providers'][$provider['name']] = [
113
                'status' => 'pending',
114
                'duration' => 0.0,
115
            ];
116
        }
117
    }
118
119
    /**
120
     * Execute providers in the configured order and track timing.
121
     *
122
     * @return array{0: float, 1: bool} [total elapsed time, processed flag]
123
     */
124
    private function runProviders(string $mode, string $groupID, string $guidChar, int $processTV): array
125
    {
126
        $totalTime = 0.0;
127
        $providerCount = count($this->providers);
128
        $processedAny = false;
129
130
        foreach ($this->providers as $index => $provider) {
131
            $pendingWork = $this->getPendingWorkForProvider($provider, $groupID, $guidChar, $processTV);
132
            if ($pendingWork === null) {
133
                $this->displayProviderSkip($provider['name'], $index + 1, $providerCount);
134
                $this->stats['providers'][$provider['name']] = [
135
                    'status' => 'skipped',
136
                    'duration' => 0.0,
137
                ];
138
139
                continue;
140
            }
141
142
            $this->displayProviderHeader($provider['name'], $index + 1, $providerCount);
143
            $this->displayProviderPreview(
144
                $provider['name'],
145
                $pendingWork['release'],
146
                $pendingWork['total'],
0 ignored issues
show
It seems like $pendingWork['total'] can also be of type Illuminate\Database\Eloquent\Builder and Illuminate\Database\Query\Builder; however, parameter $total of App\Services\TvProcessor::displayProviderPreview() does only seem to accept integer, 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

146
                /** @scrutinizer ignore-type */ $pendingWork['total'],
Loading history...
147
                $provider['status'] ?? 0
148
            );
149
150
            /** @var object $processor */
151
            $processor = ($provider['factory'])();
152
            if (property_exists($processor, 'echooutput')) {
153
                $processor->echooutput = $this->echooutput;
154
            }
155
156
            $startTime = microtime(true);
157
            $processor->processSite($groupID, $guidChar, $processTV);
158
            $elapsedTime = microtime(true) - $startTime;
159
            $processedAny = true;
160
            $this->stats['providers'][$provider['name']] = [
161
                'status' => 'processed',
162
                'duration' => $elapsedTime,
163
            ];
164
            $this->stats['totalDuration'] += $elapsedTime;
165
166
            if ($mode === self::MODE_PARALLEL) {
167
                $totalTime += $elapsedTime;
168
            }
169
170
            $this->displayProviderComplete($provider['name'], $elapsedTime);
171
        }
172
173
        return [$totalTime, $processedAny];
174
    }
175
176
    /**
177
     * Get the provider pipeline in order of preference.
178
     */
179
    private function buildProviderPipeline(): array
180
    {
181
        return [
182
            ['name' => 'Local DB', 'factory' => static fn () => new LocalDbProvider, 'status' => 0],
183
            ['name' => 'TVDB', 'factory' => static fn () => new TvdbProvider, 'status' => 0],
184
            ['name' => 'TVMaze', 'factory' => static fn () => new TvMazeProvider, 'status' => -1],
185
            ['name' => 'TMDB', 'factory' => static fn () => new TmdbProvider, 'status' => -2],
186
            ['name' => 'Trakt', 'factory' => static fn () => new TraktProvider, 'status' => -3],
187
        ];
188
    }
189
190
    /**
191
     * Determine whether a provider has pending work and return a preview release.
192
     *
193
     * @return array{release: Release, total: int}|null
194
     */
195
    private function getPendingWorkForProvider(array $provider, string $groupID, string $guidChar, int $processTV): ?array
196
    {
197
        $status = $provider['status'] ?? 0;
198
199
        $baseQuery = Release::query()
200
            ->select([
201
                'id',
202
                'guid',
203
                'leftguid',
204
                'groups_id',
205
                'searchname',
206
                'size',
207
                'categories_id',
208
                'videos_id',
209
                'tv_episodes_id',
210
                'postdate',
211
            ])
212
            ->where(['videos_id' => 0, 'tv_episodes_id' => $status])
213
            ->where('size', '>', 1048576)
214
            ->whereBetween('categories_id', [5000, 5999])
215
            ->where('categories_id', '<>', 5070);
216
217
        if ($groupID !== '') {
218
            $baseQuery->where('groups_id', $groupID);
219
        }
220
221
        if ($guidChar !== '') {
222
            $baseQuery->where('leftguid', $guidChar);
223
        }
224
225
        if ($processTV === 2) {
226
            $baseQuery->where('isrenamed', '=', 1);
227
        }
228
229
        $total = (clone $baseQuery)->count();
230
        if ($total === 0) {
231
            return null;
232
        }
233
234
        $release = (clone $baseQuery)
235
            ->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

235
            ->orderByDesc(/** @scrutinizer ignore-type */ 'postdate')
Loading history...
236
            ->first();
237
238
        if ($release === null) {
239
            return null;
240
        }
241
242
        return [
243
            'release' => $release,
244
            'total' => $total,
245
        ];
246
    }
247
248
    private function displayModeHeader(string $mode, string $guidChar = ''): void
249
    {
250
        if ($mode === self::MODE_PARALLEL) {
251
            $this->displayHeaderParallel($guidChar);
252
        } else {
253
            $this->displayHeader($guidChar);
254
        }
255
    }
256
257
    /**
258
     * Display the processing header.
259
     */
260
    private 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

260
    private 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...
261
    {
262
        // Header shown when processing starts
263
    }
264
265
    /**
266
     * Display the processing header for parallel mode.
267
     */
268
    private function displayHeaderParallel(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

268
    private function displayHeaderParallel(/** @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...
269
    {
270
        // Header shown when processing starts
271
    }
272
273
    /**
274
     * Display provider processing header.
275
     */
276
    private function displayProviderHeader(string $providerName, int $step, int $total): void
0 ignored issues
show
The parameter $providerName 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

276
    private function displayProviderHeader(/** @scrutinizer ignore-unused */ string $providerName, int $step, int $total): 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...
The parameter $step 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

276
    private function displayProviderHeader(string $providerName, /** @scrutinizer ignore-unused */ int $step, int $total): 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...
The parameter $total 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

276
    private function displayProviderHeader(string $providerName, int $step, /** @scrutinizer ignore-unused */ int $total): 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...
277
    {
278
        // Provider header shown in displayProviderPreview
279
    }
280
281
    /**
282
     * Display details about the next release the provider will work on.
283
     */
284
    private function displayProviderPreview(string $providerName, Release $release, int $total, int $status): void
0 ignored issues
show
The parameter $release 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

284
    private function displayProviderPreview(string $providerName, /** @scrutinizer ignore-unused */ Release $release, int $total, int $status): 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...
The parameter $status 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

284
    private function displayProviderPreview(string $providerName, Release $release, int $total, /** @scrutinizer ignore-unused */ int $status): 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...
285
    {
286
        if (! $this->echooutput) {
287
            return;
288
        }
289
290
        $this->colorCli->header('Processing '.$total.' TV release(s) via '.$providerName.'.');
291
    }
292
293
    /**
294
     * Display provider skip message.
295
     */
296
    private function displayProviderSkip(string $providerName, int $step, int $total): void
0 ignored issues
show
The parameter $total 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

296
    private function displayProviderSkip(string $providerName, int $step, /** @scrutinizer ignore-unused */ int $total): 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...
The parameter $providerName 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

296
    private function displayProviderSkip(/** @scrutinizer ignore-unused */ string $providerName, int $step, int $total): 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...
The parameter $step 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

296
    private function displayProviderSkip(string $providerName, /** @scrutinizer ignore-unused */ int $step, int $total): 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...
297
    {
298
        // No output needed for skipped providers
299
    }
300
301
    /**
302
     * Display provider completion message.
303
     */
304
    private function displayProviderComplete(string $providerName, float $elapsedTime): void
0 ignored issues
show
The parameter $elapsedTime 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

304
    private function displayProviderComplete(string $providerName, /** @scrutinizer ignore-unused */ float $elapsedTime): 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...
The parameter $providerName 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

304
    private function displayProviderComplete(/** @scrutinizer ignore-unused */ string $providerName, float $elapsedTime): 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...
305
    {
306
        // No output needed for individual provider completion
307
    }
308
309
    /**
310
     * Display final processing summary.
311
     */
312
    private function displaySummary(): void
313
    {
314
        // Summary handled by individual providers
315
    }
316
317
    /**
318
     * Display final processing summary for parallel mode.
319
     */
320
    private function displaySummaryParallel(float $totalTime): void
0 ignored issues
show
The parameter $totalTime 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

320
    private function displaySummaryParallel(/** @scrutinizer ignore-unused */ float $totalTime): 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...
321
    {
322
        // Summary handled by individual providers
323
    }
324
}
325