11 KiB
Xtream Mock Server — Architecture
Purpose
apps/xtream-mock-server is a self-contained Express server that emulates the
Xtream Codes API protocol. It is used for:
- Local development — run a full portal without a real Xtream subscription
- E2E testing — Playwright spins it up alongside the Angular dev server
Data Pipeline
credentials (username + password)
│
▼
credentialsToSeed(u, p) ←── deterministic polynomial hash
│
▼
faker.seed(seed) ←── all faker calls use same seed per credentials
│
▼
generateCategories() ←── live / vod / series categories
│
generateLiveStreams() ←── live TV stream list
scenario EPG fixture? ←── optional deterministic per-stream EPG override
generateVodStreams() ←── VOD movie list
generateSeriesItems() ←── series list
generateSeriesInfo() ←── nested seasons + episodes (pre-populated)
│
▼
PortalData (cached) ←── Map<"username:password", PortalData>
Re-requesting with the same credentials returns the exact same data until
POST /reset clears all caches.
File Structure
apps/xtream-mock-server/
├── project.json ← Nx targets: serve (port 3211), serve-with-watch
├── public/
│ └── marketing/ ← committed fictional release artwork PNGs
├── tsconfig.json
└── src/
├── main.ts ← Express app bootstrap, all routes wired up
└── app/
├── scenarios.ts ← Credential → ScenarioConfig mapping
├── data-store.ts ← Lazy cache, per-credentials generation
├── generators/
│ ├── categories.generator.ts
│ ├── live.generator.ts ← Live streams + EPG listings
│ ├── marketing.generator.ts ← Fictional release screenshot fixture
│ ├── vod.generator.ts ← VOD streams + VodDetails
│ └── series.generator.ts ← Series items + SeriesInfo
├── handlers/
│ ├── get-account-info.handler.ts
│ ├── get-categories.handler.ts ← live/vod/series categories
│ ├── get-full-epg.handler.ts ← full EPG + legacy typo alias
│ ├── get-streams.handler.ts ← live/vod/series stream lists
│ ├── get-vod-info.handler.ts
│ ├── get-series-info.handler.ts
│ └── get-short-epg.handler.ts
└── routes/
└── dispatch.ts ← Action → handler routing
API Protocol
Direct Xtream endpoint
GET /player_api.php?action=<action>&username=<u>&password=<p>[&...]
Response: raw JSON (no envelope). Matches the real Xtream Codes API format.
PWA proxy endpoint
IPTVnator's PWA routes Xtream calls through:
GET /xtream?url=<serverUrl>&action=<action>&username=<u>&password=<p>
Response: { payload: <data>, action: <action> }
This mirrors the backend proxy in apps/electron-backend so the same
Angular service code works in both environments.
Stream stub endpoints
GET /live/<username>/<password>/<streamId>.m3u8
GET /movie/<username>/<password>/<streamId>.<ext>
GET /series/<username>/<password>/<streamId>.<ext>
All redirect to a publicly available HLS test stream
(https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8).
Key Response Shapes
get_account_info
{
"user_info": {
"username": "user1",
"password": "pass1",
"status": "active",
"exp_date": "4102444799",
"is_trial": "0",
"active_cons": "1",
"max_connections": "2",
"allowed_output_formats": ["m3u8", "ts", "rtmp"]
},
"server_info": {
"url": "http://localhost:3211",
"port": "3211",
"timezone": "UTC",
"timestamp_now": 1234567890
}
}
get_live_categories / get_vod_categories / get_series_categories
[
{ "category_id": "101", "category_name": "News", "parent_id": 0 },
...
]
get_live_streams (sample item)
{
"num": 1,
"name": "Acme Corp TV",
"stream_type": "live",
"stream_id": 10000,
"stream_icon": "https://picsum.photos/seed/live-10000/100/100",
"epg_channel_id": "channel-10000.mock",
"category_id": "101",
"tv_archive": 0,
"tv_archive_duration": 0
}
get_short_epg (sample item)
{
"epg_listings": [
{
"id": "1000000",
"epg_id": "channel-10000.mock",
"title": "base64encodedTitle",
"description": "base64encodedDescription",
"start": "2024-01-01 12:00:00",
"end": "2024-01-01 12:30:00",
"start_timestamp": "1704110400",
"stop_timestamp": "1704112200"
}
]
}
Note: title and description are base64-encoded, matching the real Xtream API.
get_simple_data_table / get_simple_date_table
Both actions return the same full per-channel schedule shape:
{
"epg_listings": [
{
"id": "10000-current",
"epg_id": "channel-10000.mock",
"title": "base64encodedTitle",
"description": "base64encodedDescription",
"start": "2026-04-05 04:30:00",
"end": "2026-04-05 05:00:00",
"start_timestamp": "1775363400",
"stop_timestamp": "1775365200",
"channel_id": "channel-10000.mock"
}
]
}
The legacy get_simple_date_table typo alias exists because real Xtream panels
sometimes only respond to that misspelled action.
get_series_info (structure)
{
"seasons": [
{
"id": 3000100, "name": "Season 1", "season_number": 1,
"episode_count": 8, "air_date": "2022-05-14",
"cover": "https://picsum.photos/seed/season-30001-1/300/450"
}
],
"info": { "name": "...", "cover": "...", "plot": "...", "cast": "...", ... },
"episodes": {
"1": [
{
"id": "80001", "episode_num": 1, "title": "Series Name S1E1",
"season": 1, "container_extension": "mkv",
"info": { "duration_secs": 2400, "rating": 8.3, ... }
}
]
}
}
Scenarios
Key (username:password) |
Seed | Categories | Items/cat | Account status |
|---|---|---|---|---|
user1:pass1 |
1001 | 8 each | 40 | active |
large:large |
9999 | 20 each | 200 | active |
stress:stress |
7777 | 16 each | 120 | active |
series:series |
2002 | live:3, vod:4, series:15 | 30 | active |
minimal:minimal |
3003 | 2 each | 5 | active |
epg:epg |
6006 | live:2, vod:1, series:1 | 3 | active |
emptyvod:emptyvod |
7007 | 2 each | 5 | active |
marketing:marketing |
8020 | live:4, vod:4, series:4 | curated | active |
expired:expired |
4004 | 4 each | 10 | Expired |
inactive:inactive |
5005 | 4 each | 10 | Disabled |
<any other> |
hash | 6 each | 30 | active |
epg:epg fixture details
This scenario is reserved for Xtream EPG tests:
- live category
EPG Focuscontains deterministic channels such asTimezone News Timezone Newsserves a fixedget_short_epgwindow and a fullget_simple_data_tableschedule- the full schedule includes a program that spans a UTC midnight boundary and another program after midnight
- raw
start/endstrings are intentionally offset fromstart_timestamp/stop_timestamp
That deliberate mismatch lets Electron tests verify the renderer uses timestamp fields for sorting, current-program selection, progress bars, and local clock labels instead of trusting provider-local strings.
marketing:marketing fixture details
This scenario is reserved for release screenshots and marketing materials:
- VOD and series data use 30 curated fictional titles instead of faker-generated titles or real media metadata
- posters and backdrops are served from committed PNG files in
apps/xtream-mock-server/public/marketing/{poster,backdrop}/ tools/release/generate-marketing-artwork.tsgenerates the PNGs withgpt-image-2only whenOPENAI_API_KEYis present; it also writes the prompt/asset manifest atapps/xtream-mock-server/public/marketing/manifest.json- the manifest assigns distinct genre and visual-medium profiles per title, including documentary, noir, animated family, retro rescue drama, cyberpunk, anime-inspired space school, and workplace dramedy styles
- screenshot capture remains deterministic and offline because
tools/release/capture-v020-screenshots.tsconsumes the local mock server assets and never calls OpenAI - the SVG renderer in
marketing.generator.tsremains the fallback for missing assets, live logos, season covers, and episode thumbnails
Playwright Integration
Configuration (apps/web-e2e/playwright.config.ts)
The mock server is listed as a third webServer entry:
{
command: 'pnpm nx run xtream-mock-server:serve',
url: 'http://localhost:3211/health',
reuseExistingServer: !process.env['CI'],
cwd: workspaceRoot,
}
Request Interception
The Angular PWA calls localhost:3000/xtream?.... Playwright intercepts these:
await page.route('**/localhost:3000/xtream**', async (route) => {
const originalUrl = new URL(route.request().url());
const mockUrl = new URL('http://localhost:3211/xtream');
originalUrl.searchParams.forEach((v, k) => mockUrl.searchParams.set(k, v));
await route.continue({ url: mockUrl.toString() });
});
Extension Points
- Add new actions: Implement a handler function and add a
caseinroutes/dispatch.ts - Add new scenarios: Add an entry to
SCENARIOSinscenarios.ts - Add deterministic EPG fixtures: Extend
ScenarioConfig.epgFixtureand populateepgListingsByStreamIdindata-store.ts - Refresh release artwork: Run
pnpm release:artwork:dry-run, thenOPENAI_API_KEY=... pnpm release:artwork:generate, inspect the generated PNGs, and finish withpnpm release:artwork:validate - Adjust data volume: Change
itemsPerCategory,seasonsPerSeries, orepisodesPerSeasonper scenario - Custom stream URLs: Edit the HLS stub redirect in
main.ts