mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(mock-data): address poster catalog review
This commit is contained in:
1 parent
2b62db8d3f
commit
dd8b33d9fb
3 files changed
+122
-228
No files matched your search
@@ -118,12 +118,12 @@ export interface GeneratedPortalData {
|
||||
radioCategories: RawCategory[];
|
||||
channels: Map<string, RawChannel[]>; // categoryId -> channels
|
||||
radio: Map<string, RawRadioStation[]>; // categoryId -> radio stations
|
||||
vod: Map<string, RawVodItem[]>; // categoryId -> items
|
||||
vod: Map<string, RawVodItem[]>; // categoryId -> items
|
||||
/** Preserves the provider-neutral fixture order for the "*" VOD listing. */
|
||||
vodOrder?: RawVodItem[];
|
||||
series: Map<string, RawSeriesItem[]>; // categoryId -> items
|
||||
seasons: Map<string, RawSeason[]>; // seriesItemId -> seasons
|
||||
epg: Map<string, RawEpgProgram[]>; // channelId -> programs
|
||||
seasons: Map<string, RawSeason[]>; // seriesItemId -> seasons
|
||||
epg: Map<string, RawEpgProgram[]>; // channelId -> programs
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -208,9 +208,7 @@ function generateMarketingVodItems(): RawVodItem[] {
|
||||
// Generator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function generatePortalData(
|
||||
config: ScenarioConfig
|
||||
): GeneratedPortalData {
|
||||
export function generatePortalData(config: ScenarioConfig): GeneratedPortalData {
|
||||
faker.seed(config.seed);
|
||||
|
||||
const data: GeneratedPortalData = {
|
||||
@@ -240,11 +238,7 @@ export function generatePortalData(
|
||||
});
|
||||
let channelIndex = 0;
|
||||
for (const cat of data.itvCategories) {
|
||||
const channels = generateChannels(
|
||||
cat.id,
|
||||
config.itemsPerCategory,
|
||||
channelIndex
|
||||
);
|
||||
const channels = generateChannels(cat.id, config.itemsPerCategory, channelIndex);
|
||||
data.channels.set(cat.id, channels);
|
||||
for (const ch of channels) {
|
||||
data.epg.set(ch.id, generateEpg(ch.name));
|
||||
@@ -305,27 +299,13 @@ export function generatePortalData(
|
||||
}
|
||||
|
||||
// ------ Series categories + items ------
|
||||
data.seriesCategories = generateCategories(
|
||||
'series',
|
||||
config.categoryCount.series
|
||||
);
|
||||
data.seriesCategories = generateCategories('series', config.categoryCount.series);
|
||||
let seriesIndex = 0;
|
||||
for (const cat of data.seriesCategories) {
|
||||
const items = generateSeriesItems(
|
||||
cat.id,
|
||||
config.itemsPerCategory,
|
||||
seriesIndex
|
||||
);
|
||||
const items = generateSeriesItems(cat.id, config.itemsPerCategory, seriesIndex);
|
||||
data.series.set(cat.id, items);
|
||||
for (const item of items) {
|
||||
data.seasons.set(
|
||||
item.id,
|
||||
generateSeasons(
|
||||
item,
|
||||
config.seasonsPerSeries,
|
||||
config.episodesPerSeason
|
||||
)
|
||||
);
|
||||
data.seasons.set(item.id, generateSeasons(item, config.seasonsPerSeries, config.episodesPerSeason));
|
||||
}
|
||||
seriesIndex += config.itemsPerCategory;
|
||||
}
|
||||
@@ -338,95 +318,32 @@ export function generatePortalData(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ITV_GENRE_NAMES = [
|
||||
'News',
|
||||
'Sports',
|
||||
'Movies',
|
||||
'Entertainment',
|
||||
'Kids',
|
||||
'Documentary',
|
||||
'Music',
|
||||
'Comedy',
|
||||
'Drama',
|
||||
'Reality TV',
|
||||
'Lifestyle',
|
||||
'Travel',
|
||||
'Food',
|
||||
'Tech',
|
||||
'Science',
|
||||
'History',
|
||||
'Nature',
|
||||
'Animation',
|
||||
'Gaming',
|
||||
'Shopping',
|
||||
'News', 'Sports', 'Movies', 'Entertainment', 'Kids',
|
||||
'Documentary', 'Music', 'Comedy', 'Drama', 'Reality TV',
|
||||
'Lifestyle', 'Travel', 'Food', 'Tech', 'Science',
|
||||
'History', 'Nature', 'Animation', 'Gaming', 'Shopping',
|
||||
];
|
||||
|
||||
const VOD_GENRE_NAMES = [
|
||||
'Action',
|
||||
'Comedy',
|
||||
'Drama',
|
||||
'Horror',
|
||||
'Thriller',
|
||||
'Romance',
|
||||
'Sci-Fi',
|
||||
'Fantasy',
|
||||
'Animation',
|
||||
'Documentary',
|
||||
'Biography',
|
||||
'Crime',
|
||||
'Mystery',
|
||||
'Adventure',
|
||||
'Family',
|
||||
'War',
|
||||
'Western',
|
||||
'Musical',
|
||||
'Sport',
|
||||
'History',
|
||||
'Action', 'Comedy', 'Drama', 'Horror', 'Thriller',
|
||||
'Romance', 'Sci-Fi', 'Fantasy', 'Animation', 'Documentary',
|
||||
'Biography', 'Crime', 'Mystery', 'Adventure', 'Family',
|
||||
'War', 'Western', 'Musical', 'Sport', 'History',
|
||||
];
|
||||
|
||||
const SERIES_GENRE_NAMES = [
|
||||
'Drama Series',
|
||||
'Comedy Series',
|
||||
'Crime Series',
|
||||
'Sci-Fi Series',
|
||||
'Reality Shows',
|
||||
'Anime',
|
||||
'Soap Opera',
|
||||
'Mini Series',
|
||||
'Documentary Series',
|
||||
'Kids Shows',
|
||||
'Action Series',
|
||||
'Fantasy Series',
|
||||
'Medical',
|
||||
'Legal',
|
||||
'Political',
|
||||
'Romance Series',
|
||||
'Historical',
|
||||
'Thriller Series',
|
||||
'Horror Series',
|
||||
'Western Series',
|
||||
'Drama Series', 'Comedy Series', 'Crime Series', 'Sci-Fi Series',
|
||||
'Reality Shows', 'Anime', 'Soap Opera', 'Mini Series',
|
||||
'Documentary Series', 'Kids Shows', 'Action Series', 'Fantasy Series',
|
||||
'Medical', 'Legal', 'Political', 'Romance Series', 'Historical',
|
||||
'Thriller Series', 'Horror Series', 'Western Series',
|
||||
];
|
||||
|
||||
const RADIO_GENRE_NAMES = [
|
||||
'News Radio',
|
||||
'Talk Radio',
|
||||
'Jazz',
|
||||
'Classical',
|
||||
'Rock',
|
||||
'Pop Hits',
|
||||
'Electronic',
|
||||
'World Music',
|
||||
'Sports Radio',
|
||||
'Public Radio',
|
||||
'Local Stations',
|
||||
'Latin',
|
||||
'Hip Hop',
|
||||
'Ambient',
|
||||
'Oldies',
|
||||
'Country',
|
||||
'Reggae',
|
||||
'Soul',
|
||||
'Dance',
|
||||
'Weather',
|
||||
'News Radio', 'Talk Radio', 'Jazz', 'Classical', 'Rock',
|
||||
'Pop Hits', 'Electronic', 'World Music', 'Sports Radio', 'Public Radio',
|
||||
'Local Stations', 'Latin', 'Hip Hop', 'Ambient', 'Oldies',
|
||||
'Country', 'Reggae', 'Soul', 'Dance', 'Weather',
|
||||
];
|
||||
|
||||
function getGenreNames(type: 'itv' | 'vod' | 'series' | 'radio'): string[] {
|
||||
@@ -460,11 +377,7 @@ function generateCategories(
|
||||
// Channel generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateChannels(
|
||||
categoryId: string,
|
||||
count: number,
|
||||
startIndex: number
|
||||
): RawChannel[] {
|
||||
function generateChannels(categoryId: string, count: number, startIndex: number): RawChannel[] {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const globalIndex = startIndex + i;
|
||||
const id = String(10000 + globalIndex);
|
||||
@@ -523,8 +436,7 @@ function generateVodItems(
|
||||
const id = String(20000 + globalIndex);
|
||||
const title = faker.music.songName() + ': ' + faker.lorem.words(2);
|
||||
const isSeries = i / count < isSeriesFraction;
|
||||
const hasEmbeddedSeries =
|
||||
!isSeries && i / count < isSeriesFraction + embeddedSeriesFraction;
|
||||
const hasEmbeddedSeries = !isSeries && (i / count < isSeriesFraction + embeddedSeriesFraction);
|
||||
|
||||
const item: RawVodItem = {
|
||||
id,
|
||||
@@ -535,9 +447,7 @@ function generateVodItems(
|
||||
screenshot_uri: coverUrl(`vod-${id}`),
|
||||
cover: coverUrl(`vod-cover-${id}`, 300, 450),
|
||||
description: faker.lorem.paragraph(),
|
||||
actors: Array.from({ length: 4 }, () =>
|
||||
faker.person.fullName()
|
||||
).join(', '),
|
||||
actors: Array.from({ length: 4 }, () => faker.person.fullName()).join(', '),
|
||||
director: faker.person.fullName(),
|
||||
year: String(faker.date.past({ years: 20 }).getFullYear()),
|
||||
genre: faker.music.genre(),
|
||||
@@ -569,11 +479,7 @@ function generateEmbeddedEpisodes(count: number): string[] {
|
||||
// Series generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateSeriesItems(
|
||||
categoryId: string,
|
||||
count: number,
|
||||
startIndex: number
|
||||
): RawSeriesItem[] {
|
||||
function generateSeriesItems(categoryId: string, count: number, startIndex: number): RawSeriesItem[] {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const globalIndex = startIndex + i;
|
||||
const id = String(30000 + globalIndex);
|
||||
@@ -587,9 +493,7 @@ function generateSeriesItems(
|
||||
screenshot_uri: coverUrl(`series-${id}`),
|
||||
cover: coverUrl(`series-cover-${id}`, 300, 450),
|
||||
description: faker.lorem.paragraph(),
|
||||
actors: Array.from({ length: 4 }, () =>
|
||||
faker.person.fullName()
|
||||
).join(', '),
|
||||
actors: Array.from({ length: 4 }, () => faker.person.fullName()).join(', '),
|
||||
director: faker.person.fullName(),
|
||||
year: String(faker.date.past({ years: 10 }).getFullYear()),
|
||||
genres_str: [faker.music.genre(), faker.music.genre()].join(', '),
|
||||
|
||||
@@ -33,9 +33,7 @@ app.use((req, _res, next) => {
|
||||
.split(';')
|
||||
.find((c) => c.trim().startsWith('mac='))
|
||||
?.split('=')[1]
|
||||
?.trim() ??
|
||||
(req.query['macAddress'] as string) ??
|
||||
'no-mac';
|
||||
?.trim() ?? (req.query['macAddress'] as string) ?? 'no-mac';
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ${req.method} ${req.path} action=${action} mac=${mac}`
|
||||
);
|
||||
@@ -46,7 +44,9 @@ app.use((req, _res, next) => {
|
||||
// against the public origin of each request, including reverse-proxy headers.
|
||||
app.use((req, res, next) => {
|
||||
if (
|
||||
!['get_ordered_list', 'favorites'].includes(String(req.query['action']))
|
||||
!['get_ordered_list', 'favorites'].includes(
|
||||
String(req.query['action'])
|
||||
)
|
||||
) {
|
||||
next();
|
||||
return;
|
||||
@@ -89,11 +89,7 @@ app.use('/portal.php', portalRouter);
|
||||
* so no app code changes are required.
|
||||
*/
|
||||
app.get('/stalker', (req: Request, res: Response) => {
|
||||
const {
|
||||
macAddress,
|
||||
url: _url,
|
||||
...rest
|
||||
} = req.query as Record<string, string>;
|
||||
const { macAddress, url: _url, ...rest } = req.query as Record<string, string>;
|
||||
const mac = macAddress ?? '00:1a:79:00:00:01';
|
||||
|
||||
// Build a lightweight synthetic request. We need a fresh object with mutable
|
||||
@@ -160,9 +156,7 @@ process.on('unhandledRejection', (reason) => {
|
||||
// When Nx (or any process manager) closes stdin, prevent auto-exit.
|
||||
// The HTTP server handle is what keeps the process alive.
|
||||
process.stdin.resume();
|
||||
process.stdin.on('end', () => {
|
||||
/* ignore stdin close */
|
||||
});
|
||||
process.stdin.on('end', () => { /* ignore stdin close */ });
|
||||
|
||||
server.listen(PORT, () => {
|
||||
const divider = '─'.repeat(62);
|
||||
@@ -182,13 +176,9 @@ server.listen(PORT, () => {
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
console.log(
|
||||
' Any other MAC generates deterministic unique data from MAC bytes.'
|
||||
);
|
||||
console.log(' Any other MAC generates deterministic unique data from MAC bytes.');
|
||||
console.log(` Utilities:`);
|
||||
console.log(` GET http://localhost:${PORT}/health`);
|
||||
console.log(
|
||||
` POST http://localhost:${PORT}/reset (clears favorites + cache)`
|
||||
);
|
||||
console.log(` POST http://localhost:${PORT}/reset (clears favorites + cache)`);
|
||||
console.log(`${divider}\n`);
|
||||
});
|
||||
@@ -90,29 +90,29 @@ All responses follow the Stalker `portal.php` envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": [
|
||||
{
|
||||
"id": "20001",
|
||||
"name": "...",
|
||||
"cmd": "ffrt4://vod/20001/index.m3u8",
|
||||
"screenshot_uri": "https://picsum.photos/seed/vod-20001/300/200",
|
||||
"cover": "https://picsum.photos/seed/vod-cover-20001/300/450",
|
||||
"description": "...",
|
||||
"actors": "...",
|
||||
"director": "...",
|
||||
"year": "2019",
|
||||
"rating_imdb": "7.3",
|
||||
"category_id": "2001",
|
||||
"is_series": 0,
|
||||
"has_files": 1
|
||||
}
|
||||
],
|
||||
"total_items": 40,
|
||||
"max_page_items": 14,
|
||||
"cur_page": 1,
|
||||
"total_pages": 3
|
||||
}
|
||||
"js": {
|
||||
"data": [
|
||||
{
|
||||
"id": "20001",
|
||||
"name": "...",
|
||||
"cmd": "ffrt4://vod/20001/index.m3u8",
|
||||
"screenshot_uri": "https://picsum.photos/seed/vod-20001/300/200",
|
||||
"cover": "https://picsum.photos/seed/vod-cover-20001/300/450",
|
||||
"description": "...",
|
||||
"actors": "...",
|
||||
"director": "...",
|
||||
"year": "2019",
|
||||
"rating_imdb": "7.3",
|
||||
"category_id": "2001",
|
||||
"is_series": 0,
|
||||
"has_files": 1
|
||||
}
|
||||
],
|
||||
"total_items": 40,
|
||||
"max_page_items": 14,
|
||||
"cur_page": 1,
|
||||
"total_pages": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,12 +144,12 @@ marker and an `ffrt4://radio/...` command.
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"cmd": "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
|
||||
"streamer_id": "1",
|
||||
"load": "",
|
||||
"error": ""
|
||||
}
|
||||
"js": {
|
||||
"cmd": "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
|
||||
"streamer_id": "1",
|
||||
"load": "",
|
||||
"error": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,20 +159,20 @@ The stream URL is selected from a pool of 4 real public HLS test streams. The ch
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Channel Name: Program Title",
|
||||
"start": "2026-02-21T10:00:00.000Z",
|
||||
"stop": "2026-02-21T12:00:00.000Z",
|
||||
"start_timestamp": 1740128400,
|
||||
"stop_timestamp": 1740135600,
|
||||
"descr": "...",
|
||||
"category": "News"
|
||||
}
|
||||
]
|
||||
}
|
||||
"js": {
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Channel Name: Program Title",
|
||||
"start": "2026-02-21T10:00:00.000Z",
|
||||
"stop": "2026-02-21T12:00:00.000Z",
|
||||
"start_timestamp": 1740128400,
|
||||
"stop_timestamp": 1740135600,
|
||||
"descr": "...",
|
||||
"category": "News"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -183,22 +183,22 @@ generated schedule, limited by the requested `size`.
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": {
|
||||
"10000": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Channel Name: Program Title",
|
||||
"start": "2026-02-21T10:00:00.000Z",
|
||||
"stop": "2026-02-21T12:00:00.000Z",
|
||||
"start_timestamp": 1740128400,
|
||||
"stop_timestamp": 1740135600,
|
||||
"descr": "...",
|
||||
"category": "News"
|
||||
}
|
||||
]
|
||||
"js": {
|
||||
"data": {
|
||||
"10000": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Channel Name: Program Title",
|
||||
"start": "2026-02-21T10:00:00.000Z",
|
||||
"stop": "2026-02-21T12:00:00.000Z",
|
||||
"start_timestamp": 1740128400,
|
||||
"stop_timestamp": 1740135600,
|
||||
"descr": "...",
|
||||
"category": "News"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -214,18 +214,18 @@ Scenarios are defined in `src/app/scenarios.ts`. Each scenario is a `ScenarioCon
|
||||
|
||||
```typescript
|
||||
interface ScenarioConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
seed: number;
|
||||
categoryCount: { itv: number; radio: number; vod: number; series: number };
|
||||
itemsPerCategory: number;
|
||||
seasonsPerSeries: number;
|
||||
episodesPerSeason: number;
|
||||
isSeriesFraction: number; // 0–1: fraction of VOD with is_series=1
|
||||
embeddedSeriesFraction: number; // 0–1: fraction of VOD with embedded series[]
|
||||
supportsGetAllChannels?: boolean; // default true; false mimics legacy portals
|
||||
// without the ITV get_all_channels action
|
||||
marketingFixture?: true; // replace generated VOD with shared posters
|
||||
name: string;
|
||||
description: string;
|
||||
seed: number;
|
||||
categoryCount: { itv: number; radio: number; vod: number; series: number };
|
||||
itemsPerCategory: number;
|
||||
seasonsPerSeries: number;
|
||||
episodesPerSeason: number;
|
||||
isSeriesFraction: number; // 0–1: fraction of VOD with is_series=1
|
||||
embeddedSeriesFraction: number; // 0–1: fraction of VOD with embedded series[]
|
||||
supportsGetAllChannels?: boolean; // default true; false mimics legacy portals
|
||||
// without the ITV get_all_channels action
|
||||
marketingFixture?: true; // replace generated VOD with shared posters
|
||||
}
|
||||
```
|
||||
|
||||
@@ -264,17 +264,17 @@ Call `POST /reset` to clear all favorites (and regenerated data) between test ru
|
||||
|
||||
```typescript
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm nx run web:serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
url: 'http://localhost:3210/health',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
];
|
||||
{
|
||||
command: 'pnpm nx run web:serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
url: 'http://localhost:3210/health',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Playwright waits for both servers to be healthy before starting tests. If either is already running (e.g. in local dev), it reuses the existing instance.
|
||||
@@ -294,13 +294,13 @@ const MOCK_URL = 'http://localhost:3210/portal.php';
|
||||
const MOCK_MAC = '00:1A:79:00:00:01'; // default scenario
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('http://localhost:3210/reset');
|
||||
await request.post('http://localhost:3210/reset');
|
||||
});
|
||||
|
||||
test('browse VOD categories', async ({ page }) => {
|
||||
// Add portal via UI or programmatically via IndexedDB
|
||||
// Navigate to portal
|
||||
// Assert category list matches expected count (8 for default scenario)
|
||||
// Add portal via UI or programmatically via IndexedDB
|
||||
// Navigate to portal
|
||||
// Assert category list matches expected count (8 for default scenario)
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user