*/ readonly class MonitorTvShowHandler implements HandlerInterface { public function __construct( private MonitorRepository $monitorRepository, private EntityManagerInterface $entityManager, private MonitorTvEpisodeHandler $monitorTvEpisodeHandler, private MediaFiles $mediaFiles, private LoggerInterface $logger, private TmdbClient $tmdb, ) { } public function handle(CommandInterface $command): ResultInterface { $this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler'); $monitor = $this->monitorRepository->find($command->monitorId); // Check current episodes $downloadedEpisodes = $this->mediaFiles ->getEpisodes($monitor->getTitle()) ->map(fn($episode) => (object)(new PTN())->parse($episode)) ->filter(fn($episode) => property_exists($episode, 'episode') && property_exists($episode, 'season') && null !== $episode->episode && null !== $episode->season ); $this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle()); // Compare against list from TMDB $episodesInShow = Map::from( $this->tmdb->tvshowDetails($monitor->getImdbId())->episodes )->flat(1) ->filter(fn(TmdbEpisodeDto $episode) => $episode->seasonNumber >= $monitor->getSeason()) ->values(); $this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle()); $episodeMonitors = []; // Dispatch Episode commands for each missing Episode foreach ($episodesInShow as $episode) { /** @var TmdbEpisodeDto $episode */ // Only monitor future episodes $this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode->seasonNumber . '" episode "' . $episode->episodeNumber . '"'); $episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode); $this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO')); if (false === $episodeInFuture) { $this->logger->info('> [MonitorTvShowHandler] ...Skipping'); continue; } // Check if the episode is already downloaded $episodeExists = $this->episodeExists($episode, $downloadedEpisodes); $this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO')); if (true === $episodeExists) { $this->logger->info('> [MonitorTvShowHandler] ...Skipping'); continue; } // Check for existing monitors $monitorExists = $this->monitorExists($monitor, $episode); $this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO')); if (true === $monitorExists) { $this->logger->info('> [MonitorTvShowHandler] ...Skipping'); continue; } // Create the monitor $episodeMonitor = (new Monitor()) ->setParent($monitor) ->setUser($monitor->getUser()) ->setTmdbId($monitor->getTmdbId()) ->setImdbId($monitor->getImdbId()) ->setTitle($monitor->getTitle()) ->setMonitorType('tvepisode') ->setSeason($episode->seasonNumber) ->setEpisode($episode->episodeNumber) ->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null) ->setCreatedAt(new DateTimeImmutable()) ->setSearchCount(0) ->setStatus('New'); $this->monitorRepository->getEntityManager()->persist($episodeMonitor); $this->monitorRepository->getEntityManager()->flush(); $episodeMonitors[] = $episodeMonitor; // Immediately run the monitor $command = new MonitorTvEpisodeCommand($episodeMonitor->getId()); $this->monitorTvEpisodeHandler->handle($command); $this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand'); } // Set the status to Active, so it will be re-executed. $monitor->setStatus('Active'); $monitor->setLastSearch(new DateTimeImmutable()); $monitor->setSearchCount($monitor->getSearchCount() + 1); $this->entityManager->flush(); return new MonitorTvShowResult( status: 'OK', result: [ 'monitor' => $monitor, 'new_monitors' => $episodeMonitors, ] ); } private function episodeReleasedAfterMonitorCreated( string|DateTimeImmutable $monitorStartDate, TmdbEpisodeDto $episodeInShow ): bool { $monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0); $episodeAirDate = Carbon::parse($episodeInShow->airDate); return $episodeAirDate >= $monitorStartDate; } private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool { return $downloadedEpisodes->filter( fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber && $episode->season === $episodeInShow->seasonNumber )->count() > 0; } private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool { return $this->monitorRepository->findOneBy([ 'imdbId' => $monitor->getImdbId(), 'title' => $monitor->getTitle(), 'monitorType' => 'tvepisode', 'season' => $episode->seasonNumber, 'episode' => $episode->episodeNumber, 'status' => ['New', 'Active', 'In Progress'] ]) !== null; } }