# 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, search, and per-list channel sorting
- 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.
- M3U favorites and recent collection rows preserve their full `Channel` payload on unified live items so the shared live list can open the read-only channel details context menu without reconstructing partial channel data.
- Recent live rows support context-menu removal in the unified all-playlists view; the row-level delete shortcut remains available on playlist-scoped M3U recent rows.
- 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.

### Channel Sorting

- `AllChannelsViewComponent` owns sorting for the all-channels list and
  persists the selected mode under `m3u-all-channels-sort-mode`.
- `GroupsViewComponent` owns sorting for the selected group's channel pane and
  persists the selected mode under `m3u-groups-channel-sort-mode`.
- Both views support three modes: `Playlist Order`, `Name A-Z`, and `Name Z-A`.
  `Playlist Order` is the default and preserves the original channel order from
  the M3U playlist.
- Sorting is applied after the current search filter and before virtual-scroll
  rendering. Playlist order avoids cloning the full list when no search term is
  active.

### 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

#### AllChannelsViewComponent

- **Inputs**: `channels`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds`
- **Outputs**: `channelSelected`, `favoriteToggled`
- **Features**: Workspace search, persisted channel sorting, virtual scrolling, no-results placeholder

#### GroupsViewComponent

- **Inputs**: Same as AllChannelsTab + `groupedChannels`
- **Outputs**: `channelSelected`, `favoriteToggled`
- **Features**: Resizable groups rail, local group search, group visibility management, persisted selected-group channel sorting

#### FavoritesViewComponent

- **Inputs**: `favorites`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl`
- **Outputs**: `channelSelected`, `favoriteToggled`, `favoritesReordered`
- **Features**: Drag-and-drop reordering with CDK DragDrop, read-only channel details context menu

#### RecentViewComponent

- **Inputs**: recent channels, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl`
- **Outputs**: `channelSelected`, `favoriteToggled`, `recentItemRemoved`
- **Features**: Read-only channel details context menu, row-level and context-menu removal

## 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
- 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
- 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 `@iptvnator/shared/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
