diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.spec.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.spec.ts index 0f0639669..fc1f0dd0b 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.spec.ts @@ -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', diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts index d511aa52f..f004af19b 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts @@ -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 || diff --git a/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.spec.ts b/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.spec.ts index 63651ac54..76733d306 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.spec.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.spec.ts @@ -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( diff --git a/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.ts b/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.ts index 19528d3d6..fa711d402 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/xtream-series-download.adapter.ts @@ -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: { diff --git a/libs/shared/interfaces/src/lib/xtream-portal.utils.ts b/libs/shared/interfaces/src/lib/xtream-portal.utils.ts index afb76e303..3eaf73a67 100644 --- a/libs/shared/interfaces/src/lib/xtream-portal.utils.ts +++ b/libs/shared/interfaces/src/lib/xtream-portal.utils.ts @@ -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) {