From 935ef3561853159ce0a7be302b43d06f112ca613 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 14 Feb 2026 15:48:52 +0100 Subject: [PATCH] docs: add architecture documentation for M3U playlist, remote control, EPG, and Stalker portal --- docs/architecture/m3u-playlist-module.md | 321 ++++++++++++++++++ docs/architecture/remote-control.md | 245 +++++++++++++ docs/architecture/stalker-epg.md | 212 ++++++++++++ docs/architecture/stalker-portal.md | 156 +++++++++ .../stalker-store-api-baseline.md | 126 +++++++ 5 files changed, 1060 insertions(+) create mode 100644 docs/architecture/m3u-playlist-module.md create mode 100644 docs/architecture/remote-control.md create mode 100644 docs/architecture/stalker-epg.md create mode 100644 docs/architecture/stalker-portal.md create mode 100644 docs/architecture/stalker-store-api-baseline.md diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md new file mode 100644 index 000000000..d677f704d --- /dev/null +++ b/docs/architecture/m3u-playlist-module.md @@ -0,0 +1,321 @@ +# M3U Playlist Module Architecture + +This document describes the M3U playlist module architecture, which handles traditional M3U/M3U8 playlists (as opposed to Xtream Codes or Stalker Portal). + +## Overview + +The M3U playlist module provides: +- Channel list display with virtual scrolling (90,000+ channels support) +- EPG (Electronic Program Guide) integration +- Favorites management with drag-and-drop reordering +- Channel grouping and search +- Video playback with multiple player backends + +## Module Structure + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ VIDEO PLAYER PAGE │ +│ apps/web/src/app/home/video-player/ │ +├─────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌──────────────────────┐ ┌────────────────────┐ │ +│ │ Sidebar │ │ Video Player │ │ EPG List │ │ +│ │ │ │ (ArtPlayer/Video.js)│ │ (Right drawer) │ │ +│ │ ┌─────────┐ │ │ │ │ │ │ +│ │ │Channel │ │ │ │ │ │ │ +│ │ │List │ │ │ │ │ │ │ +│ │ │Container│ │ │ │ │ │ │ +│ │ └─────────┘ │ │ │ │ │ │ +│ └─────────────┘ └──────────────────────┘ └────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ NgRx STORE (m3u-state) │ +│ libs/m3u-state/ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Playlist │ │ Channel │ │ EPG │ │Favorites │ │ Filter │ │ +│ │ Reducer │ │ Reducer │ │ Reducer │ │ Reducer │ │ Reducer │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## State Management (libs/m3u-state/) + +### State Structure + +```typescript +interface PlaylistState { + // Active channel being played + active: Channel | undefined; + + // All channels from current playlist + channels: Channel[]; + + // EPG state + epg: { + epgAvailable: boolean; + activeEpgProgram: EpgProgram | undefined; + currentEpgProgram: EpgProgram | undefined; + }; + + // Playlist metadata (entity adapter) + playlistsMeta: { + ids: string[]; + entities: Record; + selectedId: string | undefined; + allPlaylistsLoaded: boolean; + selectedFilters: PlaylistSourceFilter[]; + }; +} +``` + +### Actions + +| Action Group | Actions | Purpose | +|--------------|---------|---------| +| **PlaylistActions** | `loadPlaylists`, `addPlaylist`, `removePlaylist`, `parsePlaylist`, `setActivePlaylist` | Playlist CRUD | +| **ChannelActions** | `setChannels`, `setActiveChannel`, `setAdjacentChannelAsActive` | Channel selection & navigation | +| **EpgActions** | `setActiveEpgProgram`, `setCurrentEpgProgram`, `setEpgAvailableFlag` | EPG state | +| **FavoritesActions** | `updateFavorites`, `setFavorites` | Favorites management | +| **FilterActions** | `setSelectedFilters` | Playlist type filtering | + +### Key Selectors + +```typescript +// Channel selectors +selectActive // Current playing channel +selectChannels // All channels array +selectFavorites // Favorite channel URLs + +// Playlist selectors +selectAllPlaylistsMeta // All playlists +selectActivePlaylistId // Selected playlist ID +selectCurrentPlaylist // Active playlist object +selectPlaylistTitle // Title with "Global favorites" fallback + +// EPG selectors +selectIsEpgAvailable // EPG data available flag +selectCurrentEpgProgram // Current playing program +``` + +## Channel List Container + +**Location**: `libs/ui/components/src/lib/channel-list-container/` + +### Component Architecture + +``` +channel-list-container/ +├── channel-list-container.component.ts # Parent - shared state coordinator +├── channel-list-container.component.html +├── channel-list-container.component.scss +│ +├── all-channels-tab/ # Virtual scroll + search +│ ├── all-channels-tab.component.ts +│ ├── all-channels-tab.component.html +│ └── all-channels-tab.component.scss +│ +├── groups-tab/ # Expansion panels + infinite scroll +│ ├── groups-tab.component.ts +│ ├── groups-tab.component.html +│ └── groups-tab.component.scss +│ +├── favorites-tab/ # Drag-drop reordering +│ ├── favorites-tab.component.ts +│ ├── favorites-tab.component.html +│ └── favorites-tab.component.scss +│ +└── channel-list-item/ # Individual channel display + ├── channel-list-item.component.ts + ├── channel-list-item.component.html + └── channel-list-item.component.scss +``` + +### Data Flow + +``` +┌──────────────────────────────────────────────────────────────┐ +│ ChannelListContainerComponent │ +│ (Parent) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Shared State (Signals): │ │ +│ │ - channelEpgMap: Map │ │ +│ │ - progressTick: number (30s interval) │ │ +│ │ - shouldShowEpg: boolean │ │ +│ │ - favoriteIds: Set │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────┼─────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ All │ │ Groups │ │ Favorites │ │ +│ │Channels │ │ Tab │ │ Tab │ │ +│ │ Tab │ │ │ │ │ │ +│ └────┬────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ +│ └───────────────────┴─────────────────────┘ │ +│ │ │ +│ ▼ │ +│ (channelSelected) output │ +│ │ │ +└──────────────────────────┼───────────────────────────────────┘ + ▼ + Store Dispatch + ChannelActions.setActiveChannel +``` + +### EnrichedChannel Pattern + +For performance optimization, channels are pre-enriched with EPG data: + +```typescript +interface EnrichedChannel extends Channel { + epgProgram: EpgProgram | null | undefined; + progressPercentage: number; // Pre-computed by parent +} +``` + +### Performance Optimizations + +| Optimization | Implementation | +|--------------|----------------| +| **Virtual Scroll** | CDK virtual scroll for 90,000+ channels | +| **Computed Signals** | `enrichedChannels` computed signal replaces template pipe | +| **Debounced Search** | 300ms debounce on search input | +| **Global Progress Tick** | Single 30s interval instead of per-item intervals | +| **OnPush Change Detection** | All components use OnPush | +| **Infinite Scroll in Groups** | IntersectionObserver loads 50 channels at a time | +| **Memoized Group Enrichment** | `enrichedGroupChannelsMap` computed signal | + +### Tab Components + +#### AllChannelsTabComponent +- **Inputs**: `channels`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds` +- **Outputs**: `channelSelected`, `favoriteToggled` +- **Features**: Search with 300ms debounce, virtual scrolling, no-results placeholder + +#### GroupsTabComponent +- **Inputs**: Same as AllChannelsTab + `groupedChannels` +- **Outputs**: `channelSelected`, `favoriteToggled` +- **Features**: Expansion panels, infinite scroll with IntersectionObserver, lazy loading + +#### FavoritesTabComponent +- **Inputs**: `favorites`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl` +- **Outputs**: `channelSelected`, `favoriteToggled`, `favoritesReordered` +- **Features**: Drag-and-drop reordering with CDK DragDrop + +## EPG Integration + +### EpgService (libs/services/) + +```typescript +class EpgService { + // Fetch EPG for multiple URLs + fetchEpg(urls: string[]): void; + + // Get programs for a channel + getChannelPrograms(channelId: string): void; + + // Batch fetch current programs + getCurrentProgramsForChannels(channelIds: string[]): Observable>; + + // Observables + epgAvailable$: Observable; + currentEpgPrograms$: Observable; +} +``` + +### EPG Components + +| Component | Purpose | +|-----------|---------| +| `EpgListComponent` | Timeline view for single channel | +| `EpgListItemComponent` | Individual program in timeline | +| `EpgItemDescriptionComponent` | Program details dialog | +| `MultiEpgContainerComponent` | Grid view of all channels' schedules | + +## Video Player + +**Location**: `apps/web/src/app/home/video-player/` + +### Supported Players +- **ArtPlayer** (default) - Modern player with plugins +- **Video.js** - Fallback with HLS support +- **HTML5** - Basic video element +- **Audio** - For radio streams + +### Player Features +- Channel navigation (prev/next) +- Favorites toggle +- EPG sidebar +- Multi-EPG modal view +- Channel info overlay +- External player support (MPV, VLC) in Electron + +## Interfaces + +### Channel Interface +```typescript +interface Channel { + id: string; + url: string; + name: string; + group: { title: string }; + tvg: { + id: string; // For EPG matching + name: string; + url: string; + logo: string; + rec: string; + }; + epgParams?: string; + timeshift?: string; + catchup?: { type?: string; source?: string; days?: string }; + http: { + referrer: string; + 'user-agent': string; + origin: string; + }; +} +``` + +### EpgProgram Interface +```typescript +interface EpgProgram { + start: string; // ISO string + stop: string; // ISO string + channel: string; // TVG ID + title: string; + desc: string | null; + category: string | null; + episodeNum?: string | null; + iconUrl?: string | null; + rating?: string | null; +} +``` + +## Routes + +``` +/playlists/:id # Video player with playlist +/iptv # Default IPTV route +``` + +## Adding New Features + +### To add a new tab to channel list: +1. Create component in `channel-list-container/new-tab/` +2. Accept inputs: `channels`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl` +3. Emit `channelSelected` output +4. Add to parent template and imports + +### To add EPG-related features: +1. Use `EpgService` for data fetching +2. Subscribe to `channelEpgMap` signal for current programs +3. Dispatch `EpgActions` for state updates + +### To modify favorites behavior: +1. Dispatch `FavoritesActions.updateFavorites` for toggle +2. Dispatch `FavoritesActions.setFavorites` for reordering +3. Effects automatically persist to database diff --git a/docs/architecture/remote-control.md b/docs/architecture/remote-control.md new file mode 100644 index 000000000..49b6ac0d5 --- /dev/null +++ b/docs/architecture/remote-control.md @@ -0,0 +1,245 @@ +# Remote Control Architecture + +This document describes the current remote control implementation in IPTVnator, including: + +- HTTP API exposed by Electron main process +- IPC bridge between Electron main and Angular renderer +- Feature support and integration points for M3U, Xtream, and Stalker +- Remote web UI structure and behavior + +Related architecture docs: + +- [Stalker Portal Architecture](./stalker-portal.md) +- [Stalker Portal EPG Architecture](./stalker-epg.md) + +## Scope + +Remote control is a desktop-only feature that serves a mobile-friendly web app from the Electron backend and routes remote actions into the running renderer. + +Current capabilities: + +- Channel up / down +- Channel select by number +- Volume commands (implemented in command layer; active support currently in M3U flow) +- Playback status polling (portal, live-state, channel name/number, EPG now, volume capability) + +## High-Level Flow + +1. User opens remote web UI (`http://:`). +2. Remote web app calls `/api/remote-control/*`. +3. Electron main handles API request and sends IPC to renderer: + - `CHANNEL_CHANGE` for up/down + - `REMOTE_CONTROL_COMMAND` for numeric/volume commands +4. Renderer-specific feature module (M3U/Xtream/Stalker) applies action. +5. Renderer pushes status snapshots back to main via: + - `REMOTE_CONTROL_STATUS_UPDATE` +6. Remote web app polls `/api/remote-control/status` and updates UI. + +## Backend (Electron Main) + +### HTTP server and static app hosting + +- File: `apps/electron-backend/src/app/server/http-server.ts` +- Responsibilities: + - Serves static remote app from: + - dev: `dist/apps/remote-control-web/browser` + - prod: `/remote-control-web/browser` + - Routes `/api/remote-control/*` to registered handlers. + - Starts/stops/restarts on settings updates. + +### Remote control event module + +- File: `apps/electron-backend/src/app/events/remote-control.events.ts` +- Bootstrapped in: `apps/electron-backend/src/main.ts` via `RemoteControlEvents.bootstrapRemoteControlEvents()` + +Registered endpoints: + +- `POST /api/remote-control/channel/up` +- `POST /api/remote-control/channel/down` +- `POST /api/remote-control/channel/select-number` with `{ number: }` +- `POST /api/remote-control/volume/up` +- `POST /api/remote-control/volume/down` +- `POST /api/remote-control/volume/toggle-mute` +- `GET /api/remote-control/status` + +IPC emitted to renderer: + +- `CHANNEL_CHANGE` payload: `{ direction: 'up' | 'down' }` +- `REMOTE_CONTROL_COMMAND` payload: + - `{ type: 'channel-select-number', number }` + - `{ type: 'volume-up' | 'volume-down' | 'volume-toggle-mute' }` + +Status ingestion from renderer: + +- Listens on `REMOTE_CONTROL_STATUS_UPDATE` +- Maintains in-memory `RemoteControlStatus` object returned by `/status` + +### Settings integration + +- Main handler: `apps/electron-backend/src/app/events/settings.events.ts` +- On `SETTINGS_UPDATE`, reads `remoteControl` and `remoteControlPort`, persists to store, and calls: + - `httpServer.updateSettings(enabled, port)` + +## Preload Bridge + +- File: `apps/electron-backend/src/app/api/main.preload.ts` + +Exposed APIs relevant to remote control: + +- `onChannelChange(callback) => unsubscribe` +- `onRemoteControlCommand(callback) => unsubscribe` +- `updateRemoteControlStatus(status) => void` + +Type definitions: + +- `apps/web/src/typings.d.ts` +- `global.d.ts` + +## Renderer Integrations + +## Shared helpers + +- File: `apps/web/src/app/shared/services/remote-channel-navigation.util.ts` + +Functions: + +- `getAdjacentChannelItem(...)`: wraps around on boundaries for up/down +- `getChannelItemByNumber(...)`: 1-based number to list item mapping + +Used by M3U, Xtream, and Stalker live integrations. + +## M3U integration + +- File: `apps/web/src/app/home/video-player/video-player.component.ts` + +Implemented behavior: + +- Subscribes to: + - `onChannelChange` (up/down) + - `onRemoteControlCommand` (number + volume) +- Applies channel up/down by active channel URL over `channels$` +- Applies number select through existing `switchToChannelByNumber(...)` +- Applies volume commands: + - up/down in 0.1 increments + - toggle mute with last non-zero volume restore + - persists to `localStorage` +- Publishes status snapshots via `updateRemoteControlStatus(...)`: + - `portal: 'm3u'` + - `isLiveView: true` + - channel name/number + - EPG now fields + - `supportsVolume: true`, `volume`, `muted` +- Cleans listeners/subscriptions in `ngOnDestroy`. + +## Xtream integration (live view) + +- File: `apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts` + +Implemented behavior: + +- Subscribes to: + - `onChannelChange` for up/down + - `onRemoteControlCommand` for number select +- Up/down: + - Uses selected live item `selectedItem().xtream_id` + - Navigates inside `selectItemsFromSelectedCategory()` + - Calls `playLive(nextItem)` +- Number select: + - Maps number to item in current category list + - Calls `playLive(channel)` +- Publishes status via effect: + - `portal: 'xtream'` + - `isLiveView` only when selected content type is `live` and item is selected + - channel name/number + current EPG item + - `supportsVolume: false` +- Cleans listeners in `ngOnDestroy`. + +## Stalker integration (ITV live view) + +- File: `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` + +Implemented behavior: + +- Subscribes to: + - `onChannelChange` for up/down + - `onRemoteControlCommand` for number select +- Up/down: + - Uses `selectedItem().id` + - Navigates inside `itvChannels()` + - Calls `playChannel(nextItem)` +- Number select: + - Maps number into `itvChannels()` + - Calls `playChannel(channel)` +- Publishes status via effect: + - `portal: 'stalker'` + - `isLiveView` only for selected content type `itv` with active item + - channel name/number + current EPG item + - `supportsVolume: false` +- Cleans listeners in `ngOnDestroy`. + +## Remote Web App + +### App shell + +- App: `apps/remote-control-web/src/app/app.ts` +- Template: `apps/remote-control-web/src/app/app.html` +- Style: `apps/remote-control-web/src/app/app.scss` +- Renders shared library component: `` + +### Shared remote UI library + +- Component: + - `libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts` + - `libs/ui/remote-control/src/lib/remote-control/remote-control.component.html` + - `libs/ui/remote-control/src/lib/remote-control/remote-control.component.scss` +- Service: + - `libs/ui/remote-control/src/lib/remote-control/remote-control.service.ts` + +Implemented UI behavior: + +- Channel pad (`CH+`, `CH-`) +- Numeric keypad (`0-9`, `DEL`, `CLR`, `OK`) +- Volume controls (`VOL-`, `MUTE/UNMUTE`, `VOL+`) +- Status card (portal, channel name/number, current program) +- Polls `/status` every 2s +- Uses action wrapper to refresh status after command execution + +## Settings UI and discoverability + +- Files: + - `apps/web/src/app/settings/settings.component.ts` + - `apps/web/src/app/settings/settings.component.html` +- Features: + - Toggle `remoteControl` + - Configure `remoteControlPort` + - Display local URLs and QR codes for remote access + - Local IP list loaded via `getLocalIpAddresses()` + +## Feature Matrix (Current) + +| Capability | M3U | Xtream Live | Stalker ITV | +|---|---|---|---| +| Channel up/down | Yes | Yes | Yes | +| Number select | Yes | Yes | Yes | +| Status publish | Yes | Yes | Yes | +| Volume command handling | Yes | No | No | +| `supportsVolume` in status | true | false | false | + +## Known limitations + +- Volume commands are currently no-op in Xtream and Stalker integrations. +- Remote status uses polling from web UI (2s), not push/WebSocket. +- Number-based selection is list-position based (1-based index in active list scope), not global EPG number mapping. +- Remote API currently has no auth/TLS; intended for trusted local networks. + +## Operational notes + +- UI updates in remote web app require rebuilding `remote-control-web` so Electron serves fresh `dist` assets. +- If stale UI appears, clear browser cache/hard-refresh mobile browser. + +## Future extension points + +- Add optional auth token for `/api/remote-control/*` endpoints. +- Add WebSocket/SSE status push for lower latency and reduced polling. +- Add cross-portal volume abstraction and capability negotiation. +- Add last-channel, favorites navigation, and search/select commands. diff --git a/docs/architecture/stalker-epg.md b/docs/architecture/stalker-epg.md new file mode 100644 index 000000000..bf25532a5 --- /dev/null +++ b/docs/architecture/stalker-epg.md @@ -0,0 +1,212 @@ +# Stalker Portal EPG Architecture + +This document describes the EPG (Electronic Program Guide) implementation for Stalker/Ministra portal live TV (ITV) streams. + +Related architecture docs: + +- [Stalker Portal Architecture](./stalker-portal.md) +- [Remote Control Architecture](./remote-control.md) + +## Overview + +The Stalker ITV live stream layout displays EPG data in the right panel when a live channel is playing. EPG is fetched per-channel using the Stalker `get_short_epg` API action, which returns the current program plus the next ~5 upcoming programs. The response is mapped to the shared `EpgItem` interface and rendered by the reusable `EpgViewComponent`. + +## Architecture + +``` +┌───────────────────────────────────────────────────────────────────────────┐ +│ StalkerLiveStreamLayoutComponent │ +│ apps/web/src/app/stalker/stalker-live-stream-layout/ │ +│ │ +│ ┌──────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │ +│ │ Sidebar │ │ Video Player │ │ EPG Panel │ │ +│ │ (channels) │ │ (WebPlayerView) │ │ (EpgViewComponent) │ │ +│ │ │ │ │ │ │ │ +│ │ click ──────┼────┼──► playChannel() ────┼────┼──► loadEpgFor...() │ │ +│ └──────────────┘ └──────────────────────┘ └─────────────────────┘ │ +│ │ │ │ +└──────────────────────────────┼────────────────────────────┼───────────────┘ + │ │ + ▼ ▼ + ┌──────────────────────┐ ┌────────────────────────┐ + │ StalkerStore │ │ StalkerStore │ + │ fetchLinkToPlay() │ │ fetchChannelEpg() │ + └──────────┬───────────┘ └────────────┬───────────┘ + │ │ + ▼ ▼ + ┌──────────────────────────────────────────────────────┐ + │ Stalker Portal API │ + │ action=create_link action=get_short_epg │ + └──────────────────────────────────────────────────────┘ +``` + +## Stalker EPG API + +### `get_short_epg` (per-channel) + +**Request:** +``` +GET load.php?type=itv&action=get_short_epg&ch_id={channel_id}&JsHttpRequest=1-xml +``` +Uses standard Stalker auth headers (Bearer token + MAC cookie). + +**Response:** +```json +{ + "js": { + "data": [ + { + "id": "123", + "ch_id": "45", + "name": "Program Title", + "descr": "Program description", + "time": "2025-01-15 14:00:00", + "time_to": "2025-01-15 14:30:00", + "duration": "1800", + "start_timestamp": "1736949600", + "stop_timestamp": "1736951400", + "t_time": "14:00", + "t_time_to": "14:30" + } + ] + } +} +``` + +**Notes:** +- If the channel has no `xmltv_id` set on the server, returns an empty array +- Response normalization handles both `{ js: { data: [...] } }` and `{ js: [...] }` formats +- No backend (Electron IPC) changes needed — the generic `stalker.events.ts` handler forwards any `params` to the portal URL + +### `get_epg_info` (bulk — reserved for future use) + +``` +GET load.php?type=itv&action=get_epg_info&period={hours}&JsHttpRequest=1-xml +``` + +Returns EPG for all channels for a given time period. Currently unused but the enum value `StalkerPortalActions.GetEpgInfo` is defined for future bulk EPG features. + +## Data Mapping + +### Stalker EPG → `EpgItem` Interface + +| Stalker field | `EpgItem` field | Notes | +|--------------------|--------------------|-------| +| `id` | `id` | Converted to string | +| `ch_id` | `channel_id` | Falls back to passed `channelId` | +| `name` | `title` | | +| `descr` | `description` | | +| `time` | `start` | Full datetime string | +| `time_to` | `end`, `stop` | Both set to same value | +| `start_timestamp` | `start_timestamp` | Unix timestamp as string | +| `stop_timestamp` | `stop_timestamp` | Unix timestamp as string | +| _(n/a)_ | `epg_id` | Empty string | +| _(n/a)_ | `lang` | Empty string | + +### `EpgItem` Interface + +```typescript +// libs/shared/interfaces/src/lib/epg-item.interface.ts +interface EpgItem { + id: string; + epg_id: string; + title: string; + lang: string; + start: string; + end: string; + stop: string; + description: string; + channel_id: string; + start_timestamp: string; + stop_timestamp: string; +} +``` + +## Implementation Details + +### Key Files + +| File | Purpose | +|------|---------| +| `libs/shared/interfaces/src/lib/stalker-portal-actions.enum.ts` | `GetShortEpg`, `GetEpgInfo` enum values | +| `apps/web/src/app/stalker/stalker.store.ts` | `fetchChannelEpg()` method | +| `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | EPG signals + `loadEpgForChannel()` | +| `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | `` integration | +| `libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts` | Shared EPG display component | + +### StalkerStore.fetchChannelEpg() + +Location: `apps/web/src/app/stalker/stalker.store.ts` (in `withMethods`) + +```typescript +async fetchChannelEpg(channelId: number | string): Promise +``` + +- Sends `get_short_epg` request with `ch_id` param +- Supports both full Stalker portals (authenticated via `StalkerSessionService`) and simple portals (direct IPC) +- Returns mapped `EpgItem[]` or empty array on failure +- No store state mutation — returns data directly to the component + +### StalkerLiveStreamLayoutComponent EPG Integration + +The component manages EPG state locally with signals: + +```typescript +readonly epgItems = signal([]); +readonly isLoadingEpg = signal(false); +``` + +**Flow:** +1. User clicks a channel in the sidebar → `playChannel(item)` +2. `fetchLinkToPlay()` gets the stream URL +3. If using embedded player, `streamUrl` is set and `loadEpgForChannel(item.id)` is called +4. `loadEpgForChannel()` sets loading state, calls `stalkerStore.fetchChannelEpg()`, updates `epgItems` +5. Template renders `` or a loading spinner + +### EpgViewComponent (shared) + +Location: `libs/ui/shared-portals/src/lib/epg-view/` + +Reusable component shared between Stalker and Xtream live stream layouts: +- **Input:** `epgItems: EpgItem[]` +- Displays program list with time, title, and info button +- Highlights current program with green progress bar +- Handles empty state (shows "EPG not available" message) +- Info button opens `EpgItemDescriptionComponent` dialog with title and description + +**Current program detection:** +```typescript +isCurrentProgram(item: EpgItem): boolean { + const now = new Date().getTime(); + const start = new Date(item.start).getTime(); + const stop = new Date(item.stop ?? item.end).getTime(); + return now >= start && now <= stop; +} +``` + +This works with Stalker's datetime format (`"2025-01-15 14:00:00"`) because `new Date()` parses it correctly. + +## Authentication + +EPG requests follow the same authentication pattern as all Stalker API calls: + +| Portal Type | Auth Method | +|-------------|-------------| +| **Full Stalker** (`isFullStalkerPortal: true`) | `StalkerSessionService.makeAuthenticatedRequest()` — handles token refresh and retry on 401 | +| **Simple Stalker** | Direct IPC via `DataService.sendIpcEvent(STALKER_REQUEST, ...)` — no auth headers | + +## Future Enhancements + +### Bulk EPG in Channel List Sidebar + +Use `get_epg_info` with `period=3` to pre-fetch current program titles for all channels when a category is selected: +- Call `get_epg_info` after category selection +- Build a `Map` +- Show current program name below channel title in the sidebar +- Display progress bar per channel item + +This is a separate task due to Stalker's lazy-loaded channel pagination model. + +### EPG Auto-Refresh + +Currently EPG is fetched once per channel selection. A future enhancement could add a timer to refresh EPG data periodically (e.g., every 5 minutes) to keep the "current program" indicator accurate during long viewing sessions. diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md new file mode 100644 index 000000000..748f9eeb6 --- /dev/null +++ b/docs/architecture/stalker-portal.md @@ -0,0 +1,156 @@ +# Stalker Portal Architecture + +This document describes the Stalker portal implementation in IPTVnator and where each feature is integrated. + +## Related Docs + +- [Stalker Portal EPG Architecture](./stalker-epg.md) +- [Remote Control Architecture](./remote-control.md) +- [Download Manager](./download-manager.md) +- [Category Management](./category-management.md) +- [Stalker Store API Baseline](./stalker-store-api-baseline.md) + +## Scope + +Stalker support covers: + +- Live TV (`itv`) +- VOD (`vod`) +- Series (`series`) +- VOD-as-series flows (`is_series=1` and embedded `series[]`) +- Favorites and recently viewed collections +- Search +- External player playback (shared Xtream player infrastructure) +- Remote control for live ITV navigation + +## Routing Structure + +Primary route tree lives in `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker.routes.ts`. + +- `/stalker/:id/vod` +- `/stalker/:id/series` +- `/stalker/:id/itv` +- `/stalker/:id/favorites` +- `/stalker/:id/recent` +- `/stalker/:id/search` +- `/stalker/:id/downloads` (shared downloads module from Xtream UI) + +## Runtime Architecture + +1. Angular Stalker screens call methods/resources in `StalkerStore`. +2. `StalkerStore` builds request params based on selected content type and current view state. +3. Requests go through `DataService.sendIpcEvent(STALKER_REQUEST, ...)` or `StalkerSessionService` (full portal auth). +4. Electron main process handles `STALKER_REQUEST` in `/Users/4gray/Code/iptvnator/apps/electron-backend/src/app/events/stalker.events.ts`. +5. Axios calls Stalker `load.php` API with required headers/cookies and returns normalized payloads to renderer. + +## Main UI Components + +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-main-container.component.ts` + - Category + content layout for `vod` and `series` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` + - ITV live playback, channel navigation, EPG panel integration +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts` + - Season/episode UI for all Stalker series modes +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/recently-viewed/recently-viewed.component.ts` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts` + +## Store and Data Flow + +Stalker store is now feature-composed: + +- Facade: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker.store.ts` +- Feature slices: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stores/features/*` +- Shared store utils: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stores/utils/*` + +Important store responsibilities: + +- Selected content/category/item state +- Category and paginated content resources +- ITV channel list + pagination +- Regular series seasons resource +- VOD-series (`is_series=1`) seasons + episodes resources +- Playback link creation (`create_link` flow) +- Favorites and recently viewed persistence helpers + +## VOD/Series Modes + +Stalker has multiple real-world data shapes. The current implementation supports all three: + +1. Regular Series (`/series`): +- Seasons come from API resource (`serialSeasonsResource`). +- Episodes are derived from season payload. + +2. VOD with Embedded `series[]`: +- Item is opened under VOD, but already contains episodes. +- `StalkerSeriesViewComponent` creates a pseudo-season and renders episodes directly. + +3. VOD with `is_series=1` (Ministra plugin behavior): +- Treated as series flow from VOD context. +- Seasons are fetched lazily. +- Episodes are fetched on season select. +- Uses unique generated tracking IDs for episode playback position compatibility. + +Core decision logic and normalization are centralized in: + +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-vod.utils.ts` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/models/*.ts` + +## Favorites and Recently Viewed + +Current implementation is shared via Stalker-specific helpers: + +- `createPortalCollectionResource(...)` generic collection loader +- `createPortalFavoritesResource(...)` favorites wrapper +- `createStalkerDetailViewState(...)` unified "open detail" decision +- `toggleStalkerVodFavorite(...)` shared add/remove behavior +- `normalizeStalkerEntityId(...)` and `normalizeStalkerEntityIdAsNumber(...)` for stable ID matching +- `matchesFavoriteById(...)` for cross-shape favorite matching + +Where this is used: + +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/recently-viewed/recently-viewed.component.ts` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts` +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts` + +## Remote Control Integration + +Stalker live remote control is implemented in: + +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` + +Supported today: + +- Channel up/down +- Numeric channel selection (list-position based) +- Status publish for remote UI (portal/channel/current program) + +See full backend and web-remote flow in [Remote Control Architecture](./remote-control.md). + +## EPG Integration + +EPG in Stalker ITV uses `get_short_epg` and shared `EpgViewComponent`. Full details are documented in [Stalker Portal EPG Architecture](./stalker-epg.md). + +## Shared/Reusable Infrastructure + +Stalker reuses some Xtream UI infrastructure deliberately: + +- Category content rendering route uses Xtream category content component +- Season container for episodes uses shared Xtream season UI component +- Playback position handling for series episodes reuses Xtream store position mechanisms +- Downloads route reuses shared downloads feature + +This reduces duplicate UI logic across portal types and keeps compatibility behavior aligned. + +## Regression Coverage + +Focused regression tests for Stalker VOD mode branching live in: + +- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-vod.utils.spec.ts` + +Covered scenarios include: + +- Embedded `series[]` opens series view state +- `is_series=1` opens lazy series state +- Favorite toggle helper path invokes the expected add/remove flow diff --git a/docs/architecture/stalker-store-api-baseline.md b/docs/architecture/stalker-store-api-baseline.md new file mode 100644 index 000000000..361b57bbc --- /dev/null +++ b/docs/architecture/stalker-store-api-baseline.md @@ -0,0 +1,126 @@ +# Stalker Store API Baseline + +This is the compatibility baseline for refactoring `apps/web/src/app/stalker/stalker.store.ts`. + +Goal: keep this public surface stable while splitting to feature stores. + +## Source of Truth + +- Store implementation: `apps/web/src/app/stalker/stalker.store.ts` +- Baseline created on current branch state before feature-store extraction. + +## Public State Signals + +Direct signal properties currently exposed by `signalStore`: + +- `selectedContentType: 'vod' | 'itv' | 'series'` +- `selectedCategoryId: string | null | undefined` +- `selectedVodId: string | undefined` +- `selectedSerialId: string | undefined` +- `selectedItvId: string | undefined` +- `limit: number` +- `page: number` +- `searchPhrase: string` +- `currentPlaylist: PlaylistMeta` +- `totalCount: number` +- `selectedItem: StalkerVodSource | null | undefined` +- `vodCategories: StalkerCategoryItem[]` +- `seriesCategories: StalkerCategoryItem[]` +- `itvCategories: StalkerCategoryItem[]` +- `hasMoreChannels: boolean` +- `itvChannels: StalkerItvChannel[]` +- `vodSeriesSeasons: StalkerVodSeriesSeason[]` +- `vodSeriesEpisodes: StalkerVodSeriesEpisode[]` +- `selectedVodSeriesSeasonId: string | undefined` + +## Public Computed Selectors + +- `getTotalPages: number` +- `getPaginatedContent: StalkerContentItem[] | undefined` +- `isPaginatedContentLoading: boolean` +- `isPaginatedContentFailed: unknown` +- `getSerialSeasonsResource: StalkerSeason[]` +- `isSerialSeasonsLoading: boolean` +- `getVodSeriesSeasonsResource: StalkerVodSeriesSeason[]` +- `isVodSeriesSeasonsLoading: boolean` +- `getCategoryResource: StalkerCategoryItem[]` +- `isCategoryResourceLoading: boolean` +- `isCategoryResourceFailed: unknown` +- `getSelectedCategoryName: string` + +## Exposed Resources/Props + +These are currently reachable on the store object and used internally by computed selectors: + +- `getCategoryResource` (resource) +- `getContentResource` (resource) +- `serialSeasonsResource` (resource) +- `vodSeriesSeasonsResource` (resource) +- `makeStalkerRequest(...)` + +During refactor: +- Keep compatibility for external callers that may read these directly. +- If moved/renamed internally, provide facade aliases. + +## Public Methods (Compatibility Contract) + +- `setSelectedContentType(type: 'vod' | 'itv' | 'series'): void` +- `setSelectedCategory(id: string | number | null): void` +- `setSelectedSerialId(id: string): void` +- `setSelectedVodId(id: string): void` +- `setSelectedItvId(id: string): void` +- `setLimit(limit: number): void` +- `setPage(page: number): void` +- `setCurrentPlaylist(playlist: PlaylistMeta | undefined): Promise` +- `setSelectedItem(selectedItem: StalkerVodSource | null | undefined): void` +- `clearSelectedItem(): void` +- `setCategories(type: 'vod' | 'series' | 'itv', categories: StalkerCategoryItem[]): void` +- `resetCategories(): void` +- `setItvChannels(channels: StalkerItvChannel[]): void` +- `setSearchPhrase(phrase: string): void` +- `fetchVodSeriesEpisodes(videoId: string, seasonId: string): Promise` +- `getSelectedCategory(): { id: string | number; name: string; type: 'vod' | 'itv' | 'series' }` +- `fetchLinkToPlay(portalUrl: string, macAddress: string, cmd: string, series?: number): Promise` +- `getExpireDate(): Promise` +- `addToFavorites(item: any, onDone?: () => void): void` +- `removeFromFavorites(favoriteId: string, onDone?: () => void): void` +- `fetchMovieFileId(movieId: string): Promise` +- `createLinkToPlayVod(cmd?: string, title?: string, thumbnail?: string, episodeNum?: number, episodeId?: number, startTime?: number): Promise` +- `addToRecentlyViewed(item: any): void` +- `removeFromRecentlyViewed(itemId: number, onComplete?: () => void): void` +- `fetchChannelEpg(channelId: number | string, size?: number): Promise` + +## Current Consumers (Observed) + +Top observed store API usage in app code: + +- `currentPlaylist` (16 references) +- `setSelectedContentType` (9) +- `selectedItem` (9) +- `setSelectedItem` (7) +- `setSelectedCategory` (6) +- `createLinkToPlayVod` (6) +- `removeFromFavorites` (5) +- `setPage` (4) +- `addToFavorites` (4) +- `fetchChannelEpg` (3) +- plus lower-frequency calls for paging/resources/series/recent. + +Consumer directories sampled: + +- `apps/web/src/app/stalker/**` +- `apps/web/src/app/xtream-tauri/**` +- `apps/web/src/app/shared/**` + +## Invariants to Preserve During Refactor + +- Selection IDs (`selectedVodId`, `selectedSerialId`, `selectedItvId`) are synchronized in `setSelectedItem`. +- `setSelectedCategory(...)` resets `page` to `0`. +- `createLinkToPlayVod(...)` continues to: + - support episode playback metadata + - append recently viewed + - preserve external player payload shape +- Full-portal auth path continues through `StalkerSessionService`. +- Non-auth/simple path continues through `DataService.sendIpcEvent(STALKER_REQUEST, ...)`. +- Resource-driven loading signals preserve existing names. +