Memby is handed out from a NAS or web server rather than a store, so:
- UpdateChecker gains a static-manifest source alongside Gitea. A .json
update URL is read as a manifest ({version, apkUrl, notes}); anything
else is still treated as a Gitea host. apkUrl may be relative and is
resolved against the manifest's own URL.
- Release signing config reads memby.keystore from local.properties or
the environment. Without it the build still succeeds but warns loudly:
an unsigned APK will not install, and a changed key forces every user
to uninstall before they can update.
- release.ps1 bumps the version, runs tests, builds a signed APK and
assembles dist/out/ (landing page, latest.json, versioned APK) ready to
copy onto the NAS.
- Keystores and dist/out are gitignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.8 KiB
Memby (Android TV)
An independent Android TV client for Emby, by ponzischeme89. Sign in to browse your personalised home screen, resume titles, play recent movies, and choose audio or subtitle tracks during playback. The screensaver (Dream) code remains isolated from the client work.
Features
- System screensaver via
DreamService— auto-starts on idle once selected in the TV's Screensaver settings. Also previewable from the app's home screen. - Random, cross-fading backdrops of Movies and Series that have backdrop images.
- Interactive: OK reveals Play / Favorite actions; ◄ ► change the backdrop; Back exits.
- In-app playback with Media3/ExoPlayer (movies play directly; a series plays its next-up / first episode).
- Favorites managed through the Emby API and shown on the home screen.
Tech stack
- Kotlin + Jetpack Compose for TV (
androidx.tv:tv-material) - Media3 / ExoPlayer for playback
- Retrofit + OkHttp + kotlinx.serialization for the Emby REST API
- DataStore for persisted connection/session
- Coil for backdrop image loading
Project layout
app/src/main/java/com/ponzischeme89/memby/
MembyApp.kt Application; initialises the ServiceLocator
ServiceLocator.kt Manual DI (settings + repository)
data/
SettingsStore.kt DataStore-backed connection/session state
ServerConfig.kt Which backend this build talks to
EmbyRepository.kt Content, favorites, playback; dual gateway/direct paths
analytics/RowAnalytics.kt Row engagement buffering
model/ Emby + gateway DTOs (kotlinx.serialization)
remote/EmbyApi.kt Retrofit interface for Emby
remote/GatewayApi.kt Retrofit interface for the Memby gateway
ui/
MainActivity.kt Setup, profiles, home
HomeViewModel.kt Home state, row analytics, refresh
HomeComponents.kt Navigation rail, rows, cards
MaintenanceScreen.kt Full-screen offline state
settings/SettingsSheet.kt Settings panel
screensaver/ Shared slideshow + in-app preview host
player/PlayerActivity.kt Media3 playback
screensaver/MembyDreamService.kt System screensaver (hosts ScreensaverContent)
server/ The Memby gateway (Go) — see server/README.md
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:
# 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, always direct-play)
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 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.
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:
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
- Launch Memby from the Android TV launcher.
- 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.) - 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.
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.
keytool -genkeypair -v -keystore memby-release.jks -alias memby `
-keyalg RSA -keysize 4096 -validity 10000
Keep the .jks somewhere backed up and outside the repo (*.jks is gitignored), then
point local.properties at it — also gitignored:
memby.keystore=C:/keys/memby-release.jks
memby.keystorePassword=…
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.
Each release
.\release.ps1 -Version 0.1.54 -Notes "Faster home screen" -BaseUrl https://nas.example.com/memby
That 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
Copy those to the folder the NAS serves. Old APKs can stay alongside — only latest.json
decides what the app offers, so rolling back is editing one file.
How TVs update themselves
In Memby's Settings, set the update URL to https://nas.example.com/memby/latest.json.
Check for updates then downloads and installs on the TV, no computer involved. A URL
ending in .json is read as a static manifest; anything else is treated as a Gitea host
(/api/v1/repos/{owner}/{repo}/releases/latest), so either source works.
The manifest's apkUrl may be relative (memby-0.1.54.apk) and is resolved against the
manifest's own URL, so the folder keeps working if the NAS is reached by another name.
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.
Upgrading to v0.1.53
The package and install identity both became com.ponzischeme89.memby in this release
(previously com.mattcohen.embyclientsname). Android treats a new applicationId as a
different app, so on every TV:
- Install v0.1.53 — it appears as a second Memby entry in the launcher.
- Sign in again; the previous session does not carry over.
- Uninstall the old app:
adb uninstall com.mattcohen.embyclientsname. - 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 uses Emby's direct stream (
/Videos/{id}/stream?static=true). This direct-plays containers/codecs ExoPlayer supports (most MP4/H.264, many MKV). Server-side transcoding is not requested; unusual codecs may need it — a future enhancement is to call Emby'sPlaybackInfoendpoint and use the returned HLS transcode URL (themedia3-exoplayer-hlsdependency is already included). - 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.