mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(downloads): propagate headers to episode transfers
This commit is contained in:
1 parent
8b7ecb02e3
commit
5222288e52
10 files changed
+82
-14
No files matched your search
@@ -4,4 +4,4 @@ area: downloads
|
||||
issues: [897, 1289]
|
||||
---
|
||||
|
||||
Xtream movie downloads now keep their provider-compatible identity for legacy retries after source removal. Recoverable connection drops retain validated partials and show a credential-safe code; Retry resumes only with ETag or Last-Modified, otherwise it safely restarts.
|
||||
Xtream movie and episode downloads now keep their provider-compatible identity from the first request through legacy retries after source removal. Recoverable connection drops retain validated partials and show a credential-safe code; Retry resumes only with ETag or Last-Modified, otherwise it safely restarts.
|
||||
@@ -911,9 +911,10 @@ engine` (restart required) or
|
||||
|
||||
**Download Manager**:
|
||||
|
||||
- Xtream VOD downloads default to the same provider-compatible
|
||||
`XTREAM_CLIENT_USER_AGENT` used by API requests and stream probes, while an
|
||||
explicit playlist User-Agent still wins. Retry, resume, and missing-file
|
||||
- Fresh Xtream movie and series-episode downloads propagate the playlist's
|
||||
User-Agent, Referer, and Origin, defaulting User-Agent to the same
|
||||
provider-compatible `XTREAM_CLIENT_USER_AGENT` used by API requests and
|
||||
stream probes. Retry, resume, and missing-file
|
||||
recovery also add the fallback to legacy Xtream rows that have no stored
|
||||
User-Agent. Because download rows survive source deletion, a headerless
|
||||
legacy row whose playlist is already absent receives the same IPTV-player
|
||||
|
||||
@@ -12,7 +12,7 @@ variants, contextual buttons, and theme-aware styling.
|
||||
- **Queue control (`apps/electron-backend/src/app/events/database/download-runtime.ts`)**
|
||||
`DownloadTask` mirrors a row of the shared `downloads` table (type `Download` in `libs/shared/database/src/lib/schema.ts`) plus transient cancel/pause/progress helpers (shared task types live in `download-task.ts`). Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`. The byte transfer itself lives in `download-transfer.ts`, finalization and retained-partial persistence in `download-finalize.ts`, and the renderer update broadcast in `download-broadcast.ts`.
|
||||
- **Range-aware transfer (`download-transfer.ts`)**
|
||||
The transfer streams the response through the backend's validated Axios redirect helper instead of `electron-dl`. Headers (user agent, referer, origin) are persisted in `request_headers` and re-applied through the same allowlist when read back on retry/resume. Xtream VOD downloads use the playlist's configured User-Agent when present and otherwise share the provider-compatible `XTREAM_CLIENT_USER_AGENT` used by Xtream API requests and stream probes. Retry, resume, and missing-file recovery resolve the owning playlist type and add that fallback to legacy Xtream rows without a stored User-Agent; known Stalker rows are left unchanged. Download rows deliberately outlive individually deleted playlists, so a headerless legacy row whose source no longer exists receives the same IPTV-player fallback because its original provider type cannot be recovered. Active pause/cancel operations abort the current request with `AbortController`; pause keeps the partial file and cancel removes it. Resume checks the existing `.part` size (rejecting anything that is not a regular file, so a symlink planted while paused is never followed). The first response's strong `ETag` (or `Last-Modified`) is persisted in `resume_validator`; only a partial carrying that validator may send `Range: bytes=<offset>-` plus `If-Range` and append bytes. A retained partial without a validator restarts from byte zero and overwrites its `.part`, so a changed remote representation can never be joined to an unverified prefix. A `206 Partial Content` answer must start at the requested offset (`Content-Range` is verified) before bytes are appended; any other 2xx answer — the server ignoring `Range`, or `If-Range` detecting that the remote file changed — restarts the transfer from byte zero over the same `.part` instead of failing the download.
|
||||
The transfer streams the response through the backend's validated Axios redirect helper instead of `electron-dl`. Headers (user agent, referer, origin) are persisted in `request_headers` and re-applied through the same allowlist when read back on retry/resume. Fresh Xtream movie and series-episode downloads propagate the playlist's configured headers, using its User-Agent when present and otherwise sharing the provider-compatible `XTREAM_CLIENT_USER_AGENT` used by Xtream API requests and stream probes. Retry, resume, and missing-file recovery resolve the owning playlist type and add that fallback to legacy Xtream rows without a stored User-Agent; known Stalker rows are left unchanged. Download rows deliberately outlive individually deleted playlists, so a headerless legacy row whose source no longer exists receives the same IPTV-player fallback because its original provider type cannot be recovered. Active pause/cancel operations abort the current request with `AbortController`; pause keeps the partial file and cancel removes it. Resume checks the existing `.part` size (rejecting anything that is not a regular file, so a symlink planted while paused is never followed). The first response's strong `ETag` (or `Last-Modified`) is persisted in `resume_validator`; only a partial carrying that validator may send `Range: bytes=<offset>-` plus `If-Range` and append bytes. A retained partial without a validator restarts from byte zero and overwrites its `.part`, so a changed remote representation can never be joined to an unverified prefix. A `206 Partial Content` answer must start at the requested offset (`Content-Range` is verified) before bytes are appended; any other 2xx answer — the server ignoring `Range`, or `If-Range` detecting that the remote file changed — restarts the transfer from byte zero over the same `.part` instead of failing the download.
|
||||
- **Destination collision policy**
|
||||
Existing destination files are never overwritten, inspected, or deleted.
|
||||
Before starting a new transfer, the backend atomically reserves a free
|
||||
|
||||
@@ -98,9 +98,11 @@ and private-network checks.
|
||||
|
||||
## User-Agent
|
||||
|
||||
Electron's `XTREAM_REQUEST` and stream-probe handlers plus Xtream VOD download
|
||||
requests share the exported `XTREAM_CLIENT_USER_AGENT` fallback. A playlist's
|
||||
explicit User-Agent still wins for its stream probe and download. Legacy
|
||||
Electron's `XTREAM_REQUEST` and stream-probe handlers plus fresh Xtream movie
|
||||
and series-episode download requests share the exported
|
||||
`XTREAM_CLIENT_USER_AGENT` fallback. A playlist's explicit User-Agent,
|
||||
Referer, and Origin are propagated to either download kind; the explicit
|
||||
User-Agent still wins over the fallback. Legacy
|
||||
download rows without a stored User-Agent receive the fallback when retrying,
|
||||
resuming, or recovering a missing completed file. Download rows intentionally
|
||||
survive individual source deletion; when the playlist row is already gone and
|
||||
|
||||
@@ -88,6 +88,9 @@ describe('SerialDetailsComponent', () => {
|
||||
serverUrl: 'http://xtream.example',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
userAgent: 'Provider Player/1.0',
|
||||
referrer: 'https://provider.test/player',
|
||||
origin: 'https://provider.test',
|
||||
});
|
||||
const fetchSerialDetailsWithMetadata = jest.fn();
|
||||
const cancelDetailsRequest = jest.fn();
|
||||
@@ -356,6 +359,14 @@ describe('SerialDetailsComponent', () => {
|
||||
],
|
||||
});
|
||||
expect(seasonContainer?.downloadsEnabled()).toBe(true);
|
||||
expect(seasonContainer?.xtreamDownloadContext()).toEqual({
|
||||
serverUrl: 'http://xtream.example',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
userAgent: 'Provider Player/1.0',
|
||||
referrer: 'https://provider.test/player',
|
||||
origin: 'https://provider.test',
|
||||
});
|
||||
expect(seasonContainer?.downloadMetadataContext()).toEqual(
|
||||
expect.objectContaining({
|
||||
language: 'en',
|
||||
|
||||
@@ -260,6 +260,9 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
serverUrl: playlist.serverUrl,
|
||||
username: playlist.username,
|
||||
password: playlist.password,
|
||||
userAgent: playlist.userAgent,
|
||||
referrer: playlist.referrer,
|
||||
origin: playlist.origin,
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in new issue
Block a user