diff --git a/apps/stalker-mock-server/src/app/data-generator.ts b/apps/stalker-mock-server/src/app/data-generator.ts index d877aca81..dbc8cbaf5 100644 --- a/apps/stalker-mock-server/src/app/data-generator.ts +++ b/apps/stalker-mock-server/src/app/data-generator.ts @@ -118,12 +118,12 @@ export interface GeneratedPortalData { radioCategories: RawCategory[]; channels: Map; // categoryId -> channels radio: Map; // categoryId -> radio stations - vod: Map; // categoryId -> items + vod: Map; // categoryId -> items /** Preserves the provider-neutral fixture order for the "*" VOD listing. */ vodOrder?: RawVodItem[]; series: Map; // categoryId -> items - seasons: Map; // seriesItemId -> seasons - epg: Map; // channelId -> programs + seasons: Map; // seriesItemId -> seasons + epg: Map; // 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(', '), diff --git a/apps/stalker-mock-server/src/main.ts b/apps/stalker-mock-server/src/main.ts index dfa665b2a..940620f48 100644 --- a/apps/stalker-mock-server/src/main.ts +++ b/apps/stalker-mock-server/src/main.ts @@ -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; + const { macAddress, url: _url, ...rest } = req.query as Record; 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`); }); diff --git a/docs/architecture/stalker-mock-server.md b/docs/architecture/stalker-mock-server.md index 7de3399bf..932c610d4 100644 --- a/docs/architecture/stalker-mock-server.md +++ b/docs/architecture/stalker-mock-server.md @@ -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) }); ```