refactor: обновить use case слои и интеграцию Jellyfin
This commit is contained in:
@@ -6,9 +6,12 @@ import com.project.movienight.domain.exception.EntityNotFoundException
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestControllerAdvice
|
||||
class ApiExceptionHandler {
|
||||
@@ -50,6 +53,44 @@ class ApiExceptionHandler {
|
||||
)
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
fun handleValidationException(exception: MethodArgumentNotValidException): ErrorResponse {
|
||||
val traceId = currentTraceId()
|
||||
val details =
|
||||
exception
|
||||
.bindingResult
|
||||
.fieldErrors
|
||||
.joinToString("; ") { error -> "${error.field}: ${error.defaultMessage}" }
|
||||
.ifBlank { "Invalid request" }
|
||||
log.warn("Validation error: traceId='{}', message='{}'", traceId, details)
|
||||
|
||||
return ErrorResponse(
|
||||
message = details,
|
||||
traceId = traceId,
|
||||
)
|
||||
}
|
||||
|
||||
@ExceptionHandler(ResponseStatusException::class)
|
||||
fun handleResponseStatusException(exception: ResponseStatusException): ResponseEntity<ErrorResponse> {
|
||||
val traceId = currentTraceId()
|
||||
log.warn(
|
||||
"HTTP error: traceId='{}', status='{}', message='{}'",
|
||||
traceId,
|
||||
exception.statusCode,
|
||||
exception.reason,
|
||||
)
|
||||
|
||||
return ResponseEntity
|
||||
.status(exception.statusCode)
|
||||
.body(
|
||||
ErrorResponse(
|
||||
message = exception.reason ?: exception.message,
|
||||
traceId = traceId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
fun handleUnexpectedException(exception: Exception): ErrorResponse {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.domain.exception.DomainException
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
|
||||
fun parseContentType(value: String): ContentType =
|
||||
runCatching { ContentType.valueOf(value.uppercase()) }
|
||||
.getOrElse { throw DomainException("Unsupported content type: $value") }
|
||||
|
||||
fun parseOptionalContentType(value: String?): ContentType? = value?.let { parseContentType(it) }
|
||||
@@ -4,13 +4,9 @@ import com.project.movienight.adapters.web.dto.request.CreateFilmRequest
|
||||
import com.project.movienight.adapters.web.dto.request.EditFilmRequest
|
||||
import com.project.movienight.adapters.web.dto.response.FilmResponse
|
||||
import com.project.movienight.application.ports.input.CreateFilmCommand
|
||||
import com.project.movienight.application.ports.input.CreateFilmUseCase
|
||||
import com.project.movienight.application.ports.input.DeleteFilmUseCase
|
||||
import com.project.movienight.application.ports.input.EditFilmCommand
|
||||
import com.project.movienight.application.ports.input.EditFilmUseCase
|
||||
import com.project.movienight.application.ports.input.GetAllFilmsUseCase
|
||||
import com.project.movienight.application.ports.input.GetFilmByIdUseCase
|
||||
import com.project.movienight.application.ports.input.SearchFilmByTitleUseCase
|
||||
import com.project.movienight.application.ports.input.FilmUseCase
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
@@ -25,33 +21,22 @@ 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(
|
||||
private val createFilmUseCase: CreateFilmUseCase,
|
||||
private val editFilmUseCase: EditFilmUseCase,
|
||||
private val deleteFilmUseCase: DeleteFilmUseCase,
|
||||
private val getFilmByIdUseCase: GetFilmByIdUseCase,
|
||||
private val getAllFilmsUseCase: GetAllFilmsUseCase,
|
||||
private val searchFilmByTitleUseCase: SearchFilmByTitleUseCase,
|
||||
private val filmUseCase: FilmUseCase,
|
||||
) {
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun create(
|
||||
@RequestBody request: CreateFilmRequest,
|
||||
@Valid @RequestBody request: CreateFilmRequest,
|
||||
): FilmResponse =
|
||||
FilmResponse.fromDomain(
|
||||
createFilmUseCase.create(
|
||||
filmUseCase.create(
|
||||
CreateFilmCommand(
|
||||
title = request.title,
|
||||
description = request.description,
|
||||
contentType = request.contentType.toContentTypeOrFilm(),
|
||||
contentType = parseContentType(request.contentType),
|
||||
releaseYear = request.releaseYear,
|
||||
genres = request.genres,
|
||||
cast = request.cast,
|
||||
@@ -68,16 +53,16 @@ class FilmController(
|
||||
@PatchMapping("/{id}")
|
||||
fun edit(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody request: EditFilmRequest,
|
||||
@Valid @RequestBody request: EditFilmRequest,
|
||||
): FilmResponse =
|
||||
FilmResponse.fromDomain(
|
||||
editFilmUseCase.edit(
|
||||
filmUseCase.edit(
|
||||
id = id,
|
||||
command =
|
||||
EditFilmCommand(
|
||||
title = request.title,
|
||||
description = request.description,
|
||||
contentType = request.contentType.toContentTypeOrFilm(),
|
||||
contentType = parseContentType(request.contentType),
|
||||
releaseYear = request.releaseYear,
|
||||
genres = request.genres,
|
||||
cast = request.cast,
|
||||
@@ -95,21 +80,21 @@ class FilmController(
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun delete(
|
||||
@PathVariable id: UUID,
|
||||
) = deleteFilmUseCase.delete(id)
|
||||
) = filmUseCase.delete(id)
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getById(
|
||||
@PathVariable id: UUID,
|
||||
): FilmResponse = FilmResponse.fromDomain(getFilmByIdUseCase.getById(id))
|
||||
): FilmResponse = FilmResponse.fromDomain(filmUseCase.getById(id))
|
||||
|
||||
@GetMapping
|
||||
fun getAll(): List<FilmResponse> = getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) }
|
||||
fun getAll(): List<FilmResponse> = filmUseCase.getAll().map { FilmResponse.fromDomain(it) }
|
||||
|
||||
@GetMapping("/search")
|
||||
fun searchByTitle(
|
||||
@RequestParam title: String,
|
||||
): ResponseEntity<FilmResponse> {
|
||||
val film = searchFilmByTitleUseCase.searchByTitle(title)
|
||||
val film = filmUseCase.searchByTitle(title)
|
||||
return if (film != null) {
|
||||
ResponseEntity.ok(FilmResponse.fromDomain(film))
|
||||
} else {
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.adapters.web.dto.request.CreateFilmLibraryRequest
|
||||
import com.project.movienight.adapters.web.dto.response.FilmLibraryResponse
|
||||
import com.project.movienight.adapters.web.dto.response.FilmLibraryEntryResponse
|
||||
import com.project.movienight.adapters.web.dto.response.FilmResponse
|
||||
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.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.FilmLibraryUseCase
|
||||
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
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
@@ -31,52 +19,29 @@ import java.util.UUID
|
||||
@RestController
|
||||
@RequestMapping("/api/users/{userId}/library")
|
||||
class FilmLibraryController(
|
||||
private val createFilmLibraryUseCase: CreateFilmLibraryUseCase,
|
||||
private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase,
|
||||
private val markFilmViewedUseCase: MarkFilmViewedUseCase,
|
||||
private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase,
|
||||
private val getFilmLibraryUseCase: GetFilmLibraryUseCase,
|
||||
private val getAllFilmsUseCase: GetAllFilmsUseCase,
|
||||
private val listFilmLibraryEntriesUseCase: ListFilmLibraryEntriesUseCase,
|
||||
private val filmLibraryUseCase: FilmLibraryUseCase,
|
||||
) {
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun create(
|
||||
@PathVariable userId: UUID,
|
||||
@RequestBody request: CreateFilmLibraryRequest,
|
||||
): FilmLibraryResponse =
|
||||
FilmLibraryResponse.fromDomain(
|
||||
createFilmLibraryUseCase.create(
|
||||
CreateFilmLibraryCommand(
|
||||
userId = userId,
|
||||
name = request.name,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@GetMapping
|
||||
fun get(
|
||||
@PathVariable userId: UUID,
|
||||
): FilmLibraryResponse =
|
||||
FilmLibraryResponse.fromDomain(
|
||||
getFilmLibraryUseCase.getLibrary(
|
||||
GetFilmLibraryQuery(userId = userId),
|
||||
),
|
||||
)
|
||||
|
||||
@GetMapping("/entries")
|
||||
fun list(
|
||||
@PathVariable userId: UUID,
|
||||
): List<FilmLibraryResponse> = listFilmLibraryEntriesUseCase.list(userId).map { FilmLibraryResponse.fromDomain(it) }
|
||||
): List<FilmLibraryEntryResponse> =
|
||||
filmLibraryUseCase
|
||||
.list(userId)
|
||||
.map { entry -> FilmLibraryEntryResponse.fromDomain(entry) }
|
||||
|
||||
@GetMapping("/entries")
|
||||
fun listEntries(
|
||||
@PathVariable userId: UUID,
|
||||
): List<FilmLibraryEntryResponse> = list(userId)
|
||||
|
||||
@PostMapping("/films/{filmId}")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun addFilm(
|
||||
@PathVariable userId: UUID,
|
||||
@PathVariable filmId: UUID,
|
||||
): FilmLibraryResponse =
|
||||
FilmLibraryResponse.fromDomain(
|
||||
addFilmToLibraryUseCase.addFilm(
|
||||
): FilmLibraryEntryResponse =
|
||||
FilmLibraryEntryResponse.fromDomain(
|
||||
filmLibraryUseCase.addFilm(
|
||||
AddFilmToLibraryCommand(
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
@@ -88,9 +53,9 @@ class FilmLibraryController(
|
||||
fun markViewed(
|
||||
@PathVariable userId: UUID,
|
||||
@PathVariable filmId: UUID,
|
||||
): FilmLibraryResponse =
|
||||
FilmLibraryResponse.fromDomain(
|
||||
markFilmViewedUseCase.markViewed(
|
||||
): FilmLibraryEntryResponse =
|
||||
FilmLibraryEntryResponse.fromDomain(
|
||||
filmLibraryUseCase.markViewed(
|
||||
MarkFilmViewedCommand(
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
@@ -104,7 +69,7 @@ class FilmLibraryController(
|
||||
@PathVariable userId: UUID,
|
||||
@PathVariable filmId: UUID,
|
||||
) {
|
||||
removeFilmFromLibraryUseCase.removeFilm(
|
||||
filmLibraryUseCase.removeFilm(
|
||||
RemoveFilmFromLibraryCommand(
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
@@ -115,27 +80,5 @@ class FilmLibraryController(
|
||||
@GetMapping("/available-films")
|
||||
fun getAvailableFilms(
|
||||
@PathVariable userId: UUID,
|
||||
): List<FilmResponse> {
|
||||
val userLibrary =
|
||||
runCatching {
|
||||
getFilmLibraryUseCase.getLibrary(
|
||||
GetFilmLibraryQuery(userId = userId),
|
||||
)
|
||||
}.onFailure { exception ->
|
||||
if (exception !is EntityNotFoundException) {
|
||||
throw exception
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
val allFilms = getAllFilmsUseCase.getAll()
|
||||
|
||||
val availableFilms =
|
||||
if (userLibrary != null) {
|
||||
allFilms.filter { it.id != userLibrary.filmId }
|
||||
} else {
|
||||
allFilms
|
||||
}
|
||||
|
||||
return availableFilms.map { FilmResponse.fromDomain(it) }
|
||||
}
|
||||
): List<FilmResponse> = filmLibraryUseCase.listAvailableFilms(userId).map { FilmResponse.fromDomain(it) }
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.adapters.web.dto.request.RateFilmRequest
|
||||
import com.project.movienight.adapters.web.dto.response.FilmRatingResponse
|
||||
import com.project.movienight.application.ports.input.GetFilmRatingsUseCase
|
||||
import com.project.movienight.application.ports.input.FilmRatingUseCase
|
||||
import com.project.movienight.application.ports.input.RateFilmCommand
|
||||
import com.project.movienight.application.ports.input.RateFilmUseCase
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
@@ -18,18 +18,17 @@ import java.util.UUID
|
||||
@RestController
|
||||
@RequestMapping("/api/users/{userId}/ratings")
|
||||
class FilmRatingController(
|
||||
private val rateFilmUseCase: RateFilmUseCase,
|
||||
private val getFilmRatingsUseCase: GetFilmRatingsUseCase,
|
||||
private val filmRatingUseCase: FilmRatingUseCase,
|
||||
) {
|
||||
@PostMapping("/films/{filmId}")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun rate(
|
||||
@PathVariable userId: UUID,
|
||||
@PathVariable filmId: UUID,
|
||||
@RequestBody request: RateFilmRequest,
|
||||
@Valid @RequestBody request: RateFilmRequest,
|
||||
): FilmRatingResponse =
|
||||
FilmRatingResponse.fromDomain(
|
||||
rateFilmUseCase.rate(
|
||||
filmRatingUseCase.rate(
|
||||
RateFilmCommand(
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
@@ -42,5 +41,5 @@ class FilmRatingController(
|
||||
@GetMapping
|
||||
fun list(
|
||||
@PathVariable userId: UUID,
|
||||
): List<FilmRatingResponse> = getFilmRatingsUseCase.getRatings(userId).map { FilmRatingResponse.fromDomain(it) }
|
||||
): List<FilmRatingResponse> = filmRatingUseCase.getRatings(userId).map { FilmRatingResponse.fromDomain(it) }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.adapters.web.dto.request.JellyfinEventRequest
|
||||
import com.project.movienight.application.services.JellyfinEventService
|
||||
import com.project.movienight.application.ports.input.HandleJellyfinEventCommand
|
||||
import com.project.movienight.application.ports.input.JellyfinEventUseCase
|
||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||
import jakarta.validation.Valid
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
@@ -16,7 +18,7 @@ import org.springframework.web.server.ResponseStatusException
|
||||
@RestController
|
||||
@RequestMapping("/api/integrations/jellyfin")
|
||||
class JellyfinEventsController(
|
||||
private val jellyfinEventService: JellyfinEventService,
|
||||
private val jellyfinEventUseCase: JellyfinEventUseCase,
|
||||
private val properties: JellyfinIntegrationProperties,
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(JellyfinEventsController::class.java)
|
||||
@@ -25,7 +27,7 @@ class JellyfinEventsController(
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun receiveEvent(
|
||||
@RequestHeader(value = "X-MovieNight-Plugin-Token", required = false) token: String?,
|
||||
@RequestBody request: JellyfinEventRequest,
|
||||
@Valid @RequestBody request: JellyfinEventRequest,
|
||||
) {
|
||||
if (!properties.enabled) {
|
||||
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Jellyfin integration is disabled")
|
||||
@@ -43,14 +45,16 @@ class JellyfinEventsController(
|
||||
request.jellyfinUserId,
|
||||
request.itemId,
|
||||
)
|
||||
jellyfinEventService.handleEvent(
|
||||
eventId = request.eventId,
|
||||
serverId = null,
|
||||
eventType = request.eventType,
|
||||
occurredAt = request.occurredAt,
|
||||
jellyfinUserId = request.jellyfinUserId,
|
||||
itemId = request.itemId,
|
||||
payload = request.payload,
|
||||
jellyfinEventUseCase.handle(
|
||||
HandleJellyfinEventCommand(
|
||||
eventId = request.eventId,
|
||||
serverId = null,
|
||||
eventType = request.eventType,
|
||||
occurredAt = request.occurredAt,
|
||||
jellyfinUserId = request.jellyfinUserId,
|
||||
itemId = request.itemId,
|
||||
payload = request.payload,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.application.services.JellyfinSyncService
|
||||
import com.project.movienight.application.ports.input.JellyfinSyncUseCase
|
||||
import com.project.movienight.domain.model.JellyfinSyncState
|
||||
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
@@ -11,11 +11,11 @@ import org.springframework.web.bind.annotation.RestController
|
||||
@RestController
|
||||
@RequestMapping("/api/integrations/jellyfin")
|
||||
class JellyfinSyncController(
|
||||
private val jellyfinSyncService: JellyfinSyncService,
|
||||
private val jellyfinSyncUseCase: JellyfinSyncUseCase,
|
||||
) {
|
||||
@PostMapping("/sync")
|
||||
fun syncNow(): JellyfinSyncSummary = jellyfinSyncService.syncNow()
|
||||
fun syncNow(): JellyfinSyncSummary = jellyfinSyncUseCase.syncNow()
|
||||
|
||||
@GetMapping("/sync-state")
|
||||
fun syncState(): List<JellyfinSyncState> = jellyfinSyncService.getSyncStates()
|
||||
fun syncState(): List<JellyfinSyncState> = jellyfinSyncUseCase.getSyncStates()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.project.movienight.application.ports.input.RecommendationQuery
|
||||
import com.project.movienight.application.ports.input.RejectRecommendationCommand
|
||||
import com.project.movienight.application.ports.input.RejectRecommendationUseCase
|
||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
@@ -40,7 +39,7 @@ class RecommendationController(
|
||||
.recommend(
|
||||
RecommendationQuery(
|
||||
userId = userId,
|
||||
contentType = contentType?.let { runCatching { ContentType.valueOf(it.uppercase()) }.getOrNull() },
|
||||
contentType = parseOptionalContentType(contentType),
|
||||
mood = mood,
|
||||
libraryOnly = libraryOnly,
|
||||
limit = limit,
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.adapters.security.UserPrincipal
|
||||
import com.project.movienight.adapters.web.dto.request.CreateUserRequest
|
||||
import com.project.movienight.adapters.web.dto.request.EditUserRequest
|
||||
import com.project.movienight.adapters.web.dto.response.UserResponse
|
||||
import com.project.movienight.application.ports.input.CreateUserCommand
|
||||
import com.project.movienight.application.ports.input.CreateUserUseCase
|
||||
import com.project.movienight.application.ports.input.DeleteUserUseCase
|
||||
import com.project.movienight.application.ports.input.EditUserCommand
|
||||
import com.project.movienight.application.ports.input.EditUserUseCase
|
||||
import com.project.movienight.application.ports.input.GetAllUsersUseCase
|
||||
import com.project.movienight.application.ports.input.GetUserByIdUseCase
|
||||
import com.project.movienight.application.ports.input.UserUseCase
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
@@ -25,19 +24,15 @@ import java.util.UUID
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
class UserController(
|
||||
private val createUserUseCase: CreateUserUseCase,
|
||||
private val editUserUseCase: EditUserUseCase,
|
||||
private val deleteUserUseCase: DeleteUserUseCase,
|
||||
private val getUserByIdUseCase: GetUserByIdUseCase,
|
||||
private val getAllUsersUseCase: GetAllUsersUseCase,
|
||||
private val userUseCase: UserUseCase,
|
||||
) {
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun create(
|
||||
@RequestBody request: CreateUserRequest,
|
||||
@Valid @RequestBody request: CreateUserRequest,
|
||||
): UserResponse =
|
||||
UserResponse.fromDomain(
|
||||
createUserUseCase.create(
|
||||
userUseCase.create(
|
||||
CreateUserCommand(
|
||||
name = request.name,
|
||||
email = request.email,
|
||||
@@ -46,20 +41,25 @@ class UserController(
|
||||
)
|
||||
|
||||
@GetMapping
|
||||
fun getAll(): List<UserResponse> = getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) }
|
||||
fun getAll(): List<UserResponse> = userUseCase.getAll().map { UserResponse.fromDomain(it) }
|
||||
|
||||
@GetMapping("/me")
|
||||
fun getMe(
|
||||
@AuthenticationPrincipal principal: UserPrincipal,
|
||||
): UserResponse = UserResponse.fromDomain(userUseCase.getById(principal.getId()))
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getById(
|
||||
@PathVariable id: UUID,
|
||||
): UserResponse = UserResponse.fromDomain(getUserByIdUseCase.getById(id))
|
||||
): UserResponse = UserResponse.fromDomain(userUseCase.getById(id))
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
fun edit(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody request: EditUserRequest,
|
||||
@Valid @RequestBody request: EditUserRequest,
|
||||
): UserResponse =
|
||||
UserResponse.fromDomain(
|
||||
editUserUseCase.edit(
|
||||
userUseCase.edit(
|
||||
id = id,
|
||||
command =
|
||||
EditUserCommand(
|
||||
@@ -73,5 +73,5 @@ class UserController(
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun delete(
|
||||
@PathVariable id: UUID,
|
||||
) = deleteUserUseCase.delete(id)
|
||||
) = userUseCase.delete(id)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.adapters.web.dto.request.UpsertUserPreferencesRequest
|
||||
import com.project.movienight.adapters.web.dto.response.UserPreferencesResponse
|
||||
import com.project.movienight.application.ports.input.GetUserPreferencesUseCase
|
||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesCommand
|
||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesUseCase
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
import com.project.movienight.application.ports.input.UserPreferencesUseCase
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
@@ -17,16 +16,15 @@ import java.util.UUID
|
||||
@RestController
|
||||
@RequestMapping("/api/users/{userId}/preferences")
|
||||
class UserPreferencesController(
|
||||
private val upsertUserPreferencesUseCase: UpsertUserPreferencesUseCase,
|
||||
private val getUserPreferencesUseCase: GetUserPreferencesUseCase,
|
||||
private val userPreferencesUseCase: UserPreferencesUseCase,
|
||||
) {
|
||||
@PutMapping
|
||||
fun upsert(
|
||||
@PathVariable userId: UUID,
|
||||
@RequestBody request: UpsertUserPreferencesRequest,
|
||||
@Valid @RequestBody request: UpsertUserPreferencesRequest,
|
||||
): UserPreferencesResponse =
|
||||
UserPreferencesResponse.fromDomain(
|
||||
upsertUserPreferencesUseCase.upsert(
|
||||
userPreferencesUseCase.upsert(
|
||||
UpsertUserPreferencesCommand(
|
||||
userId = userId,
|
||||
weightedGenres = request.weightedGenres,
|
||||
@@ -35,13 +33,7 @@ class UserPreferencesController(
|
||||
castAndDirectors = request.castAndDirectors,
|
||||
moods = request.moods,
|
||||
contentTypes =
|
||||
request.contentTypes.mapNotNull {
|
||||
runCatching {
|
||||
ContentType.valueOf(
|
||||
it,
|
||||
)
|
||||
}.getOrNull()
|
||||
},
|
||||
request.contentTypes.map { parseContentType(it) },
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -49,5 +41,5 @@ class UserPreferencesController(
|
||||
@GetMapping
|
||||
fun get(
|
||||
@PathVariable userId: UUID,
|
||||
): UserPreferencesResponse? = getUserPreferencesUseCase.get(userId)?.let { UserPreferencesResponse.fromDomain(it) }
|
||||
): UserPreferencesResponse? = userPreferencesUseCase.get(userId)?.let { UserPreferencesResponse.fromDomain(it) }
|
||||
}
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
data class CreateFilmLibraryRequest(
|
||||
val name: String = "My films",
|
||||
)
|
||||
@@ -1,14 +1,29 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
import jakarta.validation.constraints.Max
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Size
|
||||
|
||||
data class CreateFilmRequest(
|
||||
@field:NotBlank
|
||||
@field:Size(max = 255)
|
||||
val title: String,
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
@field:NotBlank
|
||||
val contentType: String = "FILM",
|
||||
@field:Min(1888)
|
||||
@field:Max(3000)
|
||||
val releaseYear: Int? = null,
|
||||
val genres: List<String> = emptyList(),
|
||||
val cast: List<String> = emptyList(),
|
||||
val directors: List<String> = emptyList(),
|
||||
@field:Min(0)
|
||||
@field:Max(10)
|
||||
val imdbRating: Double? = null,
|
||||
@field:Min(0)
|
||||
@field:Max(10)
|
||||
val platformRating: Double? = null,
|
||||
val externalUrl: String? = null,
|
||||
val jellyfinItemId: String? = null,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
import jakarta.validation.constraints.Email
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Size
|
||||
|
||||
data class CreateUserRequest(
|
||||
@field:NotBlank
|
||||
@field:Size(max = 255)
|
||||
val name: String,
|
||||
@field:Email
|
||||
@field:NotBlank
|
||||
@field:Size(max = 320)
|
||||
val email: String,
|
||||
)
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
import jakarta.validation.constraints.Max
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Size
|
||||
|
||||
data class EditFilmRequest(
|
||||
@field:NotBlank
|
||||
@field:Size(max = 255)
|
||||
val title: String,
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
@field:NotBlank
|
||||
val contentType: String = "FILM",
|
||||
@field:Min(1888)
|
||||
@field:Max(3000)
|
||||
val releaseYear: Int? = null,
|
||||
val genres: List<String> = emptyList(),
|
||||
val cast: List<String> = emptyList(),
|
||||
val directors: List<String> = emptyList(),
|
||||
@field:Min(0)
|
||||
@field:Max(10)
|
||||
val imdbRating: Double? = null,
|
||||
@field:Min(0)
|
||||
@field:Max(10)
|
||||
val platformRating: Double? = null,
|
||||
val externalUrl: String? = null,
|
||||
val jellyfinItemId: String? = null,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Size
|
||||
|
||||
data class EditUserRequest(
|
||||
@field:NotBlank
|
||||
@field:Size(max = 255)
|
||||
val name: String,
|
||||
@field:Size(max = 255)
|
||||
val jellyfinUserId: String? = null,
|
||||
)
|
||||
|
||||
+5
@@ -1,18 +1,23 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class JellyfinEventRequest(
|
||||
@JsonProperty("event_id")
|
||||
@field:NotBlank
|
||||
val eventId: String,
|
||||
@JsonProperty("event_type")
|
||||
@field:NotBlank
|
||||
val eventType: String,
|
||||
@JsonProperty("occurred_at")
|
||||
val occurredAt: OffsetDateTime,
|
||||
@JsonProperty("jellyfin_user_id")
|
||||
@field:NotBlank
|
||||
val jellyfinUserId: String,
|
||||
@JsonProperty("item_id")
|
||||
@field:NotBlank
|
||||
val itemId: String,
|
||||
@JsonProperty("payload_version")
|
||||
val payloadVersion: Int = 1,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package com.project.movienight.adapters.web.dto.request
|
||||
|
||||
import jakarta.validation.constraints.Max
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.Size
|
||||
|
||||
data class RateFilmRequest(
|
||||
@field:Min(1)
|
||||
@field:Max(10)
|
||||
val score: Int,
|
||||
@field:Size(max = 2048)
|
||||
val note: String? = null,
|
||||
)
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.project.movienight.adapters.web.dto.response
|
||||
|
||||
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
data class FilmLibraryEntryResponse(
|
||||
val id: UUID,
|
||||
val userId: UUID,
|
||||
val filmId: UUID,
|
||||
val comment: String?,
|
||||
val isViewed: Boolean,
|
||||
val watchedAt: LocalDateTime?,
|
||||
) {
|
||||
companion object {
|
||||
fun fromDomain(entry: FilmLibraryEntry): FilmLibraryEntryResponse =
|
||||
FilmLibraryEntryResponse(
|
||||
id = entry.id,
|
||||
userId = entry.userId,
|
||||
filmId = entry.filmId,
|
||||
comment = entry.comment,
|
||||
isViewed = entry.isViewed,
|
||||
watchedAt = entry.watchedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.project.movienight.adapters.web.dto.response
|
||||
|
||||
import com.project.movienight.domain.model.FilmLibrary
|
||||
import java.util.UUID
|
||||
|
||||
data class FilmLibraryResponse(
|
||||
val id: UUID,
|
||||
val userId: UUID,
|
||||
val filmId: UUID,
|
||||
val comment: String?,
|
||||
val isViewed: Boolean,
|
||||
val watchedAt: java.time.LocalDateTime?,
|
||||
) {
|
||||
companion object {
|
||||
fun fromDomain(filmLibrary: FilmLibrary): FilmLibraryResponse =
|
||||
FilmLibraryResponse(
|
||||
id = filmLibrary.id,
|
||||
userId = filmLibrary.userId,
|
||||
filmId = filmLibrary.filmId,
|
||||
comment = filmLibrary.comment,
|
||||
isViewed = filmLibrary.isViewed,
|
||||
watchedAt = filmLibrary.watchedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user