Implementation
Searching media#
This guide walks you through implementing in-app search for your provider using the capability-first SDK.
Overview#
To support search, expose a SearchProviderApi from your ProviderPlugin:
class TestProviderPlugin : ProviderPlugin() {
private val client by lazy { OkHttpClient() }
private val searchApi by lazy {
TestSearchApi(
client = client,
providerId = id,
tmdbApiKey = settings.getString("tmdb_api_key", null) ?: "",
)
}
override fun getSearchApi(context: Context): SearchProviderApi = searchApi
}Then implement SearchProviderApi.search(...), which returns a PaginatedResponse<FilmSearchItem>.
PaginatedResponse#
The host uses PaginatedResponse<T> to paginate results.
| Field | Meaning |
|---|---|
page | The current page number (1-based). |
results | Items for the current page. |
hasNextPage | Whether another page exists. |
totalPages | Total pages (set to 0 if unknown). |
Example using TMDB Search API#
In this section, TMDB’s search endpoints are used as an example.
Define filters (optional)#
Filters let users control what they’re searching for. Here’s a simple “Media type” filter group:
private const val FILTER_ALL = 0
private const val FILTER_MOVIE = 1
private const val FILTER_TV_SHOW = 2
class TmdbMediaTypeFilters(
name: String = "Media type",
) : FilterGroup(
name = name,
Filter.Select(
name = "",
options = listOf("All", "Movies", "TV Shows"),
state = FILTER_ALL,
)
) {
private val select: Filter.Select<String>
get() = first() as Filter.Select<String>
val selectedIndex: Int
get() = select.state
}Then register it in your API implementation:
override val filters: FilterList = FilterList(
TmdbMediaTypeFilters(),
)Define DTO classes#
Core Stubs uses Kotlinx Serialization for JSON parsing. Define DTOs using @Serializable + @SerialName.
@Serializable
data class TmdbSearchResponseDto(
val page: Int,
@SerialName("total_pages") val totalPages: Int = 0,
val results: List<TmdbSearchItemDto> = emptyList(),
)
@Serializable
data class TmdbSearchItemDto(
val id: Int,
@SerialName("media_type") val mediaType: String? = null,
val title: String? = null,
val name: String? = null,
@SerialName("poster_path") val posterPath: String? = null,
@SerialName("backdrop_path") val backdropPath: String? = null,
val overview: String? = null,
@SerialName("vote_average") val voteAverage: Double? = null,
@SerialName("original_language") val originalLanguage: String? = null,
val adult: Boolean? = null,
@SerialName("release_date") val releaseDate: String? = null,
@SerialName("first_air_date") val firstAirDate: String? = null,
)
private fun TmdbSearchItemDto.toFilmSearchItemOrNull(
providerId: String,
forcedType: FilmType? = null,
): FilmSearchItem? {
val resolvedType = forcedType ?: when (mediaType) {
"movie" -> FilmType.MOVIE
"tv" -> FilmType.TV_SHOW
else -> null
}
if (resolvedType == null) {
return null
}
val resolvedTitle = title ?: name.orEmpty()
val resolvedReleaseDate = releaseDate ?: firstAirDate
val poster = posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }
val backdrop = backdropPath?.let { "https://image.tmdb.org/t/p/w780$it" }
return FilmSearchItem(
id = id.toString(),
providerId = providerId,
filmType = resolvedType,
homePage = "https://www.themoviedb.org/${resolvedType.type}/$id",
title = resolvedTitle,
posterImage = poster,
adult = adult == true,
backdropImage = backdrop,
externalIds = mapOf(FilmIdSource.TMDB to id.toString()),
releaseDate = parseDate(resolvedReleaseDate),
rating = voteAverage,
language = originalLanguage,
overview = overview,
)
}Implement search(...)#
Pick an endpoint based on the selected filter, then parse JSON with Response.fromJson<T>().
private fun buildTmdbRequestUrl(
endpoint: String,
query: String,
page: Int,
apiKey: String,
): String {
val encodedQuery = URLEncoder.encode(query, Charsets.UTF_8.name())
return "$endpoint?api_key=$apiKey&query=$encodedQuery&page=$page"
}
override suspend fun search(
title: String,
page: Int,
filters: FilterList,
): PaginatedResponse<FilmSearchItem> {
val mediaTypeFilter = filters
.filterIsInstance<TmdbMediaTypeFilters>()
.firstOrNull()
val mediaTypeIndex = mediaTypeFilter?.selectedIndex ?: FILTER_ALL
val endpoint = when (mediaTypeIndex) {
FILTER_ALL -> "https://api.themoviedb.org/3/search/multi"
FILTER_TV_SHOW -> "https://api.themoviedb.org/3/search/tv"
FILTER_MOVIE -> "https://api.themoviedb.org/3/search/movie"
else -> "https://api.themoviedb.org/3/search/multi"
}
val dto = client
.request(url = buildTmdbRequestUrl(endpoint, title, page, tmdbApiKey))
.execute()
.fromJson<TmdbSearchResponseDto>(
errorMessage = "Couldn't parse response data!",
)
val forcedType = when (mediaTypeIndex) {
FILTER_MOVIE -> FilmType.MOVIE
FILTER_TV_SHOW -> FilmType.TV_SHOW
else -> null
}
val results = dto.results
.asSequence()
.mapNotNull { it.toFilmSearchItemOrNull(providerId = providerId, forcedType = forcedType) }
.toList()
return PaginatedResponse(
page = dto.page,
results = results,
hasNextPage = dto.page < dto.totalPages,
totalPages = dto.totalPages,
)
}fromJson(...) and OkHttpClient.request(...) are provided by Core Stubs utilities:
Final output#
class TestSearchApi(
private val client: OkHttpClient,
private val providerId: String,
private val tmdbApiKey: String,
) : SearchProviderApi {
override val filters: FilterList = FilterList(
TmdbMediaTypeFilters(),
)
override suspend fun search(
title: String,
page: Int,
filters: FilterList,
): PaginatedResponse<FilmSearchItem> {
val mediaTypeFilter = filters
.filterIsInstance<TmdbMediaTypeFilters>()
.firstOrNull()
val mediaTypeIndex = mediaTypeFilter?.selectedIndex ?: FILTER_ALL
val endpoint = when (mediaTypeIndex) {
FILTER_ALL -> "https://api.themoviedb.org/3/search/multi"
FILTER_TV_SHOW -> "https://api.themoviedb.org/3/search/tv"
FILTER_MOVIE -> "https://api.themoviedb.org/3/search/movie"
else -> "https://api.themoviedb.org/3/search/multi"
}
val dto = client
.request(url = buildTmdbRequestUrl(endpoint, title, page, tmdbApiKey))
.execute()
.fromJson<TmdbSearchResponseDto>()
val forcedType = when (mediaTypeIndex) {
FILTER_MOVIE -> FilmType.MOVIE
FILTER_TV_SHOW -> FilmType.TV_SHOW
else -> null
}
val results = dto.results
.asSequence()
.mapNotNull { it.toFilmSearchItemOrNull(providerId = providerId, forcedType = forcedType) }
.toList()
return PaginatedResponse(
page = dto.page,
results = results,
hasNextPage = dto.page < dto.totalPages,
totalPages = dto.totalPages,
)
}
}