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:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user