diff --git a/internal/database/querier.go b/internal/database/querier.go index 43d09d7c..a1b54018 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -606,6 +606,9 @@ type Querier interface { // collapsible "Ignored" section so a misclick is recoverable. ListIgnoredUnlinkedBooksForUser(ctx context.Context, userID pgtype.UUID) ([]ListIgnoredUnlinkedBooksForUserRow, error) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) + // 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). ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index d1dc1ee1..749b29f3 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -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 diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 2699d53f..9372f5e5 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -180,12 +180,16 @@ SELECT * FROM media_items WHERE library_id = $1 AND file_path = $2; SELECT * FROM media_items WHERE id = $1; -- name: ListMediaItems :many +-- 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). SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi 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 (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR mi.library_id = ANY(sqlc.narg('visible_library_ids')::uuid[])) ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2; @@ -214,6 +218,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 = sqlc.narg('library_id') ORDER BY CASE @@ -660,6 +665,7 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.na 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) AND ( mi.title ILIKE sqlc.narg('search_pattern') OR @@ -750,6 +756,7 @@ LEFT JOIN reading_progress rp ON rp.media_item_id = mi.id AND rp.user_id = sqlc. 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) -- Fuzzy author filter — per-element against the authors[] array (A2c @@ -934,6 +941,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -953,6 +961,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -973,6 +982,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) AND tag IS NOT NULL AND word_similarity(sqlc.narg('search_query'), tag) > 0.3 @@ -990,6 +1000,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) AND narrator IS NOT NULL AND word_similarity(sqlc.narg('search_query'), narrator) > 0.3 @@ -1006,6 +1017,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1025,6 +1037,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1063,6 +1076,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1131,6 +1145,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1198,6 +1213,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1267,6 +1283,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1335,6 +1352,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1406,6 +1424,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1477,6 +1496,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1548,6 +1568,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1615,6 +1636,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1682,6 +1704,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1749,6 +1772,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1820,6 +1844,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1891,6 +1916,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -1962,6 +1988,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 = sqlc.narg('user_id') 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 (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) @@ -2606,6 +2633,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; @@ -2668,6 +2696,7 @@ SELECT FROM reading_history rh JOIN media_items mi ON mi.id = rh.media_item_id WHERE rh.user_id = sqlc.arg('user_id') + AND (mi.library_type_name IS DISTINCT FROM 'podcasts') -- podcasts exclusion (plan §9) AND COALESCE(rh.reading_session_end, rh.created_at) >= sqlc.arg('start_time') AND COALESCE(rh.reading_session_end, rh.created_at) <= sqlc.arg('end_time') AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR mi.library_id = ANY(sqlc.narg('visible_library_ids')::uuid[])) @@ -3964,6 +3993,7 @@ INNER JOIN ( ORDER BY media_item_id, last_read_at DESC ) rp ON rp.media_item_id = mi.id WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::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 @@ -3972,6 +4002,7 @@ LIMIT sqlc.narg('limit'); -- name: GetRecentlyAddedItems :many SELECT mi.* FROM media_items mi WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::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 @@ -3987,6 +4018,7 @@ INNER JOIN ( ORDER BY media_item_id, last_read_at DESC ) rp ON rp.media_item_id = mi.id WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::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 @@ -3995,6 +4027,7 @@ LIMIT sqlc.narg('limit'); -- name: GetNotStartedItems :many SELECT mi.* FROM media_items mi WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::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 ( @@ -4081,6 +4114,7 @@ SELECT mi.* FROM media_items mi WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::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; -- name: GetDistinctSeries :many @@ -4113,6 +4147,7 @@ WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_ AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR library_id = ANY(sqlc.narg('visible_library_ids')::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 @@ -4173,6 +4208,7 @@ SELECT COUNT(*)::int FROM ( AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR library_id = ANY(sqlc.narg('visible_library_ids')::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 (sqlc.narg('series_query')::text IS NULL OR series ILIKE '%' || sqlc.narg('series_query')::text || '%' diff --git a/internal/handlers/opds.go b/internal/handlers/opds.go index f82e5b19..079c0560 100644 --- a/internal/handlers/opds.go +++ b/internal/handlers/opds.go @@ -1,5 +1,4 @@ package handlers - import ( "bookhoard/internal/database" "bookhoard/internal/opds" @@ -18,6 +17,24 @@ import ( "github.com/labstack/echo/v5" ) +// opdsVisibleLibraries is GetUserVisibleLibraries minus podcasts-type +// libraries (plan D16): feeds are not OPDS inventory day one — they +// surface via the /podcasts endpoints only. +func opdsVisibleLibraries(ctx context.Context, q *database.Queries, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) { + libs, err := q.GetUserVisibleLibraries(ctx, userID) + if err != nil { + return nil, err + } + out := make([]database.GetUserVisibleLibrariesRow, 0, len(libs)) + for _, lib := range libs { + if lib.TypeName == services.LibraryTypePodcasts { + continue + } + out = append(out, lib) + } + return out, nil +} + type OPDSHandler struct { db *database.Queries libraryService *services.LibraryService @@ -209,7 +226,7 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error { // Get user's visible libraries userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries")) } @@ -318,7 +335,7 @@ func (h *OPDSHandler) serveAllBooksCatalog(c *echo.Context) error { // Get user's visible libraries userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries")) } @@ -570,7 +587,7 @@ func (h *OPDSHandler) GetLibraryCatalog(c *echo.Context) error { // The library must be visible to the device owner. userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries")) } @@ -677,7 +694,7 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error { // Get user's visible libraries userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries")) } @@ -818,7 +835,7 @@ func (h *OPDSHandler) DownloadBook(c *echo.Context) error { // Get user's visible libraries userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get libraries"}) } @@ -974,7 +991,7 @@ func (h *OPDSHandler) GetCoverImage(c *echo.Context) error { // Get user's visible libraries userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get libraries"}) } @@ -1115,7 +1132,7 @@ func (h *OPDSHandler) ListFormats(c *echo.Context) error { // Get user's visible libraries userID := device.UserID.Bytes - libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}) + libraries, err := opdsVisibleLibraries(c.Request().Context(), h.db, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get libraries"}) } diff --git a/internal/services/statistics_service.go b/internal/services/statistics_service.go index 64088969..08c57064 100644 --- a/internal/services/statistics_service.go +++ b/internal/services/statistics_service.go @@ -296,6 +296,13 @@ func (s *StatisticsService) UserSummary(ctx context.Context, userID pgtype.UUID, entry.Sessions++ summary.SessionCount++ + // Podcast episodes (plan D15): sessions flow into time-spent + // (the minutes/pages/sessions math above already ran), but an + // episode is not a book — never enters BooksActive/BooksFinished. + if strings.EqualFold(strings.TrimSpace(sess.LibraryTypeName.String), "podcasts") { + continue + } + mediaUUID, err := uuid.FromBytes(sess.MediaItemID.Bytes[0:16]) if err == nil { key := mediaUUID.String() diff --git a/templates/library_switcher.templ b/templates/library_switcher.templ index 27c38579..29d79fae 100644 --- a/templates/library_switcher.templ +++ b/templates/library_switcher.templ @@ -26,6 +26,13 @@ templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, title stri > for _, lib := range libData { + // Podcasts libraries are feed-managed (plan D13): never + // in the book-library switcher — feeds surface via + // /podcasts only. Presentation-layer filter so every + // switcher consumer is correct at once. + if !IsBookLibrary(lib.TypeName) { + continue + } if lib.Offline { if lib.ID == currentLibraryID { diff --git a/templates/library_switcher_templ.go b/templates/library_switcher_templ.go index fc7b88e8..84412ca4 100644 --- a/templates/library_switcher_templ.go +++ b/templates/library_switcher_templ.go @@ -82,187 +82,201 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, title strin return templ_7745c5c3_Err } for _, lib := range libData { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !IsBookLibrary(lib.TypeName) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "continue") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } if lib.Offline { if lib.ID == currentLibraryID { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ") — storage offline") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ") — storage offline") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } else if lib.ID == currentLibraryID { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -273,7 +287,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, title strin } } if len(actions) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -283,12 +297,12 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, title strin return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -317,7 +331,7 @@ func DashboardActions() templ.Component { templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/utils.go b/templates/utils.go index e3512654..17de12db 100644 --- a/templates/utils.go +++ b/templates/utils.go @@ -117,10 +117,10 @@ func MediaBadgeIcon(badge string) string { func TotalMediaCount(libs []LibraryData) int64 { var total int64 for _, l := range libs { - // Unmounted storage contributes nothing visible: an offline - // library's holdings stay in the database but out of the UI - // until its folders return. - if l.Offline { + // Podcasts libraries are feed-managed (plan D13): not book- + // library inventory — excluded here so switcher totals stay + // consistent with the switcher's podcast-filtered option list. + if l.Offline || l.TypeName == "podcasts" { continue } total += l.MediaCount @@ -128,6 +128,13 @@ func TotalMediaCount(libs []LibraryData) int64 { return total } +// IsBookLibrary reports whether a library type participates in the +// generic book surfaces (switcher, All Books, series). Podcasts are +// feed-managed and surface via /podcasts only (plan §9/D13). +func IsBookLibrary(typeName string) bool { + return typeName != "podcasts" +} + func uuidToString(id pgtype.UUID) string { if !id.Valid { return ""