Skip to content

PRD-038: Catalog

  • Status: Draft
  • Authors: Marko
  • Target Release: v2.7 (Phase P1)
  • Parent PRD: docs/prd/PRD-035-learning-platform.md
  • Depends on: PRD-036 (read API), PRD-037 (library, scrape)
  • Downstream: PRD-039 (Player)
  • Related UX spec: docs/uxs/UXS-011-consumer-learning-app.md (Editorial Bold; catalog cards inherit tokens)
  • Related RFC: docs/rfc/RFC-099-learning-platform-consumer-client.md (client behaviour, MVP slice)
  • Supersedes: PRD-029-platform-catalog.md (deleted draft)

Summary

The Catalog is the user's view of available content — the bridge from Discovery to Player. It shows which episodes are ready, pending, or unscraped, and surfaces enough per-episode context (summary, topics, insight count, duration) to choose what to play. Two levels: the podcast view (one show) and the global catalog (all episodes across the user's library, newest first).

MVP scope (Epic 2 — local content only). The first release catalogs only the episodes already processed in the local corpus (served via the LocalCorpusSource — see API summary). In this slice effectively every catalogued episode is Ready; the Not-scraped / Pending / Request states (FR1.3, FR1.4, FR2.3, FR4.2) describe the post-#1069 behaviour and are built when scrape-on-demand (#1069) and the audio proxy (#1070) land. The card and list contracts are designed now so that adding those states later does not reshape the UI or the API.

Background & Context

  • After adding a show, users need to browse episodes, understand readiness, and navigate into the player.
  • The Catalog is the natural home for surfacing corpus-level signals (topics, insight counts, summary previews) so users choose informed — before committing to playback.
  • All enrichment is optional per Principle: cards degrade cleanly to core metadata when artifacts are absent.

Goals

  • Show the library's episodes with accurate Ready / Pending / Not-scraped status.
  • Give enough per-episode context to choose without opening the player.
  • Be the navigation hub between Discovery and Player.
  • Surface scrape progress inline for pending episodes.
  • Degrade gracefully when enrichment is absent.

Non-Goals

  • Not a search interface (search is its own surface; cross-corpus episode search is later).
  • Not a playback surface (cards only; playback is exclusively the Player).
  • Not a recommendation engine — ordering is chronological in v2.7 (personalised ordering is PRD-041).
  • Not a download manager (bridge-only; no local downloads).

Personas

Casual listener (what's ready to play?) and active learner (which episode is worth my time?).

User Stories

  • As a listener, I can see all episodes of a show I follow with clear ready/pending status.
  • As a listener, I can judge an episode (summary, topics, duration) without opening it.
  • As a browser, I can see recent episodes across my whole library to find something to play.

Functional Requirements

FR1: Global catalog view

  • FR1.1: A Home / All-Episodes view across all library podcasts, publish-date descending.
  • FR1.2: Each card: artwork, podcast name, episode title, publish date, duration, status badge.
  • FR1.3: Ready → links to Player; Not-scraped → inline "Request"; Pending → inline progress.
  • FR1.4: Pending progress sourced from RFC-065 status.json via GET /api/scrape/status/{job_id}.
  • FR1.5: Paginated / infinite scroll, 20 per page.

FR2: Podcast view

  • FR2.1: Tapping a library podcast shows its header (artwork, name, publisher, description) + episode list.
  • FR2.2: Episodes sorted publish-date descending, same card format as FR1.2.
  • FR2.3: "Request all" queues all unprocessed episodes (PRD-037 FR4.3).
  • FR2.4: Header shows totals (e.g. "48 episodes · 12 ready · 2 pending").

FR3: Episode card — enriched state

Retrospective (shipped #1091): the card is a clean lede + an expand-on-demand insights popover, not a row of metadata pills. FR3.2–FR3.4 below were superseded.

  • FR3.1: A clean one-line lede — the summary title / first sentence (summary_preview), never the bullets joined together.
  • FR3.2 (shipped): A grounded ✦ insights icon (shown when has_gi + bullets exist) reveals a popover with the full summary bullets (summary_bullets[]) on hover/tap — so the card stays compact while the complete summary is one interaction away. Topic pills were dropped from the card (topic discovery happens via the Insights panel / corpus search).
  • FR3.3 (superseded): Speaker count is not surfaced on the card.
  • FR3.4 (superseded): Insight count is not surfaced on the card; has_gi/has_kg are the cheap depth signal (they gate the insights affordance), not a displayed number.

FR4: Episode card — degraded state

  • FR4.1: Core-only cards (title, date, duration, status) omit absent fields — no broken/empty panels.
  • FR4.2: Audio-available-but-no-transcript shows "Transcript pending"; still playable (audio-only).

FR5: Navigation

  • FR5.1: Ready card → Player (PRD-039), passing the episode slug.
  • FR5.2: Breadcrumbs: Player → Catalog (podcast) → Discovery (library); back always available.
  • FR5.3: From global view, tapping podcast name/artwork → that podcast's Catalog view.

API summary

All endpoints live under the consumer namespace /api/app/* (RFC-098), isolated from the operator API.

Method Path Description Status
GET /api/app/episodes Episodes across the library (page, status) — PRD-036 FR3.1 Net-new (Epic 2)
GET /api/app/podcasts/{id}/episodes One podcast's episodes (page) Net-new (Epic 2)
GET /api/app/episodes/{slug} Detail + artifact flags — PRD-036 FR3.2 Shipped (Epic 1)
GET /api/app/library The user's subscriptions (feed ids) Shipped (Epic 1)

Server gap — the catalog list endpoints do not exist yet. Epic 1 shipped episode detail by slug, the per-user library (subscriptions), and search, but no catalog list (GET /api/app/episodes, GET /api/app/podcasts/{id}/episodes). These are the central net-new server work for Epic 2 and are backed by a pluggable ContentSource (see below). The Player (PRD-039) has no such gap — it is fully served by the Epic-1 surface.

ContentSource (pluggable catalog backend). The list endpoints read through a ContentSource abstraction. For the MVP this is a LocalCorpusSource that enumerates the already-processed local corpus (the episodes we have) — no scraping, no discovery. When #1069 (scrape-on-demand) and #1070 land, a DiscoverySource extends the same contract to surface content not yet in the corpus and to create the entry point for "add content". The catalog UI and the /api/app/episodes* shape stay unchanged across that swap.

Episode summary shape (per PRD-036): slug, title, podcast_name, publish_date, duration_seconds, status, artifacts{transcript,summary,gi,kg}, summary_preview, topics[], speaker_count, insight_count. Artifact flags drive graceful degradation.

Success Metrics

  • Library → a podcast's episode list in one tap.
  • Status always accurate; pending → ready flips without a full reload.
  • Cards show enrichment when present and degrade cleanly when absent.
  • Request a scrape directly from a card without navigating away.
  • Catalog → Player → back preserves scroll position.

Dependencies

  • PRD-036 (read API, status), PRD-037 (library, scrape).
  • RFC-065 status.json.

Open Questions

  • Cross-corpus episode search surface (deferred — separate PRD).

References

  • docs/prd/PRD-035-learning-platform.md, PRD-037-discovery.md, PRD-039-player.md
  • docs/rfc/RFC-065-agent-observable-instrumentation.md