mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
feat(portal): show each season's own poster beside the season tabs (#1628)
Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.
Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.
The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
43c1ceac16
commit
7790e68147
36 files changed
+1119
-102
No files matched your search
@@ -719,6 +719,76 @@ test('@xtream season watched toggle — marks a season, survives reload, and cle
|
||||
await expect(page.locator('.season-tabs__done')).toHaveCount(0);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Season cover beside the season tabs (provider tier)
|
||||
//
|
||||
// The default scenario's get_series_info carries a distinct seasons[].cover
|
||||
// per season, which differs from the show cover, so with TMDB enrichment off
|
||||
// the serial details page shows that cover next to the tabs and swaps it on
|
||||
// a tab click. Image requests are answered locally so a slow or blocked CDN
|
||||
// cannot fold the column (the app hides a cover whose image failed).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('@xtream season cover — shows the provider season cover and follows the selected tab', async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const TRANSPARENT_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
|
||||
'base64'
|
||||
);
|
||||
await page.route('**/picsum.photos/**', (route) =>
|
||||
route.fulfill({ contentType: 'image/png', body: TRANSPARENT_PNG })
|
||||
);
|
||||
|
||||
const categories = (await (
|
||||
await request.get(
|
||||
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_series_categories`
|
||||
)
|
||||
).json()) as Array<{ category_id: string; category_name: string }>;
|
||||
const category = categories[0];
|
||||
const seriesItems = (await (
|
||||
await request.get(
|
||||
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_series&category_id=${category.category_id}`
|
||||
)
|
||||
).json()) as Array<{ name: string; series_id: number }>;
|
||||
const targetSeries = seriesItems[0];
|
||||
|
||||
await addXtreamPortal(page);
|
||||
await page.goto(page.url().replace(/\/vod.*$/, '/series'));
|
||||
|
||||
const categoryItem = page
|
||||
.locator('.context-panel .category-item')
|
||||
.filter({ hasText: category.category_name })
|
||||
.first();
|
||||
await expect(categoryItem).toBeVisible({ timeout: 10_000 });
|
||||
await categoryItem.click();
|
||||
|
||||
const seriesCard = page
|
||||
.locator('app-grid-list mat-card')
|
||||
.filter({ hasText: targetSeries.name })
|
||||
.first();
|
||||
await expect(seriesCard).toBeVisible({ timeout: 10_000 });
|
||||
await seriesCard.click();
|
||||
|
||||
// Season 1 is auto-selected; its provider cover sits beside the tabs.
|
||||
// The mock seeds season art as `season-<id>-<n>` (cover) and
|
||||
// `season-big-<id>-<n>` (cover_big); the app prefers cover_big.
|
||||
const cover = page.locator('[data-testid="season-cover"]');
|
||||
await expect(cover).toBeVisible({ timeout: 15_000 });
|
||||
await expect(cover).toHaveAttribute(
|
||||
'src',
|
||||
new RegExp(`season(-big)?-${targetSeries.series_id}-1/`)
|
||||
);
|
||||
|
||||
// The cover follows the selected tab.
|
||||
await page.locator('.season-tabs__pill').nth(1).click();
|
||||
await expect(cover).toHaveAttribute(
|
||||
'src',
|
||||
new RegExp(`season(-big)?-${targetSeries.series_id}-2/`)
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Series-level watched toggle: the season header's ⋮ menu marks EVERY season
|
||||
// in one action (default scenario: 3 seasons × 8 episodes = 24), flips to
|
||||
|
||||
Reference in new issue
Block a user