This commit is contained in:
ponzischeme89
2026-08-20 15:06:00 +12:00
parent 549f9c5eed
commit f1164db2c5
52 changed files with 5441 additions and 158 deletions
+79
View File
@@ -108,6 +108,12 @@ 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);
-- Every episode of one series, which is what a viewer's Next Up walks and what a series
-- card's watched count is computed from. Both run on the tail of an ordinary request, and
-- without this each is a scan of every episode in the library.
CREATE INDEX IF NOT EXISTS library_items_series_episodes_idx
ON library_items (series_id) WHERE type = 'Episode';
-- 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 (
@@ -903,3 +909,76 @@ CREATE INDEX IF NOT EXISTS notification_log_user_idx
ON notification_log (emby_user_id, occurred_at DESC) WHERE emby_user_id <> '';
CREATE INDEX IF NOT EXISTS notification_log_status_idx ON notification_log (status, occurred_at DESC);
CREATE INDEX IF NOT EXISTS notification_log_kind_idx ON notification_log (kind, occurred_at DESC);
-- Viewers: the people using one Memby account.
--
-- A Memby account is the household's relationship with an Emby user; a viewer is one
-- person under it. Every account has exactly one MAIN viewer, whose state is Emby's and
-- which behaves exactly as the account did before viewers existed, and any number of
-- SHADOW viewers whose state is Memby's alone.
--
-- The main viewer's id IS the Emby user id, and that is the whole of why this feature
-- needed no migration. Every table in this schema keys a person by a bare emby_user_id
-- with no foreign key behind it, so substituting a viewer id for it leaves an existing
-- household's preferences, notifications, followed shows, search history and row stats
-- exactly where they were. A shadow id is prefixed 'v' and is therefore distinguishable
-- from Emby's 32-hex GUIDs by inspection, which is what makes that substitution safe.
CREATE TABLE IF NOT EXISTS viewers (
id TEXT PRIMARY KEY,
emby_user_id TEXT NOT NULL,
name TEXT NOT NULL,
short_name TEXT NOT NULL DEFAULT '',
colour TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL, -- main | shadow
pin_hash BYTEA,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS viewers_account_idx ON viewers (emby_user_id, created_at);
-- One main viewer per account, enforced rather than assumed: the main viewer is what a
-- request falls back to, so an account with two of them would resolve differently
-- depending on which row a query happened to return first.
CREATE UNIQUE INDEX IF NOT EXISTS viewers_account_main_idx
ON viewers (emby_user_id) WHERE kind = 'main';
-- A shadow viewer's own viewing state, in the shape of the Emby UserData block it stands
-- in for. Only the fields Memby actually renders are here: the Emby item id is the common
-- identifier, so no library metadata is duplicated and nothing here needs invalidating
-- when the catalogue changes.
--
-- There is deliberately no row for a main viewer. Their state lives in Emby, and a copy
-- of it here would be a second answer free to disagree with the one the household's other
-- Emby clients see.
CREATE TABLE IF NOT EXISTS viewer_playback_state (
viewer_id TEXT NOT NULL,
item_id TEXT NOT NULL,
series_id TEXT NOT NULL DEFAULT '',
season_id TEXT NOT NULL DEFAULT '',
position_ticks BIGINT NOT NULL DEFAULT 0,
runtime_ticks BIGINT NOT NULL DEFAULT 0,
played BOOLEAN NOT NULL DEFAULT false,
play_count INT NOT NULL DEFAULT 0,
favourite BOOLEAN NOT NULL DEFAULT false,
hidden_from_resume BOOLEAN NOT NULL DEFAULT false,
last_played_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (viewer_id, item_id)
);
-- Continue Watching for a shadow viewer is this index: what they are part-way through,
-- most recent first. The partial predicate keeps it to the rows that row can draw from
-- rather than to everything they have ever pressed Play on.
CREATE INDEX IF NOT EXISTS viewer_playback_resume_idx
ON viewer_playback_state (viewer_id, last_played_at DESC)
WHERE position_ticks > 0 AND NOT played AND NOT hidden_from_resume;
-- Next Up walks a series' episodes for the newest completion; favourites are their own
-- row, and both are asked for per viewer.
CREATE INDEX IF NOT EXISTS viewer_playback_series_idx
ON viewer_playback_state (viewer_id, series_id, last_played_at DESC)
WHERE series_id <> '';
CREATE INDEX IF NOT EXISTS viewer_playback_favourite_idx
ON viewer_playback_state (viewer_id, updated_at DESC) WHERE favourite;