fix: address PR review thread issues
Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/d4d6ebbb-2508-484e-accf-c891be54750f Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
co-authored by
devitq
parent
1f22be3401
commit
50923e2e5a
@@ -100,7 +100,7 @@ class JellyfinApiClient(
|
|||||||
throw IllegalStateException("Failed to call Jellyfin at $uri", exception)
|
throw IllegalStateException("Failed to call Jellyfin at $uri", exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
check(response.statusCode() !in 200..299) {
|
check(response.statusCode() in 200..299) {
|
||||||
"Jellyfin request failed with status ${response.statusCode()} for $uri"
|
"Jellyfin request failed with status ${response.statusCode()} for $uri"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -8,12 +8,6 @@ import org.springframework.stereotype.Repository
|
|||||||
class JellyfinEventRepository(
|
class JellyfinEventRepository(
|
||||||
private val jdbc: NamedParameterJdbcTemplate,
|
private val jdbc: NamedParameterJdbcTemplate,
|
||||||
) {
|
) {
|
||||||
fun exists(eventId: String): Boolean {
|
|
||||||
val sql = "SELECT 1 FROM jellyfin_events WHERE event_id = :eventId"
|
|
||||||
val params = MapSqlParameterSource().addValue("eventId", eventId)
|
|
||||||
return jdbc.query(sql, params) { rs, _ -> rs.getInt(1) }.any()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(
|
fun save(
|
||||||
eventId: String,
|
eventId: String,
|
||||||
serverId: String?,
|
serverId: String?,
|
||||||
@@ -22,7 +16,7 @@ class JellyfinEventRepository(
|
|||||||
jellyfinUserId: String?,
|
jellyfinUserId: String?,
|
||||||
jellyfinItemId: String?,
|
jellyfinItemId: String?,
|
||||||
payload: 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)
|
||||||
@@ -40,6 +34,12 @@ class JellyfinEventRepository(
|
|||||||
.addValue("jellyfinItemId", jellyfinItemId)
|
.addValue("jellyfinItemId", jellyfinItemId)
|
||||||
.addValue("payload", payload)
|
.addValue("payload", payload)
|
||||||
|
|
||||||
|
return jdbc.update(sql, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(eventId: String) {
|
||||||
|
val sql = "DELETE FROM jellyfin_events WHERE event_id = :eventId"
|
||||||
|
val params = MapSqlParameterSource().addValue("eventId", eventId)
|
||||||
jdbc.update(sql, params)
|
jdbc.update(sql, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class JellyfinEventsController(
|
|||||||
@RequestHeader(value = "X-MovieNight-Plugin-Token", required = false) token: String?,
|
@RequestHeader(value = "X-MovieNight-Plugin-Token", required = false) token: String?,
|
||||||
@RequestBody request: JellyfinEventRequest,
|
@RequestBody request: JellyfinEventRequest,
|
||||||
) {
|
) {
|
||||||
|
if (!properties.enabled) {
|
||||||
|
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Jellyfin integration is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
if (properties.pluginToken.isNotBlank()) {
|
if (properties.pluginToken.isNotBlank()) {
|
||||||
if (token == null || token != properties.pluginToken) {
|
if (token == null || token != properties.pluginToken) {
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid plugin token")
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid plugin token")
|
||||||
|
|||||||
@@ -33,21 +33,7 @@ class FilmLibraryService(
|
|||||||
ListFilmLibraryEntriesUseCase {
|
ListFilmLibraryEntriesUseCase {
|
||||||
override fun create(command: CreateFilmLibraryCommand): FilmLibrary {
|
override fun create(command: CreateFilmLibraryCommand): FilmLibrary {
|
||||||
findByUserId(command.userId)?.let { return it }
|
findByUserId(command.userId)?.let { return it }
|
||||||
|
throw EntityNotFoundException(entity = "Film library", id = command.userId.toString())
|
||||||
val libraryId = idGenerator.generateId()
|
|
||||||
val saved =
|
|
||||||
filmLibraryRepository.save(
|
|
||||||
FilmLibrary(
|
|
||||||
id = libraryId,
|
|
||||||
userId = command.userId,
|
|
||||||
filmId = libraryId,
|
|
||||||
comment = command.name,
|
|
||||||
isViewed = false,
|
|
||||||
watchedAt = null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
businessMetricsService.recordLibraryEvent()
|
|
||||||
return saved
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addFilm(command: AddFilmToLibraryCommand): FilmLibrary {
|
override fun addFilm(command: AddFilmToLibraryCommand): FilmLibrary {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class FilmService(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Create film request received: title='{}', descriptionLength={}'",
|
"Create film request received: title='{}', descriptionLength={}",
|
||||||
command.title,
|
command.title,
|
||||||
command.description.length,
|
command.description.length,
|
||||||
)
|
)
|
||||||
|
|||||||
+15
-4
@@ -30,23 +30,33 @@ class JellyfinEventService(
|
|||||||
itemId: String,
|
itemId: String,
|
||||||
payload: Map<String, Any>?,
|
payload: Map<String, Any>?,
|
||||||
) {
|
) {
|
||||||
if (jellyfinEventRepository.exists(eventId = eventId)) {
|
val payloadJson = payload?.let { objectMapper.writeValueAsString(it) }
|
||||||
|
val inserted =
|
||||||
|
jellyfinEventRepository.save(
|
||||||
|
eventId = eventId,
|
||||||
|
serverId = serverId,
|
||||||
|
eventType = eventType,
|
||||||
|
occurredAt = occurredAt,
|
||||||
|
jellyfinUserId = jellyfinUserId,
|
||||||
|
jellyfinItemId = itemId,
|
||||||
|
payload = payloadJson,
|
||||||
|
)
|
||||||
|
if (inserted != 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val payloadJson = payload?.let { objectMapper.writeValueAsString(it) }
|
|
||||||
jellyfinEventRepository.save(eventId, serverId, eventType, occurredAt, jellyfinUserId, itemId, payloadJson)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (playbackEventTypes.contains(eventType)) {
|
if (playbackEventTypes.contains(eventType)) {
|
||||||
val localUser = userRepository.findAll().firstOrNull { it.jellyfinUserId == jellyfinUserId }
|
val localUser = userRepository.findAll().firstOrNull { it.jellyfinUserId == 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(itemId)
|
||||||
if (film == null) {
|
if (film == null) {
|
||||||
|
jellyfinEventRepository.delete(eventId)
|
||||||
businessMetricsService.recordBackendWriteFailure()
|
businessMetricsService.recordBackendWriteFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -63,6 +73,7 @@ class JellyfinEventService(
|
|||||||
} catch (
|
} catch (
|
||||||
@Suppress("TooGenericExceptionCaught") ex: RuntimeException,
|
@Suppress("TooGenericExceptionCaught") ex: RuntimeException,
|
||||||
) {
|
) {
|
||||||
|
jellyfinEventRepository.delete(eventId)
|
||||||
businessMetricsService.recordBackendWriteFailure()
|
businessMetricsService.recordBackendWriteFailure()
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,13 @@ CREATE TABLE IF NOT EXISTS public.users (
|
|||||||
password VARCHAR(255),
|
password VARCHAR(255),
|
||||||
provider VARCHAR(64),
|
provider VARCHAR(64),
|
||||||
provider_id VARCHAR(255),
|
provider_id VARCHAR(255),
|
||||||
jellyfin_user_id VARCHAR(255),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.films (
|
CREATE TABLE IF NOT EXISTS public.films (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
title VARCHAR(255) NOT NULL,
|
title VARCHAR(255) NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL
|
||||||
content_type VARCHAR(32) NOT NULL DEFAULT 'FILM',
|
|
||||||
release_year INT,
|
|
||||||
genres TEXT NOT NULL DEFAULT '',
|
|
||||||
cast_members TEXT NOT NULL DEFAULT '',
|
|
||||||
directors TEXT NOT NULL DEFAULT '',
|
|
||||||
imdb_rating DOUBLE PRECISION,
|
|
||||||
platform_rating DOUBLE PRECISION,
|
|
||||||
external_url TEXT,
|
|
||||||
jellyfin_item_id VARCHAR(255),
|
|
||||||
jellyfin_library_id VARCHAR(255)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.favorites (
|
CREATE TABLE IF NOT EXISTS public.favorites (
|
||||||
@@ -31,42 +20,6 @@ CREATE TABLE IF NOT EXISTS public.favorites (
|
|||||||
film_id UUID NOT NULL,
|
film_id UUID NOT NULL,
|
||||||
comment VARCHAR(1024),
|
comment VARCHAR(1024),
|
||||||
is_viewed BOOLEAN NOT NULL DEFAULT FALSE,
|
is_viewed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
watched_at TIMESTAMP,
|
|
||||||
CONSTRAINT favorites_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE,
|
CONSTRAINT favorites_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
CONSTRAINT favorites_film_fk FOREIGN KEY (film_id) REFERENCES public.films(id) ON DELETE CASCADE
|
CONSTRAINT favorites_film_fk FOREIGN KEY (film_id) REFERENCES public.films(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.user_preferences (
|
|
||||||
user_id UUID PRIMARY KEY,
|
|
||||||
weighted_genres TEXT NOT NULL DEFAULT '',
|
|
||||||
plot_types TEXT NOT NULL DEFAULT '',
|
|
||||||
eras TEXT NOT NULL DEFAULT '',
|
|
||||||
cast_and_directors TEXT NOT NULL DEFAULT '',
|
|
||||||
moods TEXT NOT NULL DEFAULT '',
|
|
||||||
content_types TEXT NOT NULL DEFAULT '',
|
|
||||||
CONSTRAINT user_preferences_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.film_ratings (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
user_id UUID NOT NULL,
|
|
||||||
film_id UUID NOT NULL,
|
|
||||||
score INT NOT NULL,
|
|
||||||
note VARCHAR(2048),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT film_ratings_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE,
|
|
||||||
CONSTRAINT film_ratings_film_fk FOREIGN KEY (film_id) REFERENCES public.films(id) ON DELETE CASCADE,
|
|
||||||
CONSTRAINT film_ratings_score_range CHECK (score >= 1 AND score <= 10),
|
|
||||||
CONSTRAINT film_ratings_user_film_unique UNIQUE (user_id, film_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.jellyfin_sync_state (
|
|
||||||
user_id UUID PRIMARY KEY,
|
|
||||||
last_synced_at TIMESTAMP,
|
|
||||||
last_successful_sync_at TIMESTAMP,
|
|
||||||
last_error TEXT,
|
|
||||||
synced_item_count INT NOT NULL DEFAULT 0,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT jellyfin_sync_state_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
ALTER TABLE public.users
|
||||||
|
ADD COLUMN IF NOT EXISTS jellyfin_user_id VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS content_type VARCHAR(32) NOT NULL DEFAULT 'FILM';
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS release_year INT;
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS genres TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS cast_members TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS directors TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS imdb_rating DOUBLE PRECISION;
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS platform_rating DOUBLE PRECISION;
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS external_url TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS jellyfin_item_id VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE public.films
|
||||||
|
ADD COLUMN IF NOT EXISTS jellyfin_library_id VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE public.favorites
|
||||||
|
ADD COLUMN IF NOT EXISTS watched_at TIMESTAMP;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.user_preferences (
|
||||||
|
user_id UUID PRIMARY KEY,
|
||||||
|
weighted_genres TEXT NOT NULL DEFAULT '',
|
||||||
|
plot_types TEXT NOT NULL DEFAULT '',
|
||||||
|
eras TEXT NOT NULL DEFAULT '',
|
||||||
|
cast_and_directors TEXT NOT NULL DEFAULT '',
|
||||||
|
moods TEXT NOT NULL DEFAULT '',
|
||||||
|
content_types TEXT NOT NULL DEFAULT '',
|
||||||
|
CONSTRAINT user_preferences_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.film_ratings (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
film_id UUID NOT NULL,
|
||||||
|
score INT NOT NULL,
|
||||||
|
note VARCHAR(2048),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT film_ratings_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT film_ratings_film_fk FOREIGN KEY (film_id) REFERENCES public.films(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT film_ratings_score_range CHECK (score >= 1 AND score <= 10),
|
||||||
|
CONSTRAINT film_ratings_user_film_unique UNIQUE (user_id, film_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.jellyfin_sync_state (
|
||||||
|
user_id UUID PRIMARY KEY,
|
||||||
|
last_synced_at TIMESTAMP,
|
||||||
|
last_successful_sync_at TIMESTAMP,
|
||||||
|
last_error TEXT,
|
||||||
|
synced_item_count INT NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT jellyfin_sync_state_user_fk FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
+6
-27
@@ -36,40 +36,19 @@ class FilmLibraryServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should create new film library when user has no library`() {
|
fun `should throw EntityNotFoundException when creating library for user with no entries`() {
|
||||||
val userId = UUID.randomUUID()
|
val userId = UUID.randomUUID()
|
||||||
val libraryId = UUID.randomUUID()
|
|
||||||
val command = CreateFilmLibraryCommand(userId = userId, name = "My Films")
|
val command = CreateFilmLibraryCommand(userId = userId, name = "My Films")
|
||||||
val expectedLibrary =
|
|
||||||
FilmLibrary(
|
|
||||||
id = libraryId,
|
|
||||||
userId = userId,
|
|
||||||
filmId = libraryId,
|
|
||||||
comment = "My Films",
|
|
||||||
isViewed = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
every { filmLibraryRepository.findAll() } returns emptyList()
|
||||||
every { idGenerator.generateId() } returns libraryId
|
|
||||||
every {
|
|
||||||
filmLibraryRepository.save(
|
|
||||||
match {
|
|
||||||
it.userId == userId && it.comment == "My Films" && it.isViewed == false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} returns expectedLibrary
|
|
||||||
|
|
||||||
val result = filmLibraryService.create(command)
|
assertThrows<EntityNotFoundException> {
|
||||||
|
filmLibraryService.create(command)
|
||||||
assertNotNull(result)
|
}
|
||||||
assertEquals(libraryId, result.id)
|
|
||||||
assertEquals(userId, result.userId)
|
|
||||||
assertEquals(libraryId, result.filmId)
|
|
||||||
assertEquals("My Films", result.comment)
|
|
||||||
|
|
||||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||||
verify(exactly = 1) { idGenerator.generateId() }
|
verify(exactly = 0) { idGenerator.generateId() }
|
||||||
verify(exactly = 1) { filmLibraryRepository.save(any()) }
|
verify(exactly = 0) { filmLibraryRepository.save(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user