App v0.2.26 and gateway 0.1.20

Client: seek controls, Bazarr subtitle download and cast panel in the
player; MDBList ratings strip; episode and schedule detail pages; series
pace estimate; what's new panel; install-permission onboarding step;
synced per-profile preferences; Emby outage banner.

Gateway: rebuilt admin console (one fragment per page), preference
history and restore, merged Continue Watching, Emby health probe,
subtitle selection and Bazarr download, structured request logging with
per-request identity, and embedded build version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ponzischeme89
2026-08-06 22:33:56 +12:00
co-authored by Claude Opus 5
parent 2675e6d82b
commit 4a4df7a73c
257 changed files with 24868 additions and 3108 deletions
+109
View File
@@ -39,6 +39,24 @@ CREATE INDEX IF NOT EXISTS sessions_last_seen_idx ON sessions (last_seen_at);
CREATE UNIQUE INDEX IF NOT EXISTS sessions_user_device_idx
ON sessions (emby_user_id, device_id);
-- Every app build a television has been seen running.
--
-- A session row carries only the version in force right now, which is overwritten by the
-- next call that reports a different one, so on its own the answer to "what has this set
-- been running" is one value deep. This is keyed on device_id alone because the history
-- belongs to the television rather than to whoever is signed into it, and it outlives a
-- sign-out: the set is the same set when it comes back.
CREATE TABLE IF NOT EXISTS device_versions (
device_id TEXT NOT NULL,
client_version TEXT NOT NULL,
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (device_id, client_version)
);
CREATE INDEX IF NOT EXISTS device_versions_recent_idx
ON device_versions (device_id, last_seen_at DESC);
-- The imported library.
--
-- payload is Emby's item JSON verbatim, so rows served from here are byte-identical to
@@ -69,6 +87,34 @@ CREATE INDEX IF NOT EXISTS library_items_genres_idx ON library_items USING GIN (
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);
-- Durable raw MDBList responses. Source selection and display formatting happen at read
-- time, so changing the visible sources does not require another external API request.
CREATE TABLE IF NOT EXISTS external_media_ratings (
media_type TEXT NOT NULL,
provider TEXT NOT NULL,
provider_id TEXT NOT NULL,
ratings JSONB NOT NULL DEFAULT '[]'::jsonb,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (media_type, provider, provider_id)
);
CREATE INDEX IF NOT EXISTS external_media_ratings_fetched_idx
ON external_media_ratings (fetched_at);
-- Emby item id -> external provider identity, learned as televisions navigate.
--
-- The rating itself is keyed by the provider's id, which Emby only reveals in a
-- ProviderIds lookup. Remembering the answer is what lets a home row attach ratings to
-- forty cards from one indexed read instead of forty Emby requests, and it works for
-- items the library import has not yet re-read.
CREATE TABLE IF NOT EXISTS item_rating_refs (
item_id TEXT PRIMARY KEY,
media_type TEXT NOT NULL,
provider TEXT NOT NULL,
provider_id TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 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,
@@ -294,3 +340,66 @@ CREATE UNIQUE INDEX IF NOT EXISTS for_you_candidates_user_rank_idx
ON for_you_candidates (emby_user_id, base_rank);
CREATE INDEX IF NOT EXISTS for_you_candidates_user_runtime_rank_idx
ON for_you_candidates (emby_user_id, runtime_minutes, base_rank);
-- One viewer's TV settings, so they follow the person rather than the television. The
-- document is opaque here on purpose: the vocabulary lives in internal/api next to the
-- client contract, so adding a setting never needs a migration. What this table owns is
-- the revision, which is how a TV notices from the status poll alone that an operator (or
-- another television) changed something.
CREATE TABLE IF NOT EXISTS user_preferences (
emby_user_id TEXT PRIMARY KEY,
preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
revision BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
source TEXT NOT NULL DEFAULT 'device'
);
-- Every accepted write of the document above, so the operator can read what changed, who
-- changed it, and put a previous version back.
--
-- The document is stored whole rather than as a delta. A delta would have to be
-- interpreted against a vocabulary that lives in internal/api and can gain a setting
-- between two revisions, and restoring one would then mean replaying a chain; a whole
-- document is restorable on its own and normalised on the way out. History is capped per
-- person at write time (preferenceHistoryLimit) — this is a household, and the value of an
-- entry falls off a cliff once nobody remembers the change.
CREATE TABLE IF NOT EXISTS user_preference_revisions (
emby_user_id TEXT NOT NULL,
revision BIGINT NOT NULL,
preferences JSONB NOT NULL,
source TEXT NOT NULL DEFAULT 'device',
-- Which television wrote it, captured at write time rather than joined from sessions:
-- a set that has since been signed out still has to be nameable in the history.
device_id TEXT NOT NULL DEFAULT '',
device_name TEXT NOT NULL DEFAULT '',
client_version TEXT NOT NULL DEFAULT '',
-- The revision this one was restored from, when it was. Never a rewind: a restore is
-- a new revision carrying an old document, because the revision is what tells a TV
-- something changed and one that went backwards would leave every set believing it
-- was already up to date.
restored_from BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (emby_user_id, revision)
);
CREATE INDEX IF NOT EXISTS user_preference_revisions_user_idx
ON user_preference_revisions (emby_user_id, revision DESC);
-- Which televisions have actually taken a revision, recorded when a set fetches the
-- document rather than when the server writes it.
--
-- The status poll carries the revision to every open TV, but a TV being told is not a TV
-- having adopted: it may be switched off, mid-film, or unable to reach /v1/preferences.
-- The fetch is the only proof, so it is what writes here.
CREATE TABLE IF NOT EXISTS user_preference_acks (
emby_user_id TEXT NOT NULL,
device_id TEXT NOT NULL,
revision BIGINT NOT NULL,
device_name TEXT NOT NULL DEFAULT '',
client_version TEXT NOT NULL DEFAULT '',
acked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (emby_user_id, device_id, revision)
);
CREATE INDEX IF NOT EXISTS user_preference_acks_user_revision_idx
ON user_preference_acks (emby_user_id, revision DESC);