Implementation

Fetching media links#

This guide covers implementing MediaLinkProviderApi to emit streams and/or subtitles.

It assumes you already have:

  • A FilmMetadata object (from MetadataProviderApi)
  • 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 is a sealed type:

  • Stream: a playable stream URL
  • Subtitle: 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.

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 supportedLinkTypes to 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,
                            ),
                        ),
                    )
                )
            }
        }
    }
}