fix(downloads): preserve Xtream episode headers

This commit is contained in:
4gray committed 2026-08-02 16:49:17 +02:00
1 parent 3ccc13f852
commit 45d9dcebd5
5 files changed
+63 -8

No files matched your search

@@ -88,6 +88,9 @@ describe('SerialDetailsComponent', () => {
serverUrl: 'http://xtream.example',
username: 'user',
password: 'pass',
userAgent: 'ProtectedProvider/2.0',
referrer: 'https://referrer.example/series',
origin: 'https://origin.example',
});
const fetchSerialDetailsWithMetadata = jest.fn();
const cancelDetailsRequest = jest.fn();
@@ -384,6 +387,11 @@ describe('SerialDetailsComponent', () => {
seriesXtreamId: 103,
seasonNumber: 1,
episodeNumber: 1,
headers: {
userAgent: 'ProtectedProvider/2.0',
referer: 'https://referrer.example/series',
origin: 'https://origin.example',
},
metadataSnapshot: {
version: 1,
language: 'en',
@@ -117,6 +117,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,
metadataContext: createXtreamSeriesDownloadMetadataContext(
item.info,
this.translateService.currentLang ||
@@ -1,4 +1,7 @@
import type { XtreamSerieEpisode } from '@iptvnator/shared/interfaces';
import {
XTREAM_CLIENT_USER_AGENT,
type XtreamSerieEpisode,
} from '@iptvnator/shared/interfaces';
import { createXtreamSeriesDownloadAdapter } from './xtream-series-download.adapter';
const POSTER_URL = 'https://images.test/stills/the-one.jpg';
@@ -10,6 +13,9 @@ const OPTIONS = {
serverUrl: 'http://host/',
username: 'user',
password: 'pass',
userAgent: 'ProtectedProvider/1.0',
referrer: 'https://referrer.test/series',
origin: 'https://origin.test',
metadataContext: {
language: 'en',
title: 'Signal House',
@@ -67,6 +73,11 @@ describe('createXtreamSeriesDownloadAdapter', () => {
seriesXtreamId: 900,
seasonNumber: 2,
episodeNumber: 3,
headers: {
userAgent: 'ProtectedProvider/1.0',
referer: 'https://referrer.test/series',
origin: 'https://origin.test',
},
});
expect(metadataSnapshot).toEqual({
version: 1,
@@ -85,6 +96,27 @@ describe('createXtreamSeriesDownloadAdapter', () => {
});
});
it('keeps the Xtream client user-agent fallback for episode downloads', async () => {
const adapter = createXtreamSeriesDownloadAdapter({
...OPTIONS,
userAgent: ' ',
referrer: undefined,
origin: undefined,
});
const candidate = adapter.createCandidate(episode(), '1');
await expect(candidate?.prepare()).resolves.toEqual(
expect.objectContaining({
headers: {
userAgent: XTREAM_CLIENT_USER_AGENT,
referer: undefined,
origin: undefined,
},
})
);
});
it('preserves the existing season and episode fallback rules', async () => {
const adapter = createXtreamSeriesDownloadAdapter(OPTIONS);
const fromSeasonKey = adapter.createCandidate(
@@ -3,9 +3,10 @@ import {
createSeriesEpisodeDownloadSnapshot,
type DownloadMovieSnapshotInput,
} from '@iptvnator/portal/shared/util';
import type {
XtreamSerieEpisode,
XtreamSerieEpisodeInfo,
import {
XTREAM_CLIENT_USER_AGENT,
type XtreamSerieEpisode,
type XtreamSerieEpisodeInfo,
} from '@iptvnator/shared/interfaces';
export interface XtreamSeriesDownloadAdapterOptions {
@@ -15,6 +16,9 @@ export interface XtreamSeriesDownloadAdapterOptions {
readonly serverUrl?: string;
readonly username?: string;
readonly password?: string;
readonly userAgent?: string;
readonly referrer?: string;
readonly origin?: string;
readonly metadataContext: DownloadMovieSnapshotInput;
}
@@ -88,6 +92,13 @@ export function createXtreamSeriesDownloadAdapter(
title: `${options.title || 'Series'} - ${episodeCode} - ${episode.title}`,
url: `${normalizedServerUrl}/series/${username}/${password}/${episode.id}.${extension}`,
posterUrl: info?.movie_image,
headers: {
userAgent:
options.userAgent?.trim() ||
XTREAM_CLIENT_USER_AGENT,
referer: options.referrer,
origin: options.origin,
},
metadataSnapshot: createSeriesEpisodeDownloadSnapshot({
...options.metadataContext,
episode: {
@@ -1,8 +1,5 @@
export type XtreamPortalStatusType =
| 'active'
| 'inactive'
| 'expired'
| 'unavailable';
'active' | 'inactive' | 'expired' | 'unavailable';
export interface XtreamPortalStatusResponseLike {
user_info?: {
@@ -19,6 +16,10 @@ export interface XtreamCredentialsFromUrl {
const XTREAM_API_ENDPOINT_PATTERN = /\/(?:get|player_api)\.php$/i;
// Some Xtream panels challenge generic Node HTTP clients while allowing
// established IPTV players. Keep API, probe, and download requests aligned.
export const XTREAM_CLIENT_USER_AGENT = 'VLC/3.0.18 LibVLC/3.0.18';
export function normalizeXtreamServerUrl(value: string): string {
const trimmed = value.trim();
if (!trimmed) {