diff --git a/docs/openapi.json b/docs/openapi.json index c6b1a92c..d1dbd1ff 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -506,6 +506,350 @@ } ] } + }, + "\/users\/{username}\/played\/movies": { + "get": { + "tags": [ + "Played" + ], + "summary": "Get played movies of user", + "description": "Get all played movies and their watch dates.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "description": "Search term", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "Page", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Limit", + "required": false, + "schema": { + "type": "integer", + "default": 24 + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Sort by", + "required": false, + "schema": { + "type": "string", + "default": "title", + "enum": [ + "addedAt", + "rating", + "releaseDate", + "runtime", + "title" + ] + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order", + "required": false, + "schema": { + "type": "string", + "default": "asc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "releaseYear", + "in": "query", + "description": "Release year", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "played": { + "type": "array", + "items": { + "type": "object", + "properties": { + "movie": { + "$ref": "#/components/schemas/movie" + }, + "watchedDates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "$ref": "#/components/schemas/dateNullable" + }, + "plays": { + "$ref": "#/components/schemas/plays" + }, + "comment": { + "$ref": "#/components/schemas/comment" + } + } + } + } + } + } + }, + "currentPage": { + "type": "integer", + "example": 1 + }, + "maxPage": { + "type": "integer", + "example": 10 + } + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "authToken": [] + } + ] + }, + "post": { + "tags": [ + "Played" + ], + "summary": "Add movie plays to user", + "description": "Create or update the provided watch dates for the specified movies.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "movaryId": { + "$ref": "#/components/schemas/id" + }, + "watchDates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "watchedAt": { + "$ref": "#/components/schemas/dateNullable" + }, + "plays": { + "$ref": "#/components/schemas/playsOptional" + }, + "comment": { + "$ref": "#/components/schemas/commentOptional" + } + } + } + } + } + } + } + } + } + }, + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "authToken": [] + } + ] + }, + "put": { + "tags": [ + "Played" + ], + "summary": "Replace movie plays for user", + "description": "Create or replace the provided watch dates for the specified movies.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "movaryId": { + "$ref": "#/components/schemas/id" + }, + "watchDates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "watchedAt": { + "$ref": "#/components/schemas/dateNullable" + }, + "plays": { + "$ref": "#/components/schemas/plays" + }, + "comment": { + "$ref": "#/components/schemas/comment" + } + } + } + } + } + } + } + } + } + }, + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "authToken": [] + } + ] + }, + "delete": { + "tags": [ + "Played" + ], + "summary": "Delete movie plays from user", + "description": "Delete all watch dates of specified movies if no specific watch dates are provided.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "movaryId": { + "$ref": "#/components/schemas/id" + }, + "watchDates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/dateNullable" + }, + "required": false + } + } + } + } + } + } + }, + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "authToken": [] + } + ] + } } }, "components": { diff --git a/settings/routes.php b/settings/routes.php index b806d56f..844cb74d 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -68,18 +68,36 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/settings/account/security', [Web\SettingsController::class, 'renderSecurityAccountPage'], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('GET', '/settings/account/data', [Web\SettingsController::class, 'renderDataAccountPage'], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('GET', '/settings/server/general', [Web\SettingsController::class, 'renderServerGeneralPage'], [Web\Middleware\UserIsAuthenticated::class]); - $routes->add('GET', '/settings/server/jobs', [Web\SettingsController::class, 'renderServerJobsPage'], [Web\Middleware\UserIsAuthenticated::class, Web\Middleware\UserIsAdmin::class]); + $routes->add('GET', '/settings/server/jobs', [Web\SettingsController::class, 'renderServerJobsPage'], [ + Web\Middleware\UserIsAuthenticated::class, + Web\Middleware\UserIsAdmin::class + ]); $routes->add('POST', '/settings/server/general', [Web\SettingsController::class, 'updateServerGeneral'], [ Web\Middleware\UserIsAuthenticated::class, Web\Middleware\UserIsAdmin::class ]); - $routes->add('GET', '/settings/server/users', [Web\SettingsController::class, 'renderServerUsersPage'], [Web\Middleware\UserIsAuthenticated::class, Web\Middleware\UserIsAdmin::class]); - $routes->add('GET', '/settings/server/email', [Web\SettingsController::class, 'renderServerEmailPage'], [Web\Middleware\UserIsAuthenticated::class, Web\Middleware\UserIsAdmin::class]); - $routes->add('POST', '/settings/server/email', [Web\SettingsController::class, 'updateServerEmail'], [Web\Middleware\UserIsAuthenticated::class, Web\Middleware\UserIsAdmin::class]); - $routes->add('POST', '/settings/server/email-test', [Web\SettingsController::class, 'sendTestEmail'], [Web\Middleware\UserIsAuthenticated::class, Web\Middleware\UserIsAdmin::class]); + $routes->add('GET', '/settings/server/users', [Web\SettingsController::class, 'renderServerUsersPage'], [ + Web\Middleware\UserIsAuthenticated::class, + Web\Middleware\UserIsAdmin::class + ]); + $routes->add('GET', '/settings/server/email', [Web\SettingsController::class, 'renderServerEmailPage'], [ + Web\Middleware\UserIsAuthenticated::class, + Web\Middleware\UserIsAdmin::class + ]); + $routes->add('POST', '/settings/server/email', [Web\SettingsController::class, 'updateServerEmail'], [ + Web\Middleware\UserIsAuthenticated::class, + Web\Middleware\UserIsAdmin::class + ]); + $routes->add('POST', '/settings/server/email-test', [Web\SettingsController::class, 'sendTestEmail'], [ + Web\Middleware\UserIsAuthenticated::class, + Web\Middleware\UserIsAdmin::class + ]); $routes->add('POST', '/settings/account', [Web\SettingsController::class, 'updateGeneral'], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('POST', '/settings/account/security/update-password', [Web\SettingsController::class, 'updatePassword'], [Web\Middleware\UserIsAuthenticated::class]); - $routes->add('POST', '/settings/account/security/create-totp-uri', [Web\TwoFactorAuthenticationController::class, 'createTotpUri'], [Web\Middleware\UserIsAuthenticated::class]); + $routes->add('POST', '/settings/account/security/create-totp-uri', [ + Web\TwoFactorAuthenticationController::class, + 'createTotpUri' + ], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('POST', '/settings/account/security/disable-totp', [Web\TwoFactorAuthenticationController::class, 'disableTotp'], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('POST', '/settings/account/security/enable-totp', [Web\TwoFactorAuthenticationController::class, 'enableTotp'], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('GET', '/settings/account/export/csv/{exportType:.+}', [Web\ExportController::class, 'getCsvExport'], [Web\Middleware\UserIsAuthenticated::class]); @@ -148,8 +166,14 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/directors', [Web\DirectorsController::class, 'renderPage']); $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}', [Web\Movie\MovieController::class, 'renderPage']); $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/persons/{id:\d+}', [Web\PersonController::class, 'renderPage']); - $routes->add('DELETE', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/history', [Web\HistoryController::class, 'deleteHistoryEntry'], [Web\Middleware\UserIsAuthenticated::class]); - $routes->add('POST', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/history', [Web\HistoryController::class, 'createHistoryEntry'], [Web\Middleware\UserIsAuthenticated::class]); + $routes->add('DELETE', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/history', [ + Web\HistoryController::class, + 'deleteHistoryEntry' + ], [Web\Middleware\UserIsAuthenticated::class]); + $routes->add('POST', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/history', [ + Web\HistoryController::class, + 'createHistoryEntry' + ], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('POST', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/rating', [ Web\Movie\MovieRatingController::class, 'updateRating' @@ -174,9 +198,15 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('PUT', $routeUserHistory, [Api\HistoryController::class, 'updateHistory'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); $routeUserWatchlist = '/users/{username:[a-zA-Z0-9]+}/watchlist/movies'; - $routes->add('GET', $routeUserWatchlist , [Api\WatchlistController::class, 'getWatchlist'], [Api\Middleware\IsAuthorizedToReadUserData::class]); + $routes->add('GET', $routeUserWatchlist, [Api\WatchlistController::class, 'getWatchlist'], [Api\Middleware\IsAuthorizedToReadUserData::class]); $routes->add('POST', $routeUserWatchlist, [Api\WatchlistController::class, 'addToWatchlist'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); $routes->add('DELETE', $routeUserWatchlist, [Api\WatchlistController::class, 'deleteFromWatchlist'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); + $routeUserPlayed = '/users/{username:[a-zA-Z0-9]+}/played/movies'; + $routes->add('GET', $routeUserPlayed, [Api\PlayedController::class, 'getPlayed'], [Api\Middleware\IsAuthorizedToReadUserData::class]); + $routes->add('POST', $routeUserPlayed, [Api\PlayedController::class, 'addToPlayed'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); + $routes->add('DELETE', $routeUserPlayed, [Api\PlayedController::class, 'deleteFromPlayed'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); + $routes->add('PUT', $routeUserPlayed, [Api\PlayedController::class, 'updatePlayed'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); + $routerService->addRoutesToRouteCollector($routeCollector, $routes); } diff --git a/src/Domain/Movie/History/MovieHistoryApi.php b/src/Domain/Movie/History/MovieHistoryApi.php index 47e96ee3..a02aebef 100644 --- a/src/Domain/Movie/History/MovieHistoryApi.php +++ b/src/Domain/Movie/History/MovieHistoryApi.php @@ -45,6 +45,17 @@ public function deleteByUserId(int $userId) : void $this->repository->deleteByUserId($userId); } + public function deleteHistoryById(int $movieId, int $userId) : void + { + $this->repository->deleteHistoryById($movieId, $userId); + + if ($this->userApi->fetchUser($userId)->hasJellyfinSyncEnabled() === false) { + return; + } + + $this->jobQueueApi->addJellyfinExportMoviesJob($userId, [$movieId]); + } + public function deleteHistoryByIdAndDate(int $movieId, int $userId, ?Date $watchedAt) : void { $this->repository->deleteHistoryByIdAndDate($movieId, $userId, $watchedAt); @@ -371,6 +382,50 @@ public function fetchUniqueWatchedMoviesPaginated( return $this->urlGenerator->replacePosterPathWithImageSrcUrl($movies); } + public function fetchPlayedMoviesPaginated( + int $userId, + int $limit, + int $page, + ?string $searchTerm = null, + string $sortBy = 'title', + ?SortOrder $sortOrder = null, + ?Year $releaseYear = null, + ?string $language = null, + ?string $genre = null, + ) : array { + if ($sortOrder === null) { + $sortOrder = SortOrder::createAsc(); + } + + $movies = $this->movieRepository->fetchUniqueWatchedMoviesPaginated( + $userId, + $limit, + $page, + $searchTerm, + $sortBy, + $sortOrder, + $releaseYear, + $language, + $genre, + ); + + return $this->urlGenerator->replacePosterPathWithImageSrcUrl($movies); + } + + public function fetchWatchDatesForMovieIds(int $userId, array $movieIds) : array + { + $watchDates = []; + + foreach ($this->movieRepository->fetchWatchDatesForMovieIds($userId, $movieIds) as $watchDateData) { + $watchDates[$watchDateData['movie_id']][$watchDateData['watched_at']] = [ + 'plays' => $watchDateData['plays'], + 'comment' => $watchDateData['comment'], + ]; + } + + return $watchDates; + } + public function fetchWatchDatesOrderedByWatchedAtDesc(int $userId) : array { return $this->movieRepository->fetchWatchDatesOrderedByWatchedAtDesc($userId); diff --git a/src/Domain/Movie/History/MovieHistoryRepository.php b/src/Domain/Movie/History/MovieHistoryRepository.php index 6998be27..c409befb 100644 --- a/src/Domain/Movie/History/MovieHistoryRepository.php +++ b/src/Domain/Movie/History/MovieHistoryRepository.php @@ -38,6 +38,11 @@ public function deleteByUserId(int $userId) : void $this->dbConnection->delete('movie_user_watch_dates', ['user_id' => $userId]); } + public function deleteHistoryById(int $movieId, int $userId) : void + { + $this->dbConnection->delete('movie_user_watch_dates', ['user_id' => $userId, 'movie_id' => $movieId]); + } + public function deleteHistoryByIdAndDate(int $movieId, int $userId, ?Date $watchedAt) : void { if ($watchedAt === null) { diff --git a/src/Domain/Movie/MovieApi.php b/src/Domain/Movie/MovieApi.php index e7935d94..d4f5038b 100644 --- a/src/Domain/Movie/MovieApi.php +++ b/src/Domain/Movie/MovieApi.php @@ -47,6 +47,33 @@ public function __construct( ) { } + public function addPlaysForMovieOnDate(int $movieId, int $userId, ?Date $watchedDate, int $playsToAdd = 1, ?string $comment = null) : void + { + $historyEntry = $this->findHistoryEntryForMovieByUserOnDate($movieId, $userId, $watchedDate); + + $this->watchlistApi->removeMovieFromWatchlistAutomatically($movieId, $userId); + + if ($historyEntry === null) { + $this->historyApi->create( + $movieId, + $userId, + $watchedDate, + $playsToAdd, + $comment, + ); + + return; + } + + $this->historyApi->update( + $movieId, + $userId, + $watchedDate, + $historyEntry->getPlays() + $playsToAdd, + $comment ?? $historyEntry->getComment(), + ); + } + public function create( string $title, int $tmdbId, @@ -79,6 +106,11 @@ public function create( ); } + public function deleteHistoryById(int $movieId, int $userId) : void + { + $this->historyApi->deleteHistoryById($movieId, $userId); + } + public function deleteHistoryByIdAndDate(int $movieId, int $userId, ?Date $watchedAt) : void { $this->historyApi->deleteHistoryByIdAndDate($movieId, $userId, $watchedAt); @@ -147,11 +179,6 @@ public function fetchHistoryMovieTotalPlays(int $movieId, int $userId) : int return $this->historyApi->fetchTotalPlaysForMovieAndUserId($movieId, $userId); } - public function fetchWatchDatesOrderedByWatchedAtDesc(int $userId) : array - { - return $this->historyApi->fetchWatchDatesOrderedByWatchedAtDesc($userId); - } - public function fetchMovieIdsHavingImdbIdOrderedByLastImdbUpdatedAt( ?int $maxAgeInHours = null, ?int $limit = null, @@ -161,6 +188,30 @@ public function fetchMovieIdsHavingImdbIdOrderedByLastImdbUpdatedAt( return $this->movieRepository->fetchMovieIdsHavingImdbIdOrderedByLastImdbUpdatedAt($maxAgeInHours, $limit, $filterMovieIds, $onlyNeverSynced); } + public function fetchPlayedMoviesPaginated( + int $userId, + int $limit, + int $page, + ?string $searchTerm, + string $sortBy, + SortOrder $sortOrder, + ?Year $releaseYear, + ?string $language, + ?string $genre, + ) : array { + return $this->historyApi->fetchPlayedMoviesPaginated( + $userId, + $limit, + $page, + $searchTerm, + $sortBy, + $sortOrder, + $releaseYear, + $language, + $genre, + ); + } + public function fetchTotalPlayCount(int $userId) : int { return $this->historyApi->fetchTotalPlayCount($userId); @@ -186,6 +237,16 @@ public function fetchUniqueMovieReleaseYears(int $userId) : array return $this->historyApi->fetchUniqueMovieReleaseYears($userId); } + public function fetchUniqueWatchedMoviesCount(int $userId, ?string $searchTerm, ?Year $releaseYear, ?string $language, ?string $genre) : int + { + return $this->historyApi->fetchUniqueWatchedMoviesCount($userId, $searchTerm, $releaseYear, $language, $genre); + } + + public function fetchPlayedMoviesCount(int $userId, ?string $searchTerm, ?Year $releaseYear, ?string $language, ?string $genre) : int + { + return $this->historyApi->fetchUniqueWatchedMoviesCount($userId, $searchTerm, $releaseYear, $language, $genre); + } + public function fetchUniqueWatchedMoviesPaginated( int $userId, int $limit, @@ -210,9 +271,20 @@ public function fetchUniqueWatchedMoviesPaginated( ); } - public function fetchUniqueWatchedMoviesCount(int $userId, ?string $searchTerm, ?Year $releaseYear, ?string $language, ?string $genre) : int + public function fetchWatchDatesForMovies(int $userId, array $playedEntries) : array { - return $this->historyApi->fetchUniqueWatchedMoviesCount($userId, $searchTerm, $releaseYear, $language, $genre); + $movieIds = []; + + foreach ($playedEntries as $playedEntry) { + $movieIds[] = $playedEntry['id']; + } + + return $this->historyApi->fetchWatchDatesForMovieIds($userId, $movieIds); + } + + public function fetchWatchDatesOrderedByWatchedAtDesc(int $userId) : array + { + return $this->historyApi->fetchWatchDatesOrderedByWatchedAtDesc($userId); } public function fetchWithActor(int $personId, int $userId) : array @@ -333,33 +405,6 @@ public function findUserRating(int $movieId, int $userId) : ?PersonalRating return $this->repository->findUserRating($movieId, $userId); } - public function addPlaysForMovieOnDate(int $movieId, int $userId, ?Date $watchedDate, int $playsToAdd = 1, ?string $comment = null) : void - { - $historyEntry = $this->findHistoryEntryForMovieByUserOnDate($movieId, $userId, $watchedDate); - - $this->watchlistApi->removeMovieFromWatchlistAutomatically($movieId, $userId); - - if ($historyEntry === null) { - $this->historyApi->create( - $movieId, - $userId, - $watchedDate, - $playsToAdd, - $comment, - ); - - return; - } - - $this->historyApi->update( - $movieId, - $userId, - $watchedDate, - $historyEntry->getPlays() + $playsToAdd, - $comment ?? $historyEntry->getComment(), - ); - } - public function replaceHistoryForMovieByDate(int $movieId, int $userId, ?Date $watchedAt, int $playsPerDate, ?string $comment = null) : void { $existingHistoryEntry = $this->findHistoryEntryForMovieByUserOnDate($movieId, $userId, $watchedAt); diff --git a/src/Domain/Movie/MovieRepository.php b/src/Domain/Movie/MovieRepository.php index 45b05578..5997edc5 100644 --- a/src/Domain/Movie/MovieRepository.php +++ b/src/Domain/Movie/MovieRepository.php @@ -304,6 +304,22 @@ public function fetchHistoryCount(int $userId, ?string $searchTerm = null) : int )[0]; } + public function fetchWatchDatesForMovieIds(int $userId, array $movieIds) : array + { + $placeholders = trim(str_repeat('?, ', count($movieIds)), ', '); + + return $this->dbConnection->fetchAllAssociative( + "SELECT watched_at, plays, comment, movie_id + FROM movie_user_watch_dates + WHERE user_id = ? and movie_id in ($placeholders) + ORDER BY watched_at DESC", + [ + $userId, + ...$movieIds + ], + ); + } + public function fetchWatchDatesOrderedByWatchedAtDesc(int $userId) : array { return $this->dbConnection->fetchAllAssociative( diff --git a/src/HttpController/Api/Dto/MovieDto.php b/src/HttpController/Api/Dto/MovieDto.php index 8e7417d2..1907f24d 100644 --- a/src/HttpController/Api/Dto/MovieDto.php +++ b/src/HttpController/Api/Dto/MovieDto.php @@ -75,6 +75,11 @@ public static function create( ); } + public function getId() : int + { + return $this->id; + } + public function jsonSerialize() : array { return [ diff --git a/src/HttpController/Api/Dto/PlayedEntryDto.php b/src/HttpController/Api/Dto/PlayedEntryDto.php new file mode 100644 index 00000000..af04bbb8 --- /dev/null +++ b/src/HttpController/Api/Dto/PlayedEntryDto.php @@ -0,0 +1,25 @@ + $this->movieDto, + 'watchDates' => $this->watchDates + ]; + } +} diff --git a/src/HttpController/Api/Dto/PlayedEntryDtoList.php b/src/HttpController/Api/Dto/PlayedEntryDtoList.php new file mode 100644 index 00000000..d6fc31b9 --- /dev/null +++ b/src/HttpController/Api/Dto/PlayedEntryDtoList.php @@ -0,0 +1,22 @@ +data[] = $dto; + } +} diff --git a/src/HttpController/Api/Dto/WatchDateDto.php b/src/HttpController/Api/Dto/WatchDateDto.php new file mode 100644 index 00000000..389505b4 --- /dev/null +++ b/src/HttpController/Api/Dto/WatchDateDto.php @@ -0,0 +1,34 @@ +watchDate; + } + + public function jsonSerialize() : array + { + return [ + 'date' => $this->watchDate, + 'plays' => $this->plays, + 'comment' => $this->comment, + ]; + } +} diff --git a/src/HttpController/Api/Dto/WatchDateDtoList.php b/src/HttpController/Api/Dto/WatchDateDtoList.php new file mode 100644 index 00000000..6a678bd6 --- /dev/null +++ b/src/HttpController/Api/Dto/WatchDateDtoList.php @@ -0,0 +1,28 @@ +getWatchDate() == $dto->getWatchDate()) { + throw new \RuntimeException('Watch date must be unique'); + } + } + + $this->data[] = $dto; + } +} diff --git a/src/HttpController/Api/PlayedController.php b/src/HttpController/Api/PlayedController.php new file mode 100644 index 00000000..1744a7f5 --- /dev/null +++ b/src/HttpController/Api/PlayedController.php @@ -0,0 +1,142 @@ +requestMapper->mapUsernameFromRoute($request)->getId(); + $playedAdditions = Json::decode($request->getBody()); + + foreach ($playedAdditions as $playAddition) { + $movieId = (int)$playAddition['movaryId']; + $watchDates = $playAddition['watchDates'] ?? []; + + foreach ($watchDates as $watchDate) { + $this->movieApi->addPlaysForMovieOnDate( + $movieId, + $userId, + $watchDate['watchedAt'] !== null ? Date::createFromString($watchDate['watchedAt']) : null, + $watchDate['plays'] ?? 1, + $watchDate['comment'] ?? null, + ); + } + } + + return Response::createNoContent(); + } + + public function deleteFromPlayed(Request $request) : Response + { + $userId = $this->requestMapper->mapUsernameFromRoute($request)->getId(); + $playedDeletions = Json::decode($request->getBody()); + + foreach ($playedDeletions as $playedDeletion) { + $movieId = (int)$playedDeletion['movaryId']; + $watchDates = $playedDeletion['watchDates'] ?? []; + + if (count($watchDates) === 0) { + $this->movieApi->deleteHistoryById( + $movieId, + $userId, + ); + + continue; + } + + foreach ($watchDates as $date) { + $this->movieApi->deleteHistoryByIdAndDate( + $movieId, + $userId, + empty($date) === true ? null : Date::createFromString($date), + ); + } + } + + return Response::createNoContent(); + } + + public function getPlayed(Request $request) : Response + { + $requestData = $this->playedRequestMapper->mapRequest($request); + + $playedEntries = $this->movieApi->fetchPlayedMoviesPaginated( + $requestData->getRequestedUserId(), + $requestData->getLimit(), + $requestData->getPage(), + $requestData->getSearchTerm(), + $requestData->getSortBy(), + $requestData->getSortOrder(), + $requestData->getReleaseYear(), + $requestData->getLanguage(), + $requestData->getGenre(), + ); + + $watchDates = $this->movieApi->fetchWatchDatesForMovies($requestData->getRequestedUserId(), $playedEntries); + + $watchlistCount = $this->movieApi->fetchPlayedMoviesCount( + $requestData->getRequestedUserId(), + $requestData->getSearchTerm(), + $requestData->getReleaseYear(), + $requestData->getLanguage(), + $requestData->getGenre(), + ); + + $paginationElements = $this->paginationElementsCalculator->createPaginationElements( + $watchlistCount, + $requestData->getLimit(), + $requestData->getPage(), + ); + + return Response::createJson( + Json::encode([ + 'played' => $this->playedResponseMapper->mapPlayedEntries($playedEntries, $watchDates), + 'currentPage' => $paginationElements->getCurrentPage(), + 'maxPage' => $paginationElements->getMaxPage(), + ]), + ); + } + + public function updatePlayed(Request $request) : Response + { + $userId = $this->requestMapper->mapUsernameFromRoute($request)->getId(); + $playedUpdates = Json::decode($request->getBody()); + + foreach ($playedUpdates as $playedUpdate) { + $movieId = (int)$playedUpdate['movaryId']; + $watchDates = $playedUpdate['watchDates'] ?? []; + + foreach ($watchDates as $watchDate) { + $this->movieApi->replaceHistoryForMovieByDate( + $movieId, + $userId, + $watchDate['watchedAt'] !== null ? Date::createFromString($watchDate['watchedAt']) : null, + $watchDate['plays'], + $watchDate['comment'], + ); + } + } + + return Response::createNoContent(); + } +} diff --git a/src/HttpController/Api/RequestMapper/PlayedRequestMapper.php b/src/HttpController/Api/RequestMapper/PlayedRequestMapper.php new file mode 100644 index 00000000..c75cd775 --- /dev/null +++ b/src/HttpController/Api/RequestMapper/PlayedRequestMapper.php @@ -0,0 +1,70 @@ +getGetParameters(); + + $searchTerm = $getParameters['search'] ?? null; + $page = $getParameters['page'] ?? self::DEFAULT_PAGE; + $limit = $getParameters['limit'] ?? self::DEFAULT_LIMIT; + $sortBy = $getParameters['sortBy'] ?? self::DEFAULT_SORT_BY; + $sortOrder = $this->mapSortOrder($getParameters); + $releaseYear = $this->mapReleaseYear($getParameters); + + return WatchlistRequestDto::create( + $this->requestMapper->mapUsernameFromRoute($request)->getId(), + $searchTerm, + (int)$page, + (int)$limit, + $sortBy, + $sortOrder, + $releaseYear, + ); + } + + private function mapReleaseYear(array $getParameters) : ?Year + { + $releaseYear = $getParameters['releaseYear'] ?? self::DEFAULT_RELEASE_YEAR; + + if (empty($releaseYear) === true) { + return null; + } + + return Year::createFromString($releaseYear); + } + + private function mapSortOrder(array $getParameters) : SortOrder + { + if (isset($getParameters['sortOrder']) === false) { + return SortOrder::createAsc(); + } + + return match ($getParameters['sortOrder']) { + 'asc' => SortOrder::createAsc(), + 'desc' => SortOrder::createDesc(), + + default => throw new \RuntimeException('Not supported sort order: ' . $getParameters['sortOrder']) + }; + } +} diff --git a/src/HttpController/Api/ResponseMapper/PlayedResponseMapper.php b/src/HttpController/Api/ResponseMapper/PlayedResponseMapper.php new file mode 100644 index 00000000..a1b49ebf --- /dev/null +++ b/src/HttpController/Api/ResponseMapper/PlayedResponseMapper.php @@ -0,0 +1,54 @@ +mapPlayedEntry($playedEntryData, $watchDatesData); + + $playedEntries->add($playedEntry); + } + + return $playedEntries; + } + + private function mapPlayedEntry(array $playedEntryData, array $watchDatesData) : PlayedEntryDto + { + $movie = $this->movieResponseMapper->mapMovie($playedEntryData); + $watchDates = $this->mapWatchDates($movie->getId(), $watchDatesData); + + return PlayedEntryDto::create($movie, $watchDates); + } + + private function mapWatchDates(int $movieId, array $watchDatesData) : WatchDateDtoList + { + $watchDates = WatchDateDtoList::create(); + + foreach ($watchDatesData[$movieId] as $watchDate => $watchDateData) { + $watchDate = WatchDateDto::create( + empty($watchDate) === false ? Date::createFromString($watchDate) : null, + $watchDateData['plays'], + $watchDateData['comment'], + ); + + $watchDates->add($watchDate); + } + + return $watchDates; + } +} diff --git a/tests/rest/api/history.http b/tests/rest/api/history.http index 1c7ea76b..3253c379 100644 --- a/tests/rest/api/history.http +++ b/tests/rest/api/history.http @@ -12,7 +12,7 @@ Cache-Control: no-cache Content-Type: application/json X-Auth-Token: {{xAuthToken}} -[{"movieId" : 1, "watchedAt" : "2011-05-06", "plays" : 1, "comment" : "comment"}] +[{"movaryId" : 1, "watchedAt" : "2011-05-06", "plays" : 1, "comment" : "comment"}] #### @@ -22,7 +22,7 @@ Cache-Control: no-cache Content-Type: application/json X-Auth-Token: {{xAuthToken}} -[{"movieId" : 1, "watchedAt" : "2011-05-06"}] +[{"movaryId" : 1, "watchedAt" : "2011-05-06"}] #### @@ -32,6 +32,6 @@ Cache-Control: no-cache Content-Type: application/json X-Auth-Token: {{xAuthToken}} -[{"movieId" : 1, "watchedAt" : "2011-05-06"}] +[{"movaryId" : 1, "watchedAt" : "2011-05-06"}] #### diff --git a/tests/rest/api/played.http b/tests/rest/api/played.http new file mode 100644 index 00000000..9177594a --- /dev/null +++ b/tests/rest/api/played.http @@ -0,0 +1,72 @@ +GET http://127.0.0.1/api/users/{{username}}/played/movies?limit=10 +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +#### + +PUT http://127.0.0.1/api/users/{{username}}/played/movies +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +[ + { + "movaryId": 1, + "watchDates": [ + { + "watchedAt": null, + "plays": 2, + "comment": "Test comment" + }, + { + "watchedAt": "2024-05-06", + "plays": 2, + "comment": "Test comment" + } + ] + } +] + +#### + +POST http://127.0.0.1/api/users/{{username}}/played/movies +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +[ + { + "movaryId": 1, + "watchDates": [ + { + "watchedAt": null + }, + { + "watchedAt": "2024-05-06", + "plays": 2, + "comment": "Test comment" + } + ] + } +] + +#### + +DELETE http://127.0.0.1/api/users/{{username}}/played/movies +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +[ + { + "movaryId": 1, + "watchDates": [] + } +] + +####