Memby v0.1.53: Android TV client plus gateway
Android TV client for Emby (Kotlin, Compose for TV) and the Memby gateway (Go, Postgres, Redis) that fronts it. Client: - Setup, profiles, home rows, Media3 playback, system screensaver (Dream) - Backend chosen at build time: gateway when memby.gatewayUrl is set, otherwise direct to Emby. Both paths stay working. - Server-composed home rows, rendered verbatim so new row types ship without an app release - Full-screen animated maintenance state, row engagement telemetry Gateway: - One request per TV screen; auth, caching, search and row shaping - Library import from Emby into Postgres (manual, then hourly incremental) - Recommendations from viewing history (recency-weighted genre affinity) - Admin page for imports, an offline switch, and per-row analytics - Video always direct-plays from Emby; only metadata passes through Identity is com.ponzischeme89.memby throughout, replacing com.mattcohen.embyclientsname. A changed applicationId installs as a new app: TVs need a fresh sign-in and the old package uninstalled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
-- Gateway sessions: one row per signed-in TV.
|
||||
--
|
||||
-- token_hash is SHA-256 of the bearer token handed to the device, so a database dump
|
||||
-- does not hand over working gateway tokens. emby_token IS the live upstream token and
|
||||
-- is stored as-is: treat this volume as a secret store.
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token_hash BYTEA PRIMARY KEY,
|
||||
emby_user_id TEXT NOT NULL,
|
||||
emby_token TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
server_id TEXT NOT NULL DEFAULT '',
|
||||
device_id TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS sessions_emby_user_idx ON sessions (emby_user_id);
|
||||
CREATE INDEX IF NOT EXISTS sessions_last_seen_idx ON sessions (last_seen_at);
|
||||
|
||||
-- The imported library.
|
||||
--
|
||||
-- payload is Emby's item JSON verbatim, so rows served from here are byte-identical to
|
||||
-- rows served live. Deliberately holds NO per-user state: everything is imported with
|
||||
-- EnableUserData=false, because one household shares this table and watched/favourite
|
||||
-- flags are not shareable. Anything user-specific still comes from Emby live.
|
||||
CREATE TABLE IF NOT EXISTS library_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
series_id TEXT NOT NULL DEFAULT '',
|
||||
series_name TEXT NOT NULL DEFAULT '',
|
||||
production_year INT,
|
||||
community_rating REAL,
|
||||
genres TEXT[] NOT NULL DEFAULT '{}',
|
||||
studios TEXT[] NOT NULL DEFAULT '{}',
|
||||
date_created TIMESTAMPTZ,
|
||||
search_text TEXT NOT NULL DEFAULT '',
|
||||
payload JSONB NOT NULL,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
-- 'simple' rather than 'english': film titles are proper nouns, and stemming
|
||||
-- "Arrival" into "arriv" helps nobody.
|
||||
search_tsv tsvector GENERATED ALWAYS AS (to_tsvector('simple', search_text)) STORED
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS library_items_search_idx ON library_items USING GIN (search_tsv);
|
||||
CREATE INDEX IF NOT EXISTS library_items_genres_idx ON library_items USING GIN (genres);
|
||||
CREATE INDEX IF NOT EXISTS library_items_type_created_idx ON library_items (type, date_created DESC);
|
||||
CREATE INDEX IF NOT EXISTS library_items_synced_idx ON library_items (synced_at);
|
||||
|
||||
-- One row per import, so the admin page can show what happened and when.
|
||||
CREATE TABLE IF NOT EXISTS sync_runs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
kind TEXT NOT NULL, -- full | incremental
|
||||
trigger TEXT NOT NULL DEFAULT 'schedule', -- schedule | manual | startup
|
||||
status TEXT NOT NULL, -- running | success | failed
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
finished_at TIMESTAMPTZ,
|
||||
items_seen INT NOT NULL DEFAULT 0,
|
||||
items_upserted INT NOT NULL DEFAULT 0,
|
||||
items_removed INT NOT NULL DEFAULT 0,
|
||||
error TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS sync_runs_started_idx ON sync_runs (started_at DESC);
|
||||
|
||||
-- Small key/value store for operator switches (currently just maintenance mode). Kept in
|
||||
-- Postgres rather than memory so a restart cannot silently bring the app back up.
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value JSONB NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Row-level engagement. One row per reported event; aggregation happens at read time,
|
||||
-- which is fine at household scale and keeps the write path trivial.
|
||||
CREATE TABLE IF NOT EXISTS row_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
emby_user_id TEXT NOT NULL,
|
||||
row_id TEXT NOT NULL,
|
||||
row_kind TEXT NOT NULL DEFAULT '',
|
||||
event TEXT NOT NULL, -- impression | focus | select
|
||||
item_id TEXT NOT NULL DEFAULT '',
|
||||
dwell_ms INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS row_events_time_idx ON row_events (occurred_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS row_events_row_idx ON row_events (row_id, occurred_at DESC);
|
||||
Reference in New Issue
Block a user