refactor: обновить use case слои и интеграцию Jellyfin
This commit is contained in:
@@ -33,6 +33,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
|
implementation(libs.spring.boot.starter.aop)
|
||||||
implementation(libs.spring.boot.starter.security)
|
implementation(libs.spring.boot.starter.security)
|
||||||
implementation(libs.spring.boot.starter.cache)
|
implementation(libs.spring.boot.starter.cache)
|
||||||
implementation(libs.spring.boot.starter.data.jdbc)
|
implementation(libs.spring.boot.starter.data.jdbc)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ mockk = "1.13.13"
|
|||||||
spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" }
|
spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" }
|
||||||
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" }
|
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" }
|
||||||
spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" }
|
spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" }
|
||||||
|
spring-boot-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop" }
|
||||||
spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" }
|
spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" }
|
||||||
spring-boot-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache" }
|
spring-boot-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache" }
|
||||||
spring-boot-starter-data-jdbc = { module = "org.springframework.boot:spring-boot-starter-data-jdbc" }
|
spring-boot-starter-data-jdbc = { module = "org.springframework.boot:spring-boot-starter-data-jdbc" }
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.project.movienight.adapters.jellyfin
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinCatalogPort
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinLibraryItemSnapshot
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinRemoteUser
|
||||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||||
import com.project.movienight.domain.model.ContentType
|
import com.project.movienight.domain.model.ContentType
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -11,39 +14,18 @@ import java.net.http.HttpRequest
|
|||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
data class JellyfinRemoteUser(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class JellyfinLibraryItemSnapshot(
|
|
||||||
val jellyfinItemId: String,
|
|
||||||
val title: String,
|
|
||||||
val description: String,
|
|
||||||
val contentType: ContentType,
|
|
||||||
val releaseYear: Int?,
|
|
||||||
val genres: List<String>,
|
|
||||||
val cast: List<String>,
|
|
||||||
val directors: List<String>,
|
|
||||||
val platformRating: Double?,
|
|
||||||
val imdbRating: Double?,
|
|
||||||
val externalUrl: String?,
|
|
||||||
val jellyfinLibraryId: String?,
|
|
||||||
val isPlayed: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class JellyfinApiClient(
|
class JellyfinApiClient(
|
||||||
private val properties: JellyfinIntegrationProperties,
|
private val properties: JellyfinIntegrationProperties,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
) {
|
) : JellyfinCatalogPort {
|
||||||
private val httpClient: HttpClient =
|
private val httpClient: HttpClient =
|
||||||
HttpClient
|
HttpClient
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.connectTimeout(Duration.ofMillis(properties.requestTimeoutMs))
|
.connectTimeout(Duration.ofMillis(properties.requestTimeoutMs))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun fetchUsers(): List<JellyfinRemoteUser> =
|
override fun fetchUsers(): List<JellyfinRemoteUser> =
|
||||||
request("Users")
|
request("Users")
|
||||||
.asItems()
|
.asItems()
|
||||||
.mapNotNull { node ->
|
.mapNotNull { node ->
|
||||||
@@ -51,7 +33,7 @@ class JellyfinApiClient(
|
|||||||
JellyfinRemoteUser(id = id, name = node.fieldText("Name") ?: id)
|
JellyfinRemoteUser(id = id, name = node.fieldText("Name") ?: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchLibraryItems(userId: String): List<JellyfinLibraryItemSnapshot> =
|
override fun fetchLibraryItems(userId: String): List<JellyfinLibraryItemSnapshot> =
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
request(
|
request(
|
||||||
"Users/$userId/Items?Recursive=true&IncludeItemTypes=Movie,Series,Episode&Fields=Genres,People,ProviderIds,Overview,ProductionYear,CommunityRating,OfficialRating,ParentId,UserData",
|
"Users/$userId/Items?Recursive=true&IncludeItemTypes=Movie,Series,Episode&Fields=Genres,People,ProviderIds,Overview,ProductionYear,CommunityRating,OfficialRating,ParentId,UserData",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.project.movienight.adapters.metrics
|
package com.project.movienight.adapters.metrics
|
||||||
|
|
||||||
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.domain.model.JellyfinSyncSummary
|
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||||
import com.project.movienight.domain.model.RecommendationEventType
|
import com.project.movienight.domain.model.RecommendationEventType
|
||||||
import io.micrometer.core.instrument.Counter
|
import io.micrometer.core.instrument.Counter
|
||||||
@@ -11,8 +12,12 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
@Service
|
@Service
|
||||||
class BusinessMetricsService(
|
class BusinessMetricsService(
|
||||||
private val meterRegistry: MeterRegistry,
|
private val meterRegistry: MeterRegistry,
|
||||||
) {
|
) : BusinessMetricsPort {
|
||||||
private val recommendationRequests: Counter = meterRegistry.counter("business_recommendation_requests_total")
|
private val recommendationRequests: Counter = meterRegistry.counter("business_recommendation_requests_total")
|
||||||
|
private val filmsCreated: Counter = meterRegistry.counter("business_films_created_total")
|
||||||
|
private val filmsEdited: Counter = meterRegistry.counter("business_films_edited_total")
|
||||||
|
private val filmsDeleted: Counter = meterRegistry.counter("business_films_deleted_total")
|
||||||
|
private val filmsBlocked: Counter = meterRegistry.counter("business_films_blocked_total")
|
||||||
private val ratingsSubmitted: Counter = meterRegistry.counter("business_ratings_submitted_total")
|
private val ratingsSubmitted: Counter = meterRegistry.counter("business_ratings_submitted_total")
|
||||||
private val libraryEvents: Counter = meterRegistry.counter("business_library_events_total")
|
private val libraryEvents: Counter = meterRegistry.counter("business_library_events_total")
|
||||||
private val jellyfinSyncRuns: Counter = meterRegistry.counter("business_jellyfin_sync_runs_total")
|
private val jellyfinSyncRuns: Counter = meterRegistry.counter("business_jellyfin_sync_runs_total")
|
||||||
@@ -33,11 +38,27 @@ class BusinessMetricsService(
|
|||||||
|
|
||||||
private val backendWriteFailures: Counter = meterRegistry.counter("business_jellyfin_backend_write_failures_total")
|
private val backendWriteFailures: Counter = meterRegistry.counter("business_jellyfin_backend_write_failures_total")
|
||||||
|
|
||||||
fun recordRecommendationRequest() {
|
override fun recordFilmCreated() {
|
||||||
|
filmsCreated.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordFilmEdited() {
|
||||||
|
filmsEdited.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordFilmDeleted() {
|
||||||
|
filmsDeleted.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordFilmBlocked() {
|
||||||
|
filmsBlocked.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordRecommendationRequest() {
|
||||||
recommendationRequests.increment()
|
recommendationRequests.increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordRecommendationWeightsUpdated(eventType: RecommendationEventType) {
|
override fun recordRecommendationWeightsUpdated(eventType: RecommendationEventType) {
|
||||||
Counter
|
Counter
|
||||||
.builder("recommendation_weights_updated_total")
|
.builder("recommendation_weights_updated_total")
|
||||||
.tag("eventType", eventType.name)
|
.tag("eventType", eventType.name)
|
||||||
@@ -45,15 +66,15 @@ class BusinessMetricsService(
|
|||||||
.increment()
|
.increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordRatingSubmitted() {
|
override fun recordRatingSubmitted() {
|
||||||
ratingsSubmitted.increment()
|
ratingsSubmitted.increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordLibraryEvent() {
|
override fun recordLibraryEvent() {
|
||||||
libraryEvents.increment()
|
libraryEvents.increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordJellyfinSync(summary: JellyfinSyncSummary) {
|
override fun recordJellyfinSync(summary: JellyfinSyncSummary) {
|
||||||
jellyfinSyncRuns.increment()
|
jellyfinSyncRuns.increment()
|
||||||
jellyfinSyncedUsers.increment(summary.syncedUsers.toDouble())
|
jellyfinSyncedUsers.increment(summary.syncedUsers.toDouble())
|
||||||
jellyfinSkippedUsers.increment(summary.skippedUsers.toDouble())
|
jellyfinSkippedUsers.increment(summary.skippedUsers.toDouble())
|
||||||
@@ -61,15 +82,15 @@ class BusinessMetricsService(
|
|||||||
jellyfinSyncDuration.record(summary.durationMs, java.util.concurrent.TimeUnit.MILLISECONDS)
|
jellyfinSyncDuration.record(summary.durationMs, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordJellyfinSyncFailure() {
|
override fun recordJellyfinSyncFailure() {
|
||||||
jellyfinSyncFailures.increment()
|
jellyfinSyncFailures.increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordJellyfinUnmappedUser() {
|
override fun recordJellyfinUnmappedUser() {
|
||||||
jellyfinUnmappedUsersGaugeValue.incrementAndGet()
|
jellyfinUnmappedUsersGaugeValue.incrementAndGet()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recordBackendWriteFailure() {
|
override fun recordBackendWriteFailure() {
|
||||||
backendWriteFailures.increment()
|
backendWriteFailures.increment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ fun UserEntity.toDomain(): User =
|
|||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
email = email,
|
email = email,
|
||||||
library = null,
|
|
||||||
preferences = null,
|
preferences = null,
|
||||||
jellyfinUserId = jellyfinUserId,
|
jellyfinUserId = jellyfinUserId,
|
||||||
)
|
)
|
||||||
|
|||||||
+39
-36
@@ -1,18 +1,18 @@
|
|||||||
package com.project.movienight.adapters.persistence.jdbc
|
package com.project.movienight.adapters.persistence.jdbc
|
||||||
|
|
||||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import org.springframework.jdbc.core.JdbcTemplate
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class FilmLibraryRepository(
|
class FilmLibraryEntryRepository(
|
||||||
private val jdbc: JdbcTemplate,
|
private val jdbc: JdbcTemplate,
|
||||||
) : FilmLibraryRepositoryPort {
|
) : FilmLibraryEntryRepositoryPort {
|
||||||
private val filmLibraryRowMapper = { rs: ResultSet, _: Int ->
|
private val rowMapper = { rs: ResultSet, _: Int ->
|
||||||
FilmLibrary(
|
FilmLibraryEntry(
|
||||||
id = UUID.fromString(rs.getString("id")),
|
id = UUID.fromString(rs.getString("id")),
|
||||||
userId = UUID.fromString(rs.getString("user_id")),
|
userId = UUID.fromString(rs.getString("user_id")),
|
||||||
filmId = UUID.fromString(rs.getString("film_id")),
|
filmId = UUID.fromString(rs.getString("film_id")),
|
||||||
@@ -22,7 +22,7 @@ class FilmLibraryRepository(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun save(filmLibrary: FilmLibrary): FilmLibrary {
|
override fun save(entry: FilmLibraryEntry): FilmLibraryEntry {
|
||||||
val updatedRows =
|
val updatedRows =
|
||||||
jdbc.update(
|
jdbc.update(
|
||||||
"""
|
"""
|
||||||
@@ -30,12 +30,12 @@ class FilmLibraryRepository(
|
|||||||
SET user_id = ?, film_id = ?, comment = ?, is_viewed = ?, watched_at = ?
|
SET user_id = ?, film_id = ?, comment = ?, is_viewed = ?, watched_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
filmLibrary.userId,
|
entry.userId,
|
||||||
filmLibrary.filmId,
|
entry.filmId,
|
||||||
filmLibrary.comment,
|
entry.comment,
|
||||||
filmLibrary.isViewed,
|
entry.isViewed,
|
||||||
filmLibrary.watchedAt,
|
entry.watchedAt,
|
||||||
filmLibrary.id,
|
entry.id,
|
||||||
)
|
)
|
||||||
if (updatedRows == 0) {
|
if (updatedRows == 0) {
|
||||||
jdbc.update(
|
jdbc.update(
|
||||||
@@ -43,48 +43,51 @@ class FilmLibraryRepository(
|
|||||||
INSERT INTO favorites (id, user_id, film_id, comment, is_viewed, watched_at)
|
INSERT INTO favorites (id, user_id, film_id, comment, is_viewed, watched_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
filmLibrary.id,
|
entry.id,
|
||||||
filmLibrary.userId,
|
entry.userId,
|
||||||
filmLibrary.filmId,
|
entry.filmId,
|
||||||
filmLibrary.comment,
|
entry.comment,
|
||||||
filmLibrary.isViewed,
|
entry.isViewed,
|
||||||
filmLibrary.watchedAt,
|
entry.watchedAt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return filmLibrary
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findById(id: UUID): FilmLibrary? {
|
override fun findById(id: UUID): FilmLibraryEntry? =
|
||||||
val entries =
|
jdbc
|
||||||
jdbc.query(
|
.query(
|
||||||
"SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites WHERE id = ?",
|
"SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites WHERE id = ?",
|
||||||
filmLibraryRowMapper,
|
rowMapper,
|
||||||
id,
|
id,
|
||||||
)
|
).firstOrNull()
|
||||||
return entries.firstOrNull()
|
|
||||||
}
|
override fun findByUserId(userId: UUID): List<FilmLibraryEntry> =
|
||||||
|
jdbc.query(
|
||||||
|
"SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites WHERE user_id = ?",
|
||||||
|
rowMapper,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
|
||||||
override fun findByUserIdAndFilmId(
|
override fun findByUserIdAndFilmId(
|
||||||
userId: UUID,
|
userId: UUID,
|
||||||
filmId: UUID,
|
filmId: UUID,
|
||||||
): FilmLibrary? {
|
): FilmLibraryEntry? =
|
||||||
val entries =
|
jdbc
|
||||||
jdbc.query(
|
.query(
|
||||||
"""
|
"""
|
||||||
SELECT id, user_id, film_id, comment, is_viewed, watched_at
|
SELECT id, user_id, film_id, comment, is_viewed, watched_at
|
||||||
FROM favorites WHERE user_id = ? AND film_id = ?
|
FROM favorites WHERE user_id = ? AND film_id = ?
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
filmLibraryRowMapper,
|
rowMapper,
|
||||||
userId,
|
userId,
|
||||||
filmId,
|
filmId,
|
||||||
)
|
).firstOrNull()
|
||||||
return entries.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findAll(): List<FilmLibrary> =
|
override fun findAll(): List<FilmLibraryEntry> =
|
||||||
jdbc.query(
|
jdbc.query(
|
||||||
"SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites",
|
"SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites",
|
||||||
filmLibraryRowMapper,
|
rowMapper,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun deleteById(id: UUID) {
|
override fun deleteById(id: UUID) {
|
||||||
@@ -159,32 +159,6 @@ class FilmRepository(
|
|||||||
return films.firstOrNull()
|
return films.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByJellyfinLibraryId(jellyfinLibraryId: String): Film? {
|
|
||||||
val films =
|
|
||||||
jdbc.query(
|
|
||||||
"""
|
|
||||||
SELECT id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
content_type,
|
|
||||||
release_year,
|
|
||||||
genres,
|
|
||||||
cast_members,
|
|
||||||
directors,
|
|
||||||
imdb_rating,
|
|
||||||
platform_rating,
|
|
||||||
external_url,
|
|
||||||
jellyfin_item_id,
|
|
||||||
jellyfin_library_id
|
|
||||||
FROM films
|
|
||||||
WHERE jellyfin_library_id = ?
|
|
||||||
""".trimIndent(),
|
|
||||||
filmRowMapper,
|
|
||||||
jellyfinLibraryId,
|
|
||||||
)
|
|
||||||
return films.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findAll(): List<Film> =
|
override fun findAll(): List<Film> =
|
||||||
jdbc.query(
|
jdbc.query(
|
||||||
"""
|
"""
|
||||||
|
|||||||
+12
-24
@@ -1,5 +1,7 @@
|
|||||||
package com.project.movienight.adapters.persistence.jdbc
|
package com.project.movienight.adapters.persistence.jdbc
|
||||||
|
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinEventRecord
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinEventStorePort
|
||||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
|
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
@@ -7,16 +9,8 @@ import org.springframework.stereotype.Repository
|
|||||||
@Repository
|
@Repository
|
||||||
class JellyfinEventRepository(
|
class JellyfinEventRepository(
|
||||||
private val jdbc: NamedParameterJdbcTemplate,
|
private val jdbc: NamedParameterJdbcTemplate,
|
||||||
) {
|
) : JellyfinEventStorePort {
|
||||||
fun save(
|
override fun save(event: JellyfinEventRecord): Boolean {
|
||||||
eventId: String,
|
|
||||||
serverId: String?,
|
|
||||||
eventType: String,
|
|
||||||
occurredAt: java.time.OffsetDateTime?,
|
|
||||||
jellyfinUserId: String?,
|
|
||||||
jellyfinItemId: String?,
|
|
||||||
payload: String?,
|
|
||||||
): Int {
|
|
||||||
val sql =
|
val sql =
|
||||||
"""
|
"""
|
||||||
INSERT INTO jellyfin_events(event_id, server_id, event_type, occurred_at, jellyfin_user_id, jellyfin_item_id, payload)
|
INSERT INTO jellyfin_events(event_id, server_id, event_type, occurred_at, jellyfin_user_id, jellyfin_item_id, payload)
|
||||||
@@ -26,20 +20,14 @@ class JellyfinEventRepository(
|
|||||||
|
|
||||||
val params =
|
val params =
|
||||||
MapSqlParameterSource()
|
MapSqlParameterSource()
|
||||||
.addValue("eventId", eventId)
|
.addValue("eventId", event.eventId)
|
||||||
.addValue("serverId", serverId)
|
.addValue("serverId", event.serverId)
|
||||||
.addValue("eventType", eventType)
|
.addValue("eventType", event.eventType)
|
||||||
.addValue("occurredAt", occurredAt)
|
.addValue("occurredAt", event.occurredAt)
|
||||||
.addValue("jellyfinUserId", jellyfinUserId)
|
.addValue("jellyfinUserId", event.jellyfinUserId)
|
||||||
.addValue("jellyfinItemId", jellyfinItemId)
|
.addValue("jellyfinItemId", event.jellyfinItemId)
|
||||||
.addValue("payload", payload)
|
.addValue("payload", event.payload)
|
||||||
|
|
||||||
return jdbc.update(sql, params)
|
return jdbc.update(sql, params) == 1
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(eventId: String) {
|
|
||||||
val sql = "DELETE FROM jellyfin_events WHERE event_id = :eventId"
|
|
||||||
val params = MapSqlParameterSource().addValue("eventId", eventId)
|
|
||||||
jdbc.update(sql, params)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-5
@@ -4,6 +4,7 @@ import com.project.movienight.adapters.persistence.entity.UserEntity
|
|||||||
import com.project.movienight.adapters.persistence.entity.toDomain
|
import com.project.movienight.adapters.persistence.entity.toDomain
|
||||||
import com.project.movienight.adapters.persistence.entity.toEntity
|
import com.project.movienight.adapters.persistence.entity.toEntity
|
||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.AuthProvider
|
import com.project.movienight.domain.model.AuthProvider
|
||||||
import com.project.movienight.domain.model.User
|
import com.project.movienight.domain.model.User
|
||||||
import org.springframework.jdbc.core.JdbcTemplate
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
@@ -32,11 +33,9 @@ class UserRepository(
|
|||||||
|
|
||||||
val entity =
|
val entity =
|
||||||
if (existingUser != null) {
|
if (existingUser != null) {
|
||||||
val existingEntity = existingUser.toEntity()
|
|
||||||
user.toEntity(
|
user.toEntity(
|
||||||
provider = existingEntity.provider?.let { AuthProvider.valueOf(it) },
|
provider = findProviderById(user.id),
|
||||||
providerId = existingEntity.providerId,
|
providerId = findProviderIdById(user.id),
|
||||||
createdAt = existingEntity.createdAt,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
user.toEntity()
|
user.toEntity()
|
||||||
@@ -75,6 +74,70 @@ class UserRepository(
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createOAuthUser(
|
||||||
|
user: User,
|
||||||
|
provider: AuthProvider,
|
||||||
|
providerId: String,
|
||||||
|
): User {
|
||||||
|
val entity = user.toEntity(provider = provider, providerId = providerId)
|
||||||
|
val updatedRows =
|
||||||
|
jdbc.update(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET name = ?, email = ?, provider = ?, provider_id = ?, jellyfin_user_id = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""".trimIndent(),
|
||||||
|
entity.name,
|
||||||
|
entity.email,
|
||||||
|
entity.provider,
|
||||||
|
entity.providerId,
|
||||||
|
entity.jellyfinUserId,
|
||||||
|
entity.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updatedRows == 0) {
|
||||||
|
jdbc.update(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (id, name, email, provider, provider_id, jellyfin_user_id, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""".trimIndent(),
|
||||||
|
entity.id,
|
||||||
|
entity.name,
|
||||||
|
entity.email,
|
||||||
|
entity.provider,
|
||||||
|
entity.providerId,
|
||||||
|
entity.jellyfinUserId,
|
||||||
|
entity.createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return findById(user.id) ?: user
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun linkOAuthAccount(
|
||||||
|
userId: UUID,
|
||||||
|
provider: AuthProvider,
|
||||||
|
providerId: String,
|
||||||
|
): User {
|
||||||
|
val updatedRows =
|
||||||
|
jdbc.update(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET provider = ?, provider_id = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""".trimIndent(),
|
||||||
|
provider.name,
|
||||||
|
providerId,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updatedRows == 0) {
|
||||||
|
throw EntityNotFoundException(entity = "User", id = userId.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return findById(userId) ?: throw EntityNotFoundException(entity = "User", id = userId.toString())
|
||||||
|
}
|
||||||
|
|
||||||
override fun findById(id: UUID): User? {
|
override fun findById(id: UUID): User? {
|
||||||
val entities =
|
val entities =
|
||||||
jdbc.query(
|
jdbc.query(
|
||||||
@@ -88,13 +151,31 @@ class UserRepository(
|
|||||||
override fun findByEmail(email: String): User? {
|
override fun findByEmail(email: String): User? {
|
||||||
val entities =
|
val entities =
|
||||||
jdbc.query(
|
jdbc.query(
|
||||||
"SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?",
|
"""
|
||||||
|
SELECT id, name, email, provider, provider_id, jellyfin_user_id, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE email = ?
|
||||||
|
""".trimIndent(),
|
||||||
userEntityRowMapper,
|
userEntityRowMapper,
|
||||||
email,
|
email,
|
||||||
)
|
)
|
||||||
return entities.firstOrNull()?.toDomain()
|
return entities.firstOrNull()?.toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun findByJellyfinUserId(jellyfinUserId: String): User? {
|
||||||
|
val entities =
|
||||||
|
jdbc.query(
|
||||||
|
"""
|
||||||
|
SELECT id, name, email, provider, provider_id, jellyfin_user_id, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE jellyfin_user_id = ?
|
||||||
|
""".trimIndent(),
|
||||||
|
userEntityRowMapper,
|
||||||
|
jellyfinUserId,
|
||||||
|
)
|
||||||
|
return entities.firstOrNull()?.toDomain()
|
||||||
|
}
|
||||||
|
|
||||||
override fun findAll(): List<User> =
|
override fun findAll(): List<User> =
|
||||||
jdbc
|
jdbc
|
||||||
.query(
|
.query(
|
||||||
@@ -122,4 +203,21 @@ class UserRepository(
|
|||||||
)
|
)
|
||||||
return entities.firstOrNull()?.toDomain()
|
return entities.firstOrNull()?.toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findProviderById(id: UUID): AuthProvider? =
|
||||||
|
jdbc
|
||||||
|
.query(
|
||||||
|
"SELECT provider FROM users WHERE id = ?",
|
||||||
|
{ rs: ResultSet, _: Int -> rs.getString("provider") },
|
||||||
|
id,
|
||||||
|
).firstOrNull()
|
||||||
|
?.let { AuthProvider.valueOf(it) }
|
||||||
|
|
||||||
|
private fun findProviderIdById(id: UUID): String? =
|
||||||
|
jdbc
|
||||||
|
.query(
|
||||||
|
"SELECT provider_id FROM users WHERE id = ?",
|
||||||
|
{ rs: ResultSet, _: Int -> rs.getString("provider_id") },
|
||||||
|
id,
|
||||||
|
).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-15
@@ -1,7 +1,5 @@
|
|||||||
package com.project.movienight.adapters.security
|
package com.project.movienight.adapters.security
|
||||||
|
|
||||||
import com.project.movienight.adapters.persistence.entity.toDomain
|
|
||||||
import com.project.movienight.adapters.persistence.entity.toEntity
|
|
||||||
import com.project.movienight.application.ports.input.security.OAuth2UserInfo
|
import com.project.movienight.application.ports.input.security.OAuth2UserInfo
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
@@ -59,12 +57,11 @@ class CustomOAuth2UserService(
|
|||||||
|
|
||||||
if (userByEmail != null) {
|
if (userByEmail != null) {
|
||||||
log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail())
|
log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail())
|
||||||
val entity =
|
userRepository.linkOAuthAccount(
|
||||||
userByEmail.toEntity(
|
userId = userByEmail.id,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
providerId = userInfo.getProviderId(),
|
providerId = userInfo.getProviderId(),
|
||||||
)
|
)
|
||||||
userRepository.save(entity.toDomain())
|
|
||||||
} else {
|
} else {
|
||||||
log.debug("Creating new user for provider: {}", userInfo.getProvider())
|
log.debug("Creating new user for provider: {}", userInfo.getProvider())
|
||||||
val newUser =
|
val newUser =
|
||||||
@@ -72,14 +69,12 @@ class CustomOAuth2UserService(
|
|||||||
id = idGenerator.generateId(),
|
id = idGenerator.generateId(),
|
||||||
name = userInfo.getName(),
|
name = userInfo.getName(),
|
||||||
email = userInfo.getEmail(),
|
email = userInfo.getEmail(),
|
||||||
library = null,
|
|
||||||
)
|
)
|
||||||
val entity =
|
userRepository.createOAuthUser(
|
||||||
newUser.toEntity(
|
user = newUser,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
providerId = userInfo.getProviderId(),
|
providerId = userInfo.getProviderId(),
|
||||||
)
|
)
|
||||||
userRepository.save(entity.toDomain())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ class SecurityConfiguration(
|
|||||||
auth
|
auth
|
||||||
.requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health")
|
.requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
.requestMatchers(
|
||||||
|
"/api/integrations/jellyfin/events",
|
||||||
|
"/api/integrations/jellyfin/sync",
|
||||||
|
"/api/integrations/jellyfin/sync-state",
|
||||||
|
).permitAll()
|
||||||
.requestMatchers("/api/users/me")
|
.requestMatchers("/api/users/me")
|
||||||
.authenticated()
|
.authenticated()
|
||||||
.requestMatchers("/api/**")
|
.requestMatchers("/api/**")
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import com.project.movienight.domain.exception.EntityNotFoundException
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.slf4j.MDC
|
import org.slf4j.MDC
|
||||||
import org.springframework.http.HttpStatus
|
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.ExceptionHandler
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
class ApiExceptionHandler {
|
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)
|
@ExceptionHandler(Exception::class)
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
fun handleUnexpectedException(exception: Exception): ErrorResponse {
|
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.request.EditFilmRequest
|
||||||
import com.project.movienight.adapters.web.dto.response.FilmResponse
|
import com.project.movienight.adapters.web.dto.response.FilmResponse
|
||||||
import com.project.movienight.application.ports.input.CreateFilmCommand
|
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.EditFilmCommand
|
||||||
import com.project.movienight.application.ports.input.EditFilmUseCase
|
import com.project.movienight.application.ports.input.FilmUseCase
|
||||||
import com.project.movienight.application.ports.input.GetAllFilmsUseCase
|
import jakarta.validation.Valid
|
||||||
import com.project.movienight.application.ports.input.GetFilmByIdUseCase
|
|
||||||
import com.project.movienight.application.ports.input.SearchFilmByTitleUseCase
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping
|
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 org.springframework.web.bind.annotation.RestController
|
||||||
import java.util.UUID
|
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
|
@RestController
|
||||||
@RequestMapping("/api/films")
|
@RequestMapping("/api/films")
|
||||||
class FilmController(
|
class FilmController(
|
||||||
private val createFilmUseCase: CreateFilmUseCase,
|
private val filmUseCase: FilmUseCase,
|
||||||
private val editFilmUseCase: EditFilmUseCase,
|
|
||||||
private val deleteFilmUseCase: DeleteFilmUseCase,
|
|
||||||
private val getFilmByIdUseCase: GetFilmByIdUseCase,
|
|
||||||
private val getAllFilmsUseCase: GetAllFilmsUseCase,
|
|
||||||
private val searchFilmByTitleUseCase: SearchFilmByTitleUseCase,
|
|
||||||
) {
|
) {
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
fun create(
|
fun create(
|
||||||
@RequestBody request: CreateFilmRequest,
|
@Valid @RequestBody request: CreateFilmRequest,
|
||||||
): FilmResponse =
|
): FilmResponse =
|
||||||
FilmResponse.fromDomain(
|
FilmResponse.fromDomain(
|
||||||
createFilmUseCase.create(
|
filmUseCase.create(
|
||||||
CreateFilmCommand(
|
CreateFilmCommand(
|
||||||
title = request.title,
|
title = request.title,
|
||||||
description = request.description,
|
description = request.description,
|
||||||
contentType = request.contentType.toContentTypeOrFilm(),
|
contentType = parseContentType(request.contentType),
|
||||||
releaseYear = request.releaseYear,
|
releaseYear = request.releaseYear,
|
||||||
genres = request.genres,
|
genres = request.genres,
|
||||||
cast = request.cast,
|
cast = request.cast,
|
||||||
@@ -68,16 +53,16 @@ class FilmController(
|
|||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
fun edit(
|
fun edit(
|
||||||
@PathVariable id: UUID,
|
@PathVariable id: UUID,
|
||||||
@RequestBody request: EditFilmRequest,
|
@Valid @RequestBody request: EditFilmRequest,
|
||||||
): FilmResponse =
|
): FilmResponse =
|
||||||
FilmResponse.fromDomain(
|
FilmResponse.fromDomain(
|
||||||
editFilmUseCase.edit(
|
filmUseCase.edit(
|
||||||
id = id,
|
id = id,
|
||||||
command =
|
command =
|
||||||
EditFilmCommand(
|
EditFilmCommand(
|
||||||
title = request.title,
|
title = request.title,
|
||||||
description = request.description,
|
description = request.description,
|
||||||
contentType = request.contentType.toContentTypeOrFilm(),
|
contentType = parseContentType(request.contentType),
|
||||||
releaseYear = request.releaseYear,
|
releaseYear = request.releaseYear,
|
||||||
genres = request.genres,
|
genres = request.genres,
|
||||||
cast = request.cast,
|
cast = request.cast,
|
||||||
@@ -95,21 +80,21 @@ class FilmController(
|
|||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
fun delete(
|
fun delete(
|
||||||
@PathVariable id: UUID,
|
@PathVariable id: UUID,
|
||||||
) = deleteFilmUseCase.delete(id)
|
) = filmUseCase.delete(id)
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
fun getById(
|
fun getById(
|
||||||
@PathVariable id: UUID,
|
@PathVariable id: UUID,
|
||||||
): FilmResponse = FilmResponse.fromDomain(getFilmByIdUseCase.getById(id))
|
): FilmResponse = FilmResponse.fromDomain(filmUseCase.getById(id))
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
fun getAll(): List<FilmResponse> = getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) }
|
fun getAll(): List<FilmResponse> = filmUseCase.getAll().map { FilmResponse.fromDomain(it) }
|
||||||
|
|
||||||
@GetMapping("/search")
|
@GetMapping("/search")
|
||||||
fun searchByTitle(
|
fun searchByTitle(
|
||||||
@RequestParam title: String,
|
@RequestParam title: String,
|
||||||
): ResponseEntity<FilmResponse> {
|
): ResponseEntity<FilmResponse> {
|
||||||
val film = searchFilmByTitleUseCase.searchByTitle(title)
|
val film = filmUseCase.searchByTitle(title)
|
||||||
return if (film != null) {
|
return if (film != null) {
|
||||||
ResponseEntity.ok(FilmResponse.fromDomain(film))
|
ResponseEntity.ok(FilmResponse.fromDomain(film))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
package com.project.movienight.adapters.web
|
package com.project.movienight.adapters.web
|
||||||
|
|
||||||
import com.project.movienight.adapters.web.dto.request.CreateFilmLibraryRequest
|
import com.project.movienight.adapters.web.dto.response.FilmLibraryEntryResponse
|
||||||
import com.project.movienight.adapters.web.dto.response.FilmLibraryResponse
|
|
||||||
import com.project.movienight.adapters.web.dto.response.FilmResponse
|
import com.project.movienight.adapters.web.dto.response.FilmResponse
|
||||||
import com.project.movienight.application.ports.input.AddFilmToLibraryCommand
|
import com.project.movienight.application.ports.input.AddFilmToLibraryCommand
|
||||||
import com.project.movienight.application.ports.input.AddFilmToLibraryUseCase
|
import com.project.movienight.application.ports.input.FilmLibraryUseCase
|
||||||
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.MarkFilmViewedCommand
|
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.RemoveFilmFromLibraryCommand
|
||||||
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryUseCase
|
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
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.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
@@ -31,52 +19,29 @@ import java.util.UUID
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users/{userId}/library")
|
@RequestMapping("/api/users/{userId}/library")
|
||||||
class FilmLibraryController(
|
class FilmLibraryController(
|
||||||
private val createFilmLibraryUseCase: CreateFilmLibraryUseCase,
|
private val filmLibraryUseCase: FilmLibraryUseCase,
|
||||||
private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase,
|
|
||||||
private val markFilmViewedUseCase: MarkFilmViewedUseCase,
|
|
||||||
private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase,
|
|
||||||
private val getFilmLibraryUseCase: GetFilmLibraryUseCase,
|
|
||||||
private val getAllFilmsUseCase: GetAllFilmsUseCase,
|
|
||||||
private val listFilmLibraryEntriesUseCase: ListFilmLibraryEntriesUseCase,
|
|
||||||
) {
|
) {
|
||||||
@PostMapping
|
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
|
||||||
fun create(
|
|
||||||
@PathVariable userId: UUID,
|
|
||||||
@RequestBody request: CreateFilmLibraryRequest,
|
|
||||||
): FilmLibraryResponse =
|
|
||||||
FilmLibraryResponse.fromDomain(
|
|
||||||
createFilmLibraryUseCase.create(
|
|
||||||
CreateFilmLibraryCommand(
|
|
||||||
userId = userId,
|
|
||||||
name = request.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
fun get(
|
|
||||||
@PathVariable userId: UUID,
|
|
||||||
): FilmLibraryResponse =
|
|
||||||
FilmLibraryResponse.fromDomain(
|
|
||||||
getFilmLibraryUseCase.getLibrary(
|
|
||||||
GetFilmLibraryQuery(userId = userId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@GetMapping("/entries")
|
|
||||||
fun list(
|
fun list(
|
||||||
@PathVariable userId: UUID,
|
@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}")
|
@PostMapping("/films/{filmId}")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
fun addFilm(
|
fun addFilm(
|
||||||
@PathVariable userId: UUID,
|
@PathVariable userId: UUID,
|
||||||
@PathVariable filmId: UUID,
|
@PathVariable filmId: UUID,
|
||||||
): FilmLibraryResponse =
|
): FilmLibraryEntryResponse =
|
||||||
FilmLibraryResponse.fromDomain(
|
FilmLibraryEntryResponse.fromDomain(
|
||||||
addFilmToLibraryUseCase.addFilm(
|
filmLibraryUseCase.addFilm(
|
||||||
AddFilmToLibraryCommand(
|
AddFilmToLibraryCommand(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
@@ -88,9 +53,9 @@ class FilmLibraryController(
|
|||||||
fun markViewed(
|
fun markViewed(
|
||||||
@PathVariable userId: UUID,
|
@PathVariable userId: UUID,
|
||||||
@PathVariable filmId: UUID,
|
@PathVariable filmId: UUID,
|
||||||
): FilmLibraryResponse =
|
): FilmLibraryEntryResponse =
|
||||||
FilmLibraryResponse.fromDomain(
|
FilmLibraryEntryResponse.fromDomain(
|
||||||
markFilmViewedUseCase.markViewed(
|
filmLibraryUseCase.markViewed(
|
||||||
MarkFilmViewedCommand(
|
MarkFilmViewedCommand(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
@@ -104,7 +69,7 @@ class FilmLibraryController(
|
|||||||
@PathVariable userId: UUID,
|
@PathVariable userId: UUID,
|
||||||
@PathVariable filmId: UUID,
|
@PathVariable filmId: UUID,
|
||||||
) {
|
) {
|
||||||
removeFilmFromLibraryUseCase.removeFilm(
|
filmLibraryUseCase.removeFilm(
|
||||||
RemoveFilmFromLibraryCommand(
|
RemoveFilmFromLibraryCommand(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
@@ -115,27 +80,5 @@ class FilmLibraryController(
|
|||||||
@GetMapping("/available-films")
|
@GetMapping("/available-films")
|
||||||
fun getAvailableFilms(
|
fun getAvailableFilms(
|
||||||
@PathVariable userId: UUID,
|
@PathVariable userId: UUID,
|
||||||
): List<FilmResponse> {
|
): List<FilmResponse> = filmLibraryUseCase.listAvailableFilms(userId).map { FilmResponse.fromDomain(it) }
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.request.RateFilmRequest
|
||||||
import com.project.movienight.adapters.web.dto.response.FilmRatingResponse
|
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.RateFilmCommand
|
||||||
import com.project.movienight.application.ports.input.RateFilmUseCase
|
import jakarta.validation.Valid
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
@@ -18,18 +18,17 @@ import java.util.UUID
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users/{userId}/ratings")
|
@RequestMapping("/api/users/{userId}/ratings")
|
||||||
class FilmRatingController(
|
class FilmRatingController(
|
||||||
private val rateFilmUseCase: RateFilmUseCase,
|
private val filmRatingUseCase: FilmRatingUseCase,
|
||||||
private val getFilmRatingsUseCase: GetFilmRatingsUseCase,
|
|
||||||
) {
|
) {
|
||||||
@PostMapping("/films/{filmId}")
|
@PostMapping("/films/{filmId}")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
fun rate(
|
fun rate(
|
||||||
@PathVariable userId: UUID,
|
@PathVariable userId: UUID,
|
||||||
@PathVariable filmId: UUID,
|
@PathVariable filmId: UUID,
|
||||||
@RequestBody request: RateFilmRequest,
|
@Valid @RequestBody request: RateFilmRequest,
|
||||||
): FilmRatingResponse =
|
): FilmRatingResponse =
|
||||||
FilmRatingResponse.fromDomain(
|
FilmRatingResponse.fromDomain(
|
||||||
rateFilmUseCase.rate(
|
filmRatingUseCase.rate(
|
||||||
RateFilmCommand(
|
RateFilmCommand(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
@@ -42,5 +41,5 @@ class FilmRatingController(
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
fun list(
|
fun list(
|
||||||
@PathVariable userId: UUID,
|
@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
|
package com.project.movienight.adapters.web
|
||||||
|
|
||||||
import com.project.movienight.adapters.web.dto.request.JellyfinEventRequest
|
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 com.project.movienight.config.JellyfinIntegrationProperties
|
||||||
|
import jakarta.validation.Valid
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
@@ -16,7 +18,7 @@ import org.springframework.web.server.ResponseStatusException
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/integrations/jellyfin")
|
@RequestMapping("/api/integrations/jellyfin")
|
||||||
class JellyfinEventsController(
|
class JellyfinEventsController(
|
||||||
private val jellyfinEventService: JellyfinEventService,
|
private val jellyfinEventUseCase: JellyfinEventUseCase,
|
||||||
private val properties: JellyfinIntegrationProperties,
|
private val properties: JellyfinIntegrationProperties,
|
||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(JellyfinEventsController::class.java)
|
private val log = LoggerFactory.getLogger(JellyfinEventsController::class.java)
|
||||||
@@ -25,7 +27,7 @@ class JellyfinEventsController(
|
|||||||
@ResponseStatus(HttpStatus.OK)
|
@ResponseStatus(HttpStatus.OK)
|
||||||
fun receiveEvent(
|
fun receiveEvent(
|
||||||
@RequestHeader(value = "X-MovieNight-Plugin-Token", required = false) token: String?,
|
@RequestHeader(value = "X-MovieNight-Plugin-Token", required = false) token: String?,
|
||||||
@RequestBody request: JellyfinEventRequest,
|
@Valid @RequestBody request: JellyfinEventRequest,
|
||||||
) {
|
) {
|
||||||
if (!properties.enabled) {
|
if (!properties.enabled) {
|
||||||
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Jellyfin integration is disabled")
|
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Jellyfin integration is disabled")
|
||||||
@@ -43,14 +45,16 @@ class JellyfinEventsController(
|
|||||||
request.jellyfinUserId,
|
request.jellyfinUserId,
|
||||||
request.itemId,
|
request.itemId,
|
||||||
)
|
)
|
||||||
jellyfinEventService.handleEvent(
|
jellyfinEventUseCase.handle(
|
||||||
eventId = request.eventId,
|
HandleJellyfinEventCommand(
|
||||||
serverId = null,
|
eventId = request.eventId,
|
||||||
eventType = request.eventType,
|
serverId = null,
|
||||||
occurredAt = request.occurredAt,
|
eventType = request.eventType,
|
||||||
jellyfinUserId = request.jellyfinUserId,
|
occurredAt = request.occurredAt,
|
||||||
itemId = request.itemId,
|
jellyfinUserId = request.jellyfinUserId,
|
||||||
payload = request.payload,
|
itemId = request.itemId,
|
||||||
|
payload = request.payload,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.project.movienight.adapters.web
|
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.JellyfinSyncState
|
||||||
import com.project.movienight.domain.model.JellyfinSyncSummary
|
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
@@ -11,11 +11,11 @@ import org.springframework.web.bind.annotation.RestController
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/integrations/jellyfin")
|
@RequestMapping("/api/integrations/jellyfin")
|
||||||
class JellyfinSyncController(
|
class JellyfinSyncController(
|
||||||
private val jellyfinSyncService: JellyfinSyncService,
|
private val jellyfinSyncUseCase: JellyfinSyncUseCase,
|
||||||
) {
|
) {
|
||||||
@PostMapping("/sync")
|
@PostMapping("/sync")
|
||||||
fun syncNow(): JellyfinSyncSummary = jellyfinSyncService.syncNow()
|
fun syncNow(): JellyfinSyncSummary = jellyfinSyncUseCase.syncNow()
|
||||||
|
|
||||||
@GetMapping("/sync-state")
|
@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.RejectRecommendationCommand
|
||||||
import com.project.movienight.application.ports.input.RejectRecommendationUseCase
|
import com.project.movienight.application.ports.input.RejectRecommendationUseCase
|
||||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
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.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
@@ -40,7 +39,7 @@ class RecommendationController(
|
|||||||
.recommend(
|
.recommend(
|
||||||
RecommendationQuery(
|
RecommendationQuery(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
contentType = contentType?.let { runCatching { ContentType.valueOf(it.uppercase()) }.getOrNull() },
|
contentType = parseOptionalContentType(contentType),
|
||||||
mood = mood,
|
mood = mood,
|
||||||
libraryOnly = libraryOnly,
|
libraryOnly = libraryOnly,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package com.project.movienight.adapters.web
|
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.CreateUserRequest
|
||||||
import com.project.movienight.adapters.web.dto.request.EditUserRequest
|
import com.project.movienight.adapters.web.dto.request.EditUserRequest
|
||||||
import com.project.movienight.adapters.web.dto.response.UserResponse
|
import com.project.movienight.adapters.web.dto.response.UserResponse
|
||||||
import com.project.movienight.application.ports.input.CreateUserCommand
|
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.EditUserCommand
|
||||||
import com.project.movienight.application.ports.input.EditUserUseCase
|
import com.project.movienight.application.ports.input.UserUseCase
|
||||||
import com.project.movienight.application.ports.input.GetAllUsersUseCase
|
import jakarta.validation.Valid
|
||||||
import com.project.movienight.application.ports.input.GetUserByIdUseCase
|
|
||||||
import org.springframework.http.HttpStatus
|
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.DeleteMapping
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PatchMapping
|
import org.springframework.web.bind.annotation.PatchMapping
|
||||||
@@ -25,19 +24,15 @@ import java.util.UUID
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users")
|
@RequestMapping("/api/users")
|
||||||
class UserController(
|
class UserController(
|
||||||
private val createUserUseCase: CreateUserUseCase,
|
private val userUseCase: UserUseCase,
|
||||||
private val editUserUseCase: EditUserUseCase,
|
|
||||||
private val deleteUserUseCase: DeleteUserUseCase,
|
|
||||||
private val getUserByIdUseCase: GetUserByIdUseCase,
|
|
||||||
private val getAllUsersUseCase: GetAllUsersUseCase,
|
|
||||||
) {
|
) {
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
fun create(
|
fun create(
|
||||||
@RequestBody request: CreateUserRequest,
|
@Valid @RequestBody request: CreateUserRequest,
|
||||||
): UserResponse =
|
): UserResponse =
|
||||||
UserResponse.fromDomain(
|
UserResponse.fromDomain(
|
||||||
createUserUseCase.create(
|
userUseCase.create(
|
||||||
CreateUserCommand(
|
CreateUserCommand(
|
||||||
name = request.name,
|
name = request.name,
|
||||||
email = request.email,
|
email = request.email,
|
||||||
@@ -46,20 +41,25 @@ class UserController(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@GetMapping
|
@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}")
|
@GetMapping("/{id}")
|
||||||
fun getById(
|
fun getById(
|
||||||
@PathVariable id: UUID,
|
@PathVariable id: UUID,
|
||||||
): UserResponse = UserResponse.fromDomain(getUserByIdUseCase.getById(id))
|
): UserResponse = UserResponse.fromDomain(userUseCase.getById(id))
|
||||||
|
|
||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
fun edit(
|
fun edit(
|
||||||
@PathVariable id: UUID,
|
@PathVariable id: UUID,
|
||||||
@RequestBody request: EditUserRequest,
|
@Valid @RequestBody request: EditUserRequest,
|
||||||
): UserResponse =
|
): UserResponse =
|
||||||
UserResponse.fromDomain(
|
UserResponse.fromDomain(
|
||||||
editUserUseCase.edit(
|
userUseCase.edit(
|
||||||
id = id,
|
id = id,
|
||||||
command =
|
command =
|
||||||
EditUserCommand(
|
EditUserCommand(
|
||||||
@@ -73,5 +73,5 @@ class UserController(
|
|||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
fun delete(
|
fun delete(
|
||||||
@PathVariable id: UUID,
|
@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.request.UpsertUserPreferencesRequest
|
||||||
import com.project.movienight.adapters.web.dto.response.UserPreferencesResponse
|
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.UpsertUserPreferencesCommand
|
||||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesUseCase
|
import com.project.movienight.application.ports.input.UserPreferencesUseCase
|
||||||
import com.project.movienight.domain.model.ContentType
|
import jakarta.validation.Valid
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PutMapping
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
@@ -17,16 +16,15 @@ import java.util.UUID
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users/{userId}/preferences")
|
@RequestMapping("/api/users/{userId}/preferences")
|
||||||
class UserPreferencesController(
|
class UserPreferencesController(
|
||||||
private val upsertUserPreferencesUseCase: UpsertUserPreferencesUseCase,
|
private val userPreferencesUseCase: UserPreferencesUseCase,
|
||||||
private val getUserPreferencesUseCase: GetUserPreferencesUseCase,
|
|
||||||
) {
|
) {
|
||||||
@PutMapping
|
@PutMapping
|
||||||
fun upsert(
|
fun upsert(
|
||||||
@PathVariable userId: UUID,
|
@PathVariable userId: UUID,
|
||||||
@RequestBody request: UpsertUserPreferencesRequest,
|
@Valid @RequestBody request: UpsertUserPreferencesRequest,
|
||||||
): UserPreferencesResponse =
|
): UserPreferencesResponse =
|
||||||
UserPreferencesResponse.fromDomain(
|
UserPreferencesResponse.fromDomain(
|
||||||
upsertUserPreferencesUseCase.upsert(
|
userPreferencesUseCase.upsert(
|
||||||
UpsertUserPreferencesCommand(
|
UpsertUserPreferencesCommand(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
weightedGenres = request.weightedGenres,
|
weightedGenres = request.weightedGenres,
|
||||||
@@ -35,13 +33,7 @@ class UserPreferencesController(
|
|||||||
castAndDirectors = request.castAndDirectors,
|
castAndDirectors = request.castAndDirectors,
|
||||||
moods = request.moods,
|
moods = request.moods,
|
||||||
contentTypes =
|
contentTypes =
|
||||||
request.contentTypes.mapNotNull {
|
request.contentTypes.map { parseContentType(it) },
|
||||||
runCatching {
|
|
||||||
ContentType.valueOf(
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
}.getOrNull()
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -49,5 +41,5 @@ class UserPreferencesController(
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
fun get(
|
fun get(
|
||||||
@PathVariable userId: UUID,
|
@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
|
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(
|
data class CreateFilmRequest(
|
||||||
|
@field:NotBlank
|
||||||
|
@field:Size(max = 255)
|
||||||
val title: String,
|
val title: String,
|
||||||
|
@field:NotBlank
|
||||||
val description: String,
|
val description: String,
|
||||||
|
@field:NotBlank
|
||||||
val contentType: String = "FILM",
|
val contentType: String = "FILM",
|
||||||
|
@field:Min(1888)
|
||||||
|
@field:Max(3000)
|
||||||
val releaseYear: Int? = null,
|
val releaseYear: Int? = null,
|
||||||
val genres: List<String> = emptyList(),
|
val genres: List<String> = emptyList(),
|
||||||
val cast: List<String> = emptyList(),
|
val cast: List<String> = emptyList(),
|
||||||
val directors: List<String> = emptyList(),
|
val directors: List<String> = emptyList(),
|
||||||
|
@field:Min(0)
|
||||||
|
@field:Max(10)
|
||||||
val imdbRating: Double? = null,
|
val imdbRating: Double? = null,
|
||||||
|
@field:Min(0)
|
||||||
|
@field:Max(10)
|
||||||
val platformRating: Double? = null,
|
val platformRating: Double? = null,
|
||||||
val externalUrl: String? = null,
|
val externalUrl: String? = null,
|
||||||
val jellyfinItemId: String? = null,
|
val jellyfinItemId: String? = null,
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
package com.project.movienight.adapters.web.dto.request
|
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(
|
data class CreateUserRequest(
|
||||||
|
@field:NotBlank
|
||||||
|
@field:Size(max = 255)
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@field:Email
|
||||||
|
@field:NotBlank
|
||||||
|
@field:Size(max = 320)
|
||||||
val email: String,
|
val email: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
package com.project.movienight.adapters.web.dto.request
|
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(
|
data class EditFilmRequest(
|
||||||
|
@field:NotBlank
|
||||||
|
@field:Size(max = 255)
|
||||||
val title: String,
|
val title: String,
|
||||||
|
@field:NotBlank
|
||||||
val description: String,
|
val description: String,
|
||||||
|
@field:NotBlank
|
||||||
val contentType: String = "FILM",
|
val contentType: String = "FILM",
|
||||||
|
@field:Min(1888)
|
||||||
|
@field:Max(3000)
|
||||||
val releaseYear: Int? = null,
|
val releaseYear: Int? = null,
|
||||||
val genres: List<String> = emptyList(),
|
val genres: List<String> = emptyList(),
|
||||||
val cast: List<String> = emptyList(),
|
val cast: List<String> = emptyList(),
|
||||||
val directors: List<String> = emptyList(),
|
val directors: List<String> = emptyList(),
|
||||||
|
@field:Min(0)
|
||||||
|
@field:Max(10)
|
||||||
val imdbRating: Double? = null,
|
val imdbRating: Double? = null,
|
||||||
|
@field:Min(0)
|
||||||
|
@field:Max(10)
|
||||||
val platformRating: Double? = null,
|
val platformRating: Double? = null,
|
||||||
val externalUrl: String? = null,
|
val externalUrl: String? = null,
|
||||||
val jellyfinItemId: String? = null,
|
val jellyfinItemId: String? = null,
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
package com.project.movienight.adapters.web.dto.request
|
package com.project.movienight.adapters.web.dto.request
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
|
import jakarta.validation.constraints.Size
|
||||||
|
|
||||||
data class EditUserRequest(
|
data class EditUserRequest(
|
||||||
|
@field:NotBlank
|
||||||
|
@field:Size(max = 255)
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@field:Size(max = 255)
|
||||||
val jellyfinUserId: String? = null,
|
val jellyfinUserId: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+5
@@ -1,18 +1,23 @@
|
|||||||
package com.project.movienight.adapters.web.dto.request
|
package com.project.movienight.adapters.web.dto.request
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
data class JellyfinEventRequest(
|
data class JellyfinEventRequest(
|
||||||
@JsonProperty("event_id")
|
@JsonProperty("event_id")
|
||||||
|
@field:NotBlank
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
@JsonProperty("event_type")
|
@JsonProperty("event_type")
|
||||||
|
@field:NotBlank
|
||||||
val eventType: String,
|
val eventType: String,
|
||||||
@JsonProperty("occurred_at")
|
@JsonProperty("occurred_at")
|
||||||
val occurredAt: OffsetDateTime,
|
val occurredAt: OffsetDateTime,
|
||||||
@JsonProperty("jellyfin_user_id")
|
@JsonProperty("jellyfin_user_id")
|
||||||
|
@field:NotBlank
|
||||||
val jellyfinUserId: String,
|
val jellyfinUserId: String,
|
||||||
@JsonProperty("item_id")
|
@JsonProperty("item_id")
|
||||||
|
@field:NotBlank
|
||||||
val itemId: String,
|
val itemId: String,
|
||||||
@JsonProperty("payload_version")
|
@JsonProperty("payload_version")
|
||||||
val payloadVersion: Int = 1,
|
val payloadVersion: Int = 1,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.project.movienight.adapters.web.dto.request
|
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(
|
data class RateFilmRequest(
|
||||||
|
@field:Min(1)
|
||||||
|
@field:Max(10)
|
||||||
val score: Int,
|
val score: Int,
|
||||||
|
@field:Size(max = 2048)
|
||||||
val note: String? = null,
|
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-31
@@ -1,20 +1,20 @@
|
|||||||
package com.project.movienight.application.ports.input
|
package com.project.movienight.application.ports.input
|
||||||
|
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.Film
|
||||||
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface CreateFilmLibraryUseCase {
|
interface FilmLibraryUseCase {
|
||||||
fun create(command: CreateFilmLibraryCommand): FilmLibrary
|
fun addFilm(command: AddFilmToLibraryCommand): FilmLibraryEntry
|
||||||
}
|
|
||||||
|
|
||||||
data class CreateFilmLibraryCommand(
|
fun markViewed(command: MarkFilmViewedCommand): FilmLibraryEntry
|
||||||
val userId: UUID,
|
|
||||||
val name: String = "Мои фильмы",
|
|
||||||
)
|
|
||||||
|
|
||||||
interface AddFilmToLibraryUseCase {
|
fun removeFilm(command: RemoveFilmFromLibraryCommand): FilmLibraryEntry
|
||||||
fun addFilm(command: AddFilmToLibraryCommand): FilmLibrary
|
|
||||||
|
fun list(userId: UUID): List<FilmLibraryEntry>
|
||||||
|
|
||||||
|
fun listAvailableFilms(userId: UUID): List<Film>
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AddFilmToLibraryCommand(
|
data class AddFilmToLibraryCommand(
|
||||||
@@ -22,34 +22,14 @@ data class AddFilmToLibraryCommand(
|
|||||||
val filmId: UUID,
|
val filmId: UUID,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface MarkFilmViewedUseCase {
|
|
||||||
fun markViewed(command: MarkFilmViewedCommand): FilmLibrary
|
|
||||||
}
|
|
||||||
|
|
||||||
data class MarkFilmViewedCommand(
|
data class MarkFilmViewedCommand(
|
||||||
val userId: UUID,
|
val userId: UUID,
|
||||||
val filmId: UUID,
|
val filmId: UUID,
|
||||||
val watchedAt: LocalDateTime? = null,
|
val watchedAt: LocalDateTime? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface RemoveFilmFromLibraryUseCase {
|
|
||||||
fun removeFilm(command: RemoveFilmFromLibraryCommand): FilmLibrary
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RemoveFilmFromLibraryCommand(
|
data class RemoveFilmFromLibraryCommand(
|
||||||
val userId: UUID,
|
val userId: UUID,
|
||||||
val filmId: UUID,
|
val filmId: UUID,
|
||||||
val libraryId: UUID? = null,
|
val entryId: UUID? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface GetFilmLibraryUseCase {
|
|
||||||
fun getLibrary(query: GetFilmLibraryQuery): FilmLibrary
|
|
||||||
}
|
|
||||||
|
|
||||||
data class GetFilmLibraryQuery(
|
|
||||||
val userId: UUID,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface ListFilmLibraryEntriesUseCase {
|
|
||||||
fun list(userId: UUID): List<FilmLibrary>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.project.movienight.application.ports.input
|
|||||||
import com.project.movienight.domain.model.FilmRating
|
import com.project.movienight.domain.model.FilmRating
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface RateFilmUseCase {
|
interface FilmRatingUseCase {
|
||||||
fun rate(command: RateFilmCommand): FilmRating
|
fun rate(command: RateFilmCommand): FilmRating
|
||||||
|
|
||||||
|
fun getRatings(userId: UUID): List<FilmRating>
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RateFilmCommand(
|
data class RateFilmCommand(
|
||||||
@@ -13,7 +15,3 @@ data class RateFilmCommand(
|
|||||||
val score: Int,
|
val score: Int,
|
||||||
val note: String? = null,
|
val note: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface GetFilmRatingsUseCase {
|
|
||||||
fun getRatings(userId: UUID): List<FilmRating>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,21 @@ import com.project.movienight.domain.model.ContentType
|
|||||||
import com.project.movienight.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface CreateFilmUseCase {
|
interface FilmUseCase {
|
||||||
fun create(command: CreateFilmCommand): Film
|
fun create(command: CreateFilmCommand): Film
|
||||||
|
|
||||||
|
fun edit(
|
||||||
|
id: UUID,
|
||||||
|
command: EditFilmCommand,
|
||||||
|
): Film
|
||||||
|
|
||||||
|
fun delete(id: UUID)
|
||||||
|
|
||||||
|
fun getById(id: UUID): Film
|
||||||
|
|
||||||
|
fun getAll(): List<Film>
|
||||||
|
|
||||||
|
fun searchByTitle(title: String): Film?
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CreateFilmCommand(
|
data class CreateFilmCommand(
|
||||||
@@ -23,13 +36,6 @@ data class CreateFilmCommand(
|
|||||||
val jellyfinLibraryId: String? = null,
|
val jellyfinLibraryId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface EditFilmUseCase {
|
|
||||||
fun edit(
|
|
||||||
id: UUID,
|
|
||||||
command: EditFilmCommand,
|
|
||||||
): Film
|
|
||||||
}
|
|
||||||
|
|
||||||
data class EditFilmCommand(
|
data class EditFilmCommand(
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
@@ -44,19 +50,3 @@ data class EditFilmCommand(
|
|||||||
val jellyfinItemId: String? = null,
|
val jellyfinItemId: String? = null,
|
||||||
val jellyfinLibraryId: String? = null,
|
val jellyfinLibraryId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface DeleteFilmUseCase {
|
|
||||||
fun delete(id: UUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetFilmByIdUseCase {
|
|
||||||
fun getById(id: UUID): Film
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetAllFilmsUseCase {
|
|
||||||
fun getAll(): List<Film>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchFilmByTitleUseCase {
|
|
||||||
fun searchByTitle(title: String): Film?
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.project.movienight.application.ports.input
|
||||||
|
|
||||||
|
import com.project.movienight.domain.model.JellyfinSyncState
|
||||||
|
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
interface JellyfinEventUseCase {
|
||||||
|
fun handle(command: HandleJellyfinEventCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HandleJellyfinEventCommand(
|
||||||
|
val eventId: String,
|
||||||
|
val serverId: String?,
|
||||||
|
val eventType: String,
|
||||||
|
val occurredAt: OffsetDateTime,
|
||||||
|
val jellyfinUserId: String,
|
||||||
|
val itemId: String,
|
||||||
|
val payload: Map<String, Any>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface JellyfinSyncUseCase {
|
||||||
|
fun syncNow(): JellyfinSyncSummary
|
||||||
|
|
||||||
|
fun getSyncStates(): List<JellyfinSyncState>
|
||||||
|
}
|
||||||
+3
-5
@@ -4,8 +4,10 @@ import com.project.movienight.domain.model.ContentType
|
|||||||
import com.project.movienight.domain.model.UserPreferences
|
import com.project.movienight.domain.model.UserPreferences
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface UpsertUserPreferencesUseCase {
|
interface UserPreferencesUseCase {
|
||||||
fun upsert(command: UpsertUserPreferencesCommand): UserPreferences
|
fun upsert(command: UpsertUserPreferencesCommand): UserPreferences
|
||||||
|
|
||||||
|
fun get(userId: UUID): UserPreferences?
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UpsertUserPreferencesCommand(
|
data class UpsertUserPreferencesCommand(
|
||||||
@@ -17,7 +19,3 @@ data class UpsertUserPreferencesCommand(
|
|||||||
val moods: List<String> = emptyList(),
|
val moods: List<String> = emptyList(),
|
||||||
val contentTypes: List<ContentType> = emptyList(),
|
val contentTypes: List<ContentType> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
interface GetUserPreferencesUseCase {
|
|
||||||
fun get(userId: UUID): UserPreferences?
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,8 +3,19 @@ package com.project.movienight.application.ports.input
|
|||||||
import com.project.movienight.domain.model.User
|
import com.project.movienight.domain.model.User
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface CreateUserUseCase {
|
interface UserUseCase {
|
||||||
fun create(command: CreateUserCommand): User
|
fun create(command: CreateUserCommand): User
|
||||||
|
|
||||||
|
fun edit(
|
||||||
|
id: UUID,
|
||||||
|
command: EditUserCommand,
|
||||||
|
): User
|
||||||
|
|
||||||
|
fun delete(id: UUID)
|
||||||
|
|
||||||
|
fun getById(id: UUID): User
|
||||||
|
|
||||||
|
fun getAll(): List<User>
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CreateUserCommand(
|
data class CreateUserCommand(
|
||||||
@@ -12,26 +23,7 @@ data class CreateUserCommand(
|
|||||||
val email: String,
|
val email: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface EditUserUseCase {
|
|
||||||
fun edit(
|
|
||||||
id: UUID,
|
|
||||||
command: EditUserCommand,
|
|
||||||
): User
|
|
||||||
}
|
|
||||||
|
|
||||||
data class EditUserCommand(
|
data class EditUserCommand(
|
||||||
val name: String,
|
val name: String,
|
||||||
val jellyfinUserId: String? = null,
|
val jellyfinUserId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface DeleteUserUseCase {
|
|
||||||
fun delete(id: UUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetUserByIdUseCase {
|
|
||||||
fun getById(id: UUID): User
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetAllUsersUseCase {
|
|
||||||
fun getAll(): List<User>
|
|
||||||
}
|
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package com.project.movienight.application.ports.output
|
||||||
|
|
||||||
|
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||||
|
import com.project.movienight.domain.model.RecommendationEventType
|
||||||
|
|
||||||
|
interface BusinessMetricsPort {
|
||||||
|
fun recordFilmCreated()
|
||||||
|
|
||||||
|
fun recordFilmEdited()
|
||||||
|
|
||||||
|
fun recordFilmDeleted()
|
||||||
|
|
||||||
|
fun recordFilmBlocked()
|
||||||
|
|
||||||
|
fun recordRecommendationRequest()
|
||||||
|
|
||||||
|
fun recordRecommendationWeightsUpdated(eventType: RecommendationEventType)
|
||||||
|
|
||||||
|
fun recordRatingSubmitted()
|
||||||
|
|
||||||
|
fun recordLibraryEvent()
|
||||||
|
|
||||||
|
fun recordJellyfinSync(summary: JellyfinSyncSummary)
|
||||||
|
|
||||||
|
fun recordJellyfinSyncFailure()
|
||||||
|
|
||||||
|
fun recordJellyfinUnmappedUser()
|
||||||
|
|
||||||
|
fun recordBackendWriteFailure()
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package com.project.movienight.application.ports.output
|
||||||
|
|
||||||
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface FilmLibraryEntryRepositoryPort {
|
||||||
|
fun save(entry: FilmLibraryEntry): FilmLibraryEntry
|
||||||
|
|
||||||
|
fun findById(id: UUID): FilmLibraryEntry?
|
||||||
|
|
||||||
|
fun findByUserId(userId: UUID): List<FilmLibraryEntry>
|
||||||
|
|
||||||
|
fun findByUserIdAndFilmId(
|
||||||
|
userId: UUID,
|
||||||
|
filmId: UUID,
|
||||||
|
): FilmLibraryEntry?
|
||||||
|
|
||||||
|
fun findAll(): List<FilmLibraryEntry>
|
||||||
|
|
||||||
|
fun deleteById(id: UUID)
|
||||||
|
}
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package com.project.movienight.application.ports.output
|
|
||||||
|
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
interface FilmLibraryRepositoryPort {
|
|
||||||
fun save(filmLibrary: FilmLibrary): FilmLibrary
|
|
||||||
|
|
||||||
fun findById(id: UUID): FilmLibrary?
|
|
||||||
|
|
||||||
fun findByUserIdAndFilmId(
|
|
||||||
userId: UUID,
|
|
||||||
filmId: UUID,
|
|
||||||
): FilmLibrary?
|
|
||||||
|
|
||||||
fun findAll(): List<FilmLibrary>
|
|
||||||
|
|
||||||
fun deleteById(id: UUID)
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,6 @@ interface FilmRepositoryPort {
|
|||||||
|
|
||||||
fun findByJellyfinItemId(jellyfinItemId: String): Film?
|
fun findByJellyfinItemId(jellyfinItemId: String): Film?
|
||||||
|
|
||||||
fun findByJellyfinLibraryId(jellyfinLibraryId: String): Film?
|
|
||||||
|
|
||||||
fun findAll(): List<Film>
|
fun findAll(): List<Film>
|
||||||
|
|
||||||
fun findByTitle(title: String): Film?
|
fun findByTitle(title: String): Film?
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package com.project.movienight.application.ports.output
|
||||||
|
|
||||||
|
import com.project.movienight.domain.model.ContentType
|
||||||
|
|
||||||
|
interface JellyfinCatalogPort {
|
||||||
|
fun fetchUsers(): List<JellyfinRemoteUser>
|
||||||
|
|
||||||
|
fun fetchLibraryItems(userId: String): List<JellyfinLibraryItemSnapshot>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class JellyfinRemoteUser(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class JellyfinLibraryItemSnapshot(
|
||||||
|
val jellyfinItemId: String,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val contentType: ContentType,
|
||||||
|
val releaseYear: Int?,
|
||||||
|
val genres: List<String>,
|
||||||
|
val cast: List<String>,
|
||||||
|
val directors: List<String>,
|
||||||
|
val platformRating: Double?,
|
||||||
|
val imdbRating: Double?,
|
||||||
|
val externalUrl: String?,
|
||||||
|
val jellyfinLibraryId: String?,
|
||||||
|
val isPlayed: Boolean,
|
||||||
|
)
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.project.movienight.application.ports.output
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
interface JellyfinEventStorePort {
|
||||||
|
fun save(event: JellyfinEventRecord): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
data class JellyfinEventRecord(
|
||||||
|
val eventId: String,
|
||||||
|
val serverId: String?,
|
||||||
|
val eventType: String,
|
||||||
|
val occurredAt: OffsetDateTime?,
|
||||||
|
val jellyfinUserId: String?,
|
||||||
|
val jellyfinItemId: String?,
|
||||||
|
val payload: String?,
|
||||||
|
)
|
||||||
@@ -7,10 +7,24 @@ import java.util.UUID
|
|||||||
interface UserRepositoryPort {
|
interface UserRepositoryPort {
|
||||||
fun save(user: User): User
|
fun save(user: User): User
|
||||||
|
|
||||||
|
fun createOAuthUser(
|
||||||
|
user: User,
|
||||||
|
provider: AuthProvider,
|
||||||
|
providerId: String,
|
||||||
|
): User
|
||||||
|
|
||||||
|
fun linkOAuthAccount(
|
||||||
|
userId: UUID,
|
||||||
|
provider: AuthProvider,
|
||||||
|
providerId: String,
|
||||||
|
): User
|
||||||
|
|
||||||
fun findById(id: UUID): User?
|
fun findById(id: UUID): User?
|
||||||
|
|
||||||
fun findByEmail(email: String): User?
|
fun findByEmail(email: String): User?
|
||||||
|
|
||||||
|
fun findByJellyfinUserId(jellyfinUserId: String): User?
|
||||||
|
|
||||||
fun findAll(): List<User>
|
fun findAll(): List<User>
|
||||||
|
|
||||||
fun deleteById(id: UUID)
|
fun deleteById(id: UUID)
|
||||||
|
|||||||
@@ -1,46 +1,34 @@
|
|||||||
package com.project.movienight.application.services
|
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.AddFilmToLibraryCommand
|
||||||
import com.project.movienight.application.ports.input.AddFilmToLibraryUseCase
|
import com.project.movienight.application.ports.input.FilmLibraryUseCase
|
||||||
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.MarkFilmViewedCommand
|
||||||
import com.project.movienight.application.ports.input.MarkFilmViewedUseCase
|
|
||||||
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand
|
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand
|
||||||
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryUseCase
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||||
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
import com.project.movienight.domain.exception.DomainException
|
import com.project.movienight.domain.exception.DomainException
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.Film
|
||||||
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class FilmLibraryService(
|
class FilmLibraryService(
|
||||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
private val filmLibraryEntryRepository: FilmLibraryEntryRepositoryPort,
|
||||||
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
private val businessMetricsService: BusinessMetricsService,
|
private val businessMetricsService: BusinessMetricsPort,
|
||||||
) : CreateFilmLibraryUseCase,
|
) : FilmLibraryUseCase {
|
||||||
AddFilmToLibraryUseCase,
|
override fun addFilm(command: AddFilmToLibraryCommand): FilmLibraryEntry {
|
||||||
MarkFilmViewedUseCase,
|
ensureFilmExists(command.filmId)
|
||||||
RemoveFilmFromLibraryUseCase,
|
|
||||||
GetFilmLibraryUseCase,
|
|
||||||
ListFilmLibraryEntriesUseCase {
|
|
||||||
override fun create(command: CreateFilmLibraryCommand): FilmLibrary {
|
|
||||||
findByUserId(command.userId)?.let { return it }
|
|
||||||
throw EntityNotFoundException(entity = "Film library", id = command.userId.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addFilm(command: AddFilmToLibraryCommand): FilmLibrary {
|
val existingEntry = filmLibraryEntryRepository.findByUserIdAndFilmId(command.userId, command.filmId)
|
||||||
val existingEntry = findByUserAndFilmId(command.userId, command.filmId)
|
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
val saved =
|
val saved =
|
||||||
filmLibraryRepository.save(
|
filmLibraryEntryRepository.save(
|
||||||
existingEntry.copy(
|
existingEntry.copy(
|
||||||
isViewed = false,
|
isViewed = false,
|
||||||
watchedAt = null,
|
watchedAt = null,
|
||||||
@@ -51,8 +39,8 @@ class FilmLibraryService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val saved =
|
val saved =
|
||||||
filmLibraryRepository.save(
|
filmLibraryEntryRepository.save(
|
||||||
FilmLibrary(
|
FilmLibraryEntry(
|
||||||
id = idGenerator.generateId(),
|
id = idGenerator.generateId(),
|
||||||
userId = command.userId,
|
userId = command.userId,
|
||||||
filmId = command.filmId,
|
filmId = command.filmId,
|
||||||
@@ -65,33 +53,35 @@ class FilmLibraryService(
|
|||||||
return saved
|
return saved
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeFilm(command: RemoveFilmFromLibraryCommand): FilmLibrary {
|
override fun removeFilm(command: RemoveFilmFromLibraryCommand): FilmLibraryEntry {
|
||||||
val existingLibrary =
|
val existingEntry =
|
||||||
if (command.libraryId != null) {
|
if (command.entryId != null) {
|
||||||
filmLibraryRepository.findById(command.libraryId)
|
filmLibraryEntryRepository.findById(command.entryId)
|
||||||
?: throw EntityNotFoundException(entity = "Film library", id = command.libraryId.toString())
|
?: throw EntityNotFoundException(entity = "Film library entry", id = command.entryId.toString())
|
||||||
} else {
|
} else {
|
||||||
findByUserAndFilmId(command.userId, command.filmId)
|
filmLibraryEntryRepository.findByUserIdAndFilmId(command.userId, command.filmId)
|
||||||
?: throw EntityNotFoundException(entity = "Film library", id = command.filmId.toString())
|
?: throw EntityNotFoundException(entity = "Film library entry", id = command.filmId.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingLibrary.userId != command.userId || existingLibrary.filmId != command.filmId) {
|
if (existingEntry.userId != command.userId || existingEntry.filmId != command.filmId) {
|
||||||
throw DomainException("Film with id ${command.filmId} not found in user's library")
|
throw DomainException("Film with id ${command.filmId} not found in user's library")
|
||||||
}
|
}
|
||||||
|
|
||||||
filmLibraryRepository.deleteById(existingLibrary.id)
|
filmLibraryEntryRepository.deleteById(existingEntry.id)
|
||||||
businessMetricsService.recordLibraryEvent()
|
businessMetricsService.recordLibraryEvent()
|
||||||
return existingLibrary
|
return existingEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markViewed(command: MarkFilmViewedCommand): FilmLibrary {
|
override fun markViewed(command: MarkFilmViewedCommand): FilmLibraryEntry {
|
||||||
val existingEntry = findByUserAndFilmId(command.userId, command.filmId)
|
ensureFilmExists(command.filmId)
|
||||||
|
|
||||||
|
val existingEntry = filmLibraryEntryRepository.findByUserIdAndFilmId(command.userId, command.filmId)
|
||||||
val watchedAt = command.watchedAt ?: java.time.LocalDateTime.now()
|
val watchedAt = command.watchedAt ?: java.time.LocalDateTime.now()
|
||||||
|
|
||||||
val saved =
|
val saved =
|
||||||
if (existingEntry == null) {
|
if (existingEntry == null) {
|
||||||
filmLibraryRepository.save(
|
filmLibraryEntryRepository.save(
|
||||||
FilmLibrary(
|
FilmLibraryEntry(
|
||||||
id = idGenerator.generateId(),
|
id = idGenerator.generateId(),
|
||||||
userId = command.userId,
|
userId = command.userId,
|
||||||
filmId = command.filmId,
|
filmId = command.filmId,
|
||||||
@@ -101,7 +91,7 @@ class FilmLibraryService(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
filmLibraryRepository.save(
|
filmLibraryEntryRepository.save(
|
||||||
existingEntry.copy(
|
existingEntry.copy(
|
||||||
isViewed = true,
|
isViewed = true,
|
||||||
watchedAt = watchedAt,
|
watchedAt = watchedAt,
|
||||||
@@ -112,17 +102,14 @@ class FilmLibraryService(
|
|||||||
return saved
|
return saved
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLibrary(query: GetFilmLibraryQuery): FilmLibrary =
|
override fun list(userId: UUID): List<FilmLibraryEntry> = filmLibraryEntryRepository.findByUserId(userId)
|
||||||
findByUserId(query.userId)
|
|
||||||
?: throw EntityNotFoundException(entity = "Film library", id = query.userId.toString())
|
|
||||||
|
|
||||||
override fun list(userId: UUID): List<FilmLibrary> = filmLibraryRepository.findAll().filter { it.userId == userId }
|
override fun listAvailableFilms(userId: UUID): List<Film> {
|
||||||
|
val libraryFilmIds = list(userId).map { it.filmId }.toSet()
|
||||||
|
return filmRepository.findAll().filter { it.id !in libraryFilmIds }
|
||||||
|
}
|
||||||
|
|
||||||
private fun findByUserId(userId: UUID): FilmLibrary? =
|
private fun ensureFilmExists(filmId: UUID) {
|
||||||
filmLibraryRepository.findAll().firstOrNull { it.userId == userId }
|
filmRepository.findById(filmId) ?: throw EntityNotFoundException(entity = "Film", id = filmId.toString())
|
||||||
|
}
|
||||||
private fun findByUserAndFilmId(
|
|
||||||
userId: UUID,
|
|
||||||
filmId: UUID,
|
|
||||||
): FilmLibrary? = filmLibraryRepository.findAll().firstOrNull { it.userId == userId && it.filmId == filmId }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
import com.project.movienight.application.ports.input.FilmRatingUseCase
|
||||||
import com.project.movienight.application.ports.input.GetFilmRatingsUseCase
|
|
||||||
import com.project.movienight.application.ports.input.RateFilmCommand
|
import com.project.movienight.application.ports.input.RateFilmCommand
|
||||||
import com.project.movienight.application.ports.input.RateFilmUseCase
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
@@ -19,9 +18,8 @@ class FilmRatingService(
|
|||||||
private val filmRepository: FilmRepositoryPort,
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val filmRatingRepository: FilmRatingRepositoryPort,
|
private val filmRatingRepository: FilmRatingRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
private val businessMetricsService: BusinessMetricsService,
|
private val businessMetricsService: BusinessMetricsPort,
|
||||||
) : RateFilmUseCase,
|
) : FilmRatingUseCase {
|
||||||
GetFilmRatingsUseCase {
|
|
||||||
override fun rate(command: RateFilmCommand): FilmRating {
|
override fun rate(command: RateFilmCommand): FilmRating {
|
||||||
if (command.score !in 1..10) {
|
if (command.score !in 1..10) {
|
||||||
throw DomainException("Film rating score must be between 1 and 10")
|
throw DomainException("Film rating score must be between 1 and 10")
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.project.movienight.application.ports.input.CreateFilmCommand
|
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.EditFilmCommand
|
||||||
import com.project.movienight.application.ports.input.EditFilmUseCase
|
import com.project.movienight.application.ports.input.FilmUseCase
|
||||||
import com.project.movienight.application.ports.input.GetAllFilmsUseCase
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.application.ports.input.GetFilmByIdUseCase
|
|
||||||
import com.project.movienight.application.ports.input.SearchFilmByTitleUseCase
|
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
import com.project.movienight.config.FilmServiceProperties
|
import com.project.movienight.config.FilmServiceProperties
|
||||||
import com.project.movienight.domain.exception.BlockedValueException
|
import com.project.movienight.domain.exception.BlockedValueException
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import io.micrometer.core.instrument.Counter
|
import io.micrometer.core.annotation.Timed
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
|
||||||
import io.micrometer.core.instrument.Timer
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -26,89 +20,81 @@ class FilmService(
|
|||||||
private val filmRepository: FilmRepositoryPort,
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
private val filmConfig: FilmServiceProperties,
|
private val filmConfig: FilmServiceProperties,
|
||||||
private val meterRegistry: MeterRegistry,
|
private val businessMetricsService: BusinessMetricsPort,
|
||||||
) : CreateFilmUseCase,
|
) : FilmUseCase {
|
||||||
EditFilmUseCase,
|
|
||||||
DeleteFilmUseCase,
|
|
||||||
GetFilmByIdUseCase,
|
|
||||||
GetAllFilmsUseCase,
|
|
||||||
SearchFilmByTitleUseCase {
|
|
||||||
private val log = LoggerFactory.getLogger(javaClass)
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
@Timed(
|
||||||
|
value = "business_films_create_duration_seconds",
|
||||||
|
description = "Film creation duration",
|
||||||
|
)
|
||||||
override fun create(command: CreateFilmCommand): Film {
|
override fun create(command: CreateFilmCommand): Film {
|
||||||
val sample = Timer.start(meterRegistry)
|
log.debug(
|
||||||
|
"Create film request received: title='{}', descriptionLength={}",
|
||||||
|
command.title,
|
||||||
|
command.description.length,
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
if (filmConfig.isBlocked(command.title)) {
|
||||||
log.debug(
|
log.debug("Create film blocked by title policy: title='{}'", command.title)
|
||||||
"Create film request received: title='{}', descriptionLength={}",
|
businessMetricsService.recordFilmBlocked()
|
||||||
command.title,
|
throw BlockedValueException(target = "Film", field = "title")
|
||||||
command.description.length,
|
}
|
||||||
|
if (filmConfig.isBlocked(command.description)) {
|
||||||
|
log.debug("Create film blocked by description policy")
|
||||||
|
businessMetricsService.recordFilmBlocked()
|
||||||
|
throw BlockedValueException(target = "Film", field = "description")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (filmConfig.isBlocked(command.title)) {
|
val saved = filmRepository.save(film)
|
||||||
log.debug("Create film blocked by title policy: title='{}'", command.title)
|
businessMetricsService.recordFilmCreated()
|
||||||
filmBlockedCounter.increment()
|
return saved
|
||||||
throw BlockedValueException(target = "Film", field = "title")
|
|
||||||
}
|
|
||||||
if (filmConfig.isBlocked(command.description)) {
|
|
||||||
log.debug("Create film blocked by description policy")
|
|
||||||
filmBlockedCounter.increment()
|
|
||||||
throw BlockedValueException(target = "Film", field = "description")
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
val saved = filmRepository.save(film)
|
|
||||||
filmCreatedCounter.increment()
|
|
||||||
return saved
|
|
||||||
} finally {
|
|
||||||
sample.stop(createFilmTimer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed(
|
||||||
|
value = "business_films_edit_duration_seconds",
|
||||||
|
description = "Film edit duration",
|
||||||
|
)
|
||||||
override fun edit(
|
override fun edit(
|
||||||
id: UUID,
|
id: UUID,
|
||||||
command: EditFilmCommand,
|
command: EditFilmCommand,
|
||||||
): Film {
|
): Film {
|
||||||
val sample = Timer.start(meterRegistry)
|
log.debug("Edit film with id: {}", id)
|
||||||
|
|
||||||
try {
|
if (filmConfig.isBlocked(command.title)) {
|
||||||
log.debug("Edit film with id: {}", id)
|
log.debug("Edit film blocked by title policy: title='{}'", command.title)
|
||||||
|
businessMetricsService.recordFilmBlocked()
|
||||||
|
throw BlockedValueException(target = "Film", field = "title")
|
||||||
|
}
|
||||||
|
if (filmConfig.isBlocked(command.description)) {
|
||||||
|
log.debug("Edit film blocked by description policy")
|
||||||
|
businessMetricsService.recordFilmBlocked()
|
||||||
|
throw BlockedValueException(target = "Film", field = "description")
|
||||||
|
}
|
||||||
|
|
||||||
if (filmConfig.isBlocked(command.title)) {
|
val film =
|
||||||
log.debug("Edit film blocked by title policy: title='{}'", command.title)
|
filmRepository.findById(id)
|
||||||
filmBlockedCounter.increment()
|
?: throw EntityNotFoundException(entity = "Film", id = id.toString())
|
||||||
throw BlockedValueException(target = "Film", field = "title")
|
|
||||||
}
|
|
||||||
if (filmConfig.isBlocked(command.description)) {
|
|
||||||
log.debug("Edit film blocked by description policy")
|
|
||||||
filmBlockedCounter.increment()
|
|
||||||
throw BlockedValueException(target = "Film", field = "description")
|
|
||||||
}
|
|
||||||
|
|
||||||
var film = filmRepository.findById(id)
|
val saved =
|
||||||
|
filmRepository.save(
|
||||||
if (film == null) {
|
|
||||||
log.debug("Film not found for edit: id='{}'", id)
|
|
||||||
throw EntityNotFoundException(entity = "Film", id = id.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
film =
|
|
||||||
film.copy(
|
film.copy(
|
||||||
title = command.title,
|
title = command.title,
|
||||||
description = command.description,
|
description = command.description,
|
||||||
@@ -122,37 +108,30 @@ class FilmService(
|
|||||||
externalUrl = command.externalUrl,
|
externalUrl = command.externalUrl,
|
||||||
jellyfinItemId = command.jellyfinItemId,
|
jellyfinItemId = command.jellyfinItemId,
|
||||||
jellyfinLibraryId = command.jellyfinLibraryId,
|
jellyfinLibraryId = command.jellyfinLibraryId,
|
||||||
)
|
),
|
||||||
|
)
|
||||||
val saved = filmRepository.save(film)
|
businessMetricsService.recordFilmEdited()
|
||||||
filmEditedCounter.increment()
|
return saved
|
||||||
return saved
|
|
||||||
} finally {
|
|
||||||
sample.stop(editFilmTimer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed(
|
||||||
|
value = "business_films_delete_duration_seconds",
|
||||||
|
description = "Film deletion duration",
|
||||||
|
)
|
||||||
override fun delete(id: UUID) {
|
override fun delete(id: UUID) {
|
||||||
val sample = Timer.start(meterRegistry)
|
log.debug("Delete film with id: {}", id)
|
||||||
|
|
||||||
try {
|
val film = filmRepository.findById(id)
|
||||||
log.debug("Delete film with id: {}", id)
|
|
||||||
|
|
||||||
val film = filmRepository.findById(id)
|
if (film == null) {
|
||||||
|
log.debug("Film not found for delete: id='{}'", id)
|
||||||
if (film == null) {
|
throw EntityNotFoundException(entity = "Film", id = id.toString())
|
||||||
log.debug("Film not found for delete: id='{}'", id)
|
|
||||||
throw EntityNotFoundException(entity = "Film", id = id.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
filmRepository.deleteById(id)
|
|
||||||
|
|
||||||
filmDeletedCounter.increment()
|
|
||||||
|
|
||||||
log.info("Film deleted: id='{}'", id)
|
|
||||||
} finally {
|
|
||||||
sample.stop(deleteFilmTimer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filmRepository.deleteById(id)
|
||||||
|
businessMetricsService.recordFilmDeleted()
|
||||||
|
|
||||||
|
log.info("Film deleted: id='{}'", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getById(id: UUID): Film =
|
override fun getById(id: UUID): Film =
|
||||||
@@ -161,46 +140,4 @@ class FilmService(
|
|||||||
override fun getAll(): List<Film> = filmRepository.findAll()
|
override fun getAll(): List<Film> = filmRepository.findAll()
|
||||||
|
|
||||||
override fun searchByTitle(title: String): Film? = filmRepository.findByTitle(title)
|
override fun searchByTitle(title: String): Film? = filmRepository.findByTitle(title)
|
||||||
|
|
||||||
private val filmCreatedCounter =
|
|
||||||
Counter
|
|
||||||
.builder("film_created_total")
|
|
||||||
.description("Total number of created films")
|
|
||||||
.register(meterRegistry)
|
|
||||||
|
|
||||||
private val filmEditedCounter =
|
|
||||||
Counter
|
|
||||||
.builder("film_edited_total")
|
|
||||||
.description("Total number of successfully edited films")
|
|
||||||
.register(meterRegistry)
|
|
||||||
|
|
||||||
private val filmDeletedCounter =
|
|
||||||
Counter
|
|
||||||
.builder("film_deleted_total")
|
|
||||||
.description("Total number of successfully deleted films")
|
|
||||||
.register(meterRegistry)
|
|
||||||
|
|
||||||
private val filmBlockedCounter =
|
|
||||||
Counter
|
|
||||||
.builder("films.blocked")
|
|
||||||
.description("Total blocked film operations")
|
|
||||||
.register(meterRegistry)
|
|
||||||
|
|
||||||
private val createFilmTimer =
|
|
||||||
Timer
|
|
||||||
.builder("films.create.duration")
|
|
||||||
.description("Film creation duration")
|
|
||||||
.register(meterRegistry)
|
|
||||||
|
|
||||||
private val editFilmTimer =
|
|
||||||
Timer
|
|
||||||
.builder("films.edit.duration")
|
|
||||||
.description("Film edit duration")
|
|
||||||
.register(meterRegistry)
|
|
||||||
|
|
||||||
private val deleteFilmTimer =
|
|
||||||
Timer
|
|
||||||
.builder("films.delete.duration")
|
|
||||||
.description("Film deletion duration")
|
|
||||||
.register(meterRegistry)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-36
@@ -1,79 +1,73 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
import com.project.movienight.application.ports.input.FilmLibraryUseCase
|
||||||
import com.project.movienight.adapters.persistence.jdbc.JellyfinEventRepository
|
import com.project.movienight.application.ports.input.HandleJellyfinEventCommand
|
||||||
|
import com.project.movienight.application.ports.input.JellyfinEventUseCase
|
||||||
import com.project.movienight.application.ports.input.MarkFilmViewedCommand
|
import com.project.movienight.application.ports.input.MarkFilmViewedCommand
|
||||||
import com.project.movienight.application.ports.input.MarkFilmViewedUseCase
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinEventRecord
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinEventStorePort
|
||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.OffsetDateTime
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class JellyfinEventService(
|
class JellyfinEventService(
|
||||||
private val jellyfinEventRepository: JellyfinEventRepository,
|
private val jellyfinEventStore: JellyfinEventStorePort,
|
||||||
private val userRepository: UserRepositoryPort,
|
private val userRepository: UserRepositoryPort,
|
||||||
private val filmRepository: FilmRepositoryPort,
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val markFilmViewedUseCase: MarkFilmViewedUseCase,
|
private val filmLibraryUseCase: FilmLibraryUseCase,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val businessMetricsService: BusinessMetricsService,
|
private val businessMetricsService: BusinessMetricsPort,
|
||||||
) {
|
) : JellyfinEventUseCase {
|
||||||
private val playbackEventTypes = setOf("playback.ended", "playback.stopped", "playback.completed")
|
private val playbackEventTypes = setOf("playback.ended", "playback.stopped", "playback.completed")
|
||||||
|
|
||||||
fun handleEvent(
|
@Transactional
|
||||||
eventId: String,
|
override fun handle(command: HandleJellyfinEventCommand) {
|
||||||
serverId: String?,
|
val payloadJson = command.payload?.let { objectMapper.writeValueAsString(it) }
|
||||||
eventType: String,
|
|
||||||
occurredAt: OffsetDateTime,
|
|
||||||
jellyfinUserId: String,
|
|
||||||
itemId: String,
|
|
||||||
payload: Map<String, Any>?,
|
|
||||||
) {
|
|
||||||
val payloadJson = payload?.let { objectMapper.writeValueAsString(it) }
|
|
||||||
val inserted =
|
val inserted =
|
||||||
jellyfinEventRepository.save(
|
jellyfinEventStore.save(
|
||||||
eventId = eventId,
|
JellyfinEventRecord(
|
||||||
serverId = serverId,
|
eventId = command.eventId,
|
||||||
eventType = eventType,
|
serverId = command.serverId,
|
||||||
occurredAt = occurredAt,
|
eventType = command.eventType,
|
||||||
jellyfinUserId = jellyfinUserId,
|
occurredAt = command.occurredAt,
|
||||||
jellyfinItemId = itemId,
|
jellyfinUserId = command.jellyfinUserId,
|
||||||
payload = payloadJson,
|
jellyfinItemId = command.itemId,
|
||||||
|
payload = payloadJson,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (inserted != 1) {
|
if (!inserted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (playbackEventTypes.contains(eventType)) {
|
if (playbackEventTypes.contains(command.eventType)) {
|
||||||
val localUser = userRepository.findAll().firstOrNull { it.jellyfinUserId == jellyfinUserId }
|
val localUser = userRepository.findByJellyfinUserId(command.jellyfinUserId)
|
||||||
if (localUser == null) {
|
if (localUser == null) {
|
||||||
jellyfinEventRepository.delete(eventId)
|
|
||||||
businessMetricsService.recordJellyfinUnmappedUser()
|
businessMetricsService.recordJellyfinUnmappedUser()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val film = filmRepository.findByJellyfinItemId(itemId)
|
val film = filmRepository.findByJellyfinItemId(command.itemId)
|
||||||
if (film == null) {
|
if (film == null) {
|
||||||
jellyfinEventRepository.delete(eventId)
|
|
||||||
businessMetricsService.recordBackendWriteFailure()
|
businessMetricsService.recordBackendWriteFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
markFilmViewedUseCase.markViewed(
|
filmLibraryUseCase.markViewed(
|
||||||
MarkFilmViewedCommand(
|
MarkFilmViewedCommand(
|
||||||
userId = localUser.id,
|
userId = localUser.id,
|
||||||
filmId = film.id,
|
filmId = film.id,
|
||||||
watchedAt = occurredAt.toLocalDateTime(),
|
watchedAt = command.occurredAt.toLocalDateTime(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
businessMetricsService.recordLibraryEvent()
|
|
||||||
}
|
}
|
||||||
} catch (
|
} catch (
|
||||||
@Suppress("TooGenericExceptionCaught") ex: RuntimeException,
|
@Suppress("TooGenericExceptionCaught") ex: RuntimeException,
|
||||||
) {
|
) {
|
||||||
jellyfinEventRepository.delete(eventId)
|
|
||||||
businessMetricsService.recordBackendWriteFailure()
|
businessMetricsService.recordBackendWriteFailure()
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-35
@@ -1,18 +1,17 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.project.movienight.adapters.jellyfin.JellyfinApiClient
|
import com.project.movienight.application.ports.input.JellyfinSyncUseCase
|
||||||
import com.project.movienight.adapters.jellyfin.JellyfinLibraryItemSnapshot
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.adapters.jellyfin.JellyfinRemoteUser
|
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
|
||||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinCatalogPort
|
||||||
|
import com.project.movienight.application.ports.output.JellyfinLibraryItemSnapshot
|
||||||
import com.project.movienight.application.ports.output.JellyfinSyncStateRepositoryPort
|
import com.project.movienight.application.ports.output.JellyfinSyncStateRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||||
import com.project.movienight.domain.model.ContentType
|
|
||||||
import com.project.movienight.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import com.project.movienight.domain.model.JellyfinSyncState
|
import com.project.movienight.domain.model.JellyfinSyncState
|
||||||
import com.project.movienight.domain.model.JellyfinSyncSummary
|
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
@@ -20,18 +19,19 @@ import org.springframework.stereotype.Service
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class JellyfinSyncService(
|
class JellyfinSyncService(
|
||||||
private val properties: JellyfinIntegrationProperties,
|
private val properties: JellyfinIntegrationProperties,
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinCatalog: JellyfinCatalogPort,
|
||||||
private val userRepository: UserRepositoryPort,
|
private val userRepository: UserRepositoryPort,
|
||||||
private val filmRepository: FilmRepositoryPort,
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
private val filmLibraryEntryRepository: FilmLibraryEntryRepositoryPort,
|
||||||
private val syncStateRepository: JellyfinSyncStateRepositoryPort,
|
private val syncStateRepository: JellyfinSyncStateRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
private val businessMetricsService: BusinessMetricsService,
|
private val businessMetricsService: BusinessMetricsPort,
|
||||||
) {
|
) : JellyfinSyncUseCase {
|
||||||
@Scheduled(fixedDelayString = "\${integrations.jellyfin.sync-interval-ms:1800000}")
|
@Scheduled(fixedDelayString = "\${integrations.jellyfin.sync-interval-ms:1800000}")
|
||||||
fun scheduledSync() {
|
fun scheduledSync() {
|
||||||
if (properties.enabled) {
|
if (properties.enabled) {
|
||||||
@@ -39,13 +39,26 @@ class JellyfinSyncService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncNow(): JellyfinSyncSummary {
|
override fun syncNow(): JellyfinSyncSummary {
|
||||||
if (!properties.enabled || properties.baseUrl.isBlank() || properties.apiKey.isBlank()) {
|
if (!properties.enabled || properties.baseUrl.isBlank() || properties.apiKey.isBlank()) {
|
||||||
return JellyfinSyncSummary(syncedUsers = 0, skippedUsers = 0, syncedItems = 0, durationMs = 0)
|
return JellyfinSyncSummary(syncedUsers = 0, skippedUsers = 0, syncedItems = 0, durationMs = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
runSync()
|
||||||
|
} catch (
|
||||||
|
@Suppress("TooGenericExceptionCaught") ex: RuntimeException,
|
||||||
|
) {
|
||||||
|
businessMetricsService.recordJellyfinSyncFailure()
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSyncStates(): List<JellyfinSyncState> = syncStateRepository.findAll()
|
||||||
|
|
||||||
|
private fun runSync(): JellyfinSyncSummary {
|
||||||
val startedAt = Instant.now()
|
val startedAt = Instant.now()
|
||||||
val remoteUsers = jellyfinApiClient.fetchUsers()
|
val remoteUsers = jellyfinCatalog.fetchUsers()
|
||||||
val localUsersByJellyfinId =
|
val localUsersByJellyfinId =
|
||||||
userRepository
|
userRepository
|
||||||
.findAll()
|
.findAll()
|
||||||
@@ -64,7 +77,7 @@ class JellyfinSyncService(
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = jellyfinApiClient.fetchLibraryItems(remoteUser.id)
|
val items = jellyfinCatalog.fetchLibraryItems(remoteUser.id)
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
syncItem(localUser.id, item)
|
syncItem(localUser.id, item)
|
||||||
syncedItems += 1
|
syncedItems += 1
|
||||||
@@ -94,12 +107,22 @@ class JellyfinSyncService(
|
|||||||
return summary
|
return summary
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSyncStates(): List<JellyfinSyncState> = syncStateRepository.findAll()
|
|
||||||
|
|
||||||
private fun syncItem(
|
private fun syncItem(
|
||||||
userId: java.util.UUID,
|
userId: UUID,
|
||||||
item: JellyfinLibraryItemSnapshot,
|
item: JellyfinLibraryItemSnapshot,
|
||||||
) {
|
) {
|
||||||
|
val savedFilm = upsertFilm(item)
|
||||||
|
|
||||||
|
if (item.isPlayed) {
|
||||||
|
markFilmViewed(
|
||||||
|
userId = userId,
|
||||||
|
filmId = savedFilm.id,
|
||||||
|
watchedAt = LocalDateTime.now(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upsertFilm(item: JellyfinLibraryItemSnapshot): Film {
|
||||||
val film =
|
val film =
|
||||||
filmRepository.findByJellyfinItemId(item.jellyfinItemId)?.copy(
|
filmRepository.findByJellyfinItemId(item.jellyfinItemId)?.copy(
|
||||||
title = item.title,
|
title = item.title,
|
||||||
@@ -130,24 +153,27 @@ class JellyfinSyncService(
|
|||||||
jellyfinLibraryId = item.jellyfinLibraryId,
|
jellyfinLibraryId = item.jellyfinLibraryId,
|
||||||
)
|
)
|
||||||
|
|
||||||
val savedFilm = filmRepository.save(film)
|
return filmRepository.save(film)
|
||||||
|
}
|
||||||
|
|
||||||
if (item.isPlayed) {
|
private fun markFilmViewed(
|
||||||
val watchedAt = LocalDateTime.now()
|
userId: UUID,
|
||||||
val existingEntry = filmLibraryRepository.findByUserIdAndFilmId(userId, savedFilm.id)
|
filmId: UUID,
|
||||||
filmLibraryRepository.save(
|
watchedAt: LocalDateTime,
|
||||||
existingEntry?.copy(
|
) {
|
||||||
isViewed = true,
|
val existingEntry = filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId)
|
||||||
watchedAt = watchedAt,
|
filmLibraryEntryRepository.save(
|
||||||
) ?: FilmLibrary(
|
existingEntry?.copy(
|
||||||
id = idGenerator.generateId(),
|
isViewed = true,
|
||||||
userId = userId,
|
watchedAt = watchedAt,
|
||||||
filmId = savedFilm.id,
|
) ?: FilmLibraryEntry(
|
||||||
comment = null,
|
id = idGenerator.generateId(),
|
||||||
isViewed = true,
|
userId = userId,
|
||||||
watchedAt = watchedAt,
|
filmId = filmId,
|
||||||
),
|
comment = null,
|
||||||
)
|
isViewed = true,
|
||||||
}
|
watchedAt = watchedAt,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -3,7 +3,7 @@ package com.project.movienight.application.services
|
|||||||
import com.project.movienight.application.ports.input.CompleteRecommendationOnboardingCommand
|
import com.project.movienight.application.ports.input.CompleteRecommendationOnboardingCommand
|
||||||
import com.project.movienight.application.ports.input.CompleteRecommendationOnboardingUseCase
|
import com.project.movienight.application.ports.input.CompleteRecommendationOnboardingUseCase
|
||||||
import com.project.movienight.application.ports.input.RecommendationOnboardingResult
|
import com.project.movienight.application.ports.input.RecommendationOnboardingResult
|
||||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
@@ -11,7 +11,7 @@ import com.project.movienight.application.ports.output.UserPreferencesRepository
|
|||||||
import com.project.movienight.application.ports.output.UserRecommendationWeightsRepositoryPort
|
import com.project.movienight.application.ports.output.UserRecommendationWeightsRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import com.project.movienight.domain.model.FilmRating
|
import com.project.movienight.domain.model.FilmRating
|
||||||
import com.project.movienight.domain.model.UserPreferences
|
import com.project.movienight.domain.model.UserPreferences
|
||||||
import com.project.movienight.domain.model.UserRecommendationWeights
|
import com.project.movienight.domain.model.UserRecommendationWeights
|
||||||
@@ -25,7 +25,7 @@ class RecommendationOnboardingService(
|
|||||||
private val filmRepository: FilmRepositoryPort,
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
||||||
private val filmRatingRepository: FilmRatingRepositoryPort,
|
private val filmRatingRepository: FilmRatingRepositoryPort,
|
||||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
private val filmLibraryEntryRepository: FilmLibraryEntryRepositoryPort,
|
||||||
private val userRecommendationWeightsRepository: UserRecommendationWeightsRepositoryPort,
|
private val userRecommendationWeightsRepository: UserRecommendationWeightsRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
) : CompleteRecommendationOnboardingUseCase {
|
) : CompleteRecommendationOnboardingUseCase {
|
||||||
@@ -125,12 +125,12 @@ class RecommendationOnboardingService(
|
|||||||
userId: UUID,
|
userId: UUID,
|
||||||
filmId: UUID,
|
filmId: UUID,
|
||||||
isViewed: Boolean,
|
isViewed: Boolean,
|
||||||
): FilmLibrary {
|
): FilmLibraryEntry {
|
||||||
val watchedAt = LocalDateTime.now().takeIf { isViewed }
|
val watchedAt = LocalDateTime.now().takeIf { isViewed }
|
||||||
val existing = filmLibraryRepository.findByUserIdAndFilmId(userId, filmId)
|
val existing = filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId)
|
||||||
return filmLibraryRepository.save(
|
return filmLibraryEntryRepository.save(
|
||||||
existing?.copy(isViewed = isViewed, watchedAt = watchedAt)
|
existing?.copy(isViewed = isViewed, watchedAt = watchedAt)
|
||||||
?: FilmLibrary(
|
?: FilmLibraryEntry(
|
||||||
id = idGenerator.generateId(),
|
id = idGenerator.generateId(),
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
|
|||||||
+7
-7
@@ -1,13 +1,13 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
|
||||||
import com.project.movienight.application.ports.input.AcceptRecommendationCommand
|
import com.project.movienight.application.ports.input.AcceptRecommendationCommand
|
||||||
import com.project.movienight.application.ports.input.AcceptRecommendationUseCase
|
import com.project.movienight.application.ports.input.AcceptRecommendationUseCase
|
||||||
import com.project.movienight.application.ports.input.GetRecommendationsUseCase
|
import com.project.movienight.application.ports.input.GetRecommendationsUseCase
|
||||||
import com.project.movienight.application.ports.input.RecommendationQuery
|
import com.project.movienight.application.ports.input.RecommendationQuery
|
||||||
import com.project.movienight.application.ports.input.RejectRecommendationCommand
|
import com.project.movienight.application.ports.input.RejectRecommendationCommand
|
||||||
import com.project.movienight.application.ports.input.RejectRecommendationUseCase
|
import com.project.movienight.application.ports.input.RejectRecommendationUseCase
|
||||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
|
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
@@ -17,7 +17,7 @@ import com.project.movienight.application.ports.output.UserRecommendationWeights
|
|||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import com.project.movienight.domain.model.FilmRating
|
import com.project.movienight.domain.model.FilmRating
|
||||||
import com.project.movienight.domain.model.RecommendationEvent
|
import com.project.movienight.domain.model.RecommendationEvent
|
||||||
import com.project.movienight.domain.model.RecommendationEventType
|
import com.project.movienight.domain.model.RecommendationEventType
|
||||||
@@ -34,14 +34,14 @@ import kotlin.math.sqrt
|
|||||||
@Service
|
@Service
|
||||||
class RecommendationService(
|
class RecommendationService(
|
||||||
private val filmRepository: FilmRepositoryPort,
|
private val filmRepository: FilmRepositoryPort,
|
||||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
private val filmLibraryEntryRepository: FilmLibraryEntryRepositoryPort,
|
||||||
private val filmRatingRepository: FilmRatingRepositoryPort,
|
private val filmRatingRepository: FilmRatingRepositoryPort,
|
||||||
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
||||||
private val userRepository: UserRepositoryPort,
|
private val userRepository: UserRepositoryPort,
|
||||||
private val recommendationEventRepository: RecommendationEventRepositoryPort,
|
private val recommendationEventRepository: RecommendationEventRepositoryPort,
|
||||||
private val userRecommendationWeightsRepository: UserRecommendationWeightsRepositoryPort,
|
private val userRecommendationWeightsRepository: UserRecommendationWeightsRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
private val businessMetricsService: BusinessMetricsService,
|
private val businessMetricsService: BusinessMetricsPort,
|
||||||
) : GetRecommendationsUseCase,
|
) : GetRecommendationsUseCase,
|
||||||
AcceptRecommendationUseCase,
|
AcceptRecommendationUseCase,
|
||||||
RejectRecommendationUseCase {
|
RejectRecommendationUseCase {
|
||||||
@@ -54,7 +54,7 @@ class RecommendationService(
|
|||||||
|
|
||||||
val preferences = userPreferencesRepository.findByUserId(query.userId)
|
val preferences = userPreferencesRepository.findByUserId(query.userId)
|
||||||
val ratings = filmRatingRepository.findByUserId(query.userId)
|
val ratings = filmRatingRepository.findByUserId(query.userId)
|
||||||
val libraryEntries = filmLibraryRepository.findAll().filter { it.userId == query.userId }
|
val libraryEntries = filmLibraryEntryRepository.findByUserId(query.userId)
|
||||||
val libraryFilmIds = libraryEntries.map { it.filmId }.toSet()
|
val libraryFilmIds = libraryEntries.map { it.filmId }.toSet()
|
||||||
val watchedFilmIds = libraryEntries.filter { it.isViewed }.map { it.filmId }.toSet()
|
val watchedFilmIds = libraryEntries.filter { it.isViewed }.map { it.filmId }.toSet()
|
||||||
val films = filmRepository.findAll()
|
val films = filmRepository.findAll()
|
||||||
@@ -278,7 +278,7 @@ class RecommendationService(
|
|||||||
private fun buildUserProfile(
|
private fun buildUserProfile(
|
||||||
preferences: UserPreferences?,
|
preferences: UserPreferences?,
|
||||||
ratings: List<FilmRating>,
|
ratings: List<FilmRating>,
|
||||||
libraryEntries: List<FilmLibrary>,
|
libraryEntries: List<FilmLibraryEntry>,
|
||||||
filmsById: Map<UUID, Film>,
|
filmsById: Map<UUID, Film>,
|
||||||
weights: UserRecommendationWeights,
|
weights: UserRecommendationWeights,
|
||||||
): SparseVector {
|
): SparseVector {
|
||||||
|
|||||||
+2
-4
@@ -1,8 +1,7 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.project.movienight.application.ports.input.GetUserPreferencesUseCase
|
|
||||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesCommand
|
import com.project.movienight.application.ports.input.UpsertUserPreferencesCommand
|
||||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesUseCase
|
import com.project.movienight.application.ports.input.UserPreferencesUseCase
|
||||||
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
|
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
|
||||||
import com.project.movienight.domain.model.UserPreferences
|
import com.project.movienight.domain.model.UserPreferences
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -10,8 +9,7 @@ import org.springframework.stereotype.Service
|
|||||||
@Service
|
@Service
|
||||||
class UserPreferencesService(
|
class UserPreferencesService(
|
||||||
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
||||||
) : UpsertUserPreferencesUseCase,
|
) : UserPreferencesUseCase {
|
||||||
GetUserPreferencesUseCase {
|
|
||||||
override fun upsert(command: UpsertUserPreferencesCommand): UserPreferences =
|
override fun upsert(command: UpsertUserPreferencesCommand): UserPreferences =
|
||||||
userPreferencesRepository.save(
|
userPreferencesRepository.save(
|
||||||
UserPreferences(
|
UserPreferences(
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
package com.project.movienight.application.services
|
package com.project.movienight.application.services
|
||||||
|
|
||||||
import com.project.movienight.application.ports.input.CreateUserCommand
|
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.EditUserCommand
|
||||||
import com.project.movienight.application.ports.input.EditUserUseCase
|
import com.project.movienight.application.ports.input.UserUseCase
|
||||||
import com.project.movienight.application.ports.input.GetAllUsersUseCase
|
|
||||||
import com.project.movienight.application.ports.input.GetUserByIdUseCase
|
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||||
import com.project.movienight.config.UserServiceProperties
|
import com.project.movienight.config.UserServiceProperties
|
||||||
@@ -21,11 +17,7 @@ class UserService(
|
|||||||
private val userRepository: UserRepositoryPort,
|
private val userRepository: UserRepositoryPort,
|
||||||
private val idGenerator: IdGenerator,
|
private val idGenerator: IdGenerator,
|
||||||
private val userConfig: UserServiceProperties,
|
private val userConfig: UserServiceProperties,
|
||||||
) : CreateUserUseCase,
|
) : UserUseCase {
|
||||||
EditUserUseCase,
|
|
||||||
DeleteUserUseCase,
|
|
||||||
GetUserByIdUseCase,
|
|
||||||
GetAllUsersUseCase {
|
|
||||||
override fun create(command: CreateUserCommand): User {
|
override fun create(command: CreateUserCommand): User {
|
||||||
if (userConfig.isBlocked(command.name)) {
|
if (userConfig.isBlocked(command.name)) {
|
||||||
throw BlockedValueException(target = "User", field = "name")
|
throw BlockedValueException(target = "User", field = "name")
|
||||||
@@ -36,7 +28,6 @@ class UserService(
|
|||||||
id = idGenerator.generateId(),
|
id = idGenerator.generateId(),
|
||||||
name = command.name,
|
name = command.name,
|
||||||
email = command.email,
|
email = command.email,
|
||||||
library = null,
|
|
||||||
jellyfinUserId = null,
|
jellyfinUserId = null,
|
||||||
)
|
)
|
||||||
return userRepository.save(user)
|
return userRepository.save(user)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties
|
|||||||
data class JellyfinIntegrationProperties(
|
data class JellyfinIntegrationProperties(
|
||||||
val enabled: Boolean = false,
|
val enabled: Boolean = false,
|
||||||
val baseUrl: String = "",
|
val baseUrl: String = "",
|
||||||
val webUrl: String = "",
|
val webUrl: String = baseUrl,
|
||||||
val apiKey: String = "",
|
val apiKey: String = "",
|
||||||
val syncIntervalMs: Long = 1_800_000,
|
val syncIntervalMs: Long = 1_800_000,
|
||||||
val requestTimeoutMs: Long = 20_000,
|
val requestTimeoutMs: Long = 20_000,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.project.movienight.config
|
||||||
|
|
||||||
|
import io.micrometer.core.aop.TimedAspect
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class MetricsConfiguration {
|
||||||
|
@Bean
|
||||||
|
fun timedAspect(meterRegistry: MeterRegistry): TimedAspect = TimedAspect(meterRegistry)
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ package com.project.movienight.domain.model
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class FilmLibrary(
|
data class FilmLibraryEntry(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val userId: UUID,
|
val userId: UUID,
|
||||||
val filmId: UUID,
|
val filmId: UUID,
|
||||||
@@ -6,7 +6,6 @@ data class User(
|
|||||||
val id: UUID,
|
val id: UUID,
|
||||||
val name: String,
|
val name: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val library: FilmLibrary?,
|
|
||||||
val preferences: UserPreferences? = null,
|
val preferences: UserPreferences? = null,
|
||||||
val jellyfinUserId: String? = null,
|
val jellyfinUserId: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,12 +6,26 @@ erDiagram
|
|||||||
UUID id PK
|
UUID id PK
|
||||||
VARCHAR name
|
VARCHAR name
|
||||||
VARCHAR email
|
VARCHAR email
|
||||||
|
VARCHAR provider
|
||||||
|
VARCHAR provider_id
|
||||||
|
VARCHAR jellyfin_user_id
|
||||||
|
TIMESTAMP created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
films {
|
films {
|
||||||
UUID id PK
|
UUID id PK
|
||||||
VARCHAR title
|
VARCHAR title
|
||||||
TEXT description
|
TEXT description
|
||||||
|
VARCHAR content_type
|
||||||
|
INT release_year
|
||||||
|
TEXT genres
|
||||||
|
TEXT cast_members
|
||||||
|
TEXT directors
|
||||||
|
DOUBLE imdb_rating
|
||||||
|
DOUBLE platform_rating
|
||||||
|
TEXT external_url
|
||||||
|
VARCHAR jellyfin_item_id
|
||||||
|
VARCHAR jellyfin_library_id
|
||||||
}
|
}
|
||||||
|
|
||||||
favorites {
|
favorites {
|
||||||
@@ -20,8 +34,53 @@ erDiagram
|
|||||||
UUID film_id FK
|
UUID film_id FK
|
||||||
VARCHAR comment
|
VARCHAR comment
|
||||||
BOOLEAN is_viewed
|
BOOLEAN is_viewed
|
||||||
|
TIMESTAMP watched_at
|
||||||
|
}
|
||||||
|
|
||||||
|
user_preferences {
|
||||||
|
UUID user_id PK,FK
|
||||||
|
TEXT weighted_genres
|
||||||
|
TEXT plot_types
|
||||||
|
TEXT eras
|
||||||
|
TEXT cast_and_directors
|
||||||
|
TEXT moods
|
||||||
|
TEXT content_types
|
||||||
|
}
|
||||||
|
|
||||||
|
film_ratings {
|
||||||
|
UUID id PK
|
||||||
|
UUID user_id FK
|
||||||
|
UUID film_id FK
|
||||||
|
INT score
|
||||||
|
VARCHAR note
|
||||||
|
TIMESTAMP created_at
|
||||||
|
TIMESTAMP updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
jellyfin_events {
|
||||||
|
VARCHAR event_id PK
|
||||||
|
VARCHAR server_id
|
||||||
|
VARCHAR event_type
|
||||||
|
TIMESTAMP occurred_at
|
||||||
|
VARCHAR jellyfin_user_id
|
||||||
|
VARCHAR jellyfin_item_id
|
||||||
|
JSON payload
|
||||||
|
TIMESTAMP created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
jellyfin_sync_state {
|
||||||
|
UUID user_id PK,FK
|
||||||
|
TIMESTAMP last_synced_at
|
||||||
|
TIMESTAMP last_successful_sync_at
|
||||||
|
TEXT last_error
|
||||||
|
INT synced_item_count
|
||||||
|
TIMESTAMP updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
users ||--o{ favorites : has
|
users ||--o{ favorites : has
|
||||||
films ||--o{ favorites : linked
|
films ||--o{ favorites : linked
|
||||||
|
users ||--o{ film_ratings : rates
|
||||||
|
films ||--o{ film_ratings : rated
|
||||||
|
users ||--|| user_preferences : configures
|
||||||
|
users ||--|| jellyfin_sync_state : syncs
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
DROP TABLE IF EXISTS public.ratings;
|
||||||
|
|
||||||
|
ALTER TABLE public.users
|
||||||
|
DROP COLUMN IF EXISTS jellyfin_id;
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
DROP COLUMN IF EXISTS jellyfin_id;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_jellyfin_user_id ON public.users(jellyfin_user_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_films_jellyfin_item_id ON public.films(jellyfin_item_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_films_jellyfin_library_id ON public.films(jellyfin_library_id);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.project.movienight
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
|
class ClassLoaderTest {
|
||||||
|
@Test
|
||||||
|
fun `can load OAuth2ClientProperties class`() {
|
||||||
|
val clazz =
|
||||||
|
Class.forName(
|
||||||
|
"org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties",
|
||||||
|
)
|
||||||
|
assertNotNull(clazz)
|
||||||
|
println("Successfully loaded: ${clazz.name}")
|
||||||
|
println("ClassLoader: ${clazz.classLoader}")
|
||||||
|
}
|
||||||
|
}
|
||||||
-4
@@ -26,7 +26,6 @@ class UserEntityMappingTest {
|
|||||||
assertEquals(entity.id, user.id)
|
assertEquals(entity.id, user.id)
|
||||||
assertEquals(entity.name, user.name)
|
assertEquals(entity.name, user.name)
|
||||||
assertEquals(entity.email, user.email)
|
assertEquals(entity.email, user.email)
|
||||||
assertNull(user.library)
|
|
||||||
assertNull(user.jellyfinUserId)
|
assertNull(user.jellyfinUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ class UserEntityMappingTest {
|
|||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
name = "Jane",
|
name = "Jane",
|
||||||
email = "jane@mail.com",
|
email = "jane@mail.com",
|
||||||
library = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val entity = user.toEntity(AuthProvider.YANDEX, "yandex456")
|
val entity = user.toEntity(AuthProvider.YANDEX, "yandex456")
|
||||||
@@ -56,7 +54,6 @@ class UserEntityMappingTest {
|
|||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
name = "Bob",
|
name = "Bob",
|
||||||
email = "bob@mail.com",
|
email = "bob@mail.com",
|
||||||
library = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val entity = user.toEntity()
|
val entity = user.toEntity()
|
||||||
@@ -72,7 +69,6 @@ class UserEntityMappingTest {
|
|||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
name = "Alice",
|
name = "Alice",
|
||||||
email = "alice@email.com",
|
email = "alice@email.com",
|
||||||
library = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val mapped = original.toEntity().toDomain()
|
val mapped = original.toEntity().toDomain()
|
||||||
|
|||||||
+39
-39
@@ -1,7 +1,7 @@
|
|||||||
package com.project.movienight.adapters.persistence.jdbc
|
package com.project.movienight.adapters.persistence.jdbc
|
||||||
|
|
||||||
import com.project.movienight.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import com.project.movienight.domain.model.User
|
import com.project.movienight.domain.model.User
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@@ -19,9 +19,9 @@ import java.util.UUID
|
|||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
class FilmLibraryRepositoryIntegrationTest {
|
class FilmLibraryEntryRepositoryIntegrationTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var filmLibraryRepository: FilmLibraryRepository
|
private lateinit var filmLibraryEntryRepository: FilmLibraryEntryRepository
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var userRepository: UserRepository
|
private lateinit var userRepository: UserRepository
|
||||||
@@ -62,7 +62,7 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `should save new film library entry and return saved entry`() {
|
fun `should save new film library entry and return saved entry`() {
|
||||||
val entry =
|
val entry =
|
||||||
FilmLibrary(
|
FilmLibraryEntry(
|
||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
userId = testUser.id,
|
userId = testUser.id,
|
||||||
filmId = testFilm.id,
|
filmId = testFilm.id,
|
||||||
@@ -70,7 +70,7 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
isViewed = false,
|
isViewed = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val savedEntry = filmLibraryRepository.save(entry)
|
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
assertNotNull(savedEntry)
|
assertNotNull(savedEntry)
|
||||||
assertEquals(entry.id, savedEntry.id)
|
assertEquals(entry.id, savedEntry.id)
|
||||||
@@ -83,17 +83,17 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `should update existing film library entry`() {
|
fun `should update existing film library entry`() {
|
||||||
val entryId = UUID.randomUUID()
|
val entryId = UUID.randomUUID()
|
||||||
val originalEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Хочу посмотреть", false)
|
val originalEntry = FilmLibraryEntry(entryId, testUser.id, testFilm.id, "Хочу посмотреть", false)
|
||||||
filmLibraryRepository.save(originalEntry)
|
filmLibraryEntryRepository.save(originalEntry)
|
||||||
|
|
||||||
val updatedEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Уже посмотрел, потрясающе!", true)
|
val updatedEntry = FilmLibraryEntry(entryId, testUser.id, testFilm.id, "Уже посмотрел, потрясающе!", true)
|
||||||
val result = filmLibraryRepository.save(updatedEntry)
|
val result = filmLibraryEntryRepository.save(updatedEntry)
|
||||||
|
|
||||||
assertEquals(entryId, result.id)
|
assertEquals(entryId, result.id)
|
||||||
assertEquals("Уже посмотрел, потрясающе!", result.comment)
|
assertEquals("Уже посмотрел, потрясающе!", result.comment)
|
||||||
assertTrue(result.isViewed)
|
assertTrue(result.isViewed)
|
||||||
|
|
||||||
val foundEntry = filmLibraryRepository.findById(entryId)
|
val foundEntry = filmLibraryEntryRepository.findById(entryId)
|
||||||
assertNotNull(foundEntry)
|
assertNotNull(foundEntry)
|
||||||
assertEquals("Уже посмотрел, потрясающе!", foundEntry?.comment)
|
assertEquals("Уже посмотрел, потрясающе!", foundEntry?.comment)
|
||||||
assertTrue(foundEntry?.isViewed ?: false)
|
assertTrue(foundEntry?.isViewed ?: false)
|
||||||
@@ -101,10 +101,10 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should find film library entry by id`() {
|
fun `should find film library entry by id`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Обязательно посмотреть", false)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Обязательно посмотреть", false)
|
||||||
filmLibraryRepository.save(entry)
|
filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||||
|
|
||||||
assertNotNull(foundEntry)
|
assertNotNull(foundEntry)
|
||||||
assertEquals(entry.id, foundEntry?.id)
|
assertEquals(entry.id, foundEntry?.id)
|
||||||
@@ -118,22 +118,22 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
fun `should return null when film library entry not found by id`() {
|
fun `should return null when film library entry not found by id`() {
|
||||||
val nonExistentId = UUID.randomUUID()
|
val nonExistentId = UUID.randomUUID()
|
||||||
|
|
||||||
val foundEntry = filmLibraryRepository.findById(nonExistentId)
|
val foundEntry = filmLibraryEntryRepository.findById(nonExistentId)
|
||||||
|
|
||||||
assertNull(foundEntry)
|
assertNull(foundEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should find all film library entries`() {
|
fun `should find all film library entries`() {
|
||||||
val entry1 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 1", false)
|
val entry1 = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 1", false)
|
||||||
val entry2 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 2", true)
|
val entry2 = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 2", true)
|
||||||
val entry3 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
val entry3 = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
||||||
|
|
||||||
filmLibraryRepository.save(entry1)
|
filmLibraryEntryRepository.save(entry1)
|
||||||
filmLibraryRepository.save(entry2)
|
filmLibraryEntryRepository.save(entry2)
|
||||||
filmLibraryRepository.save(entry3)
|
filmLibraryEntryRepository.save(entry3)
|
||||||
|
|
||||||
val allEntries = filmLibraryRepository.findAll()
|
val allEntries = filmLibraryEntryRepository.findAll()
|
||||||
|
|
||||||
assertEquals(3, allEntries.size)
|
assertEquals(3, allEntries.size)
|
||||||
assertTrue(allEntries.any { it.id == entry1.id })
|
assertTrue(allEntries.any { it.id == entry1.id })
|
||||||
@@ -143,19 +143,19 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should return empty list when no film library entries exist`() {
|
fun `should return empty list when no film library entries exist`() {
|
||||||
val allEntries = filmLibraryRepository.findAll()
|
val allEntries = filmLibraryEntryRepository.findAll()
|
||||||
|
|
||||||
assertTrue(allEntries.isEmpty())
|
assertTrue(allEntries.isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should delete film library entry by id`() {
|
fun `should delete film library entry by id`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Для удаления", false)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Для удаления", false)
|
||||||
filmLibraryRepository.save(entry)
|
filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
filmLibraryRepository.deleteById(entry.id)
|
filmLibraryEntryRepository.deleteById(entry.id)
|
||||||
|
|
||||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||||
assertNull(foundEntry)
|
assertNull(foundEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +163,14 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
fun `should not throw exception when deleting non-existent entry`() {
|
fun `should not throw exception when deleting non-existent entry`() {
|
||||||
val nonExistentId = UUID.randomUUID()
|
val nonExistentId = UUID.randomUUID()
|
||||||
|
|
||||||
filmLibraryRepository.deleteById(nonExistentId)
|
filmLibraryEntryRepository.deleteById(nonExistentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should save entry with null comment`() {
|
fun `should save entry with null comment`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
||||||
|
|
||||||
val savedEntry = filmLibraryRepository.save(entry)
|
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
assertNotNull(savedEntry)
|
assertNotNull(savedEntry)
|
||||||
assertNull(savedEntry.comment)
|
assertNull(savedEntry.comment)
|
||||||
@@ -178,9 +178,9 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should save entry with isViewed true`() {
|
fun `should save entry with isViewed true`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Посмотрел", true)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Посмотрел", true)
|
||||||
|
|
||||||
val savedEntry = filmLibraryRepository.save(entry)
|
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
assertNotNull(savedEntry)
|
assertNotNull(savedEntry)
|
||||||
assertTrue(savedEntry.isViewed)
|
assertTrue(savedEntry.isViewed)
|
||||||
@@ -188,9 +188,9 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should save entry with isViewed false`() {
|
fun `should save entry with isViewed false`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Еще не смотрел", false)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Еще не смотрел", false)
|
||||||
|
|
||||||
val savedEntry = filmLibraryRepository.save(entry)
|
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
assertNotNull(savedEntry)
|
assertNotNull(savedEntry)
|
||||||
assertFalse(savedEntry.isViewed)
|
assertFalse(savedEntry.isViewed)
|
||||||
@@ -198,23 +198,23 @@ class FilmLibraryRepositoryIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should cascade delete entries when user is deleted`() {
|
fun `should cascade delete entries when user is deleted`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Любимый фильм пользователя", false)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Любимый фильм пользователя", false)
|
||||||
filmLibraryRepository.save(entry)
|
filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
userRepository.deleteById(testUser.id)
|
userRepository.deleteById(testUser.id)
|
||||||
|
|
||||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||||
assertNull(foundEntry)
|
assertNull(foundEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should cascade delete entries when film is deleted`() {
|
fun `should cascade delete entries when film is deleted`() {
|
||||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Запись о фильме", false)
|
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Запись о фильме", false)
|
||||||
filmLibraryRepository.save(entry)
|
filmLibraryEntryRepository.save(entry)
|
||||||
|
|
||||||
filmRepository.deleteById(testFilm.id)
|
filmRepository.deleteById(testFilm.id)
|
||||||
|
|
||||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||||
assertNull(foundEntry)
|
assertNull(foundEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+45
-8
@@ -1,5 +1,6 @@
|
|||||||
package com.project.movienight.adapters.persistence.jdbc
|
package com.project.movienight.adapters.persistence.jdbc
|
||||||
|
|
||||||
|
import com.project.movienight.domain.model.AuthProvider
|
||||||
import com.project.movienight.domain.model.User
|
import com.project.movienight.domain.model.User
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@@ -46,7 +47,6 @@ class UserRepositoryIntegrationTest {
|
|||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
name = "John Doe",
|
name = "John Doe",
|
||||||
email = "john@example.com",
|
email = "john@example.com",
|
||||||
library = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val savedUser = userRepository.save(user)
|
val savedUser = userRepository.save(user)
|
||||||
@@ -61,10 +61,10 @@ class UserRepositoryIntegrationTest {
|
|||||||
fun `should update existing user`() {
|
fun `should update existing user`() {
|
||||||
// given
|
// given
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val originalUser = User(userId, "John Doe", "john@example.com", null)
|
val originalUser = User(userId, "John Doe", "john@example.com")
|
||||||
userRepository.save(originalUser)
|
userRepository.save(originalUser)
|
||||||
|
|
||||||
val updatedUser = User(userId, "Jane Doe", "jane@example.com", null)
|
val updatedUser = User(userId, "Jane Doe", "jane@example.com")
|
||||||
val result = userRepository.save(updatedUser)
|
val result = userRepository.save(updatedUser)
|
||||||
|
|
||||||
assertEquals(userId, result.id)
|
assertEquals(userId, result.id)
|
||||||
@@ -80,7 +80,7 @@ class UserRepositoryIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `should find user by id`() {
|
fun `should find user by id`() {
|
||||||
// given
|
// given
|
||||||
val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
|
val user = User(UUID.randomUUID(), "John Doe", "john@example.com")
|
||||||
userRepository.save(user)
|
userRepository.save(user)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -108,9 +108,9 @@ class UserRepositoryIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `should find all users`() {
|
fun `should find all users`() {
|
||||||
// given
|
// given
|
||||||
val user1 = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
|
val user1 = User(UUID.randomUUID(), "John Doe", "john@example.com")
|
||||||
val user2 = User(UUID.randomUUID(), "Jane Smith", "jane@example.com", null)
|
val user2 = User(UUID.randomUUID(), "Jane Smith", "jane@example.com")
|
||||||
val user3 = User(UUID.randomUUID(), "Bob Johnson", "bob@example.com", null)
|
val user3 = User(UUID.randomUUID(), "Bob Johnson", "bob@example.com")
|
||||||
|
|
||||||
userRepository.save(user1)
|
userRepository.save(user1)
|
||||||
userRepository.save(user2)
|
userRepository.save(user2)
|
||||||
@@ -138,7 +138,7 @@ class UserRepositoryIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `should delete user by id`() {
|
fun `should delete user by id`() {
|
||||||
// given
|
// given
|
||||||
val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
|
val user = User(UUID.randomUUID(), "John Doe", "john@example.com")
|
||||||
userRepository.save(user)
|
userRepository.save(user)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -157,4 +157,41 @@ class UserRepositoryIntegrationTest {
|
|||||||
// when & then (no exception should be thrown)
|
// when & then (no exception should be thrown)
|
||||||
userRepository.deleteById(nonExistentId)
|
userRepository.deleteById(nonExistentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should create OAuth user with provider identity`() {
|
||||||
|
val user = User(UUID.randomUUID(), "OAuth User", "oauth@example.com")
|
||||||
|
|
||||||
|
val savedUser = userRepository.createOAuthUser(user, AuthProvider.GOOGLE, "google-123")
|
||||||
|
|
||||||
|
assertEquals(user.id, savedUser.id)
|
||||||
|
assertEquals(user.email, savedUser.email)
|
||||||
|
|
||||||
|
val foundByProvider = userRepository.findByProviderAndProviderId(AuthProvider.GOOGLE, "google-123")
|
||||||
|
assertNotNull(foundByProvider)
|
||||||
|
assertEquals(user.id, foundByProvider?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should link OAuth account to existing user`() {
|
||||||
|
val user = userRepository.save(User(UUID.randomUUID(), "Link User", "link@example.com"))
|
||||||
|
|
||||||
|
val linkedUser = userRepository.linkOAuthAccount(user.id, AuthProvider.YANDEX, "yandex-456")
|
||||||
|
|
||||||
|
assertEquals(user.id, linkedUser.id)
|
||||||
|
val foundByProvider = userRepository.findByProviderAndProviderId(AuthProvider.YANDEX, "yandex-456")
|
||||||
|
assertNotNull(foundByProvider)
|
||||||
|
assertEquals(user.id, foundByProvider?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `find by email should include jellyfin user id`() {
|
||||||
|
val user = userRepository.save(User(UUID.randomUUID(), "Jellyfin User", "jellyfin@example.com"))
|
||||||
|
jdbcTemplate.update("UPDATE users SET jellyfin_user_id = ? WHERE id = ?", "jellyfin-789", user.id)
|
||||||
|
|
||||||
|
val foundUser = userRepository.findByEmail(user.email)
|
||||||
|
|
||||||
|
assertNotNull(foundUser)
|
||||||
|
assertEquals("jellyfin-789", foundUser?.jellyfinUserId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
package com.project.movienight.adapters.web
|
package com.project.movienight.adapters.web
|
||||||
|
|
||||||
import com.project.movienight.application.ports.input.CreateFilmUseCase
|
import com.project.movienight.application.ports.input.FilmUseCase
|
||||||
import com.project.movienight.application.ports.input.DeleteFilmUseCase
|
|
||||||
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.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@@ -19,20 +14,15 @@ import java.util.UUID
|
|||||||
|
|
||||||
class FilmControllerSearchTest {
|
class FilmControllerSearchTest {
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
private lateinit var searchFilmByTitleUseCase: SearchFilmByTitleUseCase
|
private lateinit var filmUseCase: FilmUseCase
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
searchFilmByTitleUseCase = mockk()
|
filmUseCase = mockk()
|
||||||
|
|
||||||
val controller =
|
val controller =
|
||||||
FilmController(
|
FilmController(
|
||||||
createFilmUseCase = mockk<CreateFilmUseCase>(),
|
filmUseCase = filmUseCase,
|
||||||
editFilmUseCase = mockk<EditFilmUseCase>(),
|
|
||||||
deleteFilmUseCase = mockk<DeleteFilmUseCase>(),
|
|
||||||
getFilmByIdUseCase = mockk<GetFilmByIdUseCase>(),
|
|
||||||
getAllFilmsUseCase = mockk<GetAllFilmsUseCase>(),
|
|
||||||
searchFilmByTitleUseCase = searchFilmByTitleUseCase,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
@@ -43,7 +33,7 @@ class FilmControllerSearchTest {
|
|||||||
val title = "Inception"
|
val title = "Inception"
|
||||||
val film = Film(id = UUID.randomUUID(), title = title, description = "A dream heist")
|
val film = Film(id = UUID.randomUUID(), title = title, description = "A dream heist")
|
||||||
|
|
||||||
every { searchFilmByTitleUseCase.searchByTitle(title) } returns film
|
every { filmUseCase.searchByTitle(title) } returns film
|
||||||
|
|
||||||
mockMvc
|
mockMvc
|
||||||
.get("/api/films/search") {
|
.get("/api/films/search") {
|
||||||
@@ -55,14 +45,14 @@ class FilmControllerSearchTest {
|
|||||||
jsonPath("$.description") { value("A dream heist") }
|
jsonPath("$.description") { value("A dream heist") }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(exactly = 1) { searchFilmByTitleUseCase.searchByTitle(title) }
|
verify(exactly = 1) { filmUseCase.searchByTitle(title) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `search returns 404 when title is not found`() {
|
fun `search returns 404 when title is not found`() {
|
||||||
val title = "Unknown Title"
|
val title = "Unknown Title"
|
||||||
|
|
||||||
every { searchFilmByTitleUseCase.searchByTitle(title) } returns null
|
every { filmUseCase.searchByTitle(title) } returns null
|
||||||
|
|
||||||
mockMvc
|
mockMvc
|
||||||
.get("/api/films/search") {
|
.get("/api/films/search") {
|
||||||
@@ -72,6 +62,6 @@ class FilmControllerSearchTest {
|
|||||||
content { string("") }
|
content { string("") }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(exactly = 1) { searchFilmByTitleUseCase.searchByTitle(title) }
|
verify(exactly = 1) { filmUseCase.searchByTitle(title) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-174
@@ -1,15 +1,15 @@
|
|||||||
package com.project.movienight.application.services
|
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.AddFilmToLibraryCommand
|
||||||
import com.project.movienight.application.ports.input.CreateFilmLibraryCommand
|
|
||||||
import com.project.movienight.application.ports.input.GetFilmLibraryQuery
|
|
||||||
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand
|
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand
|
||||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
|
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||||
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
import com.project.movienight.domain.exception.DomainException
|
import com.project.movienight.domain.exception.DomainException
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.FilmLibrary
|
import com.project.movienight.domain.model.Film
|
||||||
|
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.justRun
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@@ -22,83 +22,52 @@ import org.junit.jupiter.api.assertThrows
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class FilmLibraryServiceTest {
|
class FilmLibraryServiceTest {
|
||||||
private lateinit var filmLibraryRepository: FilmLibraryRepositoryPort
|
private lateinit var filmLibraryEntryRepository: FilmLibraryEntryRepositoryPort
|
||||||
|
private lateinit var filmRepository: FilmRepositoryPort
|
||||||
private lateinit var idGenerator: IdGenerator
|
private lateinit var idGenerator: IdGenerator
|
||||||
private lateinit var businessMetricsService: BusinessMetricsService
|
private lateinit var businessMetricsService: BusinessMetricsPort
|
||||||
private lateinit var filmLibraryService: FilmLibraryService
|
private lateinit var filmLibraryService: FilmLibraryService
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
filmLibraryRepository = mockk()
|
filmLibraryEntryRepository = mockk()
|
||||||
|
filmRepository = mockk()
|
||||||
idGenerator = mockk()
|
idGenerator = mockk()
|
||||||
businessMetricsService = mockk(relaxed = true)
|
businessMetricsService = mockk(relaxed = true)
|
||||||
filmLibraryService = FilmLibraryService(filmLibraryRepository, idGenerator, businessMetricsService)
|
filmLibraryService =
|
||||||
}
|
FilmLibraryService(
|
||||||
|
filmLibraryEntryRepository,
|
||||||
@Test
|
filmRepository,
|
||||||
fun `should throw EntityNotFoundException when creating library for user with no entries`() {
|
idGenerator,
|
||||||
val userId = UUID.randomUUID()
|
businessMetricsService,
|
||||||
val command = CreateFilmLibraryCommand(userId = userId, name = "My Films")
|
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
|
||||||
|
|
||||||
assertThrows<EntityNotFoundException> {
|
|
||||||
filmLibraryService.create(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
|
||||||
verify(exactly = 0) { idGenerator.generateId() }
|
|
||||||
verify(exactly = 0) { filmLibraryRepository.save(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should return existing library when user already has one`() {
|
|
||||||
val userId = UUID.randomUUID()
|
|
||||||
val existingLibrary =
|
|
||||||
FilmLibrary(
|
|
||||||
id = UUID.randomUUID(),
|
|
||||||
userId = userId,
|
|
||||||
filmId = UUID.randomUUID(),
|
|
||||||
comment = "Existing Library",
|
|
||||||
isViewed = false,
|
|
||||||
)
|
)
|
||||||
val command = CreateFilmLibraryCommand(userId = userId, name = "New Library")
|
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
|
||||||
|
|
||||||
val result = filmLibraryService.create(command)
|
|
||||||
|
|
||||||
assertEquals(existingLibrary, result)
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
|
||||||
verify(exactly = 0) { idGenerator.generateId() }
|
|
||||||
verify(exactly = 0) { filmLibraryRepository.save(any()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should add film to new library when user has no library`() {
|
fun `should add film as new library entry`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val filmId = UUID.randomUUID()
|
val filmId = UUID.randomUUID()
|
||||||
val libraryId = UUID.randomUUID()
|
val entryId = UUID.randomUUID()
|
||||||
val command = AddFilmToLibraryCommand(userId = userId, filmId = filmId)
|
val command = AddFilmToLibraryCommand(userId = userId, filmId = filmId)
|
||||||
val expectedLibrary =
|
val expectedEntry =
|
||||||
FilmLibrary(
|
FilmLibraryEntry(
|
||||||
id = libraryId,
|
id = entryId,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
comment = null,
|
comment = null,
|
||||||
isViewed = false,
|
isViewed = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
every { filmRepository.findById(filmId) } returns Film(filmId, "Film", "Description")
|
||||||
every { idGenerator.generateId() } returns libraryId
|
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns null
|
||||||
|
every { idGenerator.generateId() } returns entryId
|
||||||
every {
|
every {
|
||||||
filmLibraryRepository.save(
|
filmLibraryEntryRepository.save(
|
||||||
match {
|
match {
|
||||||
it.userId == userId && it.filmId == filmId && it.comment == null && it.isViewed == false
|
it.userId == userId && it.filmId == filmId && it.comment == null && it.isViewed == false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} returns expectedLibrary
|
} returns expectedEntry
|
||||||
|
|
||||||
val result = filmLibraryService.addFilm(command)
|
val result = filmLibraryService.addFilm(command)
|
||||||
|
|
||||||
@@ -106,62 +75,46 @@ class FilmLibraryServiceTest {
|
|||||||
assertEquals(filmId, result.filmId)
|
assertEquals(filmId, result.filmId)
|
||||||
assertEquals(userId, result.userId)
|
assertEquals(userId, result.userId)
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||||
verify(exactly = 1) { idGenerator.generateId() }
|
verify(exactly = 1) { idGenerator.generateId() }
|
||||||
verify(exactly = 1) { filmLibraryRepository.save(any()) }
|
verify(exactly = 1) { filmLibraryEntryRepository.save(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should add film as a new library entry when another film already exists`() {
|
fun `should reset viewed state when adding existing entry`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val oldFilmId = UUID.randomUUID()
|
val filmId = UUID.randomUUID()
|
||||||
val newFilmId = UUID.randomUUID()
|
val existingEntry =
|
||||||
val existingLibrary =
|
FilmLibraryEntry(
|
||||||
FilmLibrary(
|
|
||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = oldFilmId,
|
filmId = filmId,
|
||||||
comment = "My Library",
|
comment = "My Library",
|
||||||
isViewed = true,
|
isViewed = true,
|
||||||
)
|
)
|
||||||
val command = AddFilmToLibraryCommand(userId = userId, filmId = newFilmId)
|
val updatedEntry = existingEntry.copy(isViewed = false, watchedAt = null)
|
||||||
val createdLibrary =
|
|
||||||
FilmLibrary(
|
|
||||||
id = UUID.randomUUID(),
|
|
||||||
userId = userId,
|
|
||||||
filmId = newFilmId,
|
|
||||||
comment = null,
|
|
||||||
isViewed = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns existingEntry
|
||||||
every { idGenerator.generateId() } returns createdLibrary.id
|
every { filmRepository.findById(filmId) } returns Film(filmId, "Film", "Description")
|
||||||
every {
|
every { filmLibraryEntryRepository.save(updatedEntry) } returns updatedEntry
|
||||||
filmLibraryRepository.save(
|
|
||||||
match {
|
|
||||||
it.id == createdLibrary.id && it.userId == userId && it.filmId == newFilmId && it.isViewed == false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} returns createdLibrary
|
|
||||||
|
|
||||||
val result = filmLibraryService.addFilm(command)
|
val result = filmLibraryService.addFilm(AddFilmToLibraryCommand(userId = userId, filmId = filmId))
|
||||||
|
|
||||||
assertEquals(newFilmId, result.filmId)
|
assertEquals(updatedEntry, result)
|
||||||
assertEquals(false, result.isViewed)
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||||
verify(exactly = 1) { idGenerator.generateId() }
|
verify(exactly = 0) { idGenerator.generateId() }
|
||||||
verify(exactly = 1) { filmLibraryRepository.save(any()) }
|
verify(exactly = 1) { filmLibraryEntryRepository.save(updatedEntry) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should remove film from library successfully`() {
|
fun `should remove film from library successfully`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val filmId = UUID.randomUUID()
|
val filmId = UUID.randomUUID()
|
||||||
val libraryId = UUID.randomUUID()
|
val entryId = UUID.randomUUID()
|
||||||
val existingLibrary =
|
val existingEntry =
|
||||||
FilmLibrary(
|
FilmLibraryEntry(
|
||||||
id = libraryId,
|
id = entryId,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = filmId,
|
filmId = filmId,
|
||||||
comment = "My Library",
|
comment = "My Library",
|
||||||
@@ -169,122 +122,70 @@ class FilmLibraryServiceTest {
|
|||||||
)
|
)
|
||||||
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId)
|
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId)
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns existingEntry
|
||||||
justRun { filmLibraryRepository.deleteById(libraryId) }
|
justRun { filmLibraryEntryRepository.deleteById(entryId) }
|
||||||
|
|
||||||
val result = filmLibraryService.removeFilm(command)
|
val result = filmLibraryService.removeFilm(command)
|
||||||
|
|
||||||
assertEquals(existingLibrary, result)
|
assertEquals(existingEntry, result)
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||||
verify(exactly = 1) { filmLibraryRepository.deleteById(libraryId) }
|
verify(exactly = 1) { filmLibraryEntryRepository.deleteById(entryId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should throw EntityNotFoundException when removing film from non-existent library`() {
|
fun `should throw EntityNotFoundException when removing non-existent entry`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val filmId = UUID.randomUUID()
|
val filmId = UUID.randomUUID()
|
||||||
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId)
|
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId)
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns null
|
||||||
|
|
||||||
assertThrows<EntityNotFoundException> {
|
assertThrows<EntityNotFoundException> {
|
||||||
filmLibraryService.removeFilm(command)
|
filmLibraryService.removeFilm(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||||
verify(exactly = 0) { filmLibraryRepository.deleteById(any()) }
|
verify(exactly = 0) { filmLibraryEntryRepository.deleteById(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should throw DomainException when removing film that is not in library`() {
|
fun `should throw DomainException when entry id belongs to another film`() {
|
||||||
val userId = UUID.randomUUID()
|
|
||||||
val libraryFilmId = UUID.randomUUID()
|
|
||||||
val differentFilmId = UUID.randomUUID()
|
|
||||||
val existingLibrary =
|
|
||||||
FilmLibrary(
|
|
||||||
id = UUID.randomUUID(),
|
|
||||||
userId = userId,
|
|
||||||
filmId = libraryFilmId,
|
|
||||||
comment = "My Library",
|
|
||||||
isViewed = false,
|
|
||||||
)
|
|
||||||
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = differentFilmId)
|
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
|
||||||
|
|
||||||
assertThrows<DomainException> {
|
|
||||||
filmLibraryService.removeFilm(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
|
||||||
verify(exactly = 0) { filmLibraryRepository.deleteById(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should throw EntityNotFoundException when libraryId does not match`() {
|
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val filmId = UUID.randomUUID()
|
val filmId = UUID.randomUUID()
|
||||||
val actualLibraryId = UUID.randomUUID()
|
val entryId = UUID.randomUUID()
|
||||||
val wrongLibraryId = UUID.randomUUID()
|
val existingEntry =
|
||||||
val existingLibrary =
|
FilmLibraryEntry(
|
||||||
FilmLibrary(
|
id = entryId,
|
||||||
id = actualLibraryId,
|
|
||||||
userId = userId,
|
|
||||||
filmId = filmId,
|
|
||||||
comment = "My Library",
|
|
||||||
isViewed = false,
|
|
||||||
)
|
|
||||||
val command =
|
|
||||||
RemoveFilmFromLibraryCommand(
|
|
||||||
userId = userId,
|
|
||||||
filmId = filmId,
|
|
||||||
libraryId = wrongLibraryId,
|
|
||||||
)
|
|
||||||
|
|
||||||
every { filmLibraryRepository.findById(wrongLibraryId) } returns null
|
|
||||||
|
|
||||||
assertThrows<EntityNotFoundException> {
|
|
||||||
filmLibraryService.removeFilm(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findById(wrongLibraryId) }
|
|
||||||
verify(exactly = 0) { filmLibraryRepository.deleteById(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should get library successfully`() {
|
|
||||||
val userId = UUID.randomUUID()
|
|
||||||
val existingLibrary =
|
|
||||||
FilmLibrary(
|
|
||||||
id = UUID.randomUUID(),
|
|
||||||
userId = userId,
|
userId = userId,
|
||||||
filmId = UUID.randomUUID(),
|
filmId = UUID.randomUUID(),
|
||||||
comment = "My Library",
|
comment = "My Library",
|
||||||
isViewed = false,
|
isViewed = false,
|
||||||
)
|
)
|
||||||
val query = GetFilmLibraryQuery(userId = userId)
|
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId, entryId = entryId)
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
every { filmLibraryEntryRepository.findById(entryId) } returns existingEntry
|
||||||
|
|
||||||
val result = filmLibraryService.getLibrary(query)
|
assertThrows<DomainException> {
|
||||||
|
filmLibraryService.removeFilm(command)
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(existingLibrary, result)
|
verify(exactly = 1) { filmLibraryEntryRepository.findById(entryId) }
|
||||||
|
verify(exactly = 0) { filmLibraryEntryRepository.deleteById(any()) }
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should throw EntityNotFoundException when getting non-existent library`() {
|
fun `should list entries by user`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val query = GetFilmLibraryQuery(userId = userId)
|
val entries =
|
||||||
|
listOf(
|
||||||
|
FilmLibraryEntry(UUID.randomUUID(), userId, UUID.randomUUID(), null, false),
|
||||||
|
)
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
every { filmLibraryEntryRepository.findByUserId(userId) } returns entries
|
||||||
|
|
||||||
assertThrows<EntityNotFoundException> {
|
assertEquals(entries, filmLibraryService.list(userId))
|
||||||
filmLibraryService.getLibrary(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
verify(exactly = 1) { filmLibraryEntryRepository.findByUserId(userId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package com.project.movienight.application.services
|
|||||||
|
|
||||||
import com.project.movienight.application.ports.input.CreateFilmCommand
|
import com.project.movienight.application.ports.input.CreateFilmCommand
|
||||||
import com.project.movienight.application.ports.input.EditFilmCommand
|
import com.project.movienight.application.ports.input.EditFilmCommand
|
||||||
|
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||||
import com.project.movienight.application.ports.output.IdGenerator
|
import com.project.movienight.application.ports.output.IdGenerator
|
||||||
import com.project.movienight.config.FilmServiceProperties
|
import com.project.movienight.config.FilmServiceProperties
|
||||||
import com.project.movienight.domain.exception.BlockedValueException
|
import com.project.movienight.domain.exception.BlockedValueException
|
||||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||||
import com.project.movienight.domain.model.Film
|
import com.project.movienight.domain.model.Film
|
||||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.justRun
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@@ -24,7 +24,7 @@ class FilmServiceTest {
|
|||||||
private lateinit var filmRepository: FilmRepositoryPort
|
private lateinit var filmRepository: FilmRepositoryPort
|
||||||
private lateinit var idGenerator: IdGenerator
|
private lateinit var idGenerator: IdGenerator
|
||||||
private lateinit var filmConfig: FilmServiceProperties
|
private lateinit var filmConfig: FilmServiceProperties
|
||||||
private lateinit var meterRegistry: SimpleMeterRegistry
|
private lateinit var businessMetricsService: BusinessMetricsPort
|
||||||
private lateinit var filmService: FilmService
|
private lateinit var filmService: FilmService
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -32,8 +32,8 @@ class FilmServiceTest {
|
|||||||
filmRepository = mockk()
|
filmRepository = mockk()
|
||||||
idGenerator = mockk()
|
idGenerator = mockk()
|
||||||
filmConfig = mockk()
|
filmConfig = mockk()
|
||||||
meterRegistry = SimpleMeterRegistry()
|
businessMetricsService = mockk(relaxed = true)
|
||||||
filmService = FilmService(filmRepository, idGenerator, filmConfig, meterRegistry)
|
filmService = FilmService(filmRepository, idGenerator, filmConfig, businessMetricsService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class UserServiceTest {
|
|||||||
fun `should create user successfully`() {
|
fun `should create user successfully`() {
|
||||||
val command = CreateUserCommand(name = "John Doe", email = "john@example.com")
|
val command = CreateUserCommand(name = "John Doe", email = "john@example.com")
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val expectedUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null)
|
val expectedUser = User(id = userId, name = "John Doe", email = "john@example.com")
|
||||||
|
|
||||||
every { userConfig.isBlocked("John Doe") } returns false
|
every { userConfig.isBlocked("John Doe") } returns false
|
||||||
every { idGenerator.generateId() } returns userId
|
every { idGenerator.generateId() } returns userId
|
||||||
@@ -74,8 +74,8 @@ class UserServiceTest {
|
|||||||
fun `should edit user successfully`() {
|
fun `should edit user successfully`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val command = EditUserCommand(name = "Jane Doe")
|
val command = EditUserCommand(name = "Jane Doe")
|
||||||
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null)
|
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com")
|
||||||
val updatedUser = User(id = userId, name = "Jane Doe", email = "john@example.com", library = null)
|
val updatedUser = User(id = userId, name = "Jane Doe", email = "john@example.com")
|
||||||
|
|
||||||
every { userConfig.isBlocked("Jane Doe") } returns false
|
every { userConfig.isBlocked("Jane Doe") } returns false
|
||||||
every { userRepository.findById(userId) } returns existingUser
|
every { userRepository.findById(userId) } returns existingUser
|
||||||
@@ -128,7 +128,7 @@ class UserServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `should delete user successfully`() {
|
fun `should delete user successfully`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null)
|
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com")
|
||||||
|
|
||||||
every { userRepository.findById(userId) } returns existingUser
|
every { userRepository.findById(userId) } returns existingUser
|
||||||
justRun { userRepository.deleteById(userId) }
|
justRun { userRepository.deleteById(userId) }
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.project.movienight.config
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Primary
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
@EnableWebSecurity
|
||||||
|
class TestSecurityConfiguration {
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
fun testSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests { auth ->
|
||||||
|
auth.anyRequest().permitAll()
|
||||||
|
}.csrf { csrf ->
|
||||||
|
csrf.disable()
|
||||||
|
}.headers { headers ->
|
||||||
|
headers.frameOptions { frameOptions ->
|
||||||
|
frameOptions.sameOrigin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user