NNTmux /
newznab-tmux
| 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
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
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
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 |