Реализована рекомендательная система: добавлены гибридный скоринг фильмов, события рекомендаций, accept/reject endpoints, watchUrl для Jellyfin, API DTO ответа и логирование выдачи рекомендаций.
This commit is contained in:
@@ -1,34 +1,91 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.adapters.web.dto.response.RecommendationEventResponse
|
||||
import com.project.movienight.adapters.web.dto.response.RecommendationResponse
|
||||
import com.project.movienight.application.ports.input.AcceptRecommendationCommand
|
||||
import com.project.movienight.application.ports.input.AcceptRecommendationUseCase
|
||||
import com.project.movienight.application.ports.input.GetRecommendationsUseCase
|
||||
import com.project.movienight.application.ports.input.RejectRecommendationCommand
|
||||
import com.project.movienight.application.ports.input.RejectRecommendationUseCase
|
||||
import com.project.movienight.application.ports.input.RecommendationQuery
|
||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
import com.project.movienight.domain.model.RecommendationResult
|
||||
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.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users/{userId}/recommendations")
|
||||
class RecommendationController(
|
||||
private val getRecommendationsUseCase: GetRecommendationsUseCase,
|
||||
private val acceptRecommendationUseCase: AcceptRecommendationUseCase,
|
||||
private val rejectRecommendationUseCase: RejectRecommendationUseCase,
|
||||
private val jellyfinProperties: JellyfinIntegrationProperties,
|
||||
) {
|
||||
@GetMapping
|
||||
fun recommend(
|
||||
@PathVariable userId: UUID,
|
||||
@RequestParam(required = false) contentType: String?,
|
||||
@RequestParam(required = false) mood: String?,
|
||||
@RequestParam(required = false, defaultValue = "false") libraryOnly: Boolean,
|
||||
@RequestParam(required = false, defaultValue = "10") limit: Int,
|
||||
): List<RecommendationResult> =
|
||||
): List<RecommendationResponse> =
|
||||
getRecommendationsUseCase.recommend(
|
||||
RecommendationQuery(
|
||||
userId = userId,
|
||||
contentType = contentType?.let { runCatching { ContentType.valueOf(it) }.getOrNull() },
|
||||
contentType = contentType?.let { runCatching { ContentType.valueOf(it.uppercase()) }.getOrNull() },
|
||||
mood = mood,
|
||||
libraryOnly = libraryOnly,
|
||||
limit = limit,
|
||||
),
|
||||
).map { recommendation ->
|
||||
RecommendationResponse.fromDomain(
|
||||
recommendation = recommendation,
|
||||
watchUrl = buildWatchUrl(recommendation.film.jellyfinItemId),
|
||||
)
|
||||
}
|
||||
|
||||
@PostMapping("/{filmId}/accept")
|
||||
fun accept(
|
||||
@PathVariable userId: UUID,
|
||||
@PathVariable filmId: UUID,
|
||||
): RecommendationEventResponse =
|
||||
RecommendationEventResponse.fromDomain(
|
||||
acceptRecommendationUseCase.accept(
|
||||
AcceptRecommendationCommand(
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@PostMapping("/{filmId}/reject")
|
||||
fun reject(
|
||||
@PathVariable userId: UUID,
|
||||
@PathVariable filmId: UUID,
|
||||
): RecommendationEventResponse =
|
||||
RecommendationEventResponse.fromDomain(
|
||||
rejectRecommendationUseCase.reject(
|
||||
RejectRecommendationCommand(
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun buildWatchUrl(jellyfinItemId: String?): String? {
|
||||
if (jellyfinItemId.isNullOrBlank() || jellyfinProperties.webUrl.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val baseUrl = jellyfinProperties.webUrl.trimEnd('/')
|
||||
val encodedItemId = URLEncoder.encode(jellyfinItemId, StandardCharsets.UTF_8)
|
||||
return "$baseUrl/web/#/details?id=$encodedItemId"
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.project.movienight.adapters.web.dto.response
|
||||
|
||||
import com.project.movienight.domain.model.RecommendationEvent
|
||||
import com.project.movienight.domain.model.RecommendationEventType
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
data class RecommendationEventResponse(
|
||||
val id: UUID,
|
||||
val userId: UUID,
|
||||
val filmId: UUID,
|
||||
val eventType: RecommendationEventType,
|
||||
val score: Double?,
|
||||
val createdAt: LocalDateTime,
|
||||
) {
|
||||
companion object {
|
||||
fun fromDomain(event: RecommendationEvent): RecommendationEventResponse =
|
||||
RecommendationEventResponse(
|
||||
id = event.id,
|
||||
userId = event.userId,
|
||||
filmId = event.filmId,
|
||||
eventType = event.eventType,
|
||||
score = event.score,
|
||||
createdAt = event.createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.project.movienight.adapters.web.dto.response
|
||||
|
||||
import com.project.movienight.domain.model.RecommendationResult
|
||||
import java.util.UUID
|
||||
|
||||
data class RecommendationResponse(
|
||||
val filmId: UUID,
|
||||
val title: String,
|
||||
val score: Double,
|
||||
val reasons: List<String>,
|
||||
val jellyfinItemId: String?,
|
||||
val watchUrl: String?,
|
||||
val film: FilmResponse,
|
||||
) {
|
||||
companion object {
|
||||
fun fromDomain(
|
||||
recommendation: RecommendationResult,
|
||||
watchUrl: String?,
|
||||
): RecommendationResponse {
|
||||
val film = recommendation.film
|
||||
return RecommendationResponse(
|
||||
filmId = film.id,
|
||||
title = film.title,
|
||||
score = recommendation.score,
|
||||
reasons = recommendation.reasons,
|
||||
jellyfinItemId = film.jellyfinItemId,
|
||||
watchUrl = watchUrl,
|
||||
film = FilmResponse.fromDomain(film),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user