-- 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);

-- Significant, user-scoped app journeys. Values are deliberately categorical: content
-- names, search terms, setting values and other free text do not belong in this table.
-- journey_id is generated by the client for one foreground visit; emby_user_id is always
-- taken from the authenticated gateway session rather than trusted from the payload.
CREATE TABLE IF NOT EXISTS journey_events (
    id           BIGSERIAL PRIMARY KEY,
    occurred_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    emby_user_id TEXT        NOT NULL,
    journey_id   TEXT        NOT NULL,
    sequence     INT         NOT NULL DEFAULT 0,
    category     TEXT        NOT NULL,
    action       TEXT        NOT NULL,
    screen       TEXT        NOT NULL DEFAULT '',
    feature      TEXT        NOT NULL DEFAULT '',
    source       TEXT        NOT NULL DEFAULT '',
    target       TEXT        NOT NULL DEFAULT '',
    item_id      TEXT        NOT NULL DEFAULT '',
    item_type    TEXT        NOT NULL DEFAULT '',
    outcome      TEXT        NOT NULL DEFAULT ''
);

CREATE UNIQUE INDEX IF NOT EXISTS journey_events_journey_sequence_idx
    ON journey_events (emby_user_id, journey_id, sequence);
CREATE INDEX IF NOT EXISTS journey_events_user_time_idx
    ON journey_events (emby_user_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS journey_events_feature_time_idx
    ON journey_events (feature, 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);
