Implementation
Fetching media links#
This guide covers implementing MediaLinkProviderApi to emit streams and/or subtitles.
It assumes you already have:
- A
FilmMetadataobject (fromMetadataProviderApi) - Basic scraping knowledge (for HTML-based sources)
Overview#
Media links represent sources the host can play (streams) or load (subtitles). In the capability-first SDK, links are produced as a Flow<MediaLink>:
class TestMediaLinkApi(
private val client: OkHttpClient,
) : MediaLinkProviderApi {
override val supportedLinkTypes: Set<MediaLinkType> = setOf(
MediaLinkType.STREAMS,
// MediaLinkType.SUBTITLES,
)
override fun getLinks(
film: FilmMetadata,
episode: Episode?,
): Flow<MediaLink> {
// return flow { emit(...) }
}
}MediaLink#
MediaLink is a sealed type:
Stream: a playable stream URLSubtitle: a subtitle URL + language
Flags (Flag) allow you to declare constraints (auth, geo restrictions, third-party gateways, etc.).
References:
Example (scraping TMDB watch providers)#
This example scrapes TMDB’s “watch providers” page and emits Stream links.
Override getLinks(...)#
Implement getLinks(...) using a flow { ... } builder and emit as soon as each link is discovered.
override fun getLinks(
film: FilmMetadata,
episode: Episode?,
): Flow<MediaLink> = flow {
// TODO
}Prefer streaming results (multiple emit(...) calls) instead of buffering everything in memory.
Fetch + parse HTML#
Use OkHttpClient.request(...) and Response.asJsoup().
val mediaType = film.filmType.type
require(mediaType == "movie" || mediaType == "tv") {
"Invalid media type: $mediaType"
}
val id = film.externalIds[FilmIdSource.TMDB] ?: film.id
val response = FlxDispatchers.withIOContext {
client.request(
url = "https://www.themoviedb.org/${mediaType}/${id}/watch?locale=US",
).execute()
}
val html = response.asJsoup()withIOContext { ... } is available via FlxDispatchers.
Scrape providers + emit streams#
For each provider entry, decode the redirect target and emit a Stream.
html.select("div.ott_provider li a").forEach { element ->
val href = element.attr("href")
val title = element.attr("title")
val logoUrl = element.select("img").attr("src")
val providerName = title
.split(" on ")
.lastOrNull()
?.trim()
.orEmpty()
.ifBlank { "Unknown Provider" }
val url = href
.split("&r=")
.getOrNull(1)
?.split("&")
?.firstOrNull()
if (url != null) {
val decodedUrl = URLDecoder.decode(url, "UTF-8")
emit(
Stream(
name = providerName,
description = title,
url = decodedUrl,
flags = setOf(
Flag.Trusted(
name = providerName,
logo = logoUrl,
),
),
),
)
}
}This is just an example scraper. Real providers typically scrape their own streaming sites.
Final output#
Remember:
- Emit links as soon as you find them.
- Use
supportedLinkTypesto declare what you emit (STREAMS,SUBTITLES, or both). - Use
Flags to signal auth, geo restrictions, or third-party gateways.
class TestMediaLinkApi(
private val client: OkHttpClient,
) : MediaLinkProviderApi {
override val supportedLinkTypes: Set<MediaLinkType> = setOf(MediaLinkType.STREAMS)
override fun getLinks(
film: FilmMetadata,
episode: Episode?,
): Flow<MediaLink> = flow {
val mediaType = film.filmType.type
val id = film.externalIds[FilmIdSource.TMDB] ?: film.id
val response = FlxDispatchers.withIOContext {
client.request(
url = "https://www.themoviedb.org/${mediaType}/${id}/watch?locale=US",
).execute()
}
val html = response.asJsoup()
html.select("div.ott_provider li a").forEach { element ->
val href = element.attr("href")
val title = element.attr("title")
val logoUrl = element.select("img").attr("src")
val providerName = title
.split(" on ")
.lastOrNull()
?.trim()
.orEmpty()
.ifBlank { "Unknown Provider" }
val url = href
.split("&r=")
.getOrNull(1)
?.split("&")
?.firstOrNull()
if (url != null) {
val decodedUrl = URLDecoder.decode(url, "UTF-8")
emit(
Stream(
name = providerName,
description = title,
url = decodedUrl,
flags = setOf(
Flag.Trusted(
name = providerName,
logo = logoUrl,
),
),
)
)
}
}
}
}