fix(downloads): resume interrupted Xtream VOD transfers (#1329)

* fix(downloads): resume interrupted Xtream VOD transfers

* fix(downloads): validate partials before resuming

* fix(downloads): propagate headers to episode transfers
This commit is contained in:
4gray authored and GitHub committed 2026-08-01 22:01:10 +02:00
1 parent d44045de31
commit aba89d64cf
29 files changed
+894 -92

No files matched your search

@@ -1,5 +1,6 @@
import {
DownloadMetadataSnapshot,
XTREAM_CLIENT_USER_AGENT,
XtreamSerieEpisode,
XtreamSerieEpisodeInfo,
} from '@iptvnator/shared/interfaces';
@@ -19,6 +20,9 @@ export interface XtreamEpisodeDownloadContext {
serverUrl?: string;
username?: string;
password?: string;
userAgent?: string;
referrer?: string;
origin?: string;
}
/** Provider-neutral metadata already loaded by the detail host. */
@@ -34,6 +38,11 @@ export interface XtreamEpisodeDownloadRequest {
seriesXtreamId: number;
seasonNumber: number;
episodeNumber: number;
headers: {
userAgent: string;
referer?: string;
origin?: string;
};
metadataSnapshot?: DownloadMetadataSnapshot;
}
@@ -150,6 +159,11 @@ export function buildXtreamEpisodeDownloadRequest(options: {
seriesXtreamId: seriesId,
seasonNumber,
episodeNumber,
headers: {
userAgent: context.userAgent?.trim() || XTREAM_CLIENT_USER_AGENT,
referer: context.referrer,
origin: context.origin,
},
...(options.metadataContext
? {
metadataSnapshot: buildMetadataSnapshot(
@@ -1,4 +1,7 @@
import { XtreamSerieEpisode } from '@iptvnator/shared/interfaces';
import {
XTREAM_CLIENT_USER_AGENT,
XtreamSerieEpisode,
} from '@iptvnator/shared/interfaces';
import {
buildXtreamEpisodeDownloadRequest,
getEpisodeDownloadId,
@@ -76,6 +79,35 @@ describe('episode-download.util', () => {
seriesXtreamId: 900,
seasonNumber: 2,
episodeNumber: 3,
headers: {
userAgent: XTREAM_CLIENT_USER_AGENT,
referer: undefined,
origin: undefined,
},
});
});
it('forwards playlist request headers for the first episode transfer', () => {
const request = buildXtreamEpisodeDownloadRequest({
episode: episode({ id: '55' }),
context: {
serverUrl: 'http://host',
username: 'u',
password: 'p',
userAgent: 'Provider Player/1.0',
referrer: 'https://provider.test/player',
origin: 'https://provider.test',
},
playlistId: 'pl-1',
seriesId: 900,
seriesTitle: 'Show',
fallbackSeasonKey: '1',
});
expect(request.headers).toEqual({
userAgent: 'Provider Player/1.0',
referer: 'https://provider.test/player',
origin: 'https://provider.test',
});
});
@@ -157,6 +157,9 @@ describe('SeasonContainerComponent', () => {
serverUrl: 'http://host',
username: 'u',
password: 'p',
userAgent: 'Provider Player/1.0',
referrer: 'https://provider.test/player',
origin: 'https://provider.test',
});
fixture.componentRef.setInput('downloadMetadataContext', {
language: 'en',
@@ -170,6 +173,11 @@ describe('SeasonContainerComponent', () => {
expect(downloadsServiceStub.startDownload).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Signal House - S01E01 - Pilot',
headers: {
userAgent: 'Provider Player/1.0',
referer: 'https://provider.test/player',
origin: 'https://provider.test',
},
metadataSnapshot: expect.objectContaining({
mediaKind: 'series',
title: 'Signal House',
@@ -36,6 +36,7 @@ import {
getEpisodeDownloadId,
isStalkerEpisode,
type SeasonContainerDownloadMetadataContext,
type XtreamEpisodeDownloadContext,
} from './episode-download.util';
import {
EPISODE_INFO_PLAY,
@@ -51,11 +52,7 @@ import { SeasonTabsComponent } from './season-tabs.component';
type EpisodeViewMode = 'grid' | 'list';
const EPISODE_VIEW_MODE_KEY = 'iptvnator_episode_view_mode';
export interface SeasonContainerXtreamDownloadContext {
serverUrl?: string;
username?: string;
password?: string;
}
export type SeasonContainerXtreamDownloadContext = XtreamEpisodeDownloadContext;
export type { SeasonContainerDownloadMetadataContext } from './episode-download.util';