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
@@ -2,14 +2,21 @@ package com.project.movienight.adapters.web
import com.project.movienight.adapters.web.dto.request.CreateFilmLibraryRequest
import com.project.movienight.adapters.web.dto.response.FilmLibraryResponse
import com.project.movienight.adapters.web.dto.response.FilmResponse
import com.project.movienight.application.ports.input.AddFilmToLibraryCommand
import com.project.movienight.application.ports.input.AddFilmToLibraryUseCase
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.MarkFilmViewedUseCase
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.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
@@ -26,8 +33,11 @@ import java.util.UUID
class FilmLibraryController(
private val createFilmLibraryUseCase: CreateFilmLibraryUseCase,
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)
@@ -54,6 +64,11 @@ class FilmLibraryController(
),
)
@GetMapping("/entries")
fun list(
@PathVariable userId: UUID,
): List<FilmLibraryResponse> = listFilmLibraryEntriesUseCase.list(userId).map { FilmLibraryResponse.fromDomain(it) }
@PostMapping("/films/{filmId}")
@ResponseStatus(HttpStatus.CREATED)
fun addFilm(
@@ -69,6 +84,20 @@ class FilmLibraryController(
),
)
@PostMapping("/films/{filmId}/viewed")
fun markViewed(
@PathVariable userId: UUID,
@PathVariable filmId: UUID,
): FilmLibraryResponse =
FilmLibraryResponse.fromDomain(
markFilmViewedUseCase.markViewed(
MarkFilmViewedCommand(
userId = userId,
filmId = filmId,
),
),
)
@DeleteMapping("/films/{filmId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun removeFilm(
@@ -82,4 +111,31 @@ class FilmLibraryController(
),
)
}
@GetMapping("/available-films")
fun getAvailableFilms(
@PathVariable userId: UUID,
): List<FilmResponse> {
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) }
}
}