An open API service indexing awesome lists of open source software.

https://github.com/renaudallard/multistream

Native Android app (phone, tablet, Android TV) that federates ten streaming services: search across catalogs, deep-link straight into the right app at a title, and track locally what you have watched.
https://github.com/renaudallard/multistream

android android-tv compose-for-tv deep-linking disney-plus jetpack-compose kotlin meta-search netflix plex prime-video streaming watch-tracker

Last synced: 11 days ago
JSON representation

Native Android app (phone, tablet, Android TV) that federates ten streaming services: search across catalogs, deep-link straight into the right app at a title, and track locally what you have watched.

Awesome Lists containing this project

README

          


multistream: one app to rule them all

multistream


One Android app (phone, tablet, and Android TV / Google TV) that federates the catalogs of
your installed streaming apps.


Download the latest APK
Total downloads
Kotlin 2.0.21
Android phone, tablet, TV
minSdk 24
Jetpack Compose and Compose for TV

Search across the services from one box, **browse by genre** without typing a query, see show
information, **launch directly** into the right app at the right title, and track **locally** what
you have watched and where you are in a series.

The eleven services: **Netflix**, **Disney+**, **Prime Video**, **Molotov**, **Zattoo**, **Arte**,
**Plex**, **RTBF Auvio**, **RTL Play**, **Play RTS**, **ICI Tou.tv**.

The interface is available in English and French, following the device language.

## Contents

