456 lines
22 KiB
SQL
456 lines
22 KiB
SQL
-- 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 '',
|
|
device_name TEXT NOT NULL DEFAULT 'Memby TV',
|
|
client_version TEXT NOT NULL DEFAULT '',
|
|
client_protocol TEXT NOT NULL DEFAULT '',
|
|
client_capabilities TEXT[] NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS device_name TEXT NOT NULL DEFAULT 'Memby TV';
|
|
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS client_version TEXT NOT NULL DEFAULT '';
|
|
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS client_protocol TEXT NOT NULL DEFAULT '';
|
|
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS client_capabilities TEXT[] NOT NULL DEFAULT '{}';
|
|
|
|
-- 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);
|
|
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
|
|
-- 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);
|
|
|
|
-- 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,
|
|
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);
|
|
|
|
-- Search terms are retained separately from row engagement so they can inform future
|
|
-- ranking/recommendation work without coupling that analysis to rendered rows.
|
|
CREATE TABLE IF NOT EXISTS search_history (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
emby_user_id TEXT NOT NULL,
|
|
query TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS search_history_user_time_idx
|
|
ON search_history (emby_user_id, occurred_at DESC);
|
|
|
|
-- A user's explicitly followed TV series. Unlike library_items this is intentionally
|
|
-- user-scoped: following a show is a Memby preference, not Emby library state.
|
|
CREATE TABLE IF NOT EXISTS user_shows (
|
|
emby_user_id TEXT NOT NULL,
|
|
item_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
year INT,
|
|
image_tag TEXT NOT NULL DEFAULT '',
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (emby_user_id, item_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS user_shows_user_added_idx
|
|
ON user_shows (emby_user_id, added_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_notification_preferences (
|
|
emby_user_id TEXT PRIMARY KEY,
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
show_return_alerts BOOLEAN NOT NULL DEFAULT true,
|
|
lead_days INT NOT NULL DEFAULT 7 CHECK (lead_days BETWEEN 1 AND 30),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Notifications are materialised so read/dismissed state follows the user to every TV.
|
|
-- source_key is deterministic, preventing the same return date from being announced
|
|
-- again whenever the app refreshes.
|
|
CREATE TABLE IF NOT EXISTS user_notifications (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
emby_user_id TEXT NOT NULL,
|
|
source_key TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
item_id TEXT NOT NULL DEFAULT '',
|
|
title TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
event_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
read_at TIMESTAMPTZ,
|
|
dismissed_at TIMESTAMPTZ,
|
|
UNIQUE (emby_user_id, source_key)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS user_notifications_user_created_idx
|
|
ON user_notifications (emby_user_id, created_at DESC);
|
|
|
|
-- Recommendation-relevant Tracearr history. The public Tracearr API has no user or
|
|
-- since cursor, so stable source ids make these rows the durable deduplication boundary.
|
|
-- Deliberately omit artwork, stream-detail blobs and other fields unused by ranking.
|
|
CREATE TABLE IF NOT EXISTS tracearr_sessions (
|
|
server_id TEXT NOT NULL DEFAULT '',
|
|
tracearr_session_id TEXT NOT NULL,
|
|
tracearr_user_id TEXT NOT NULL DEFAULT '',
|
|
username TEXT NOT NULL DEFAULT '',
|
|
state TEXT NOT NULL DEFAULT '',
|
|
media_type TEXT NOT NULL DEFAULT '',
|
|
media_title TEXT NOT NULL DEFAULT '',
|
|
show_title TEXT NOT NULL DEFAULT '',
|
|
season_number INT,
|
|
episode_number INT,
|
|
production_year INT,
|
|
started_at TIMESTAMPTZ,
|
|
stopped_at TIMESTAMPTZ,
|
|
duration_ms BIGINT NOT NULL DEFAULT 0,
|
|
progress_ms BIGINT NOT NULL DEFAULT 0,
|
|
total_duration_ms BIGINT NOT NULL DEFAULT 0,
|
|
watched BOOLEAN NOT NULL DEFAULT false,
|
|
device TEXT NOT NULL DEFAULT '',
|
|
player TEXT NOT NULL DEFAULT '',
|
|
product TEXT NOT NULL DEFAULT '',
|
|
platform TEXT NOT NULL DEFAULT '',
|
|
is_transcode BOOLEAN NOT NULL DEFAULT false,
|
|
video_decision TEXT NOT NULL DEFAULT '',
|
|
audio_decision TEXT NOT NULL DEFAULT '',
|
|
source_video_codec TEXT NOT NULL DEFAULT '',
|
|
source_audio_codec TEXT NOT NULL DEFAULT '',
|
|
emby_item_id TEXT NOT NULL DEFAULT '',
|
|
emby_series_id TEXT NOT NULL DEFAULT '',
|
|
source_fingerprint BYTEA NOT NULL,
|
|
source_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
imported_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (server_id, tracearr_session_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS tracearr_sessions_user_time_idx
|
|
ON tracearr_sessions (tracearr_user_id, started_at DESC);
|
|
CREATE INDEX IF NOT EXISTS tracearr_sessions_username_time_idx
|
|
ON tracearr_sessions (lower(username), started_at DESC);
|
|
CREATE INDEX IF NOT EXISTS tracearr_sessions_emby_item_idx
|
|
ON tracearr_sessions (emby_item_id) WHERE emby_item_id <> '';
|
|
CREATE INDEX IF NOT EXISTS tracearr_sessions_emby_series_idx
|
|
ON tracearr_sessions (emby_series_id) WHERE emby_series_id <> '';
|
|
|
|
-- One compact derived profile per Emby user. Variable affinity maps stay together as
|
|
-- JSON because the builder reads and replaces the whole profile; no request filters
|
|
-- inside these maps.
|
|
CREATE TABLE IF NOT EXISTS recommendation_user_profiles (
|
|
emby_user_id TEXT PRIMARY KEY,
|
|
tracearr_user_id TEXT NOT NULL DEFAULT '',
|
|
tracearr_username TEXT NOT NULL DEFAULT '',
|
|
source_session_count INT NOT NULL DEFAULT 0,
|
|
mean_completion_ratio REAL NOT NULL DEFAULT 0,
|
|
typical_session_minutes INT NOT NULL DEFAULT 0,
|
|
genre_affinity JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
title_affinity JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
studio_affinity JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
context_affinity JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
codec_outcomes JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
weighted_profile JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
algorithm_version TEXT NOT NULL DEFAULT '',
|
|
signals_through TIMESTAMPTZ,
|
|
built_at TIMESTAMPTZ,
|
|
pool_built_at TIMESTAMPTZ,
|
|
dirty_since TIMESTAMPTZ DEFAULT now(),
|
|
last_error TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
ALTER TABLE recommendation_user_profiles
|
|
ADD COLUMN IF NOT EXISTS context_affinity JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
ALTER TABLE recommendation_user_profiles
|
|
ADD COLUMN IF NOT EXISTS algorithm_version TEXT NOT NULL DEFAULT '';
|
|
ALTER TABLE recommendation_user_profiles
|
|
ADD COLUMN IF NOT EXISTS weighted_profile JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
|
|
-- Explicit recommendation feedback is separate from Emby favourites: More Like This
|
|
-- changes discovery affinity, while Not for Me is a hard user-scoped exclusion.
|
|
CREATE TABLE IF NOT EXISTS recommendation_actions (
|
|
emby_user_id TEXT NOT NULL,
|
|
item_id TEXT NOT NULL,
|
|
action TEXT NOT NULL CHECK (action IN ('more_like_this', 'not_for_me')),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (emby_user_id, item_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS recommendation_actions_user_idx
|
|
ON recommendation_actions (emby_user_id, updated_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS recommendation_onboarding (
|
|
emby_user_id TEXT PRIMARY KEY,
|
|
preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS recommendation_profiles_dirty_idx
|
|
ON recommendation_user_profiles (dirty_since)
|
|
WHERE dirty_since IS NOT NULL;
|
|
|
|
-- Every eligible ranked title is retained. At household scale this is only tens of
|
|
-- thousands of compact rows and gives short runtime filters far more headroom than the
|
|
-- old 240-title request pool.
|
|
CREATE TABLE IF NOT EXISTS for_you_candidates (
|
|
emby_user_id TEXT NOT NULL,
|
|
item_id TEXT NOT NULL,
|
|
base_rank INT NOT NULL,
|
|
base_score REAL NOT NULL DEFAULT 0,
|
|
runtime_minutes INT NOT NULL DEFAULT 0,
|
|
affinity_score REAL NOT NULL DEFAULT 0,
|
|
compatibility_score REAL NOT NULL DEFAULT 0,
|
|
compatibility_label TEXT NOT NULL DEFAULT '',
|
|
reason_kind TEXT NOT NULL DEFAULT '',
|
|
reason_genre TEXT NOT NULL DEFAULT '',
|
|
reason_source_session_id TEXT NOT NULL DEFAULT '',
|
|
reason_source_item_id TEXT NOT NULL DEFAULT '',
|
|
reason_source_title TEXT NOT NULL DEFAULT '',
|
|
recommendation_reason TEXT NOT NULL DEFAULT '',
|
|
built_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (emby_user_id, item_id),
|
|
FOREIGN KEY (item_id) REFERENCES library_items(id) ON DELETE CASCADE
|
|
);
|
|
|
|
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);
|
|
|
|
-- Which colour schemes an operator has decided a particular viewer may choose from.
|
|
--
|
|
-- Deliberately not part of user_preferences: that document is the viewer's own choices and
|
|
-- is written by every television they own, where this is policy about them and is written
|
|
-- only by the console. Keeping them apart is what stops a TV pushing itself a theme it was
|
|
-- not offered simply by including the id in a settings write.
|
|
--
|
|
-- **A person with no rows here may choose anything.** Absence is permissive, because no row
|
|
-- exists for anybody until an operator restricts somebody — reading it the other way round
|
|
-- would empty every picker in the house the day this ships. It also means "allowed
|
|
-- everything" and "never configured" are stored identically, which is correct: they are the
|
|
-- same decision.
|
|
--
|
|
-- Seasonal themes are never in here. They are not grantable per person; the only switch is
|
|
-- the seasonal_themes feature flag, and it is the operator's, for the whole household.
|
|
CREATE TABLE IF NOT EXISTS user_themes (
|
|
emby_user_id TEXT NOT NULL,
|
|
theme_id TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (emby_user_id, theme_id)
|
|
);
|
|
|
|
-- Subtitles the gateway fetched itself, which is the one place Memby holds a subtitle.
|
|
--
|
|
-- Bazarr does not need this: it writes the file beside the media file, so Emby finds it
|
|
-- and the track arrives down the ordinary PlaybackInfo path. OpenSubtitles has no such
|
|
-- reach — the gateway has no access to the media directory — so a file fetched from it is
|
|
-- kept here and served back as a sidecar. That is the whole difference between the two
|
|
-- providers, and it is why this table exists at all.
|
|
--
|
|
-- It is deliberately durable rather than a cache. A subtitle somebody fetched mid-film is
|
|
-- one they will want again on the next episode of the same evening and on a rewatch a year
|
|
-- later; spending a provider's daily download quota twice for the same file would be the
|
|
-- feature working against the household. Rows are small — a subtitle is tens of kilobytes
|
|
-- — and are deleted with nothing else, because nothing else knows the file exists.
|
|
CREATE TABLE IF NOT EXISTS downloaded_subtitles (
|
|
id TEXT PRIMARY KEY,
|
|
item_id TEXT NOT NULL,
|
|
language TEXT NOT NULL,
|
|
label TEXT NOT NULL DEFAULT '',
|
|
forced BOOLEAN NOT NULL DEFAULT false,
|
|
hearing_impaired BOOLEAN NOT NULL DEFAULT false,
|
|
format TEXT NOT NULL DEFAULT 'srt',
|
|
provider TEXT NOT NULL DEFAULT '',
|
|
content BYTEA NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS downloaded_subtitles_item_idx ON downloaded_subtitles (item_id);
|