diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 5e2f446..4a13da8 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,3 +1,7 @@ +twig: + globals: + watchStatsProvider: '@tracky\watchstats\WatchStatsProvider' + when@test: twig: strict_variables: true diff --git a/src/main/php/tracky/HistoryEntry.php b/src/main/php/tracky/HistoryEntry.php new file mode 100644 index 0000000..82642d3 --- /dev/null +++ b/src/main/php/tracky/HistoryEntry.php @@ -0,0 +1,76 @@ +view; + } + + public function getItem(): Episode|Movie + { + return $this->item; + } + + public function getViewCount(): int + { + return $this->watchStatsProvider->getItemStatsByView($this->view)->getCount(); + } + + /** + * @return list + */ + public static function getFromViews(array $views, EpisodeRepository $episodeRepository, MovieRepository $movieRepository, WatchStatsProvider $watchStatsProvider): array + { + $perTypeItems = []; + + // Split up list of views to list of items per type + foreach ($views as $view) { + $type = $view->getType()->value; + + if (!isset($perTypeItems[$type])) { + $perTypeItems[$type] = []; + } + + $perTypeItems[$type][] = $view->getItem(); + } + + // Fetch items per type + foreach ($perTypeItems as $type => $items) { + $items = array_unique($items); + + switch ($type) { + case ViewType::EPISODE->value: + $items = $episodeRepository->findByIds($items); + break; + case ViewType::MOVIE->value: + $items = $movieRepository->findByIds($items); + break; + } + + $perTypeItems[$type] = array_combine(array_map(fn(Episode|Movie $item) => $item->getId(), $items), $items); + } + + $historyEntries = []; + + foreach ($views as $view) { + $historyEntries[] = new HistoryEntry($view, $perTypeItems[$view->getType()->value][$view->getItem()], $watchStatsProvider); + } + + return $historyEntries; + } +} diff --git a/src/main/php/tracky/controller/HistoryController.php b/src/main/php/tracky/controller/HistoryController.php index 07a0974..4d73a90 100644 --- a/src/main/php/tracky/controller/HistoryController.php +++ b/src/main/php/tracky/controller/HistoryController.php @@ -1,19 +1,19 @@ query->getInt("page", 1); @@ -47,11 +47,9 @@ public function getPage(Request $request, User $user, EntityManagerInterface $en switch ($type) { case ViewType::EPISODE: $criteria["item"] = $item; - $viewRepository = $entityManager->getRepository(EpisodeView::class); break; case ViewType::MOVIE: $criteria["item"] = $item; - $viewRepository = $entityManager->getRepository(MovieView::class); break; } @@ -60,14 +58,16 @@ public function getPage(Request $request, User $user, EntityManagerInterface $en $pagination = new Pagination($page, $count, $this->itemsPerPage, $this->maxPreviousNextPages); if ($dateRange === null) { - $firstPage = $this->sorted($viewRepository->getPaged($criteria, 1, $this->itemsPerPage, $type, $dateRange)); - $lastPage = $this->sorted($viewRepository->getPaged($criteria, $pagination->getLastPage(), $this->itemsPerPage, $type, $dateRange)); + $firstPage = $viewRepository->getPaged($criteria, 1, $this->itemsPerPage, $type, $dateRange); + $lastPage = $viewRepository->getPaged($criteria, $pagination->getLastPage(), $this->itemsPerPage, $type, $dateRange); if (!empty($firstPage) and !empty($lastPage)) { $dateRange = new DateRange($lastPage[count($lastPage) - 1]->getDateTime()->toDate(), $firstPage[0]->getDateTime()->toDate()); } } + $views = $viewRepository->getPaged($criteria, $page, $this->itemsPerPage, $type, $dateRange); + return $this->render("user/history.twig", [ "user" => $user, "dateRange" => $dateRange, @@ -76,22 +76,7 @@ public function getPage(Request $request, User $user, EntityManagerInterface $en "item" => $item ], "pagination" => $pagination, - "entries" => $this->sorted($viewRepository->getPaged($criteria, $page, $this->itemsPerPage, $type, $dateRange)) + "entries" => HistoryEntry::getFromViews($views, $episodeRepository, $movieRepository, new WatchStatsProvider($viewRepository, $user)) ]); } - - private function sorted(array $entries): array - { - usort($entries, function (ViewEntry $entry1, ViewEntry $entry2) { - if ($entry1->getDateTime() < $entry2->getDateTime()) { - return 1; - } elseif ($entry1->getDateTime() > $entry2->getDateTime()) { - return -1; - } else { - return 0; - } - }); - - return $entries; - } } diff --git a/src/main/php/tracky/controller/HomeController.php b/src/main/php/tracky/controller/HomeController.php index 5125838..eb1639d 100644 --- a/src/main/php/tracky/controller/HomeController.php +++ b/src/main/php/tracky/controller/HomeController.php @@ -4,14 +4,14 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -use tracky\model\Episode; -use tracky\model\User; +use tracky\HistoryEntry; use tracky\orm\EpisodeRepository; use tracky\orm\MovieRepository; use tracky\orm\ShowRepository; use tracky\orm\ViewRepository; use tracky\scrobbler\Scrobbler; use tracky\ViewType; +use tracky\watchstats\WatchStatsProvider; class HomeController extends AbstractController { @@ -24,7 +24,7 @@ public function __construct( } #[Route("/", name: "home_page")] - public function home(ShowRepository $showRepository, EpisodeRepository $episodeRepository, MovieRepository $movieRepository, ViewRepository $viewRepository, Scrobbler $scrobbler): Response + public function home(ShowRepository $showRepository, EpisodeRepository $episodeRepository, MovieRepository $movieRepository, ViewRepository $viewRepository, WatchStatsProvider $watchStatsProvider, Scrobbler $scrobbler): Response { $nowWatching = null; $latestWatchedEpisodes = null; @@ -34,9 +34,14 @@ public function home(ShowRepository $showRepository, EpisodeRepository $episodeR $user = $this->getUser(); if ($user !== null) { $nowWatching = $scrobbler->getNowWatching($user); - $latestWatchedEpisodes = $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], $this->maxEpisodes, type: ViewType::EPISODE); - $latestWatchedMovies = $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], $this->maxMovies, type: ViewType::MOVIE); - $nextEpisodes = $this->getNextEpisodes($showRepository, $user); + + $episodeViews = $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], $this->maxEpisodes, type: ViewType::EPISODE); + $movieViews = $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], $this->maxMovies, type: ViewType::MOVIE); + + $latestWatchedEpisodes = HistoryEntry::getFromViews($episodeViews, $episodeRepository, $movieRepository, $watchStatsProvider); + $latestWatchedMovies = HistoryEntry::getFromViews($movieViews, $episodeRepository, $movieRepository, $watchStatsProvider); + + $nextEpisodes = $this->getNextEpisodes($showRepository, $watchStatsProvider); } return $this->render("index.twig", [ @@ -49,41 +54,30 @@ public function home(ShowRepository $showRepository, EpisodeRepository $episodeR ]); } - private function getNextEpisodes(ShowRepository $showRepository, User $user) + private function getNextEpisodes(ShowRepository $showRepository, WatchStatsProvider $watchStatsProvider) { - $latestEpisodes = []; - $nextEpisodes = []; - - foreach ($showRepository->findAllWithEpisodesAndViews($user->getId()) as $show) { - $latestWatchedEpisodes = $show->getLatestWatchedEpisodes($user, 1, true); - if (!empty($latestWatchedEpisodes)) { - $latestEpisodes[] = $latestWatchedEpisodes[0]; - } - } - - usort($latestEpisodes, function ($item1, $item2) { - list(, $item1Timestamp) = $item1; - list(, $item2Timestamp) = $item2; + /** + * @var list + */ + $episodes = []; + foreach ($showRepository->findAllWithEpisodes() as $show) { + $latestWatchedEpisode = $show->getLatestWatchedEpisodes($watchStatsProvider, 1)[0] ?? null; - if ($item1Timestamp === $item2Timestamp) { - return 0; + if ($latestWatchedEpisode === null) { + continue; } - return ($item1Timestamp > $item2Timestamp) ? -1 : 1; - }); - - foreach ($latestEpisodes as $item) { - /** - * @var Episode - */ - $episode = $item[0]; - $nextEpisode = $episode->getNextEpisode(); - if ($nextEpisode !== null) { - $nextEpisodes[] = $nextEpisode; + $nextEpisode = $latestWatchedEpisode[0]->getNextEpisode(); + if ($nextEpisode === null) { + continue; } + + $episodes[] = [$nextEpisode, $latestWatchedEpisode[1]->getLastWatched()]; } - return array_slice($nextEpisodes, 0, $this->maxNextEpisodeShows); + usort($episodes, fn($item1, $item2) => $item2[1] <=> $item1[1]); + + return array_map(fn($item) => $item[0], array_slice($episodes, 0, $this->maxNextEpisodeShows)); } } diff --git a/src/main/php/tracky/controller/MovieController.php b/src/main/php/tracky/controller/MovieController.php index 7fde630..a7ff3fd 100644 --- a/src/main/php/tracky/controller/MovieController.php +++ b/src/main/php/tracky/controller/MovieController.php @@ -15,7 +15,7 @@ use tracky\datetime\DateTime; use tracky\ImageFetcher; use tracky\model\Movie; -use tracky\model\MovieView; +use tracky\model\View; use tracky\orm\MovieRepository; use tracky\orm\ViewRepository; use tracky\ViewType; @@ -55,31 +55,31 @@ public function getMoviesPage(Request $request): Response $sortDirection = self::SORT_ASC; } + if ($user !== null) { + $watchStats = $this->viewRepository->getWatchStatsForUser($user, ViewType::MOVIE); + } else { + $watchStats = null; + } + // Special sorting for playCount and lastPlayed if ($user !== null and ($sortField === "playCount" or $sortField === "lastPlayed")) { - $movies = $this->movieRepository->findAllWithViews($user->getId()); - usort($movies, function (Movie $movie1, Movie $movie2) use ($user, $sortField, $sortDirection) { - $views1 = $movie1->getViewsForUser($user); - $views2 = $movie2->getViewsForUser($user); + $movies = $this->movieRepository->findAll(); + + usort($movies, function (Movie $movie1, Movie $movie2) use ($sortField, $sortDirection, $watchStats) { + $item1 = $watchStats->getStatsForItem($movie1); + $item2 = $watchStats->getStatsForItem($movie2); + + $value1 = 0; + $value2 = 0; switch ($sortField) { case "playCount": - $value1 = count($views1); - $value2 = count($views2); + $value1 = $item1?->getCount() ?? 0; + $value2 = $item2?->getCount() ?? 0; break; case "lastPlayed": - $lastView1 = $views1->last(); - if ($lastView1 === false) { - $lastView1 = null; - } - - $lastView2 = $views2->last(); - if ($lastView2 === false) { - $lastView2 = null; - } - - $value1 = $lastView1?->getDateTime()?->getTimestamp() ?? 0; - $value2 = $lastView2?->getDateTime()?->getTimestamp() ?? 0; + $value1 = $item1?->getLastWatched()?->getTimestamp() ?? 0; + $value2 = $item2?->getLastWatched()?->getTimestamp() ?? 0; break; } @@ -101,7 +101,8 @@ public function getMoviesPage(Request $request): Response "field" => $sortField, "direction" => $sortDirection ], - "movies" => $movies + "movies" => $movies, + "watchStats" => $watchStats ]); } @@ -124,17 +125,23 @@ public function getMovieImage(Movie $movie, ImageFetcher $imageFetcher): Respons #[Route("/movies/{movie}", name: "movies_single_page", methods: ["GET"])] public function getMoviePage(Movie $movie): Response { + $user = $this->getUser(); + if ($user !== null) { + $itemWatchStats = $this->viewRepository->getWatchStatsForUser($user, ViewType::MOVIE)->getStatsForItem($movie); + } else { + $itemWatchStats = null; + } + return $this->render("movie.twig", [ - "movie" => $movie + "movie" => $movie, + "itemWatchStats" => $itemWatchStats ]); } #[Route("/movies/{movie}", name: "movies_remove_movie_action", methods: ["DELETE"])] #[IsGranted("IS_AUTHENTICATED")] - public function removeMovie(Movie $movie, EntityManagerInterface $entityManager): Response + public function removeMovie(Movie $movie, ViewRepository $viewRepository, EntityManagerInterface $entityManager): Response { - $viewRepository = $entityManager->getRepository(MovieView::class); - // Make sure no view exists for this movie if ($viewRepository->count(["item" => $movie->getId()], type: ViewType::MOVIE)) { return $this->json([ @@ -158,12 +165,12 @@ public function addView(Movie $movie, Request $request, EntityManagerInterface $ throw new BadRequestException("Invalid payload"); } - $movieView = new MovieView; - $movieView->setMovie($movie); - $movieView->setUser($this->getUser()); - $movieView->setDateTime($dateTime); + $view = new View; + $view->setItem($movie); + $view->setUser($this->getUser()); + $view->setDateTime($dateTime); - $entityManager->persist($movieView); + $entityManager->persist($view); $entityManager->flush(); return new Response("View added to database"); @@ -171,9 +178,9 @@ public function addView(Movie $movie, Request $request, EntityManagerInterface $ #[Route("/movies/{movie}/views/all", name: "movies_remove_views_action", methods: ["DELETE"])] #[IsGranted("IS_AUTHENTICATED")] - public function removeViewsByEpisode(Movie $movie, EntityManagerInterface $entityManager): Response + public function removeViewsByMovie(Movie $movie, ViewRepository $viewRepository, EntityManagerInterface $entityManager): Response { - $views = $movie->getViewsForUser($this->getUser()); + $views = $viewRepository->findBy(["item" => $movie->getId(), "user" => $this->getUser(), "type" => ViewType::EPISODE->value]); foreach ($views as $view) { $entityManager->remove($view); diff --git a/src/main/php/tracky/controller/ShowController.php b/src/main/php/tracky/controller/ShowController.php index 7469be6..2895687 100644 --- a/src/main/php/tracky/controller/ShowController.php +++ b/src/main/php/tracky/controller/ShowController.php @@ -15,12 +15,13 @@ use tracky\datetime\DateTime; use tracky\ImageFetcher; use tracky\model\Episode; -use tracky\model\EpisodeView; use tracky\model\Season; use tracky\model\Show; +use tracky\model\View; use tracky\orm\ShowRepository; use tracky\orm\ViewRepository; use tracky\ViewType; +use tracky\watchstats\WatchStatsProvider; class ShowController extends AbstractController { @@ -81,10 +82,8 @@ public function getShowOverviewPage(Show $show): Response #[Route("/shows/{show}", name: "shows_show_remove_action", methods: ["DELETE"])] #[IsGranted("IS_AUTHENTICATED")] - public function removeShow(Show $show, EntityManagerInterface $entityManager): Response + public function removeShow(Show $show, ViewRepository $viewRepository, EntityManagerInterface $entityManager): Response { - $viewRepository = $entityManager->getRepository(EpisodeView::class); - // Make sure no episode view exists for this show foreach ($show->getSeasons() as $season) { foreach ($season->getEpisodes() as $episode) { @@ -124,53 +123,53 @@ public function getRandomEpisodesPage(int $show): Response #[Route("/shows/{show}/latest-watched", name: "shows_latest_watched_episodes_page")] #[IsGranted("IS_AUTHENTICATED")] - public function getLatestWatchedEpisodesPage(int $show): Response + public function getLatestWatchedEpisodesPage(int $show, WatchStatsProvider $watchStatsProvider): Response { - $show = $this->showRepository->findByIdWithEpisodesAndViews($show, $this->getUser()->getId()); + $show = $this->showRepository->findByIdWithEpisodes($show); return $this->render("shows/episodes.twig", [ "show" => $show, "title" => "shows.latest-watched-episodes", - "episodes" => $show->getLatestWatchedEpisodes($this->getUser(), $this->maxEpisodes) + "episodes" => array_map(fn($item) => $item[0], $show->getLatestWatchedEpisodes($watchStatsProvider, $this->maxEpisodes)) ]); } #[Route("/shows/{show}/most-watched", name: "shows_most_watched_episodes_page")] #[IsGranted("IS_AUTHENTICATED")] - public function getMostWatchedEpisodesPage(int $show): Response + public function getMostWatchedEpisodesPage(int $show, WatchStatsProvider $watchStatsProvider): Response { - $show = $this->showRepository->findByIdWithEpisodesAndViews($show, $this->getUser()->getId()); + $show = $this->showRepository->findByIdWithEpisodes($show); return $this->render("shows/episodes.twig", [ "show" => $show, "title" => "shows.most-watched-episodes", - "episodes" => $show->getMostOrLeastWatchedEpisodes($this->getUser(), $this->maxEpisodes) + "episodes" => array_map(fn($item) => $item[0], $show->getMostOrLeastWatchedEpisodes($watchStatsProvider, $this->maxEpisodes, false)) ]); } #[Route("/shows/{show}/least-watched", name: "shows_least_watched_episodes_page")] #[IsGranted("IS_AUTHENTICATED")] - public function getLeastWatchedEpisodesPage(int $show): Response + public function getLeastWatchedEpisodesPage(int $show, WatchStatsProvider $watchStatsProvider): Response { - $show = $this->showRepository->findByIdWithEpisodesAndViews($show, $this->getUser()->getId()); + $show = $this->showRepository->findByIdWithEpisodes($show); return $this->render("shows/episodes.twig", [ "show" => $show, "title" => "shows.least-watched-episodes", - "episodes" => $show->getMostOrLeastWatchedEpisodes($this->getUser(), $this->maxEpisodes, true) + "episodes" => array_map(fn($item) => $item[0], $show->getMostOrLeastWatchedEpisodes($watchStatsProvider, $this->maxEpisodes, true)) ]); } #[Route("/shows/{show}/unwatched", name: "shows_unwatched_episodes_page")] #[IsGranted("IS_AUTHENTICATED")] - public function getUnwatchedEpisodesPage(int $show): Response + public function getUnwatchedEpisodesPage(int $show, WatchStatsProvider $watchStatsProvider): Response { - $show = $this->showRepository->findByIdWithEpisodesAndViews($show, $this->getUser()->getId()); + $show = $this->showRepository->findByIdWithEpisodes($show); return $this->render("shows/episodes.twig", [ "show" => $show, "title" => "shows.unwatched-episodes", - "episodes" => $show->getUnwatchedEpisodes($this->getUser()) + "episodes" => $show->getUnwatchedEpisodes($watchStatsProvider) ]); } @@ -209,12 +208,12 @@ public function addView(Show $show, int $seasonNumber, int $episodeNumber, Reque throw new BadRequestException("Invalid payload"); } - $episodeView = new EpisodeView; - $episodeView->setEpisode($episode); - $episodeView->setUser($this->getUser()); - $episodeView->setDateTime($dateTime); + $view = new View; + $view->setItem($episode); + $view->setUser($this->getUser()); + $view->setDateTime($dateTime); - $entityManager->persist($episodeView); + $entityManager->persist($view); $entityManager->flush(); return new Response("View added to database"); @@ -222,7 +221,7 @@ public function addView(Show $show, int $seasonNumber, int $episodeNumber, Reque #[Route("/shows/{show}/seasons/{seasonNumber}/episodes/{episodeNumber}/views/all", name: "shows_remove_episode_view_action", methods: ["DELETE"])] #[IsGranted("IS_AUTHENTICATED")] - public function removeViewsByEpisode(Show $show, int $seasonNumber, int $episodeNumber, EntityManagerInterface $entityManager): Response + public function removeViewsByEpisode(Show $show, int $seasonNumber, int $episodeNumber, ViewRepository $viewRepository, EntityManagerInterface $entityManager): Response { $season = $show->getSeason($seasonNumber); if ($season === null) { @@ -234,7 +233,7 @@ public function removeViewsByEpisode(Show $show, int $seasonNumber, int $episode throw new NotFoundHttpException("Episode not found"); } - $views = $episode->getViewsForUser($this->getUser()); + $views = $viewRepository->findBy(["item" => $episode->getId(), "user" => $this->getUser(), "type" => ViewType::EPISODE->value]); foreach ($views as $view) { $entityManager->remove($view); @@ -259,11 +258,15 @@ public function removeViewById(Show $show, int $seasonNumber, int $episodeNumber throw new NotFoundHttpException("Episode not found"); } - $view = $this->viewRepository->findOneBy(["id" => $entryId, "user" => $this->getUser()], type: ViewType::EPISODE); + $view = $this->viewRepository->findOneBy(["id" => $entryId, "user" => $this->getUser(), "type" => ViewType::EPISODE->value]); if ($view === null) { throw new NotFoundHttpException("View not found"); } + if ($view->getItem() !== $episode->getId()) { + throw new NotFoundHttpException("View item does not match episode"); + } + $entityManager->remove($view); $entityManager->flush(); diff --git a/src/main/php/tracky/controller/UserController.php b/src/main/php/tracky/controller/UserController.php index 48b83d4..67b8452 100644 --- a/src/main/php/tracky/controller/UserController.php +++ b/src/main/php/tracky/controller/UserController.php @@ -9,12 +9,16 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use tracky\HistoryEntry; use tracky\model\User; +use tracky\orm\EpisodeRepository; +use tracky\orm\MovieRepository; use tracky\orm\ShowRepository; use tracky\orm\ViewRepository; use tracky\orm\UserRepository; use tracky\scrobbler\Scrobbler; use tracky\ViewType; +use tracky\watchstats\WatchStatsProvider; class UserController extends AbstractController { @@ -108,22 +112,27 @@ public function getRegisterPage(Request $request, UserPasswordHasherInterface $p } #[Route("/users/{username}", name: "user_profile_page")] - public function getProfilePage(User $user, ViewRepository $viewRepository, Scrobbler $scrobbler): Response + public function getProfilePage(User $user, ViewRepository $viewRepository, EpisodeRepository $episodeRepository, MovieRepository $movieRepository, Scrobbler $scrobbler): Response { + $watchStatsProvider = new WatchStatsProvider($viewRepository, $user); + $episodeViews = $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], 10, type: ViewType::EPISODE); + $movieViews = $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], 10, type: ViewType::MOVIE); + return $this->render("user/profile.twig", [ "user" => $user, "nowWatching" => $scrobbler->getNowWatching($user), - "latestWatchedEpisodes" => $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], 10, type: ViewType::EPISODE), - "latestWatchedMovies" => $viewRepository->findBy(["user" => $user->getId()], ["dateTime" => "desc"], 10, type: ViewType::MOVIE) + "latestWatchedEpisodes" => HistoryEntry::getFromViews($episodeViews, $episodeRepository, $movieRepository, $watchStatsProvider), + "latestWatchedMovies" => HistoryEntry::getFromViews($movieViews, $episodeRepository, $movieRepository, $watchStatsProvider), ]); } #[Route("/users/{username}/show-progress", name: "user_profile_show_progress_page")] - public function getShowProgressForUser(User $user, ShowRepository $showRepository): Response + public function getShowProgressForUser(User $user, ShowRepository $showRepository, ViewRepository $viewRepository): Response { return $this->render("user/show-progress.twig", [ "user" => $user, - "shows" => $showRepository->findAllWithEpisodesAndViews($user->getId()) + "shows" => $showRepository->findAllWithEpisodes(), + "watchStatsCollection" => (new WatchStatsProvider($viewRepository, $user))->getStatsForType(ViewType::EPISODE) ]); } } diff --git a/src/main/php/tracky/model/Episode.php b/src/main/php/tracky/model/Episode.php index a6552ed..d672b20 100644 --- a/src/main/php/tracky/model/Episode.php +++ b/src/main/php/tracky/model/Episode.php @@ -1,13 +1,13 @@ "ASC"])] - private mixed $views; - public function getSeason(): Season { return $this->season; @@ -86,14 +82,6 @@ public function setFirstAired(?Date $firstAired): Episode return $this; } - public function getViewsForUser(User $user) - { - $criteria = Criteria::create(); - $criteria->where(Criteria::expr()->eq("user", $user)); - - return $this->views->matching($criteria); - } - public function getPreviousEpisode(): ?Episode { $season = $this->getSeason(); @@ -128,6 +116,11 @@ public function getNextEpisode(): ?Episode return null; } + public function getViewType(): ViewType + { + return ViewType::EPISODE; + } + public function __toString(): string { return sprintf("%dx%d %s", $this->getSeason()->getNumber(), $this->getNumber(), $this->getTitle()); diff --git a/src/main/php/tracky/model/EpisodeView.php b/src/main/php/tracky/model/EpisodeView.php deleted file mode 100644 index 3452515..0000000 --- a/src/main/php/tracky/model/EpisodeView.php +++ /dev/null @@ -1,30 +0,0 @@ -item; - } - - public function setEpisode(Episode $episode): EpisodeView - { - $this->item = $episode; - return $this; - } - - public function getType(): ViewType - { - return ViewType::EPISODE; - } -} diff --git a/src/main/php/tracky/model/Movie.php b/src/main/php/tracky/model/Movie.php index 3e07a12..7ff16e3 100644 --- a/src/main/php/tracky/model/Movie.php +++ b/src/main/php/tracky/model/Movie.php @@ -1,13 +1,13 @@ "ASC"])] - private mixed $views; - public function getTitle(): string { return $this->title; @@ -76,11 +72,8 @@ public function setYear(?int $year): Movie return $this; } - public function getViewsForUser(User $user) + public function getViewType(): ViewType { - $criteria = Criteria::create(); - $criteria->where(Criteria::expr()->eq("user", $user)); - - return $this->views->matching($criteria); + return ViewType::MOVIE; } } diff --git a/src/main/php/tracky/model/MovieView.php b/src/main/php/tracky/model/MovieView.php deleted file mode 100644 index 73557fb..0000000 --- a/src/main/php/tracky/model/MovieView.php +++ /dev/null @@ -1,30 +0,0 @@ -item; - } - - public function setMovie(Movie $movie): MovieView - { - $this->item = $movie; - return $this; - } - - public function getType(): ViewType - { - return ViewType::MOVIE; - } -} diff --git a/src/main/php/tracky/model/Show.php b/src/main/php/tracky/model/Show.php index c92f8a9..b6c888e 100644 --- a/src/main/php/tracky/model/Show.php +++ b/src/main/php/tracky/model/Show.php @@ -8,6 +8,7 @@ use tracky\model\traits\PosterImage; use tracky\model\traits\DataProvider; use tracky\orm\ShowRepository; +use tracky\watchstats\WatchStatsProvider; #[ORM\Entity(repositoryClass: ShowRepository::class)] #[ORM\Table(name: "shows")] @@ -176,101 +177,93 @@ public function getRandomEpisodes(int $count): array return $randomEpisodes; } - public function getLatestWatchedEpisodes(User $user, int $count, bool $includeTimestamp = false): array + /** + * @return list + */ + public function getLatestWatchedEpisodes(WatchStatsProvider $watchStatsProvider, int $count, bool $includeWatchStats = false): array { - $allEpisodes = []; + $episodes = $this->getWatchedEpisodesSortedByLastWatched($watchStatsProvider); - foreach ($this->getSeasons() as $season) { - foreach ($season->getEpisodes() as $episode) { - $views = $episode->getViewsForUser($user); - $viewCount = count($views); - if (!$viewCount) { - continue; - } + return array_slice($episodes, 0, $count); + } - $allEpisodes[] = [$episode, $views->last()->getDateTime()->getTimestamp()]; - } + /** + * @return list + */ + public function getMostOrLeastWatchedEpisodes(WatchStatsProvider $watchStatsProvider, int $count, bool $leastWatched): array + { + $episodes = $this->getWatchedEpisodes($watchStatsProvider); + + usort($episodes, function($item1, $item2) { + $item1WatchStats = $item1[1]; + $item2WatchStats = $item2[1]; + + return $item2WatchStats->getCount() <=> $item1WatchStats->getCount(); + }); + + if ($leastWatched) { + $episodes = array_reverse($episodes); } - usort($allEpisodes, function ($item1, $item2) { - list(, $item1Timestamp) = $item1; - list(, $item2Timestamp) = $item2; + return array_slice($episodes, 0, $count); + } + /** + * @return list + */ + public function getWatchedEpisodesSortedByLastWatched(WatchStatsProvider $watchStatsProvider): array + { + $episodes = $this->getWatchedEpisodes($watchStatsProvider); - if ($item1Timestamp === $item2Timestamp) { - return 0; - } + usort($episodes, function ($item1, $item2) { + $item1WatchStats = $item1[1]; + $item2WatchStats = $item2[1]; - return ($item1Timestamp > $item2Timestamp) ? -1 : 1; + return $item2WatchStats->getLastWatched() <=> $item1WatchStats->getLastWatched(); }); - if (!$includeTimestamp) { - foreach ($allEpisodes as &$item) { - $item = $item[0]; - } - } - - return array_slice($allEpisodes, 0, $count); + return $episodes; } - public function getMostOrLeastWatchedEpisodes(User $user, int $count, bool $leastWatched = false): array + /** + * @return list + */ + public function getWatchedEpisodes(WatchStatsProvider $watchStatsProvider): array { $allEpisodes = []; foreach ($this->getSeasons() as $season) { foreach ($season->getEpisodes() as $episode) { - $views = $episode->getViewsForUser($user); - $viewCount = count($views); - if (!$viewCount) { + $itemWatchStats = $watchStatsProvider->getItemStats($episode); + if ($itemWatchStats === null or !$itemWatchStats->getCount()) { continue; } - $allEpisodes[] = [$episode, $viewCount, $views->last()->getDateTime()->getTimestamp()]; - } - } - - usort($allEpisodes, function ($item1, $item2) { - list(, $item1Count, $item1Timestamp) = $item1; - list(, $item2Count, $item2Timestamp) = $item2; - - - if ($item1Count === $item2Count) { - if ($item1Timestamp === $item2Timestamp) { - return 0; - } - - return ($item1Timestamp > $item2Timestamp) ? -1 : 1; + $allEpisodes[] = [$episode, $itemWatchStats]; } - - return ($item1Count > $item2Count) ? -1 : 1; - }); - - foreach ($allEpisodes as &$item) { - $item = $item[0]; } - if ($leastWatched) { - $allEpisodes = array_reverse($allEpisodes); - } - - return array_slice($allEpisodes, 0, $count); + return $allEpisodes; } - public function getUnwatchedEpisodes(User $user): array + /** + * @return Episode[] + */ + public function getUnwatchedEpisodes(WatchStatsProvider $watchStatsProvider): array { - $episodes = []; + $unwatchedEpisodes = []; foreach ($this->getSeasons() as $season) { foreach ($season->getEpisodes() as $episode) { - $views = $episode->getViewsForUser($user); - if (count($views)) { + $itemWatchStats = $watchStatsProvider->getItemStats($episode); + if ($itemWatchStats !== null and $itemWatchStats->getCount()) { continue; } - $episodes[] = $episode; + $unwatchedEpisodes[] = $episode; } } - return $episodes; + return $unwatchedEpisodes; } } diff --git a/src/main/php/tracky/model/ViewEntry.php b/src/main/php/tracky/model/View.php similarity index 51% rename from src/main/php/tracky/model/ViewEntry.php rename to src/main/php/tracky/model/View.php index 0e92c20..80612cf 100644 --- a/src/main/php/tracky/model/ViewEntry.php +++ b/src/main/php/tracky/model/View.php @@ -3,14 +3,12 @@ use Doctrine\ORM\Mapping as ORM; use tracky\datetime\DateTime; +use tracky\orm\ViewRepository; use tracky\ViewType; -#[ORM\Entity] +#[ORM\Entity(repositoryClass: ViewRepository::class)] #[ORM\Table(name: "views")] -#[ORM\InheritanceType("SINGLE_TABLE")] -#[ORM\DiscriminatorColumn(name: "type", enumType: ViewType::class, type: "string", columnDefinition: "ENUM('episode', 'movie')")] -#[ORM\DiscriminatorMap([ViewType::EPISODE->value => EpisodeView::class, ViewType::MOVIE->value => MovieView::class])] -abstract class ViewEntry extends BaseEntity +class View extends BaseEntity { #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: "user", referencedColumnName: "id")] @@ -19,6 +17,12 @@ abstract class ViewEntry extends BaseEntity #[ORM\Column(name: "datetime", type: "datetime")] protected DateTime $dateTime; + #[ORM\Column(name: "item", type: "integer")] + protected int $item; + + #[ORM\Column(name: "type", enumType: ViewType::class, type: "string", columnDefinition: "ENUM('episode', 'movie')")] + protected ViewType $type; + public function getUser(): User { return $this->user; @@ -41,5 +45,25 @@ public function setDateTime(DateTime $dateTime): self return $this; } - abstract public function getType(): ViewType; + public function getItem(): int + { + return $this->item; + } + + public function setItem(Episode|Movie $item): self + { + $this->item = $item->getId(); + return $this; + } + + public function getType(): ViewType + { + return $this->type; + } + + public function setType(ViewType $type): self + { + $this->type = $type; + return $this; + } } diff --git a/src/main/php/tracky/orm/EpisodeRepository.php b/src/main/php/tracky/orm/EpisodeRepository.php index 8ebee70..a804c61 100644 --- a/src/main/php/tracky/orm/EpisodeRepository.php +++ b/src/main/php/tracky/orm/EpisodeRepository.php @@ -34,4 +34,20 @@ public function search(string $query, ?int $showId = null) ->getQuery() ->getResult(); } + + /** + * @return Episode[] + */ + public function findByIds(array $ids): array + { + if (empty($ids)) { + return []; + } + + $queryBuilder = $this->createQueryBuilder("episode"); + + $queryBuilder->where($queryBuilder->expr()->in("episode.id", $ids)); + + return $queryBuilder->getQuery()->getResult(); + } } diff --git a/src/main/php/tracky/orm/EpisodeViewRepository.php b/src/main/php/tracky/orm/EpisodeViewRepository.php deleted file mode 100644 index a0913f5..0000000 --- a/src/main/php/tracky/orm/EpisodeViewRepository.php +++ /dev/null @@ -1,13 +0,0 @@ -getEntityManager()->createQuery(" - SELECT movie, view - FROM tracky\model\Movie movie - LEFT JOIN movie.views view - LEFT JOIN view.user user - WHERE user.id IS NULL OR user.id = :userId - "); + if (empty($ids)) { + return []; + } - $query->setParameter("userId", $userId); + $queryBuilder = $this->createQueryBuilder("movie"); - return $query->getResult(); + $queryBuilder->where($queryBuilder->expr()->in("movie.id", $ids)); + + return $queryBuilder->getQuery()->getResult(); } } diff --git a/src/main/php/tracky/orm/MovieViewRepository.php b/src/main/php/tracky/orm/MovieViewRepository.php deleted file mode 100644 index a9c5c71..0000000 --- a/src/main/php/tracky/orm/MovieViewRepository.php +++ /dev/null @@ -1,13 +0,0 @@ -getOneOrNullResult(); } - - /** - * @return Show[] - */ - public function findAllWithEpisodesAndViews(int $userId): array - { - $query = $this->getEntityManager()->createQuery(" - SELECT show, season, episode, view - FROM tracky\model\Show show - LEFT JOIN show.seasons season - LEFT JOIN season.episodes episode - LEFT JOIN episode.views view - LEFT JOIN view.user user - WHERE user.id IS NULL OR user.id = :userId - ORDER BY show.title ASC - "); - - $query->setParameter("userId", $userId); - - $query->setFetchMode(Show::class, "seasons", ClassMetadataInfo::FETCH_EAGER); - $query->setFetchMode(Season::class, "episodes", ClassMetadataInfo::FETCH_EAGER); - $query->setFetchMode(Episode::class, "views", ClassMetadataInfo::FETCH_EAGER); - - return $query->getResult(); - } } diff --git a/src/main/php/tracky/orm/ViewRepository.php b/src/main/php/tracky/orm/ViewRepository.php index 3b80d2f..c926e8f 100644 --- a/src/main/php/tracky/orm/ViewRepository.php +++ b/src/main/php/tracky/orm/ViewRepository.php @@ -5,14 +5,16 @@ use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use tracky\datetime\DateRange; -use tracky\model\ViewEntry; +use tracky\model\User; +use tracky\model\View; use tracky\ViewType; +use tracky\watchstats\WatchStatsCollection; class ViewRepository extends ServiceEntityRepository { - public function __construct(ManagerRegistry $registry, string $entityClass = ViewEntry::class) + public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, $entityClass); + parent::__construct($registry, View::class); } private function getQueryBuilder(string $select, array $criteria, ?array $orderBy = null, $limit = null, $offset = null, ?ViewType $type = null, ?DateRange $dateRange = null): QueryBuilder @@ -38,7 +40,7 @@ private function getQueryBuilder(string $select, array $criteria, ?array $orderB if ($type !== null) { $queryBuilder - ->andWhere("view INSTANCE OF :type") + ->andWhere("view.type = :type") ->setParameter(":type", $type->value); } @@ -59,6 +61,9 @@ private function getQueryBuilder(string $select, array $criteria, ?array $orderB return $queryBuilder; } + /** + * @return View[] + */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null, ?ViewType $type = null, ?DateRange $dateRange = null): array { $queryBuilder = $this->getQueryBuilder("view", $criteria, $orderBy, $limit, $offset, $type, $dateRange); @@ -66,7 +71,7 @@ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $ return $queryBuilder->getQuery()->getResult(); } - public function findOneBy(array $criteria, ?array $orderBy = null, ?ViewType $type = null): ?object + public function findOneBy(array $criteria, ?array $orderBy = null, ?ViewType $type = null): ?View { $items = $this->findBy($criteria, $orderBy, 1, type: $type); @@ -77,6 +82,22 @@ public function findOneBy(array $criteria, ?array $orderBy = null, ?ViewType $ty return $items[0]; } + /** + * @return int[] + */ + public function getItemIdsBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null, ?ViewType $type = null, ?DateRange $dateRange = null): array + { + $queryBuilder = $this->getQueryBuilder("view", $criteria, $orderBy, $limit, $offset, $type, $dateRange); + + $ids = []; + + foreach ($queryBuilder->getQuery()->getResult() as $view) { + $ids[] = $view->getItem(); + } + + return $ids; + } + public function count(array $criteria = [], ?ViewType $type = null, ?DateRange $dateRange = null): int { $queryBuilder = $this->getQueryBuilder(select: "count(view.id)", criteria: $criteria, type: $type, dateRange: $dateRange); @@ -90,4 +111,23 @@ public function getPaged(array $criteria, int $page, int $perPage, ?ViewType $ty return $this->findBy($criteria, ["dateTime" => "desc"], $perPage, $offset, $type, $dateRange); } + + public function getWatchStatsForUser(User $user, ViewType $viewType): WatchStatsCollection + { + $rows = $this->createQueryBuilder("view") + ->select(" + view.item, + COUNT(view.id) AS watchCount, + MAX(view.dateTime) AS lastWatched + ") + ->where("view.user = :user") + ->andWhere("view.type = :type") + ->groupBy("view.item") + ->setParameter("user", $user) + ->setParameter("type", $viewType->value) + ->getQuery() + ->getArrayResult(); + + return WatchStatsCollection::fromQueryRows($rows); + } } diff --git a/src/main/php/tracky/scrobbler/Scrobbler.php b/src/main/php/tracky/scrobbler/Scrobbler.php index 7303953..7fa80d4 100644 --- a/src/main/php/tracky/scrobbler/Scrobbler.php +++ b/src/main/php/tracky/scrobbler/Scrobbler.php @@ -5,12 +5,11 @@ use tracky\dataprovider\Helper; use tracky\datetime\DateTime; use tracky\model\Episode; -use tracky\model\EpisodeView; use tracky\model\Movie; -use tracky\model\MovieView; use tracky\model\ScrobbleQueueItem; use tracky\model\Show; use tracky\model\User; +use tracky\model\View; use tracky\orm\MovieRepository; use tracky\orm\ShowRepository; use UnexpectedValueException; @@ -132,12 +131,12 @@ private function addEpisodeView(array $json, DateTime $dateTime, User $user): vo { $episode = $this->getEpisode($json); - $episodeView = new EpisodeView; - $episodeView->setEpisode($episode); - $episodeView->setUser($user); - $episodeView->setDateTime($dateTime); + $view = new View; + $view->setItem($episode); + $view->setUser($user); + $view->setDateTime($dateTime); - $this->entityManager->persist($episodeView); + $this->entityManager->persist($view); $this->entityManager->flush(); } @@ -145,12 +144,12 @@ private function addMovieView(array $json, DateTime $dateTime, User $user): void { $movie = $this->getMovie($json); - $movieView = new MovieView; - $movieView->setMovie($movie); - $movieView->setUser($user); - $movieView->setDateTime($dateTime); + $view = new View; + $view->setItem($movie); + $view->setUser($user); + $view->setDateTime($dateTime); - $this->entityManager->persist($movieView); + $this->entityManager->persist($view); $this->entityManager->flush(); } diff --git a/src/main/php/tracky/watchstats/ItemWatchStats.php b/src/main/php/tracky/watchstats/ItemWatchStats.php new file mode 100644 index 0000000..b7b8514 --- /dev/null +++ b/src/main/php/tracky/watchstats/ItemWatchStats.php @@ -0,0 +1,22 @@ +count; + } + + public function getLastWatched(): DateTime + { + return $this->lastWatched; + } +} diff --git a/src/main/php/tracky/watchstats/WatchStatsCollection.php b/src/main/php/tracky/watchstats/WatchStatsCollection.php new file mode 100644 index 0000000..315c387 --- /dev/null +++ b/src/main/php/tracky/watchstats/WatchStatsCollection.php @@ -0,0 +1,42 @@ + + */ + private array $data; + + public function __construct(array $data = []) + { + $this->data = $data; + } + + public static function fromQueryRows(array $rows) + { + $data = []; + + foreach ($rows as $row) { + $data[(int) $row["item"]] = new ItemWatchStats(count: (int) $row["watchCount"], lastWatched: new DateTime($row["lastWatched"])); + } + + return new static($data); + } + + public function getStatsForItem(Episode|Movie|int $item): ?ItemWatchStats + { + if ($item instanceof BaseEntity) { + $id = $item->getId(); + } else { + $id = $item; + } + + return $this->data[$id] ?? null; + } +} diff --git a/src/main/php/tracky/watchstats/WatchStatsProvider.php b/src/main/php/tracky/watchstats/WatchStatsProvider.php new file mode 100644 index 0000000..50b48f3 --- /dev/null +++ b/src/main/php/tracky/watchstats/WatchStatsProvider.php @@ -0,0 +1,57 @@ +, WatchStatsCollection> + */ + private array $perTypeCollection; + + public function __construct( + private readonly ViewRepository $viewRepository, + + #[Autowire(service: "Symfony\\Bundle\\SecurityBundle\\Security")] + User|Security $userOrSecurity + ) + { + if ($userOrSecurity instanceof Security) { + $this->user = $userOrSecurity->getUser(); + } else { + $this->user = $userOrSecurity; + } + } + + public function getStatsForType(ViewType $type): ?WatchStatsCollection + { + if ($this->user === null) { + return null; + } + + if (!isset($this->perTypeCollection[$type->value])) { + $this->perTypeCollection[$type->value] = $this->viewRepository->getWatchStatsForUser($this->user, $type); + } + + return $this->perTypeCollection[$type->value]; + } + + public function getItemStats(Episode|Movie|int $item): ?ItemWatchStats + { + return $this->getStatsForType($item->getViewType())?->getStatsForItem($item); + } + + public function getItemStatsByView(View $view): ?ItemWatchStats + { + return $this->getStatsForType($view->getType())?->getStatsForItem($view->getItem()); + } +} diff --git a/src/main/resources/assets/images/missing-image-poster.svg b/src/main/resources/assets/images/missing-image-poster.svg new file mode 100644 index 0000000..4433157 --- /dev/null +++ b/src/main/resources/assets/images/missing-image-poster.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NO IMAGE + + + + AVAILABLE + + + diff --git a/src/main/resources/assets/images/missing-image-wide.svg b/src/main/resources/assets/images/missing-image-wide.svg new file mode 100644 index 0000000..8697d91 --- /dev/null +++ b/src/main/resources/assets/images/missing-image-wide.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NO IMAGE + + + + AVAILABLE + + + diff --git a/src/main/resources/assets/script/main.ts b/src/main/resources/assets/script/main.ts index f510b26..6ac485d 100644 --- a/src/main/resources/assets/script/main.ts +++ b/src/main/resources/assets/script/main.ts @@ -3,21 +3,12 @@ import "./history"; import "./library-management"; import "./view"; -document.addEventListener("DOMContentLoaded", () => { - let lazyBackgroundObserver = new IntersectionObserver((entries) => { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - let element = entry.target as HTMLElement; - element.style.backgroundImage = `url(${element.dataset.imageUrl})`; - lazyBackgroundObserver.unobserve(element); - } - }); - }); - - document.querySelectorAll(".lazy-background").forEach(function (lazyBackground) { - lazyBackgroundObserver.observe(lazyBackground); - }); +// @ts-ignore +import missingImagePoster from "../images/missing-image-poster.svg"; +// @ts-ignore +import missingImageWide from "../images/missing-image-wide.svg"; +document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".season-dropdown").forEach((dropdown) => { dropdown.addEventListener("shown.bs.dropdown", () => { let menu = dropdown.querySelector(".dropdown-menu"); @@ -29,4 +20,16 @@ document.addEventListener("DOMContentLoaded", () => { }); }); }); + + document.querySelectorAll("img.image-poster").forEach((element) => { + element.addEventListener("error", () => { + (element as HTMLImageElement).src = missingImagePoster; + }); + }); + + document.querySelectorAll("img.image-wide").forEach((element) => { + element.addEventListener("error", () => { + (element as HTMLImageElement).src = missingImageWide; + }); + }); }); diff --git a/src/main/resources/assets/style/main.scss b/src/main/resources/assets/style/main.scss index 102335a..5461ceb 100644 --- a/src/main/resources/assets/style/main.scss +++ b/src/main/resources/assets/style/main.scss @@ -64,17 +64,13 @@ html { background: $navbar-bg; } -.scaled-image { - background-repeat: no-repeat; - background-position: center; - border-radius: var(--bs-border-radius) 0 0 var(--bs-border-radius); +.image-poster, .image-wide { + transition: all 0.3s ease; } -@media (max-width: 767px) { - .scaled-image { - height: 300px; - border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0; - } +.image-poster:hover, .image-wide:hover { + transform: scale(1.05); + border-radius: var(--bs-border-radius); } .app-layout { @@ -105,7 +101,36 @@ html { } .show-progress-image { - background-size: contain; + display: block; +} + +.show-progress-image img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +@media (min-width: 768px) { + .show-progress-image { + aspect-ratio: 2 / 3; + } + + .show-progress-image img { + border-top-left-radius: var(--bs-border-radius); + border-bottom-left-radius: var(--bs-border-radius); + } +} + +@media (max-width: 767px) { + .show-progress-image { + aspect-ratio: auto; + } + + .show-progress-image img { + border-top-left-radius: var(--bs-border-radius); + border-top-right-radius: var(--bs-border-radius); + } } .show-progress-seasons { diff --git a/templates/components/episode-card-small.twig b/templates/components/episode-card-small.twig index fd8e7f6..d91d4a0 100644 --- a/templates/components/episode-card-small.twig +++ b/templates/components/episode-card-small.twig @@ -1,7 +1,7 @@ {% set linkUrl = path("shows_season_page", {"show": episode.season.show.id, "number": episode.season.number}) ~ "#e" ~ episode.number %}
- +
diff --git a/templates/components/episode-card-wide.twig b/templates/components/episode-card-wide.twig index c7250b8..db650b4 100644 --- a/templates/components/episode-card-wide.twig +++ b/templates/components/episode-card-wide.twig @@ -1,6 +1,8 @@
-
+
+ +
diff --git a/templates/components/history-entry-card-small.twig b/templates/components/history-entry-card-small.twig index f65a084..15154c5 100644 --- a/templates/components/history-entry-card-small.twig +++ b/templates/components/history-entry-card-small.twig @@ -1,39 +1,40 @@ -{% set metadata = {"entry-type": entry.type.value, "entry-id": entry.id} %} +{# Note: entry is of type HistoryEntry #} +{% set metadata = {"entry-type": entry.view.type.value, "entry-id": entry.view.id, "views": entry.viewCount} %} -{% if entry.type.value == "episode" %} - {% set episode = entry.episode %} +{% if entry.view.type.value == "episode" %} + {% set episode = entry.item %} {% set season = episode.season %} {% set show = season.show %} {% set linkUrl = path("shows_season_page", {"show": show.id, "number": season.number}) ~ "#e" ~ episode.number %} {% set imageUrl = path("shows_episode_image", {"show": show.id, "seasonNumber": season.number, "episodeNumber": episode.number}) %} - {% set views = episode.viewsForUser(user) %} - {% set metadata = metadata|merge({"views": views|length, "show": show.id, "season": season.number, "episode": episode.number}) %} -{% elseif entry.type.value == "movie" %} - {% set movie = entry.movie %} + {% set imageType = "wide" %} + {% set metadata = metadata|merge({"show": show.id, "season": season.number, "episode": episode.number}) %} +{% elseif entry.view.type.value == "movie" %} + {% set movie = entry.item %} {% set linkUrl = path("movies_single_page", {"movie": movie.id}) %} {% set imageUrl = path("movies_get_image", {"movie": movie.id}) %} - {% set views = movie.viewsForUser(user) %} - {% set metadata = metadata|merge({"views": views|length, "movie": movie.id}) %} + {% set imageType = "poster" %} + {% set metadata = metadata|merge({"movie": movie.id}) %} {% endif %}
- +
- {% if entry.type.value == "episode" %} + {% if entry.view.type.value == "episode" %}
{{ season.number }}x{{ episode.number }} {{ episode.title|default("unknown-title"|trans) }}
- {% elseif entry.type.value == "movie" %} + {% elseif entry.view.type.value == "movie" %}
{{ movie.title }}
{% endif %} -
{{ entry.datetime|format_datetime }}
+
{{ entry.view.datetime|format_datetime }}
- {% if user == app.user %} + {% if entry.view.user == app.user %} {% endif %}
diff --git a/templates/components/history-entry-list-small.twig b/templates/components/history-entry-list-small.twig index f3274e9..0067cd8 100644 --- a/templates/components/history-entry-list-small.twig +++ b/templates/components/history-entry-list-small.twig @@ -1,7 +1,7 @@
{% for entry in entries %}
- {% include "components/history-entry-card-small.twig" with {"entry": entry} %} + {% include "components/history-entry-card-small.twig" with {"entry": entry} only %}
{% endfor %}
diff --git a/templates/components/movie-card-small.twig b/templates/components/movie-card-small.twig index 94cc635..3e7c35f 100644 --- a/templates/components/movie-card-small.twig +++ b/templates/components/movie-card-small.twig @@ -1,7 +1,7 @@ {% set linkUrl = path("movies_single_page", {"movie": movie.id}) %}
- +
@@ -14,12 +14,10 @@ {{ movie.year }} - {% if is_granted("IS_AUTHENTICATED") %} - {% set viewsForUser = movie.getViewsForUser(app.user) %} - {% if viewsForUser|length %} - {{ "views.views"|trans({"%count%": viewsForUser|length}) }} - {{ viewsForUser.last.dateTime|format_datetime }} - {% endif %} + {% set itemWatchStats = watchStatsProvider.getItemStats(movie) %} + {% if itemWatchStats %} + {{ "views.views"|trans({"%count%": itemWatchStats.count}) }} + {{ itemWatchStats.lastWatched|format_datetime }} {% endif %}
diff --git a/templates/components/movie-card-wide.twig b/templates/components/movie-card-wide.twig index 4bb9dcc..be62ba6 100644 --- a/templates/components/movie-card-wide.twig +++ b/templates/components/movie-card-wide.twig @@ -1,6 +1,8 @@
-
+
+ +
{{ movie.title }}
diff --git a/templates/components/now-watching-episode.twig b/templates/components/now-watching-episode.twig index 4988683..5d4fdcc 100644 --- a/templates/components/now-watching-episode.twig +++ b/templates/components/now-watching-episode.twig @@ -1,25 +1,33 @@ {% set linkUrl = path("shows_season_page", {"show": episode.season.show.id, "number": episode.season.number}) ~ "#e" ~ episode.number %}
- -
+
+
-
- {{ episode.title|default("unknown-title"|trans) }} -
-
{{ "shows.episode"|trans({"%number%": episode.season.number ~ "x" ~ episode.number}) }}
-
- {% if episode.runtime is not null %} - {{ "runtime"|trans({"%runtime%": episode.runtime}) }} - {% endif %} - {% if episode.firstAired is not null %} - {{ episode.firstAired|format_date }} - {% endif %} - {% include "components/view.twig" with {"type": "episode", "item": episode} only %} +
+
+
{{ episode.title|default("unknown-title"|trans) }}
+ + {{ "shows.episode"|trans({"%number%": episode.number}) }} + {% if episode.firstAired is not null %} + • {{ episode.firstAired|format_date }} + {% endif %} + {% if episode.runtime is not null %} + • {{ "runtime"|trans({"%runtime%": episode.runtime}) }} + {% endif %} + +
+ + {% include "components/view-buttons.twig" with {"type": "episode", "item": episode} only %}
+
{{ episode.plot }}
+ +
+ {% include "components/view-last-watched.twig" with {"type": "episode", "item": episode} only %} +
diff --git a/templates/components/now-watching-movie.twig b/templates/components/now-watching-movie.twig index 1c960aa..2d9a073 100644 --- a/templates/components/now-watching-movie.twig +++ b/templates/components/now-watching-movie.twig @@ -1,27 +1,35 @@ {% set linkUrl = path("movies_single_page", {"movie": movie.id}) %}
- -
+
+
-
- {{ movie.title }} -
-
- {% if movie.tagline is not null %} - {{ movie.tagline }} - {% endif %} +
+
+
{{ movie.title }}
+ {% if movie.tagline is not null %} + {{ movie.tagline }} + {% endif %} + + + {% if movie.runtime is not null %} + {{ "runtime"|trans({"%runtime%": movie.runtime}) }} • + {% endif %} - {% if movie.runtime is not null %} - {{ "runtime"|trans({"%runtime%": movie.runtime}) }} - {% endif %} + {{ movie.year }} + +
- {{ movie.year }} - {% include "components/view.twig" with {"type": "movie", "item": movie} only %} + {% include "components/view-buttons.twig" with {"type": "movie", "item": movie} only %}
+
{{ movie.plot }}
+ +
+ {% include "components/view-last-watched.twig" with {"type": "movie", "item": movie} only %} +
diff --git a/templates/components/season-card-small.twig b/templates/components/season-card-small.twig index e897ddd..ae57d21 100644 --- a/templates/components/season-card-small.twig +++ b/templates/components/season-card-small.twig @@ -1,7 +1,7 @@ {% set linkUrl = path("shows_season_page", {"show": season.show.id, "number": season.number}) %}
- +
diff --git a/templates/components/show-card-small.twig b/templates/components/show-card-small.twig index 296535b..0b77c1a 100644 --- a/templates/components/show-card-small.twig +++ b/templates/components/show-card-small.twig @@ -1,7 +1,7 @@ {% set linkUrl = path("shows_show_page", {"show": show.id}) %}
- +
diff --git a/templates/components/view-last-watched.twig b/templates/components/view-last-watched.twig index e7e1a25..9bcb6f8 100644 --- a/templates/components/view-last-watched.twig +++ b/templates/components/view-last-watched.twig @@ -1,6 +1,6 @@ {% if is_granted("IS_AUTHENTICATED") %} - {% set viewsForUser = item.getViewsForUser(app.user) %} - {% if viewsForUser|length %} - {{ "views.last-watched"|trans }}: {{ viewsForUser.last.dateTime|format_datetime }} ({{ "views.views"|trans({"%count%": viewsForUser|length}) }}) + {% set itemWatchStats = watchStatsProvider.getItemStats(item) %} + {% if itemWatchStats %} + {{ "views.last-watched"|trans }}: {{ itemWatchStats.lastWatched|format_datetime }} ({{ "views.views"|trans({"%count%": itemWatchStats.count}) }}) {% endif %} {% endif %} diff --git a/templates/index.twig b/templates/index.twig index 37cc2a5..3af3037 100644 --- a/templates/index.twig +++ b/templates/index.twig @@ -5,20 +5,22 @@ {% block content %} {% if app.user %} {% if nowWatching %} -

{{ "now-watching"|trans }}

+
+

{{ "now-watching"|trans }}

- {% if nowWatching.entry.className == "Episode" %} - {% include "components/now-watching-episode.twig" with {"episode": nowWatching.entry, "duration": nowWatching.duration, "progress": nowWatching.progress} %} - {% elseif nowWatching.entry.className == "Movie" %} - {% include "components/now-watching-movie.twig" with {"movie": nowWatching.entry, "duration": nowWatching.duration, "progress": nowWatching.progress} %} - {% endif %} + {% if nowWatching.entry.className == "Episode" %} + {% include "components/now-watching-episode.twig" with {"episode": nowWatching.entry, "duration": nowWatching.duration, "progress": nowWatching.progress} %} + {% elseif nowWatching.entry.className == "Movie" %} + {% include "components/now-watching-movie.twig" with {"movie": nowWatching.entry, "duration": nowWatching.duration, "progress": nowWatching.progress} %} + {% endif %} +
{% endif %}

{{ "shows.latest-watched-episodes"|trans }}

{% if latestWatchedEpisodes %} - {% include "components/history-entry-list-small.twig" with {"entries": latestWatchedEpisodes, "user": app.user} %} + {% include "components/history-entry-list-small.twig" with {"entries": latestWatchedEpisodes} %} {% else %}
{{ "shows.no-episodes-available"|trans }}
{% endif %} @@ -28,7 +30,7 @@

{{ "movies.latest-watched-movies"|trans }}

{% if latestWatchedMovies %} - {% include "components/history-entry-list-small.twig" with {"entries": latestWatchedMovies, "user": app.user} %} + {% include "components/history-entry-list-small.twig" with {"entries": latestWatchedMovies} %} {% else %}
{{ "movies.no-movies-available"|trans }}
{% endif %} diff --git a/templates/library-management/add-items.twig b/templates/library-management/add-items.twig index f58a5b0..e89141e 100644 --- a/templates/library-management/add-items.twig +++ b/templates/library-management/add-items.twig @@ -35,7 +35,7 @@ {% for result in results %}
- +
{{ result.title }}
{{ result.year }}
diff --git a/templates/movie.twig b/templates/movie.twig index 753f798..a026f44 100644 --- a/templates/movie.twig +++ b/templates/movie.twig @@ -5,7 +5,7 @@ {% block content %}
- +
diff --git a/templates/movies.twig b/templates/movies.twig index c4f261e..19e6d89 100644 --- a/templates/movies.twig +++ b/templates/movies.twig @@ -41,7 +41,7 @@
{% if movies %} - {% include "components/movie-list-small.twig" with {"movies": movies} %} + {% include "components/movie-list-small.twig" with {"movies": movies, "watchStats": watchStats} only %} {% else %}
{{ "movies.no-movies-available"|trans }}
{% endif %} diff --git a/templates/shows/show.twig b/templates/shows/show.twig index 74bf813..eb826a5 100644 --- a/templates/shows/show.twig +++ b/templates/shows/show.twig @@ -23,7 +23,7 @@ {{ _self.navItem("shows_latest_watched_episodes_page", {"show": show.id}, "shows.latest-watched-episodes"|trans) }} {{ _self.navItem("shows_most_watched_episodes_page", {"show": show.id}, "shows.most-watched-episodes"|trans) }} {{ _self.navItem("shows_least_watched_episodes_page", {"show": show.id}, "shows.least-watched-episodes"|trans) }} - {{ _self.navItem("shows_unwatched_episodes_page", {"show": show.id}, "shows.unwatched-episodes"|trans({"%count%": show.getUnwatchedEpisodes(app.user)|length})) }} + {{ _self.navItem("shows_unwatched_episodes_page", {"show": show.id}, "shows.unwatched-episodes"|trans({"%count%": show.getUnwatchedEpisodes(watchStatsProvider)|length})) }} {% endif %} diff --git a/templates/user/show-progress.twig b/templates/user/show-progress.twig index a374da2..685ee3f 100644 --- a/templates/user/show-progress.twig +++ b/templates/user/show-progress.twig @@ -17,7 +17,9 @@
- + + +

{{ show.title }}

@@ -31,7 +33,7 @@ {% for season in show.seasons %} {% set perEpisodePercentage = 100 / totalEpisodes %} {% for episode in season.episodes %} - {% set watched = episode.getViewsForUser(user)|length > 0 %} + {% set watched = watchStatsCollection.getStatsForItem(episode)?.count > 0 %} {% if watched %} {% set watchedEpisodes = watchedEpisodes + 1 %} {% endif %} @@ -59,7 +61,7 @@ {% set perEpisodePercentage = 100 / totalEpisodes %} {% for episode in season.episodes %} - {% set watched = episode.getViewsForUser(user)|length > 0 %} + {% set watched = watchStatsCollection.getStatsForItem(episode)?.count > 0 %} {% if watched %} {% set watchedEpisodes = watchedEpisodes + 1 %} {% endif %}