fix(stalker): derive missing season numbers

This commit is contained in:
4gray committed 2026-07-20 22:00:06 +02:00
1 parent 1c3cc6c9fa
commit 8cfc64a3dc
5 files changed
+76 -14

No files matched your search

+3 -1
View File
@@ -43,7 +43,9 @@ Do not stop after making the detail view render.
`series`.
4. Before either inline or external episode playback, persist the parent
`seriesXtreamId` plus resolved `seasonNumber` and `episodeNumber`. Keep
generated episode tracking IDs stable for lazy `is_series` episodes.
generated episode tracking IDs stable for lazy `is_series` episodes. When
`season_number` is absent, derive the coordinate from the same naturally
ordered season list used by quick start; do not default every season to 1.
5. The dashboard reads saved playback positions; it must not infer episode
numbers from provider payloads. Legacy rows without season/episode metadata
remain badge-less until that episode is played again.
+3
View File
@@ -205,6 +205,9 @@ Series inline playback behavior is shared across all three modes:
metadata for workspace surfaces to render an episode badge. Existing rows
without those fields are intentionally not migrated and remain badge-less
until the episode is played again.
- Ministra payloads may omit `season_number`. Episode mapping and lazy
quick-start labels share the same naturally ordered season fallback so later
seasons are not persisted as season 1.
The VOD-series contract is cross-surface:
@@ -78,6 +78,44 @@ describe('stalker-series.adapters', () => {
expect(firstEpisode.id).not.toBe(mapped['1'][1].id);
});
it('derives missing VOD-series season numbers from natural season order', () => {
const mapped = mapVodSeriesEpisodes([
{
id: 'season-2',
video_id: 'v1',
name: 'Season 2',
season_number: '',
episodes: [
{
id: 'episode-2',
series_number: 1,
name: 'Second season pilot',
},
],
isLoading: false,
isExpanded: false,
},
{
id: 'season-1',
video_id: 'v1',
name: 'Season 1',
season_number: '',
episodes: [
{
id: 'episode-1',
series_number: 1,
name: 'Pilot',
},
],
isLoading: false,
isExpanded: false,
},
]);
expect(mapped['Season 1'][0].season).toBe(1);
expect(mapped['Season 2'][0].season).toBe(2);
});
it('maps embedded series payload into regular season episodes', () => {
const regularSeasons = mapRegularSeriesSeasons(
{
@@ -7,6 +7,11 @@ import {
} from './models';
import { isStalkerSeriesFlag } from './stalker-vod.utils';
const naturalSeasonCollator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base',
});
export interface VodSeriesSeasonVm {
id: string;
video_id: string;
@@ -154,7 +159,7 @@ export function mapVodSeriesEpisodes(
seasons.forEach((season) => {
const seasonKey = season.season_number || season.name || season.id;
const seasonNum = toEpisodeNumber(seasonKey) || 1;
const seasonNum = getVodSeriesSeasonNumber(season, seasons);
mapped[seasonKey] = (season.episodes ?? []).map((episode) => {
const episodeNum =
@@ -230,3 +235,24 @@ export function mapRegularSeriesEpisodes(
export function getVodSeriesSeasonKey(season: VodSeriesSeasonVm): string {
return season.season_number || season.name || season.id;
}
export function getVodSeriesSeasonNumber(
season: VodSeriesSeasonVm,
seasons: ReadonlyArray<VodSeriesSeasonVm>
): number {
const parsedSeasonNumber = Number(season.season_number);
if (Number.isInteger(parsedSeasonNumber) && parsedSeasonNumber > 0) {
return parsedSeasonNumber;
}
const orderedSeasons = [...seasons].sort((seasonA, seasonB) =>
naturalSeasonCollator.compare(
getVodSeriesSeasonKey(seasonA),
getVodSeriesSeasonKey(seasonB)
)
);
const seasonIndex = orderedSeasons.findIndex(
(candidate) => candidate.id === season.id
);
return seasonIndex >= 0 ? seasonIndex + 1 : 1;
}
@@ -6,6 +6,7 @@ import {
} from '@iptvnator/portal/shared/util';
import {
getVodSeriesSeasonKey,
getVodSeriesSeasonNumber,
type VodSeriesSeasonVm,
} from '@iptvnator/portal/stalker/data-access';
import type {
@@ -160,16 +161,8 @@ function getLazyVodSeriesEpisodeLabel(
season: VodSeriesSeasonVm,
seasons: ReadonlyArray<VodSeriesSeasonVm>
): string {
const seasonIndex = seasons.findIndex((item) => item.id === season.id);
const parsedSeasonNumber = Number(season.season_number);
const seasonNumber =
Number.isInteger(parsedSeasonNumber) && parsedSeasonNumber > 0
? parsedSeasonNumber
: getFallbackSeasonNumber(seasonIndex);
return formatSeriesEpisodeCode(seasonNumber, 1);
}
function getFallbackSeasonNumber(seasonIndex: number): number {
return seasonIndex >= 0 ? seasonIndex + 1 : 1;
return formatSeriesEpisodeCode(
getVodSeriesSeasonNumber(season, seasons),
1
);
}