Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion app/src/main/kotlin/com/arflix/tv/data/api/TmdbApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,16 @@ data class TmdbMovieDetails(
val budget: Long = 0,
val genres: List<TmdbGenre> = emptyList(),
val status: String? = null,
val adult: Boolean = false
val adult: Boolean = false,
@SerializedName("belongs_to_collection") val belongsToCollection: TmdbCollectionRef? = null
)

/** Reference to a TMDB collection (franchise) returned inside movie/TV details. */
data class TmdbCollectionRef(
val id: Int = 0,
val name: String? = null,
@SerializedName("poster_path") val posterPath: String? = null,
@SerializedName("backdrop_path") val backdropPath: String? = null
)

data class TmdbTvDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2395,7 +2395,33 @@ class MediaRepository @Inject constructor(
detailsCache[cacheKey] = CacheEntry(item, System.currentTimeMillis())
return item
}


/**
* Get the TMDB collection (franchise) reference for a movie.
* Calls /movie/{id} directly to access the `belongs_to_collection` field,
* which is discarded by getMovieDetails() → toMediaItem().
* The response is cached by OkHttp, making the redundant call negligible.
*/
suspend fun getMovieCollectionRef(movieId: Int): com.arflix.tv.data.api.TmdbCollectionRef? {
return runCatching {
tmdbApi.getMovieDetails(movieId, apiKey, language = contentLanguage).belongsToCollection
}.getOrNull()
}

/**
* Fetch all movies in a TMDB collection (franchise).
* Calls TMDB /collection/{id} and maps the parts array to Movie MediaItems.
* Used by the Details page to show franchise rows (e.g. "Cars Collection").
*/
suspend fun getTmdbCollectionItems(collectionId: Int): List<MediaItem> {
val response = runCatching {
tmdbApi.getTmdbCollection(collectionId, apiKey, language = contentLanguage)
}.getOrNull() ?: return emptyList()
return response.parts
.sortedBy { it.releaseDate.orEmpty() }
.map { it.toMediaItem(MediaType.MOVIE) }
}

/**
* Get season episodes with Trakt watched status
*/
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/com/arflix/tv/navigation/AppNavigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ fun AppNavigation(
onNavigateToDetails = { type, id ->
navController.navigate(Screen.Details.createRoute(type, id))
},
onNavigateToCollection = { catalogId ->
navController.navigate(Screen.CollectionDetails.createRoute(catalogId))
},
onNavigateToHome = {
navigateHome()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ import androidx.tv.foundation.lazy.grid.itemsIndexed
import androidx.tv.foundation.lazy.grid.rememberTvLazyGridState
import coil.compose.AsyncImage
import com.arflix.tv.data.model.CatalogConfig
import com.arflix.tv.data.model.CatalogKind
import com.arflix.tv.data.model.CatalogSourceType
import com.arflix.tv.data.model.CollectionGroupKind
import com.arflix.tv.data.model.CollectionSourceConfig
import com.arflix.tv.data.model.CollectionSourceKind
import com.arflix.tv.data.model.CollectionTileShape
import com.arflix.tv.data.model.MediaItem
import com.arflix.tv.data.model.MediaType
import com.arflix.tv.data.repository.CatalogRepository
Expand Down Expand Up @@ -118,6 +123,7 @@ class CollectionDetailsViewModel @Inject constructor(
const val FIRST_PAGE = 8
const val PAGE_STEP = 12
const val BACKGROUND_PREFETCH_DELAY_MS = 350L
const val TMDB_COLLECTION_PREFIX = "tmdb_collection:"
}

fun load(catalogId: String) {
Expand All @@ -130,6 +136,7 @@ class CollectionDetailsViewModel @Inject constructor(

_uiState.value = CollectionDetailsUiState(isLoadingMovies = true, isLoadingSeries = true)
val catalog = catalogRepository.getCatalogs().firstOrNull { it.id == catalogId }
?: syntheticTmdbCollectionCatalog(catalogId)
if (catalog == null) {
_uiState.value = CollectionDetailsUiState(
isLoadingMovies = false,
Expand Down Expand Up @@ -167,6 +174,34 @@ class CollectionDetailsViewModel @Inject constructor(
}
}

private fun syntheticTmdbCollectionCatalog(catalogId: String): CatalogConfig? {
if (!catalogId.startsWith(TMDB_COLLECTION_PREFIX)) return null

val payload = catalogId.removePrefix(TMDB_COLLECTION_PREFIX)
val collectionId = payload.substringBefore(":").toIntOrNull() ?: return null
val collectionName = payload.substringAfter(":", missingDelimiterValue = "")
.trim()
.takeIf { it.isNotBlank() }
?: "Collection"

return CatalogConfig(
id = catalogId,
title = collectionName,
sourceType = CatalogSourceType.PREINSTALLED,
isPreinstalled = true,
kind = CatalogKind.COLLECTION,
collectionGroup = CollectionGroupKind.FRANCHISE,
collectionTileShape = CollectionTileShape.POSTER,
collectionSources = listOf(
CollectionSourceConfig(
kind = CollectionSourceKind.TMDB_COLLECTION,
mediaType = "movie",
tmdbCollectionId = collectionId
)
)
)
}

private suspend fun loadInitialTab(catalog: CatalogConfig, tab: CollectionTab) {
val page = runCatching {
mediaRepository.loadCollectionCatalogPage(
Expand Down
Loading