* feat(audiobooks): redesign audiobook library around resume and series progression Audiobook libraries previously reused the video-shaped library page: a backdrop carousel hero (audiobooks have square covers and no backdrops), movie-style default sections, and a browse grid whose primary audiobook axes (author, narrator, series) were buried as filters. Backend: - New next_in_series section type: surfaces the next unstarted book, by series_index, in series the profile has finished a book of, ordered by most recent finish. Registered as a library-staple recipe. - New GET /api/v1/catalog/audiobook-groups endpoint: grouped browse by author/narrator/series with book count, total duration, per-profile progress counts, and poster URLs for cover stacks. - Audiobook library defaults: continue-listening is featured (renders as the Now Listening hero) with next-in-series directly after it. A data migration upgrades existing audiobook libraries, skipping layouts where an admin already featured a section. Frontend: - NowListeningHero replaces HeroBanner for audiobook libraries: resume deck with chapter position, hours left, ambient color from the cover, and one-click resume; remaining in-progress books render as the Continue Listening row. - Library tab gains Books/Series/Authors/Narrators browse axes persisted via the type param; selecting a group drops into the Books grid with the matching filter applied. - "Recommended" tab is labeled "Home" for audiobook libraries; audiobook continue cards use square covers and hr/min time-left formatting. - Shared audiobook chapter/file/duration helpers extracted to web/src/lib/audiobooks (deduplicated from AudiobookContent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(audiobooks): address review feedback on library redesign - Push library scoping into the next-in-series candidate SQL so finished series whose next book lives in another library can't consume the candidate limit and starve a library-scoped section (Codex P2). - Paginate the audiobook groups fetch until the server-reported total is reached (500/page, 20-page bound) so client-side filtering sees the complete author/narrator/series list (Codex P2, CodeRabbit). - Make the redesign migration rollback-safe: rows the Up touches carry config markers (featured_by_migration / seeded_by_migration) and the Down reverts only marked rows, leaving admin-set featured state and hand-created next_in_series sections alone (Codex P2, CodeRabbit). - Gate NowListeningHero's detail-derived files/credits on the detail matching the deck item, so Resume can't start the new book with the previous book's files while keepPreviousData shows stale detail (Codex P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
123 lines
3.9 KiB
SQL
123 lines
3.9 KiB
SQL
-- +goose Up
|
|
-- +goose StatementBegin
|
|
-- Audiobook library Home redesign: the Continue Listening section becomes the
|
|
-- featured "Now Listening" resume hero, and a next_in_series row surfaces the
|
|
-- next unstarted book in series the profile has finished.
|
|
--
|
|
-- Rows touched here carry a marker key in config so the Down migration can
|
|
-- revert exactly what this migration changed and nothing else.
|
|
--
|
|
-- Mark the first continue-listening section of each audiobook library as
|
|
-- featured, but only when the admin has not already featured something else in
|
|
-- that library's layout.
|
|
WITH target AS (
|
|
SELECT DISTINCT ON (ps.library_id) ps.id
|
|
FROM page_sections ps
|
|
JOIN media_folders mf ON mf.id = ps.library_id
|
|
WHERE ps.scope = 'library'
|
|
AND lower(mf.type) IN ('audiobook', 'audiobooks')
|
|
AND ps.section_type = 'continue_watching'
|
|
AND ps.config->>'continue_type' = 'listening'
|
|
AND NOT ps.featured
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM page_sections f
|
|
WHERE f.scope = 'library'
|
|
AND f.library_id = ps.library_id
|
|
AND f.featured
|
|
)
|
|
ORDER BY ps.library_id, ps.position ASC
|
|
)
|
|
UPDATE page_sections ps
|
|
SET featured = TRUE,
|
|
config = ps.config || '{"featured_by_migration":"20260609222830"}'::jsonb,
|
|
updated_at = NOW()
|
|
FROM target t
|
|
WHERE ps.id = t.id;
|
|
-- +goose StatementEnd
|
|
|
|
-- +goose StatementBegin
|
|
-- Insert a next_in_series section into each audiobook library that lacks one,
|
|
-- directly after its continue-listening section (or at the end of the layout
|
|
-- when no continue-listening section exists).
|
|
WITH anchors AS (
|
|
SELECT DISTINCT ON (ps.library_id)
|
|
ps.library_id,
|
|
ps.position + 1 AS insert_position
|
|
FROM page_sections ps
|
|
JOIN media_folders mf ON mf.id = ps.library_id
|
|
WHERE ps.scope = 'library'
|
|
AND lower(mf.type) IN ('audiobook', 'audiobooks')
|
|
AND ps.section_type = 'continue_watching'
|
|
AND ps.config->>'continue_type' = 'listening'
|
|
ORDER BY ps.library_id, ps.position ASC
|
|
),
|
|
targets AS (
|
|
SELECT
|
|
mf.id AS library_id,
|
|
COALESCE(
|
|
a.insert_position,
|
|
(
|
|
SELECT COALESCE(MAX(p2.position) + 1, 0)
|
|
FROM page_sections p2
|
|
WHERE p2.scope = 'library' AND p2.library_id = mf.id
|
|
)
|
|
) AS insert_position
|
|
FROM media_folders mf
|
|
LEFT JOIN anchors a ON a.library_id = mf.id
|
|
WHERE lower(mf.type) IN ('audiobook', 'audiobooks')
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM page_sections e
|
|
WHERE e.scope = 'library'
|
|
AND e.library_id = mf.id
|
|
AND e.section_type = 'next_in_series'
|
|
)
|
|
),
|
|
shifted AS (
|
|
UPDATE page_sections ps
|
|
SET position = ps.position + 1,
|
|
updated_at = NOW()
|
|
FROM targets t
|
|
WHERE ps.scope = 'library'
|
|
AND ps.library_id = t.library_id
|
|
AND ps.position >= t.insert_position
|
|
RETURNING 1
|
|
)
|
|
INSERT INTO page_sections (
|
|
id, scope, library_id, position, section_type, title, featured,
|
|
item_limit, config, enabled, created_at, updated_at
|
|
)
|
|
SELECT
|
|
gen_random_uuid()::text,
|
|
'library',
|
|
t.library_id,
|
|
t.insert_position,
|
|
'next_in_series',
|
|
'Next in Your Series',
|
|
false,
|
|
20,
|
|
'{"seeded_by_migration":"20260609222830"}'::jsonb,
|
|
true,
|
|
NOW(),
|
|
NOW()
|
|
FROM targets t;
|
|
-- +goose StatementEnd
|
|
|
|
-- +goose Down
|
|
-- +goose StatementBegin
|
|
-- Revert only the rows the Up migration touched, identified by the marker
|
|
-- keys it wrote. Admin-created next_in_series sections and sections featured
|
|
-- before (or after) the upgrade are left alone.
|
|
DELETE FROM page_sections
|
|
WHERE scope = 'library'
|
|
AND section_type = 'next_in_series'
|
|
AND config->>'seeded_by_migration' = '20260609222830';
|
|
|
|
UPDATE page_sections
|
|
SET featured = FALSE,
|
|
config = config - 'featured_by_migration',
|
|
updated_at = NOW()
|
|
WHERE scope = 'library'
|
|
AND section_type = 'continue_watching'
|
|
AND config->>'featured_by_migration' = '20260609222830';
|
|
-- +goose StatementEnd
|