{"id":50749266,"url":"https://github.com/renaudallard/multistream","last_synced_at":"2026-06-11T00:01:25.904Z","repository":{"id":362702950,"uuid":"1259160212","full_name":"renaudallard/multistream","owner":"renaudallard","description":"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.","archived":false,"fork":false,"pushed_at":"2026-06-05T15:11:55.000Z","size":1067,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-06-05T15:22:00.489Z","etag":null,"topics":["android","android-tv","compose-for-tv","deep-linking","disney-plus","jetpack-compose","kotlin","meta-search","netflix","plex","prime-video","streaming","watch-tracker"],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/renaudallard.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T08:44:06.000Z","updated_at":"2026-06-05T15:11:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/renaudallard/multistream","commit_stats":null,"previous_names":["renaudallard/multistream"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/renaudallard/multistream","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fmultistream","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fmultistream/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fmultistream/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fmultistream/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/renaudallard","download_url":"https://codeload.github.com/renaudallard/multistream/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fmultistream/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34175887,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-tv","compose-for-tv","deep-linking","disney-plus","jetpack-compose","kotlin","meta-search","netflix","plex","prime-video","streaming","watch-tracker"],"created_at":"2026-06-11T00:00:58.171Z","updated_at":"2026-06-11T00:01:25.887Z","avatar_url":"https://github.com/renaudallard.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/banner.png\" alt=\"multistream: one app to rule them all\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003emultistream\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  One Android app (phone, tablet, \u003cb\u003eand\u003c/b\u003e Android TV / Google TV) that federates the catalogs of\n  your installed streaming apps.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/renaudallard/multistream/releases/latest\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/renaudallard/multistream?label=Download%20APK\u0026logo=android\u0026logoColor=white\u0026color=3DDC84\" alt=\"Download the latest APK\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/renaudallard/multistream/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/downloads/renaudallard/multistream/total?label=downloads\u0026logo=github\u0026logoColor=white\u0026color=2C3E50\" alt=\"Total downloads\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Kotlin-2.0.21-7F52FF?logo=kotlin\u0026logoColor=white\" alt=\"Kotlin 2.0.21\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Android-phone%20%C2%B7%20tablet%20%C2%B7%20TV-3DDC84?logo=android\u0026logoColor=white\" alt=\"Android phone, tablet, TV\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/minSdk-24-2C3E50\" alt=\"minSdk 24\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Jetpack%20Compose-%26%20Compose%20for%20TV-4285F4?logo=jetpackcompose\u0026logoColor=white\" alt=\"Jetpack Compose and Compose for TV\"\u003e\n\u003c/p\u003e\n\nSearch across the services from one box, **browse by genre** without typing a query, see show\ninformation, **launch directly** into the right app at the right title, and track **locally** what\nyou have watched and where you are in a series.\n\nThe eleven services: **Netflix**, **Disney+**, **Prime Video**, **Molotov**, **Zattoo**, **Arte**,\n**Plex**, **RTBF Auvio**, **RTL Play**, **Play RTS**, **ICI Tou.tv**.\n\nThe interface is available in English and French, following the device language.\n\n## Contents\n\n- [How it works](#how-it-works)\n- [Services and capabilities](#services-and-capabilities)\n- [Episodes and watched state](#episodes-and-watched-state)\n- [Login](#login)\n- [Deep links](#deep-links)\n- [Modules](#modules)\n- [Build and run](#build-and-run)\n- [Testing and verification](#testing-and-verification)\n- [Legal / personal use](#legal--personal-use)\n\n## How it works\n\nLaunch plus local watch-tracking is the always-works spine; catalog search is a best-effort,\nper-provider capability layered on top. Each provider is a self-contained leaf module that\nadvertises `ProviderCapabilities` (can it search? browse by genre? deep-link a title? an episode? is\nit live TV?),\nand the UI reads those flags and degrades gracefully: a provider that cannot search still launches\nand tracks. There is no DI framework. A small hand-written `AppGraph` wires everything and composes\nthe providers into a registry, so one flaky provider never breaks the app. Search fans out to every\nenabled provider in parallel, merges the rows into one card per title across services, and ranks the\nlist by how closely each title matches the query (a full-phrase match before partial-word ones).\nOpening a series lists its episodes by asking every provider that can enumerate them and unioning the\nresults, so a service carrying the full run completes one that holds only part of it.\n\nOn each launch the app asks GitHub for the latest release and, if it is newer than the running build,\nshows a dismissible banner linking straight to the new APK. The check is best-effort: offline, a\nrate-limited API, or any error just leaves the banner hidden.\n\n## Services and capabilities\n\nThe spine works for **all eleven**: deep-link launch, local watch tracking (watched/unwatched,\nseries next-episode, watchlist, continue-watching), a per-provider region setting, and one adaptive\nshell for phone and Android TV.\n\n| Service | Search | Genre | Launch | Details | Login | Notes |\n|---|:--:|:--:|:--:|:--:|:--:|---|\n| **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 |\n| **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 |\n| **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) |\n| **Molotov** | ✅ | 9 | deep link | summary | email / password | verified on a real device; rich title and program deep links; its API carries no cast |\n| **Zattoo** | ✅ | — | live channel | — | email / password | live TV: deep-links to the program's live channel (`zattoo.com/live/\u003ccid\u003e`); the guide carries no synopsis |\n| **Arte** | ✅ | 2 | title page | summary | optional | free public API; the region selects the catalog language |\n| **Plex** | ✅ | 10 † | watch.plex.tv | cast, summary | optional | anonymous Discover; the device sign-in auto-discovers and searches your own server |\n| **RTBF Auvio** | ✅ | 2 | title page | — | optional | free public API |\n| **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 |\n| **Play RTS** | ✅ | 4 | video page | — | optional | free SRG SSR Integration Layer; video results only |\n| **ICI Tou.tv** | ✅ | 9 | title page | cast, summary | optional | Radio-Canada's public catalog API (anonymous, worldwide); lists episodes; only playback is Canada-locked |\n\n`✅ Search` = a real catalog query from this app. `Genre` = how many of the ten canonical genres\n(Comedy, Drama, Horror, Action, Documentary, Sci-Fi, Crime, Romance, Animation, Kids) the service can\nbrowse with no search term, from genre chips on the Search screen; public broadcasters organize their\ncatalogs by theme, so they map to fewer genres. `†` Plex browses the genres of your own server's\nlibrary, so it needs a connected server (and returns nothing for a Discover-only account). `\\*` marks\na one-time WebView login. Search requires that login for Netflix and Prime Video, and the\nemail/password login for Disney+, Molotov and Zattoo; Arte, Plex, RTBF Auvio, RTL Play, Play RTS and\nICI Tou.tv search without a login. `Details` = what the title screen adds when you open a result: a\nplot summary, and the billed cast where the service exposes it (a release year shows wherever search\nreturns one); `—` providers show the poster, title and year only.\n\nLive search is verified on a real device across all eleven services, and genre browse on every service\nthat supports it. The search box carries a clear button that wipes the query in one tap, and leading or\ntrailing spaces are stripped before the search runs. Leave the search box empty and the genre chips\nappear; tapping one fans out to the providers that carry that genre and merges the catalogs, exactly\nlike a text search. A small built-in\nsample catalog also ships for an offline demo; live search itself runs only on a device with network.\n\n## Episodes and watched state\n\nOpening a series fetches its episodes from every provider that can enumerate them and unions them by\nseason and episode number, so a service carrying the full run completes one that holds only part of\nit. Plex lists episodes from your own server; Prime Video reads them from the signed-in detail page,\nfetching one page per season so every season is covered.\n\nWhere a service exposes it, the detail screen also offers \"Sync watched from \u003cservice\u003e\", which\nimports which episodes you have already watched there into your local history. This is verified for\nNetflix, Plex, Prime Video and Disney+. Prime reads each episode's playback progress across every\nseason; Disney+ collects every episode's id and batches them through its userState lookup, since its\ncatalog carries no inline progress. ICI Tou.tv (after the optional login) is limited by Radio-Canada's\nAPI, which exposes only a continue-watching resume point per show, so it marks the current season up\nto where you left off rather than a full history.\n\n## Login\n\nLogin is per-provider and never required for search except where noted above.\n\n- **Netflix, Prime Video** open a one-time **WebView login** (Settings, \"Log in (browser)\") that\n  captures cookies into the encrypted secret store. Their search needs it.\n- **Disney+, Molotov, Zattoo** use an email and password form.\n- **Plex** searches anonymously. The **optional** login is Plex's device sign-in (so it works with\n  two-factor accounts): tap \"Link account\", approve in the browser at `app.plex.tv/auth` where any\n  2FA is handled, and the app keeps the account token and auto-discovers your own Plex Media Server\n  over its secure (https) connection (no token to paste), searching that server with a Discover\n  fallback. A server with secure connections turned off is reached only through the Discover fallback.\n- **Arte, RTBF Auvio** search without login. An **optional** WebView login captures the site session\n  and passes it to the search best-effort.\n- **RTL Play** has no login: its lfvp catalog API is anonymous (geo-restricted to Belgium), and an\n  RTL account session applies to a different host, so it would not affect catalog search anyway.\n- **Play RTS** searches without login (its SRG SSR Integration Layer catalog is public); an\n  **optional** WebView login captures your rts.ch account session and passes it to the search\n  best-effort.\n- **ICI Tou.tv** searches without login (the catalog and detail endpoints are anonymous and answer\n  worldwide). The **optional** login is Radio-Canada's account sign-in (Azure AD B2C) in a WebView; it\n  captures the access token and unlocks importing your watch progress. Only playback is geo-locked to\n  Canada, which stays inside the official app.\n\nSecrets live in `EncryptedSharedPreferences`; clearing the app data or logging out wipes them.\n\n## Deep links\n\nVerified from each app's decoded manifest. Playback activities are never forced; multistream opens\nthe **title page** and the user presses play inside the official app.\n\n- **Netflix** `https://www.netflix.com/title/\u003cid\u003e` (plus the `nflx://` scheme) and an in-app search\n  deep link.\n- **Disney+** `https://www.disneyplus.com/browse/entity-\u003cid\u003e`, with `disneyplus://\u003cid\u003e` as a fallback.\n- **Prime Video** `https://app.primevideo.com/detail?gti=\u003cASIN\u003e`. The bundled APK is the TV\n  (\"living-room\") build; on phones the mobile package `com.amazon.avod.thirdpartyclient` is tried.\n- **Molotov** `https://www.molotov.tv/\u003cslug\u003e` web links, carried as a deep-link hint.\n- **Zattoo** `https://zattoo.com/live/\u003ccid\u003e` opens the program's live channel (the app catches every\n  `zattoo.com` URL; the `/live` route comes from its bundle).\n- **Arte** `https://www.arte.tv/\u003clang\u003e/videos/\u003cid\u003e/`, with `arte://collection/\u003cid\u003e` as a fallback.\n- **Plex** `https://watch.plex.tv/\u003cmovie|show\u003e/\u003cslug\u003e` for Discover hits; server-library hits have no\n  public slug and open the Plex app.\n- **RTBF Auvio** `https://auvio.rtbf.be\u003cpath\u003e`.\n- **RTL Play** `https://www.rtlplay.be/rtlplay/\u003cslug\u003e~\u003cdetailId\u003e` opens the title; the in-app search\n  row opens `https://www.rtlplay.be/rtlplay/recherche?q=\u003cquery\u003e`.\n- **Play RTS** `https://www.rts.ch/play/tv/redirect/detail/\u003cid\u003e` (the numeric id from the media URN).\n- **ICI Tou.tv** `https://ici.tou.tv/\u003cslug\u003e` opens the show page in the Tou.tv app (package `tv.tou.android`).\n\n## Modules\n\n```\napp                  UI (Compose + Compose for TV), navigation, hand-written AppGraph, sample catalog\ncore/model           pure Kotlin: Title/Season/Episode/Availability/ProviderRef/TitleKey,\n                     normalizeTitle(), mergeResults(), computeNextEpisode()\ncore/data            Room (watch tracking), DataStore settings, encrypted secrets\ncore/net             shared OkHttp client, tolerant JSON helpers, in-memory cookie jar\nprovider/api         StreamingProvider interface, ProviderCapabilities, Launcher, DeepLinks, WebLoginSpec\nprovider/\u003cservice\u003e   one leaf module per service:\n                     netflix · disney · prime · molotov · zattoo · arte · plex · rtbf · rtl · rts · toutv\n```\n\n`core/*` and the feature screens never depend on a concrete provider; only `app` wires them, so a\nflaky provider stays contained.\n\n## Build and run\n\nPrerequisites on this machine: **JDK 21** (`/usr/lib/jvm/java-21-openjdk-arm64`) and the **Android\nSDK** at `~/Android/Sdk` (platform `android-35`, build-tools 35). The system `gradle` is too old, so\nalways use the wrapper. Toolchain: Kotlin 2.0.21, AGP 8.7.2, `compileSdk`/`targetSdk` 35, `minSdk` 24.\n\n```bash\nexport JAVA_HOME=/usr/lib/jvm/java-21-openjdk-arm64\n./gradlew assembleDebug      # -\u003e app/build/outputs/apk/debug/multistream-debug.apk\n./gradlew test               # runs the JVM unit tests\n./gradlew installDebug       # installs to a connected device/emulator (adb)\n```\n\nRelease build (signed and R8-shrunk):\n\n```bash\n# Signing creds live in keystore.properties (git-ignored): storeFile, storePassword, keyAlias, keyPassword.\n# A dev key (multistream-release.keystore) is used by default; swap in your own for Play distribution.\n./gradlew :app:assembleRelease   # -\u003e app/build/outputs/apk/release/multistream.apk (~2.4 MB, v2-signed)\n./gradlew :app:bundleRelease     # -\u003e app/build/outputs/bundle/release/multistream-release.aab (Play upload)\n```\n\n`local.properties` (git-ignored) points Gradle at the SDK: `sdk.dir=/home/r/Android/Sdk`.\n\n### Installing the target streaming apps (for deep-link testing)\n\nThey live in `apks/`. Netflix is a plain APK; the others are split `.xapk` bundles, so unzip and use\n`install-multiple`:\n\n```bash\nadb install \"apks/Netflix_9.65.0+build+9+64253_APKPure.apk\"\n# for each .xapk: unzip it, then\nadb install-multiple \u003cpkg\u003e.apk config.*.apk\n```\n\nVerify a deep link directly:\n\n```bash\nadb shell am start -a android.intent.action.VIEW -d \"https://www.netflix.com/title/80057281\" com.netflix.mediaclient\n```\n\n## Testing and verification\n\nJVM unit tests (run anywhere):\n\n- `core/model` covers title reconciliation and merge (year tolerance, type guard, external-id match)\n  and the next-episode computation.\n- `provider/api` covers the deep-link URL formats (`DeepLinks`).\n- Each searchable provider (`netflix`, `disney`, `prime`, `molotov`, `zattoo`, `arte`, `plex`,\n  `rtbf`, `rts`, `rtl`, `toutv`) replays its API client against OkHttp `MockWebServer` (plain HTTP, no Android runtime).\n\nRoom DAO SQL is validated at compile time by the Room KSP processor.\n\n**Environment limitation (this host):** it is headless **aarch64 with no `/dev/kvm`**, so the Android\nemulator cannot run, and Robolectric cannot run either (Conscrypt ships no `linux-aarch_64` native).\nAndroid-runtime tests (Room integration, intent resolution) and on-device runs must therefore happen\non an **x86_64 machine, a KVM-enabled host, or a physical device** over `adb`. Everything that does\nnot need an Android runtime is verified here: a working APK plus the JVM tests above.\n\n## Legal / personal use\n\nFor personal use with your own accounts. The app never bypasses DRM: playback always happens inside\nthe official app. multistream only queries catalogs and fires a deep-link intent.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frenaudallard%2Fmultistream","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frenaudallard%2Fmultistream","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frenaudallard%2Fmultistream/lists"}