This commit is contained in:
Laskco 2026-07-26 04:47:48 +05:30 committed by GitHub
commit 098563bb1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 154 additions and 29 deletions

View file

@ -38,6 +38,7 @@ data class MetaDetails(
val trailers: List<MetaTrailer> = emptyList(),
val links: List<MetaLink> = emptyList(),
val videos: List<MetaVideo> = emptyList(),
val imdbId: String? = null,
)
enum class MoreLikeThisSource {

View file

@ -55,6 +55,7 @@ internal object MetaDetailsParser {
trailers = meta.trailers(),
links = links,
videos = meta.videos(),
imdbId = meta.string("imdb_id") ?: meta.string("imdbId"),
)
}

View file

@ -261,11 +261,17 @@ fun MetaDetailsScreen(
return@LaunchedEffect
}
val imdbId = extractImdbId(metaForRatings.id) ?: extractImdbId(id)
val imdbId = metaForRatings.imdbId?.takeIf { it.startsWith("tt", ignoreCase = true) }
?: extractImdbId(metaForRatings.id)
?: extractImdbId(id)
val tmdbId = extractTmdbId(metaForRatings.id)
?: extractTmdbId(id)
?: TmdbService.ensureTmdbId(metaForRatings.id, metaForRatings.type)?.toIntOrNull()
?: TmdbService.ensureTmdbId(id, type)?.toIntOrNull()
?: TmdbService.ensureTmdbId(
metaForRatings.id,
metaForRatings.type,
metaForRatings.imdbId,
)?.toIntOrNull()
?: TmdbService.ensureTmdbId(id, type, metaForRatings.imdbId)?.toIntOrNull()
if (imdbId == null && tmdbId == null) {
episodeImdbRatings = emptyMap()
@ -1565,6 +1571,7 @@ private fun MetaDetails.toMetaPreview(): MetaPreview =
releaseInfo = releaseInfo,
imdbRating = imdbRating,
genres = genres,
imdbId = imdbId,
)
private fun LazyListScope.configuredMetaSectionItems(

View file

@ -57,6 +57,7 @@ internal object HomeCatalogParser {
genres = meta.array("genres").mapNotNull { genre ->
genre.jsonPrimitive.contentOrNull?.takeIf { it.isNotBlank() }
},
imdbId = meta.string("imdb_id") ?: meta.string("imdbId"),
)
if (seenKeys.add(item.stableKey())) {
add(item)

View file

@ -18,6 +18,7 @@ data class MetaPreview(
val voteCount: Int? = null,
val imdbRating: String? = null,
val genres: List<String> = emptyList(),
val imdbId: String? = null,
)
fun MetaPreview.stableKey(): String = "$type:$id"

View file

@ -49,7 +49,9 @@ object MdbListMetadataService {
if (!settings.enabled) return false
if (settings.apiKey.trim().isBlank()) return false
if (settings.enabledProvidersInPriorityOrder().isEmpty()) return false
return extractImdbId(meta.id) != null || extractImdbId(fallbackItemId) != null
return validImdbId(meta.imdbId) != null ||
extractImdbId(meta.id) != null ||
extractImdbId(fallbackItemId) != null
}
suspend fun enrichMeta(
@ -62,7 +64,8 @@ object MdbListMetadataService {
}
val apiKey = settings.apiKey.trim()
val imdbId = extractImdbId(meta.id)
val imdbId = validImdbId(meta.imdbId)
?: extractImdbId(meta.id)
?: extractImdbId(fallbackItemId)
?: return meta.copy(externalRatings = emptyList())
val mediaType = toMdbListMediaType(meta.type)
@ -108,6 +111,11 @@ object MdbListMetadataService {
ratings
}
private fun validImdbId(value: String?): String? =
value
?.trim()
?.takeIf { imdbRegex.matches(it) }
private suspend fun fetchProviderRating(
imdbId: String,
mediaType: String,

View file

@ -524,8 +524,8 @@ object TmdbMetadataService {
if (!settings.enabled || !settings.hasApiKey) return meta
val tmdbType = normalizeMetaType(meta.type)
val tmdbId = TmdbService.ensureTmdbId(meta.id, tmdbType)
?: TmdbService.ensureTmdbId(fallbackItemId, tmdbType)
val tmdbId = TmdbService.ensureTmdbId(meta.id, tmdbType, meta.imdbId)
?: TmdbService.ensureTmdbId(fallbackItemId, tmdbType, meta.imdbId)
?: return meta
val needsEpisodes = (

View file

@ -15,22 +15,26 @@ object TmdbService {
private val tmdbToImdbCache = linkedMapOf<String, String>()
private val cacheMutex = Mutex()
suspend fun ensureTmdbId(videoId: String, mediaType: String): String? {
suspend fun ensureTmdbId(
videoId: String,
mediaType: String,
fallbackImdbId: String? = null,
): String? {
val apiKey = currentApiKey() ?: return null
val normalizedType = normalizeMediaType(mediaType)
val normalized = videoId
.removePrefix("tmdb:")
.removePrefix("movie:")
.removePrefix("series:")
.substringBefore(':')
.substringBefore('/')
.trim()
tmdbLookupCandidates(videoId, fallbackImdbId).forEach { candidate ->
if (candidate.all(Char::isDigit)) return candidate
if (candidate.startsWith("tt", ignoreCase = true)) {
imdbToTmdb(
imdbId = candidate,
mediaType = normalizedType,
apiKey = apiKey,
)?.let { return it }
}
}
if (normalized.isBlank()) return null
if (normalized.all(Char::isDigit)) return normalized
if (!normalized.startsWith("tt", ignoreCase = true)) return null
return imdbToTmdb(imdbId = normalized, mediaType = mediaType, apiKey = apiKey)
return null
}
suspend fun tmdbToImdb(tmdbId: Int, mediaType: String): String? {
@ -110,6 +114,24 @@ object TmdbService {
}
}
internal fun tmdbLookupCandidates(
videoId: String,
fallbackImdbId: String?,
): List<String> =
listOfNotNull(videoId, fallbackImdbId)
.mapNotNull { rawId ->
rawId
.trim()
.removePrefix("tmdb:")
.removePrefix("movie:")
.removePrefix("series:")
.substringBefore(':')
.substringBefore('/')
.trim()
.takeIf(String::isNotBlank)
}
.distinct()
internal fun buildTmdbUrl(
endpoint: String,
apiKey: String,

View file

@ -106,12 +106,19 @@ object TraktRelatedRepository {
): ResolvedRelatedTarget? {
val type = resolveRelatedType(meta = meta, fallbackItemType = fallbackItemType) ?: return null
resolveDirectPathId(meta.id)?.let { return ResolvedRelatedTarget(type, it) }
resolveDirectPathId(meta.imdbId)?.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()
?: TmdbService.ensureTmdbId(meta.id, meta.type, meta.imdbId)?.toIntOrNull()
?: fallbackItemId?.let {
TmdbService.ensureTmdbId(
it,
fallbackItemType ?: meta.type,
meta.imdbId,
)
}?.toIntOrNull()
?: return null
return resolveViaTraktSearch(type = type, tmdbId = tmdbId, headers = headers)

View file

@ -22,7 +22,8 @@ class MetaDetailsParserTest {
{
"id": "mal:62516",
"type": "series",
"name": "The Fragrant Flower Blooms with Dignity"
"name": "The Fragrant Flower Blooms with Dignity",
"imdb_id": "tt30217403"
}
""".trimIndent(),
)
@ -30,6 +31,7 @@ class MetaDetailsParserTest {
assertEquals("mal:62516", result.id)
assertEquals("series", result.type)
assertEquals("The Fragrant Flower Blooms with Dignity", result.name)
assertEquals("tt30217403", result.imdbId)
}
@Test

View file

@ -9,12 +9,17 @@ class HomeCatalogParserTest {
fun `parse catalog response de-duplicates repeated metas but preserves raw count`() {
val result = HomeCatalogParser.parseCatalogResponse(
"""
{
"metas": [
{ "id": "mal:62516", "type": "series", "name": "A" },
{ "id": "mal:62516", "type": "series", "name": "A duplicate" },
{ "id": "mal:1", "type": "movie", "name": "B" }
]
{
"metas": [
{
"id": "mal:62516",
"type": "series",
"name": "A",
"imdb_id": "tt30217403"
},
{ "id": "mal:62516", "type": "series", "name": "A duplicate" },
{ "id": "mal:1", "type": "movie", "name": "B" }
]
}
""".trimIndent(),
)
@ -25,6 +30,7 @@ class HomeCatalogParserTest {
result.items.map { it.stableKey() },
)
assertEquals("A", result.items.first().name)
assertEquals("tt30217403", result.items.first().imdbId)
}
@Test

View file

@ -0,0 +1,29 @@
package com.nuvio.app.features.mdblist
import com.nuvio.app.features.details.MetaDetails
import kotlin.test.Test
import kotlin.test.assertTrue
class MdbListMetadataServiceTest {
@Test
fun `addon imdb alias enables ratings for anime id`() {
val meta = MetaDetails(
id = "mal:49894",
type = "series",
name = "Hero Classroom",
imdbId = "tt28254942",
)
assertTrue(
MdbListMetadataService.shouldFetchForMeta(
meta = meta,
fallbackItemId = meta.id,
settings = MdbListSettings(
enabled = true,
apiKey = "test",
),
),
)
}
}

View file

@ -0,0 +1,40 @@
package com.nuvio.app.features.tmdb
import kotlin.test.Test
import kotlin.test.assertEquals
class TmdbServiceTest {
@Test
fun `anime id keeps addon imdb alias as fallback candidate`() {
assertEquals(
listOf("mal", "tt28254942"),
tmdbLookupCandidates(
videoId = "mal:49894",
fallbackImdbId = "tt28254942",
),
)
}
@Test
fun `numeric tmdb id remains ahead of fallback imdb alias`() {
assertEquals(
listOf("228234", "tt28254942"),
tmdbLookupCandidates(
videoId = "tmdb:228234",
fallbackImdbId = "tt28254942",
),
)
}
@Test
fun `duplicate primary and fallback ids are resolved once`() {
assertEquals(
listOf("tt0133093"),
tmdbLookupCandidates(
videoId = "tt0133093:1:1",
fallbackImdbId = "tt0133093",
),
)
}
}