Implementation

Loading catalogs#

This guide covers how to expose catalogs (home-page sections) using the capability-first CatalogProviderApi.

It assumes you’ve read the paging container section in the search guide: PaginatedResponse.

Overview#

Catalogs represent curated collections of media content shown on the home screen.

To support catalogs, expose a CatalogProviderApi from your ProviderPlugin:

class TestProviderPlugin : ProviderPlugin() {
    private val client by lazy { OkHttpClient() }
 
    private val catalogApi by lazy {
        TestCatalogApi(
            client = client,
            providerId = id,
            tmdbApiKey = settings.getString("tmdb_api_key", null) ?: "",
        )
    }
 
    override fun getCatalogApi(context: Context): CatalogProviderApi = catalogApi
}

CatalogProviderApi has two key pieces:

  • getCatalogs(): returns a List<ProviderCatalog> asynchronously
  • getCatalogItems(...): loads items for a specific catalog + page
class TestCatalogApi(
    private val client: OkHttpClient,
    private val providerId: String,
    private val tmdbApiKey: String,
) : CatalogProviderApi {
    override suspend fun getCatalogs(): List<ProviderCatalog> = emptyList()
 
    override suspend fun getCatalogItems(
        catalog: ProviderCatalog,
        page: Int,
    ): PaginatedResponse<FilmSearchItem> {
        // ...
    }
}

ProviderCatalog#

A ProviderCatalog describes a catalog entry.

PropertyDescription
nameDisplay name shown in the host UI.
urlEndpoint (or resolvable URL) used to load items for this catalog.
mediaTypeWhether this catalog represents movies or TV shows (FilmType).
canPaginateWhether the host should show “Load more”.
imageOptional image/icon for the catalog.
providerIdThe provider id that owns this catalog.
descriptionOptional description shown in the host UI.
headersOptional request headers to use when calling url.

Example using TMDB Discover API#

In this section, TMDB’s discover endpoints are used as an example.

Define DTO classes#

Reuse the DTOs (and mapping) from the Searching media guide.

Define your catalogs#

Convert endpoints into ProviderCatalog instances. Here, we use TMDB’s discover/movie and discover/tv.

private val trendingMovies = ProviderCatalog(
    name = "Trending Movies",
    url = "https://api.themoviedb.org/3/discover/movie",
    mediaType = FilmType.MOVIE,
    canPaginate = true,
    providerId = providerId,
    description = "Popular movies right now",
)
 
private val popularTvShows = ProviderCatalog(
    name = "Popular TV Shows",
    url = "https://api.themoviedb.org/3/discover/tv",
    mediaType = FilmType.TV_SHOW,
    canPaginate = true,
    image = "https://api.themoviedb.org/sample_icon.png",
    providerId = providerId,
)
 
override suspend fun getCatalogs(): List<ProviderCatalog> = listOf(
    trendingMovies,
    popularTvShows,
)

Implement getCatalogItems(...)#

Call the catalog URL, add paging/query params, parse the response, and map items to FilmSearchItem.

private fun buildCatalogUrl(
    baseUrl: String,
    page: Int,
    apiKey: String,
): String {
    return "$baseUrl?api_key=$apiKey&page=$page"
}
 
override suspend fun getCatalogItems(
    catalog: ProviderCatalog,
    page: Int,
): PaginatedResponse<FilmSearchItem> {
    val dto = client
        .request(url = buildCatalogUrl(catalog.url, page, tmdbApiKey))
        .execute()
        .fromJson<TmdbSearchResponseDto>()
 
    val forcedType = catalog.mediaType
 
    val results = dto.results
        .mapNotNull { it.toFilmSearchItemOrNull(providerId = providerId, forcedType = forcedType) }
 
    return PaginatedResponse(
        page = dto.page,
        results = results,
        hasNextPage = dto.page < dto.totalPages,
        totalPages = dto.totalPages,
    )
}

If your catalog API does not return total_pages, you can set totalPages = 0 and derive hasNextPage based on the returned list size.

Final output#

class TestCatalogApi(
    private val client: OkHttpClient,
    private val providerId: String,
    private val tmdbApiKey: String,
) : CatalogProviderApi {
    private val trendingMovies = ProviderCatalog(
        name = "Trending Movies",
        url = "https://api.themoviedb.org/3/discover/movie",
        mediaType = FilmType.MOVIE,
        canPaginate = true,
        providerId = providerId,
    )
 
    private val popularTvShows = ProviderCatalog(
        name = "Popular TV Shows",
        url = "https://api.themoviedb.org/3/discover/tv",
        mediaType = FilmType.TV_SHOW,
        canPaginate = true,
        providerId = providerId,
    )
 
    override suspend fun getCatalogs(): List<ProviderCatalog> = listOf(
        trendingMovies,
        popularTvShows,
    )
 
    override suspend fun getCatalogItems(
        catalog: ProviderCatalog,
        page: Int,
    ): PaginatedResponse<FilmSearchItem> {
        val dto = client
            .request(url = buildCatalogUrl(catalog.url, page, tmdbApiKey))
            .execute()
            .fromJson<TmdbSearchResponseDto>()
 
        val forcedType = catalog.mediaType
 
        val results = dto.results
            .mapNotNull { it.toFilmSearchItemOrNull(providerId = providerId, forcedType = forcedType) }
 
        return PaginatedResponse(
            page = dto.page,
            results = results,
            hasNextPage = dto.page < dto.totalPages,
            totalPages = dto.totalPages,
        )
    }
}