*/ private array $providers; private array $stats = [ 'mode' => self::MODE_PIPELINE, 'totalDuration' => 0.0, 'providers' => [], ]; public function __construct(bool $echooutput) { $this->echooutput = $echooutput; $this->colorCli = new ColorCLI; $this->providers = $this->buildProviderPipeline(); } /** * Process all TV related releases across supported providers. * * @param string $groupID Group ID to process * @param string $guidChar GUID character to process * @param int|string|null $processTV 0/1/2 or '' to read from settings * @param string $mode Processing mode: 'pipeline' (sequential) or 'parallel' (simultaneous) */ public function process(string $groupID = '', string $guidChar = '', int|string|null $processTV = '', string $mode = self::MODE_PIPELINE): void { $processTV = (int) (is_numeric($processTV) ? $processTV : Settings::settingValue('lookuptv')); if ($processTV <= 0) { return; } if ($mode === self::MODE_PARALLEL) { $this->processParallel($groupID, $guidChar, $processTV); } else { $this->processPipeline($groupID, $guidChar, $processTV); } } /** * Retrieve statistics from the most recent run. */ public function getStats(): array { return $this->stats; } /** * Process releases through providers in parallel (all providers process all releases). * This is faster but uses more API calls. Compatible with Forking class. */ private function processParallel(string $groupID, string $guidChar, int $processTV): void { $this->resetStats(self::MODE_PARALLEL); $this->displayModeHeader(self::MODE_PARALLEL, $guidChar); [$totalTime, $processedAny] = $this->runProviders(self::MODE_PARALLEL, $groupID, $guidChar, $processTV); if ($processedAny) { $this->displaySummaryParallel($totalTime); } } /** * Process releases through providers in pipeline (sequential, each processes failures from previous). * This is more efficient and reduces API calls significantly. */ private function processPipeline(string $groupID, string $guidChar, int $processTV): void { $this->resetStats(self::MODE_PIPELINE); $this->displayModeHeader(self::MODE_PIPELINE, $guidChar); [, $processedAny] = $this->runProviders(self::MODE_PIPELINE, $groupID, $guidChar, $processTV); if ($processedAny) { $this->displaySummary(); } } private function resetStats(string $mode): void { $this->stats = [ 'mode' => $mode, 'totalDuration' => 0.0, 'providers' => [], ]; foreach ($this->providers as $provider) { $this->stats['providers'][$provider['name']] = [ 'status' => 'pending', 'duration' => 0.0, ]; } } /** * Execute providers in the configured order and track timing. * * @return array{0: float, 1: bool} [total elapsed time, processed flag] */ private function runProviders(string $mode, string $groupID, string $guidChar, int $processTV): array { $totalTime = 0.0; $providerCount = count($this->providers); $processedAny = false; foreach ($this->providers as $index => $provider) { $pendingWork = $this->getPendingWorkForProvider($provider, $groupID, $guidChar, $processTV); if ($pendingWork === null) { $this->displayProviderSkip($provider['name'], $index + 1, $providerCount); $this->stats['providers'][$provider['name']] = [ 'status' => 'skipped', 'duration' => 0.0, ]; continue; } $this->displayProviderHeader($provider['name'], $index + 1, $providerCount); $this->displayProviderPreview( $provider['name'], $pendingWork['release'], $pendingWork['total'], $provider['status'] ?? 0 ); /** @var object $processor */ $processor = ($provider['factory'])(); if (property_exists($processor, 'echooutput')) { $processor->echooutput = $this->echooutput; } $startTime = microtime(true); $processor->processSite($groupID, $guidChar, $processTV); $elapsedTime = microtime(true) - $startTime; $processedAny = true; $this->stats['providers'][$provider['name']] = [ 'status' => 'processed', 'duration' => $elapsedTime, ]; $this->stats['totalDuration'] += $elapsedTime; if ($mode === self::MODE_PARALLEL) { $totalTime += $elapsedTime; } $this->displayProviderComplete($provider['name'], $elapsedTime); } return [$totalTime, $processedAny]; } /** * Get the provider pipeline in order of preference. */ private function buildProviderPipeline(): array { return [ ['name' => 'Local DB', 'factory' => static fn () => new LocalDB, 'status' => 0], ['name' => 'TVDB', 'factory' => static fn () => new TVDB, 'status' => 0], ['name' => 'TVMaze', 'factory' => static fn () => new TVMaze, 'status' => -1], ['name' => 'TMDB', 'factory' => static fn () => new TMDB, 'status' => -2], ['name' => 'Trakt', 'factory' => static fn () => new TraktTv, 'status' => -3], ]; } /** * Determine whether a provider has pending work and return a preview release. * * @return array{release: Release, total: int}|null */ private function getPendingWorkForProvider(array $provider, string $groupID, string $guidChar, int $processTV): ?array { $status = $provider['status'] ?? 0; $baseQuery = Release::query() ->select([ 'id', 'guid', 'leftguid', 'groups_id', 'searchname', 'size', 'categories_id', 'videos_id', 'tv_episodes_id', 'postdate', ]) ->where(['videos_id' => 0, 'tv_episodes_id' => $status]) ->where('size', '>', 1048576) ->whereBetween('categories_id', [5000, 5999]) ->where('categories_id', '<>', 5070); if ($groupID !== '') { $baseQuery->where('groups_id', $groupID); } if ($guidChar !== '') { $baseQuery->where('leftguid', $guidChar); } if ($processTV === 2) { $baseQuery->where('isrenamed', '=', 1); } $total = (clone $baseQuery)->count(); if ($total === 0) { return null; } $release = (clone $baseQuery) ->orderByDesc('postdate') ->first(); if ($release === null) { return null; } return [ 'release' => $release, 'total' => $total, ]; } private function displayModeHeader(string $mode, string $guidChar = ''): void { if ($mode === self::MODE_PARALLEL) { $this->displayHeaderParallel($guidChar); } else { $this->displayHeader($guidChar); } } /** * Display the processing header. */ private function displayHeader(string $guidChar = ''): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->headerOver('▶ TV Processing'); if ($guidChar !== '') { $this->colorCli->primaryOver(' → '); $this->colorCli->headerOver('PIPELINE Mode'); $this->colorCli->primaryOver(' → '); $this->colorCli->warningOver('Bucket: '); $this->colorCli->header(strtoupper($guidChar)); } else { $this->colorCli->primaryOver(' → '); $this->colorCli->header('PIPELINE Mode'); } echo "\n"; } /** * Display the processing header for parallel mode. */ private function displayHeaderParallel(string $guidChar = ''): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->headerOver('▶ TV Processing'); if ($guidChar !== '') { $this->colorCli->primaryOver(' → '); $this->colorCli->headerOver('PARALLEL Mode'); $this->colorCli->primaryOver(' → '); $this->colorCli->warningOver('Bucket: '); $this->colorCli->header(strtoupper($guidChar)); } else { $this->colorCli->primaryOver(' → '); $this->colorCli->header('PARALLEL Mode'); } echo "\n"; } /** * Display provider processing header. */ private function displayProviderHeader(string $providerName, int $step, int $total): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->primaryOver(' ['); $this->colorCli->warningOver($step); $this->colorCli->primaryOver('/'); $this->colorCli->warningOver($total); $this->colorCli->primaryOver('] '); $this->colorCli->headerOver('→ '); $this->colorCli->header($providerName); } /** * Display details about the next release the provider will work on. */ private function displayProviderPreview(string $providerName, Release $release, int $total, int $status): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->primaryOver(' ↳ '); $this->colorCli->warningOver($total); $this->colorCli->primaryOver(' pending release(s)'); $this->colorCli->primaryOver(' | Filter: '); $this->colorCli->alternate($this->formatStatusLabel($status)); echo "\n"; $this->colorCli->primaryOver(' · Next: '); $this->colorCli->headerOver($release->searchname ?? 'n/a'); $this->colorCli->primaryOver(' | ID '); $this->colorCli->warningOver((string) $release->id); $this->colorCli->primaryOver(' | GUID '); $this->colorCli->warningOver((string) $release->guid); echo "\n"; } /** * Display provider skip message. */ private function displayProviderSkip(string $providerName, int $step, int $total): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->primaryOver(' ['); $this->colorCli->warningOver($step); $this->colorCli->primaryOver('/'); $this->colorCli->warningOver($total); $this->colorCli->primaryOver('] '); $this->colorCli->alternateOver('→ '); $this->colorCli->alternate($providerName.' → No work remaining, skipping'); echo "\n"; } /** * Display provider completion message. */ private function displayProviderComplete(string $providerName, float $elapsedTime): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->primaryOver(' ✓ '); $this->colorCli->primaryOver($providerName); $this->colorCli->primaryOver(' → '); $this->colorCli->alternateOver('Completed in '); $this->colorCli->warning(sprintf('%.2fs', $elapsedTime)); echo "\n"; } /** * Display final processing summary. */ private function displaySummary(): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->primaryOver('✓ Pipeline Complete'); $this->colorCli->primaryOver(' → '); $this->colorCli->primary('Local DB → TVDB → TVMaze → TMDB → Trakt'); echo "\n"; } /** * Display final processing summary for parallel mode. */ private function displaySummaryParallel(float $totalTime): void { if (! $this->echooutput) { return; } echo "\n"; $this->colorCli->primaryOver('✓ Parallel Processing Complete'); $this->colorCli->primaryOver(' → '); $this->colorCli->warningOver('Total: '); $this->colorCli->warning(sprintf('%.2fs', $totalTime)); echo "\n"; } private function formatStatusLabel(int $status): string { return match ($status) { 0 => 'tv_episodes_id = 0 (Unmatched)', -1 => 'tv_episodes_id = -1 (Awaiting TVMaze)', -2 => 'tv_episodes_id = -2 (Awaiting TMDB)', -3 => 'tv_episodes_id = -3 (Awaiting Trakt)', default => 'tv_episodes_id = '.$status, }; } }