Files
KiraTV---App-Linux/docs/architecture/m3u-playlist-module.md
T
2026-05-03 23:43:09 +02:00

425 lines
19 KiB
Markdown

# 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
- Per-playlist group visibility management in the groups view
- Video playback with multiple player backends
## Module Structure
```
┌─────────────────────────────────────────────────────────────────────┐
│ VIDEO PLAYER PAGE │
│ libs/playlist/m3u/feature-player/src/lib/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;
// Whether the current route is still resolving channel data
channelsLoading: boolean;
// 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<string, PlaylistMeta>;
selectedId: string | undefined;
allPlaylistsLoaded: boolean;
selectedFilters: PlaylistSourceFilter[];
};
}
```
`PlaylistMeta` is the persisted playlist-facing subset of the playlist entity.
For M3U playlists it now also carries `hiddenGroupTitles?: string[]`, which is
used by the groups view to remember which group titles the user has hidden.
### 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
selectChannelsLoading // Channel list loading flag
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<string, EpgProgram> │ │
│ │ - progressTick: number (30s interval) │ │
│ │ - shouldShowEpg: boolean │ │
│ │ - favoriteIds: Set<string> │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
│ │ All │ │ Groups │ │ Favorites │ │
│ │Channels │ │ Tab │ │ Tab │ │
│ │ Tab │ │ │ │ │ │
│ └────┬────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ └───────────────────┴─────────────────────┘ │
│ │ │
│ ▼ │
│ (channelSelected) output │
│ │ │
└──────────────────────────┼───────────────────────────────────┘
▼
Store Dispatch
ChannelActions.setActiveChannel
```
### Loading States
- `M3uWorkspaceRouteSession` owns route-driven channel loading for the player/sidebar routes: `all` and `groups`.
- The route session sets `channelsLoading` before `getPlaylist()` resolves and clears it when `ChannelActions.setChannels` lands.
- `ChannelListContainerComponent` now renders a dedicated skeleton state while `channelsLoading` is true.
- `ChannelListContainerComponent` no longer clears `channels` on destroy; route/session code is the single owner of shared list lifecycle during navigation.
- The dedicated `/workspace/playlists/:id/favorites` and `/workspace/playlists/:id/recent` collection routes do not drive the shared sidebar channel list; they default to the `playlist` scope so rail links always open the current playlist view, not the last persisted global scope.
- Empty playlists and empty search results are no longer conflated:
- loading: skeletons
- empty source: no channels in the playlist after loading completes
- empty search: no matches within an already loaded playlist
### Group Visibility Management
- `GroupsViewComponent` owns the M3U-only "Manage groups" action and dialog in
`libs/ui/components/src/lib/channel-list-container/groups-view/`.
- The groups rail header also owns an inline search toggle that filters the
currently visible groups without mutating the workspace-level route search
term used by the broader channel views.
- The dialog operates on the full grouped dataset, while the left rail and
channel pane render only groups whose titles are not listed in
`hiddenGroupTitles`.
- `ChannelListContainerComponent` reads `hiddenGroupTitles` from the active M3U
playlist metadata and passes it into the groups view. Saving dialog changes
dispatches `PlaylistActions.updatePlaylistMeta`.
- `PlaylistsService.updatePlaylistMeta()` persists `hiddenGroupTitles` into the
stored playlist payload, and M3U refresh/update flows preserve the existing
value when refreshed playlist data omits the field.
- The groups route keeps the manage action reachable even when every group is
hidden by separating "playlist has no groups" from "no visible/search-matching
groups" empty states.
### EnrichedChannel Pattern
For performance optimization, channels are pre-enriched with EPG data:
```typescript
interface EnrichedChannel extends Channel {
epgProgram: EpgProgram | null | undefined;
logo: string; // Playlist tvg-logo first, XMLTV icon fallback second
progressPercentage: number; // Pre-computed by parent
}
```
The renderer now keeps two lookup maps for M3U collection views:
- `channelEpgMap` for current-program preview data
- `channelIconMap` for XMLTV channel icon fallback data
Logo resolution is runtime-only and follows this rule:
1. playlist `tvg-logo`
2. matched XMLTV `<channel><icon src="...">`
3. generic `live_tv` fallback in the list item component
EPG lookup keys use the same precedence in both program and icon paths:
1. `tvg-id`
2. `tvg-name`
3. channel name
### 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`, `channelIconMap`, `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`, `channelIconMap`, `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<Map<string, EpgProgram>>;
// Batch fetch XMLTV channel metadata for logo fallback
getChannelMetadataForChannels(channelIds: string[]): Observable<Map<string, EpgChannelMetadata | null>>;
// Observables
epgAvailable$: Observable<boolean>;
currentEpgPrograms$: Observable<EpgProgram[]>;
}
```
### 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**: `libs/playlist/m3u/feature-player/src/lib/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
- M3U archive/catch-up playback for supported replay schemes
### Archive / Catch-Up Playback
- The shared EPG UI only shows the archive replay badge when the host confirms
that the selected M3U channel has a playable replay scheme. Archive days
alone are not enough.
- M3U catch-up support is resolved in `m3u-utils` from channel metadata and
the archived program start time.
- Supported replay precedence:
1. `catchup.source` if it is an HTTP(S) URL. IPTVNator rewrites or appends
standard `utc` and `lutc` query params on that URL.
2. Legacy same-stream shift playback when `catchup.type === 'shift'`. In
that case IPTVNator rewrites or appends `utc` and `lutc` on `channel.url`.
3. Legacy same-stream shift fallback when no explicit catch-up mode is
declared, archive-day metadata exists (`tvg.rec`, `timeshift`, or
`catchup.days`), and `channel.url` itself is an HTTP(S) stream URL. This
covers providers that only advertise archive retention such as
`tvg-rec="7"` but still expect standard `utc` and `lutc` query params on
the live URL.
- `tvg.rec`, `timeshift`, and `catchup.days` still define the archive window
shown in the EPG, but replay remains unavailable when the provider declares a
different explicit catch-up scheme that IPTVNator does not understand or when
the stream URL itself is not an HTTP(S) replay target.
- Active replay is stored separately from the selected channel in
`playlistState.activePlaybackUrl`. Inline and external players use
`activePlaybackUrl ?? activeChannel.url`, and returning to live playback
clears the override.
## 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 };
radio: string;
http: {
referrer: string;
'user-agent': string;
origin: string;
};
}
```
### Playlist State Additions
```typescript
interface PlaylistState {
active: Channel | undefined;
activePlaybackUrl: string | null;
currentEpgProgram: EpgProgram | undefined;
epgAvailable: boolean;
channels: Channel[];
}
```
### 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