diff --git a/libs/ui/components/src/lib/season-container/episode-download.util.ts b/libs/ui/components/src/lib/season-container/episode-download.util.ts deleted file mode 100644 index de013e077..000000000 --- a/libs/ui/components/src/lib/season-container/episode-download.util.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - DownloadMetadataSnapshot, - XtreamSerieEpisode, - XtreamSerieEpisodeInfo, -} from '@iptvnator/shared/interfaces'; -import { - createMovieDownloadSnapshot, - createSeriesEpisodeDownloadSnapshot, - type DownloadMovieSnapshotInput, -} from '@iptvnator/portal/shared/util'; - -/** - * Pure helpers for mapping episodes (Xtream and Stalker-mapped) to the - * download-manager content ids and download requests used by the season - * container. - */ - -export interface XtreamEpisodeDownloadContext { - serverUrl?: string; - username?: string; - password?: string; -} - -/** Provider-neutral metadata already loaded by the detail host. */ -export type SeasonContainerDownloadMetadataContext = DownloadMovieSnapshotInput; - -export interface XtreamEpisodeDownloadRequest { - playlistId: string; - xtreamId: number; - contentType: 'episode'; - title: string; - url: string; - posterUrl?: string; - seriesXtreamId: number; - seasonNumber: number; - episodeNumber: number; - metadataSnapshot?: DownloadMetadataSnapshot; -} - -function rowCoordinate(value: unknown): number | undefined { - const parsed = Number(value); - return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined; -} - -function buildMetadataSnapshot( - metadata: SeasonContainerDownloadMetadataContext, - episode: XtreamSerieEpisode, - seasonNumber: unknown, - episodeNumber: unknown -): DownloadMetadataSnapshot { - const normalizedSeason = rowCoordinate(seasonNumber); - const normalizedEpisode = rowCoordinate(episodeNumber); - if (normalizedSeason === undefined || normalizedEpisode === undefined) { - return { - ...createMovieDownloadSnapshot(metadata), - mediaKind: 'series', - }; - } - - const info = Array.isArray(episode.info) - ? undefined - : (episode.info as XtreamSerieEpisodeInfo); - return createSeriesEpisodeDownloadSnapshot({ - ...metadata, - episode: { - seasonNumber: normalizedSeason, - episodeNumber: normalizedEpisode, - title: episode.title, - plot: info?.plot, - stillUrl: info?.movie_image, - }, - }); -} - -export function hashString(str: string): number { - let hash = 0; - for (let index = 0; index < str.length; index++) { - const char = str.charCodeAt(index); - hash = (hash << 5) - hash + char; - hash &= hash; - } - return Math.abs(hash); -} - -export function isStalkerEpisode(episode: XtreamSerieEpisode): boolean { - const customSid = (episode as { custom_sid?: string }).custom_sid; - return customSid === 'vod-series' || customSid === 'regular-series'; -} - -export function getEpisodeDownloadId(episode: XtreamSerieEpisode): number { - const customSid = (episode as { custom_sid?: string }).custom_sid; - - if (customSid === 'regular-series') { - const cmd = (episode as { originalCmd?: string }).originalCmd; - if (cmd) { - const match = cmd.match(/file_(\d+)/); - if (match) { - return Number(match[1]); - } - return hashString(cmd); - } - return Number(episode.id); - } - - if (customSid === 'vod-series') { - const originalId = (episode as { originalId?: string | number }) - .originalId; - const numericId = Number(originalId); - return Number.isNaN(numericId) - ? hashString(String(originalId)) - : numericId; - } - - const numericId = Number(episode.id); - return Number.isNaN(numericId) ? hashString(String(episode.id)) : numericId; -} - -export function buildXtreamEpisodeDownloadRequest(options: { - episode: XtreamSerieEpisode; - context: XtreamEpisodeDownloadContext; - playlistId: string; - seriesId: number; - seriesTitle: string; - fallbackSeasonKey: string | undefined; - posterUrl?: string; - metadataContext?: SeasonContainerDownloadMetadataContext; -}): XtreamEpisodeDownloadRequest { - const { episode, context, playlistId, seriesId, seriesTitle } = options; - const serverUrl = context.serverUrl?.replace(/\/$/, '') || ''; - const username = context.username || ''; - const password = context.password || ''; - const extension = episode.container_extension || 'mp4'; - const seasonNumber = - episode.season || Number(options.fallbackSeasonKey) || 1; - const episodeNumber = episode.episode_num || 1; - const title = `${seriesTitle || 'Series'} - S${String( - seasonNumber - ).padStart( - 2, - '0' - )}E${String(episodeNumber).padStart(2, '0')} - ${episode.title}`; - - return { - playlistId, - xtreamId: Number(episode.id), - contentType: 'episode', - title, - url: `${serverUrl}/series/${username}/${password}/${episode.id}.${extension}`, - posterUrl: options.posterUrl, - seriesXtreamId: seriesId, - seasonNumber, - episodeNumber, - ...(options.metadataContext - ? { - metadataSnapshot: buildMetadataSnapshot( - options.metadataContext, - episode, - seasonNumber, - episodeNumber - ), - } - : {}), - }; -} diff --git a/libs/ui/components/src/lib/season-container/episode-utils.spec.ts b/libs/ui/components/src/lib/season-container/episode-utils.spec.ts index c363f2852..c5eb1c7f2 100644 --- a/libs/ui/components/src/lib/season-container/episode-utils.spec.ts +++ b/libs/ui/components/src/lib/season-container/episode-utils.spec.ts @@ -1,158 +1,8 @@ -import { XtreamSerieEpisode } from '@iptvnator/shared/interfaces'; -import { - buildXtreamEpisodeDownloadRequest, - getEpisodeDownloadId, - hashString, - isStalkerEpisode, -} from './episode-download.util'; import { formatEpisodePositionText, parseDuration, } from './episode-progress.util'; -const episode = (overrides: Record): XtreamSerieEpisode => - ({ id: '10', episode_num: 1, title: 'Ep', ...overrides }) as never; - -describe('episode-download.util', () => { - it('detects stalker-mapped episodes', () => { - expect(isStalkerEpisode(episode({ custom_sid: 'vod-series' }))).toBe( - true - ); - expect( - isStalkerEpisode(episode({ custom_sid: 'regular-series' })) - ).toBe(true); - expect(isStalkerEpisode(episode({}))).toBe(false); - }); - - it('resolves download ids per episode source', () => { - expect(getEpisodeDownloadId(episode({}))).toBe(10); - expect( - getEpisodeDownloadId( - episode({ - custom_sid: 'regular-series', - originalCmd: '/media/file_777.mpg', - }) - ) - ).toBe(777); - expect( - getEpisodeDownloadId( - episode({ custom_sid: 'vod-series', originalId: '42' }) - ) - ).toBe(42); - expect( - getEpisodeDownloadId( - episode({ custom_sid: 'vod-series', originalId: 'abc' }) - ) - ).toBe(hashString('abc')); - }); - - it('builds an Xtream episode download request', () => { - const request = buildXtreamEpisodeDownloadRequest({ - episode: episode({ - id: '55', - episode_num: 3, - season: 2, - container_extension: 'mkv', - title: 'The One', - }), - context: { - serverUrl: 'http://host/', - username: 'u', - password: 'p', - }, - playlistId: 'pl-1', - seriesId: 900, - seriesTitle: 'Show', - fallbackSeasonKey: '1', - }); - - expect(request).toEqual({ - playlistId: 'pl-1', - xtreamId: 55, - contentType: 'episode', - title: 'Show - S02E03 - The One', - url: 'http://host/series/u/p/55.mkv', - posterUrl: undefined, - seriesXtreamId: 900, - seasonNumber: 2, - episodeNumber: 3, - }); - }); - - it('adds the loaded series and episode metadata when provided', () => { - const request = buildXtreamEpisodeDownloadRequest({ - episode: episode({ - id: '55', - episode_num: 3, - season: 2, - title: 'The One', - info: { - plot: 'Episode plot', - movie_image: 'https://images.test/stills/one.jpg', - }, - }), - context: { serverUrl: 'http://host' }, - playlistId: 'pl-1', - seriesId: 900, - seriesTitle: 'Show', - fallbackSeasonKey: '1', - metadataContext: { - language: 'de', - title: 'Show', - plot: 'Series plot', - genres: ['Drama'], - posterUrl: 'poster.jpg', - providerCategoryId: '7', - cast: [{ name: 'Actor' }], - }, - }); - - expect(request.metadataSnapshot).toEqual( - expect.objectContaining({ - language: 'de', - mediaKind: 'series', - title: 'Show', - plot: 'Series plot', - genres: ['Drama'], - providerCategoryId: '7', - cast: [{ name: 'Actor' }], - episode: { - seasonNumber: 2, - episodeNumber: 3, - title: 'The One', - plot: 'Episode plot', - stillUrl: 'https://images.test/stills/one.jpg', - }, - }) - ); - }); - - it('creates a valid sparse snapshot without provider enrichment', () => { - const request = buildXtreamEpisodeDownloadRequest({ - episode: episode({ season: 1, info: [] }), - context: {}, - playlistId: 'pl-1', - seriesId: 900, - seriesTitle: 'Show', - fallbackSeasonKey: undefined, - metadataContext: { language: 'en', title: 'Show' }, - }); - - expect(request.metadataSnapshot).toEqual( - expect.objectContaining({ - language: 'en', - mediaKind: 'series', - title: 'Show', - episode: { - seasonNumber: 1, - episodeNumber: 1, - title: 'Ep', - }, - }) - ); - }); -}); - describe('episode-progress.util', () => { it('parses duration strings', () => { expect(parseDuration('01:00:30')).toBe(3630); diff --git a/libs/ui/components/src/lib/season-container/season-container.component.html b/libs/ui/components/src/lib/season-container/season-container.component.html index 4285c4ca1..68a107240 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.html +++ b/libs/ui/components/src/lib/season-container/season-container.component.html @@ -5,26 +5,61 @@ {{ 'PORTALS.SEASONS_AND_EPISODES' | translate }} - @if (hasSeasons() && !showSeasonEmptyState()) { -
- - + @if (downloadPresentationVisible()) { + + } + @if (hasSeasons() && !showSeasonEmptyState()) { +
+ + + grid_view + + + view_list + + +
+ }
} @@ -155,73 +190,52 @@ }}
- @if (isElectron() && downloadsEnabled()) { - @if (isEpisodeDownloaded(episode)) { - - } @else if (isEpisodePaused(episode)) { - - } @else if ( - isEpisodeDownloading(episode) - ) { - - } @else { - - } + @if (downloadPresentationVisible()) { + @let downloadState = + episodeDownloadState(episode); + }
@@ -412,63 +426,45 @@ }} } - @if (isElectron() && downloadsEnabled()) { - @if (isEpisodeDownloaded(episode)) { - - } @else if (isEpisodePaused(episode)) { - - } @else if (isEpisodeDownloading(episode)) { - - } @else { - - } + @if (downloadPresentationVisible()) { + @let downloadState = + episodeDownloadState(episode); + } @if (episodeInfo?.plot) {