feat(mock-data): add shared screenshot-safe poster catalog (#1271)

Moves the fictional movie catalog into `libs/shared/marketing-fixtures` so the
Xtream and Stalker mocks describe the same titles, and adds 20 rendered posters
plus the shared fixture types behind them.

Supporting changes made while getting it green:

- `shared-marketing-fixtures` is classified Tier B in the coverage policy. Not
  Tier A: it is fictional fixture data, so a statement percentage over it means
  nothing, and a Tier A entry would pull it into the merged coverage map and the
  ratchet. Tier B still runs its spec in CI. `stalker-mock-server` needs no entry
  of its own — it is already Tier C and the Tier B/C runner falls back to
  `pnpm nx test <project>`, so its new `marketing-poster-url.spec.ts` runs.
- Two release-capture defects the catalog reorder introduced, both fixed in
  `tools/release/capture-app-driver.ts`:
  - VOD stream ids are `MARKETING_VOD_STREAM_ID_BASE + index` and the generator
    now lists the showcase movies first, so 62000-62002 became Black Harbor, The
    Paper Astronaut and Summer Static while the dashboard seeding still mapped
    those ids to the previous titles' backdrops.
  - the raw `tsx` spawn of the Xtream mock lacked `--tsconfig
    tsconfig.base.json`, so the mock could not resolve
    `@iptvnator/shared/marketing-fixtures` and the capture never started. Both
    mock projects' own serve targets already passed the flag.
This commit is contained in:
4gray authored and GitHub committed 2026-07-27 08:10:57 +02:00
1 parent e1c4853a39
commit e55d55b47f
70 files changed
+2025 -34

No files matched your search

+1
View File
@@ -258,6 +258,7 @@ This is an Nx monorepo with the following structure:
- **shared/logging** - Dependency-free structured redaction for diagnostic logs
- **shared/database** - Canonical Drizzle schema and DB connection (used by the Electron backend)
- **shared/m3u-utils** - M3U playlist utilities
- **shared/marketing-fixtures** - Provider-neutral fictional movie metadata shared by the Xtream and Stalker marketing mocks
- **shared/testing** - Shared test helpers
- **ui/components** - Reusable UI components (incl. channel list)
- **ui/epg** - EPG UI (timeline ribbon, multi-EPG, progress panel, program dialogs)
+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
+13
View File
@@ -0,0 +1,13 @@
export default {
displayName: 'stalker-mock-server',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/stalker-mock-server',
};
+10 -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": {
@@ -33,6 +33,14 @@
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/stalker-mock-server/jest.config.ts",
"tsConfig": "apps/stalker-mock-server/tsconfig.spec.json"
}
}
}
}
@@ -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 = '/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) ?? [];
@@ -0,0 +1,65 @@
import {
buildRequestOrigin,
resolveMarketingPosterUrls,
} from './marketing-poster-url';
type RequestLike = Parameters<typeof buildRequestOrigin>[0];
function request(
protocol: string,
headers: Record<string, string | undefined>
): RequestLike {
return {
protocol,
get: (name: string) => headers[name.toLowerCase()],
};
}
describe('marketing poster URLs', () => {
it('uses the forwarded public origin for relative poster paths', () => {
const req = request('http', {
host: 'stalker-mock:3210',
'x-forwarded-host': 'demo.example:9443',
'x-forwarded-proto': 'https',
});
const payload = {
js: {
data: [
{
cover: '/assets/marketing/poster/black-harbor.png',
screenshot_uri:
'/assets/marketing/poster/black-harbor.png',
},
],
},
};
expect(
resolveMarketingPosterUrls(payload, buildRequestOrigin(req))
).toEqual({
js: {
data: [
{
cover: 'https://demo.example:9443/assets/marketing/poster/black-harbor.png',
screenshot_uri:
'https://demo.example:9443/assets/marketing/poster/black-harbor.png',
},
],
},
});
});
it('preserves unrelated URLs and values', () => {
const payload = {
cover: 'https://picsum.photos/seed/movie/300/450',
count: 1,
};
expect(
resolveMarketingPosterUrls(
payload,
buildRequestOrigin(request('http', { host: 'localhost:3210' }))
)
).toEqual(payload);
});
});
@@ -0,0 +1,51 @@
type RequestOriginSource = {
protocol: string;
get(name: string): string | undefined;
};
const MARKETING_POSTER_PATH_PREFIX = '/assets/marketing/poster/';
export function buildRequestOrigin(request: RequestOriginSource): string {
const protocol =
firstForwardedValue(request.get('x-forwarded-proto')) ??
request.protocol;
const host =
firstForwardedValue(request.get('x-forwarded-host')) ??
request.get('host');
return host ? `${protocol}://${host}` : '';
}
export function resolveMarketingPosterUrls(
payload: unknown,
origin: string
): unknown {
if (
typeof payload === 'string' &&
payload.startsWith(MARKETING_POSTER_PATH_PREFIX)
) {
return origin ? new URL(payload, origin).href : payload;
}
if (Array.isArray(payload)) {
return payload.map((item) => resolveMarketingPosterUrls(item, origin));
}
if (payload && typeof payload === 'object') {
return Object.fromEntries(
Object.entries(payload).map(([key, value]) => [
key,
resolveMarketingPosterUrls(value, origin),
])
);
}
return payload;
}
function firstForwardedValue(value: string | undefined): string | undefined {
return value
?.split(',', 1)[0]
?.trim()
.replace(/^(?:https?):\/\//, '');
}
@@ -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,
},
};
/**
+41
View File
@@ -1,13 +1,22 @@
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';
import dispatchPortalAction from './app/routes/dispatch.js';
import { resetAll } from './app/data-store.js';
import { SCENARIOS } from './app/scenarios.js';
import {
buildRequestOrigin,
resolveMarketingPosterUrls,
} from './app/marketing-poster-url.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
@@ -31,10 +40,42 @@ app.use((req, _res, next) => {
next();
});
// Marketing fixtures store deployment-neutral asset paths. Resolve them
// 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'])
)
) {
next();
return;
}
const sendJson = res.json.bind(res);
const requestOrigin = buildRequestOrigin(req);
res.json = ((body: unknown) =>
sendJson(
resolveMarketingPosterUrls(body, requestOrigin)
)) as Response['json'];
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);
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
+6 -2
View File
@@ -153,8 +153,12 @@ application code.
## Release Demo Artwork
The `marketing:marketing` fixture uses 30 original fictional titles for release
screenshots. Poster and backdrop PNGs are served from:
The `marketing:marketing` fixture uses 65 original fictional titles for release
screenshots. Its core 30-title artwork pack includes matched posters and
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": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

@@ -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 },
@@ -133,7 +159,7 @@ const LIVE_CHANNELS: MarketingLiveChannel[] = [
},
];
const MOVIES: MarketingMovie[] = [
const GENERATED_ARTWORK_MOVIES: MarketingMovie[] = [
{
categoryId: '5201',
name: 'Crimson Skylark',
@@ -352,6 +378,14 @@ const MOVIES: MarketingMovie[] = [
},
];
const POSTER_SHOWCASE_MOVIES: MarketingMovie[] =
SHARED_POSTER_SHOWCASE_MOVIES.map(toMarketingMovie);
const MOVIES: MarketingMovie[] = [
...POSTER_SHOWCASE_MOVIES,
...GENERATED_ARTWORK_MOVIES,
];
const SERIES: MarketingSeries[] = [
{
categoryId: '5301',
@@ -531,7 +565,7 @@ export function buildMarketingPortalFixture(): MarketingPortalFixture {
export function listMarketingArtworkFixtures(): MarketingArtworkFixture[] {
return [
...MOVIES.map((movie) => ({
...GENERATED_ARTWORK_MOVIES.map((movie) => ({
contentType: 'movie' as const,
description: movie.description,
genre: movie.genre,
+16
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,15 @@ 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. Fixtures keep deployment-neutral poster paths,
then the JSON middleware resolves them against the request origin (including
forwarded host/protocol) as
`<portal-origin>/assets/marketing/poster/<slug>.png`. `main.ts` serves the
committed PNG directory directly, so the Xtream server does not need to run.
### Adding a New Scenario
1. Add an entry to the `SCENARIOS` map in `src/app/scenarios.ts`.
+7 -1
View File
@@ -273,8 +273,14 @@ labels instead of trusting provider-local strings.
This scenario is reserved for release screenshots and marketing materials:
- VOD and series data use 30 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;
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
@@ -0,0 +1,184 @@
# First Fictional Poster Pair Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Generate and validate two contrasting fictional movie posters that establish the approved live-action drama and family-animation directions before producing the remaining eight.
**Architecture:** Each poster is generated independently with the built-in image-generation tool and an exact genre-specific prompt. The selected native portrait output is copied into the mock-server poster directory, downscaled to 512 × 768, and visually inspected at final size. The two files remain uncommitted until the user approves the visual direction.
**Tech Stack:** Built-in `image_gen`, PNG assets, macOS `sips`, `file`, `shasum`
---
### Task 1: Generate `ORCHARD WALLS`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/orchard-walls.png`
- [ ] **Step 1: Generate the native portrait image**
Use the built-in `image_gen` tool once with this exact prompt:
```text
Use case: ads-marketing
Asset type: original fictional theatrical movie poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable poster for a quiet contemporary drama titled “ORCHARD WALLS”; it must look like an ordinary professionally distributed live-action film rather than cinematic concept art
Scene/backdrop: a modest rural apple orchard in early autumn, one low weathered stone wall, pale overcast sky, soft distant hills
Subject: one entirely fictional man in his late 50s wearing plain faded work clothes, seated on the wall beneath a fruit tree and looking toward the empty orchard; natural posture, restrained expression, no celebrity resemblance
Style/medium: straightforward realistic movie-poster photography, modest independent-film polish, natural texture, slightly muted print color, no hyperreal detail
Composition/framing: vertical 2:3; the man occupies about one third of the image; one tree and one wall form the whole composition; generous quiet sky; strong silhouette readable at thumbnail size
Lighting/mood: soft cloudy daylight with a small patch of warm late-afternoon light, humane, reflective, hopeful but unsentimental
Color palette: faded olive, stone gray, muted rust, pale cream
Text (verbatim): “ORCHARD WALLS” exactly once, large restrained uppercase serif lettering centered in the upper sky, clearly legible
Constraints: original fictional person and setting; title only; no actor names; no tagline; no credit block; no logos; no brands; no watermark
Avoid: prison corridors, uniforms, guards, electric chairs, supernatural light, famous actors, glossy face montage, dramatic teal-orange grading, volumetric rays, excessive detail, copied movie-poster layouts, malformed hands, extra text, misspelled title
```
Expected result: a restrained one-character drama poster with the exact title
`ORCHARD WALLS`, no other text, and no recognizable person or existing-film
motif.
- [ ] **Step 2: Inspect the generated image before saving**
Use `view_image` on the exact absolute path returned by `image_gen`.
Reject and regenerate if the title is misspelled, any extra text appears, the
person resembles a recognizable actor, the hands are visibly malformed, or the
image reads as elaborate concept art instead of a simple drama poster.
- [ ] **Step 3: Save and downscale the selected image**
Copy the exact generated path returned by the successful tool call to:
```text
apps/xtream-mock-server/public/marketing/poster/orchard-walls.png
```
Then run:
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/orchard-walls.png
```
Expected result: `orchard-walls.png` is rewritten at 512 × 768.
- [ ] **Step 4: Verify the final asset**
Run:
```bash
file apps/xtream-mock-server/public/marketing/poster/orchard-walls.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/orchard-walls.png
```
Expected `file` output includes:
```text
PNG image data, 512 x 768, 8-bit/color RGB
```
Use `view_image` on the final workspace file and confirm the title remains
legible after downscaling.
### Task 2: Generate `BLUE CURRENT`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/blue-current.png`
- [ ] **Step 1: Generate the native portrait image**
Use the built-in `image_gen` tool once with this exact prompt:
```text
Use case: ads-marketing
Asset type: original fictional animated-family-movie poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable poster for an original family animation titled “BLUE CURRENT”; it should resemble professional theatrical animation marketing in clarity and charm without imitating any studio or existing film
Scene/backdrop: open blue ocean with two broad curved current ribbons, a softly lit sandy shelf far below, and only a few simple sea plants
Subject: one entirely original young royal-blue nudibranch sea slug with leaf-shaped golden frills, a compact rounded body, two expressive eyes, and a curious determined expression; it swims diagonally upward while carrying one tiny smooth white pebble
Style/medium: polished but simple stylized 3D family animation, appealing clear shapes, moderate detail, soft materials, not photorealistic and not concept art
Composition/framing: vertical 2:3; one large character centered slightly below the middle; current ribbons create a clean upward curve; uncluttered background; readable at very small thumbnail size
Lighting/mood: bright filtered daylight from above, cheerful, adventurous, gentle
Color palette: ocean blue, turquoise, golden yellow, small white accent
Text (verbatim): “BLUE CURRENT” exactly once, large friendly rounded uppercase lettering centered at the top, clearly legible
Constraints: wholly original character design; title only; no actor names; no tagline; no credit block; no studio logo; no brand; no watermark
Avoid: clownfish, orange-and-white striped fish, parent-and-child fish pairs, sea turtles, forgetful blue fish, coral-reef ensemble casts, recognizable animation-studio character styling, copied poster layouts, hyper-detailed underwater scenery, scary teeth, extra text, misspelled title
```
Expected result: a bright one-character animated poster with the exact title
`BLUE CURRENT`, no other text, and a protagonist unrelated to recognizable
underwater-film characters.
- [ ] **Step 2: Inspect the generated image before saving**
Use `view_image` on the exact absolute path returned by `image_gen`.
Reject and regenerate if the title is misspelled, extra text or a studio mark
appears, the protagonist resembles a known character, the design is too
detailed, or the image contains an ensemble instead of one clear protagonist.
- [ ] **Step 3: Save and downscale the selected image**
Copy the exact generated path returned by the successful tool call to:
```text
apps/xtream-mock-server/public/marketing/poster/blue-current.png
```
Then run:
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/blue-current.png
```
Expected result: `blue-current.png` is rewritten at 512 × 768.
- [ ] **Step 4: Verify the final asset**
Run:
```bash
file apps/xtream-mock-server/public/marketing/poster/blue-current.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/blue-current.png
```
Expected `file` output includes:
```text
PNG image data, 512 x 768, 8-bit/color RGB
```
Use `view_image` on the final workspace file and confirm the title and
protagonist remain readable after downscaling.
### Task 3: Present the Review Checkpoint
**Files:**
- Inspect: `apps/xtream-mock-server/public/marketing/poster/orchard-walls.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/blue-current.png`
- [ ] **Step 1: Verify the two-file scope**
Run:
```bash
git status --short -- apps/xtream-mock-server/public/marketing/poster
```
Expected result: the two new sample files appear. The earlier
`signal-nine.png` draft may also remain untracked but must not be modified by
this plan.
- [ ] **Step 2: Report the generation details**
Show both final workspace images to the user. Report:
- the two absolute workspace paths;
- the exact final dimensions and PNG color mode;
- that the built-in image-generation path was used;
- that the two prompts above were used;
- whether any output required regeneration and why.
- [ ] **Step 3: Pause for visual approval**
Do not generate the remaining eight posters and do not commit the two sample
assets until the user approves both the live-action and animated directions.
@@ -0,0 +1,458 @@
# Remaining Fictional Posters Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Generate, review, and save the remaining eight original fictional movie posters in the approved ten-poster collection.
**Architecture:** Each poster is an independent built-in `image_gen` generation with a genre-specific prompt and no visual dependency on existing repository posters. Every native result is inspected, saved as a 512 × 768 RGB PNG, checked for exact title text and originality, then committed separately after spec and visual-quality review. The existing `signal-nine.png` draft is explicitly replaced because the user requested a simpler real-film direction.
**Tech Stack:** Built-in `image_gen`, PNG assets, macOS `sips`, `file`, `shasum`, Git
---
## Shared Execution Contract
For every task:
1. Read the image-generation skill and its shared prompting references.
2. Do not inspect or use existing poster images as visual references.
3. Use one built-in `image_gen` call with the task's exact prompt.
4. Inspect the native result with `view_image`.
5. Regenerate once if the title is misspelled, extra text appears, anatomy is
malformed, a recognizable actor or franchise motif appears, or the result
violates the task's simplicity requirement.
6. Do not use CLI fallback. If a transient network error repeats, report
`BLOCKED`.
7. Copy the accepted native result to the exact task path and run the task's
`sips` command.
8. Verify the final workspace file with `file`, `shasum -a 256`, and
`view_image`.
9. Add and commit only the task's poster file after its spec and visual-quality
reviews pass.
### Task 1: Replace `SIGNAL NINE`
**Files:**
- Replace: `apps/xtream-mock-server/public/marketing/poster/signal-nine.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical thriller poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable poster for a contemporary urban conspiracy thriller titled “SIGNAL NINE”; it must look like conventional professionally distributed key art, not hyper-detailed science-fiction concept art
Scene/backdrop: a mostly dark underground train platform reduced to simple soft-focus shapes, one stopped silver train, one circular red signal lamp
Subject: two entirely fictional adult leads with no celebrity resemblance; one large close portrait of a female radio journalist in her late 30s, calm but alert, with a smaller male transit investigator in his early 40s standing near the train in the lower background
Style/medium: realistic commercial movie-poster photography with a restrained clean composite, natural faces, subtle print grain, limited detail
Composition/framing: vertical 2:3; one dominant face taking the upper-right half, one small standing figure below-left, red signal as the only graphic accent, strong black negative space; clear at thumbnail size
Lighting/mood: cool charcoal and steel blue with one muted red light, tense, grounded, contemporary
Text (verbatim): “SIGNAL NINE” exactly once, large condensed uppercase sans-serif in off-white at the bottom, clearly legible
Constraints: original fictional people; title only; no actor names; no tagline; no credits; no logos; no brands; no watermark
Avoid: green code rain, sunglasses, leather trench coats, pills, guns, explosions, futuristic cityscapes, neon overload, superhero poses, multiple face montage, recognizable actors, copied poster layouts, excessive skin detail, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/signal-nine.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/signal-nine.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/signal-nine.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, no extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/signal-nine.png
git commit -m "feat(mock-server): add Signal Nine poster"
```
### Task 2: Generate `CHECKOUT AT NOON`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical comedy poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable mainstream comedy poster titled “CHECKOUT AT NOON”; bright, human, and professionally photographed rather than glossy concept art
Scene/backdrop: a seamless warm butter-yellow studio background with one simple hotel luggage cart
Subject: three entirely fictional adults with no celebrity resemblance arranged in an awkward group pose around the cart: an exhausted hotel manager in a burgundy blazer, a cheerful traveler sitting on one suitcase, and a stern older guest holding a tiny room key; natural comedic expressions, contemporary ordinary clothing
Style/medium: clean commercial ensemble photography, lightly playful retouching, crisp simple shapes, moderate realism
Composition/framing: vertical 2:3; full and three-quarter figures, triangular ensemble arrangement, luggage cart as one clear prop, generous clean margins; readable as a small catalog tile
Lighting/mood: soft bright studio light, warm, upbeat, gently chaotic
Color palette: butter yellow, burgundy, cobalt blue, cream
Text (verbatim): “CHECKOUT AT NOON” exactly once, large bold cobalt uppercase sans-serif in three compact lines near the top-left, clearly legible
Constraints: three original fictional adults; title only; no actor names; no tagline; no credits; no hotel logo; no brand; no watermark
Avoid: oversized objects, slapstick action, floating luggage, celebrity likenesses, glamour fashion poses, busy hotel lobby, explosions, weapons, copied comedy-poster poses, excessive detail, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, three plausible people,
one luggage cart, no extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png
git commit -m "feat(mock-server): add Checkout at Noon poster"
```
### Task 3: Generate `VESPER CROWN`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/vesper-crown.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical fantasy poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable fantasy-adventure poster titled “VESPER CROWN”; one strong original image rather than an epic character montage
Scene/backdrop: a vast pale salt flat at violet dusk with a thin horizon and one distant dark mountain
Subject: one entirely fictional young female royal archivist in a practical dark plum travel coat, seen from behind in the lower third; above the horizon floats one large weathered copper crown made from seven uneven geometric arcs, softly suspended without flames
Style/medium: restrained live-action fantasy key art, realistic landscape photography with one practical-looking symbolic object, subtle print texture
Composition/framing: vertical 2:3; small lone figure below, large crown silhouette centered in open sky, minimal elements, iconic thumbnail readability
Lighting/mood: violet twilight, cool quiet mystery, thin copper highlights
Color palette: dusty violet, pale salt white, oxidized copper, dark plum
Text (verbatim): “VESPER CROWN” exactly once, elegant uppercase serif in pale copper near the bottom, clearly legible
Constraints: original person, costume, object, and world; title only; no actor names; no tagline; no credits; no logos; no brands; no watermark
Avoid: swords, dragons, thrones, castles, elves, medieval armor, fire, magical energy beams, floating face montage, recognizable fantasy-franchise motifs, overly ornate crown, excessive particles, copied poster layouts, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/vesper-crown.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/vesper-crown.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/vesper-crown.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, one figure and one
seven-arc copper crown, no extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/vesper-crown.png
git commit -m "feat(mock-server): add Vesper Crown poster"
```
### Task 4: Generate `LAST DETOUR`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/last-detour.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical road-action poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable road-action thriller poster titled “LAST DETOUR”; grounded and physical, not a blockbuster explosion montage
Scene/backdrop: an empty two-lane mountain road at dry late afternoon, one sharp bend, low scrub, distant gray ridges
Subject: one weathered olive-green 1990s compact hatchback stopped diagonally at the roadside; two entirely fictional adults with no celebrity resemblance stand beside it, an older female mechanic in a faded tan work jacket and a younger male courier in a dark red sweatshirt, both looking back down the road
Style/medium: realistic location photography shaped into clean theatrical key art, subtle grain, restrained action tone
Composition/framing: vertical 2:3; car occupies lower middle, two figures form a simple diagonal, road leads into distance, no montage, readable at thumbnail size
Lighting/mood: hard warm sun and long cool shadows, wary, urgent, grounded
Color palette: sun-bleached tan, olive green, asphalt gray, dark red
Text (verbatim): “LAST DETOUR” exactly once, bold slightly condensed uppercase lettering in warm off-white across the lower road, clearly legible
Constraints: original fictional people and unbranded car; title only; no actor names; no tagline; no credits; no logos; no watermark
Avoid: car brands, luxury sports cars, racing, explosions, guns, helicopters, flying debris, urban skyline, celebrity likenesses, franchise action poses, teal-orange grading, multiple vehicles, excessive detail, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/last-detour.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/last-detour.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/last-detour.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, one unbranded hatchback
and two plausible people, no extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/last-detour.png
git commit -m "feat(mock-server): add Last Detour poster"
```
### Task 5: Generate `BORROWED SUMMER`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical romantic-drama poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable romantic-drama poster titled “BORROWED SUMMER”; intimate naturalistic photography rather than glossy romance advertising
Scene/backdrop: the open window of an old local bus parked beside a quiet lakeside road at golden hour, softly blurred reeds and water beyond
Subject: two entirely fictional adults in their early 30s with no celebrity resemblance; one woman sits inside the bus by the open window while one man stands outside, their faces separated by the window frame, sharing a restrained almost-smile rather than touching
Style/medium: naturalistic live-action film photography, warm grain, gentle lens softness, modest festival-film poster finish
Composition/framing: vertical 2:3; two profiles occupy opposite sides of the window frame, simple geometric separation, lake and sunlight remain soft; clear emotional read at thumbnail size
Lighting/mood: warm low sun, tender, fleeting, bittersweet, unforced
Color palette: honey gold, faded bus green, lake blue-gray, soft skin tones
Text (verbatim): “BORROWED SUMMER” exactly once, modest lowercase-style serif rendered in uppercase letters near the bottom, warm cream and clearly legible
Constraints: two original fictional adults; title only; no actor names; no tagline; no credits; no bus logo; no brand; no watermark
Avoid: kissing, wedding imagery, glamour retouching, dramatic tears, heart shapes, celebrity likenesses, backlit face halos, Korean or other extra text, copied romance-poster poses, montage, excessive detail, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, two plausible original
profiles separated by the bus window, no extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png
git commit -m "feat(mock-server): add Borrowed Summer poster"
```
### Task 6: Generate `UNDER THE FLOOR`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/under-the-floor.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical horror poster for IPTV mock data, portrait 2:3
Primary request: create a minimal believable supernatural-horror poster titled “UNDER THE FLOOR”; one unsettling image with no monster reveal and no gore
Scene/backdrop: an empty modest child's bedroom at night, faded cream wallpaper, simple iron bed partly visible, old dark wooden floorboards
Subject: one floorboard near the center is lifted by two centimeters, revealing only a narrow black gap; a single thin red sewing thread crosses the floor and disappears into the gap
Style/medium: realistic restrained horror photography, practical set lighting, subtle film grain, minimal graphic design
Composition/framing: vertical 2:3; low camera angle close to floor, lifted board and red thread as the only focal point, large quiet wall area above, instantly readable silhouette
Lighting/mood: cold moonlight from one unseen window, faint warm hallway spill, silent and deeply uneasy
Color palette: blue-gray, aged cream, dark brown, one restrained red accent
Text (verbatim): “UNDER THE FLOOR” exactly once, narrow uppercase serif in faded cream centered high on the wall, clearly legible
Constraints: title only; no people; no actor names; no tagline; no credits; no logos; no brands; no watermark; no visible creature
Avoid: eyes in darkness, hands, dolls, clowns, blood, gore, teeth, faces, pentagrams, haunted-house exterior, jump-scare imagery, famous horror motifs, extreme darkness that hides the floorboard, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/under-the-floor.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/under-the-floor.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/under-the-floor.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, one lifted floorboard
and one red thread, no person, creature, gore, or extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/under-the-floor.png
git commit -m "feat(mock-server): add Under the Floor poster"
```
### Task 7: Generate `RED WINTER`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/red-winter.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical space-survival poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable science-fiction survival poster titled “RED WINTER”; minimal, physical, and lonely rather than elaborate space concept art
Scene/backdrop: a frozen exoplanet plain covered in pale red snow, one low black ridge, nearly empty dusty-pink sky, one small pale ringed moon
Subject: one tiny entirely fictional planetary botanist in a practical off-white insulated field suit walking away from camera while dragging a narrow sample sled; no visible face, no weapon
Style/medium: restrained live-action science-fiction key art with realistic location texture, simple matte-composite finish, subtle print grain
Composition/framing: vertical 2:3; person and sled occupy the bottom fifth, long single trail crosses the red snow, moon small in upper sky, strong negative space and thumbnail silhouette
Lighting/mood: diffuse frozen daylight, isolated, resilient, quiet
Color palette: pale brick red, dusty rose, off-white, charcoal
Text (verbatim): “RED WINTER” exactly once, wide uppercase sans-serif in off-white centered in the upper sky, clearly legible
Constraints: original world, suit, and character; title only; no actor names; no tagline; no credits; no mission logo; no brand; no watermark
Avoid: Mars landmarks, orange desert, giant astronaut close-up, space station, spaceship, laser, glowing helmet, explosions, storm debris, famous space-film compositions, excessive stars, excessive detail, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/red-winter.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/red-winter.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/red-winter.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, one small botanist, one
sled, one trail, and one small ringed moon, no extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/red-winter.png
git commit -m "feat(mock-server): add Red Winter poster"
```
### Task 8: Generate `SATURDAY CHAMPIONS`
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/saturday-champions.png`
- [ ] **Step 1: Generate the poster**
```text
Use case: ads-marketing
Asset type: original fictional theatrical family sports-comedy poster for IPTV mock data, portrait 2:3
Primary request: create a simple believable family sports-comedy poster titled “SATURDAY CHAMPIONS”; warm ensemble photography with ordinary kids, not an exaggerated sports spectacle
Scene/backdrop: a muddy community soccer field on a bright cloudy Saturday, plain chain-link fence and small neighborhood clubhouse softly blurred behind
Subject: six entirely fictional children aged 10–12 with varied appearances in mismatched unbranded green-and-yellow soccer uniforms posing for an awkward team photo; one child holds a normal scuffed soccer ball, one goalkeeper wears gloves slightly too large, all expressions natural and distinct
Style/medium: cheerful realistic family-film photography, clean theatrical poster composite, modest color polish, no hyperreal detail
Composition/framing: vertical 2:3; two standing rows of three children, simple team-photo geometry with slight comic imperfection, ball visible near center, generous sky for title, clear at thumbnail size
Lighting/mood: soft bright daylight after rain, optimistic, funny, sincere
Color palette: grass green, warm yellow, muddy brown, sky blue-gray
Text (verbatim): “SATURDAY CHAMPIONS” exactly once, large friendly uppercase slab-serif in warm yellow across the upper sky, clearly legible
Constraints: six original fictional children; safe age-appropriate presentation; title only; no actor names; no tagline; no credits; no team badge; no brand; no watermark
Avoid: professional stadium, famous club colors or kits, oversized soccer ball, adults, trophy, fireworks, acrobatic action, celebrity likenesses, glamour poses, copied sports-film ensemble layouts, extra limbs, malformed hands, extra text, misspelled title
```
- [ ] **Step 2: Save and downscale**
```bash
sips -z 768 512 apps/xtream-mock-server/public/marketing/poster/saturday-champions.png
```
- [ ] **Step 3: Verify**
```bash
file apps/xtream-mock-server/public/marketing/poster/saturday-champions.png
shasum -a 256 apps/xtream-mock-server/public/marketing/poster/saturday-champions.png
```
Expected: RGB PNG, exactly 512 × 768, exact title once, exactly six plausible
children, one ball, no badges, brands, or extra text.
- [ ] **Step 4: Commit after both reviews pass**
```bash
git add -- apps/xtream-mock-server/public/marketing/poster/saturday-champions.png
git commit -m "feat(mock-server): add Saturday Champions poster"
```
### Task 9: Validate the Complete Collection
**Files:**
- Inspect: `apps/xtream-mock-server/public/marketing/poster/signal-nine.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/orchard-walls.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/blue-current.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/vesper-crown.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/last-detour.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/under-the-floor.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/red-winter.png`
- Inspect: `apps/xtream-mock-server/public/marketing/poster/saturday-champions.png`
- [ ] **Step 1: Verify file properties**
```bash
file \
apps/xtream-mock-server/public/marketing/poster/signal-nine.png \
apps/xtream-mock-server/public/marketing/poster/orchard-walls.png \
apps/xtream-mock-server/public/marketing/poster/blue-current.png \
apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png \
apps/xtream-mock-server/public/marketing/poster/vesper-crown.png \
apps/xtream-mock-server/public/marketing/poster/last-detour.png \
apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png \
apps/xtream-mock-server/public/marketing/poster/under-the-floor.png \
apps/xtream-mock-server/public/marketing/poster/red-winter.png \
apps/xtream-mock-server/public/marketing/poster/saturday-champions.png
```
Expected: all ten files are 512 × 768, 8-bit RGB PNGs.
- [ ] **Step 2: Verify final hashes**
```bash
shasum -a 256 \
apps/xtream-mock-server/public/marketing/poster/signal-nine.png \
apps/xtream-mock-server/public/marketing/poster/orchard-walls.png \
apps/xtream-mock-server/public/marketing/poster/blue-current.png \
apps/xtream-mock-server/public/marketing/poster/checkout-at-noon.png \
apps/xtream-mock-server/public/marketing/poster/vesper-crown.png \
apps/xtream-mock-server/public/marketing/poster/last-detour.png \
apps/xtream-mock-server/public/marketing/poster/borrowed-summer.png \
apps/xtream-mock-server/public/marketing/poster/under-the-floor.png \
apps/xtream-mock-server/public/marketing/poster/red-winter.png \
apps/xtream-mock-server/public/marketing/poster/saturday-champions.png
```
Expected: ten non-empty SHA-256 values with no duplicate hashes.
- [ ] **Step 3: Perform pairwise visual review**
Inspect all ten final files. Confirm that the collection communicates ten
different releases through distinct genre, palette, composition, type style,
and medium while retaining small-thumbnail readability. Reject a poster if it
looks like a template reuse, has obvious AI artifacts, or resembles a known
film property.
- [ ] **Step 4: Validate repository scope**
```bash
git status --short
git log --oneline -12
```
Expected: no uncommitted poster assets or plan changes remain. The local
`.superpowers/` visual-companion directory may remain untracked and is excluded
from the asset scope.
@@ -0,0 +1,109 @@
# Twenty Shared Marketing Posters Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Generate 20 original posters and expose the complete approved poster showcase through both Xtream and Stalker marketing fixtures.
**Architecture:** A new pure Nx library owns provider-neutral poster-showcase metadata. Xtream and Stalker adapt the same ordered fixtures into their respective protocol shapes and independently serve the shared local PNG files.
**Tech Stack:** TypeScript, Nx, Jest, Express, built-in image generation, PNG/sips validation
---
### Task 1: Create the shared fixture library
**Files:**
- Create: `libs/shared/marketing-fixtures/project.json`
- Create: `libs/shared/marketing-fixtures/src/index.ts`
- Create: `libs/shared/marketing-fixtures/src/lib/marketing-movie.fixture.ts`
- Create: `libs/shared/marketing-fixtures/src/lib/marketing-movie.fixture.spec.ts`
- Create: `libs/shared/marketing-fixtures/tsconfig.json`
- Create: `libs/shared/marketing-fixtures/tsconfig.lib.json`
- Create: `libs/shared/marketing-fixtures/tsconfig.spec.json`
- Create: `libs/shared/marketing-fixtures/jest.config.ts`
- Modify: `tsconfig.base.json`
- [ ] Define a provider-neutral fixture type with category key, title, slug,
description, director, actors, genre, rating, tagline, and year.
- [ ] Move the existing 15 poster-showcase fixtures into the library.
- [ ] Add the 20 approved new fixtures before the existing fixtures.
- [ ] Add Jest coverage for exact counts, unique titles/slugs, ordering, and
valid category keys.
- [ ] Run `pnpm nx test shared-marketing-fixtures` and
`pnpm nx lint shared-marketing-fixtures`.
### Task 2: Generate the 20 posters
**Files:**
- Create: `apps/xtream-mock-server/public/marketing/poster/black-harbor.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/the-paper-astronaut.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/summer-static.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/house-of-tides.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/open-late.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/white-room-six.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/field-notes.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/a-thousand-steps.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/the-small-hours.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/copper-rain.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/willow-engine.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/quiet-thunder.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/parallel-kitchens.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/first-string.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/the-long-museum.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/cloud-hotel.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/unpaid-overtime.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/deep-green.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/november-radio.png`
- Create: `apps/xtream-mock-server/public/marketing/poster/the-last-orange.png`
- [ ] Issue one built-in image-generation request per title.
- [ ] Inspect title spelling, composition, originality, and forbidden text.
- [ ] Copy approved outputs into the project and downscale with
`sips -z 768 512`.
- [ ] Verify every file is an RGB PNG at exactly 512 × 768.
- [ ] Build and inspect a 5 × 4 contact sheet of the final project files.
### Task 3: Adapt Xtream to the shared showcase
**Files:**
- Modify: `apps/xtream-mock-server/src/app/generators/marketing.generator.ts`
- Modify: `apps/xtream-mock-server/README.md`
- Modify: `docs/architecture/xtream-mock-server.md`
- [ ] Replace the local `POSTER_SHOWCASE_MOVIES` metadata with a mapping from
the shared fixture library.
- [ ] Put showcase movies before generated-artwork movies in `MOVIES`.
- [ ] Keep `listMarketingArtworkFixtures()` limited to the original generated
artwork pack.
- [ ] Update documented title counts from 45 to 65.
- [ ] Run `pnpm nx lint xtream-mock-server`.
### Task 4: Add the Stalker marketing scenario
**Files:**
- Modify: `apps/stalker-mock-server/src/app/scenarios.ts`
- Modify: `apps/stalker-mock-server/src/app/data-generator.ts`
- Modify: `apps/stalker-mock-server/src/main.ts`
- Modify: `apps/stalker-mock-server/README.md`
- Modify: `docs/architecture/stalker-mock-server.md`
- [ ] Add `marketingFixture?: true` and MAC `00:1A:79:00:00:07`.
- [ ] Build deterministic Stalker VOD categories/items from the shared fixture.
- [ ] Add the local poster asset route backed by the shared PNG directory.
- [ ] Document credentials, offline artwork behavior, and screenshot use.
- [ ] Run `pnpm nx lint stalker-mock-server`.
### Task 5: Verify both providers
**Files:**
- Inspect all files changed above.
- [ ] Run `pnpm nx test shared-marketing-fixtures`.
- [ ] Start both mock servers with Nx.
- [ ] Assert Xtream returns all 35 showcase titles and the 20 new titles first.
- [ ] Assert Stalker marketing VOD returns all 35 showcase titles and the 20 new
titles first.
- [ ] Fetch every new poster URL from both servers and assert `image/png`.
- [ ] Run `pnpm run release:notes:validate` and `git diff --check`.
- [ ] Confirm docs are updated; skip a release note because mock-server and
documentation paths are release-gate exempt.
@@ -0,0 +1,88 @@
# Fictional Movie Poster Series Design
## Objective
Create ten original fictional movie posters for IPTVnator mock data and
marketing screenshots. The posters should feel like unrelated real theatrical
releases collected by a streaming service, rather than a coordinated AI art
series.
The existing poster assets are not visual references. The user-provided
streaming-catalog screenshot is a collection-level reference for simplicity,
genre legibility, and commercial poster conventions only. No individual poster
from it may be reproduced or closely imitated.
## Output Contract
- Destination:
`apps/xtream-mock-server/public/marketing/poster/`
- Format: RGB PNG
- Final dimensions: 512 × 768 pixels
- Typography: the English movie title only
- Exclude actor names, taglines, credit blocks, studios, brands, and watermarks
- Use lowercase kebab-case filenames derived from the title
## Visual Direction
The series uses different visual languages by genre:
- thrillers may use large faces and one restrained environmental cue;
- dramas may use a quiet scene or a single symbolic composition;
- comedies may use bright studio backgrounds and simple ensemble poses;
- animation may use one expressive original character and a clear environment;
- fantasy and science fiction may use one hero or silhouette with one large
world-building motif.
Every poster should remain readable as a small catalog thumbnail. Prefer one
clear idea, one dominant focal point, a limited palette, and no more than three
prominent characters. Avoid hyper-detailed concept art, excessive cinematic
lighting, glossy AI-style texture, and dense montage unless the genre requires
a restrained ensemble.
All characters, settings, titles, costumes, props, and graphic devices must be
original. Do not use recognizable actors or reproduce franchise characters,
signature symbols, famous poses, or identifiable poster layouts.
## Poster Lineup
1. `SIGNAL NINE` — urban conspiracy thriller; two leads and a subway signal.
2. `ORCHARD WALLS` — humane drama; one character in a quiet landscape.
3. `BLUE CURRENT` — family animation; an original underwater protagonist.
4. `CHECKOUT AT NOON` — comedy; a bright ensemble composition.
5. `VESPER CROWN` — fantasy; one hero and one large symbolic object.
6. `LAST DETOUR` — road action film; a car and two characters.
7. `BORROWED SUMMER` — romantic drama; two people in warm natural light.
8. `UNDER THE FLOOR` — horror; one minimal unsettling image.
9. `RED WINTER` — space survival film; a silhouette and a planet.
10. `SATURDAY CHAMPIONS` — family sports comedy; a colorful team composition.
## Production Sequence
Use the built-in image-generation tool, with one distinct prompt and generation
call per poster. Generate at the tool's native portrait size, inspect the
result, then downscale the selected output to 512 × 768 and save it in the
destination directory.
Begin with two contrasting samples, `ORCHARD WALLS` and `BLUE CURRENT`, so the
user can validate both live-action and animated directions. Generate the
remaining eight only after that review.
## Acceptance Checks
For each poster:
1. Confirm the title is spelled exactly once and remains legible at thumbnail
size.
2. Confirm the subject, composition, and palette clearly communicate the genre.
3. Reject recognizable actors, existing characters, franchise motifs, copied
layouts, malformed faces or hands, extra text, logos, and watermarks.
4. Confirm the final file is an RGB PNG at exactly 512 × 768 pixels.
5. Inspect the final downscaled asset, not only the native generated image.
## Repository Impact
This work adds mock-server marketing assets only. It does not change runtime
behavior, architecture, data flow, or public APIs. Canonical repository
documentation, release notes, unit tests, and E2E tests are not required.
Validation is limited to file format, dimensions, visual inspection, and final
workspace status.
@@ -0,0 +1,96 @@
# Shared Marketing Poster Catalog Design
## Goal
Add 20 original fictional movie posters in the same approved theatrical style
as the existing poster showcase, then expose the complete showcase through both
the Xtream and Stalker mock servers for deterministic screenshot capture.
## Poster set
The new titles are:
1. Black Harbor — maritime crime thriller
2. The Paper Astronaut — stop-motion family animation
3. Summer Static — coming-of-age drama
4. House of Tides — gothic coastal mystery
5. Open Late — workplace comedy
6. White Room Six — psychological thriller
7. Field Notes — nature documentary
8. A Thousand Steps — mountain sports drama
9. The Small Hours — urban romantic drama
10. Copper Rain — neo-noir
11. Willow Engine — family fantasy
12. Quiet Thunder — contemporary western drama
13. Parallel Kitchens — science-fiction comedy
14. First String — music drama
15. The Long Museum — art mystery
16. Cloud Hotel — family animation
17. Unpaid Overtime — office satire
18. Deep Green — survival adventure
19. November Radio — period drama
20. The Last Orange — Mediterranean family drama
Every poster is an RGB PNG at exactly 512 × 768 pixels. The artwork uses one
clear theatrical key image, a legible English title exactly once, original
fictional people or characters, and no actor names, taglines, credits, brands,
logos, watermarks, or copied franchise imagery.
## Shared fixture boundary
A new pure Nx utility library owns the manually approved movie metadata:
`libs/shared/marketing-fixtures`
Its public API exports the fixture type, the existing 15 showcase movies, the
new 20 movies, and the combined 35-title showcase. The fixture uses provider-
neutral category keys. Xtream and Stalker translate those keys to their own
category identifiers and response shapes.
This keeps one source of truth without making either mock application import
from the other application. The existing 30-title generated artwork manifest
remains separate: it still owns its matched poster/backdrop generation jobs,
while the shared showcase owns manually approved local posters and uses each
server's deterministic backdrop fallback where needed.
## Xtream integration
`marketing.generator.ts` imports the shared showcase and adapts it to
`MarketingMovie`. Showcase movies are ordered before the original generated
artwork movies so their posters appear in initial marketing catalog grids.
The existing `marketing:marketing` credentials remain unchanged.
The existing Xtream artwork route continues to serve PNGs from
`apps/xtream-mock-server/public/marketing/poster/`, even though catalog URLs
end in `.svg` for fallback compatibility.
## Stalker integration
Stalker adds a dedicated `marketing` scenario at MAC
`00:1A:79:00:00:07`. For this scenario, VOD categories and items come from the
shared showcase instead of faker. The list uses ordinary non-series VOD shapes,
stable IDs, local cover URLs, fictional metadata, and deterministic playback
commands.
Stalker also exposes `/assets/marketing/poster/:slug` and reads the same
committed PNG directory. It serves the asset itself rather than depending on
the Xtream server being online. Missing files return 404; the marketing
scenario must never emit a missing poster URL.
## Screenshot visibility
The 20 new movies are first in the combined showcase. Both provider adapters
preserve that order, making new posters visible on the first catalog pages
used for screenshots. All artwork is local and requires no internet access.
## Verification
- Shared fixture tests prove exactly 35 unique titles/slugs, with the 20 new
entries first and valid provider-neutral category keys.
- All 20 new PNGs are inspected and verified as RGB 512 × 768 images.
- Xtream `marketing:marketing` returns all 35 showcase titles and every new
poster URL responds with `image/png`.
- Stalker MAC `00:1A:79:00:00:07` returns the same showcase and every new cover
URL responds with `image/png`.
- Run the shared fixture tests, both mock-server lints, release-note validation,
and targeted HTTP smoke checks.
@@ -0,0 +1,21 @@
import baseConfig from '../../../eslint.config.mjs';
export default [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
],
},
],
},
languageOptions: {
parser: await import('jsonc-eslint-parser'),
},
},
];
@@ -0,0 +1,13 @@
module.exports = {
displayName: 'shared-marketing-fixtures',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/shared/marketing-fixtures',
};
@@ -0,0 +1,11 @@
{
"name": "@iptvnator/shared/marketing-fixtures",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}
@@ -0,0 +1,26 @@
{
"name": "shared-marketing-fixtures",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/marketing-fixtures/src",
"projectType": "library",
"tags": ["scope:shared", "domain:shared-runtime", "type:util"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/shared/marketing-fixtures",
"main": "libs/shared/marketing-fixtures/src/index.ts",
"tsConfig": "libs/shared/marketing-fixtures/tsconfig.lib.json",
"assets": []
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/marketing-fixtures/jest.config.cts"
}
}
}
}
@@ -0,0 +1 @@
export * from './lib/shared-marketing-fixtures';
@@ -0,0 +1,71 @@
import {
NEW_MARKETING_MOVIES,
POSTER_SHOWCASE_MOVIES,
} from './shared-marketing-fixtures';
const EXPECTED_NEW_TITLES = [
'Black Harbor',
'The Paper Astronaut',
'Summer Static',
'House of Tides',
'Open Late',
'White Room Six',
'Field Notes',
'A Thousand Steps',
'The Small Hours',
'Copper Rain',
'Willow Engine',
'Quiet Thunder',
'Parallel Kitchens',
'First String',
'The Long Museum',
'Cloud Hotel',
'Unpaid Overtime',
'Deep Green',
'November Radio',
'The Last Orange',
];
describe('poster showcase marketing fixtures', () => {
it('keeps the 20 new movies first in the approved order', () => {
expect(NEW_MARKETING_MOVIES).toHaveLength(20);
expect(NEW_MARKETING_MOVIES.map((movie) => movie.name)).toEqual(
EXPECTED_NEW_TITLES
);
expect(
POSTER_SHOWCASE_MOVIES.slice(0, EXPECTED_NEW_TITLES.length).map(
(movie) => movie.name
)
).toEqual(EXPECTED_NEW_TITLES);
});
it('exposes 35 unique titles and slugs', () => {
expect(POSTER_SHOWCASE_MOVIES).toHaveLength(35);
expect(
new Set(POSTER_SHOWCASE_MOVIES.map((movie) => movie.name)).size
).toBe(35);
expect(
new Set(POSTER_SHOWCASE_MOVIES.map((movie) => movie.slug)).size
).toBe(35);
expect(
POSTER_SHOWCASE_MOVIES.every((movie) =>
/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(movie.slug)
)
).toBe(true);
});
it('uses only provider-neutral category keys', () => {
const validCategoryKeys = new Set([
'action-mystery',
'future-fantasy',
'family-comedy',
'drama-documentary',
]);
expect(
POSTER_SHOWCASE_MOVIES.every((movie) =>
validCategoryKeys.has(movie.categoryKey)
)
).toBe(true);
});
});
@@ -0,0 +1,483 @@
/* eslint-disable max-lines -- This file is a single static, provider-neutral catalog with no behavior. */
export type MarketingMovieCategoryKey =
'action-mystery' | 'future-fantasy' | 'family-comedy' | 'drama-documentary';
export type MarketingMovieFixture = {
actors: string;
categoryKey: MarketingMovieCategoryKey;
description: string;
director: string;
genre: string;
name: string;
rating: number;
slug: string;
tagline: string;
year: number;
};
export const NEW_MARKETING_MOVIES = [
{
categoryKey: 'action-mystery',
name: 'Black Harbor',
slug: 'black-harbor',
description:
'A dock inspector follows a missing cargo ledger through a fogbound harbor where every night crew remembers a different ship.',
director: 'Nadia Rook',
actors: 'Mara Venn, Elias Shore, Tomas Bell, June Cross',
genre: 'Maritime Crime Thriller',
rating: 8.2,
tagline: 'Every tide returns a secret.',
year: 2026,
},
{
categoryKey: 'family-comedy',
name: 'The Paper Astronaut',
slug: 'the-paper-astronaut',
description:
'A careful paper astronaut crosses a handmade bedroom universe to return one fallen star before morning.',
director: 'Lumi North',
actors: 'Kit Arden, Orla Moss, Finn Vale, Mina Hart',
genre: 'Stop-Motion Family Animation',
rating: 8.6,
tagline: 'Fold a small world. Take a giant step.',
year: 2026,
},
{
categoryKey: 'drama-documentary',
name: 'Summer Static',
slug: 'summer-static',
description:
'Four friends rebuild an abandoned community radio booth during the final week before their coastal town changes forever.',
director: 'Elise March',
actors: 'Ari Cole, Leni Wynn, Milo Reed, Tessa Vale',
genre: 'Coming-of-Age Drama',
rating: 8.0,
tagline: 'The clearest signal never lasts.',
year: 2025,
},
{
categoryKey: 'action-mystery',
name: 'House of Tides',
slug: 'house-of-tides',
description:
'A marine surveyor returns to a cliff house whose rooms appear only when the winter tide retreats.',
director: 'Iris Shore',
actors: 'Rhea Voss, Owen Marr, Selah Quinn, Bram North',
genre: 'Gothic Coastal Mystery',
rating: 8.3,
tagline: 'The sea leaves one door open.',
year: 2025,
},
{
categoryKey: 'family-comedy',
name: 'Open Late',
slug: 'open-late',
description:
'Three exhausted night-shift clerks turn a quiet neighborhood shop into an accidental refuge before sunrise.',
director: 'Milo Hart',
actors: 'Clara Moss, Ben Rowe, Juno Pike, Evelyn Cole',
genre: 'Workplace Comedy',
rating: 7.8,
tagline: 'Nothing ordinary happens after midnight.',
year: 2026,
},
{
categoryKey: 'action-mystery',
name: 'White Room Six',
slug: 'white-room-six',
description:
'A sleep researcher wakes inside her own clinical trial with five empty rooms and a recording she does not remember making.',
director: 'Noah Venn',
actors: 'Amara Cross, Theo Holt, Cora Wynn, Elias Vale',
genre: 'Psychological Thriller',
rating: 8.1,
tagline: 'The experiment ended yesterday.',
year: 2026,
},
{
categoryKey: 'drama-documentary',
name: 'Field Notes',
slug: 'field-notes',
description:
'A patient botanist documents the last flowering season of an overlooked wetland and the people protecting it.',
director: 'Ruth Arden',
actors: 'Nora Bell, Kai March, Mina Shore, Owen Reed',
genre: 'Nature Documentary',
rating: 8.5,
tagline: 'Look closely before it changes.',
year: 2024,
},
{
categoryKey: 'drama-documentary',
name: 'A Thousand Steps',
slug: 'a-thousand-steps',
description:
'A retired mountain guide returns to a vertical race carrying a promise made twenty years earlier.',
director: 'Tomas North',
actors: 'Darius Holt, Lina Cross, Rafi Bell, Selah Moss',
genre: 'Mountain Sports Drama',
rating: 8.2,
tagline: 'The summit is not the finish.',
year: 2025,
},
{
categoryKey: 'drama-documentary',
name: 'The Small Hours',
slug: 'the-small-hours',
description:
'Two strangers waiting for the first tram walk through a sleeping city and borrow one honest night from their real lives.',
director: 'Mara Shore',
actors: 'Nadia Wynn, Julian Hart, Elise Vale, Theo Reed',
genre: 'Urban Romantic Drama',
rating: 8.0,
tagline: 'Morning can wait one more stop.',
year: 2024,
},
{
categoryKey: 'action-mystery',
name: 'Copper Rain',
slug: 'copper-rain',
description:
'A municipal photographer discovers the same copper umbrella at every unexplained blackout across the old city.',
director: 'Ren Voss',
actors: 'Iris Marr, Leo Cross, Omar Vale, June Hart',
genre: 'Neo-Noir Mystery',
rating: 8.1,
tagline: 'Some reflections arrive first.',
year: 2025,
},
{
categoryKey: 'future-fantasy',
name: 'Willow Engine',
slug: 'willow-engine',
description:
'A young mechanic awakens a walking wooden engine and follows it into a valley disappearing from every map.',
director: 'Talia Moss',
actors: 'Lumi Vale, Kit Shore, Orla Reed, Finn Arden',
genre: 'Family Fantasy Adventure',
rating: 8.4,
tagline: 'Every old tree remembers a road.',
year: 2026,
},
{
categoryKey: 'drama-documentary',
name: 'Quiet Thunder',
slug: 'quiet-thunder',
description:
'A widowed horse trainer and her estranged son prepare one stubborn mare for the final county storm season.',
director: 'Owen Rook',
actors: 'Tessa Marr, Adrian Cole, Ruth Bell, Nico Hart',
genre: 'Contemporary Western Drama',
rating: 7.9,
tagline: 'The land speaks before the sky.',
year: 2024,
},
{
categoryKey: 'family-comedy',
name: 'Parallel Kitchens',
slug: 'parallel-kitchens',
description:
'Two rival cooks discover matching pantry doors that open into each other’s restaurants in neighboring realities.',
director: 'Cora March',
actors: 'Mina Wynn, Elias Rowe, Bram Pike, Juno Vale',
genre: 'Science-Fiction Comedy',
rating: 7.9,
tagline: 'Dinner is served twice.',
year: 2026,
},
{
categoryKey: 'drama-documentary',
name: 'First String',
slug: 'first-string',
description:
'A reserve orchestra cellist receives one unexpected solo and twenty-four hours to decide whose career it will replace.',
director: 'Lina Venn',
actors: 'Amara Moss, Theo North, Clara Shore, Ren Hart',
genre: 'Music Drama',
rating: 8.3,
tagline: 'One note changes every seat.',
year: 2025,
},
{
categoryKey: 'action-mystery',
name: 'The Long Museum',
slug: 'the-long-museum',
description:
'An overnight guard follows a newly appeared corridor through galleries that display tomorrow’s missing objects.',
director: 'June Vale',
actors: 'Bram Cole, Iris Wynn, Selah Hart, Rafi Cross',
genre: 'Art Mystery',
rating: 8.2,
tagline: 'The newest room has always been there.',
year: 2026,
},
{
categoryKey: 'family-comedy',
name: 'Cloud Hotel',
slug: 'cloud-hotel',
description:
'A shy apprentice opens a tiny floating hotel for weather creatures stranded between seasons.',
director: 'Orla March',
actors: 'Lumi Bell, Kit Vale, Finn Moss, Ari Shore',
genre: 'Animated Family Fantasy',
rating: 8.5,
tagline: 'Every storm needs somewhere to rest.',
year: 2026,
},
{
categoryKey: 'family-comedy',
name: 'Unpaid Overtime',
slug: 'unpaid-overtime',
description:
'Five office workers locked in after hours attempt to finish one impossible presentation without doing any more work.',
director: 'Milo Cross',
actors: 'Cora Hart, Ben Vale, Juno Moss, Adrian Reed',
genre: 'Office Satire',
rating: 7.7,
tagline: 'The deadline already went home.',
year: 2025,
},
{
categoryKey: 'action-mystery',
name: 'Deep Green',
slug: 'deep-green',
description:
'Two cave ecologists follow an underground forest beyond the mapped air line after their only exit floods.',
director: 'Nadia Holt',
actors: 'Rhea Shore, Elias North, Mina Cross, Tomas Bell',
genre: 'Survival Adventure',
rating: 8.0,
tagline: 'Below the roots, keep breathing.',
year: 2026,
},
{
categoryKey: 'drama-documentary',
name: 'November Radio',
slug: 'november-radio',
description:
'In 1978, a remote weather announcer keeps broadcasting through a mountain blackout to listeners she cannot hear.',
director: 'Elena Rook',
actors: 'Mara Voss, Owen Hart, Ruth Vale, Theo March',
genre: 'Period Drama',
rating: 8.4,
tagline: 'Stay with the signal.',
year: 2024,
},
{
categoryKey: 'drama-documentary',
name: 'The Last Orange',
slug: 'the-last-orange',
description:
'Three generations gather to harvest a failing seaside orchard and divide everything except the final fruit.',
director: 'Iris Cole',
actors: 'Evelyn Shore, Nadia Bell, Leo Hart, Mina Vale',
genre: 'Mediterranean Family Drama',
rating: 8.1,
tagline: 'Some inheritances must be shared.',
year: 2025,
},
] as const satisfies readonly MarketingMovieFixture[];
export const EXISTING_MARKETING_MOVIES = [
{
categoryKey: 'drama-documentary',
name: 'Orchard Walls',
slug: '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,
},
{
categoryKey: 'family-comedy',
name: 'Blue Current',
slug: '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,
},
{
categoryKey: 'action-mystery',
name: 'Signal Nine',
slug: '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,
},
{
categoryKey: 'family-comedy',
name: 'Checkout at Noon',
slug: '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,
},
{
categoryKey: 'future-fantasy',
name: 'Vesper Crown',
slug: '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,
},
{
categoryKey: 'action-mystery',
name: 'Last Detour',
slug: '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,
},
{
categoryKey: 'drama-documentary',
name: 'Borrowed Summer',
slug: '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,
},
{
categoryKey: 'action-mystery',
name: 'Under the Floor',
slug: '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,
},
{
categoryKey: 'future-fantasy',
name: 'Red Winter',
slug: '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,
},
{
categoryKey: 'family-comedy',
name: 'Saturday Champions',
slug: '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,
},
{
categoryKey: 'drama-documentary',
name: 'The Silent Verdict',
slug: '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,
},
{
categoryKey: 'action-mystery',
name: 'Low Tide Letters',
slug: '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,
},
{
categoryKey: 'family-comedy',
name: 'Salt & Cedar',
slug: '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,
},
{
categoryKey: 'action-mystery',
name: 'Brass Horizon',
slug: '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,
},
{
categoryKey: 'family-comedy',
name: 'Moonlight Delivery',
slug: '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,
},
] as const satisfies readonly MarketingMovieFixture[];
export const POSTER_SHOWCASE_MOVIES = [
...NEW_MARKETING_MOVIES,
...EXISTING_MARKETING_MOVIES,
] as const satisfies readonly MarketingMovieFixture[];
@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": [
"jest.config.ts",
"jest.config.cts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}
@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"jest.config.cts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
+4 -4
View File
@@ -58,10 +58,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",
+6
View File
@@ -277,6 +277,12 @@
"validationCommand": "pnpm nx test release-tools",
"reason": "Node tests validate release-note parsing, rendering and gate policy; the scripts are release tooling, not shipped source."
},
{
"name": "shared-marketing-fixtures",
"root": "libs/shared/marketing-fixtures",
"validationCommand": "pnpm nx test shared-marketing-fixtures",
"reason": "Screenshot-safe fictional poster catalog shared by both mock servers; the spec asserts catalog order and shape, and percentage coverage over fixture data would not mean anything."
},
{
"name": "remote-control-web",
"root": "apps/remote-control-web",
+16 -4
View File
@@ -72,7 +72,14 @@ export async function ensureXtreamMockServer(
const child = spawn(
path.join(workspaceRoot, 'node_modules/.bin/tsx'),
['apps/xtream-mock-server/src/main.ts'],
// The mock server imports `@iptvnator/shared/marketing-fixtures`, and
// tsx only resolves that path alias when pointed at the base config —
// matching how the project's own serve target invokes it.
[
'--tsconfig',
'tsconfig.base.json',
'apps/xtream-mock-server/src/main.ts',
],
{
cwd: workspaceRoot,
env: { ...process.env, NODE_ENV: 'development', PORT: '3211' },
@@ -217,10 +224,15 @@ async function seedDashboardActivity(page: Page): Promise<void> {
`${XTREAM_MOCK_ORIGIN}/assets/marketing/backdrop/${title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')}.svg?size=${encodeURIComponent('1280x720')}`;
// Xtream VOD stream ids are assigned by array position
// (`MARKETING_VOD_STREAM_ID_BASE + index` in marketing.generator.ts), and
// the generator lists POSTER_SHOWCASE_MOVIES first. Keep these three titles
// in step with the first three entries of that fixture list, otherwise the
// seeded backdrops belong to different movies than the ids resolve to.
const items = [
{ xtreamId: 62000, type: 'movie', backdropUrl: backdrop('Crimson Skylark'), recent: true },
{ xtreamId: 62001, type: 'movie', backdropUrl: backdrop('The Voltage Guard'), recent: true },
{ xtreamId: 62002, type: 'movie', backdropUrl: backdrop('Midnight Mantle'), recent: false },
{ xtreamId: 62000, type: 'movie', backdropUrl: backdrop('Black Harbor'), recent: true },
{ xtreamId: 62001, type: 'movie', backdropUrl: backdrop('The Paper Astronaut'), recent: true },
{ xtreamId: 62002, type: 'movie', backdropUrl: backdrop('Summer Static'), recent: false },
{ xtreamId: 72000, type: 'series', backdropUrl: backdrop('Skyline Sentinels'), recent: true },
{ xtreamId: 72001, type: 'series', backdropUrl: backdrop('The Aegis Club'), recent: false },
] as const;
+4 -1
View File
@@ -108,7 +108,10 @@
"@iptvnator/workspace/dashboard/data-access": [
"libs/workspace/dashboard/data-access/src/index.ts"
],
"@package": ["./package.json"]
"@package": ["./package.json"],
"@iptvnator/shared/marketing-fixtures": [
"./libs/shared/marketing-fixtures/src/index.ts"
]
}
},
"exclude": ["node_modules", "tmp"],