mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
/**
|
|
* Setup actions that browse the seeded sources for a shot: portal catalogs
|
|
* and live lists, the M3U groups view, and alternative sources. Actions
|
|
* here call each other directly rather than through `runAction`, which
|
|
* keeps this module free of a dependency on the dispatcher.
|
|
*/
|
|
|
|
import type { Page } from '@playwright/test';
|
|
|
|
import {
|
|
type CaptureAction,
|
|
clickHrefSuffix,
|
|
goHome,
|
|
openXtreamSection,
|
|
requirePlaylistId,
|
|
} from './capture-navigation-helpers';
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Portal catalogs */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
async function openXtreamVod(page: Page, param: string | null): Promise<void> {
|
|
await openXtreamSection(page, 'vod', param ?? 'Action & Mystery');
|
|
await page.waitForURL(/\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+/, {
|
|
timeout: 30_000,
|
|
});
|
|
await page
|
|
.locator('app-content-hero')
|
|
.waitFor({ state: 'visible', timeout: 30_000 });
|
|
await page.waitForTimeout(700);
|
|
}
|
|
|
|
export async function openXtreamSeries(
|
|
page: Page,
|
|
param: string | null
|
|
): Promise<void> {
|
|
await openXtreamSection(page, 'series', param ?? 'Urban Drama');
|
|
await page.waitForURL(
|
|
/\/workspace\/xtreams\/[^/]+\/series\/[^/]+\/[^/]+/,
|
|
{ timeout: 30_000 }
|
|
);
|
|
await page
|
|
.locator('app-season-container')
|
|
.waitFor({ state: 'visible', timeout: 30_000 });
|
|
|
|
// Season tabs auto-select a season; click the first pill only
|
|
// when no episodes rendered on their own.
|
|
const episode = page.locator('.episode-card, .episode-list-item').first();
|
|
|
|
if (!(await episode.isVisible().catch(() => false))) {
|
|
await page
|
|
.locator('.season-tabs__pill, [data-testid="season-dropdown"]')
|
|
.first()
|
|
.click();
|
|
}
|
|
|
|
await episode.waitFor({ state: 'visible', timeout: 20_000 });
|
|
await page.waitForTimeout(700);
|
|
}
|
|
|
|
export async function openM3uGroups(page: Page): Promise<void> {
|
|
const playlistId = requirePlaylistId('playlists');
|
|
|
|
await goHome(page);
|
|
await page
|
|
.locator(`a[href*="/workspace/playlists/${playlistId}"]`)
|
|
.first()
|
|
.click();
|
|
await page.waitForURL(
|
|
(url) => url.href.includes(`/workspace/playlists/${playlistId}/`),
|
|
{ timeout: 20_000 }
|
|
);
|
|
await clickHrefSuffix(page, `/workspace/playlists/${playlistId}/groups`);
|
|
await page
|
|
.locator('.group-nav-item')
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 20_000 });
|
|
await page.locator('.group-nav-item').first().click();
|
|
// Deliberately no channel click: starting playback would pull a
|
|
// real HLS stream (the mock redirects to a public demo stream),
|
|
// and third-party video frames must never enter a release shot.
|
|
await page
|
|
.locator('[data-test-id="channel-item"]')
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 20_000 });
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Live lists */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* Opens a portal's live section and selects a category. Deliberately no
|
|
* channel click: playback would pull the mock's redirect to a public demo
|
|
* stream, and third-party video frames must never enter a published shot.
|
|
*/
|
|
async function openLiveCategory(
|
|
page: Page,
|
|
provider: 'xtreams' | 'stalker',
|
|
liveSection: 'live' | 'itv',
|
|
param: string | null
|
|
): Promise<void> {
|
|
const playlistId = requirePlaylistId(provider);
|
|
|
|
await goHome(page);
|
|
await clickHrefSuffix(page, `/workspace/${provider}/${playlistId}/vod`);
|
|
await clickHrefSuffix(
|
|
page,
|
|
`/workspace/${provider}/${playlistId}/${liveSection}`
|
|
);
|
|
|
|
const categories = page.locator(
|
|
'app-workspace-context-panel .category-item'
|
|
);
|
|
const category = param
|
|
? categories.filter({ hasText: param }).first()
|
|
: categories.first();
|
|
|
|
await category.waitFor({ state: 'visible', timeout: 30_000 });
|
|
await category.click();
|
|
await page
|
|
.locator('app-channel-list-item')
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 30_000 });
|
|
await page.waitForTimeout(700);
|
|
}
|
|
|
|
async function openXtreamLive(page: Page, param: string | null): Promise<void> {
|
|
await openLiveCategory(page, 'xtreams', 'live', param);
|
|
}
|
|
|
|
async function openStalkerLive(
|
|
page: Page,
|
|
param: string | null
|
|
): Promise<void> {
|
|
await openLiveCategory(page, 'stalker', 'itv', param);
|
|
}
|
|
|
|
async function openXtreamLiveChannel(
|
|
page: Page,
|
|
param: string | null
|
|
): Promise<void> {
|
|
// Unlike `open-xtream-live`, this selects a channel: the marketing
|
|
// scenario serves live streams from local bytes (`local-media`),
|
|
// so playback never reaches a public stream. The player shows a
|
|
// format error, which the phone-view shot never frames; what it
|
|
// needs is the remote status the selection publishes.
|
|
await openXtreamLive(page, param);
|
|
const channel = page.locator('app-channel-list-item').first();
|
|
|
|
await channel.click();
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Alternative sources (guide shots) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* Opens a movie of the primary portal whose copy also exists in the secondary
|
|
* one (seeded with `secondaryXtream`), opens the "…" menu and waits for lazy
|
|
* discovery to add its "Other sources" row.
|
|
*/
|
|
async function openXtreamVodSources(
|
|
page: Page,
|
|
param: string | null
|
|
): Promise<void> {
|
|
requirePlaylistId('xtreams-secondary');
|
|
await openXtreamVod(page, param ?? 'Action & Mystery');
|
|
await page.locator('[data-testid="vod-more-menu"]').click();
|
|
await page
|
|
.locator('[data-test-id="vod-menu-sources"]')
|
|
.waitFor({ state: 'visible', timeout: 30_000 });
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
async function openXtreamVodSourcesMenu(
|
|
page: Page,
|
|
param: string | null
|
|
): Promise<void> {
|
|
await openXtreamVodSources(page, param);
|
|
await page.locator('[data-test-id="vod-menu-sources"]').click();
|
|
|
|
const menu = page.locator('app-vod-sources-menu');
|
|
|
|
await menu.waitFor({ state: 'visible', timeout: 15_000 });
|
|
// "check all" probes every unchecked copy (one get_vod_info plus a
|
|
// HEAD against the mock) so the rows carry verdicts in the frame.
|
|
const checkAll = menu.locator('.sources-menu__check-all');
|
|
|
|
if (await checkAll.isVisible().catch(() => false)) {
|
|
await checkAll.click();
|
|
await checkAll
|
|
.waitFor({ state: 'hidden', timeout: 30_000 })
|
|
.catch(() => undefined);
|
|
}
|
|
|
|
await page.waitForTimeout(700);
|
|
}
|
|
|
|
export const PORTAL_ACTIONS: Readonly<Record<string, CaptureAction>> = {
|
|
'open-xtream-vod': openXtreamVod,
|
|
'open-xtream-series': openXtreamSeries,
|
|
'open-m3u-groups': openM3uGroups,
|
|
'open-xtream-live': openXtreamLive,
|
|
'open-xtream-live-channel': openXtreamLiveChannel,
|
|
'open-stalker-live': openStalkerLive,
|
|
'open-xtream-vod-sources': openXtreamVodSources,
|
|
'open-xtream-vod-sources-menu': openXtreamVodSourcesMenu,
|
|
};
|