mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
chore: resolve master conflicts for embedded mpv branch
Entire-Checkpoint: 5b514fe72836
This commit is contained in:
commit
ec2f6a702a
290 files changed
+12183
-4142
No files matched your search
@@ -126,6 +126,23 @@ The shared row should be reused instead of rebuilding channel markup per view.
|
||||
- Add bottom padding so the last program is not clipped
|
||||
- Current program card uses the same selection treatment as selected channels
|
||||
|
||||
### Collapsible Live EPG
|
||||
|
||||
- Live TV layouts with an internal player use `app-live-epg-panel` around the
|
||||
EPG content, including playlist-specific live pages and the global
|
||||
favorites/recent live tabs.
|
||||
- The live panel toolbar owns the current-program summary and live date
|
||||
navigation together, so `app-epg-list` hides its internal date navigator when
|
||||
projected inside that panel.
|
||||
- Collapsed state is shared across M3U, Xtream, and Stalker with
|
||||
`live-epg-panel-state`; missing or invalid values restore to expanded.
|
||||
- The collapsed panel is a slim current-program strip with a trailing progress
|
||||
line and an expand button. Date controls stay out of the collapsed strip.
|
||||
- Do not render the collapsed strip for external MPV/VLC playback; those
|
||||
layouts keep the full EPG-only panel.
|
||||
- Keep the EPG content mounted while collapsed so current-program state can
|
||||
continue updating.
|
||||
|
||||
### EPG Card
|
||||
|
||||
- Radius:
|
||||
|
||||
@@ -5,6 +5,7 @@ This document describes the M3U playlist module architecture, which handles trad
|
||||
## 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
|
||||
@@ -80,32 +81,32 @@ 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 |
|
||||
| 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
|
||||
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
|
||||
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
|
||||
selectIsEpgAvailable; // EPG data available flag
|
||||
selectCurrentEpgProgram; // Current playing program
|
||||
```
|
||||
|
||||
## Channel List Container
|
||||
@@ -182,9 +183,9 @@ channel-list-container/
|
||||
- `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
|
||||
- loading: skeletons
|
||||
- empty source: no channels in the playlist after loading completes
|
||||
- empty search: no matches within an already loaded playlist
|
||||
|
||||
### Group Visibility Management
|
||||
|
||||
@@ -213,8 +214,8 @@ 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
|
||||
logo: string; // Playlist tvg-logo first, XMLTV icon fallback second
|
||||
progressPercentage: number; // Pre-computed by parent
|
||||
}
|
||||
```
|
||||
|
||||
@@ -237,29 +238,32 @@ EPG lookup keys use the same precedence in both program and icon paths:
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
@@ -277,10 +281,14 @@ class EpgService {
|
||||
getChannelPrograms(channelId: string): void;
|
||||
|
||||
// Batch fetch current programs
|
||||
getCurrentProgramsForChannels(channelIds: string[]): Observable<Map<string, EpgProgram>>;
|
||||
getCurrentProgramsForChannels(
|
||||
channelIds: string[]
|
||||
): Observable<Map<string, EpgProgram>>;
|
||||
|
||||
// Batch fetch XMLTV channel metadata for logo fallback
|
||||
getChannelMetadataForChannels(channelIds: string[]): Observable<Map<string, EpgChannelMetadata | null>>;
|
||||
getChannelMetadataForChannels(
|
||||
channelIds: string[]
|
||||
): Observable<Map<string, EpgChannelMetadata | null>>;
|
||||
|
||||
// Observables
|
||||
epgAvailable$: Observable<boolean>;
|
||||
@@ -290,27 +298,31 @@ class EpgService {
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
- Collapsible inline EPG panel for internal players, persisted through the
|
||||
shared `live-epg-panel-state` preference
|
||||
- Multi-EPG modal view
|
||||
- Channel info overlay
|
||||
- External player support (MPV, VLC) in Electron
|
||||
@@ -324,16 +336,16 @@ class EpgService {
|
||||
- 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.
|
||||
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
|
||||
@@ -346,6 +358,7 @@ class EpgService {
|
||||
## Interfaces
|
||||
|
||||
### Channel Interface
|
||||
|
||||
```typescript
|
||||
interface Channel {
|
||||
id: string;
|
||||
@@ -353,7 +366,7 @@ interface Channel {
|
||||
name: string;
|
||||
group: { title: string };
|
||||
tvg: {
|
||||
id: string; // For EPG matching
|
||||
id: string; // For EPG matching
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
@@ -384,11 +397,12 @@ interface PlaylistState {
|
||||
```
|
||||
|
||||
### EpgProgram Interface
|
||||
|
||||
```typescript
|
||||
interface EpgProgram {
|
||||
start: string; // ISO string
|
||||
stop: string; // ISO string
|
||||
channel: string; // TVG ID
|
||||
start: string; // ISO string
|
||||
stop: string; // ISO string
|
||||
channel: string; // TVG ID
|
||||
title: string;
|
||||
desc: string | null;
|
||||
category: string | null;
|
||||
@@ -408,17 +422,20 @@ interface EpgProgram {
|
||||
## 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
|
||||
@@ -78,21 +78,21 @@ GET load.php?type=itv&action=get_short_epg&ch_id={channel_id}&size={n}&JsHttpReq
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -120,21 +120,21 @@ GET load.php?type=itv&action=get_epg_info&period={hours}&JsHttpRequest=1-xml
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": {
|
||||
"45": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Program Title",
|
||||
"descr": "Program description",
|
||||
"time": "2025-01-15 14:00:00",
|
||||
"time_to": "2025-01-15 16:00:00",
|
||||
"start_timestamp": "1736949600",
|
||||
"stop_timestamp": "1736956800"
|
||||
"js": {
|
||||
"data": {
|
||||
"45": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Program Title",
|
||||
"descr": "Program description",
|
||||
"time": "2025-01-15 14:00:00",
|
||||
"time_to": "2025-01-15 16:00:00",
|
||||
"start_timestamp": "1736949600",
|
||||
"stop_timestamp": "1736956800"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -153,16 +153,16 @@ The short EPG path now exists only for the active-panel fallback flow.
|
||||
|
||||
Key mapped fields:
|
||||
|
||||
| Stalker field | `EpgItem` field |
|
||||
| --- | --- |
|
||||
| `id` | `id` |
|
||||
| `ch_id` | `channel_id` |
|
||||
| `name` | `title` |
|
||||
| `descr` | `description` |
|
||||
| `time` | `start` |
|
||||
| `time_to` | `end`, `stop` |
|
||||
| Stalker field | `EpgItem` field |
|
||||
| ----------------- | ----------------- |
|
||||
| `id` | `id` |
|
||||
| `ch_id` | `channel_id` |
|
||||
| `name` | `title` |
|
||||
| `descr` | `description` |
|
||||
| `time` | `start` |
|
||||
| `time_to` | `end`, `stop` |
|
||||
| `start_timestamp` | `start_timestamp` |
|
||||
| `stop_timestamp` | `stop_timestamp` |
|
||||
| `stop_timestamp` | `stop_timestamp` |
|
||||
|
||||
### Active panel data (`get_epg_info` / fallback) → `EpgProgram`
|
||||
|
||||
@@ -180,13 +180,13 @@ Normalization rules:
|
||||
|
||||
### Key files
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-epg.feature.ts` | bulk cache and fallback handling |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | active-channel EPG loading and controlled `app-epg-list` wiring |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | active panel template |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts` | row preview loading |
|
||||
| `libs/ui/shared-portals/src/lib/epg-list/epg-list.component.ts` | shared controlled EPG list with date navigator |
|
||||
| File | Purpose |
|
||||
| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-epg.feature.ts` | bulk cache and fallback handling |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | active-channel EPG loading and controlled `app-epg-list` wiring |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | active panel template |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts` | row preview loading |
|
||||
| `libs/ui/shared-portals/src/lib/epg-list/epg-list.component.ts` | shared controlled EPG list with date navigator |
|
||||
|
||||
### Store API
|
||||
|
||||
@@ -215,6 +215,10 @@ playlists.
|
||||
`get_short_epg`
|
||||
|
||||
The active panel no longer uses local EPG pagination or a "Load more" button.
|
||||
When Stalker live TV is playing through an internal player, the active panel is
|
||||
wrapped in the shared collapsible live EPG panel. The collapsed/expanded state
|
||||
uses the shared `live-epg-panel-state` preference and is only applied after a
|
||||
stream URL has been resolved; external playback keeps the full EPG-only panel.
|
||||
|
||||
### Channel row preview flow
|
||||
|
||||
@@ -238,10 +242,10 @@ derived locally from `bulkItvEpgByChannel`:
|
||||
|
||||
EPG requests follow the standard Stalker request path:
|
||||
|
||||
| Portal type | Auth path |
|
||||
| --- | --- |
|
||||
| Full Stalker portal | `StalkerSessionService.makeAuthenticatedRequest()` |
|
||||
| Simple Stalker portal | generic IPC request path via Electron |
|
||||
| Portal type | Auth path |
|
||||
| --------------------- | -------------------------------------------------- |
|
||||
| Full Stalker portal | `StalkerSessionService.makeAuthenticatedRequest()` |
|
||||
| Simple Stalker portal | generic IPC request path via Electron |
|
||||
|
||||
No EPG-specific backend transport was needed; the Electron Stalker request
|
||||
handler forwards portal params directly.
|
||||
|
||||
@@ -54,7 +54,12 @@ Provider route integration:
|
||||
3. Xtream and Stalker parent routes attach route-scoped session providers that
|
||||
bootstrap the active playlist, sync provider section state, and clean up
|
||||
provider-local state when the route is destroyed.
|
||||
4. Workspace routes no longer rely on nested provider shell components for
|
||||
4. Xtream route bootstrap is DB-first for already imported Electron playlists:
|
||||
if the requested section has persisted categories and content, the route
|
||||
hydrates from SQLite even when the portal status probe reports unavailable,
|
||||
expired, or inactive. Fresh/no-cache Xtream routes still use the status probe
|
||||
to block remote imports before the loading overlay starts.
|
||||
5. Workspace routes no longer rely on nested provider shell components for
|
||||
hidden local chrome.
|
||||
|
||||
## Shell Structure
|
||||
@@ -126,6 +131,18 @@ Command palette behavior is shell-owned but view-extensible:
|
||||
metadata through `WorkspaceHeaderContextService`.
|
||||
5. Filtering matches command labels, descriptions, and keywords, and keyboard
|
||||
selection always lands on the first enabled command.
|
||||
6. A "Recently used" section is rendered above the standard groups when the
|
||||
query is empty and at least one stored id resolves to a visible+enabled
|
||||
command; ids are persisted via `RecentCommandsService` (capped at 5,
|
||||
stored at `STORE_KEY.RecentCommands`). Storage is **not** pruned by route
|
||||
visibility — a navigation command like `Open sources` is invisible while
|
||||
the user is on `/workspace/sources` but the id stays in storage so it
|
||||
reappears in the recent section after navigating away.
|
||||
7. Five "Switch player to X" commands are registered globally by
|
||||
`WorkspacePlayerCommandsContributor`. The MPV/VLC entries are visible only
|
||||
in Electron, and the entry matching the current `SettingsStore.player()`
|
||||
value is disabled. The new player setting applies to the next playback
|
||||
session; an existing stream is not re-mounted.
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
|
||||
Reference in new issue
Block a user