fix(mock-data): address poster catalog review

This commit is contained in:
4gray committed 2026-07-26 19:52:27 +02:00
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(', '),
+8 -18
View File
@@ -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`);
});
+84 -84
View File
@@ -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)
});
```