Implement user onboarding and enhanced .strm file creation

- Added automated onboarding dialog for new users to pick genres, eras, and content types.
- Enhanced "Add Movie" functionality to support folder-per-movie structure with Year and IMDb ID.
- Improved ui.js with custom dialogs for Onboarding, Rating, and Adding Movies.
- Fixed API accessibility by using standard [Authorize] attributes.
- Added "Mark Viewed" and "Sync" actions to the UI.

Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot]
2026-05-22 10:45:43 +00:00
co-authored by devitq
parent 4dc806263a
commit b59514a6eb
95 changed files with 4625 additions and 98 deletions
@@ -0,0 +1,81 @@
package com.project.movienight.application.services
import com.fasterxml.jackson.databind.ObjectMapper
import com.project.movienight.adapters.metrics.BusinessMetricsService
import com.project.movienight.adapters.persistence.jdbc.JellyfinEventRepository
import com.project.movienight.application.ports.input.MarkFilmViewedCommand
import com.project.movienight.application.ports.input.MarkFilmViewedUseCase
import com.project.movienight.application.ports.output.FilmRepositoryPort
import com.project.movienight.application.ports.output.UserRepositoryPort
import org.springframework.stereotype.Service
import java.time.OffsetDateTime
@Service
class JellyfinEventService(
private val jellyfinEventRepository: JellyfinEventRepository,
private val userRepository: UserRepositoryPort,
private val filmRepository: FilmRepositoryPort,
private val markFilmViewedUseCase: MarkFilmViewedUseCase,
private val objectMapper: ObjectMapper,
private val businessMetricsService: BusinessMetricsService,
) {
private val playbackEventTypes = setOf("playback.ended", "playback.stopped", "playback.completed")
fun handleEvent(
eventId: String,
serverId: String?,
eventType: String,
occurredAt: OffsetDateTime,
jellyfinUserId: String,
itemId: String,
payload: Map<String, Any>?,
) {
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
}
try {
if (playbackEventTypes.contains(eventType)) {
val localUser = userRepository.findAll().firstOrNull { it.jellyfinUserId == jellyfinUserId }
if (localUser == null) {
jellyfinEventRepository.delete(eventId)
businessMetricsService.recordJellyfinUnmappedUser()
return
}
val film = filmRepository.findByJellyfinItemId(itemId)
if (film == null) {
jellyfinEventRepository.delete(eventId)
businessMetricsService.recordBackendWriteFailure()
return
}
markFilmViewedUseCase.markViewed(
MarkFilmViewedCommand(
userId = localUser.id,
filmId = film.id,
watchedAt = occurredAt.toLocalDateTime(),
),
)
businessMetricsService.recordLibraryEvent()
}
} catch (
@Suppress("TooGenericExceptionCaught") ex: RuntimeException,
) {
jellyfinEventRepository.delete(eventId)
businessMetricsService.recordBackendWriteFailure()
throw ex
}
}
}