From ea7e66c1a00bc4bfa69be7d0a79e986e93d39963 Mon Sep 17 00:00:00 2001 From: ITQ Date: Wed, 20 May 2026 03:38:30 +0300 Subject: [PATCH] (scope): [body] [footer(s)] --- .../movienight/MovieNightApplication.kt | 2 + .../movienight/adapters/web/FilmController.kt | 26 ++++ .../adapters/web/FilmLibraryController.kt | 35 ++++-- .../movienight/adapters/web/UserController.kt | 1 + .../web/dto/request/CreateFilmRequest.kt | 10 ++ .../web/dto/request/EditFilmRequest.kt | 10 ++ .../web/dto/request/EditUserRequest.kt | 1 + .../web/dto/response/FilmLibraryResponse.kt | 2 + .../adapters/web/dto/response/FilmResponse.kt | 21 ++++ .../adapters/web/dto/response/UserResponse.kt | 2 + .../ports/input/FilmLibraryUseCase.kt | 15 +++ .../application/ports/input/FilmUseCase.kt | 21 ++++ .../application/ports/input/UserUseCase.kt | 1 + .../services/FilmLibraryService.kt | 117 +++++++++++++----- .../application/services/FilmService.kt | 61 ++++----- .../application/services/UserService.kt | 9 +- src/main/resources/application.yaml | 8 ++ .../services/FilmLibraryServiceTest.kt | 36 ++++-- 18 files changed, 291 insertions(+), 87 deletions(-) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index 39274c8..6db897a 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -3,8 +3,10 @@ package com.project.movienight import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication +@EnableScheduling @ConfigurationPropertiesScan("com.project.movienight.config") class MovieNightApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt index 3699e33..9bec40b 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt @@ -25,6 +25,12 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import java.util.UUID +private fun String.toContentTypeOrFilm(): com.project.movienight.domain.model.ContentType = + runCatching { + com.project.movienight.domain.model.ContentType + .valueOf(this) + }.getOrDefault(com.project.movienight.domain.model.ContentType.FILM) + @RestController @RequestMapping("/api/films") class FilmController( @@ -45,6 +51,16 @@ class FilmController( CreateFilmCommand( title = request.title, description = request.description, + contentType = request.contentType.toContentTypeOrFilm(), + releaseYear = request.releaseYear, + genres = request.genres, + cast = request.cast, + directors = request.directors, + imdbRating = request.imdbRating, + platformRating = request.platformRating, + externalUrl = request.externalUrl, + jellyfinItemId = request.jellyfinItemId, + jellyfinLibraryId = request.jellyfinLibraryId, ), ), ) @@ -61,6 +77,16 @@ class FilmController( EditFilmCommand( title = request.title, description = request.description, + contentType = request.contentType.toContentTypeOrFilm(), + releaseYear = request.releaseYear, + genres = request.genres, + cast = request.cast, + directors = request.directors, + imdbRating = request.imdbRating, + platformRating = request.platformRating, + externalUrl = request.externalUrl, + jellyfinItemId = request.jellyfinItemId, + jellyfinLibraryId = request.jellyfinLibraryId, ), ), ) diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt index c2ed037..12c2c5e 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -11,6 +11,9 @@ import com.project.movienight.application.ports.input.GetAllFilmsUseCase import com.project.movienight.application.ports.input.GetFilmByIdUseCase import com.project.movienight.application.ports.input.GetFilmLibraryQuery import com.project.movienight.application.ports.input.GetFilmLibraryUseCase +import com.project.movienight.application.ports.input.ListFilmLibraryEntriesUseCase +import com.project.movienight.application.ports.input.MarkFilmViewedCommand +import com.project.movienight.application.ports.input.MarkFilmViewedUseCase import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand import com.project.movienight.application.ports.input.RemoveFilmFromLibraryUseCase import com.project.movienight.domain.exception.EntityNotFoundException @@ -30,10 +33,10 @@ import java.util.UUID class FilmLibraryController( private val createFilmLibraryUseCase: CreateFilmLibraryUseCase, private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase, + private val markFilmViewedUseCase: MarkFilmViewedUseCase, private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase, private val getFilmLibraryUseCase: GetFilmLibraryUseCase, - private val getFilmByIdUseCase: GetFilmByIdUseCase, - private val getAllFilmsUseCase: GetAllFilmsUseCase, + private val listFilmLibraryEntriesUseCase: ListFilmLibraryEntriesUseCase, ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -60,18 +63,10 @@ class FilmLibraryController( ), ) - @GetMapping("/films") - fun getAllFilmsInLibrary( + @GetMapping("/entries") + fun list( @PathVariable userId: UUID, - ): List { - val library = - getFilmLibraryUseCase.getLibrary( - GetFilmLibraryQuery(userId = userId), - ) - - val film = getFilmByIdUseCase.getById(library.filmId) - return listOf(FilmResponse.fromDomain(film)) - } + ): List = listFilmLibraryEntriesUseCase.list(userId).map { FilmLibraryResponse.fromDomain(it) } @PostMapping("/films/{filmId}") @ResponseStatus(HttpStatus.CREATED) @@ -88,6 +83,20 @@ class FilmLibraryController( ), ) + @PostMapping("/films/{filmId}/viewed") + fun markViewed( + @PathVariable userId: UUID, + @PathVariable filmId: UUID, + ): FilmLibraryResponse = + FilmLibraryResponse.fromDomain( + markFilmViewedUseCase.markViewed( + MarkFilmViewedCommand( + userId = userId, + filmId = filmId, + ), + ), + ) + @DeleteMapping("/films/{filmId}") @ResponseStatus(HttpStatus.NO_CONTENT) fun removeFilm( diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index bccf5bc..e06954c 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -64,6 +64,7 @@ class UserController( command = EditUserCommand( name = request.name, + jellyfinUserId = request.jellyfinUserId, ), ), ) diff --git a/src/main/kotlin/com/project/movienight/adapters/web/dto/request/CreateFilmRequest.kt b/src/main/kotlin/com/project/movienight/adapters/web/dto/request/CreateFilmRequest.kt index 994429d..82f7348 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/dto/request/CreateFilmRequest.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/dto/request/CreateFilmRequest.kt @@ -3,4 +3,14 @@ package com.project.movienight.adapters.web.dto.request data class CreateFilmRequest( val title: String, val description: String, + val contentType: String = "FILM", + val releaseYear: Int? = null, + val genres: List = emptyList(), + val cast: List = emptyList(), + val directors: List = emptyList(), + val imdbRating: Double? = null, + val platformRating: Double? = null, + val externalUrl: String? = null, + val jellyfinItemId: String? = null, + val jellyfinLibraryId: String? = null, ) diff --git a/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditFilmRequest.kt b/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditFilmRequest.kt index 9e476c3..60eddce 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditFilmRequest.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditFilmRequest.kt @@ -3,4 +3,14 @@ package com.project.movienight.adapters.web.dto.request data class EditFilmRequest( val title: String, val description: String, + val contentType: String = "FILM", + val releaseYear: Int? = null, + val genres: List = emptyList(), + val cast: List = emptyList(), + val directors: List = emptyList(), + val imdbRating: Double? = null, + val platformRating: Double? = null, + val externalUrl: String? = null, + val jellyfinItemId: String? = null, + val jellyfinLibraryId: String? = null, ) diff --git a/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditUserRequest.kt b/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditUserRequest.kt index 83ddd24..358e0e4 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditUserRequest.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/dto/request/EditUserRequest.kt @@ -2,4 +2,5 @@ package com.project.movienight.adapters.web.dto.request data class EditUserRequest( val name: String, + val jellyfinUserId: String? = null, ) diff --git a/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmLibraryResponse.kt b/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmLibraryResponse.kt index 8ba6c01..90a339d 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmLibraryResponse.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmLibraryResponse.kt @@ -9,6 +9,7 @@ data class FilmLibraryResponse( val filmId: UUID, val comment: String?, val isViewed: Boolean, + val watchedAt: java.time.LocalDateTime?, ) { companion object { fun fromDomain(filmLibrary: FilmLibrary): FilmLibraryResponse = @@ -18,6 +19,7 @@ data class FilmLibraryResponse( filmId = filmLibrary.filmId, comment = filmLibrary.comment, isViewed = filmLibrary.isViewed, + watchedAt = filmLibrary.watchedAt, ) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmResponse.kt b/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmResponse.kt index 239196d..4948540 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmResponse.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/dto/response/FilmResponse.kt @@ -1,5 +1,6 @@ package com.project.movienight.adapters.web.dto.response +import com.project.movienight.domain.model.ContentType import com.project.movienight.domain.model.Film import java.util.UUID @@ -7,6 +8,16 @@ data class FilmResponse( val id: UUID, val title: String, val description: String, + val contentType: ContentType, + val releaseYear: Int?, + val genres: List, + val cast: List, + val directors: List, + val imdbRating: Double?, + val platformRating: Double?, + val externalUrl: String?, + val jellyfinItemId: String?, + val jellyfinLibraryId: String?, ) { companion object { fun fromDomain(film: Film): FilmResponse = @@ -14,6 +25,16 @@ data class FilmResponse( id = film.id, title = film.title, description = film.description, + contentType = film.contentType, + releaseYear = film.releaseYear, + genres = film.genres, + cast = film.cast, + directors = film.directors, + imdbRating = film.imdbRating, + platformRating = film.platformRating, + externalUrl = film.externalUrl, + jellyfinItemId = film.jellyfinItemId, + jellyfinLibraryId = film.jellyfinLibraryId, ) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/dto/response/UserResponse.kt b/src/main/kotlin/com/project/movienight/adapters/web/dto/response/UserResponse.kt index 48f5dd8..b1b94f4 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/dto/response/UserResponse.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/dto/response/UserResponse.kt @@ -7,6 +7,7 @@ data class UserResponse( val id: UUID, val name: String, val email: String, + val jellyfinUserId: String?, ) { companion object { fun fromDomain(user: User): UserResponse = @@ -14,6 +15,7 @@ data class UserResponse( id = user.id, name = user.name, email = user.email, + jellyfinUserId = user.jellyfinUserId, ) } } diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/FilmLibraryUseCase.kt b/src/main/kotlin/com/project/movienight/application/ports/input/FilmLibraryUseCase.kt index cf9a0b8..3100547 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/input/FilmLibraryUseCase.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/FilmLibraryUseCase.kt @@ -1,6 +1,7 @@ package com.project.movienight.application.ports.input import com.project.movienight.domain.model.FilmLibrary +import java.time.LocalDateTime import java.util.UUID interface CreateFilmLibraryUseCase { @@ -21,6 +22,16 @@ data class AddFilmToLibraryCommand( val filmId: UUID, ) +interface MarkFilmViewedUseCase { + fun markViewed(command: MarkFilmViewedCommand): FilmLibrary +} + +data class MarkFilmViewedCommand( + val userId: UUID, + val filmId: UUID, + val watchedAt: LocalDateTime? = null, +) + interface RemoveFilmFromLibraryUseCase { fun removeFilm(command: RemoveFilmFromLibraryCommand): FilmLibrary } @@ -38,3 +49,7 @@ interface GetFilmLibraryUseCase { data class GetFilmLibraryQuery( val userId: UUID, ) + +interface ListFilmLibraryEntriesUseCase { + fun list(userId: UUID): List +} diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt b/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt index db3f4b0..27098c7 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt @@ -1,5 +1,6 @@ package com.project.movienight.application.ports.input +import com.project.movienight.domain.model.ContentType import com.project.movienight.domain.model.Film import java.util.UUID @@ -10,6 +11,16 @@ interface CreateFilmUseCase { data class CreateFilmCommand( val title: String, val description: String, + val contentType: ContentType = ContentType.FILM, + val releaseYear: Int? = null, + val genres: List = emptyList(), + val cast: List = emptyList(), + val directors: List = emptyList(), + val imdbRating: Double? = null, + val platformRating: Double? = null, + val externalUrl: String? = null, + val jellyfinItemId: String? = null, + val jellyfinLibraryId: String? = null, ) interface EditFilmUseCase { @@ -22,6 +33,16 @@ interface EditFilmUseCase { data class EditFilmCommand( val title: String, val description: String, + val contentType: ContentType = ContentType.FILM, + val releaseYear: Int? = null, + val genres: List = emptyList(), + val cast: List = emptyList(), + val directors: List = emptyList(), + val imdbRating: Double? = null, + val platformRating: Double? = null, + val externalUrl: String? = null, + val jellyfinItemId: String? = null, + val jellyfinLibraryId: String? = null, ) interface DeleteFilmUseCase { diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/UserUseCase.kt b/src/main/kotlin/com/project/movienight/application/ports/input/UserUseCase.kt index b417525..b066a4f 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/input/UserUseCase.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/UserUseCase.kt @@ -21,6 +21,7 @@ interface EditUserUseCase { data class EditUserCommand( val name: String, + val jellyfinUserId: String? = null, ) interface DeleteUserUseCase { diff --git a/src/main/kotlin/com/project/movienight/application/services/FilmLibraryService.kt b/src/main/kotlin/com/project/movienight/application/services/FilmLibraryService.kt index ba64e2a..13ec743 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmLibraryService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmLibraryService.kt @@ -1,11 +1,15 @@ package com.project.movienight.application.services +import com.project.movienight.adapters.metrics.BusinessMetricsService import com.project.movienight.application.ports.input.AddFilmToLibraryCommand import com.project.movienight.application.ports.input.AddFilmToLibraryUseCase import com.project.movienight.application.ports.input.CreateFilmLibraryCommand import com.project.movienight.application.ports.input.CreateFilmLibraryUseCase import com.project.movienight.application.ports.input.GetFilmLibraryQuery import com.project.movienight.application.ports.input.GetFilmLibraryUseCase +import com.project.movienight.application.ports.input.ListFilmLibraryEntriesUseCase +import com.project.movienight.application.ports.input.MarkFilmViewedCommand +import com.project.movienight.application.ports.input.MarkFilmViewedUseCase import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand import com.project.movienight.application.ports.input.RemoveFilmFromLibraryUseCase import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort @@ -20,70 +24,119 @@ import java.util.UUID class FilmLibraryService( private val filmLibraryRepository: FilmLibraryRepositoryPort, private val idGenerator: IdGenerator, + private val businessMetricsService: BusinessMetricsService, ) : CreateFilmLibraryUseCase, AddFilmToLibraryUseCase, + MarkFilmViewedUseCase, RemoveFilmFromLibraryUseCase, - GetFilmLibraryUseCase { + GetFilmLibraryUseCase, + ListFilmLibraryEntriesUseCase { override fun create(command: CreateFilmLibraryCommand): FilmLibrary { - val existingLibrary = findByUserId(command.userId) - if (existingLibrary != null) { - return existingLibrary - } + findByUserId(command.userId)?.let { return it } - return filmLibraryRepository.save( - FilmLibrary( - id = idGenerator.generateId(), - userId = command.userId, - filmId = idGenerator.generateId(), - comment = command.name, - isViewed = false, - ), - ) + val libraryId = idGenerator.generateId() + val saved = + filmLibraryRepository.save( + FilmLibrary( + id = libraryId, + userId = command.userId, + filmId = libraryId, + comment = command.name, + isViewed = false, + watchedAt = null, + ), + ) + businessMetricsService.recordLibraryEvent() + return saved } override fun addFilm(command: AddFilmToLibraryCommand): FilmLibrary { - val existingLibrary = findByUserId(command.userId) - if (existingLibrary == null) { - return filmLibraryRepository.save( + val existingEntry = findByUserAndFilmId(command.userId, command.filmId) + if (existingEntry != null) { + val saved = + filmLibraryRepository.save( + existingEntry.copy( + isViewed = false, + watchedAt = null, + ), + ) + businessMetricsService.recordLibraryEvent() + return saved + } + + val saved = + filmLibraryRepository.save( FilmLibrary( id = idGenerator.generateId(), userId = command.userId, filmId = command.filmId, comment = null, isViewed = false, + watchedAt = null, ), ) - } - - return filmLibraryRepository.save( - existingLibrary.copy( - filmId = command.filmId, - isViewed = false, - ), - ) + businessMetricsService.recordLibraryEvent() + return saved } override fun removeFilm(command: RemoveFilmFromLibraryCommand): FilmLibrary { val existingLibrary = - findByUserId(command.userId) - ?: throw EntityNotFoundException(entity = "Film library", id = command.userId.toString()) + if (command.libraryId != null) { + filmLibraryRepository.findById(command.libraryId) + ?: throw EntityNotFoundException(entity = "Film library", id = command.libraryId.toString()) + } else { + findByUserAndFilmId(command.userId, command.filmId) + ?: throw EntityNotFoundException(entity = "Film library", id = command.filmId.toString()) + } - if (command.libraryId != null && command.libraryId != existingLibrary.id) { - throw EntityNotFoundException(entity = "Film library", id = command.libraryId.toString()) - } - - if (existingLibrary.filmId != command.filmId) { + if (existingLibrary.userId != command.userId || existingLibrary.filmId != command.filmId) { throw DomainException("Film with id ${command.filmId} not found in user's library") } filmLibraryRepository.deleteById(existingLibrary.id) + businessMetricsService.recordLibraryEvent() return existingLibrary } + override fun markViewed(command: MarkFilmViewedCommand): FilmLibrary { + val existingEntry = findByUserAndFilmId(command.userId, command.filmId) + val watchedAt = command.watchedAt ?: java.time.LocalDateTime.now() + + val saved = + if (existingEntry == null) { + filmLibraryRepository.save( + FilmLibrary( + id = idGenerator.generateId(), + userId = command.userId, + filmId = command.filmId, + comment = null, + isViewed = true, + watchedAt = watchedAt, + ), + ) + } else { + filmLibraryRepository.save( + existingEntry.copy( + isViewed = true, + watchedAt = watchedAt, + ), + ) + } + businessMetricsService.recordLibraryEvent() + return saved + } + override fun getLibrary(query: GetFilmLibraryQuery): FilmLibrary = findByUserId(query.userId) ?: throw EntityNotFoundException(entity = "Film library", id = query.userId.toString()) + override fun list(userId: UUID): List = filmLibraryRepository.findAll().filter { it.userId == userId } + private fun findByUserId(userId: UUID): FilmLibrary? = filmLibraryRepository.findAll().firstOrNull { it.userId == userId } + + private fun findByUserAndFilmId( + userId: UUID, + filmId: UUID, + ): FilmLibrary? = filmLibraryRepository.findAll().firstOrNull { it.userId == userId && it.filmId == filmId } } diff --git a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt index bbd32b1..564667f 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -56,21 +56,23 @@ class FilmService( throw BlockedValueException(target = "Film", field = "description") } - val film = - Film( - id = idGenerator.generateId(), - title = command.title, - description = command.description, - ) - val saved = filmRepository.save(film) - - filmCreatedCounter.increment() - - log.info("Film created: id='{}', title='{}'", saved.id, saved.title) - return saved - } finally { - sample.stop(createFilmTimer) - } + val film = + Film( + id = idGenerator.generateId(), + title = command.title, + description = command.description, + contentType = command.contentType, + releaseYear = command.releaseYear, + genres = command.genres, + cast = command.cast, + directors = command.directors, + imdbRating = command.imdbRating, + platformRating = command.platformRating, + externalUrl = command.externalUrl, + jellyfinItemId = command.jellyfinItemId, + jellyfinLibraryId = command.jellyfinLibraryId, + ) + return filmRepository.save(film) } override fun edit( @@ -100,20 +102,23 @@ class FilmService( throw EntityNotFoundException(entity = "Film", id = id.toString()) } - val updatedFilm = - film.copy( - title = command.title, - description = command.description, - ) - val saved = filmRepository.save(updatedFilm) + film = + film.copy( + title = command.title, + description = command.description, + contentType = command.contentType, + releaseYear = command.releaseYear, + genres = command.genres, + cast = command.cast, + directors = command.directors, + imdbRating = command.imdbRating, + platformRating = command.platformRating, + externalUrl = command.externalUrl, + jellyfinItemId = command.jellyfinItemId, + jellyfinLibraryId = command.jellyfinLibraryId, + ) - filmEditedCounter.increment() - - log.info("Film edited: id='{}'", saved.id) - return saved - } finally { - sample.stop(editFilmTimer) - } + return filmRepository.save(film) } override fun delete(id: UUID) { diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index da2a84b..684da5f 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -37,6 +37,7 @@ class UserService( name = command.name, email = command.email, library = null, + jellyfinUserId = null, ) return userRepository.save(user) } @@ -50,7 +51,13 @@ class UserService( } var user = userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString()) - user = user.copy(name = command.name) + + user = + user.copy( + name = command.name, + jellyfinUserId = command.jellyfinUserId ?: user.jellyfinUserId, + ) + return userRepository.save(user) } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 284e69a..9f562dc 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -95,6 +95,14 @@ info: description: MovieNight backend service version: ${project.version:unknown} +integrations: + jellyfin: + enabled: ${JELLYFIN_SYNC_ENABLED:false} + base-url: ${JELLYFIN_BASE_URL:} + api-key: ${JELLYFIN_API_KEY:} + sync-interval-ms: ${JELLYFIN_SYNC_INTERVAL_MS:1800000} + request-timeout-ms: ${JELLYFIN_REQUEST_TIMEOUT_MS:20000} + services: user: blocked-names: diff --git a/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt index 1556f83..b029c84 100644 --- a/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt +++ b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt @@ -1,5 +1,6 @@ package com.project.movienight.application.services +import com.project.movienight.adapters.metrics.BusinessMetricsService import com.project.movienight.application.ports.input.AddFilmToLibraryCommand import com.project.movienight.application.ports.input.CreateFilmLibraryCommand import com.project.movienight.application.ports.input.GetFilmLibraryQuery @@ -23,32 +24,33 @@ import java.util.UUID class FilmLibraryServiceTest { private lateinit var filmLibraryRepository: FilmLibraryRepositoryPort private lateinit var idGenerator: IdGenerator + private lateinit var businessMetricsService: BusinessMetricsService private lateinit var filmLibraryService: FilmLibraryService @BeforeEach fun setup() { filmLibraryRepository = mockk() idGenerator = mockk() - filmLibraryService = FilmLibraryService(filmLibraryRepository, idGenerator) + businessMetricsService = mockk(relaxed = true) + filmLibraryService = FilmLibraryService(filmLibraryRepository, idGenerator, businessMetricsService) } @Test fun `should create new film library when user has no library`() { val userId = UUID.randomUUID() val libraryId = UUID.randomUUID() - val filmId = UUID.randomUUID() val command = CreateFilmLibraryCommand(userId = userId, name = "My Films") val expectedLibrary = FilmLibrary( id = libraryId, userId = userId, - filmId = filmId, + filmId = libraryId, comment = "My Films", isViewed = false, ) every { filmLibraryRepository.findAll() } returns emptyList() - every { idGenerator.generateId() } returnsMany listOf(libraryId, filmId) + every { idGenerator.generateId() } returns libraryId every { filmLibraryRepository.save( match { @@ -62,11 +64,11 @@ class FilmLibraryServiceTest { assertNotNull(result) assertEquals(libraryId, result.id) assertEquals(userId, result.userId) - assertEquals(filmId, result.filmId) + assertEquals(libraryId, result.filmId) assertEquals("My Films", result.comment) verify(exactly = 1) { filmLibraryRepository.findAll() } - verify(exactly = 2) { idGenerator.generateId() } + verify(exactly = 1) { idGenerator.generateId() } verify(exactly = 1) { filmLibraryRepository.save(any()) } } @@ -131,7 +133,7 @@ class FilmLibraryServiceTest { } @Test - fun `should add film to existing library`() { + fun `should add film as a new library entry when another film already exists`() { val userId = UUID.randomUUID() val oldFilmId = UUID.randomUUID() val newFilmId = UUID.randomUUID() @@ -144,16 +146,24 @@ class FilmLibraryServiceTest { isViewed = true, ) val command = AddFilmToLibraryCommand(userId = userId, filmId = newFilmId) - val updatedLibrary = existingLibrary.copy(filmId = newFilmId, isViewed = false) + val createdLibrary = + FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = newFilmId, + comment = null, + isViewed = false, + ) every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + every { idGenerator.generateId() } returns createdLibrary.id every { filmLibraryRepository.save( match { - it.filmId == newFilmId && it.isViewed == false + it.id == createdLibrary.id && it.userId == userId && it.filmId == newFilmId && it.isViewed == false }, ) - } returns updatedLibrary + } returns createdLibrary val result = filmLibraryService.addFilm(command) @@ -161,7 +171,7 @@ class FilmLibraryServiceTest { assertEquals(false, result.isViewed) verify(exactly = 1) { filmLibraryRepository.findAll() } - verify(exactly = 0) { idGenerator.generateId() } + verify(exactly = 1) { idGenerator.generateId() } verify(exactly = 1) { filmLibraryRepository.save(any()) } } @@ -253,13 +263,13 @@ class FilmLibraryServiceTest { libraryId = wrongLibraryId, ) - every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + every { filmLibraryRepository.findById(wrongLibraryId) } returns null assertThrows { filmLibraryService.removeFilm(command) } - verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 1) { filmLibraryRepository.findById(wrongLibraryId) } verify(exactly = 0) { filmLibraryRepository.deleteById(any()) } }