Files
silo-server/internal/notifications/item_kind.go
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

71 lines
2.8 KiB
Go

package notifications
// Availability-fact tables for flat item kinds. Movies keep the dedicated
// table they launched with; every later kind shares item_availability, which
// carries a kind discriminator column.
const (
movieAvailabilityTable = "movie_availability"
itemAvailabilityTable = "item_availability"
)
// flatItemKind describes one non-episode release-event kind: a single
// media_items row becomes newly available and is announced broadcast-only
// (server channels), with no per-profile fanout. Movies were the first flat
// kind; audiobooks and ebooks followed. Adding another kind means one entry
// here, one AvailabilityKinds flag wired in the library-ingest executor, one
// notify_new_* channel column, and a migration widening the release_events
// kind CHECK constraints.
type flatItemKind struct {
// Kind is the release_events.kind / seed-state value, and doubles as the
// dedupe-key prefix ("audiobook:{library_id}:{item_id}").
Kind string
// ItemType is the media_items.type value the kind announces. It doubles
// as the display noun in copy ("New audiobook available on Silo").
ItemType string
// AvailabilityTable receives the one-way "first became available in this
// library" facts for the kind.
AvailabilityTable string
// WantsToggle reads the channel's notify_new_* toggle for the kind.
WantsToggle func(ServerChannel) bool
// Selected reads the ingest scope's AvailabilityKinds flag for the kind.
Selected func(AvailabilityKinds) bool
}
// flatItemKinds is the ordered registry of every flat release-event kind.
// Order affects only fixture/sample ordering.
var flatItemKinds = []flatItemKind{
{
Kind: EventKindMovie,
ItemType: mediaTypeMovie,
AvailabilityTable: movieAvailabilityTable,
WantsToggle: func(c ServerChannel) bool { return c.NotifyNewMovies },
Selected: func(k AvailabilityKinds) bool { return k.Movies },
},
{
Kind: EventKindAudiobook,
ItemType: mediaTypeAudiobook,
AvailabilityTable: itemAvailabilityTable,
WantsToggle: func(c ServerChannel) bool { return c.NotifyNewAudiobooks },
Selected: func(k AvailabilityKinds) bool { return k.Audiobooks },
},
{
Kind: EventKindEbook,
ItemType: mediaTypeEbook,
AvailabilityTable: itemAvailabilityTable,
WantsToggle: func(c ServerChannel) bool { return c.NotifyNewEbooks },
Selected: func(k AvailabilityKinds) bool { return k.Ebooks },
},
}
// flatKindByString looks up a registry entry by event kind. ok is false for
// episode and for unknown kinds (a newer node's kind string reaching an old
// node), so callers skip rather than misrender what they cannot describe.
func flatKindByString(kind string) (flatItemKind, bool) {
for _, k := range flatItemKinds {
if k.Kind == kind {
return k, true
}
}
return flatItemKind{}, false
}