Files
memby/server/internal/store/schema.sql
T

105 lines
4.9 KiB
SQL
Raw Normal View History

-- 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 '',
2026-07-27 21:06:51 +12:00
device_name TEXT NOT NULL DEFAULT 'Memby TV',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
2026-07-27 21:06:51 +12:00
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS device_name TEXT NOT NULL DEFAULT 'Memby TV';
-- Older builds could create more than one token for the same physical TV. Keep the most
-- recently used row before adding the identity constraint.
DELETE FROM sessions older
USING sessions newer
WHERE older.emby_user_id = newer.emby_user_id
AND older.device_id = newer.device_id
AND (
older.last_seen_at < newer.last_seen_at
OR (older.last_seen_at = newer.last_seen_at AND older.token_hash < newer.token_hash)
);
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);
2026-07-27 21:06:51 +12:00
CREATE UNIQUE INDEX IF NOT EXISTS sessions_user_device_idx
ON sessions (emby_user_id, device_id);
-- 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);