refactor: обновить use case слои и интеграцию Jellyfin

This commit is contained in:
skettiks
2026-05-22 14:43:26 +03:00
parent f4bccf4bf0
commit ef995422dd
67 changed files with 1144 additions and 995 deletions
@@ -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) }
}
@@ -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,
)
@@ -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,
)
@@ -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,
)
}
}
@@ -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,
)
}
}