From 87986d88d0d6f94de5ace07c31bbf1606cc1d8d3 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Wed, 27 May 2026 14:54:58 -0400 Subject: [PATCH] fix(catalog): use season posters for episode cards --- internal/catalog/episode_catalog_source.go | 5 +++-- internal/sections/fetcher.go | 10 ++++----- .../components/ContinueWatchingCard.test.tsx | 8 +++---- web/src/components/ContinueWatchingCard.tsx | 22 +++++++++++-------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/internal/catalog/episode_catalog_source.go b/internal/catalog/episode_catalog_source.go index 56b19e3a..3bb04af0 100644 --- a/internal/catalog/episode_catalog_source.go +++ b/internal/catalog/episode_catalog_source.go @@ -28,8 +28,8 @@ const episodeCatalogSelectBody = `( COALESCE(e.imdb_id, '') AS imdb_id, COALESCE(e.tmdb_id, '') AS tmdb_id, COALESCE(e.tvdb_id, '') AS tvdb_id, - COALESCE(e.still_path, '') AS poster_path, - COALESCE(e.still_thumbhash, '') AS poster_thumbhash, + COALESCE(NULLIF(s.poster_path, ''), NULLIF(si.poster_path, ''), NULLIF(e.still_path, ''), '') AS poster_path, + COALESCE(NULLIF(s.poster_thumbhash, ''), NULLIF(si.poster_thumbhash, ''), NULLIF(e.still_thumbhash, ''), '') AS poster_thumbhash, COALESCE(si.backdrop_path, '') AS backdrop_path, COALESCE(si.backdrop_thumbhash, '') AS backdrop_thumbhash, COALESCE(si.logo_path, '') AS logo_path, @@ -58,6 +58,7 @@ const episodeCatalogSelectBody = `( e.updated_at FROM episodes e JOIN media_items si ON si.content_id = e.series_id + LEFT JOIN seasons s ON s.content_id = e.season_id WHERE %s ) mi` diff --git a/internal/sections/fetcher.go b/internal/sections/fetcher.go index 01dbb759..9a771d7e 100644 --- a/internal/sections/fetcher.go +++ b/internal/sections/fetcher.go @@ -2153,7 +2153,7 @@ func (f *Fetcher) fetchEpisodeTargetsByContentIDs(ctx context.Context, contentID effectiveLibraryIDs := effectiveFetchLibraryIDs(libraryIDs, filter) - fromClause := "episodes e JOIN media_items si ON e.series_id = si.content_id" + fromClause := "episodes e JOIN media_items si ON e.series_id = si.content_id LEFT JOIN seasons s ON s.content_id = e.season_id" if libraryID != nil || effectiveLibraryIDs != nil { fromClause += " JOIN media_item_libraries mil ON si.content_id = mil.content_id" } @@ -2187,16 +2187,16 @@ func (f *Fetcher) fetchEpisodeTargetsByContentIDs(ctx context.Context, contentID e.overview, e.runtime, e.rating_imdb, - e.still_path, - e.still_thumbhash, + COALESCE(NULLIF(s.poster_path, ''), NULLIF(si.poster_path, ''), NULLIF(e.still_path, ''), '') AS poster_path, + COALESCE(NULLIF(s.poster_thumbhash, ''), NULLIF(si.poster_thumbhash, ''), NULLIF(e.still_thumbhash, ''), '') AS poster_thumbhash, e.season_number, e.episode_number, e.air_date, si.title, si.genres, si.content_rating, - si.backdrop_path, - si.backdrop_thumbhash, + COALESCE(NULLIF(e.still_path, ''), NULLIF(si.backdrop_path, ''), '') AS backdrop_path, + COALESCE(NULLIF(e.still_thumbhash, ''), NULLIF(si.backdrop_thumbhash, ''), '') AS backdrop_thumbhash, si.logo_path, si.status FROM %s diff --git a/web/src/components/ContinueWatchingCard.test.tsx b/web/src/components/ContinueWatchingCard.test.tsx index cfa6cd5a..bd4a3f18 100644 --- a/web/src/components/ContinueWatchingCard.test.tsx +++ b/web/src/components/ContinueWatchingCard.test.tsx @@ -11,7 +11,7 @@ vi.mock("@/playback/watchPlaybackContext", () => ({ })); describe("ContinueWatchingCard", () => { - it("prefers the poster image for episodes (poster_url is the horizontal still)", () => { + it("prefers the backdrop image for section episodes (backdrop_url is the horizontal still)", () => { const queryClient = new QueryClient(); const markup = renderToStaticMarkup( @@ -34,7 +34,7 @@ describe("ContinueWatchingCard", () => { position_seconds: 120, duration_seconds: 3600, progress_updated_at: "2026-03-07T00:00:00Z", - poster_url: "/episode-poster.jpg", + poster_url: "/season-poster.jpg", poster_thumbhash: "", backdrop_url: "/episode-backdrop.jpg", backdrop_thumbhash: "", @@ -45,8 +45,8 @@ describe("ContinueWatchingCard", () => { , ); - expect(markup).toContain('src="/episode-poster.jpg"'); - expect(markup).not.toContain('src="/episode-backdrop.jpg"'); + expect(markup).toContain('src="/episode-backdrop.jpg"'); + expect(markup).not.toContain('src="/season-poster.jpg"'); }); it("prefers the backdrop image for movies (poster_url is a vertical poster)", () => { diff --git a/web/src/components/ContinueWatchingCard.tsx b/web/src/components/ContinueWatchingCard.tsx index c73b9cd8..fd7985f9 100644 --- a/web/src/components/ContinueWatchingCard.tsx +++ b/web/src/components/ContinueWatchingCard.tsx @@ -144,19 +144,23 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) { ? "w-[140px] shrink-0 sm:w-[160px] lg:w-[185px]" : "w-[260px] shrink-0 sm:w-[315px]"; const imageAspect = isPoster ? "aspect-[2/3]" : "aspect-video"; - // Episodes store the horizontal still in poster_url (see episode_catalog_source.go); - // wide-variant movies/series/seasons need the backdrop for the 16:9 card. - // Poster variant always wants the vertical poster. + const isSectionEpisode = "sectionItem" in props && props.sectionItem?.type === "episode"; + // Section episode payloads use poster_url for vertical season/series artwork + // and backdrop_url for the episode still used by wide cards. const imagePrimary = isPoster ? card.posterUrl - : card.type === "episode" - ? card.posterUrl - : card.backdropUrl; + : isSectionEpisode + ? card.backdropUrl + : card.type === "episode" + ? card.posterUrl + : card.backdropUrl; const imageFallback = isPoster ? card.backdropUrl - : card.type === "episode" - ? card.backdropUrl - : card.posterUrl; + : isSectionEpisode + ? card.posterUrl + : card.type === "episode" + ? card.backdropUrl + : card.posterUrl; const imageSrc = imagePrimary || imageFallback; return (