mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
feat: implement Trakt image utilities and remove hydration
This commit is contained in:
parent
1119456ae0
commit
c962a0ac24
5 changed files with 109 additions and 295 deletions
|
|
@ -0,0 +1,60 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private val traktHostPattern = Regex("""^[a-z0-9.-]*trakt\.tv/""", RegexOption.IGNORE_CASE)
|
||||
|
||||
@Serializable
|
||||
internal data class TraktImagesDto(
|
||||
val fanart: List<String>? = null,
|
||||
val poster: List<String>? = null,
|
||||
val logo: List<String>? = null,
|
||||
val clearart: List<String>? = null,
|
||||
val banner: List<String>? = null,
|
||||
val thumb: List<String>? = null,
|
||||
)
|
||||
|
||||
internal fun List<String>?.firstTraktImageUrl(): String? {
|
||||
return orEmpty()
|
||||
.firstOrNull { it.isNotBlank() }
|
||||
?.toTraktImageUrl()
|
||||
}
|
||||
|
||||
internal fun String.toTraktImageUrl(): String {
|
||||
val normalized = trim()
|
||||
return when {
|
||||
normalized.startsWith("https://", ignoreCase = true) -> normalized
|
||||
normalized.startsWith("http://", ignoreCase = true) -> "https://${normalized.substringAfter("://")}"
|
||||
normalized.startsWith("//") -> "https:$normalized"
|
||||
traktHostPattern.containsMatchIn(normalized) -> "https://$normalized"
|
||||
else -> normalized
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TraktImagesDto?.traktPosterUrl(): String? = this?.poster.firstTraktImageUrl()
|
||||
|
||||
internal fun TraktImagesDto?.traktFanartUrl(): String? = this?.fanart.firstTraktImageUrl()
|
||||
|
||||
internal fun TraktImagesDto?.traktLogoUrl(): String? = this?.logo.firstTraktImageUrl()
|
||||
|
||||
internal fun TraktImagesDto?.traktClearartUrl(): String? = this?.clearart.firstTraktImageUrl()
|
||||
|
||||
internal fun TraktImagesDto?.traktBannerUrl(): String? = this?.banner.firstTraktImageUrl()
|
||||
|
||||
internal fun TraktImagesDto?.traktThumbUrl(): String? = this?.thumb.firstTraktImageUrl()
|
||||
|
||||
internal fun TraktImagesDto?.traktBestPosterUrl(): String? {
|
||||
return traktPosterUrl() ?: traktFanartUrl()
|
||||
}
|
||||
|
||||
internal fun TraktImagesDto?.traktBestBackdropUrl(): String? {
|
||||
return traktFanartUrl() ?: traktBannerUrl() ?: traktThumbUrl() ?: traktPosterUrl()
|
||||
}
|
||||
|
||||
internal fun TraktImagesDto?.traktBestLandscapeUrl(): String? {
|
||||
return traktThumbUrl() ?: traktFanartUrl() ?: traktBannerUrl() ?: traktPosterUrl()
|
||||
}
|
||||
|
||||
internal fun TraktImagesDto?.traktBestLogoUrl(): String? {
|
||||
return traktLogoUrl() ?: traktClearartUrl()
|
||||
}
|
||||
|
|
@ -1,20 +1,15 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.addons.httpGetTextWithHeaders
|
||||
import com.nuvio.app.features.addons.httpPostJsonWithHeaders
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.library.LibraryItem
|
||||
import com.nuvio.app.features.tmdb.TmdbService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -28,7 +23,6 @@ import org.jetbrains.compose.resources.getString
|
|||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
|
@ -38,8 +32,6 @@ import kotlinx.serialization.json.Json
|
|||
private const val BASE_URL = "https://api.trakt.tv"
|
||||
private const val WATCHLIST_KEY = "trakt:watchlist"
|
||||
private const val PERSONAL_LIST_PREFIX = "trakt:list:"
|
||||
private const val METADATA_FETCH_TIMEOUT_MS = 3_500L
|
||||
private const val METADATA_FETCH_CONCURRENCY = 5
|
||||
private const val LIST_FETCH_CONCURRENCY = 4
|
||||
private const val SNAPSHOT_CACHE_TTL_MS = 60_000L
|
||||
private const val LIST_TABS_CACHE_TTL_MS = 60_000L
|
||||
|
|
@ -68,7 +60,6 @@ object TraktLibraryRepository {
|
|||
|
||||
private var hasLoaded = false
|
||||
private val refreshMutex = Mutex()
|
||||
private var hydrationJob: Job? = null
|
||||
private var lastRefreshAtMs: Long = 0L
|
||||
private var lastListTabsRefreshAtMs: Long = 0L
|
||||
|
||||
|
|
@ -91,8 +82,6 @@ object TraktLibraryRepository {
|
|||
}
|
||||
|
||||
fun onProfileChanged() {
|
||||
hydrationJob?.cancel()
|
||||
hydrationJob = null
|
||||
hasLoaded = false
|
||||
lastRefreshAtMs = 0L
|
||||
lastListTabsRefreshAtMs = 0L
|
||||
|
|
@ -101,8 +90,6 @@ object TraktLibraryRepository {
|
|||
}
|
||||
|
||||
fun clearLocalState() {
|
||||
hydrationJob?.cancel()
|
||||
hydrationJob = null
|
||||
hasLoaded = false
|
||||
lastRefreshAtMs = 0L
|
||||
lastListTabsRefreshAtMs = 0L
|
||||
|
|
@ -154,8 +141,6 @@ object TraktLibraryRepository {
|
|||
return
|
||||
}
|
||||
|
||||
AddonRepository.initialize()
|
||||
|
||||
val headers = TraktAuthRepository.authorizedHeaders()
|
||||
if (headers == null) {
|
||||
_uiState.value = TraktLibraryUiState()
|
||||
|
|
@ -173,7 +158,6 @@ object TraktLibraryRepository {
|
|||
hasLoaded = true,
|
||||
errorMessage = null,
|
||||
)
|
||||
hydrateMissingMetadataAsync(_uiState.value)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
|
|
@ -195,7 +179,6 @@ object TraktLibraryRepository {
|
|||
errorMessage = null,
|
||||
)
|
||||
persistSnapshot(_uiState.value)
|
||||
hydrateMissingMetadataAsync(_uiState.value)
|
||||
lastRefreshAtMs = now
|
||||
}
|
||||
}
|
||||
|
|
@ -421,7 +404,6 @@ object TraktLibraryRepository {
|
|||
entriesByList = cached.entriesByList,
|
||||
)
|
||||
_uiState.value = state.copy(isLoading = false, errorMessage = null, hasLoaded = true)
|
||||
hydrateMissingMetadataAsync(_uiState.value)
|
||||
}
|
||||
|
||||
private fun persistSnapshot(state: TraktLibraryUiState) {
|
||||
|
|
@ -432,59 +414,6 @@ object TraktLibraryRepository {
|
|||
TraktLibraryStorage.savePayload(json.encodeToString(payload))
|
||||
}
|
||||
|
||||
private fun hydrateMissingMetadataAsync(state: TraktLibraryUiState) {
|
||||
if (state.entriesByList.isEmpty()) return
|
||||
if (state.allItems.none(::shouldHydrateTraktLibraryItem)) return
|
||||
|
||||
hydrationJob?.cancel()
|
||||
hydrationJob = scope.launch {
|
||||
val hydratedEntriesByList = runCatching {
|
||||
hydrateEntriesFromAddonMeta(state.entriesByList)
|
||||
}.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
log.w { "Background Trakt metadata hydration failed: ${error.message}" }
|
||||
}.getOrNull() ?: return@launch
|
||||
|
||||
refreshMutex.withLock {
|
||||
val current = _uiState.value
|
||||
if (current.entriesByList.isEmpty()) return@withLock
|
||||
|
||||
val mergedEntriesByList = mergeHydratedEntries(
|
||||
currentEntriesByList = current.entriesByList,
|
||||
hydratedEntriesByList = hydratedEntriesByList,
|
||||
)
|
||||
if (mergedEntriesByList == current.entriesByList) return@withLock
|
||||
|
||||
val rebuilt = rebuildUiState(
|
||||
listTabs = current.listTabs,
|
||||
entriesByList = mergedEntriesByList,
|
||||
).copy(
|
||||
isLoading = current.isLoading,
|
||||
hasLoaded = current.hasLoaded,
|
||||
errorMessage = current.errorMessage,
|
||||
)
|
||||
|
||||
_uiState.value = rebuilt
|
||||
persistSnapshot(rebuilt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeHydratedEntries(
|
||||
currentEntriesByList: Map<String, List<LibraryItem>>,
|
||||
hydratedEntriesByList: Map<String, List<LibraryItem>>,
|
||||
): Map<String, List<LibraryItem>> {
|
||||
val hydratedByContentKey = hydratedEntriesByList.values
|
||||
.flatten()
|
||||
.associateBy { contentKey(it.id, it.type) }
|
||||
|
||||
return currentEntriesByList.mapValues { (_, entries) ->
|
||||
entries.map { entry ->
|
||||
hydratedByContentKey[contentKey(entry.id, entry.type)] ?: entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchListTabs(headers: Map<String, String>): List<TraktListTab> {
|
||||
val watchlistTabs = listOf(
|
||||
TraktListTab(
|
||||
|
|
@ -544,83 +473,6 @@ object TraktLibraryRepository {
|
|||
entriesByList.toMap()
|
||||
}
|
||||
|
||||
private suspend fun hydrateEntriesFromAddonMeta(
|
||||
entriesByList: Map<String, List<LibraryItem>>,
|
||||
): Map<String, List<LibraryItem>> = coroutineScope {
|
||||
if (entriesByList.isEmpty()) return@coroutineScope entriesByList
|
||||
|
||||
val uniqueItems = entriesByList.values
|
||||
.flatten()
|
||||
.distinctBy { contentKey(it.id, it.type) }
|
||||
if (uniqueItems.isEmpty()) return@coroutineScope entriesByList
|
||||
|
||||
val semaphore = Semaphore(METADATA_FETCH_CONCURRENCY)
|
||||
val hydratedByKey = uniqueItems
|
||||
.map { item ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
val hydrated = hydrateItemFromAddonMeta(item)
|
||||
contentKey(item.id, item.type) to hydrated
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.toMap()
|
||||
|
||||
entriesByList.mapValues { (_, entries) ->
|
||||
entries.map { entry -> hydratedByKey[contentKey(entry.id, entry.type)] ?: entry }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hydrateItemFromAddonMeta(item: LibraryItem): LibraryItem {
|
||||
if (!shouldHydrateTraktLibraryItem(item)) {
|
||||
return item
|
||||
}
|
||||
|
||||
val typeCandidates = if (normalizeType(item.type) == "movie") {
|
||||
listOf("movie")
|
||||
} else {
|
||||
listOf("series", "tv")
|
||||
}
|
||||
|
||||
val idCandidates = buildList {
|
||||
add(item.id)
|
||||
if (item.id.startsWith("tmdb:")) {
|
||||
add(item.id.substringAfter(':'))
|
||||
}
|
||||
if (item.id.startsWith("trakt:")) {
|
||||
add(item.id.substringAfter(':'))
|
||||
}
|
||||
}.distinct()
|
||||
|
||||
if (idCandidates.isEmpty()) {
|
||||
return item
|
||||
}
|
||||
|
||||
for (type in typeCandidates) {
|
||||
for (id in idCandidates) {
|
||||
val meta = withTimeoutOrNull(METADATA_FETCH_TIMEOUT_MS) {
|
||||
MetaDetailsRepository.fetch(type = type, id = id)
|
||||
}
|
||||
if (meta == null) continue
|
||||
|
||||
val shouldOverrideName = item.name.isBlank() || item.name == item.id
|
||||
return item.copy(
|
||||
name = if (shouldOverrideName) meta.name else item.name,
|
||||
poster = item.poster.orValidImageUrl(meta.poster),
|
||||
banner = item.banner.orValidImageUrl(meta.background),
|
||||
logo = item.logo.orValidImageUrl(meta.logo),
|
||||
description = item.description.orIfBlank(meta.description),
|
||||
releaseInfo = item.releaseInfo.orIfBlank(meta.releaseInfo),
|
||||
imdbRating = item.imdbRating.orIfBlank(meta.imdbRating),
|
||||
genres = if (item.genres.isEmpty()) meta.genres else item.genres,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
private suspend fun fetchPersonalLists(headers: Map<String, String>): List<TraktListTab> {
|
||||
val payload = httpGetTextWithHeaders(
|
||||
url = "$BASE_URL/users/me/lists",
|
||||
|
|
@ -786,10 +638,9 @@ object TraktLibraryRepository {
|
|||
?: ids?.trakt?.let { "trakt:$it" }
|
||||
?: return null
|
||||
|
||||
val poster = media.images?.poster.firstNonBlankImageUrl()
|
||||
?: media.images?.fanart.firstNonBlankImageUrl()
|
||||
val banner = media.images?.banner.firstNonBlankImageUrl()
|
||||
val logo = media.images?.logo.firstNonBlankImageUrl()
|
||||
val poster = media.images.traktBestPosterUrl()
|
||||
val banner = media.images.traktBestBackdropUrl()
|
||||
val logo = media.images.traktBestLogoUrl()
|
||||
|
||||
val savedAt = item.listedAt
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
|
@ -829,34 +680,6 @@ object TraktLibraryRepository {
|
|||
return yearText.toIntOrNull()
|
||||
}
|
||||
|
||||
private fun String?.orIfBlank(fallback: String?): String? {
|
||||
val current = this?.trim().takeUnless { it.isNullOrBlank() }
|
||||
if (current != null) return current
|
||||
return fallback?.trim().takeUnless { it.isNullOrBlank() }
|
||||
}
|
||||
|
||||
private fun String?.orValidImageUrl(fallback: String?): String? {
|
||||
val current = this.normalizeImageUrl()
|
||||
if (current != null) return current
|
||||
return fallback.normalizeImageUrl()
|
||||
}
|
||||
|
||||
private fun List<String>?.firstNonBlankImageUrl(): String? {
|
||||
return this
|
||||
?.asSequence()
|
||||
?.mapNotNull { it.normalizeImageUrl() }
|
||||
?.firstOrNull()
|
||||
}
|
||||
|
||||
private fun String?.normalizeImageUrl(): String? {
|
||||
val value = this?.trim().takeUnless { it.isNullOrBlank() } ?: return null
|
||||
val normalized = if (value.startsWith("//")) "https:$value" else value
|
||||
return normalized.takeIf {
|
||||
it.startsWith("https://", ignoreCase = true) ||
|
||||
it.startsWith("http://", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
private val imdbRegex = Regex("tt\\d+")
|
||||
}
|
||||
|
||||
|
|
@ -866,11 +689,6 @@ private data class StoredTraktLibraryPayload(
|
|||
val entriesByList: Map<String, List<LibraryItem>> = emptyMap(),
|
||||
)
|
||||
|
||||
internal fun shouldHydrateTraktLibraryItem(item: LibraryItem): Boolean {
|
||||
val missingDisplayName = item.name.isBlank() || item.name == item.id
|
||||
return missingDisplayName || item.poster.isNullOrBlank() || item.releaseInfo.isNullOrBlank()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class TraktListSummaryDto(
|
||||
val name: String? = null,
|
||||
|
|
@ -902,14 +720,6 @@ private data class TraktMediaDto(
|
|||
val images: TraktImagesDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class TraktImagesDto(
|
||||
val fanart: List<String>? = null,
|
||||
val poster: List<String>? = null,
|
||||
val logo: List<String>? = null,
|
||||
val banner: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class TraktIdsDto(
|
||||
val trakt: Int? = null,
|
||||
|
|
|
|||
|
|
@ -301,49 +301,9 @@ object TraktPublicListSourceResolver {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun List<String>?.firstTraktImageUrl(): String? {
|
||||
return orEmpty()
|
||||
.firstOrNull { it.isNotBlank() }
|
||||
?.toTraktImageUrl()
|
||||
}
|
||||
|
||||
internal fun String.toTraktImageUrl(): String {
|
||||
val normalized = trim()
|
||||
return when {
|
||||
normalized.startsWith("https://", ignoreCase = true) -> normalized
|
||||
normalized.startsWith("http://", ignoreCase = true) -> "https://${normalized.substringAfter("://")}"
|
||||
normalized.startsWith("//") -> "https:$normalized"
|
||||
traktHostPattern.containsMatchIn(normalized) -> "https://$normalized"
|
||||
else -> normalized
|
||||
}
|
||||
}
|
||||
|
||||
private fun PublicTraktImagesDto?.traktPosterUrl(): String? = this?.poster.firstTraktImageUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktFanartUrl(): String? = this?.fanart.firstTraktImageUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktLogoUrl(): String? = this?.logo.firstTraktImageUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktClearartUrl(): String? = this?.clearart.firstTraktImageUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktBannerUrl(): String? = this?.banner.firstTraktImageUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktThumbUrl(): String? = this?.thumb.firstTraktImageUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktBestPosterUrl(): String? =
|
||||
traktPosterUrl() ?: traktFanartUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktBestBackdropUrl(): String? =
|
||||
traktFanartUrl() ?: traktBannerUrl() ?: traktThumbUrl() ?: traktPosterUrl()
|
||||
|
||||
private fun PublicTraktImagesDto?.traktBestLogoUrl(): String? =
|
||||
traktLogoUrl() ?: traktClearartUrl()
|
||||
|
||||
private fun Double.formatRating(): String =
|
||||
((this * 10).roundToInt() / 10.0).toString()
|
||||
|
||||
private val traktHostPattern = Regex("""^[a-z0-9.-]*trakt\.tv/""", RegexOption.IGNORE_CASE)
|
||||
|
||||
@Serializable
|
||||
private data class PublicTraktSearchResultDto(
|
||||
val type: String? = null,
|
||||
|
|
@ -404,7 +364,7 @@ private data class PublicTraktMovieDto(
|
|||
val released: String? = null,
|
||||
val rating: Double? = null,
|
||||
val genres: List<String>? = null,
|
||||
val images: PublicTraktImagesDto? = null,
|
||||
val images: TraktImagesDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -416,15 +376,5 @@ private data class PublicTraktShowDto(
|
|||
@SerialName("first_aired") val firstAired: String? = null,
|
||||
val rating: Double? = null,
|
||||
val genres: List<String>? = null,
|
||||
val images: PublicTraktImagesDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class PublicTraktImagesDto(
|
||||
val fanart: List<String>? = null,
|
||||
val poster: List<String>? = null,
|
||||
val logo: List<String>? = null,
|
||||
val clearart: List<String>? = null,
|
||||
val banner: List<String>? = null,
|
||||
val thumb: List<String>? = null,
|
||||
val images: TraktImagesDto? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class TraktImageUtilsTest {
|
||||
|
||||
@Test
|
||||
fun normalizesTraktHostedImageUrls() {
|
||||
assertEquals(
|
||||
"https://media.trakt.tv/images/movies/poster.jpg.webp",
|
||||
listOf("media.trakt.tv/images/movies/poster.jpg.webp").firstTraktImageUrl(),
|
||||
)
|
||||
assertEquals(
|
||||
"https://media.trakt.tv/images/movies/poster.jpg.webp",
|
||||
listOf("//media.trakt.tv/images/movies/poster.jpg.webp").firstTraktImageUrl(),
|
||||
)
|
||||
assertEquals(
|
||||
"https://media.trakt.tv/images/movies/poster.jpg.webp",
|
||||
listOf("http://media.trakt.tv/images/movies/poster.jpg.webp").firstTraktImageUrl(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectsBestTraktImages() {
|
||||
val images = TraktImagesDto(
|
||||
fanart = listOf("media.trakt.tv/images/movies/fanart.jpg.webp"),
|
||||
logo = listOf("media.trakt.tv/images/movies/logo.png.webp"),
|
||||
thumb = listOf("media.trakt.tv/images/movies/thumb.jpg.webp"),
|
||||
)
|
||||
|
||||
assertEquals("https://media.trakt.tv/images/movies/fanart.jpg.webp", images.traktBestPosterUrl())
|
||||
assertEquals("https://media.trakt.tv/images/movies/fanart.jpg.webp", images.traktBestBackdropUrl())
|
||||
assertEquals("https://media.trakt.tv/images/movies/thumb.jpg.webp", images.traktBestLandscapeUrl())
|
||||
assertEquals("https://media.trakt.tv/images/movies/logo.png.webp", images.traktBestLogoUrl())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsNullWhenTraktImagesAreMissing() {
|
||||
assertNull(emptyList<String>().firstTraktImageUrl())
|
||||
assertNull(TraktImagesDto().traktBestPosterUrl())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
import com.nuvio.app.features.library.LibraryItem
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TraktLibraryRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun `hydration skips items that already have core library data`() {
|
||||
val item = LibraryItem(
|
||||
id = "tt1234567",
|
||||
type = "movie",
|
||||
name = "Example",
|
||||
poster = "https://image.tmdb.org/t/p/w500/poster.jpg",
|
||||
banner = null,
|
||||
logo = null,
|
||||
description = null,
|
||||
releaseInfo = "2024",
|
||||
imdbRating = null,
|
||||
genres = emptyList(),
|
||||
posterShape = PosterShape.Poster,
|
||||
savedAtEpochMs = 1L,
|
||||
)
|
||||
|
||||
assertFalse(shouldHydrateTraktLibraryItem(item))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hydration keeps filling missing poster metadata`() {
|
||||
val item = LibraryItem(
|
||||
id = "tt7654321",
|
||||
type = "series",
|
||||
name = "Example Show",
|
||||
poster = null,
|
||||
banner = null,
|
||||
logo = null,
|
||||
description = "",
|
||||
releaseInfo = "2025",
|
||||
imdbRating = null,
|
||||
genres = emptyList(),
|
||||
posterShape = PosterShape.Poster,
|
||||
savedAtEpochMs = 1L,
|
||||
)
|
||||
|
||||
assertTrue(shouldHydrateTraktLibraryItem(item))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue