diff --git a/src/Base/Service/MediaFiles.php b/src/Base/Service/MediaFiles.php index bdb16d4..a99eca5 100644 --- a/src/Base/Service/MediaFiles.php +++ b/src/Base/Service/MediaFiles.php @@ -168,6 +168,37 @@ class MediaFiles return false; } + /** + * @param string $tvshowTitle + * @return array|false + */ + public function tvshowExists(string $tvshowTitle): Map|false + { + $existingEpisodes = $this->getEpisodes($tvshowTitle, false); + + if ($existingEpisodes->isEmpty()) { + return false; + } + + $episodes = new Map; + /** @var SplFileInfo $episode */ + foreach ($existingEpisodes as $episode) { + $ptn = (object) (new PTN())->parse($episode->getFilename()); + + if (!property_exists($ptn, 'season') || !property_exists($ptn, 'episode')) { + continue; + } + + $episodes->push($episode); + } + + if ($episodes->count() > 0) { + return $episodes; + } + + return false; + } + public function movieExists(string $title) { $filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title; diff --git a/src/Library/Action/Command/GetMediaFromLibraryCommand.php b/src/Library/Action/Command/GetMediaFromLibraryCommand.php new file mode 100644 index 0000000..d69f858 --- /dev/null +++ b/src/Library/Action/Command/GetMediaFromLibraryCommand.php @@ -0,0 +1,20 @@ + + */ +class GetMediaFromLibraryCommand implements CommandInterface +{ + public function __construct( + public ?int $userId = null, + public ?string $mediaType = null, + public ?string $imdbId = null, + public ?string $title = null, + public ?string $season = null, + public ?string $episode = null, + ) {} +} diff --git a/src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php b/src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php new file mode 100644 index 0000000..ee8354f --- /dev/null +++ b/src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php @@ -0,0 +1,169 @@ + + */ +class GetMediaInfoFromLibraryHandler implements HandlerInterface +{ + public function __construct( + private readonly TmdbClient $tmdb, + private readonly MediaFiles $mediaFiles, + private readonly LoggerInterface $logger, + private readonly MonitorRepository $monitorRepository, + ) {} + + public function handle(C $command): R + { + $result = new GetMediaFromLibraryResult(); + $tmdbResult = $this->fetchTmdbData($command->imdbId, $command->mediaType); + + if (null === $tmdbResult) { + $this->logger->warning('[GetMediaInfoFromLibraryHandler] TMDb result was not found, this may lead to issues in the rest of the library search', (array) $command); + } + + $this->setResultExists($tmdbResult->mediaType, $tmdbResult->title, $result); + if ($result->notExists()) { + return $result; + } + + $this->parseFromTmdbResult($tmdbResult, $result); + + if ($command->mediaType === MediaType::TvShow->value) { + $this->setEpisodes($tmdbResult, $result); + $this->setSeasons($tmdbResult, $result); + $this->setMonitors($command->userId, $command->imdbId, $result); + } + + return $result; + } + + private function fetchTmdbData(string $imdbId, string $mediaType): ?TmdbResult + { + return match($mediaType) { + MediaType::Movie->value => $this->tmdb->movieDetails($imdbId), + MediaType::TvShow->value => $this->tmdb->tvShowDetails($imdbId), + default => null, + }; + } + + private function setResultExists(string $mediaType, string $title, GetMediaFromLibraryResult $result): void + { + $fsResult = match($mediaType) { + MediaType::Movie->value => $this->mediaFiles->movieExists($title), + MediaType::TvShow->value => $this->mediaFiles->tvShowExists($title), + default => false, + }; + if (false === $fsResult) { + $result->setExists(false); + } else { + $result->setExists(true); + } + } + + public function parseFromTmdbResult(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void + { + $result->setTitle($tmdbResult->title); + $result->setMediaType($tmdbResult->mediaType); + $result->setImdbId($tmdbResult->imdbId); + } + + public function setEpisodes(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void + { + $existingEpisodeFiles = $this->mediaFiles->tvshowExists($tmdbResult->title); + + $existingEpisodeMap = []; + foreach ($existingEpisodeFiles as $file) { + /** @var \SplFileInfo $file */ + $ptn = (object) new PTN()->parse($file->getBasename()); + + if (!array_key_exists($ptn->season, $existingEpisodeMap)) { + $existingEpisodeMap[$ptn->season] = []; + } + + if (!in_array($ptn->episode, $existingEpisodeMap[$ptn->season])) { + $existingEpisodeMap[$ptn->season][] = $ptn->episode; + } + } + + $existingEpisodes = []; + $missingEpisodes = []; + foreach ($tmdbResult->episodes as $season => $episodes) { + foreach ($episodes as $episode) { + if (array_key_exists($season, $existingEpisodeMap)) { + if (in_array($episode->episodeNumber, $existingEpisodeMap[$season])) { + $existingEpisodes[] = $episode; + } else { + $missingEpisodes[] = $episode; + } + } else { + $missingEpisodes[] = $episode; + } + } + } + $result->setEpisodes($existingEpisodes); + $result->setMissingEpisodes($missingEpisodes); + } + + public function setSeasons(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void + { + $existingEpisodeFiles = $this->mediaFiles->tvshowExists($tmdbResult->title); + + $existingEpisodeMap = []; + foreach ($existingEpisodeFiles as $file) { + /** @var \SplFileInfo $file */ + $ptn = (object) new PTN()->parse($file->getBasename()); + $existingEpisodeMap[$ptn->season][] = $ptn->episode; + } + + $existingFullSeasons = []; + $existingPartialSeasons = []; + $missingSeasons = []; + foreach ($existingEpisodeMap as $season => $episodes) { + if (count($tmdbResult->episodes[$season]) === count($episodes)) { + $existingFullSeasons[] = $season; + } elseif (count($episodes) > 0) { + $existingPartialSeasons[] = $season; + } + } + + $seasons = array_keys($tmdbResult->episodes); + foreach ($seasons as $season) { + if (!in_array($season, $existingFullSeasons) && !in_array($season, $existingPartialSeasons)) { + $missingSeasons[] = $season; + } + } + + $result->setSeasons($existingFullSeasons); + $result->setPartialSeasons($existingPartialSeasons); + $result->setMissingSeasons($missingSeasons); + } + + public function setMonitors(int $userId, string $imdbId, GetMediaFromLibraryResult $result) + { + $result->setMonitorCount( + $this->monitorRepository->countUserChildrenByParentId($userId, $imdbId) + ); + $result->setActiveMonitorCount( + $this->monitorRepository->countUserActiveChildrenByParentId($userId, $imdbId) + ); + $result->setCompleteMonitorCount( + $this->monitorRepository->countUserCompleteChildrenByParentId($userId, $imdbId) + ); + } +} diff --git a/src/Library/Action/Input/GetMediaInfoFromLibraryInput.php b/src/Library/Action/Input/GetMediaInfoFromLibraryInput.php new file mode 100644 index 0000000..a91f06c --- /dev/null +++ b/src/Library/Action/Input/GetMediaInfoFromLibraryInput.php @@ -0,0 +1,39 @@ + + */ +class GetMediaInfoFromLibraryInput implements InputInterface +{ + public function __construct( + #[SourceRequest('imdbId', nullify: true)] + public ?string $imdbId = null, + #[SourceRequest('title', nullify: true)] + public ?string $title = null, + #[SourceRequest('season', nullify: true)] + public ?string $season = null, + #[SourceRequest('episode', nullify: true)] + public ?string $episode = null, + ) {} + + public function toCommand(): C + { + if (null === $this->imdbId && null === $this->title) { + throw new \InvalidArgumentException('Either imdbId or title must be set', 400); + } + + return new GetMediaFromLibraryCommand( + imdbId: $this->imdbId, + title: $this->title, + season: $this->season, + episode: $this->episode, + ); + } +} diff --git a/src/Library/Action/Result/GetMediaFromLibraryResult.php b/src/Library/Action/Result/GetMediaFromLibraryResult.php new file mode 100644 index 0000000..2dca198 --- /dev/null +++ b/src/Library/Action/Result/GetMediaFromLibraryResult.php @@ -0,0 +1,171 @@ +exists; + } + + public function notExists(): bool + { + return !$this->exists; + } + + public function setExists(bool $exists): void + { + $this->exists = $exists; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getImdbId(): ?string + { + return $this->imdbId; + } + + public function setImdbId(?string $imdbId): void + { + $this->imdbId = $imdbId; + } + + public function getMediaType(): ?string + { + return $this->mediaType; + } + + public function setMediaType(?string $mediaType): void + { + $this->mediaType = $mediaType; + } + + public function getEpisodes(): ?array + { + return $this->episodes; + } + + public function setEpisodes(?array $episodes): void + { + $this->episodes = $episodes; + } + + public function getEpisodeCount(): ?int + { + return count($this->episodes); + } + + public function getMissingEpisodes(): ?array + { + return $this->missingEpisodes; + } + + public function setMissingEpisodes(?array $missingEpisodes): void + { + $this->missingEpisodes = $missingEpisodes; + } + + public function getMissingEpisodeCount(): ?int + { + return count($this->missingEpisodes); + } + + public function getSeasons(): ?array + { + return $this->seasons; + } + + public function setSeasons(?array $seasons): void + { + $this->seasons = $seasons; + } + + public function getSeasonCount(): ?int + { + return count($this->seasons); + } + + public function getPartialSeasons(): ?array + { + return $this->partialSeasons; + } + + public function setPartialSeasons(?array $partialSeasons): void + { + $this->partialSeasons = $partialSeasons; + } + + public function getPartialSeasonCount(): ?int + { + return count($this->partialSeasons); + } + + public function getMissingSeasons(): ?array + { + return $this->missingSeasons; + } + + public function setMissingSeasons(?array $missingSeasons): void + { + $this->missingSeasons = $missingSeasons; + } + + public function getMissingSeasonCount(): ?int + { + return count($this->missingSeasons); + } + + public function getMonitorCount(): ?int + { + return $this->monitorCount; + } + + public function setMonitorCount(?int $monitorCount): void + { + $this->monitorCount = $monitorCount; + } + + public function getActiveMonitorCount(): ?int + { + return $this->activeMonitorCount; + } + + public function setActiveMonitorCount(?int $activeMonitorCount): void + { + $this->activeMonitorCount = $activeMonitorCount; + } + + public function getCompleteMonitorCount(): ?int + { + return $this->completeMonitorCount; + } + + public function setCompleteMonitorCount(?int $completeMonitorCount): void + { + $this->completeMonitorCount = $completeMonitorCount; + } +} diff --git a/src/Monitor/Framework/Controller/WebController.php b/src/Monitor/Framework/Controller/WebController.php index 686a161..67fcc5b 100644 --- a/src/Monitor/Framework/Controller/WebController.php +++ b/src/Monitor/Framework/Controller/WebController.php @@ -3,6 +3,8 @@ namespace App\Monitor\Framework\Controller; use App\Download\Action\Input\DeleteDownloadInput; +use App\Library\Action\Command\GetMediaFromLibraryCommand; +use App\Library\Action\Handler\GetMediaInfoFromLibraryHandler; use App\Monitor\Action\Handler\AddMonitorHandler; use App\Monitor\Action\Handler\DeleteMonitorHandler; use App\Monitor\Action\Input\AddMonitorInput; @@ -40,7 +42,7 @@ class WebController extends AbstractController } #[Route('/monitors/{id}', name: 'app.monitor.view', methods: ['GET'])] - public function viewMonitor(Monitor $monitor, GetMediaInfoHandler $getMediaInfoHandler) + public function viewMonitor(Monitor $monitor, GetMediaInfoHandler $getMediaInfoHandler, GetMediaInfoFromLibraryHandler $handler) { $media = $getMediaInfoHandler->handle( new GetMediaInfoCommand( @@ -48,9 +50,19 @@ class WebController extends AbstractController mediaType: 'tvshows', ) ); + $libraryResult = $handler->handle( + new GetMediaFromLibraryCommand( + $this->getUser()->getId(), + $media->media->mediaType, + $media->media->imdbId, + $media->media->title, + ) + ); + return $this->render('monitor/view.html.twig', [ 'monitor' => $monitor, 'results' => $media, + 'library' => $libraryResult ]); } } diff --git a/src/Monitor/Framework/Repository/MonitorRepository.php b/src/Monitor/Framework/Repository/MonitorRepository.php index 64a068d..b6808a6 100644 --- a/src/Monitor/Framework/Repository/MonitorRepository.php +++ b/src/Monitor/Framework/Repository/MonitorRepository.php @@ -41,4 +41,83 @@ class MonitorRepository extends ServiceEntityRepository ->getQuery(); return $query->getResult(); } + + public function getActiveUserMonitors() + { + return $this->asPaginator($this->monitorRepository->createQueryBuilder('m') + ->andWhere('m.status IN (:statuses)') + ->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)') + ->andWhere('m.parent IS NULL') + ->setParameter('statuses', ['New', 'In Progress', 'Active']) + ->setParameter('term', '%'.$this->term.'%') + ->orderBy('m.id', 'DESC') + ->getQuery() + ); + } + + public function getChildMonitorsByParentId(int $parentId) + { + return $this->asPaginator( + $this->monitorRepository->createQueryBuilder('m') + ->andWhere("m.parent = :parentId") + ->setParameter('parentId', $parentId) + ->orderBy('m.id', 'DESC') + ->getQuery() + ); + } + + public function getCompleteUserMonitors() + { + return $this->asPaginator($this->monitorRepository->createQueryBuilder('m') + ->andWhere('m.status = :status') + ->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)') + ->setParameter('status', 'Complete') + ->setParameter('term', '%'.$this->term.'%') + ->orderBy('m.id', 'DESC') + ->getQuery() + ); + } + + public function countUserChildrenByParentId(int $userId, string $imdbId): ?int + { + return $this->createQueryBuilder('m') + ->select('COUNT(m.id)') + ->andWhere('m.user = :user') + ->andWhere('m.imdbId = :imdbId') + ->setParameter('user', $userId) + ->setParameter('imdbId', $imdbId) + ->getQuery() + ->getSingleScalarResult() + ; + } + + public function countUserActiveChildrenByParentId(int $userId, string $imdbId): ?int + { + return $this->createQueryBuilder('m') + ->select('COUNT(m.id)') + ->andWhere('m.user = :user') + ->andWhere('m.imdbId = :imdbId') + ->andWhere('m.status IN (:statuses)') + ->setParameter('user', $userId) + ->setParameter('statuses', ['Active', 'New', 'In Progress']) + ->setParameter('imdbId', $imdbId) + ->getQuery() + ->getSingleScalarResult() + ; + } + + public function countUserCompleteChildrenByParentId(int $userId, string $imdbId): ?int + { + return $this->createQueryBuilder('m') + ->select('COUNT(m.id)') + ->andWhere('m.user = :user') + ->andWhere('m.imdbId = :imdbId') + ->andWhere('m.status IN (:statuses)') + ->setParameter('user', $userId) + ->setParameter('statuses', ['Complete']) + ->setParameter('imdbId', $imdbId) + ->getQuery() + ->getSingleScalarResult() + ; + } } diff --git a/templates/monitor/view.html.twig b/templates/monitor/view.html.twig index 534098f..e5cb0d9 100644 --- a/templates/monitor/view.html.twig +++ b/templates/monitor/view.html.twig @@ -26,25 +26,34 @@ {{ results.media.description }}

-
- {% if results.media.stars != null %} - Starring: {{ results.media.stars|join(', ') }}
- {% endif %} +
+
+ {% if results.media.stars != null %} + Starring: {{ results.media.stars|join(', ') }}
+ {% endif %} - {% if results.media.directors != null %} - Directors: {{ results.media.directors|join(', ') }}
- {% endif %} + {% if results.media.directors != null %} + Directors: {{ results.media.directors|join(', ') }}
+ {% endif %} - {% if results.media.producers != null %} - Producers: {{ results.media.producers|join(', ') }}
- {% endif %} + {% if results.media.producers != null %} + Producers: {{ results.media.producers|join(', ') }}
+ {% endif %} - {% if results.media.creators != null %} - Creators: {{ results.media.creators|join(', ') }}
- {% endif %} + {% if results.media.creators != null %} + Creators: {{ results.media.creators|join(', ') }}
+ {% endif %} +
+ +
+ {% if results.media.premiereDate %} + Premiered: {{ results.media.premiereDate|date('n/j/Y', 'UTC') }}
+ {% endif %} +
{% if results.media.genres != null %}
+{# Genres:
#} {% for genre in results.media.genres %} {{ genre }} {% endfor %} @@ -53,14 +62,55 @@
{% if results.media.mediaType == "tvshows" %} -
- - {{ results.media.numberSeasons }} season(s) - - - {{ results.media.premiereDate|date(null, 'UTC') }} - +
+
+ In Your Library +
+
+ Seasons +
+ + {{ library.seasonCount }} full + + + {{ library.partialSeasonCount }} partial + + + {{ library.missingSeasonCount }} missing + +
+
+ +
+ Episodes +
+ + {{ library.episodeCount }} existing + + + {{ library.missingEpisodeCount }} missing + +
+
+ +
+ Monitors +
+ + {{ library.monitorCount }} total + + + {{ library.activeMonitorCount }} active + + + {{ library.completeMonitorCount }} complete + +
+
+
+
+ {% endif %} {% if "movies" == results.media.mediaType %}