# 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=&username=&password=

[&...] ``` 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=&action=&username=&password=

``` Response: `{ payload: , 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///.m3u8 GET /movie///. GET /series///. ``` All redirect to a publicly available HLS test stream (`https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8`). --- ## Key Response Shapes ### `get_account_info` ```json { "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` ```json [ { "category_id": "101", "category_name": "News", "parent_id": 0 }, ... ] ``` ### `get_live_streams` (sample item) ```json { "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) ```json { "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: ```json { "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) ```json { "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 | | `` | hash | 6 each | 30 | active | ### `epg:epg` fixture details This scenario is reserved for Xtream EPG tests: - live category `EPG Focus` contains deterministic channels such as `Timezone News` - `Timezone News` serves a fixed `get_short_epg` window and a full `get_simple_data_table` schedule - the full schedule includes a program that spans a UTC midnight boundary and another program after midnight - raw `start` / `end` strings are intentionally offset from `start_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.ts` generates the PNGs with `gpt-image-2` only when `OPENAI_API_KEY` is present; it also writes the prompt/asset manifest at `apps/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.ts` consumes the local mock server assets and never calls OpenAI - the SVG renderer in `marketing.generator.ts` remains 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: ```typescript { 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: ```typescript 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 `case` in `routes/dispatch.ts` - **Add new scenarios**: Add an entry to `SCENARIOS` in `scenarios.ts` - **Add deterministic EPG fixtures**: Extend `ScenarioConfig.epgFixture` and populate `epgListingsByStreamId` in `data-store.ts` - **Refresh release artwork**: Run `pnpm release:artwork:dry-run`, then `OPENAI_API_KEY=... pnpm release:artwork:generate`, inspect the generated PNGs, and finish with `pnpm release:artwork:validate` - **Adjust data volume**: Change `itemsPerCategory`, `seasonsPerSeries`, or `episodesPerSeason` per scenario - **Custom stream URLs**: Edit the HLS stub redirect in `main.ts`