diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index efbfeaf66..c8fe87a75 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -1008,6 +1008,12 @@
Nuvio Sync
Watch progress source set to Trakt
Watch progress source set to Nuvio Sync
+ More Like This source
+ Choose where recommendations come from on detail pages
+ More Like This source
+ Select the source for recommendations shown on detail pages.
+ Trakt
+ TMDB
Continue Watching Window
Trakt history considered for continue watching
Continue Watching Window
@@ -1133,6 +1139,8 @@
Director
Failed to load
More Like This
+ Powered by TMDB
+ Powered by Trakt
Seasons
This addon returned videos for the series, but none included season or episode numbers.
This addon did not provide episode metadata for this series.
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt
index 3ac52ded4..de32bc32b 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt
@@ -31,6 +31,7 @@ data class MetaDetails(
val website: String? = null,
val hasScheduledVideos: Boolean = false,
val moreLikeThis: List = emptyList(),
+ val moreLikeThisSource: MoreLikeThisSource? = null,
val collectionName: String? = null,
val collectionItems: List = emptyList(),
val trailers: List = emptyList(),
@@ -38,6 +39,11 @@ data class MetaDetails(
val videos: List = emptyList(),
)
+enum class MoreLikeThisSource {
+ TMDB,
+ TRAKT,
+}
+
data class MetaExternalRating(
val source: String,
val value: Double,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt
index fcb432b94..b1654a892 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt
@@ -13,6 +13,11 @@ import com.nuvio.app.features.mdblist.MdbListSettingsRepository
import com.nuvio.app.features.tmdb.TmdbMetadataService
import com.nuvio.app.features.tmdb.TmdbService
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
+import com.nuvio.app.features.trakt.TraktAuthRepository
+import com.nuvio.app.features.trakt.TraktConnectionMode
+import com.nuvio.app.features.trakt.TraktRelatedRepository
+import com.nuvio.app.features.trakt.TraktSettingsRepository
+import com.nuvio.app.features.trakt.shouldUseTraktMoreLikeThis
import com.nuvio.app.features.watchprogress.CurrentDateProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -58,7 +63,7 @@ object MetaDetailsRepository {
}
val cachedBaseMeta = cachedEntry.baseMeta
- if (!shouldFetchMdbListOnMetaScreen(cachedBaseMeta, id, mdbListSettings)) {
+ if (!shouldEnrichForMetaScreen(cachedBaseMeta, id, mdbListSettings)) {
_uiState.value = MetaDetailsUiState(meta = cachedBaseMeta.withUnreleasedFilter())
activeRequestKey = requestKey
return
@@ -81,6 +86,7 @@ object MetaDetailsRepository {
requestKey = requestKey,
meta = cachedBaseMeta,
fallbackItemId = id,
+ fallbackItemType = type,
settings = mdbListSettings,
settingsFingerprint = metaScreenSettingsFingerprint,
)
@@ -116,6 +122,7 @@ object MetaDetailsRepository {
requestKey = requestKey,
meta = tmdbMeta,
fallbackItemId = id,
+ fallbackItemType = type,
mdbListSettings = mdbListSettings,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
@@ -139,6 +146,7 @@ object MetaDetailsRepository {
requestKey = requestKey,
meta = result,
fallbackItemId = metaLookupId,
+ fallbackItemType = type,
mdbListSettings = mdbListSettings,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
@@ -152,6 +160,7 @@ object MetaDetailsRepository {
requestKey = requestKey,
meta = tmdbMeta,
fallbackItemId = id,
+ fallbackItemType = type,
mdbListSettings = mdbListSettings,
metaScreenSettingsFingerprint = metaScreenSettingsFingerprint,
)
@@ -300,13 +309,14 @@ object MetaDetailsRepository {
requestKey: String,
meta: MetaDetails,
fallbackItemId: String,
+ fallbackItemType: String,
mdbListSettings: com.nuvio.app.features.mdblist.MdbListSettings,
metaScreenSettingsFingerprint: String,
) {
val cachedEntry = CachedMetaEntry(baseMeta = meta)
cachedMetaByRequestKey[requestKey] = cachedEntry
- if (!shouldFetchMdbListOnMetaScreen(meta, fallbackItemId, mdbListSettings)) {
+ if (!shouldEnrichForMetaScreen(meta, fallbackItemId, mdbListSettings)) {
_uiState.value = MetaDetailsUiState(meta = meta.withUnreleasedFilter())
activeRequestKey = requestKey
return
@@ -321,6 +331,7 @@ object MetaDetailsRepository {
requestKey = requestKey,
meta = meta,
fallbackItemId = fallbackItemId,
+ fallbackItemType = fallbackItemType,
settings = mdbListSettings,
settingsFingerprint = metaScreenSettingsFingerprint,
)
@@ -337,16 +348,22 @@ object MetaDetailsRepository {
requestKey: String,
meta: MetaDetails,
fallbackItemId: String,
+ fallbackItemType: String,
settings: com.nuvio.app.features.mdblist.MdbListSettings,
settingsFingerprint: String,
): MetaDetails {
- val enrichedMeta = withTimeoutOrNull(MDBLIST_ENRICH_TIMEOUT_MS) {
+ val mdbListEnrichedMeta = withTimeoutOrNull(MDBLIST_ENRICH_TIMEOUT_MS) {
MdbListMetadataService.enrichMeta(
meta = meta,
fallbackItemId = fallbackItemId,
settings = settings,
)
} ?: meta
+ val enrichedMeta = applyMoreLikeThisSource(
+ meta = mdbListEnrichedMeta,
+ fallbackItemId = fallbackItemId,
+ fallbackItemType = fallbackItemType,
+ )
cachedMetaByRequestKey[requestKey] = cachedMetaByRequestKey[requestKey]
?.copy(
@@ -362,6 +379,49 @@ object MetaDetailsRepository {
return enrichedMeta
}
+ private suspend fun applyMoreLikeThisSource(
+ meta: MetaDetails,
+ fallbackItemId: String,
+ fallbackItemType: String,
+ ): MetaDetails {
+ TraktSettingsRepository.ensureLoaded()
+ TraktAuthRepository.ensureLoaded()
+ TmdbSettingsRepository.ensureLoaded()
+
+ val traktSettings = TraktSettingsRepository.uiState.value
+ val isTraktAuthenticated = TraktAuthRepository.uiState.value.mode == TraktConnectionMode.CONNECTED
+ val shouldUseTrakt = shouldUseTraktMoreLikeThis(
+ isAuthenticated = isTraktAuthenticated,
+ source = traktSettings.moreLikeThisSource,
+ ) && supportsMoreLikeThis(meta, fallbackItemType)
+
+ if (shouldUseTrakt) {
+ val items = runCatching {
+ TraktRelatedRepository.getRelated(
+ meta = meta,
+ fallbackItemId = fallbackItemId,
+ fallbackItemType = fallbackItemType,
+ )
+ }.onFailure { error ->
+ log.w { "Failed to load Trakt related titles for ${meta.id}: ${error.message}" }
+ }.getOrDefault(emptyList())
+
+ return meta.copy(
+ moreLikeThis = items,
+ moreLikeThisSource = MoreLikeThisSource.TRAKT.takeIf { items.isNotEmpty() },
+ )
+ }
+
+ val tmdbSettings = TmdbSettingsRepository.snapshot()
+ if (!tmdbSettings.enabled || !tmdbSettings.useMoreLikeThis) {
+ return meta.copy(moreLikeThis = emptyList(), moreLikeThisSource = null)
+ }
+
+ return meta.copy(
+ moreLikeThisSource = MoreLikeThisSource.TMDB.takeIf { meta.moreLikeThis.isNotEmpty() },
+ )
+ }
+
private fun shouldFetchMdbListOnMetaScreen(
meta: MetaDetails,
fallbackItemId: String,
@@ -372,18 +432,63 @@ object MetaDetailsRepository {
settings = settings,
)
+ private fun shouldEnrichForMetaScreen(
+ meta: MetaDetails,
+ fallbackItemId: String,
+ settings: com.nuvio.app.features.mdblist.MdbListSettings,
+ ): Boolean {
+ if (shouldFetchMdbListOnMetaScreen(meta, fallbackItemId, settings)) return true
+ return shouldApplyMoreLikeThisSource(meta)
+ }
+
+ private fun shouldApplyMoreLikeThisSource(meta: MetaDetails): Boolean {
+ TraktSettingsRepository.ensureLoaded()
+ TraktAuthRepository.ensureLoaded()
+ TmdbSettingsRepository.ensureLoaded()
+
+ val traktSettings = TraktSettingsRepository.uiState.value
+ val isTraktAuthenticated = TraktAuthRepository.uiState.value.mode == TraktConnectionMode.CONNECTED
+ val tmdbSettings = TmdbSettingsRepository.snapshot()
+ return shouldUseTraktMoreLikeThis(
+ isAuthenticated = isTraktAuthenticated,
+ source = traktSettings.moreLikeThisSource,
+ ) || !tmdbSettings.enabled || !tmdbSettings.useMoreLikeThis || meta.moreLikeThisSource == null && meta.moreLikeThis.isNotEmpty()
+ }
+
private fun buildMetaScreenSettingsFingerprint(
settings: com.nuvio.app.features.mdblist.MdbListSettings,
): String {
+ TraktSettingsRepository.ensureLoaded()
+ TraktAuthRepository.ensureLoaded()
+ TmdbSettingsRepository.ensureLoaded()
val providers = settings.enabledProvidersInPriorityOrder().joinToString(",")
- return "${settings.enabled}:${settings.apiKey.trim()}:$providers"
+ val traktSettings = TraktSettingsRepository.uiState.value
+ val traktAuthMode = TraktAuthRepository.uiState.value.mode
+ val tmdbSettings = TmdbSettingsRepository.snapshot()
+ return buildString {
+ append("${settings.enabled}:${settings.apiKey.trim()}:$providers")
+ append("|more_like=${traktSettings.moreLikeThisSource}:$traktAuthMode")
+ append("|tmdb=${tmdbSettings.enabled}:${tmdbSettings.useMoreLikeThis}:${tmdbSettings.hasApiKey}:${tmdbSettings.language}")
+ }
}
+ private fun supportsMoreLikeThis(meta: MetaDetails, fallbackItemType: String): Boolean =
+ normalizeMoreLikeThisType(meta.type) != null || normalizeMoreLikeThisType(fallbackItemType) != null
+
+ private fun normalizeMoreLikeThisType(value: String?): String? =
+ when (value?.trim()?.lowercase()) {
+ "movie", "film" -> "movie"
+ "series", "show", "tv", "tvshow" -> "series"
+ else -> null
+ }
+
private fun MetaDetails.withUnreleasedFilter(): MetaDetails {
if (!HomeCatalogSettingsRepository.snapshot().hideUnreleasedContent) return this
val todayIsoDate = CurrentDateProvider.todayIsoDate()
+ val releasedMoreLikeThis = moreLikeThis.filterReleasedItems(todayIsoDate)
return copy(
- moreLikeThis = moreLikeThis.filterReleasedItems(todayIsoDate),
+ moreLikeThis = releasedMoreLikeThis,
+ moreLikeThisSource = moreLikeThisSource.takeIf { releasedMoreLikeThis.isNotEmpty() },
collectionItems = collectionItems.filterReleasedItems(todayIsoDate),
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt
index 18c1b4b3c..1e48bb62d 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt
@@ -89,6 +89,7 @@ import com.nuvio.app.features.library.toLibraryItem
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.streams.AddonStreamWarmupRepository
import com.nuvio.app.features.streams.StreamAutoPlayPolicy
+import com.nuvio.app.features.tmdb.TmdbSettingsRepository
import com.nuvio.app.features.tmdb.TmdbService
import com.nuvio.app.features.trakt.TraktAuthRepository
import com.nuvio.app.features.trakt.TraktCommentReview
@@ -96,6 +97,7 @@ import com.nuvio.app.features.trakt.TraktCommentsRepository
import com.nuvio.app.features.trakt.TraktCommentsSettings
import com.nuvio.app.features.trakt.TraktConnectionMode
import com.nuvio.app.features.trakt.TraktListTab
+import com.nuvio.app.features.trakt.TraktSettingsRepository
import com.nuvio.app.features.trailer.TrailerPlaybackResolver
import com.nuvio.app.features.trailer.TrailerPlaybackSource
import com.nuvio.app.features.watched.WatchedRepository
@@ -140,6 +142,14 @@ fun MetaDetailsScreen(
TraktAuthRepository.ensureLoaded()
TraktAuthRepository.uiState
}.collectAsStateWithLifecycle()
+ val traktSettingsUiState by remember {
+ TraktSettingsRepository.ensureLoaded()
+ TraktSettingsRepository.uiState
+ }.collectAsStateWithLifecycle()
+ val tmdbSettingsUiState by remember {
+ TmdbSettingsRepository.ensureLoaded()
+ TmdbSettingsRepository.uiState
+ }.collectAsStateWithLifecycle()
val libraryUiState by remember {
LibraryRepository.ensureLoaded()
LibraryRepository.uiState
@@ -237,6 +247,22 @@ fun MetaDetailsScreen(
}
}
+ LaunchedEffect(
+ type,
+ id,
+ displayedMeta?.id,
+ uiState.isLoading,
+ traktSettingsUiState.moreLikeThisSource,
+ traktAuthUiState.mode,
+ tmdbSettingsUiState.enabled,
+ tmdbSettingsUiState.useMoreLikeThis,
+ tmdbSettingsUiState.language,
+ ) {
+ if (displayedMeta != null && !uiState.isLoading) {
+ MetaDetailsRepository.load(type, id)
+ }
+ }
+
LaunchedEffect(networkStatusUiState.condition, displayedMeta, uiState.isLoading, type, id) {
when (networkStatusUiState.condition) {
NetworkCondition.NoInternet,
@@ -1417,11 +1443,17 @@ private fun ConfiguredMetaSections(
}
MetaScreenSectionKey.MORE_LIKE_THIS -> {
if (hasMoreLikeThisSection) {
+ val sourceLabel = when (meta.moreLikeThisSource) {
+ MoreLikeThisSource.TMDB -> stringResource(Res.string.detail_more_like_this_powered_by_tmdb)
+ MoreLikeThisSource.TRAKT -> stringResource(Res.string.detail_more_like_this_powered_by_trakt)
+ null -> null
+ }
DetailPosterRailSection(
title = stringResource(Res.string.details_more_like_this),
items = meta.moreLikeThis,
watchedKeys = watchedKeys,
showHeader = showHeader,
+ sourceLabel = sourceLabel,
onPosterClick = onOpenMeta,
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt
index 856f442c3..58c2269f8 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailPosterRailSection.kt
@@ -1,8 +1,15 @@
package com.nuvio.app.features.details.components
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.NuvioShelfSection
@@ -19,28 +26,45 @@ fun DetailPosterRailSection(
modifier: Modifier = Modifier,
showHeader: Boolean = true,
headerHorizontalPadding: Dp = 0.dp,
+ sourceLabel: String? = null,
onPosterClick: ((MetaPreview) -> Unit)? = null,
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
) {
if (items.isEmpty()) return
- NuvioShelfSection(
- title = if (showHeader) title else "",
- entries = items,
- modifier = modifier,
- headerHorizontalPadding = headerHorizontalPadding,
- rowContentPadding = PaddingValues(horizontal = headerHorizontalPadding),
- showHeaderAccent = false,
- key = { item -> item.stableKey() },
- ) { item ->
- HomePosterCard(
- item = item,
- isWatched = WatchingState.isPosterWatched(
- watchedKeys = watchedKeys,
+ Column(modifier = modifier.fillMaxWidth()) {
+ NuvioShelfSection(
+ title = if (showHeader) title else "",
+ entries = items,
+ headerHorizontalPadding = headerHorizontalPadding,
+ rowContentPadding = PaddingValues(horizontal = headerHorizontalPadding),
+ showHeaderAccent = false,
+ key = { item -> item.stableKey() },
+ ) { item ->
+ HomePosterCard(
item = item,
- ),
- onClick = onPosterClick?.let { { it(item) } },
- onLongClick = onPosterLongClick?.let { { it(item) } },
- )
+ isWatched = WatchingState.isPosterWatched(
+ watchedKeys = watchedKeys,
+ item = item,
+ ),
+ onClick = onPosterClick?.let { { it(item) } },
+ onLongClick = onPosterLongClick?.let { { it(item) } },
+ )
+ }
+
+ sourceLabel
+ ?.takeIf { it.isNotBlank() }
+ ?.let { label ->
+ Text(
+ text = label,
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(end = headerHorizontalPadding, top = 4.dp),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
index d98da5120..796cca623 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
@@ -809,6 +809,7 @@ internal fun settingsSearchEntries(
PlaybackSearchRow("trakt-watch-progress", stringResource(Res.string.trakt_watch_progress_title), stringResource(Res.string.trakt_watch_progress_subtitle)),
PlaybackSearchRow("trakt-continue-watching-window", stringResource(Res.string.trakt_continue_watching_window), stringResource(Res.string.trakt_continue_watching_subtitle)),
PlaybackSearchRow("trakt-comments", stringResource(Res.string.settings_trakt_comments), stringResource(Res.string.settings_trakt_comments_description)),
+ PlaybackSearchRow("trakt-more-like-this-source", stringResource(Res.string.trakt_more_like_this_source_title), stringResource(Res.string.trakt_more_like_this_source_subtitle)),
).forEach { row ->
addRow(
page = SettingsPage.TraktAuthentication,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/TraktSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/TraktSettingsPage.kt
index 198b31238..ac9815580 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/TraktSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/TraktSettingsPage.kt
@@ -43,6 +43,7 @@ import com.nuvio.app.features.trakt.TraktBrandAsset
import com.nuvio.app.features.trakt.TraktAuthUiState
import com.nuvio.app.features.trakt.TraktConnectionMode
import com.nuvio.app.features.trakt.TraktContinueWatchingDaysOptions
+import com.nuvio.app.features.trakt.MoreLikeThisSourcePreference
import com.nuvio.app.features.trakt.TraktSettingsRepository
import com.nuvio.app.features.trakt.TraktSettingsUiState
import com.nuvio.app.features.trakt.WatchProgressSource
@@ -82,6 +83,12 @@ import nuvio.composeapp.generated.resources.trakt_library_source_subtitle
import nuvio.composeapp.generated.resources.trakt_library_source_title
import nuvio.composeapp.generated.resources.trakt_library_source_trakt
import nuvio.composeapp.generated.resources.trakt_library_source_trakt_selected
+import nuvio.composeapp.generated.resources.trakt_more_like_this_source_dialog_subtitle
+import nuvio.composeapp.generated.resources.trakt_more_like_this_source_dialog_title
+import nuvio.composeapp.generated.resources.trakt_more_like_this_source_subtitle
+import nuvio.composeapp.generated.resources.trakt_more_like_this_source_title
+import nuvio.composeapp.generated.resources.trakt_more_like_this_source_tmdb
+import nuvio.composeapp.generated.resources.trakt_more_like_this_source_trakt
import nuvio.composeapp.generated.resources.trakt_watch_progress_dialog_subtitle
import nuvio.composeapp.generated.resources.trakt_watch_progress_dialog_title
import nuvio.composeapp.generated.resources.trakt_watch_progress_nuvio_selected
@@ -148,11 +155,13 @@ private fun TraktFeatureRows(
var showLibrarySourceDialog by rememberSaveable { mutableStateOf(false) }
var showWatchProgressDialog by rememberSaveable { mutableStateOf(false) }
var showContinueWatchingWindowDialog by rememberSaveable { mutableStateOf(false) }
+ var showMoreLikeThisSourceDialog by rememberSaveable { mutableStateOf(false) }
var statusMessage by rememberSaveable { mutableStateOf(null) }
val librarySourceValue = librarySourceModeLabel(settingsUiState.librarySourceMode)
val watchProgressValue = watchProgressSourceLabel(settingsUiState.watchProgressSource)
val continueWatchingWindowValue = continueWatchingDaysCapLabel(settingsUiState.continueWatchingDaysCap)
+ val moreLikeThisSourceValue = moreLikeThisSourceLabel(settingsUiState.moreLikeThisSource)
val traktProgressSelectedMessage = stringResource(Res.string.trakt_watch_progress_trakt_selected)
val nuvioProgressSelectedMessage = stringResource(Res.string.trakt_watch_progress_nuvio_selected)
val traktLibrarySelectedMessage = stringResource(Res.string.trakt_library_source_trakt_selected)
@@ -189,6 +198,14 @@ private fun TraktFeatureRows(
isTablet = isTablet,
onCheckedChange = onCommentsEnabledChange,
)
+ SettingsGroupDivider(isTablet = isTablet)
+ TraktSettingsActionRow(
+ title = stringResource(Res.string.trakt_more_like_this_source_title),
+ description = stringResource(Res.string.trakt_more_like_this_source_subtitle),
+ value = moreLikeThisSourceValue,
+ isTablet = isTablet,
+ onClick = { showMoreLikeThisSourceDialog = true },
+ )
statusMessage?.takeIf { it.isNotBlank() }?.let { message ->
SettingsGroupDivider(isTablet = isTablet)
TraktInfoRow(
@@ -239,6 +256,17 @@ private fun TraktFeatureRows(
onDismiss = { showContinueWatchingWindowDialog = false },
)
}
+
+ if (showMoreLikeThisSourceDialog) {
+ MoreLikeThisSourceDialog(
+ selectedSource = settingsUiState.moreLikeThisSource,
+ onSourceSelected = { source ->
+ TraktSettingsRepository.setMoreLikeThisSource(source)
+ showMoreLikeThisSourceDialog = false
+ },
+ onDismiss = { showMoreLikeThisSourceDialog = false },
+ )
+ }
}
@Composable
@@ -322,6 +350,13 @@ private fun watchProgressSourceLabel(source: WatchProgressSource): String =
WatchProgressSource.NUVIO_SYNC -> stringResource(Res.string.trakt_watch_progress_source_nuvio)
}
+@Composable
+private fun moreLikeThisSourceLabel(source: MoreLikeThisSourcePreference): String =
+ when (source) {
+ MoreLikeThisSourcePreference.TRAKT -> stringResource(Res.string.trakt_more_like_this_source_trakt)
+ MoreLikeThisSourcePreference.TMDB -> stringResource(Res.string.trakt_more_like_this_source_tmdb)
+ }
+
@Composable
private fun continueWatchingDaysCapLabel(daysCap: Int): String {
val normalized = normalizeTraktContinueWatchingDaysCap(daysCap)
@@ -494,6 +529,59 @@ private fun ContinueWatchingWindowDialog(
}
}
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun MoreLikeThisSourceDialog(
+ selectedSource: MoreLikeThisSourcePreference,
+ onSourceSelected: (MoreLikeThisSourcePreference) -> Unit,
+ onDismiss: () -> Unit,
+) {
+ BasicAlertDialog(onDismissRequest = onDismiss) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ color = MaterialTheme.colorScheme.surface,
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Text(
+ text = stringResource(Res.string.trakt_more_like_this_source_dialog_title),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold,
+ )
+ Text(
+ text = stringResource(Res.string.trakt_more_like_this_source_dialog_subtitle),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ listOf(MoreLikeThisSourcePreference.TRAKT, MoreLikeThisSourcePreference.TMDB).forEach { source ->
+ TraktDialogOption(
+ label = moreLikeThisSourceLabel(source),
+ selected = source == selectedSource,
+ onClick = { onSourceSelected(source) },
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = stringResource(Res.string.settings_playback_dialog_close),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ }
+}
+
@Composable
private fun TraktDialogOption(
label: String,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tmdb/TmdbMetadataService.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tmdb/TmdbMetadataService.kt
index 0a29dd1be..dd690f741 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tmdb/TmdbMetadataService.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tmdb/TmdbMetadataService.kt
@@ -7,6 +7,7 @@ import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.features.details.MetaPerson
import com.nuvio.app.features.details.MetaTrailer
import com.nuvio.app.features.details.MetaVideo
+import com.nuvio.app.features.details.MoreLikeThisSource
import com.nuvio.app.features.details.PersonDetail
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
@@ -618,6 +619,7 @@ object TmdbMetadataService {
country = enrichment.countries.takeIf { it.isNotEmpty() }?.joinToString(", "),
language = enrichment.language,
moreLikeThis = enrichment.moreLikeThis,
+ moreLikeThisSource = MoreLikeThisSource.TMDB.takeIf { enrichment.moreLikeThis.isNotEmpty() },
collectionName = enrichment.collectionName,
collectionItems = enrichment.collectionItems,
trailers = enrichment.trailers,
@@ -726,7 +728,10 @@ object TmdbMetadataService {
}
if (enrichment != null && settings.useMoreLikeThis) {
- updated = updated.copy(moreLikeThis = enrichment.moreLikeThis)
+ updated = updated.copy(
+ moreLikeThis = enrichment.moreLikeThis,
+ moreLikeThisSource = MoreLikeThisSource.TMDB.takeIf { enrichment.moreLikeThis.isNotEmpty() },
+ )
}
if (enrichment != null && settings.useCollections) {
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktRelatedRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktRelatedRepository.kt
new file mode 100644
index 000000000..c0b61befb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktRelatedRepository.kt
@@ -0,0 +1,327 @@
+package com.nuvio.app.features.trakt
+
+import co.touchlab.kermit.Logger
+import com.nuvio.app.features.addons.httpRequestRaw
+import com.nuvio.app.features.details.MetaDetails
+import com.nuvio.app.features.home.MetaPreview
+import com.nuvio.app.features.home.PosterShape
+import com.nuvio.app.features.tmdb.TmdbService
+import io.ktor.http.encodeURLParameter
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlin.math.roundToInt
+
+private const val BASE_URL = "https://api.trakt.tv"
+private const val RELATED_LIMIT = 20
+private const val RELATED_CACHE_TTL_MS = 10 * 60_000L
+
+object TraktRelatedRepository {
+ private val log = Logger.withTag("TraktRelated")
+ private val json = Json { ignoreUnknownKeys = true }
+ private val cacheMutex = Mutex()
+ private val cache = mutableMapOf()
+
+ suspend fun getRelated(
+ meta: MetaDetails,
+ fallbackItemId: String? = null,
+ fallbackItemType: String? = null,
+ forceRefresh: Boolean = false,
+ ): List {
+ val headers = TraktAuthRepository.authorizedHeaders() ?: return emptyList()
+ val target = resolveRelatedTarget(
+ meta = meta,
+ fallbackItemId = fallbackItemId,
+ fallbackItemType = fallbackItemType,
+ headers = headers,
+ ) ?: return emptyList()
+ val cacheKey = "${target.type.apiValue}|${target.pathId}"
+
+ if (forceRefresh) {
+ cacheMutex.withLock { cache.remove(cacheKey) }
+ }
+
+ if (!forceRefresh) {
+ cacheMutex.withLock {
+ cache[cacheKey]?.let { cached ->
+ if (TraktPlatformClock.nowEpochMs() - cached.updatedAtMs <= RELATED_CACHE_TTL_MS) {
+ return cached.items
+ }
+ }
+ }
+ }
+
+ val items = fetchRelated(target = target, headers = headers)
+ .distinctBy { it.stableRelatedKey() }
+ .take(RELATED_LIMIT)
+
+ cacheMutex.withLock {
+ cache[cacheKey] = TimedCache(
+ items = items,
+ updatedAtMs = TraktPlatformClock.nowEpochMs(),
+ )
+ }
+ return items
+ }
+
+ fun clearCache() {
+ cache.clear()
+ }
+
+ private suspend fun fetchRelated(
+ target: ResolvedRelatedTarget,
+ headers: Map,
+ ): List {
+ val endpoint = when (target.type) {
+ TraktRelatedType.MOVIE -> "movies"
+ TraktRelatedType.SHOW -> "shows"
+ }
+ val response = httpRequestRaw(
+ method = "GET",
+ url = buildTraktUrl("$endpoint/${target.pathId}/related", mapOf("extended" to "full,images")),
+ headers = jsonHeaders(headers),
+ body = "",
+ )
+
+ if (response.status == 404) return emptyList()
+ if (response.status !in 200..299) {
+ error("Failed to load Trakt related titles (${response.status})")
+ }
+
+ return when (target.type) {
+ TraktRelatedType.MOVIE -> json.decodeFromString>(response.body)
+ .mapNotNull { it.toMetaPreview() }
+ TraktRelatedType.SHOW -> json.decodeFromString>(response.body)
+ .mapNotNull { it.toMetaPreview() }
+ }
+ }
+
+ private suspend fun resolveRelatedTarget(
+ meta: MetaDetails,
+ fallbackItemId: String?,
+ fallbackItemType: String?,
+ headers: Map,
+ ): ResolvedRelatedTarget? {
+ val type = resolveRelatedType(meta = meta, fallbackItemType = fallbackItemType) ?: return null
+ resolveDirectPathId(meta.id)?.let { return ResolvedRelatedTarget(type, it) }
+ resolveDirectPathId(fallbackItemId)?.let { return ResolvedRelatedTarget(type, it) }
+
+ val tmdbId = resolveTmdbCandidate(meta.id)
+ ?: resolveTmdbCandidate(fallbackItemId)
+ ?: TmdbService.ensureTmdbId(meta.id, meta.type)?.toIntOrNull()
+ ?: fallbackItemId?.let { TmdbService.ensureTmdbId(it, fallbackItemType ?: meta.type) }?.toIntOrNull()
+ ?: return null
+
+ return resolveViaTraktSearch(type = type, tmdbId = tmdbId, headers = headers)
+ }
+
+ private fun resolveRelatedType(meta: MetaDetails, fallbackItemType: String?): TraktRelatedType? {
+ return when (normalizeRelatedType(meta.type)) {
+ TraktRelatedType.MOVIE -> TraktRelatedType.MOVIE
+ TraktRelatedType.SHOW -> TraktRelatedType.SHOW
+ null -> normalizeRelatedType(fallbackItemType)
+ }
+ }
+
+ private fun normalizeRelatedType(value: String?): TraktRelatedType? =
+ when (value?.trim()?.lowercase()) {
+ "movie", "film" -> TraktRelatedType.MOVIE
+ "series", "show", "tv", "tvshow" -> TraktRelatedType.SHOW
+ else -> null
+ }
+
+ private fun resolveDirectPathId(value: String?): String? {
+ val raw = value?.trim().orEmpty()
+ if (raw.isBlank()) return null
+ extractImdbId(raw)?.let { return it }
+ parseTraktContentIds(raw).trakt?.let { return it.toString() }
+ return raw
+ .takeIf { !it.startsWith("tmdb:", ignoreCase = true) }
+ ?.takeIf { it.all(Char::isDigit) }
+ }
+
+ private fun resolveTmdbCandidate(value: String?): Int? =
+ parseTraktContentIds(value).tmdb ?: extractTmdbId(value)
+
+ private suspend fun resolveViaTraktSearch(
+ type: TraktRelatedType,
+ tmdbId: Int,
+ headers: Map,
+ ): ResolvedRelatedTarget? {
+ val response = runCatching {
+ httpRequestRaw(
+ method = "GET",
+ url = buildTraktUrl(
+ endpoint = "search/tmdb/$tmdbId",
+ query = mapOf("type" to type.apiValue),
+ ),
+ headers = jsonHeaders(headers),
+ body = "",
+ )
+ }.onFailure { error ->
+ log.w(error) { "TMDB to Trakt lookup failed for tmdbId=$tmdbId" }
+ }.getOrNull() ?: return null
+
+ if (response.status == 404) return null
+ if (response.status !in 200..299) {
+ log.w { "Failed to resolve Trakt id for tmdbId=$tmdbId (${response.status})" }
+ return null
+ }
+
+ val results = runCatching {
+ json.decodeFromString>(response.body)
+ }.getOrDefault(emptyList())
+ val match = results.firstOrNull { it.type.equals(type.apiValue, ignoreCase = true) }
+ val ids = when (type) {
+ TraktRelatedType.MOVIE -> match?.movie?.ids
+ TraktRelatedType.SHOW -> match?.show?.ids
+ }
+ return ids?.bestPathId()?.let { ResolvedRelatedTarget(type, it) }
+ }
+
+ private fun buildTraktUrl(endpoint: String, query: Map = emptyMap()): String {
+ val queryString = (query + mapOf("page" to "1", "limit" to RELATED_LIMIT.toString()))
+ .entries
+ .filter { (_, value) -> value.isNotBlank() }
+ .joinToString("&") { (key, value) ->
+ "${key.encodeURLParameter()}=${value.encodeURLParameter()}"
+ }
+ return "$BASE_URL/${endpoint.trim('/')}" + if (queryString.isBlank()) "" else "?$queryString"
+ }
+
+ private fun jsonHeaders(headers: Map): Map =
+ mapOf("Accept" to "application/json") + headers
+}
+
+private data class TimedCache(
+ val items: List,
+ val updatedAtMs: Long,
+)
+
+private enum class TraktRelatedType(val apiValue: String) {
+ MOVIE("movie"),
+ SHOW("show"),
+}
+
+private data class ResolvedRelatedTarget(
+ val type: TraktRelatedType,
+ val pathId: String,
+)
+
+@Serializable
+private data class TraktRelatedSearchResultDto(
+ val type: String? = null,
+ val movie: TraktRelatedSearchItemDto? = null,
+ val show: TraktRelatedSearchItemDto? = null,
+)
+
+@Serializable
+private data class TraktRelatedSearchItemDto(
+ val ids: TraktExternalIds? = null,
+)
+
+@Serializable
+private data class TraktRelatedMovieDto(
+ val title: String? = null,
+ @SerialName("original_title") val originalTitle: String? = null,
+ val year: Int? = null,
+ val ids: TraktExternalIds? = null,
+ val overview: String? = null,
+ val released: String? = null,
+ val rating: Double? = null,
+ val genres: List? = null,
+ val images: TraktImagesDto? = null,
+)
+
+@Serializable
+private data class TraktRelatedShowDto(
+ val title: String? = null,
+ @SerialName("original_title") val originalTitle: String? = null,
+ val year: Int? = null,
+ val ids: TraktExternalIds? = null,
+ val overview: String? = null,
+ @SerialName("first_aired") val firstAired: String? = null,
+ val rating: Double? = null,
+ val genres: List? = null,
+ val images: TraktImagesDto? = null,
+)
+
+private fun TraktRelatedMovieDto.toMetaPreview(): MetaPreview? {
+ val normalizedTitle = title?.trim()?.takeIf(String::isNotBlank)
+ ?: originalTitle?.trim()?.takeIf(String::isNotBlank)
+ ?: return null
+ val contentId = normalizeTraktContentId(ids, fallback = fallbackTraktContentId(ids, "movie"))
+ if (contentId.isBlank()) return null
+
+ return MetaPreview(
+ id = contentId,
+ type = "movie",
+ name = normalizedTitle,
+ poster = images.traktBestPosterUrl(),
+ banner = images.traktBestBackdropUrl(),
+ logo = images.traktBestLogoUrl(),
+ posterShape = PosterShape.Poster,
+ description = overview?.trim()?.takeIf(String::isNotBlank),
+ releaseInfo = year?.toString() ?: released?.take(4),
+ rawReleaseDate = released,
+ imdbRating = rating?.formatTraktRating(),
+ genres = genres.orEmpty(),
+ )
+}
+
+private fun TraktRelatedShowDto.toMetaPreview(): MetaPreview? {
+ val normalizedTitle = title?.trim()?.takeIf(String::isNotBlank)
+ ?: originalTitle?.trim()?.takeIf(String::isNotBlank)
+ ?: return null
+ val contentId = normalizeTraktContentId(ids, fallback = fallbackTraktContentId(ids, "series"))
+ if (contentId.isBlank()) return null
+
+ return MetaPreview(
+ id = contentId,
+ type = "series",
+ name = normalizedTitle,
+ poster = images.traktBestPosterUrl(),
+ banner = images.traktBestBackdropUrl(),
+ logo = images.traktBestLogoUrl(),
+ posterShape = PosterShape.Poster,
+ description = overview?.trim()?.takeIf(String::isNotBlank),
+ releaseInfo = year?.toString() ?: firstAired?.take(4),
+ rawReleaseDate = firstAired,
+ imdbRating = rating?.formatTraktRating(),
+ genres = genres.orEmpty(),
+ )
+}
+
+private fun fallbackTraktContentId(ids: TraktExternalIds?, typePrefix: String): String? =
+ ids?.slug?.takeIf { it.isNotBlank() }?.let { "$typePrefix:$it" }
+ ?: ids?.trakt?.let { "trakt:$it" }
+
+private fun TraktExternalIds.bestPathId(): String? =
+ imdb?.takeIf { it.isNotBlank() }
+ ?: trakt?.toString()
+ ?: slug?.takeIf { it.isNotBlank() }
+
+private fun MetaPreview.stableRelatedKey(): String = "$type:$id"
+
+private fun extractImdbId(value: String?): String? =
+ value
+ ?.trim()
+ ?.split(':', '/', '?', '&')
+ ?.firstOrNull { part -> part.startsWith("tt", ignoreCase = true) }
+ ?.takeIf { it.length > 2 }
+
+private fun extractTmdbId(value: String?): Int? {
+ val trimmed = value?.trim().orEmpty()
+ if (trimmed.isBlank()) return null
+ return trimmed
+ .takeIf { it.startsWith("tmdb:", ignoreCase = true) }
+ ?.substringAfter(':')
+ ?.substringBefore(':')
+ ?.substringBefore('/')
+ ?.toIntOrNull()
+}
+
+private fun Double.formatTraktRating(): String =
+ ((this * 10).roundToInt() / 10.0).toString()
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepository.kt
index ee9cccd49..3b22d9dc3 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepository.kt
@@ -41,10 +41,24 @@ val DEFAULT_LIBRARY_SOURCE_MODE: LibrarySourceMode = LibrarySourceMode.TRAKT
fun librarySourceModeFromStorage(value: String?): LibrarySourceMode =
LibrarySourceMode.entries.firstOrNull { it.name == value } ?: DEFAULT_LIBRARY_SOURCE_MODE
+@Serializable
+enum class MoreLikeThisSourcePreference {
+ TRAKT,
+ TMDB;
+
+ companion object {
+ fun fromStorage(value: String?): MoreLikeThisSourcePreference =
+ entries.firstOrNull { it.name == value } ?: DEFAULT_MORE_LIKE_THIS_SOURCE
+ }
+}
+
+val DEFAULT_MORE_LIKE_THIS_SOURCE: MoreLikeThisSourcePreference = MoreLikeThisSourcePreference.TRAKT
+
data class TraktSettingsUiState(
val watchProgressSource: WatchProgressSource = DEFAULT_WATCH_PROGRESS_SOURCE,
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
val librarySourceMode: LibrarySourceMode = DEFAULT_LIBRARY_SOURCE_MODE,
+ val moreLikeThisSource: MoreLikeThisSourcePreference = DEFAULT_MORE_LIKE_THIS_SOURCE,
)
@Serializable
@@ -52,6 +66,7 @@ private data class StoredTraktSettings(
val watchProgressSource: String? = null,
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
val librarySourceMode: String? = null,
+ val moreLikeThisSource: String? = null,
)
object TraktSettingsRepository {
@@ -101,6 +116,13 @@ object TraktSettingsRepository {
persist()
}
+ fun setMoreLikeThisSource(source: MoreLikeThisSourcePreference) {
+ ensureLoaded()
+ if (_uiState.value.moreLikeThisSource == source) return
+ _uiState.value = _uiState.value.copy(moreLikeThisSource = source)
+ persist()
+ }
+
private fun loadFromDisk() {
hasLoaded = true
@@ -119,6 +141,7 @@ object TraktSettingsRepository {
watchProgressSource = WatchProgressSource.fromStorage(stored.watchProgressSource),
continueWatchingDaysCap = normalizeTraktContinueWatchingDaysCap(stored.continueWatchingDaysCap),
librarySourceMode = librarySourceModeFromStorage(stored.librarySourceMode),
+ moreLikeThisSource = MoreLikeThisSourcePreference.fromStorage(stored.moreLikeThisSource),
)
} else {
TraktSettingsUiState()
@@ -132,6 +155,7 @@ object TraktSettingsRepository {
watchProgressSource = _uiState.value.watchProgressSource.name,
continueWatchingDaysCap = _uiState.value.continueWatchingDaysCap,
librarySourceMode = _uiState.value.librarySourceMode.name,
+ moreLikeThisSource = _uiState.value.moreLikeThisSource.name,
),
),
)
@@ -164,3 +188,8 @@ fun shouldUseTraktLibrary(
isAuthenticated: Boolean,
source: LibrarySourceMode,
): Boolean = effectiveLibrarySourceMode(isAuthenticated, source) == LibrarySourceMode.TRAKT
+
+fun shouldUseTraktMoreLikeThis(
+ isAuthenticated: Boolean,
+ source: MoreLikeThisSourcePreference,
+): Boolean = isAuthenticated && source == MoreLikeThisSourcePreference.TRAKT
diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepositoryTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepositoryTest.kt
index f504fcc8e..c051244ae 100644
--- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepositoryTest.kt
+++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktSettingsRepositoryTest.kt
@@ -34,6 +34,19 @@ class TraktSettingsRepositoryTest {
assertEquals(LibrarySourceMode.LOCAL, librarySourceModeFromStorage("LOCAL"))
}
+ @Test
+ fun `more like this source defaults to Trakt for unset or invalid storage`() {
+ assertEquals(MoreLikeThisSourcePreference.TRAKT, MoreLikeThisSourcePreference.fromStorage(null))
+ assertEquals(MoreLikeThisSourcePreference.TRAKT, MoreLikeThisSourcePreference.fromStorage(""))
+ assertEquals(MoreLikeThisSourcePreference.TRAKT, MoreLikeThisSourcePreference.fromStorage("not-a-source"))
+ }
+
+ @Test
+ fun `more like this source restores valid storage values`() {
+ assertEquals(MoreLikeThisSourcePreference.TRAKT, MoreLikeThisSourcePreference.fromStorage("TRAKT"))
+ assertEquals(MoreLikeThisSourcePreference.TMDB, MoreLikeThisSourcePreference.fromStorage("TMDB"))
+ }
+
@Test
fun `continue watching cap normalizes finite windows and all history`() {
assertEquals(TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL, normalizeTraktContinueWatchingDaysCap(0))
@@ -64,4 +77,26 @@ class TraktSettingsRepositoryTest {
effectiveLibrarySourceMode(isAuthenticated = true, source = LibrarySourceMode.TRAKT),
)
}
+
+ @Test
+ fun `Trakt more like this is active only when authenticated and selected`() {
+ assertFalse(
+ shouldUseTraktMoreLikeThis(
+ isAuthenticated = false,
+ source = MoreLikeThisSourcePreference.TRAKT,
+ ),
+ )
+ assertFalse(
+ shouldUseTraktMoreLikeThis(
+ isAuthenticated = true,
+ source = MoreLikeThisSourcePreference.TMDB,
+ ),
+ )
+ assertTrue(
+ shouldUseTraktMoreLikeThis(
+ isAuthenticated = true,
+ source = MoreLikeThisSourcePreference.TRAKT,
+ ),
+ )
+ }
}