Files
silo-server/migrations/sql/20260702033712_audiobook_ebook_notifications.sql
866392fecd feat(notifications): announce new audiobooks and ebooks on server channels (#260)
Audiobook and ebook libraries previously never entered the Recently
Added pipeline: availability detection only ran for TV/movie/mixed
libraries and release_events only knew episode/movie kinds, so server
channels (Discord/generic webhooks) could not announce new audiobooks
or ebooks.

Generalize the movie path into a flat-item-kind registry
(internal/notifications/item_kind.go) driving availability detection,
recording, channel toggles, payload rendering, test fixtures, and the
admin backfill seeder. New kinds share a kind-discriminated
item_availability table; movie_availability stays as-is. Channels gain
notify_new_audiobooks/notify_new_ebooks toggles (default on, additive
API fields) and embeds carry the author from item_people. Flood-safe by
construction: existing libraries seed silently on their first
post-upgrade full scan.

Extract internal/librarykind to replace the is*LibraryType helper
copies that had drifted across scanner, libraryingest, and metadata
(metadata's movie check silently included mixed; now spelled
explicitly).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:42:28 -04:00

66 lines
3.0 KiB
SQL

-- +goose Up
-- +goose StatementBegin
-- Audiobook and ebook "recently added" notifications. Both kinds are flat
-- item kinds like movies (item_id only, no series/episode columns): they
-- widen the existing item-shaped branch of the release_events constraints
-- and get their own server-channel toggles.
ALTER TABLE public.release_events
DROP CONSTRAINT release_events_kind_check,
DROP CONSTRAINT release_events_kind_shape_check;
ALTER TABLE public.release_events
ADD CONSTRAINT release_events_kind_check
CHECK (kind IN ('episode', 'movie', 'audiobook', 'ebook')),
ADD CONSTRAINT release_events_kind_shape_check CHECK (
(kind = 'episode' AND series_id IS NOT NULL AND episode_id IS NOT NULL
AND season_number IS NOT NULL AND episode_number IS NOT NULL
AND episode_key IS NOT NULL)
OR (kind IN ('movie', 'audiobook', 'ebook') AND item_id IS NOT NULL)
);
-- Generic flat-item availability facts for every kind after movie: one-way
-- "item first became available in this library" rows, movie_availability's
-- shape plus a kind discriminator. Movies deliberately stay in their original
-- table (renaming a populated hot-path table buys nothing); a future kind
-- widens the kind CHECKs here and on release_events, no new table.
CREATE TABLE public.item_availability (
library_id integer NOT NULL,
item_id text NOT NULL,
kind text NOT NULL,
available_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT item_availability_pkey PRIMARY KEY (library_id, item_id, kind),
CONSTRAINT item_availability_kind_check CHECK (kind IN ('audiobook', 'ebook'))
);
-- Per-channel content toggles for the new kinds, defaulting on like the
-- movie/episode toggles. No back-catalog flood risk: existing audiobook and
-- ebook libraries have no notification_content_seed_state rows for these
-- kinds yet, so their first full scan seeds silently.
ALTER TABLE public.notification_server_channels
ADD COLUMN notify_new_audiobooks boolean NOT NULL DEFAULT true,
ADD COLUMN notify_new_ebooks boolean NOT NULL DEFAULT true;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE public.notification_server_channels
DROP COLUMN IF EXISTS notify_new_ebooks,
DROP COLUMN IF EXISTS notify_new_audiobooks;
DROP TABLE IF EXISTS public.item_availability;
DELETE FROM public.release_events WHERE kind IN ('audiobook', 'ebook');
ALTER TABLE public.release_events
DROP CONSTRAINT IF EXISTS release_events_kind_shape_check,
DROP CONSTRAINT IF EXISTS release_events_kind_check;
ALTER TABLE public.release_events
ADD CONSTRAINT release_events_kind_check CHECK (kind IN ('episode', 'movie')),
ADD CONSTRAINT release_events_kind_shape_check CHECK (
(kind = 'episode' AND series_id IS NOT NULL AND episode_id IS NOT NULL
AND season_number IS NOT NULL AND episode_number IS NOT NULL
AND episode_key IS NOT NULL)
OR (kind = 'movie' AND item_id IS NOT NULL)
);
-- +goose StatementEnd