Files
bookhoard/internal/services
John O'Keefe 0b7ad0f376 feat(podcasts): schema, RLS policy extension, sqlc queries — feed core foundations
Podcast integration task 1.1 (plan: podcast-integration.md §6/§8, D20).

WHY: podcasts are a new first-class media domain — RSS feeds users
subscribe to personally, with episodes as media_items rows so progress,
stats and the audio player work unchanged. Visibility is per-user
(strict: only your subscriptions), which after the RLS migration
(2026-10-29) belongs in the POLICIES, not Go helpers.

Schema (idempotent — schema.sql re-executes every boot):
- 'podcasts' library type seeded (ON CONFLICT DO NOTHING, ARRAY[]::TEXT[]
  cast required — bare ARRAY[] 42802s under ON_ERROR_STOP)
- podcast_feeds: per-library feed entity (UNIQUE(library_id, feed_url)),
  full RSS/iTunes/Podcasting-2.0 metadata inventory, conditional-GET
  validators, is_complete poll backoff
- podcast_subscriptions: the ONLY user-writable podcast state (display
  name, auto-download policy); deliberately UNGOVERNED — it IS the read
  policy's lookup table
- media_items episode columns: source/source_feed_id/episode_guid/
  feed_position/season/episode/enclosure_url/declared_duration/
  transcript_url/chapters_url/hosts/guests + download state machine
  (pending|queued|downloading|downloaded|failed + error + attempts),
  all partial-indexed so scan rows never touch them
- libraries.allow_member_feeds (first per-library knob); library_folders
  .purpose ('podcast' download roots, scanner-skipped)

RLS (inside the labeled policy block):
- p_media_items_visible gains the podcast-episode subscription predicate
  + admin escape (source IS DISTINCT FROM 'podcast_episode' guard keeps
  scan rows on the cheap path); unset identity still fails loud
- podcast_feeds governed identically: library-visibility join AND
  subscriber-or-admin SELECT; permissive write stand-ins (mandatory at
  ENABLE time; fetcher/worker declare first-admin, add-feed upsert uses
  ON CONFLICT DO UPDATE RETURNING so creators never need SELECT on
  foreign rows)
- Verified against dev DB: migration identity sees nothing, admin sees
  all, member sees feeds/episodes only post-subscribe (probed in a
  rolled-back tx — no rows persisted)

sqlc: 30 new queries (feed upsert/CRUD, subscriptions, episode
upsert-with-write-after-success-guard, download state transitions,
poller worklist, subscription-scoped search) — no visibility clauses
anywhere, per the D20 scoping model.
2026-10-29 15:36:11 -04:00
..