- [How it works](#how-it-works)
- [Services and capabilities](#services-and-capabilities)
- [Episodes and watched state](#episodes-and-watched-state)
- [Login](#login)
- [Deep links](#deep-links)
- [Modules](#modules)
- [Build and run](#build-and-run)
- [Testing and verification](#testing-and-verification)
- [Legal / personal use](#legal--personal-use)

## How it works

Launch plus local watch-tracking is the always-works spine; catalog search is a best-effort,
per-provider capability layered on top. Each provider is a self-contained leaf module that
advertises `ProviderCapabilities` (can it search? browse by genre? deep-link a title? an episode? is
it live TV?),
and the UI reads those flags and degrades gracefully: a provider that cannot search still launches
and tracks. There is no DI framework. A small hand-written `AppGraph` wires everything and composes
the providers into a registry, so one flaky provider never breaks the app. Search fans out to every
enabled provider in parallel, merges the rows into one card per title across services, and ranks the
list by how closely each title matches the query (a full-phrase match before partial-word ones).
Opening a series lists its episodes by asking every provider that can enumerate them and unioning the
results, so a service carrying the full run completes one that holds only part of it.

On each launch the app asks GitHub for the latest release and, if it is newer than the running build,
shows a dismissible banner linking straight to the new APK. The check is best-effort: offline, a
rate-limited API, or any error just leaves the banner hidden.

## Services and capabilities

The spine works for **all eleven**: deep-link launch, local watch tracking (watched/unwatched,
series next-episode, watchlist, continue-watching), a per-provider region setting, and one adaptive
shell for phone and Android TV.

| Service | Search | Genre | Launch | Details | Login | Notes |
|---|:--:|:--:|:--:|:--:|:--:|---|
| **Netflix** | ✅ | 8 | title page | cast, summary | WebView \* | title and in-app-search deep links; search verified on a real device, the session can need a fresh login after heavy use |
| **Disney+** | ✅ | 10 | title page | cast, summary | email / password | verified on a real device; films and series are typed correctly, so episodes list only for series |
| **Prime Video** | ✅ | 10 | detail page | summary | WebView \* | verified on a real device; the TV build is bundled and the mobile package is tried on phones; web-search art is 16:9 (no portrait) |
| **Molotov** | ✅ | 9 | deep link | summary | email / password | verified on a real device; rich title and program deep links; its API carries no cast |
| **Zattoo** | ✅ | — | live channel | — | email / password | live TV: deep-links to the program's live channel (`zattoo.com/live/`); the guide carries no synopsis |
| **Arte** | ✅ | 2 | title page | summary | optional | free public API; the region selects the catalog language |
| **Plex** | ✅ | 10 † | watch.plex.tv | cast, summary | optional | anonymous Discover; the device sign-in auto-discovers and searches your own server |
| **RTBF Auvio** | ✅ | 2 | title page | — | optional | free public API |
| **RTL Play** | ✅ | 1 | title page | cast, summary | — | catalog search and details via DPG Media's lfvp API (anonymous, but Belgium-only); needs a Belgian connection |
| **Play RTS** | ✅ | 4 | video page | — | optional | free SRG SSR Integration Layer; video results only |
| **ICI Tou.tv** | ✅ | 9 | title page | cast, summary | optional | Radio-Canada's public catalog API (anonymous, worldwide); lists episodes; only playback is Canada-locked |

`✅ Search` = a real catalog query from this app. `Genre` = how many of the ten canonical genres
(Comedy, Drama, Horror, Action, Documentary, Sci-Fi, Crime, Romance, Animation, Kids) the service can
browse with no search term, from genre chips on the Search screen; public broadcasters organize their
catalogs by theme, so they map to fewer genres. `†` Plex browses the genres of your own server's
library, so it needs a connected server (and returns nothing for a Discover-only account). `\*` marks
a one-time WebView login. Search requires that login for Netflix and Prime Video, and the
email/password login for Disney+, Molotov and Zattoo; Arte, Plex, RTBF Auvio, RTL Play, Play RTS and
ICI Tou.tv search without a login. `Details` = what the title screen adds when you open a result: a
plot summary, and the billed cast where the service exposes it (a release year shows wherever search
returns one); `—` providers show the poster, title and year only.

Live search is verified on a real device across all eleven services, and genre browse on every service
that supports it. The search box carries a clear button that wipes the query in one tap, and leading or
trailing spaces are stripped before the search runs. Leave the search box empty and the genre chips
appear; tapping one fans out to the providers that carry that genre and merges the catalogs, exactly
like a text search. A small built-in
sample catalog also ships for an offline demo; live search itself runs only on a device with network.

## Episodes and watched state

Opening a series fetches its episodes from every provider that can enumerate them and unions them by
season and episode number, so a service carrying the full run completes one that holds only part of
it. Plex lists episodes from your own server; Prime Video reads them from the signed-in detail page,
fetching one page per season so every season is covered.

Where a service exposes it, the detail screen also offers "Sync watched from ", which
imports which episodes you have already watched there into your local history. This is verified for
Netflix, Plex, Prime Video and Disney+. Prime reads each episode's playback progress across every
season; Disney+ collects every episode's id and batches them through its userState lookup, since its
catalog carries no inline progress. ICI Tou.tv (after the optional login) is limited by Radio-Canada's
API, which exposes only a continue-watching resume point per show, so it marks the current season up
to where you left off rather than a full history.

## Login

Login is per-provider and never required for search except where noted above.

- **Netflix, Prime Video** open a one-time **WebView login** (Settings, "Log in (browser)") that
captures cookies into the encrypted secret store. Their search needs it.
- **Disney+, Molotov, Zattoo** use an email and password form.
- **Plex** searches anonymously. The **optional** login is Plex's device sign-in (so it works with
two-factor accounts): tap "Link account", approve in the browser at `app.plex.tv/auth` where any
2FA is handled, and the app keeps the account token and auto-discovers your own Plex Media Server
over its secure (https) connection (no token to paste), searching that server with a Discover
fallback. A server with secure connections turned off is reached only through the Discover fallback.
- **Arte, RTBF Auvio** search without login. An **optional** WebView login captures the site session
and passes it to the search best-effort.
- **RTL Play** has no login: its lfvp catalog API is anonymous (geo-restricted to Belgium), and an
RTL account session applies to a different host, so it would not affect catalog search anyway.
- **Play RTS** searches without login (its SRG SSR Integration Layer catalog is public); an
**optional** WebView login captures your rts.ch account session and passes it to the search
best-effort.
- **ICI Tou.tv** searches without login (the catalog and detail endpoints are anonymous and answer
worldwide). The **optional** login is Radio-Canada's account sign-in (Azure AD B2C) in a WebView; it
captures the access token and unlocks importing your watch progress. Only playback is geo-locked to
Canada, which stays inside the official app.

Secrets live in `EncryptedSharedPreferences`; clearing the app data or logging out wipes them.

## Deep links

Verified from each app's decoded manifest. Playback activities are never forced; multistream opens
the **title page** and the user presses play inside the official app.

- **Netflix** `https://www.netflix.com/title/` (plus the `nflx://` scheme) and an in-app search
deep link.
- **Disney+** `https://www.disneyplus.com/browse/entity-`, with `disneyplus://` as a fallback.
- **Prime Video** `https://app.primevideo.com/detail?gti=`. The bundled APK is the TV
("living-room") build; on phones the mobile package `com.amazon.avod.thirdpartyclient` is tried.
- **Molotov** `https://www.molotov.tv/` web links, carried as a deep-link hint.
- **Zattoo** `https://zattoo.com/live/` opens the program's live channel (the app catches every
`zattoo.com` URL; the `/live` route comes from its bundle).
- **Arte** `https://www.arte.tv//videos//`, with `arte://collection/` as a fallback.
- **Plex** `https://watch.plex.tv//` for Discover hits; server-library hits have no
public slug and open the Plex app.
- **RTBF Auvio** `https://auvio.rtbf.be`.
- **RTL Play** `https://www.rtlplay.be/rtlplay/~` opens the title; the in-app search
row opens `https://www.rtlplay.be/rtlplay/recherche?q=`.
- **Play RTS** `https://www.rts.ch/play/tv/redirect/detail/` (the numeric id from the media URN).
- **ICI Tou.tv** `https://ici.tou.tv/` opens the show page in the Tou.tv app (package `tv.tou.android`).

## Modules

```
app UI (Compose + Compose for TV), navigation, hand-written AppGraph, sample catalog
core/model pure Kotlin: Title/Season/Episode/Availability/ProviderRef/TitleKey,
normalizeTitle(), mergeResults(), computeNextEpisode()
core/data Room (watch tracking), DataStore settings, encrypted secrets
core/net shared OkHttp client, tolerant JSON helpers, in-memory cookie jar
provider/api StreamingProvider interface, ProviderCapabilities, Launcher, DeepLinks, WebLoginSpec
provider/ one leaf module per service:
netflix · disney · prime · molotov · zattoo · arte · plex · rtbf · rtl · rts · toutv
```

`core/*` and the feature screens never depend on a concrete provider; only `app` wires them, so a
flaky provider stays contained.

## Build and run

Prerequisites on this machine: **JDK 21** (`/usr/lib/jvm/java-21-openjdk-arm64`) and the **Android
SDK** at `~/Android/Sdk` (platform `android-35`, build-tools 35). The system `gradle` is too old, so
always use the wrapper. Toolchain: Kotlin 2.0.21, AGP 8.7.2, `compileSdk`/`targetSdk` 35, `minSdk` 24.

```bash
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-arm64
./gradlew assembleDebug # -> app/build/outputs/apk/debug/multistream-debug.apk
./gradlew test # runs the JVM unit tests
./gradlew installDebug # installs to a connected device/emulator (adb)
```

Release build (signed and R8-shrunk):

```bash
# Signing creds live in keystore.properties (git-ignored): storeFile, storePassword, keyAlias, keyPassword.
# A dev key (multistream-release.keystore) is used by default; swap in your own for Play distribution.
./gradlew :app:assembleRelease # -> app/build/outputs/apk/release/multistream.apk (~2.4 MB, v2-signed)
./gradlew :app:bundleRelease # -> app/build/outputs/bundle/release/multistream-release.aab (Play upload)
```

`local.properties` (git-ignored) points Gradle at the SDK: `sdk.dir=/home/r/Android/Sdk`.

### Installing the target streaming apps (for deep-link testing)

They live in `apks/`. Netflix is a plain APK; the others are split `.xapk` bundles, so unzip and use
`install-multiple`:

```bash
adb install "apks/Netflix_9.65.0+build+9+64253_APKPure.apk"
# for each .xapk: unzip it, then
adb install-multiple .apk config.*.apk
```

Verify a deep link directly:

```bash
adb shell am start -a android.intent.action.VIEW -d "https://www.netflix.com/title/80057281" com.netflix.mediaclient
```

## Testing and verification

JVM unit tests (run anywhere):

- `core/model` covers title reconciliation and merge (year tolerance, type guard, external-id match)
and the next-episode computation.
- `provider/api` covers the deep-link URL formats (`DeepLinks`).
- Each searchable provider (`netflix`, `disney`, `prime`, `molotov`, `zattoo`, `arte`, `plex`,
`rtbf`, `rts`, `rtl`, `toutv`) replays its API client against OkHttp `MockWebServer` (plain HTTP, no Android runtime).

Room DAO SQL is validated at compile time by the Room KSP processor.

**Environment limitation (this host):** it is headless **aarch64 with no `/dev/kvm`**, so the Android
emulator cannot run, and Robolectric cannot run either (Conscrypt ships no `linux-aarch_64` native).
Android-runtime tests (Room integration, intent resolution) and on-device runs must therefore happen
on an **x86_64 machine, a KVM-enabled host, or a physical device** over `adb`. Everything that does
not need an Android runtime is verified here: a working APK plus the JVM tests above.

## Legal / personal use

For personal use with your own accounts. The app never bypasses DRM: playback always happens inside
the official app. multistream only queries catalogs and fires a deep-link intent.