feat(podcasts): generic-surface exclusions — podcasts never render as books (task 1.3)

§9 / D3 / D13 / D15 / D16: podcasts-type libraries are excluded from
every generic book surface. Under RLS these are pure UX/type filters
(engine already hides other users' episodes); their job is preventing
episode grids in surfaces designed for books. The discriminator is the
denormalized media_items.library_type_name (IS DISTINCT FROM 'podcasts'
— NULL rows predate the column and stay visible).

SQL (queries.sql, marked '-- podcasts exclusion (plan §9)'):
ListMediaItems/ListMediaItemsSorted (All Books) · SearchMediaItems +
SearchMediaItemsUnified (generic search) · FacetCountsUnderFilters (all
14 arms) + the six Search*Values facet-suggestion queries ·
GetDistinctSeries + Count (series pages) · GetLibraryItems (collections
picker) · dashboard system sections (Continue Reading / Recently Added
/ Recently Read / Not Started) · GetPopularBooks + GetTopBooksByMinutes
(book rankings must not rank episodes).

Go: statistics summary counts episodes' sessions into time-spent (D15
keeps that) but skips them in BooksActive/BooksFinished — an episode is
not a book. OPDS filters podcasts libraries via one helper at all 7
library-walk sites (D16).

Templ: LibrarySwitcher skips podcasts entries and TotalMediaCount
excludes them — presentation-layer filter so every switcher consumer
(dashboard, collections, custom-section builder) is correct at once
(D13); podcasts surface via /podcasts only.
This commit is contained in:
John O'Keefe committed 2026-10-29 15:44:48 -04:00
1 parent c0e6993273
commit 9a148c2344
8 files changed
+174 -47

No files matched your search

+36
View File
@@ -2620,6 +2620,7 @@ JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
CROSS JOIN LATERAL unnest(mi.authors) AS a
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -2688,6 +2689,7 @@ JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
CROSS JOIN LATERAL unnest(mi.publishers) AS p
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -2755,6 +2757,7 @@ JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
CROSS JOIN LATERAL unnest(mi.narrators) AS n
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -2824,6 +2827,7 @@ JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
CROSS JOIN LATERAL unnest(mi.tags) AS t
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -2892,6 +2896,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -2963,6 +2968,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3034,6 +3040,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3105,6 +3112,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3172,6 +3180,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3239,6 +3248,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3306,6 +3316,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3377,6 +3388,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3448,6 +3460,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -3519,6 +3532,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
@@ -4822,6 +4836,7 @@ INNER JOIN (
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY rp.last_read_at DESC
@@ -5621,6 +5636,7 @@ WHERE ($2::uuid IS NULL OR library_id = $2::uuid)
AND ($3::uuid[] IS NULL OR library_id = ANY($3::uuid[]))
AND archived_at IS NULL
AND missing_scan_count = 0
AND (library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9: series pages)
AND series IS NOT NULL AND series != ''
-- Multi-field search (series page): the query matches the series
-- NAME, any book's AUTHOR, or any book's TITLE in the group. It is
@@ -5754,6 +5770,7 @@ SELECT COUNT(*)::int FROM (
AND ($3::uuid[] IS NULL OR library_id = ANY($3::uuid[]))
AND archived_at IS NULL
AND missing_scan_count = 0
AND (library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9: series pages)
AND series IS NOT NULL AND series != ''
AND ($4::text IS NULL OR
series ILIKE '%' || $4::text || '%'
@@ -6488,6 +6505,7 @@ SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.descr
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9: collections picker)
ORDER BY mi.created_at DESC
`
@@ -8928,6 +8946,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) {
const GetNotStartedItems = `-- name: GetNotStartedItems :many
SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND NOT EXISTS (
@@ -9435,6 +9454,7 @@ SELECT
FROM media_items mi
JOIN reading_history rh ON rh.media_item_id = mi.id
WHERE rh.user_id = $1
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
GROUP BY mi.id, mi.title, mi.author
ORDER BY read_count DESC
LIMIT $2
@@ -9658,6 +9678,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams
const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many
SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
@@ -9789,6 +9810,7 @@ INNER JOIN (
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY rp.last_read_at DESC
@@ -10619,6 +10641,7 @@ SELECT
FROM reading_history rh
JOIN media_items mi ON mi.id = rh.media_item_id
WHERE rh.user_id = $1
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND COALESCE(rh.reading_session_end, rh.created_at) >= $2
AND COALESCE(rh.reading_session_end, rh.created_at) <= $3
AND ($4::uuid[] IS NULL OR mi.library_id = ANY($4::uuid[]))
@@ -12947,6 +12970,7 @@ JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
WHERE mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND (mi.library_type_name IS DISTINCT FROM 'podcasts')
AND ($3::uuid[] IS NULL OR mi.library_id = ANY($3::uuid[]))
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2
`
@@ -13051,6 +13075,9 @@ type ListMediaItemsRow struct {
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
}
// Podcasts exclusion (plan §9): generic book surfaces never list
// podcast episodes (library_type_name is the denormalized, trigger/
// fetcher-maintained discriminator; NULL rows predate it and stay).
func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) {
rows, err := q.db.Query(ctx, ListMediaItems, arg.Limit, arg.Offset, arg.VisibleLibraryIds)
if err != nil {
@@ -13837,6 +13864,7 @@ JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
WHERE mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.library_id = $1
ORDER BY
CASE
@@ -15921,6 +15949,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
@@ -15981,6 +16010,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
@@ -16041,6 +16071,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
@@ -16101,6 +16132,7 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND (
mi.title ILIKE $3 OR
@@ -16413,6 +16445,7 @@ LEFT JOIN reading_progress rp ON rp.media_item_id = mi.id AND rp.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND ($2::uuid IS NULL
OR mi.library_id = $2::uuid)
-- Fuzzy author filter — per-element against the authors[] array (A2c
@@ -17018,6 +17051,7 @@ JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
CROSS JOIN LATERAL unnest(mi.narrators) AS narrator
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
AND narrator IS NOT NULL
AND word_similarity($1, narrator) > 0.3
@@ -17075,6 +17109,7 @@ FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
@@ -17136,6 +17171,7 @@ JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
CROSS JOIN LATERAL unnest(mi.tags_search) AS tag
WHERE COALESCE(lv.is_visible, true) = true
AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9)
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
AND tag IS NOT NULL
AND word_similarity($1, tag) > 0.3