Files
newznab-tmux-NNTmux/app/Services/TvProcessing/Pipes/TvdbPipe.php
T
2026-05-30 23:31:00 +02:00

304 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TvProcessing\Pipes;
use App\Services\FanartTvService;
use App\Services\TvProcessing\Providers\TvdbProvider;
use App\Services\TvProcessing\TvProcessingPassable;
use App\Services\TvProcessing\TvProcessingResult;
/**
* Pipe for TVDB API lookups.
*/
class TvdbPipe extends AbstractTvProviderPipe
{
// Video type constants (matching Videos class protected constants)
private const TYPE_TV = 0;
protected int $priority = 20;
private ?TvdbProvider $tvdb = null;
private ?FanartTvService $fanart = null;
public function getName(): string
{
return 'TVDB';
}
public function getStatusCode(): int
{
return 0; // PROCESS_TVDB
}
/**
* Get or create the TVDB instance.
*/
private function getTvdb(): TvdbProvider
{
if ($this->tvdb === null) {
$this->tvdb = new TvdbProvider;
}
return $this->tvdb;
}
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());
}
$tvdb = $this->getTvdb();
$siteId = false;
$posterUrl = '';
// Find the Video ID if it already exists by checking the title
$videoId = $tvdb->getByTitle($cleanName, self::TYPE_TV);
// 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 = $tvdb->getByTitle($nameWithoutYear, self::TYPE_TV);
}
if ($videoId !== 0) {
$siteId = $tvdb->getSiteByID('tvdb', (int) $videoId);
// If show exists in local DB but doesn't have a TVDB ID, use the existing video
// and process episode matching without trying to search TVDB API
if ($siteId === false || $siteId === 0) {
// Show exists in our DB (likely from another source like TMDB)
// Skip TVDB API search and proceed to episode matching
$this->outputFoundInDb($cleanName);
return $this->processEpisodeForExistingVideo($passable, $tvdb, $videoId, $parsedInfo);
}
}
// Check if we have a valid country
$country = (
isset($parsedInfo['country']) && strlen((string) $parsedInfo['country']) === 2
? (string) $parsedInfo['country']
: ''
);
if ($siteId === false || $siteId === 0) { // @phpstan-ignore identical.alwaysFalse
// Not in local DB, search TVDB
$this->outputSearching($cleanName);
$tvdbShow = $tvdb->getShowInfo((string) $cleanName);
// If not found and cleanName contains a year in parentheses, try without the year
if ($tvdbShow === false && preg_match('/^(.+?)\s*\(\d{4}\)$/', (string) $cleanName, $yearMatch)) {
$nameWithoutYear = trim($yearMatch[1]);
$tvdbShow = $tvdb->getShowInfo($nameWithoutYear);
}
if (is_array($tvdbShow)) {
$tvdbShow['country'] = $country;
$videoId = $tvdb->add($tvdbShow);
$siteId = (int) $tvdbShow['tvdb'];
$posterUrl = $tvdbShow['poster'] ?? '';
}
} 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]);
}
// Fetch poster if available
if (! empty($posterUrl)) {
$tvdb->getPoster($videoId);
} else {
$this->fetchFanartPoster($videoId, $siteId);
}
// 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
$tvdb->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 (! $tvdb->countEpsByVideoID($videoId)) {
$tvdb->getEpisodeInfo($siteId, -1, -1, $videoId);
}
// Check if we have the episode for this video ID
$episode = $tvdb->getBySeasonEp($videoId, $seriesNo, $episodeNo, $parsedInfo['airdate'] ?? '');
if ($episode === false) {
if ($seriesNo !== '' && $episodeNo !== '') {
// Try to get episode from TVDB
$tvdbEpisode = $tvdb->getEpisodeInfo($siteId, (int) $seriesNo, (int) $episodeNo, $videoId);
if ($tvdbEpisode) {
$episode = $tvdb->addEpisode($videoId, $tvdbEpisode);
}
}
if ($episode === false && $hasAirdate) {
// Refresh episode cache and attempt airdate match
$tvdb->getEpisodeInfo($siteId, -1, -1, $videoId);
$episode = $tvdb->getBySeasonEp($videoId, 0, 0, $parsedInfo['airdate']);
}
}
if ($episode !== false && is_numeric($episode) && $episode > 0) {
// Success!
$tvdb->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
$tvdb->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,
]);
}
/**
* Fetch poster from Fanart.tv.
*/
private function fetchFanartPoster(int $videoId, int $siteId): void
{
if ($this->fanart === null) {
$this->fanart = new FanartTvService;
}
if (! $this->fanart->isConfigured()) {
return;
}
$posterUrl = $this->fanart->getBestTvPoster($siteId);
if (! empty($posterUrl)) {
$this->getTvdb()->posterUrl = $posterUrl;
$this->getTvdb()->getPoster($videoId);
}
}
/**
* 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 TVDB ID.
*
* @param array<string, mixed> $parsedInfo
*/
private function processEpisodeForExistingVideo(
TvProcessingPassable $passable,
TvdbProvider $tvdb,
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
$tvdb->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 = $tvdb->getBySeasonEp($videoId, $seriesNo, $episodeNo, $parsedInfo['airdate'] ?? '');
if ($episode !== false && is_numeric($episode) && $episode > 0) {
$tvdb->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
$tvdb->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,
]);
}
}