This commit is contained in:
ponzischeme89
2026-08-17 13:13:10 +12:00
parent 12b27c77c3
commit 43ab18854c
39 changed files with 1707 additions and 1528 deletions
+175 -136
View File
@@ -1,11 +1,68 @@
# Memby (Android TV)
<div align="center">
An independent Android TV client for Emby, by **ponzischeme89**. Memby combines a
personalised television launcher, full media player, system screensaver and an optional
self-hosted gateway that shapes the experience for every viewer using your library.
<img src="memby-icon.png" alt="Memby" width="128" height="128">
# Memby
**An independent Android TV client for Emby.**
A personalised television launcher, a full media player, a system screensaver and an
optional self-hosted gateway that shapes the experience for every viewer in the house.
[![Licence: GPL v2](https://img.shields.io/badge/licence-GPL--2.0-green.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Android%20TV-green.svg)](#build--install)
[![Min SDK](https://img.shields.io/badge/minSdk-23-green.svg)](#build--install)
[![Kotlin](https://img.shields.io/badge/Kotlin-Compose%20for%20TV-green.svg)](#tech-stack)
[![Gateway](https://img.shields.io/badge/gateway-Go%20%2B%20Postgres%20%2B%20Redis-green.svg)](server/README.md)
Source: [g.sublogue.com/admin/memby](https://g.sublogue.com/admin/memby)
</div>
---
## Contents
- [How it works](#how-it-works)
- [Features](#features)
- [Browse and discover](#browse-and-discover)
- [Playback](#playback)
- [Profiles, televisions and administration](#profiles-televisions-and-administration)
- [Screensaver and updates](#screensaver-and-updates)
- [Tech stack](#tech-stack)
- [Build & install](#build--install)
- [Server address](#server-address)
- [First run](#first-run)
- [Running the gateway](#running-the-gateway)
- [Project layout](#project-layout)
- [Distributing builds](#distributing-builds)
- [One-time: a signing key](#one-time-a-signing-key)
- [NAS deployment release (recommended)](#nas-deployment-release-recommended)
- [Local PowerShell release (compatibility path)](#local-powershell-release-compatibility-path)
- [How TVs update themselves](#how-tvs-update-themselves)
- [Upgrading to v0.1.53](#upgrading-to-v0153)
- [Notes & limitations](#notes--limitations)
- [Licence](#licence)
---
## How it works
The client can talk to Emby directly, or through the **Memby gateway** — a Go service in
`server/` that runs in Docker alongside Postgres and Redis and owns auth, caching, search
and the shaping of TV screens. With a gateway the launcher is one request instead of four,
and the TV holds a revocable gateway token rather than a live Emby token.
```
direct: TV ──────────────────────────────► Emby
gateway: TV ──► Memby gateway ──► Emby (metadata + artwork)
TV ─────────────────────► Emby (video stream; never proxied by the gateway)
```
Which one a build uses is decided by `memby.gatewayUrl` in `gradle.properties`: set it and
the app is a thin client; leave it blank and the app talks to Emby directly. Both paths are
maintained — the direct path is the fallback when the container is down.
## Features
### Browse and discover
@@ -59,12 +116,79 @@ Source: [g.sublogue.com/admin/memby](https://g.sublogue.com/admin/memby)
## Tech stack
- Kotlin + Jetpack **Compose for TV** (`androidx.tv:tv-material3`)
- **Media3 / ExoPlayer** for playback
- Retrofit + OkHttp + kotlinx.serialization for the Emby REST API
- DataStore for persisted profiles, settings and cached launcher state
- Coil for backdrop image loading
- Go gateway with Postgres and Redis
| Layer | Built with |
| --- | --- |
| UI | Kotlin + Jetpack **Compose for TV** (`androidx.tv:tv-material3`) |
| Playback | **Media3 / ExoPlayer** |
| Networking | Retrofit + OkHttp + kotlinx.serialization, over one shared HTTP stack |
| Storage | DataStore for profiles, settings and cached launcher state |
| Images | Coil |
| Gateway | Go, with Postgres and Redis |
## Build & install
You need **JDK 17** and the **Android SDK** (Android Studio bundles both).
Open the folder in Android Studio (Giraffe/Koala or newer) and let it sync, **or** from a
terminal:
```powershell
# Android Studio writes local.properties automatically. If building from the CLI,
# point it at your SDK first:
"sdk.dir=C:\\Users\\<you>\\AppData\\Local\\Android\\Sdk" | Out-File -Encoding ascii local.properties
.\gradlew.bat assembleDebug # build the APK
.\gradlew.bat test # JVM unit tests
.\gradlew.bat installDebug # install to a connected Android TV / emulator
# Preferred for a TV already showing the Dream: clears Memby, installs, then reopens
# it so the old render surface cannot remain black. Pass -Serial when more than one
# device is attached.
.\deploy-debug.ps1 -Serial 192.168.20.3:41479
```
The APK lands in `app/build/outputs/apk/debug/app-debug.apk`.
## Server address
Memby is built for one Emby server, so the address is baked into the APK instead of being
typed on a TV remote. Set it in `gradle.properties`:
```properties
memby.serverUrl=http://192.168.1.10:8096
```
It can also come from `~/.gradle/gradle.properties` (keeps it out of the repo) or a single
build: `.\gradlew.bat assembleDebug -Pmemby.serverUrl=http://192.168.1.10:8096`.
The value becomes `BuildConfig.EMBY_SERVER_URL`, read through `data/ServerConfig.kt`. When
it is set, the setup screen only asks for a username and password, and the address wins
over whatever a saved session recorded — so moving the server is a property change plus a
reinstall, with no user action. Leaving the property **blank** restores the original
behaviour: users type the address themselves.
## First run
1. Launch **Memby** from the Android TV launcher.
2. Sign in with your Emby username and password. (If the build has no hardwired server,
enter its address first, e.g. `http://192.168.1.10:8096`.)
3. **Preview screensaver** to test it, or **Set as system screensaver** to open the TV's
screensaver settings and choose "Memby Screensaver".
## Running the gateway
See [`server/README.md`](server/README.md) to run the container. In short, the gateway
imports Emby's catalogue into Postgres (once manually, then hourly for new episodes),
composes the home rows — including "Recommended from your watching history" — and serves an
admin console at `/admin/` for imports, an offline switch, and per-row engagement.
For Tracearr-powered recommendations, create a read-only public API key in Tracearr and set
`MEMBY_TRACEARR_URL` plus `MEMBY_TRACEARR_API_KEY` on the gateway (and optionally
`MEMBY_TRACEARR_SERVER_ID` when Tracearr monitors several servers). Tracearr credentials
remain server-side; the Android app only receives ranked Emby items and short reasons. The
gateway imports compact session signals into Postgres every five minutes and prepares an
over-provisioned per-user pool, so opening For You normally performs one indexed database
read. The live on-demand path remains available for cold starts and failed rebuilds.
## Project layout
@@ -90,98 +214,21 @@ app/src/main/java/com/ponzischeme89/memby/
player/PlayerActivity.kt Media3 playback
screensaver/MembyDreamService.kt System screensaver (hosts ScreensaverContent)
admin-ui/ The operations console (React, served by nginx)
server/ The Memby gateway (Go) — see server/README.md
benchmark/ Macrobenchmarks and baseline-profile generation
```
## Build & install
You need **JDK 17** and the **Android SDK** (Android Studio bundles both).
Open the folder in Android Studio (Giraffe/Koala or newer) and let it sync, **or** from a
terminal:
```powershell
# Android Studio writes local.properties automatically. If building from the CLI,
# point it at your SDK first:
"sdk.dir=C:\\Users\\<you>\\AppData\\Local\\Android\\Sdk" | Out-File -Encoding ascii local.properties
.\gradlew.bat assembleDebug # build the APK
.\gradlew.bat installDebug # install to a connected Android TV / emulator
# Preferred for a TV already showing the Dream: clears Memby, installs, then reopens
# it so the old render surface cannot remain black. Pass -Serial when more than one
# device is attached.
.\deploy-debug.ps1 -Serial 192.168.20.3:41479
```
The APK lands in `app/build/outputs/apk/debug/app-debug.apk`.
## Two ways to run
The client can talk to Emby directly, or through the **Memby gateway** — a Go service in
`server/` that runs in Docker alongside Postgres and Redis and owns auth, caching, search
and the shaping of TV screens. With a gateway the launcher is one request instead of four,
and the TV holds a revocable gateway token rather than a live Emby token.
```
direct: TV ──────────────────────────────► Emby
gateway: TV ──► Memby gateway ──► Emby (metadata + artwork)
TV ─────────────────────► Emby (video stream; never proxied by the gateway)
```
Which one a build uses is decided by `memby.gatewayUrl` in `gradle.properties`: set it and
the app is a thin client; leave it blank and nothing changes from the direct path below.
See [`server/README.md`](server/README.md) to run the container.
The gateway also imports Emby's catalogue into Postgres (once manually, then hourly for
new episodes), composes the home rows — including "Recommended from your watching
history" — and has an admin page at `/admin/` for imports, an offline switch, and
per-row engagement.
For Tracearr-powered recommendations, create a read-only public API key in Tracearr and
set `MEMBY_TRACEARR_URL` plus `MEMBY_TRACEARR_API_KEY` on the gateway (and optionally
`MEMBY_TRACEARR_SERVER_ID` when Tracearr monitors several servers). Tracearr credentials
remain server-side; the Android app only receives ranked Emby items and short reasons.
The gateway imports compact session signals into Postgres every five minutes and prepares
an over-provisioned per-user pool, so opening For You normally performs one indexed
database read. The live on-demand path remains available for cold starts and failed
rebuilds.
## Server address
Memby is built for one Emby server, so the address is baked into the APK instead of being
typed on a TV remote. Set it in `gradle.properties`:
```properties
memby.serverUrl=http://192.168.1.10:8096
```
It can also come from `~/.gradle/gradle.properties` (keeps it out of the repo) or a single
build: `.\gradlew.bat assembleDebug -Pmemby.serverUrl=http://192.168.1.10:8096`.
The value becomes `BuildConfig.EMBY_SERVER_URL`, read through
`data/ServerConfig.kt`. When it is set, the setup screen only asks for a username and
password, and the address wins over whatever a saved session recorded — so moving the
server is a property change plus a reinstall, with no user action. Leaving the property
**blank** restores the original behaviour: users type the address themselves.
## First run
1. Launch **Memby** from the Android TV launcher.
2. Sign in with your Emby username and password. (If the build has no hardwired server,
enter its address first, e.g. `http://192.168.1.10:8096`.)
3. **Preview screensaver** to test it, or **Set as system screensaver** to open the TV's
screensaver settings and choose "Memby Screensaver".
## Distributing builds
Memby is handed out as an APK from your own web server or NAS, and updates itself from
the same folder.
Memby is handed out as an APK from your own web server or NAS, and updates itself from the
same folder.
### One-time: a signing key
Android identifies an app by `applicationId` **plus signing key**. Every update must be
signed with the *same* key, or the TV rejects it as a different app. Lose the key and
every user has to uninstall and reinstall.
signed with the *same* key, or the TV rejects it as a different app. Lose the key and every
user has to uninstall and reinstall.
```powershell
keytool -genkeypair -v -keystore memby-release.jks -alias memby `
@@ -198,30 +245,23 @@ memby.keyAlias=memby
memby.keyPassword=
```
Without these, `assembleRelease` still builds but the APK is unsigned and will not
install. The build prints a warning saying so.
Without these, `assembleRelease` still builds but the APK is unsigned and will not install.
The build prints a warning saying so.
### Docker release builder (recommended)
### NAS deployment release (recommended)
The backend Compose project includes an isolated `memby-builder` service. It keeps the
Android SDK out of the gateway image, fetches the newest semantic GitHub tag, builds with
the tagged source's Gradle wrapper, signs with the existing externally mounted keystore,
verifies the APK and publishes it into the gateway's existing release volume and update
policy. Admin Console → App updates provides a **Build latest release** button, optional
exact-tag fallback, required-update confirmation, live status and build output.
After the one-time secret-file setup in [`builder/README.md`](builder/README.md), use the
Admin Console button. The Linux/NAS command-line fallback remains:
```sh
docker compose run --rm --build memby-builder release
```
`deploy-server.ps1` builds and publishes the TV app in the same operation that replaces the
gateway: it signs with the existing keystore, verifies the APK and publishes it through
`POST /admin/api/release` once the new gateway is healthy. A pushed semantic tag does the
same thing through `.gitea/workflows/release.yml`. The gateway builds nothing itself.
The signed APK and its checksum are stored in the `memby-releases` volume at
`/data/releases/memby-<version>.apk` and
`/data/releases/memby-<version>.apk.sha256`. Gradle downloads are retained in the
`memby-gradle-cache` volume. The keystore and credentials remain read-only Compose secrets
under `/run/secrets` and are never copied into the image or repository.
`/data/releases/memby-<version>.apk` and `/data/releases/memby-<version>.apk.sha256`. The
release-publish credential is a read-only Compose secret under `/run/secrets` and is never
copied into the image or repository.
Admin Console → App updates sets the update policy — latest version, APK URL, release
notes, and the required/destructive toggles.
### Local PowerShell release (compatibility path)
@@ -231,17 +271,16 @@ under `/run/secrets` and are never copied into the image or repository.
-SourceUrl https://g.sublogue.com/admin/memby
```
That existing workflow still bumps `versionName`/`versionCode`, runs the tests, builds a
signed APK, and fills
`dist/out/` with:
That workflow bumps `versionName`/`versionCode`, runs the tests, builds a signed APK, and
fills `dist/out/` with:
```
index.html the page people are sent to
latest.json the update manifest the app polls
memby-0.1.54.apk the build
index.html the page people are sent to
latest.json the update manifest the app polls
memby-0.1.54.apk the build
memby-0.1.54.apk.sha256 the SHA-256 checksum
LICENSE GNU GPL v2 terms
NOTICE copyright and third-party acknowledgements
LICENSE GNU GPL v2 terms
NOTICE copyright and third-party acknowledgements
```
Copy those to the folder the NAS serves. Old APKs can stay alongside — only `latest.json`
@@ -252,8 +291,8 @@ decides what the app offers, so rolling back is editing one file.
The gateway checks the running app version on launch and returns an optional or mandatory
update with its download address. Memby downloads the APK, verifies its checksum, package
and release signature, then commits it through Android's package installer. Update policy
and the download address are managed in the gateway's admin console; viewers do not need
to configure an update source on the television.
and the download address are managed in the gateway's admin console; viewers do not need to
configure an update source on the television.
First install on each TV still has to be manual — the **Downloader** app pointed at the
landing page is the usual route, and the page explains it.
@@ -267,31 +306,31 @@ different app, so on every TV:
1. Install v0.1.53 — it appears as a **second** Memby entry in the launcher.
2. Sign in again; the previous session does not carry over.
3. Uninstall the old app: `adb uninstall com.mattcohen.embyclientsname`.
4. Re-select Memby in the TV's Screensaver settings — the Dream's component name changed
as well, so the old selection no longer resolves.
4. Re-select Memby in the TV's Screensaver settings — the Dream's component name changed as
well, so the old selection no longer resolves.
## Notes & limitations
- Playback negotiates through Emby's `PlaybackInfo` endpoint, then streams directly from
Emby. The Wholphin/Jellyfin-derived capability engine reports Android's H.264 and HEVC
profiles, maximum levels and resolutions rather than assuming every decoder handles every
file. The Moonfin-derived audio path reports formats the TV can decode or bitstream, offers
automatic receiver detection and manual per-codec passthrough overrides, and decodes the
rest to PCM through Media3's FFmpeg renderer. Emby can preserve a supported video stream
while converting only incompatible audio or subtitles. If a vendor decoder still fails,
Media3 tries another decoder and Memby ultimately requests an H.264 HLS transcode instead
of abandoning playback.
- Cleartext HTTP is enabled so local `http://` servers work out of the box. For an HTTPS-only
server this is unnecessary but harmless.
file. The Moonfin-derived audio path reports formats the TV can decode or bitstream,
offers automatic receiver detection and manual per-codec passthrough overrides, and
decodes the rest to PCM through Media3's FFmpeg renderer. Emby can preserve a supported
video stream while converting only incompatible audio or subtitles. If a vendor decoder
still fails, Media3 tries another decoder and Memby ultimately requests an H.264 HLS
transcode instead of abandoning playback.
- Cleartext HTTP is enabled so local `http://` servers work out of the box. For an
HTTPS-only server this is unnecessary but harmless.
- The device is remembered across sign-outs (stable `DeviceId`); credentials are cleared.
## Licence
Memby is free software licensed under the [GNU General Public License v2](LICENSE).
Copyright and third-party acknowledgements are recorded in [NOTICE](NOTICE). Distributed
APKs and server binaries must be accompanied by the corresponding source in accordance
with GPLv2. The Android TV app also exposes the source link, notices, and complete licence
under **Settings → About / Licences**.
APKs and server binaries must be accompanied by the corresponding source in accordance with
GPLv2. The Android TV app also exposes the source link, notices, and complete licence under
**Settings → About / Licences**.
Playback capability probing and device-profile generation contain GPLv2 adaptations from
[Wholphin](https://github.com/damontecres/Wholphin), itself derived in part from