feat: add recommendation system #53
@@ -5,9 +5,9 @@ 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.RecommendationQuery
|
||||
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 org.springframework.web.bind.annotation.GetMapping
|
||||
@@ -36,20 +36,21 @@ class RecommendationController(
|
||||
@RequestParam(required = false, defaultValue = "false") libraryOnly: Boolean,
|
||||
@RequestParam(required = false, defaultValue = "10") limit: Int,
|
||||
): List<RecommendationResponse> =
|
||||
getRecommendationsUseCase.recommend(
|
||||
RecommendationQuery(
|
||||
userId = userId,
|
||||
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),
|
||||
)
|
||||
}
|
||||
getRecommendationsUseCase
|
||||
.recommend(
|
||||
RecommendationQuery(
|
||||
userId = userId,
|
||||
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(
|
||||
|
||||
+19
-4
@@ -4,9 +4,9 @@ import com.project.movienight.adapters.metrics.BusinessMetricsService
|
||||
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.RecommendationQuery
|
||||
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.application.ports.output.FilmLibraryRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
@@ -179,7 +179,15 @@ class RecommendationService(
|
||||
preferences?.eras.orEmpty().forEach { profile.add(feature("era", it), PREFERENCE_ERA_WEIGHT) }
|
||||
preferences?.castAndDirectors.orEmpty().forEach { profile.add(feature("person", it), PREFERENCE_PERSON_WEIGHT) }
|
||||
preferences?.moods.orEmpty().forEach { profile.add(feature("mood", it), PREFERENCE_MOOD_WEIGHT) }
|
||||
preferences?.contentTypes.orEmpty().forEach { profile.add(feature("type", it.name), PREFERENCE_CONTENT_TYPE_WEIGHT) }
|
||||
preferences
|
||||
?.contentTypes
|
||||
.orEmpty()
|
||||
.forEach {
|
||||
profile.add(
|
||||
feature("type", it.name),
|
||||
PREFERENCE_CONTENT_TYPE_WEIGHT,
|
||||
)
|
||||
}
|
||||
|
||||
ratings.forEach { rating ->
|
||||
val film = filmsById[rating.filmId] ?: return@forEach
|
||||
@@ -309,7 +317,13 @@ class RecommendationService(
|
||||
film: Film,
|
||||
preferences: UserPreferences?,
|
||||
): Double {
|
||||
val preferredGenres = preferences?.weightedGenres.orEmpty().keys.map(::normalize).toSet()
|
||||
val preferredGenres =
|
||||
preferences
|
||||
?.weightedGenres
|
||||
.orEmpty()
|
||||
.keys
|
||||
.map(::normalize)
|
||||
.toSet()
|
||||
val filmGenres = film.genres.map(::normalize).toSet()
|
||||
return when {
|
||||
preferredGenres.isEmpty() -> BASE_DIVERSITY_SCORE
|
||||
@@ -423,7 +437,8 @@ class RecommendationService(
|
||||
return dot / (leftNorm * rightNorm)
|
||||
}
|
||||
|
||||
private fun roundScore(score: Double): Double = kotlin.math.round(score * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR
|
||||
private fun roundScore(score: Double): Double =
|
||||
kotlin.math.round(score * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR
|
||||
|
||||
private fun Iterable<Double>.averageOrNull(): Double? {
|
||||
val values = toList()
|
||||
|
||||
Reference in New Issue
Block a user