mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 07:21:58 +00:00
Merge 6e84b3ac36 into 70d3eee9d2
This commit is contained in:
commit
514b96bc02
2 changed files with 117 additions and 22 deletions
|
|
@ -5,10 +5,15 @@ import com.nuvio.app.features.addons.httpGetText
|
||||||
import com.nuvio.app.features.catalog.CatalogPage
|
import com.nuvio.app.features.catalog.CatalogPage
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.home.PosterShape
|
import com.nuvio.app.features.home.PosterShape
|
||||||
|
import com.nuvio.app.features.tmdb.TmdbLocalizedArtwork
|
||||||
|
import com.nuvio.app.features.tmdb.TmdbMetadataService
|
||||||
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
|
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
|
||||||
import com.nuvio.app.features.tmdb.buildTmdbUrl
|
import com.nuvio.app.features.tmdb.buildTmdbUrl
|
||||||
import com.nuvio.app.features.tmdb.normalizeTmdbLanguage
|
import com.nuvio.app.features.tmdb.normalizeTmdbLanguage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -228,8 +233,19 @@ object TmdbCollectionSourceResolver {
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
query = mapOf("language" to language),
|
query = mapOf("language" to language),
|
||||||
) ?: error("TMDB collection not found")
|
) ?: error("TMDB collection not found")
|
||||||
val items = body.parts.orEmpty()
|
val parts = body.parts.orEmpty()
|
||||||
.mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) }
|
val items = coroutineScope {
|
||||||
|
parts.map { part ->
|
||||||
|
async {
|
||||||
|
val artwork = TmdbMetadataService.fetchLocalizedArtwork(
|
||||||
|
tmdbId = part.id,
|
||||||
|
mediaType = "movie",
|
||||||
|
language = language,
|
||||||
|
)
|
||||||
|
part.toPreview(TmdbCollectionMediaType.MOVIE, artwork)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
}
|
||||||
.sortedFor(source.sortBy)
|
.sortedFor(source.sortBy)
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
return CatalogPage(items = items, rawItemCount = items.size, nextSkip = null)
|
return CatalogPage(items = items, rawItemCount = items.size, nextSkip = null)
|
||||||
|
|
@ -406,14 +422,21 @@ object TmdbCollectionSourceResolver {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TmdbCollectionPart.toPreview(mediaType: TmdbCollectionMediaType): MetaPreview? {
|
private fun TmdbCollectionPart.toPreview(
|
||||||
|
mediaType: TmdbCollectionMediaType,
|
||||||
|
localizedArtwork: TmdbLocalizedArtwork = TmdbLocalizedArtwork(null, null),
|
||||||
|
): MetaPreview? {
|
||||||
val title = title?.takeIf { it.isNotBlank() } ?: return null
|
val title = title?.takeIf { it.isNotBlank() } ?: return null
|
||||||
|
val localizedPoster = TmdbMetadataService.imageUrl(localizedArtwork.poster, "w500")
|
||||||
|
val localizedBackdrop = TmdbMetadataService.imageUrl(localizedArtwork.backdrop, "w1280")
|
||||||
return MetaPreview(
|
return MetaPreview(
|
||||||
id = "tmdb:$id",
|
id = "tmdb:$id",
|
||||||
type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie",
|
type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie",
|
||||||
name = title,
|
name = title,
|
||||||
poster = imageUrl(posterPath, "w500") ?: imageUrl(backdropPath, "w780"),
|
poster = localizedPoster
|
||||||
banner = imageUrl(backdropPath, "w1280"),
|
?: imageUrl(posterPath, "w500")
|
||||||
|
?: imageUrl(backdropPath, "w780"),
|
||||||
|
banner = localizedBackdrop ?: imageUrl(backdropPath, "w1280"),
|
||||||
posterShape = PosterShape.Poster,
|
posterShape = PosterShape.Poster,
|
||||||
description = overview?.takeIf { it.isNotBlank() },
|
description = overview?.takeIf { it.isNotBlank() },
|
||||||
releaseInfo = releaseDate?.take(4),
|
releaseInfo = releaseDate?.take(4),
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ object TmdbMetadataService {
|
||||||
private val entityHeaderCache = mutableMapOf<String, TmdbEntityHeader>()
|
private val entityHeaderCache = mutableMapOf<String, TmdbEntityHeader>()
|
||||||
private val entityRailCache = mutableMapOf<String, List<MetaPreview>>()
|
private val entityRailCache = mutableMapOf<String, List<MetaPreview>>()
|
||||||
private val previewArtworkCache = mutableMapOf<String, TmdbPreviewArtwork>()
|
private val previewArtworkCache = mutableMapOf<String, TmdbPreviewArtwork>()
|
||||||
|
private val localizedArtworkCache = mutableMapOf<String, TmdbLocalizedArtwork>()
|
||||||
|
|
||||||
suspend fun fetchPersonDetail(
|
suspend fun fetchPersonDetail(
|
||||||
personId: Int,
|
personId: Int,
|
||||||
|
|
@ -530,6 +531,56 @@ object TmdbMetadataService {
|
||||||
val logo: String?,
|
val logo: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a localized poster + backdrop for a movie or TV id from
|
||||||
|
* `/{mediaType}/{id}/images?include_image_language=<lang>,null,en`.
|
||||||
|
*
|
||||||
|
* TMDB's collection / list / credits endpoints only return one default
|
||||||
|
* poster path per part — usually the English-favored one regardless of the
|
||||||
|
* `language=fr` query param. Calling `/images` separately is the only way
|
||||||
|
* to get artwork actually localized in the user's language. Results cached
|
||||||
|
* per `(tmdbId, mediaType, language)` to avoid hammering TMDB.
|
||||||
|
*
|
||||||
|
* For English locales the call is skipped: the default poster is already
|
||||||
|
* the English-favored one.
|
||||||
|
*/
|
||||||
|
internal suspend fun fetchLocalizedArtwork(
|
||||||
|
tmdbId: Int,
|
||||||
|
mediaType: String,
|
||||||
|
language: String,
|
||||||
|
): TmdbLocalizedArtwork = withContext(Dispatchers.Default) {
|
||||||
|
val normalizedLanguage = normalizeTmdbLanguage(language)
|
||||||
|
if (normalizedLanguage.startsWith("en", ignoreCase = true)) {
|
||||||
|
return@withContext TmdbLocalizedArtwork(poster = null, backdrop = null)
|
||||||
|
}
|
||||||
|
val cacheKey = "$tmdbId:$mediaType:$normalizedLanguage"
|
||||||
|
localizedArtworkCache[cacheKey]?.let { return@withContext it }
|
||||||
|
|
||||||
|
val includeImageLanguage = buildString {
|
||||||
|
append(normalizedLanguage.substringBefore("-"))
|
||||||
|
append(",")
|
||||||
|
append(normalizedLanguage)
|
||||||
|
append(",en,null")
|
||||||
|
}
|
||||||
|
val images = fetch<TmdbImagesResponse>(
|
||||||
|
endpoint = "$mediaType/$tmdbId/images",
|
||||||
|
query = mapOf("include_image_language" to includeImageLanguage),
|
||||||
|
)
|
||||||
|
val artwork = TmdbLocalizedArtwork(
|
||||||
|
poster = images?.posters.orEmpty().selectBestLocalizedImagePath(normalizedLanguage),
|
||||||
|
backdrop = images?.backdrops.orEmpty().selectBestLocalizedImagePath(normalizedLanguage),
|
||||||
|
)
|
||||||
|
localizedArtworkCache[cacheKey] = artwork
|
||||||
|
artwork
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an absolute TMDB image URL for the given relative `file_path`.
|
||||||
|
* Exposed for callers (e.g. collection resolvers) that need to construct
|
||||||
|
* URLs from paths returned by [fetchLocalizedArtwork].
|
||||||
|
*/
|
||||||
|
internal fun imageUrl(filePath: String?, size: String): String? = buildImageUrl(filePath, size)
|
||||||
|
|
||||||
private suspend fun fetchPreviewArtwork(
|
private suspend fun fetchPreviewArtwork(
|
||||||
tmdbId: Int,
|
tmdbId: Int,
|
||||||
mediaType: String,
|
mediaType: String,
|
||||||
|
|
@ -1074,24 +1125,38 @@ object TmdbMetadataService {
|
||||||
query = mapOf("language" to language),
|
query = mapOf("language" to language),
|
||||||
) ?: return null to emptyList()
|
) ?: return null to emptyList()
|
||||||
|
|
||||||
val items = response.parts
|
val sortedParts = response.parts
|
||||||
.sortedBy { it.releaseDate ?: "9999" }
|
.sortedBy { it.releaseDate ?: "9999" }
|
||||||
.mapNotNull { part ->
|
val items = coroutineScope {
|
||||||
val title = part.title?.trim()?.takeIf(String::isNotBlank) ?: return@mapNotNull null
|
sortedParts.map { part ->
|
||||||
MetaPreview(
|
async {
|
||||||
id = "tmdb:${part.id}",
|
val title = part.title?.trim()?.takeIf(String::isNotBlank) ?: return@async null
|
||||||
type = "movie",
|
val localized = fetchLocalizedArtwork(
|
||||||
name = title,
|
tmdbId = part.id,
|
||||||
poster = buildImageUrl(part.backdropPath, "w780")
|
mediaType = "movie",
|
||||||
?: buildImageUrl(part.posterPath, "w500"),
|
language = language,
|
||||||
banner = buildImageUrl(part.backdropPath, "w1280"),
|
)
|
||||||
posterShape = PosterShape.Landscape,
|
val poster = buildImageUrl(localized.backdrop, "w780")
|
||||||
description = part.overview?.trim()?.takeIf(String::isNotBlank),
|
?: buildImageUrl(part.backdropPath, "w780")
|
||||||
releaseInfo = part.releaseDate?.take(4),
|
?: buildImageUrl(localized.poster, "w500")
|
||||||
rawReleaseDate = part.releaseDate,
|
?: buildImageUrl(part.posterPath, "w500")
|
||||||
imdbRating = part.voteAverage?.formatRating(),
|
val banner = buildImageUrl(localized.backdrop, "w1280")
|
||||||
)
|
?: buildImageUrl(part.backdropPath, "w1280")
|
||||||
}
|
MetaPreview(
|
||||||
|
id = "tmdb:${part.id}",
|
||||||
|
type = "movie",
|
||||||
|
name = title,
|
||||||
|
poster = poster,
|
||||||
|
banner = banner,
|
||||||
|
posterShape = PosterShape.Landscape,
|
||||||
|
description = part.overview?.trim()?.takeIf(String::isNotBlank),
|
||||||
|
releaseInfo = part.releaseDate?.take(4),
|
||||||
|
rawReleaseDate = part.releaseDate,
|
||||||
|
imdbRating = part.voteAverage?.formatRating(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
val result = response.name?.trim()?.takeIf(String::isNotBlank) to items
|
val result = response.name?.trim()?.takeIf(String::isNotBlank) to items
|
||||||
collectionCache[cacheKey] = result
|
collectionCache[cacheKey] = result
|
||||||
|
|
@ -1606,6 +1671,13 @@ private data class TmdbCrewMember(
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class TmdbImagesResponse(
|
private data class TmdbImagesResponse(
|
||||||
val logos: List<TmdbImage> = emptyList(),
|
val logos: List<TmdbImage> = emptyList(),
|
||||||
|
val posters: List<TmdbImage> = emptyList(),
|
||||||
|
val backdrops: List<TmdbImage> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class TmdbLocalizedArtwork(
|
||||||
|
val poster: String?,
|
||||||
|
val backdrop: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue