feat(mock-data): share poster catalog across mocks

This commit is contained in:
4gray committed 2026-07-26 04:05:03 +02:00
1 parent 51c0d54208
commit 0b2c475b6c
13 files changed
+201 -217

No files matched your search

+7 -1
View File
@@ -39,6 +39,7 @@ Then in IPTVnator, add a new Stalker portal:
| `00:1A:79:00:00:04` | **is-series** | 60% of VOD items have `is_series=1` — tests the Ministra lazy-season flow |
| `00:1A:79:00:00:05` | **embedded-series** | 50% of VOD items have embedded `series[]` arrays — tests the embedded series flow |
| `00:1A:79:00:00:06` | **legacy-pagination** | No `get_all_channels` support — tests the paginated `get_ordered_list` crawl fallback for the full ITV channel list |
| `00:1A:79:00:00:07` | **marketing-demo** | 35 original poster movies with the newest 20 first — safe for screenshots and marketing |
| `<any other MAC>` | **auto** | MAC bytes used as seed → deterministic unique dataset |
## Configuration
@@ -74,7 +75,12 @@ All endpoints are served at `GET /portal.php?action=<action>&...` matching the r
## Cover Images
Cover images and logos use [Picsum Photos](https://picsum.photos) (e.g. `https://picsum.photos/seed/{id}/300/200`). These are real images served from a CDN — no local setup required, but an internet connection is needed for images to display.
Generated scenarios use [Picsum Photos](https://picsum.photos) for cover images
and logos, so they need an internet connection to display artwork. The
`marketing-demo` scenario instead uses the committed, screenshot-safe poster
catalog shared with the Xtream mock. Stalker serves those PNGs itself from
`/assets/marketing/poster/<slug>.png`, so screenshots remain deterministic and
offline once the repository is checked out.
## Stream URLs
+2 -2
View File
@@ -9,7 +9,7 @@
"continuous": true,
"executor": "nx:run-commands",
"options": {
"command": "pnpm tsx apps/stalker-mock-server/src/main.ts",
"command": "pnpm tsx --tsconfig tsconfig.base.json apps/stalker-mock-server/src/main.ts",
"cwd": "{workspaceRoot}",
"color": true,
"env": {
@@ -22,7 +22,7 @@
"continuous": true,
"executor": "nx:run-commands",
"options": {
"command": "pnpm tsx watch apps/stalker-mock-server/src/main.ts",
"command": "pnpm tsx watch --tsconfig tsconfig.base.json apps/stalker-mock-server/src/main.ts",
"cwd": "{workspaceRoot}",
"color": true,
"env": {
@@ -1,4 +1,8 @@
import { faker } from '@faker-js/faker';
import {
MarketingMovieCategoryKey,
POSTER_SHOWCASE_MOVIES,
} from '@iptvnator/shared/marketing-fixtures';
import { ScenarioConfig } from './scenarios.js';
// ---------------------------------------------------------------------------
@@ -115,6 +119,8 @@ export interface GeneratedPortalData {
channels: Map<string, RawChannel[]>; // categoryId -> channels
radio: Map<string, RawRadioStation[]>; // categoryId -> radio stations
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
@@ -146,6 +152,58 @@ function logoUrl(seed: string): string {
return `https://picsum.photos/seed/logo-${seed}/100/100`;
}
const STALKER_MARKETING_VOD_CATEGORIES: RawCategory[] = [
{ id: '2901', title: 'Action & Mystery', alias: 'action_mystery' },
{ id: '2902', title: 'Cosmic & Future Worlds', alias: 'future_worlds' },
{ id: '2903', title: 'Family & Comedy', alias: 'family_comedy' },
{
id: '2904',
title: 'Documentary & Drama',
alias: 'documentary_drama',
},
];
const STALKER_MARKETING_VOD_CATEGORY_IDS: Record<
MarketingMovieCategoryKey,
string
> = {
'action-mystery': '2901',
'future-fantasy': '2902',
'family-comedy': '2903',
'drama-documentary': '2904',
};
function generateMarketingVodItems(): RawVodItem[] {
const assetBaseUrl = `http://localhost:${process.env['PORT'] ?? '3210'}/assets/marketing/poster`;
return POSTER_SHOWCASE_MOVIES.map((movie, index) => {
const id = String(29_000 + index);
const posterUrl = `${assetBaseUrl}/${movie.slug}.png`;
const rating = movie.rating.toFixed(1);
return {
id,
name: movie.name,
o_name: movie.name,
title: movie.name,
cmd: `ffrt4://vod/${id}/index.m3u8`,
screenshot_uri: posterUrl,
cover: posterUrl,
description: `${movie.tagline} ${movie.description}`,
actors: movie.actors,
director: movie.director,
year: String(movie.year),
genre: movie.genre,
genres_str: movie.genre,
rating_imdb: rating,
rating_kinopoisk: rating,
category_id: STALKER_MARKETING_VOD_CATEGORY_IDS[movie.categoryKey],
is_series: 0,
has_files: 1,
};
});
}
// ---------------------------------------------------------------------------
// Generator
// ---------------------------------------------------------------------------
@@ -205,20 +263,39 @@ export function generatePortalData(config: ScenarioConfig): GeneratedPortalData
}
// ------ VOD categories + items ------
data.vodCategories = generateCategories('vod', config.categoryCount.vod);
let vodIndex = 0;
for (const cat of data.vodCategories) {
const items = generateVodItems(
cat.id,
config.itemsPerCategory,
vodIndex,
config.isSeriesFraction,
config.embeddedSeriesFraction,
config.seasonsPerSeries,
config.episodesPerSeason
if (config.marketingFixture) {
data.vodCategories = STALKER_MARKETING_VOD_CATEGORIES.map(
(category) => ({ ...category })
);
data.vod.set(cat.id, items);
vodIndex += config.itemsPerCategory;
const marketingVodItems = generateMarketingVodItems();
data.vodOrder = marketingVodItems;
for (const category of data.vodCategories) {
data.vod.set(
category.id,
marketingVodItems.filter(
(item) => item.category_id === category.id
)
);
}
} else {
data.vodCategories = generateCategories(
'vod',
config.categoryCount.vod
);
let vodIndex = 0;
for (const cat of data.vodCategories) {
const items = generateVodItems(
cat.id,
config.itemsPerCategory,
vodIndex,
config.isSeriesFraction,
config.embeddedSeriesFraction,
config.seasonsPerSeries,
config.episodesPerSeason
);
data.vod.set(cat.id, items);
vodIndex += config.itemsPerCategory;
}
}
// ------ Series categories + items ------
@@ -68,8 +68,12 @@ export function handleGetOrderedList(req: Request, res: Response): void {
} else {
// vod (default)
if (categoryId === '*') {
for (const items of data.vod.values()) {
allItems.push(...items);
if (data.vodOrder) {
allItems.push(...data.vodOrder);
} else {
for (const items of data.vod.values()) {
allItems.push(...items);
}
}
} else {
allItems = data.vod.get(categoryId) ?? [];
@@ -21,6 +21,8 @@ export interface ScenarioConfig {
* `get_ordered_list` crawling (legacy portals).
*/
supportsGetAllChannels?: boolean;
/** Replace generated VOD with the shared screenshot-safe poster catalog. */
marketingFixture?: true;
}
/**
@@ -107,6 +109,18 @@ export const SCENARIOS: Record<string, ScenarioConfig> = {
embeddedSeriesFraction: 0,
supportsGetAllChannels: false,
},
'00:1a:79:00:00:07': {
name: 'marketing-demo',
description: 'Screenshot-safe portal with 35 original poster movies',
seed: 7007,
categoryCount: { itv: 4, radio: 4, vod: 4, series: 4 },
itemsPerCategory: 6,
seasonsPerSeries: 2,
episodesPerSeason: 5,
isSeriesFraction: 0,
embeddedSeriesFraction: 0,
marketingFixture: true,
},
};
/**
+15
View File
@@ -1,4 +1,5 @@
import http from 'http';
import { join } from 'node:path';
import express, { Request, Response } from 'express';
import cors from 'cors';
import portalRouter from './app/routes/portal.route.js';
@@ -8,6 +9,10 @@ import { SCENARIOS } from './app/scenarios.js';
const PORT = parseInt(process.env['PORT'] ?? '3210', 10);
const app = express();
const MARKETING_POSTER_DIRECTORY = join(
process.cwd(),
'apps/xtream-mock-server/public/marketing/poster'
);
// ---------------------------------------------------------------------------
// Middleware
@@ -35,6 +40,16 @@ app.use((req, _res, next) => {
// Routes
// ---------------------------------------------------------------------------
// Serve the shared screenshot-safe poster catalog directly from this process.
app.use(
'/assets/marketing/poster',
express.static(MARKETING_POSTER_DIRECTORY, {
fallthrough: false,
immutable: true,
maxAge: '1y',
})
);
// Stalker portal.php endpoint (direct portal protocol, Electron mode)
app.use('/portal.php', portalRouter);
+5 -4
View File
@@ -153,11 +153,12 @@ application code.
## Release Demo Artwork
The `marketing:marketing` fixture uses 45 original fictional titles for release
The `marketing:marketing` fixture uses 65 original fictional titles for release
screenshots. Its core 30-title artwork pack includes matched posters and
backdrops; 15 additional movie-poster showcase titles use approved local poster
PNGs and the deterministic SVG fallback for missing backdrops. Assets are served
from:
backdrops; 35 additional movie-poster showcase titles use approved local poster
PNGs and the deterministic SVG fallback for missing backdrops. The newest 20
showcase movies are ordered first so they appear immediately in screenshot
catalog grids. Assets are served from:
```text
apps/xtream-mock-server/public/marketing/{poster,backdrop}/
+2 -2
View File
@@ -9,7 +9,7 @@
"continuous": true,
"executor": "nx:run-commands",
"options": {
"command": "pnpm tsx apps/xtream-mock-server/src/main.ts",
"command": "pnpm tsx --tsconfig tsconfig.base.json apps/xtream-mock-server/src/main.ts",
"cwd": "{workspaceRoot}",
"color": true,
"env": {
@@ -22,7 +22,7 @@
"continuous": true,
"executor": "nx:run-commands",
"options": {
"command": "pnpm tsx watch apps/xtream-mock-server/src/main.ts",
"command": "pnpm tsx watch --tsconfig tsconfig.base.json apps/xtream-mock-server/src/main.ts",
"cwd": "{workspaceRoot}",
"color": true,
"env": {
@@ -1,3 +1,8 @@
import {
MarketingMovieCategoryKey,
MarketingMovieFixture,
POSTER_SHOWCASE_MOVIES as SHARED_POSTER_SHOWCASE_MOVIES,
} from '@iptvnator/shared/marketing-fixtures';
import { RawCategory } from './categories.generator.js';
import {
buildEpgListing,
@@ -83,6 +88,27 @@ const VOD_CATEGORIES: RawCategory[] = [
{ category_id: '5204', category_name: 'Documentary & Drama', parent_id: 0 },
];
const XTREAM_VOD_CATEGORY_IDS: Record<MarketingMovieCategoryKey, string> = {
'action-mystery': '5201',
'future-fantasy': '5202',
'family-comedy': '5203',
'drama-documentary': '5204',
};
function toMarketingMovie(movie: MarketingMovieFixture): MarketingMovie {
return {
actors: movie.actors,
categoryId: XTREAM_VOD_CATEGORY_IDS[movie.categoryKey],
description: movie.description,
director: movie.director,
genre: movie.genre,
name: movie.name,
rating: movie.rating,
tagline: movie.tagline,
year: movie.year,
};
}
const SERIES_CATEGORIES: RawCategory[] = [
{ category_id: '5301', category_name: 'Urban Drama', parent_id: 0 },
{ category_id: '5302', category_name: 'Speculative Series', parent_id: 0 },
@@ -352,192 +378,12 @@ const GENERATED_ARTWORK_MOVIES: MarketingMovie[] = [
},
];
const POSTER_SHOWCASE_MOVIES: MarketingMovie[] = [
{
categoryId: '5204',
name: 'Orchard Walls',
description:
'A quiet prison gardener and a new volunteer rebuild an abandoned orchard while both reconsider the lives waiting beyond its walls.',
director: 'Elena March',
actors: 'Darius Cole, Mina Hart, Ruth Bell, Owen Pike',
genre: 'Human Drama',
rating: 8.4,
tagline: 'Some walls are grown, not built.',
year: 2025,
},
{
categoryId: '5203',
name: 'Blue Current',
description:
'A young reef fish and an anxious lantern companion cross a changing ocean current to guide their family home.',
director: 'Talia North',
actors: 'Lumi Vale, Finn Reed, Orla Moss, Kit Arden',
genre: 'Animated Family Adventure',
rating: 8.6,
tagline: 'The smallest light can find the way.',
year: 2026,
},
{
categoryId: '5201',
name: 'Signal Nine',
description:
'A transit reporter and a railway investigator trace a repeating emergency signal through a city that insists nothing happened.',
director: 'Mara Venn',
actors: 'Nadia Cross, Elias Rowe, June Mercer, Tomas Vale',
genre: 'Urban Conspiracy Thriller',
rating: 8.1,
tagline: 'The ninth signal was never sent.',
year: 2026,
},
{
categoryId: '5204',
name: 'Checkout at Noon',
description:
'An exhausted hotel manager has one morning to settle a cheerful traveler and a formidable guest into the same impossible room.',
director: 'Milo Hart',
actors: 'Adrian Moss, Clara Wynn, Evelyn Shore, Ben Vale',
genre: 'Ensemble Comedy',
rating: 7.7,
tagline: 'Every guest has a different plan.',
year: 2025,
},
{
categoryId: '5202',
name: 'Vesper Crown',
description:
'A royal archivist crosses a silent salt kingdom to recover a broken crown before its seven pieces choose a new ruler.',
director: 'Iris March',
actors: 'Selah Quinn, Rowan Pike, Mira Holt, Theo Arden',
genre: 'Fantasy Adventure',
rating: 8.3,
tagline: 'Seven pieces remember the throne.',
year: 2026,
},
{
categoryId: '5201',
name: 'Last Detour',
description:
'A veteran mechanic and a bicycle courier take one mountain road too far and discover why every map avoids it.',
director: 'Noah Rook',
actors: 'Tessa Marr, Leo Venn, Omar Pike, Nina Cross',
genre: 'Road Action Thriller',
rating: 7.9,
tagline: 'The long way back is the only way out.',
year: 2025,
},
{
categoryId: '5204',
name: 'Borrowed Summer',
description:
'Two former friends meet beside a lakeside bus route and spend one final season deciding which memories still belong to them.',
director: 'Lina Shore',
actors: 'Mara Wynn, Julian Hart, Elise Rowe, Finn Cole',
genre: 'Romantic Drama',
rating: 8.0,
tagline: 'Some seasons are only passing through.',
year: 2024,
},
{
categoryId: '5201',
name: 'Under the Floor',
description:
'A tenant renovating an empty family home follows a red thread beneath the floorboards and into a history no room remembers.',
director: 'Oren Vale',
actors: 'Rhea Moss, Daniel Cross, Mina Shore, Asa Bell',
genre: 'Supernatural Horror',
rating: 7.8,
tagline: 'The house kept one thing hidden.',
year: 2025,
},
{
categoryId: '5202',
name: 'Red Winter',
description:
'A planetary botanist crosses a frozen red plain with the last living samples after her remote habitat falls silent.',
director: 'Vera Holt',
actors: 'Nora Venn, Kai March, Elias Moss, Ruth Arden',
genre: 'Science Fiction Survival',
rating: 8.5,
tagline: 'Life travels light.',
year: 2026,
},
{
categoryId: '5203',
name: 'Saturday Champions',
description:
'Six neighborhood footballers with mismatched kits train for the one match their muddy community field has been waiting for.',
director: 'Tomas Hart',
actors: 'Milo Quinn, Ari Bell, Leni Moss, Kit Rowe',
genre: 'Family Sports Comedy',
rating: 7.9,
tagline: 'The score is not the whole story.',
year: 2025,
},
{
categoryId: '5204',
name: 'The Silent Verdict',
description:
'A public defender receives one sealed envelope after an empty courtroom closes and must decide whether justice can survive the truth.',
director: 'June Marr',
actors: 'Amara Voss, Theo Reed, Elias Hart, Cora Wynn',
genre: 'Courtroom Drama',
rating: 8.2,
tagline: 'The final evidence said nothing.',
year: 2026,
},
{
categoryId: '5201',
name: 'Low Tide Letters',
description:
'An aging postal worker crosses the tidal flats to deliver a final bundle of letters to a village missing from every new map.',
director: 'Owen Shore',
actors: 'Bram North, Iris Cole, Lena March, Rafi Bell',
genre: 'Coastal Mystery',
rating: 8.1,
tagline: 'The sea returns every address.',
year: 2024,
},
{
categoryId: '5204',
name: 'Salt & Cedar',
description:
'Two estranged siblings reopen their family restaurant and discover that the old recipes settle fewer arguments than they start.',
director: 'Cora Vale',
actors: 'Mina Wynn, Adrian Rowe, Ruth Moss, Nico Hart',
genre: 'Culinary Dramedy',
rating: 7.8,
tagline: 'Every table remembers.',
year: 2025,
},
{
categoryId: '5201',
name: 'Brass Horizon',
description:
'A desert aircraft engineer races an experimental survey plane ahead of a dust front that has erased every route home.',
director: 'Ren Arden',
actors: 'Talia Cross, Owen Pike, Mara Bell, Jules North',
genre: 'Retro Aviation Adventure',
rating: 8.0,
tagline: 'Build what the horizon cannot stop.',
year: 2023,
},
{
categoryId: '5203',
name: 'Moonlight Delivery',
description:
'A young night courier climbs a winding miniature city to deliver three glowing parcels before the moon disappears.',
director: 'Lumi Hart',
actors: 'Kit Vale, Orla Reed, Finn Moss, Ari Shore',
genre: 'Animated Family Adventure',
rating: 8.4,
tagline: 'Every doorstep needs a little light.',
year: 2026,
},
];
const POSTER_SHOWCASE_MOVIES: MarketingMovie[] =
SHARED_POSTER_SHOWCASE_MOVIES.map(toMarketingMovie);
const MOVIES: MarketingMovie[] = [
...GENERATED_ARTWORK_MOVIES,
...POSTER_SHOWCASE_MOVIES,
...GENERATED_ARTWORK_MOVIES,
];
const SERIES: MarketingSeries[] = [
+14
View File
@@ -14,6 +14,7 @@ The mock server enables:
1. **Local development** without access to a real Stalker portal
2. **Playwright E2E testing** with predictable, deterministic data
3. **Scenario-based testing** via predefined MAC addresses that map to specific data shapes
4. **Screenshot-safe marketing capture** with committed fictional posters shared with the Xtream mock
## Key Design Decisions
@@ -56,6 +57,11 @@ faker.seed(config.seed) // scenario seed; unknown MACs: macToSeed(mac)
│ ├── is_series=1 items (fraction, Ministra flow)
│ └── embedded series[] items (fraction)
│
├── marketingFixture? → shared curated VOD catalog
│ ├── @iptvnator/shared/marketing-fixtures
│ ├── vod Map<categoryId, item[]>
│ └── vodOrder[] preserves screenshot catalog order
│
└── generateCategories('series', N) → seriesCategories[]
└── generateSeriesItems() → series Map<categoryId, item[]>
└── generateSeasons() → seasons Map<seriesItemId, season[]>
@@ -219,6 +225,7 @@ interface ScenarioConfig {
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
}
```
@@ -230,6 +237,13 @@ supporting scenarios, `get_all_channels` (`get-all-channels.handler.ts`,
`{ js: { data, total_items } }` response, excluding channels from censored
(adult) genres.
The `marketing-demo` scenario (`00:1A:79:00:00:07`) replaces faker-generated
VOD with the 35-movie provider-neutral showcase catalog from
`@iptvnator/shared/marketing-fixtures`. Its newest 20 movies are returned first
for the wildcard VOD listing. Poster URLs point back to the Stalker process at
`/assets/marketing/poster/<slug>.png`; `main.ts` serves the committed PNG
directory directly, so the Xtream server does not need to be running.
### Adding a New Scenario
1. Add an entry to the `SCENARIOS` map in `src/app/scenarios.ts`.
+6 -3
View File
@@ -273,11 +273,14 @@ labels instead of trusting provider-local strings.
This scenario is reserved for release screenshots and marketing materials:
- VOD and series data use 45 curated fictional titles instead of faker-generated
- VOD and series data use 65 curated fictional titles instead of faker-generated
titles or real media metadata
- the core 30-title generated artwork pack keeps matched posters and backdrops;
15 additional movie-poster showcase titles use approved local poster PNGs and
the deterministic SVG fallback for their missing backdrops
35 additional movie-poster showcase titles use approved local poster PNGs and
the deterministic SVG fallback for their missing backdrops; the newest 20
appear first in the VOD catalog
- showcase metadata comes from the provider-neutral
`@iptvnator/shared/marketing-fixtures` catalog shared with the Stalker mock
- posters and backdrops are served from committed PNG files in
`apps/xtream-mock-server/public/marketing/{poster,backdrop}/`
- `tools/release/generate-marketing-artwork.ts` generates the PNGs with
+4 -4
View File
@@ -57,10 +57,10 @@
"serve:website": "nx serve website",
"build:website": "nx build website",
"i18n:check": "node tools/i18n/check-drift.mjs",
"release:artwork:dry-run": "tsx tools/release/generate-marketing-artwork.ts --dry-run",
"release:artwork:manifest": "tsx tools/release/generate-marketing-artwork.ts --manifest",
"release:artwork:generate": "tsx tools/release/generate-marketing-artwork.ts --generate",
"release:artwork:validate": "tsx tools/release/generate-marketing-artwork.ts --validate",
"release:artwork:dry-run": "tsx --tsconfig tsconfig.base.json tools/release/generate-marketing-artwork.ts --dry-run",
"release:artwork:manifest": "tsx --tsconfig tsconfig.base.json tools/release/generate-marketing-artwork.ts --manifest",
"release:artwork:generate": "tsx --tsconfig tsconfig.base.json tools/release/generate-marketing-artwork.ts --generate",
"release:artwork:validate": "tsx --tsconfig tsconfig.base.json tools/release/generate-marketing-artwork.ts --validate",
"release:notes:validate": "node tools/release/build-release-notes.mjs --validate",
"release:notes:github": "node tools/release/build-release-notes.mjs --format github",
"release:notes:changelog": "node tools/release/build-release-notes.mjs --format changelog",
+7 -3
View File
@@ -126,7 +126,11 @@ async function ensureXtreamMockServer(): Promise<ChildProcess | undefined> {
const child = spawn(
path.join(workspaceRoot, 'node_modules/.bin/tsx'),
['apps/xtream-mock-server/src/main.ts'],
[
'--tsconfig',
'tsconfig.base.json',
'apps/xtream-mock-server/src/main.ts',
],
{
cwd: workspaceRoot,
env: {
@@ -515,7 +519,7 @@ async function openXtreamVodDetails(page: Page): Promise<void> {
const xtreamId = requireCapturedPlaylistId('xtreams');
await clickHrefSuffix(page, `/workspace/xtreams/${xtreamId}/vod`);
await clickCategoryByName(page, 'Hero Premieres');
await clickCategoryByName(page, 'Action & Mystery');
await clickFirstGridCard(page);
await page.waitForURL(/\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+/, {
timeout: 30_000,
@@ -531,7 +535,7 @@ async function openXtreamSeriesSeason(page: Page): Promise<void> {
const xtreamId = requireCapturedPlaylistId('xtreams');
await clickHrefSuffix(page, `/workspace/xtreams/${xtreamId}/series`);
await clickCategoryByName(page, 'Hero Teams');
await clickCategoryByName(page, 'Urban Drama');
await clickFirstGridCard(page);
await page.waitForURL(/\/workspace\/xtreams\/[^/]+\/series\/[^/]+\/[^/]+/, {
timeout: 30_000,