mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
fix(downloads): preserve specials season identity
This commit is contained in:
1 parent
ef1d20ed63
commit
4fb04b340c
6 files changed
+78
-21
No files matched your search
@@ -978,7 +978,9 @@ engine` (restart required) or
|
||||
for both providers; Stalker playback identifiers only resolve the URL. Exact
|
||||
`(playlistId, contentType, xtreamId)` matches are authoritative, while
|
||||
complete playlist/series/season/episode coordinates are a fail-closed legacy
|
||||
fallback that migrates reusable rows to the canonical id. Stalker persists
|
||||
fallback that migrates reusable rows to the canonical id. Numeric season
|
||||
zero, including fallback key `"0"`, remains a valid Specials coordinate for
|
||||
both providers. Stalker persists
|
||||
`episode_identity_scope` separately for regular `/series`, embedded VOD
|
||||
`series[]`, and lazy Ministra VOD `is_series`. Known different scopes do not
|
||||
match; a pre-scope coordinate row is ambiguous and blocked, while an exact
|
||||
|
||||
@@ -85,10 +85,10 @@ variants, contextual buttons, and theme-aware styling.
|
||||
the preflight joins the single trailing refresh; later download-update
|
||||
broadcasts cannot delay that assigned refresh. Restored files therefore
|
||||
become stable skips without requiring a Stalker URL/network request. Both
|
||||
providers
|
||||
use normalized `episode.id` as the canonical
|
||||
providers use normalized `episode.id` as the canonical
|
||||
episode `xtreamId`; Stalker `originalCmd` and `originalId` participate only
|
||||
in URL resolution.
|
||||
in URL resolution. Provider adapters preserve numeric season zero, including
|
||||
a fallback season key of `"0"`, so Specials keep distinct `S00` coordinates.
|
||||
The exact `(playlistId, contentType, xtreamId)` identity is authoritative.
|
||||
Complete `(playlistId, seriesXtreamId, seasonNumber, episodeNumber)`
|
||||
coordinates are a legacy episode-compatibility fallback. Stalker also stores
|
||||
|
||||
+18
-6
@@ -268,23 +268,35 @@ describe('createStalkerSeriesDownloadAdapter', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('preserves the season-key and episode-number fallback rules', () => {
|
||||
it('preserves specials season zero and the existing fallback rules', async () => {
|
||||
const adapter = createStalkerSeriesDownloadAdapter(options());
|
||||
const specials = adapter.createCandidate(
|
||||
episode({ season: 0, episode_num: 0 }),
|
||||
'4'
|
||||
);
|
||||
|
||||
expect(specials?.identity).toEqual(
|
||||
expect.objectContaining({ seasonNumber: 0, episodeNumber: 1 })
|
||||
);
|
||||
expect(
|
||||
adapter.createCandidate(episode({ season: 0, episode_num: 0 }), '4')
|
||||
adapter.createCandidate(episode({ season: Number.NaN }), '0')
|
||||
?.identity
|
||||
).toEqual(
|
||||
expect.objectContaining({ seasonNumber: 4, episodeNumber: 1 })
|
||||
expect.objectContaining({ seasonNumber: 0, episodeNumber: 3 })
|
||||
);
|
||||
expect(
|
||||
adapter.createCandidate(
|
||||
episode({ season: 0, episode_num: Number.NaN }),
|
||||
episode({ season: Number.NaN, episode_num: Number.NaN }),
|
||||
undefined
|
||||
)?.identity
|
||||
).toEqual(
|
||||
expect.objectContaining({ seasonNumber: 1, episodeNumber: 1 })
|
||||
);
|
||||
await expect(specials?.prepare()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Signal House - S00E01 - The Call',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the Stalker Portal title fallback', async () => {
|
||||
@@ -356,11 +368,11 @@ describe('createStalkerSeriesDownloadAdapter', () => {
|
||||
['non-positive season', {}, { season: -1 }, '2'],
|
||||
['unsafe episode number', {}, { episode_num: 1.5 }, '2'],
|
||||
['non-positive episode number', {}, { episode_num: -1 }, '2'],
|
||||
['non-positive fallback season', {}, { season: 0 }, '-2'],
|
||||
['non-positive fallback season', {}, { season: Number.NaN }, '-2'],
|
||||
[
|
||||
'unsafe fallback season',
|
||||
{},
|
||||
{ season: 0 },
|
||||
{ season: Number.NaN },
|
||||
String(Number.MAX_SAFE_INTEGER + 1),
|
||||
],
|
||||
] as const)(
|
||||
|
||||
+20
-2
@@ -61,6 +61,23 @@ function positiveSafeInteger(value: unknown): number | null {
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function downloadSeasonNumber(
|
||||
value: unknown,
|
||||
fallbackSeasonKey: string | undefined
|
||||
): number | null {
|
||||
const parsed = Number(value);
|
||||
if (Number.isSafeInteger(parsed)) {
|
||||
return parsed >= 0 ? parsed : null;
|
||||
}
|
||||
if (value != null && !(typeof value === 'number' && Number.isNaN(value))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedFallback = fallbackSeasonKey?.trim();
|
||||
const fallback = Number(normalizedFallback || 1);
|
||||
return Number.isSafeInteger(fallback) && fallback >= 0 ? fallback : null;
|
||||
}
|
||||
|
||||
function episodeInfo(
|
||||
episode: XtreamSerieEpisode
|
||||
): XtreamSerieEpisodeInfo | undefined {
|
||||
@@ -83,8 +100,9 @@ export function createStalkerSeriesDownloadAdapter(
|
||||
const item = options.item;
|
||||
const xtreamId = positiveSafeInteger(episode.id);
|
||||
const seriesXtreamId = positiveSafeInteger(options.seriesId);
|
||||
const seasonNumber = positiveSafeInteger(
|
||||
episode.season || Number(fallbackSeasonKey) || 1
|
||||
const seasonNumber = downloadSeasonNumber(
|
||||
episode.season,
|
||||
fallbackSeasonKey
|
||||
);
|
||||
const episodeNumber = positiveSafeInteger(episode.episode_num || 1);
|
||||
const episodeIdentityScope =
|
||||
|
||||
+14
-7
@@ -117,26 +117,33 @@ describe('createXtreamSeriesDownloadAdapter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves the existing season and episode fallback rules', async () => {
|
||||
it('preserves specials season zero and the existing fallback rules', async () => {
|
||||
const adapter = createXtreamSeriesDownloadAdapter(OPTIONS);
|
||||
const fromSeasonKey = adapter.createCandidate(
|
||||
const specials = adapter.createCandidate(
|
||||
episode({ season: 0, episode_num: 0 }),
|
||||
'4'
|
||||
);
|
||||
const fromSeasonKey = adapter.createCandidate(
|
||||
episode({ season: Number.NaN }),
|
||||
'0'
|
||||
);
|
||||
const defaults = adapter.createCandidate(
|
||||
episode({ season: 0, episode_num: Number.NaN }),
|
||||
episode({ season: Number.NaN, episode_num: Number.NaN }),
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(specials?.identity).toEqual(
|
||||
expect.objectContaining({ seasonNumber: 0, episodeNumber: 1 })
|
||||
);
|
||||
expect(fromSeasonKey?.identity).toEqual(
|
||||
expect.objectContaining({ seasonNumber: 4, episodeNumber: 1 })
|
||||
expect.objectContaining({ seasonNumber: 0, episodeNumber: 3 })
|
||||
);
|
||||
expect(defaults?.identity).toEqual(
|
||||
expect.objectContaining({ seasonNumber: 1, episodeNumber: 1 })
|
||||
);
|
||||
await expect(fromSeasonKey?.prepare()).resolves.toEqual(
|
||||
await expect(specials?.prepare()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Signal House - S04E01 - The One',
|
||||
title: 'Signal House - S00E01 - The One',
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -180,7 +187,7 @@ describe('createXtreamSeriesDownloadAdapter', () => {
|
||||
['non-positive series id', { seriesId: 0 }, {}, undefined],
|
||||
['unsafe season', {}, { season: 1.5 }, undefined],
|
||||
['unsafe episode number', {}, { episode_num: -1 }, undefined],
|
||||
['unsafe fallback season', {}, { season: 0 }, '-2'],
|
||||
['unsafe fallback season', {}, { season: Number.NaN }, '-2'],
|
||||
] as const)(
|
||||
'returns null for %s',
|
||||
(_label, optionOverrides, episodeOverrides, fallbackSeasonKey) => {
|
||||
|
||||
+20
-2
@@ -31,6 +31,23 @@ function positiveSafeInteger(value: unknown): number | null {
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function downloadSeasonNumber(
|
||||
value: unknown,
|
||||
fallbackSeasonKey: string | undefined
|
||||
): number | null {
|
||||
const parsed = Number(value);
|
||||
if (Number.isSafeInteger(parsed)) {
|
||||
return parsed >= 0 ? parsed : null;
|
||||
}
|
||||
if (value != null && !(typeof value === 'number' && Number.isNaN(value))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedFallback = fallbackSeasonKey?.trim();
|
||||
const fallback = Number(normalizedFallback || 1);
|
||||
return Number.isSafeInteger(fallback) && fallback >= 0 ? fallback : null;
|
||||
}
|
||||
|
||||
function episodeInfo(
|
||||
episode: XtreamSerieEpisode
|
||||
): XtreamSerieEpisodeInfo | undefined {
|
||||
@@ -48,8 +65,9 @@ export function createXtreamSeriesDownloadAdapter(
|
||||
const password = options.password;
|
||||
const xtreamId = positiveSafeInteger(episode.id);
|
||||
const seriesXtreamId = positiveSafeInteger(options.seriesId);
|
||||
const seasonNumber = positiveSafeInteger(
|
||||
episode.season || Number(fallbackSeasonKey) || 1
|
||||
const seasonNumber = downloadSeasonNumber(
|
||||
episode.season,
|
||||
fallbackSeasonKey
|
||||
);
|
||||
const episodeNumber = positiveSafeInteger(episode.episode_num || 1);
|
||||
|
||||
|
||||
Reference in new issue
Block a user