Proyecto App Windows
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# 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`
|
||||
|
||||
```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 |
|
||||
| `<any other>` | 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`
|
||||
Reference in New Issue
Block a user