Hotfix/merge conflicts #60
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import random
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DATASET_URL = "https://raw.githubusercontent.com/sidooms/MovieTweetings/master/latest/movies.dat"
|
||||
DEFAULT_OUTPUT_DIR = "./Jellyfin_Movies"
|
||||
DEFAULT_COUNT = 1000
|
||||
|
||||
def download_dataset(url):
|
||||
logger.info(f"Downloading dataset from {url}...")
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
data = response.read().decode('utf-8')
|
||||
logger.info("Dataset downloaded successfully.")
|
||||
return data.splitlines()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download dataset: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def parse_movies(lines):
|
||||
"""
|
||||
Parse the movies.dat file.
|
||||
Format: IMDbID::Title (Year)::Genres
|
||||
Example: 0000008::Edison Kinetoscopic Record of a Sneeze (1894)::Documentary|Short
|
||||
"""
|
||||
movies = []
|
||||
# Regex to extract Title and Year from "Title (Year)"
|
||||
title_year_pattern = re.compile(r'(.*)\s+\((\d{4})\)$')
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split('::')
|
||||
if len(parts) >= 2:
|
||||
imdb_id_raw = parts[0]
|
||||
title_year_raw = parts[1]
|
||||
|
||||
# Format IMDb ID to ttXXXXXXX
|
||||
if imdb_id_raw.isdigit():
|
||||
imdb_id = f"tt{imdb_id_raw.zfill(7)}"
|
||||
else:
|
||||
continue
|
||||
|
||||
match = title_year_pattern.match(title_year_raw)
|
||||
if match:
|
||||
title = match.group(1).strip()
|
||||
year = match.group(2)
|
||||
|
||||
# Clean title for filesystem (remove invalid characters)
|
||||
safe_title = re.sub(r'[\\/*?:"<>|]', "", title)
|
||||
safe_title = safe_title.strip()
|
||||
|
||||
if safe_title:
|
||||
movies.append({
|
||||
'imdb_id': imdb_id,
|
||||
'title': safe_title,
|
||||
'year': year
|
||||
})
|
||||
|
||||
logger.info(f"Parsed {len(movies)} valid movies from dataset.")
|
||||
return movies
|
||||
|
||||
def create_dummy_video(filepath):
|
||||
"""Create a minimal valid dummy video file (mp4)."""
|
||||
try:
|
||||
mp4_header = b"\x00\x00\x00\x18ftypmp42\x00\x00\x00\x00mp42isom\x00\x00\x00\x00moov\x00\x00\x00\x08mvhd"
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(mp4_header)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create dummy video {filepath}: {e}")
|
||||
return False
|
||||
|
||||
def generate_library(movies, output_dir, count):
|
||||
"""Generate the folder structure and dummy files."""
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
logger.info(f"Created output directory: {output_dir}")
|
||||
|
||||
generated_imdb_ids = set()
|
||||
created_count = 0
|
||||
skipped_count = 0
|
||||
failed_count = 0
|
||||
|
||||
logger.info(f"Starting generation of up to {count} movies...")
|
||||
|
||||
# Shuffle to get diverse movies
|
||||
random.shuffle(movies)
|
||||
|
||||
for movie in movies:
|
||||
if created_count >= count:
|
||||
break
|
||||
|
||||
if movie['imdb_id'] in generated_imdb_ids:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Jellyfin naming convention: Movie Name (year) [imdbid-tt1234567]
|
||||
folder_name = f"{movie['title']} ({movie['year']}) [imdbid-{movie['imdb_id']}]"
|
||||
folder_path = os.path.join(output_dir, folder_name)
|
||||
|
||||
file_name = f"{folder_name}.mp4"
|
||||
file_path = os.path.join(folder_path, file_name)
|
||||
|
||||
if os.path.exists(file_path):
|
||||
skipped_count += 1
|
||||
generated_imdb_ids.add(movie['imdb_id'])
|
||||
continue
|
||||
|
||||
try:
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
if create_dummy_video(file_path):
|
||||
created_count += 1
|
||||
generated_imdb_ids.add(movie['imdb_id'])
|
||||
else:
|
||||
failed_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {folder_name}: {e}")
|
||||
failed_count += 1
|
||||
|
||||
logger.info("--- Generation Summary ---")
|
||||
logger.info(f"Target count: {count}")
|
||||
logger.info(f"Successfully created: {created_count}")
|
||||
logger.info(f"Skipped (already exists or duplicate): {skipped_count}")
|
||||
logger.info(f"Failed: {failed_count}")
|
||||
|
||||
return created_count > 0
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate a dummy Jellyfin movie library.")
|
||||
parser.add_argument("--output-dir", type=str, default=DEFAULT_OUTPUT_DIR,
|
||||
help=f"Directory to create the library in (default: {DEFAULT_OUTPUT_DIR})")
|
||||
parser.add_argument("--count", type=int, default=DEFAULT_COUNT,
|
||||
help=f"Number of movies to generate (default: {DEFAULT_COUNT})")
|
||||
parser.add_argument("--dataset-url", type=str, default=DEFAULT_DATASET_URL,
|
||||
help="URL to the movies.dat file")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
lines = download_dataset(args.dataset_url)
|
||||
if not lines:
|
||||
logger.error("No dataset lines to process.")
|
||||
sys.exit(1)
|
||||
|
||||
movies = parse_movies(lines)
|
||||
|
||||
if not movies:
|
||||
logger.error("No movies parsed from the dataset.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(movies) < args.count:
|
||||
logger.warning(f"Requested {args.count} movies, but only {len(movies)} available.")
|
||||
args.count = len(movies)
|
||||
|
||||
success = generate_library(movies, args.output_dir, args.count)
|
||||
|
||||
if success:
|
||||
logger.info(f"Library generation complete. You can now mount '{os.path.abspath(args.output_dir)}' into Jellyfin.")
|
||||
else:
|
||||
logger.error("Library generation failed.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,6 +2,9 @@ package com.project.movienight.adapters.jellyfin
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.project.movienight.application.ports.output.JellyfinLibraryItemSnapshot
|
||||
import com.project.movienight.application.ports.output.JellyfinRemoteUser
|
||||
import com.project.movienight.application.ports.output.JellyfinSyncSourcePort
|
||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -11,39 +14,18 @@ import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
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
|
||||
class JellyfinApiClient(
|
||||
private val properties: JellyfinIntegrationProperties,
|
||||
private val objectMapper: ObjectMapper,
|
||||
) {
|
||||
) : JellyfinSyncSourcePort {
|
||||
private val httpClient: HttpClient =
|
||||
HttpClient
|
||||
.newBuilder()
|
||||
.connectTimeout(Duration.ofMillis(properties.requestTimeoutMs))
|
||||
.build()
|
||||
|
||||
fun fetchUsers(): List<JellyfinRemoteUser> =
|
||||
override fun fetchUsers(): List<JellyfinRemoteUser> =
|
||||
request("Users")
|
||||
.asItems()
|
||||
.mapNotNull { node ->
|
||||
@@ -51,7 +33,7 @@ class JellyfinApiClient(
|
||||
JellyfinRemoteUser(id = id, name = node.fieldText("Name") ?: id)
|
||||
}
|
||||
|
||||
fun fetchLibraryItems(userId: String): List<JellyfinLibraryItemSnapshot> =
|
||||
override fun fetchLibraryItems(userId: String): List<JellyfinLibraryItemSnapshot> =
|
||||
@Suppress("MaxLineLength")
|
||||
request(
|
||||
"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
|
||||
|
||||
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||
import com.project.movienight.domain.model.RecommendationEventType
|
||||
import io.micrometer.core.instrument.Counter
|
||||
@@ -33,7 +34,7 @@ class BusinessMetricsService(
|
||||
|
||||
private val backendWriteFailures: Counter = meterRegistry.counter("business_jellyfin_backend_write_failures_total")
|
||||
|
||||
fun recordRecommendationRequest() {
|
||||
override fun recordRecommendationRequest() {
|
||||
recommendationRequests.increment()
|
||||
}
|
||||
|
||||
@@ -49,11 +50,11 @@ class BusinessMetricsService(
|
||||
ratingsSubmitted.increment()
|
||||
}
|
||||
|
||||
fun recordLibraryEvent() {
|
||||
override fun recordLibraryEvent() {
|
||||
libraryEvents.increment()
|
||||
}
|
||||
|
||||
fun recordJellyfinSync(summary: JellyfinSyncSummary) {
|
||||
override fun recordJellyfinSync(summary: JellyfinSyncSummary) {
|
||||
jellyfinSyncRuns.increment()
|
||||
jellyfinSyncedUsers.increment(summary.syncedUsers.toDouble())
|
||||
jellyfinSkippedUsers.increment(summary.skippedUsers.toDouble())
|
||||
@@ -61,15 +62,15 @@ class BusinessMetricsService(
|
||||
jellyfinSyncDuration.record(summary.durationMs, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
fun recordJellyfinSyncFailure() {
|
||||
override fun recordJellyfinSyncFailure() {
|
||||
jellyfinSyncFailures.increment()
|
||||
}
|
||||
|
||||
fun recordJellyfinUnmappedUser() {
|
||||
override fun recordJellyfinUnmappedUser() {
|
||||
jellyfinUnmappedUsersGaugeValue.incrementAndGet()
|
||||
}
|
||||
|
||||
fun recordBackendWriteFailure() {
|
||||
override fun recordBackendWriteFailure() {
|
||||
backendWriteFailures.increment()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
package com.project.movienight.adapters.persistence.jdbc
|
||||
|
||||
import com.project.movienight.application.ports.output.JellyfinEventRepositoryPort
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
||||
import org.springframework.stereotype.Repository
|
||||
@@ -7,8 +8,8 @@ import org.springframework.stereotype.Repository
|
||||
@Repository
|
||||
class JellyfinEventRepository(
|
||||
private val jdbc: NamedParameterJdbcTemplate,
|
||||
) {
|
||||
fun save(
|
||||
) : JellyfinEventRepositoryPort {
|
||||
override fun save(
|
||||
eventId: String,
|
||||
serverId: String?,
|
||||
eventType: String,
|
||||
@@ -37,7 +38,7 @@ class JellyfinEventRepository(
|
||||
return jdbc.update(sql, params)
|
||||
}
|
||||
|
||||
fun delete(eventId: String) {
|
||||
override fun delete(eventId: String) {
|
||||
val sql = "DELETE FROM jellyfin_events WHERE event_id = :eventId"
|
||||
val params = MapSqlParameterSource().addValue("eventId", eventId)
|
||||
jdbc.update(sql, params)
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.project.movienight.application.ports.output
|
||||
|
||||
import com.project.movienight.domain.model.JellyfinSyncSummary
|
||||
|
||||
interface BusinessMetricsPort {
|
||||
fun recordRecommendationRequest()
|
||||
|
||||
fun recordRatingSubmitted()
|
||||
|
||||
fun recordLibraryEvent()
|
||||
|
||||
fun recordJellyfinSync(summary: JellyfinSyncSummary)
|
||||
|
||||
fun recordJellyfinSyncFailure()
|
||||
|
||||
fun recordJellyfinUnmappedUser()
|
||||
|
||||
fun recordBackendWriteFailure()
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.project.movienight.application.ports.output
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
interface JellyfinEventRepositoryPort {
|
||||
fun save(
|
||||
eventId: String,
|
||||
serverId: String?,
|
||||
eventType: String,
|
||||
occurredAt: OffsetDateTime?,
|
||||
jellyfinUserId: String?,
|
||||
jellyfinItemId: String?,
|
||||
payload: String?,
|
||||
): Int
|
||||
|
||||
fun delete(eventId: String)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.project.movienight.application.ports.output
|
||||
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
interface JellyfinSyncSourcePort {
|
||||
fun fetchUsers(): List<JellyfinRemoteUser>
|
||||
|
||||
fun fetchLibraryItems(userId: String): List<JellyfinLibraryItemSnapshot>
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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.AddFilmToLibraryUseCase
|
||||
import com.project.movienight.application.ports.input.CreateFilmLibraryCommand
|
||||
@@ -12,6 +11,7 @@ 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.application.ports.output.BusinessMetricsPort
|
||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
||||
import com.project.movienight.application.ports.output.IdGenerator
|
||||
import com.project.movienight.domain.exception.DomainException
|
||||
@@ -25,7 +25,7 @@ import java.util.UUID
|
||||
class FilmLibraryService(
|
||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
||||
private val idGenerator: IdGenerator,
|
||||
private val businessMetricsService: BusinessMetricsService,
|
||||
private val businessMetricsService: BusinessMetricsPort,
|
||||
) : CreateFilmLibraryUseCase,
|
||||
AddFilmToLibraryUseCase,
|
||||
MarkFilmViewedUseCase,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
||||
import com.project.movienight.application.ports.input.GetFilmRatingsUseCase
|
||||
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.FilmRepositoryPort
|
||||
import com.project.movienight.application.ports.output.IdGenerator
|
||||
@@ -19,7 +19,7 @@ class FilmRatingService(
|
||||
private val filmRepository: FilmRepositoryPort,
|
||||
private val filmRatingRepository: FilmRatingRepositoryPort,
|
||||
private val idGenerator: IdGenerator,
|
||||
private val businessMetricsService: BusinessMetricsService,
|
||||
private val businessMetricsService: BusinessMetricsPort,
|
||||
) : RateFilmUseCase,
|
||||
GetFilmRatingsUseCase {
|
||||
override fun rate(command: RateFilmCommand): FilmRating {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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.BusinessMetricsPort
|
||||
import com.project.movienight.application.ports.output.JellyfinEventRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -12,12 +12,12 @@ import java.time.OffsetDateTime
|
||||
|
||||
@Service
|
||||
class JellyfinEventService(
|
||||
private val jellyfinEventRepository: JellyfinEventRepository,
|
||||
private val jellyfinEventRepository: JellyfinEventRepositoryPort,
|
||||
private val userRepository: UserRepositoryPort,
|
||||
private val filmRepository: FilmRepositoryPort,
|
||||
private val markFilmViewedUseCase: MarkFilmViewedUseCase,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val businessMetricsService: BusinessMetricsService,
|
||||
private val businessMetricsService: BusinessMetricsPort,
|
||||
) {
|
||||
private val playbackEventTypes = setOf("playback.ended", "playback.stopped", "playback.completed")
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.adapters.jellyfin.JellyfinApiClient
|
||||
import com.project.movienight.adapters.jellyfin.JellyfinLibraryItemSnapshot
|
||||
import com.project.movienight.adapters.jellyfin.JellyfinRemoteUser
|
||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
||||
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
import com.project.movienight.application.ports.output.IdGenerator
|
||||
import com.project.movienight.application.ports.output.JellyfinSyncStateRepositoryPort
|
||||
import com.project.movienight.application.ports.output.JellyfinLibraryItemSnapshot
|
||||
import com.project.movienight.application.ports.output.JellyfinRemoteUser
|
||||
import com.project.movienight.application.ports.output.JellyfinSyncSourcePort
|
||||
import com.project.movienight.application.ports.output.UserRepositoryPort
|
||||
import com.project.movienight.config.JellyfinIntegrationProperties
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
@@ -24,13 +24,13 @@ import java.time.LocalDateTime
|
||||
@Service
|
||||
class JellyfinSyncService(
|
||||
private val properties: JellyfinIntegrationProperties,
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val jellyfinApiClient: JellyfinSyncSourcePort,
|
||||
private val userRepository: UserRepositoryPort,
|
||||
private val filmRepository: FilmRepositoryPort,
|
||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
||||
private val syncStateRepository: JellyfinSyncStateRepositoryPort,
|
||||
private val idGenerator: IdGenerator,
|
||||
private val businessMetricsService: BusinessMetricsService,
|
||||
private val businessMetricsService: BusinessMetricsPort,
|
||||
) {
|
||||
@Scheduled(fixedDelayString = "\${integrations.jellyfin.sync-interval-ms:1800000}")
|
||||
fun scheduledSync() {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.project.movienight.domain.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class RecommendationContext(
|
||||
val userId: UUID,
|
||||
val contentType: ContentType? = null,
|
||||
val mood: String? = null,
|
||||
val libraryOnly: Boolean = false,
|
||||
val limit: Int = 10,
|
||||
)
|
||||
|
||||
data class RecommendationResult(
|
||||
val film: Film,
|
||||
val score: Double,
|
||||
val reasons: List<String>,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.project.movienight.domain.model
|
||||
|
||||
data class RecommendationResult(
|
||||
val film: Film,
|
||||
val score: Double,
|
||||
val reasons: List<String>,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import java.util.UUID
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
class RecommendationControllerTest {
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@Test
|
||||
fun `invalid content type should return bad request`() {
|
||||
mockMvc
|
||||
.get("/api/users/${UUID.randomUUID()}/recommendations") {
|
||||
param("contentType", "INVALID")
|
||||
}.andExpect {
|
||||
status { isBadRequest() }
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.application.ports.input.RecommendationQuery
|
||||
import com.project.movienight.application.ports.output.BusinessMetricsPort
|
||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
|
||||
import com.project.movienight.domain.model.Film
|
||||
import com.project.movienight.domain.model.FilmRating
|
||||
import com.project.movienight.domain.model.UserPreferences
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.UUID
|
||||
|
||||
class RecommendationServiceTest {
|
||||
private val filmRepository: FilmRepositoryPort = mockk()
|
||||
private val filmLibraryRepository: FilmLibraryRepositoryPort = mockk()
|
||||
private val filmRatingRepository: FilmRatingRepositoryPort = mockk()
|
||||
private val userPreferencesRepository: UserPreferencesRepositoryPort = mockk()
|
||||
private val businessMetricsPort: BusinessMetricsPort = mockk(relaxed = true)
|
||||
|
||||
private val recommendationService =
|
||||
RecommendationService(
|
||||
filmRepository = filmRepository,
|
||||
filmLibraryRepository = filmLibraryRepository,
|
||||
filmRatingRepository = filmRatingRepository,
|
||||
userPreferencesRepository = userPreferencesRepository,
|
||||
businessMetricsService = businessMetricsPort,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `should rank highly rated films above poorly rated films`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val lowRatedFilmId = UUID.randomUUID()
|
||||
val highRatedFilmId = UUID.randomUUID()
|
||||
|
||||
val lowRatedFilm = Film(id = lowRatedFilmId, title = "Low", description = "Low")
|
||||
val highRatedFilm = Film(id = highRatedFilmId, title = "High", description = "High")
|
||||
|
||||
every { userPreferencesRepository.findByUserId(userId) } returns UserPreferences(userId = userId)
|
||||
every { filmRatingRepository.findByUserId(userId) } returns
|
||||
listOf(
|
||||
FilmRating(id = UUID.randomUUID(), userId = userId, filmId = lowRatedFilmId, score = 1),
|
||||
FilmRating(id = UUID.randomUUID(), userId = userId, filmId = highRatedFilmId, score = 10),
|
||||
)
|
||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
||||
every { filmRepository.findAll() } returns listOf(lowRatedFilm, highRatedFilm)
|
||||
|
||||
val result = recommendationService.recommend(RecommendationQuery(userId = userId))
|
||||
|
||||
assertEquals(highRatedFilmId, result.first().film.id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user