trakt === null) { $this->trakt = new TraktProvider; } return $this->trakt; } protected function process(TvProcessingPassable $passable): TvProcessingResult { $parsedInfo = $passable->getParsedInfo(); $context = $passable->context; if ($parsedInfo === null || empty($parsedInfo['cleanname'])) { return TvProcessingResult::notFound($this->getName()); } $cleanName = $parsedInfo['cleanname']; // Check if we've already failed this title if ($this->isInTitleCache($cleanName)) { $this->outputSkipped($cleanName); return TvProcessingResult::skipped('previously failed', $this->getName()); } $trakt = $this->getTrakt(); $siteId = false; // Find the Video ID if it already exists $videoId = $trakt->getByTitle($cleanName, self::TYPE_TV, self::SOURCE_TRAKT); // If not found and cleanName contains a year in parentheses, try without the year if ($videoId === 0 && preg_match('/^(.+?)\s*\(\d{4}\)$/', (string) $cleanName, $yearMatch)) { $nameWithoutYear = trim($yearMatch[1]); $videoId = $trakt->getByTitle($nameWithoutYear, self::TYPE_TV, self::SOURCE_TRAKT); } if ($videoId !== 0) { $siteId = $trakt->getSiteIDFromVideoID('trakt', (int) $videoId); // If show exists in local DB but doesn't have a Trakt ID, use the existing video // and process episode matching without trying to search Trakt API if ($siteId === false || $siteId === 0) { // Show exists in our DB (likely from another source like TMDB) // Skip Trakt API search and proceed to episode matching $this->outputFoundInDb($cleanName); return $this->processEpisodeForExistingVideo($passable, $trakt, $videoId, $parsedInfo); } } if ($videoId === 0) { // Not in local DB, search Trakt $this->outputSearching($cleanName); $traktShow = $trakt->getShowInfo((string) $cleanName); // If not found and cleanName contains a year in parentheses, try without the year if ($traktShow === false && preg_match('/^(.+?)\s*\(\d{4}\)$/', (string) $cleanName, $yearMatch)) { $nameWithoutYear = trim($yearMatch[1]); $traktShow = $trakt->getShowInfo($nameWithoutYear); } if (is_array($traktShow)) { $videoId = $trakt->add($traktShow); $siteId = (int) $traktShow['trakt']; } } else { $this->outputFoundInDb($cleanName); } if ((int) $videoId === 0 || (int) $siteId === 0) { // Show not found $this->addToTitleCache($cleanName); $this->outputNotFound($cleanName); return TvProcessingResult::notFound($this->getName(), ['title' => $cleanName]); } // Process episode $seriesNo = ! empty($parsedInfo['season']) ? preg_replace('/^S0*/i', '', (string) $parsedInfo['season']) : ''; $episodeNo = ! empty($parsedInfo['episode']) ? preg_replace('/^E0*/i', '', (string) $parsedInfo['episode']) : ''; $hasAirdate = ! empty($parsedInfo['airdate']); if ($episodeNo === 'all') { // Full season release $trakt->setVideoIdFound($videoId, $context->releaseId, 0); $this->outputFullSeason($cleanName); return TvProcessingResult::matched($videoId, 0, $this->getName(), ['full_season' => true]); } // Download all episodes if new show to reduce API/bandwidth usage if (! $trakt->countEpsByVideoID($videoId)) { $trakt->getEpisodeInfo($siteId, -1, -1, $videoId); } // Check if we have the episode for this video ID $episode = $trakt->getBySeasonEp($videoId, $seriesNo, $episodeNo, $parsedInfo['airdate'] ?? ''); if ($episode === false) { if ($seriesNo !== '' && $episodeNo !== '') { // Try to get episode from Trakt with fallback to other IDs $traktEpisode = $trakt->getEpisodeInfo($siteId, (int) $seriesNo, (int) $episodeNo, $videoId); if ($traktEpisode) { $episode = $trakt->addEpisode($videoId, $traktEpisode); } } if ($episode === false && $hasAirdate) { // Refresh episode cache and attempt airdate match $trakt->getEpisodeInfo($siteId, -1, -1, $videoId); $episode = $trakt->getBySeasonEp($videoId, 0, 0, $parsedInfo['airdate']); } } if ($episode !== false && is_numeric($episode) && $episode > 0) { // Success! $trakt->setVideoIdFound($videoId, $context->releaseId, $episode); $this->outputMatch( $cleanName, $seriesNo !== '' ? (int) $seriesNo : null, $episodeNo !== '' ? (int) $episodeNo : null, $hasAirdate ? $parsedInfo['airdate'] : null ); return TvProcessingResult::matched($videoId, (int) $episode, $this->getName()); } // Episode not found $trakt->setVideoIdFound($videoId, $context->releaseId, 0); if ($this->echoOutput) { cli()->primaryOver(' → '); cli()->alternateOver($this->truncateTitle($cleanName)); if ($hasAirdate) { cli()->primaryOver(' | '); cli()->warningOver($parsedInfo['airdate']); } cli()->primaryOver(' → '); cli()->warning('Episode not found'); } return TvProcessingResult::notFound($this->getName(), [ 'video_id' => $videoId, 'episode_not_found' => true, ]); } /** * Output full season match message. */ private function outputFullSeason(string $title): void { if (! $this->echoOutput) { return; } cli()->primaryOver(' → '); cli()->headerOver($this->truncateTitle($title)); cli()->primaryOver(' → '); cli()->primary('Full Season matched'); } /** * Process episode matching for a video that already exists in local DB. * This is used when the show was added from another source (e.g., TMDB) and doesn't have a Trakt ID. * * @param array $parsedInfo */ private function processEpisodeForExistingVideo( TvProcessingPassable $passable, TraktProvider $trakt, int $videoId, array $parsedInfo ): TvProcessingResult { $context = $passable->context; $cleanName = $parsedInfo['cleanname']; $seriesNo = ! empty($parsedInfo['season']) ? preg_replace('/^S0*/i', '', (string) $parsedInfo['season']) : ''; $episodeNo = ! empty($parsedInfo['episode']) ? preg_replace('/^E0*/i', '', (string) $parsedInfo['episode']) : ''; $hasAirdate = ! empty($parsedInfo['airdate']); if ($episodeNo === 'all') { // Full season release $trakt->setVideoIdFound($videoId, $context->releaseId, 0); $this->outputFullSeason($cleanName); return TvProcessingResult::matched($videoId, 0, $this->getName(), ['full_season' => true]); } // Try to find episode in local DB $episode = $trakt->getBySeasonEp($videoId, $seriesNo, $episodeNo, $parsedInfo['airdate'] ?? ''); if ($episode !== false && is_numeric($episode) && $episode > 0) { $trakt->setVideoIdFound($videoId, $context->releaseId, $episode); $this->outputMatch( $cleanName, $seriesNo !== '' ? (int) $seriesNo : null, $episodeNo !== '' ? (int) $episodeNo : null, $hasAirdate ? $parsedInfo['airdate'] : null ); return TvProcessingResult::matched($videoId, (int) $episode, $this->getName()); } // Episode not found in local DB - mark video but episode not matched $trakt->setVideoIdFound($videoId, $context->releaseId, 0); if ($this->echoOutput) { cli()->primaryOver(' → '); cli()->alternateOver($this->truncateTitle($cleanName)); if ($hasAirdate) { cli()->primaryOver(' | '); cli()->warningOver($parsedInfo['airdate']); } cli()->primaryOver(' → '); cli()->warning('Episode not in local DB'); } return TvProcessingResult::notFound($this->getName(), [ 'video_id' => $videoId, 'episode_not_found' => true, ]); } }