docs: sync CLAUDE.md, AGENTS.md and architecture docs with actual code

Full audit of CLAUDE.md, AGENTS.md, README.md and docs/architecture/
against the codebase; every fix is backed by current code:

- remove documented-but-unimplemented IPTVNATOR_DISABLE_HARDWARE_ACCELERATION
  flag (no reads anywhere in apps/, libs/, tools/)
- CLAUDE.md: add epg_channel_mappings to the schema table list
- m3u-playlist-module: *-tab dirs -> *-view (+recent-view), selectActivePlaylist,
  real PlaylistState shape, ChannelEpgMetadata instead of removed EnrichedChannel,
  actual /workspace/playlists routes, per-view outputs, live-epg-panel-state key
- workspace-dashboard: per-rail Settings.dashboardRails toggles, three missing
  rails in the diagram, split live-favorites/recent-live rails,
  welcome-dashboard empty-state type, RECENTLY_WATCHED_LIVE_TV title key
- stalker-portal: CategoryContentViewComponent for vod/series, collection-route
  components for favorites/recent, corrected series-view/favorites-button paths,
  actor/:personId route, epg panel selectors
- category-management: reloadCategories lives in with-content.feature.ts,
  workspace-context-panel owns the dialog, XtreamPendingRestoreService flow
- stalker-mock-server (+app README): scenario-seeded faker, resetAll() clears
  content cache too, ordinal season episode ids, handlers/ dir location
- sqlite-db-worker: cancellation shipped (drop from out-of-scope), full
  operations module list
- portal-detail-navigation: replace three removed component paths
- tmdb-metadata-enrichment: details cache keys are id:<tmdbId>|v2
- electron-security: CSP frame-src youtube-nocookie exception,
  sandbox: !frameCopyExperiment nuance
- download-manager: libs/portal/xtream instead of xtream-electron folder,
  data-driven downloads nav, drop removed app-search-result-item note
- playlist-backup-restore: settings-backup facade owns the import handoff
- workspace-shell: functional workspaceEntryRedirect, playlists route children
- iptvnator-ui-guidelines: EPG card radius 11px, detail-view mixin is `base`
- embedded-mpv-native, player-controls-contract: minor precision fixes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-24 08:16:33 +02:00
1 parent db70b07093
commit 59c15493a7
19 files changed
+217 -162

No files matched your search

-6
View File
@@ -77,12 +77,6 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
`@iptvnator/shared/logging` or the redacting portal logger before reaching
`console.*`; never log raw credentials while debugging.
- GPU/compositor debugging:
```bash
IPTVNATOR_DISABLE_HARDWARE_ACCELERATION=1 nx serve electron-backend
```
- If local Nx state gets weird before a rerun:
```bash
+1 -6
View File
@@ -134,12 +134,6 @@ Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
`console.*`; never log raw credentials while debugging.
For GPU/compositor debugging:
```bash
IPTVNATOR_DISABLE_HARDWARE_ACCELERATION=1 nx serve electron-backend
```
If the Nx daemon gets into a bad state before rerunning Electron:
```bash
@@ -578,6 +572,7 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use
- `favorites` - User favorites
- `recentlyViewed` - Watch history
- `epgChannels`, `epgPrograms` - Persisted EPG data
- `epgChannelMappings` (`epg_channel_mappings`) - Manual EPG channel mappings (defined in `epg-mapping.schema.ts`, re-exported by `schema.ts`)
- `playbackPositions` - Resume positions
- `downloads` - Download manager state
- `appState` - Key-value app state (also tracks one-off data migrations)
-7
View File
@@ -300,13 +300,6 @@ This redirects the SQLite database, Electron user data, and local config under
the given directory. Delete that directory whenever you want a fresh empty
state.
If you need to debug renderer freezes or GPU/compositor issues in Electron, you
can disable hardware acceleration for a run:
```
$ IPTVNATOR_DISABLE_HARDWARE_ACCELERATION=1 pnpm run serve:backend
```
If you need startup diagnostics for a white screen or a frozen route, you can
also turn on opt-in Electron tracing. These logs are written to the Electron
terminal output so they still help when the renderer DevTools never open:
+14 -13
View File
@@ -127,19 +127,20 @@ apps/stalker-mock-server/
│ ├── scenarios.ts # MAC → scenario config mapping
│ ├── data-generator.ts # Seeded faker data generation
│ ├── data-store.ts # Lazy per-MAC in-memory cache
│ └── routes/
│ ├── portal.route.ts # /portal.php dispatcher
│ └── handlers/
│ ├── handshake.handler.ts
│ ├── do-auth.handler.ts
│ ├── get-categories.handler.ts
│ ├── get-ordered-list.handler.ts
│ ├── get-seasons.handler.ts
│ ├── create-link.handler.ts
│ ├── favorites.handler.ts
│ ├── get-epg-info.handler.ts
│ ├── get-short-epg.handler.ts
│ └── get-genres.handler.ts
│ ├── routes/
│ │ ├── portal.route.ts # /portal.php route
│ │ └── dispatch.ts # Shared Stalker action dispatcher
│ └── handlers/
│ ├── handshake.handler.ts
│ ├── do-auth.handler.ts
│ ├── get-categories.handler.ts
│ ├── get-ordered-list.handler.ts
│ ├── get-seasons.handler.ts
│ ├── create-link.handler.ts
│ ├── favorites.handler.ts
│ ├── get-epg-info.handler.ts
│ ├── get-short-epg.handler.ts
│ └── get-genres.handler.ts
├── project.json
├── tsconfig.json
└── README.md
+18 -14
View File
@@ -75,9 +75,9 @@ ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0
### Store
**File**: `libs/portal/xtream/data-access/src/lib/stores/xtream.store.ts`
**File**: `libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts`
Added `reloadCategories()` method to refresh categories from database after visibility changes, ensuring the sidebar updates immediately.
`reloadCategories()` (exposed on the `XtreamStore` facade via feature composition) refreshes categories from the database after visibility changes, ensuring the sidebar updates immediately.
## Behavior Notes
@@ -133,24 +133,28 @@ apps/electron-backend/src/app/
libs/services/src/lib/
└── database-electron.service.ts # Service methods (with hidden category support)
libs/ui/components/src/lib/recent-playlists/
└── recent-playlists.component.ts # Stores hidden categories to localStorage on refresh
libs/playlist/shared/ui/src/lib/
├── recent-playlists/
│ └── recent-playlists.component.ts # Persists hidden categories (restore state) via XtreamPendingRestoreService on refresh
└── playlist-refresh-action.service.ts # Same restore-state persistence for the header refresh action
libs/services/src/lib/
└── xtream-pending-restore.service.ts # localStorage keyed `xtream-restore-{playlistId}`
libs/workspace/shell/feature/src/lib/
└── workspace-context-panel/
└── workspace-context-panel.component.ts # Tune button; lazy-loads the dialog, calls reloadCategories()
libs/portal/xtream/feature/src/lib/
├── category-management-dialog/ # Dialog component
│ ├── category-management-dialog.component.ts
│ ├── category-management-dialog.component.html
│ └── category-management-dialog.component.scss
├── xtream-main-container.component.ts # Added button & dialog
├── xtream-main-container.component.html
├── live-stream-layout/
│ ├── live-stream-layout.component.ts # Added button & dialog
│ └── live-stream-layout.component.html
└── category-management-dialog/ # Dialog component
├── category-management-dialog.component.ts
├── category-management-dialog.component.html
└── category-management-dialog.component.scss
libs/portal/xtream/data-access/src/lib/
├── data-sources/
│ └── electron-xtream-data-source.ts # Reads/passes hidden categories on save
└── stores/xtream.store.ts # Added reloadCategories method
└── stores/features/with-content.feature.ts # reloadCategories() (exposed on XtreamStore)
apps/web/src/assets/i18n/
└── en.json # Added translation keys
+4 -6
View File
@@ -1,11 +1,11 @@
# Download Manager Architecture
The download manager is a desktop-only feature that layers a curated queue, progress tracking, storage configuration, and playback controls on top of the existing Xtream (xtream-electron folder) + Stalker viewers. Backend work is handled in the Electron process while the Angular renderer surface exposes a dedicated `/downloads` route, contextual buttons, and theme-aware styling.
The download manager is a desktop-only feature that layers a curated queue, progress tracking, storage configuration, and playback controls on top of the existing Xtream (`libs/portal/xtream`) + Stalker (`libs/portal/stalker`) portal views. Backend work is handled in the Electron process while the Angular renderer surface exposes a dedicated `/downloads` route, contextual buttons, and theme-aware styling.
## Backend responsibilities
- **Queue control (`apps/electron-backend/src/app/events/database/download-runtime.ts`)**
`DownloadTask` mirrors the shared `DownloadItem` table plus transient cancel/progress helpers. Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`.
`DownloadTask` mirrors a row of the shared `downloads` table (type `Download` in `libs/shared/database/src/lib/schema.ts`) plus transient cancel/progress helpers. Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`.
- **electron-dl integration**
`startDownload()` calls `electron-dl`'s `download()` helper. Headers (user agent, referer, origin) are attached, and the `onStarted`, `onProgress`, `onCompleted`, and `onCancel` callbacks translate the helper's payload into Drizzle updates. A cancellation requested before `onStarted` is remembered and applied as soon as Electron supplies the `DownloadItem`, so the request cannot be lost in the startup race.
- **Destination collision policy**
@@ -24,10 +24,8 @@ The download manager is a desktop-only feature that layers a curated queue, prog
- **Downloads service** (`libs/services/src/lib/downloads.service.ts`)
Signals back the current download list while `hasDownloads` and `isAvailable` gates UI rendering. Before each download the service asks the main process for the authorized folder and calls `downloadsStart`. The backend extracts the file extension from the URL or falls back to `mp4`. `onDownloadsUpdate` updates the signal, while helper methods `retryDownload`, `removeDownload`, `cancelDownload`, and `playDownload` talk to the corresponding IPC commands so retries reuse existing rows and completed items can open the recorded path.
- **Downloads view** (`libs/portal/downloads/feature`)
A standalone page exposes the queue, desktop-only messaging, folder picker, and action buttons. `downloads.component.html` now wraps the list inside a scrollable panel (`downloads__list-wrapper`) so long queues stay reachable, and `downloads.component.scss` drives a bold two-tone aesthetic inspired by the frontend-design mandate—gradient cards, floating avatars, and theme-aware variables triggered via `body.dark-theme`.
A standalone page exposes the queue, desktop-only messaging, folder picker, and action buttons. `downloads.component.html` wraps the list inside a scrollable panel (`downloads__list-wrapper`) so long queues stay reachable, and `downloads.component.scss` drives gradient cards with theme-aware styling through Angular Material system CSS variables (`var(--mat-sys-*)`, `var(--app-*)`, `color-mix`) — theming tracks the active Material theme rather than a `body.dark-theme` hook.
Failed/canceled cards now show retry/delete controls, queued/downloading cards show a cancel icon, and completed cards render inline play/open buttons with `mat-icon` cues. The header also shows the resolved download folder and a `CHANGE FOLDER` action.
- **Theme fixes**
To keep typography legible in both modes, `app-search-result-item` now inherits color from `:host-context(body.dark-theme)` and `:host-context(body:not(.dark-theme))`, ensuring dense light-theme grids no longer show white text on white backgrounds.
## Global API surface
@@ -37,7 +35,7 @@ The download manager is a desktop-only feature that layers a curated queue, prog
## Routing and navigation
- `/downloads` is available under both portal flavors: the Xtream routes already load `DownloadsComponent`, and the Stalker routes now import the same component so the sidebar link can target `/stalker/:id/downloads` without returning to the startup screen.
- The navigation component already points `routerLink="./downloads"` inside the shared nav pane, so both portals reuse the same download page.
- Downloads navigation is data-driven: `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts` emits a `downloads` section link (`path: [...root, 'downloads']`) for both portals, so they reuse the same download page.
## Queuing, persistence, and UX notes
+10 -6
View File
@@ -9,7 +9,9 @@ explicit hardened `webPreferences` object:
- `contextIsolation: true`
- `nodeIntegration: false`
- `sandbox: true`
- `sandbox: !frameCopyExperiment` — `true` by default; the opt-in Embedded MPV
frame-copy experiment is the one path that disables the renderer sandbox
(`contextIsolation`/`nodeIntegration` stay hardened regardless)
- `webSecurity: true`
- `preload: apps/electron-backend/src/app/api/main.preload.ts`
@@ -98,11 +100,13 @@ produce both `dmg` and `zip` targets, and publish a single merged
The Angular shell defines a baseline CSP in `apps/web/src/index.html`.
The policy keeps the application self-hosted for scripts, blocks object and
frame embedding, limits forms to the app origin, and allows IPTV playback
sources through `media-src` and `connect-src` for `http:`, `https:`, `blob:`,
and `data:`. The policy keeps `script-src` self-hosted and currently keeps
`unsafe-inline` for existing inline styles.
The policy keeps the application self-hosted for scripts, blocks object
embedding (`object-src 'none'`) while allowing frames only from
`https://www.youtube-nocookie.com` (`frame-src`, used for TMDB trailers),
limits forms to the app origin, and allows IPTV playback sources through
`media-src` and `connect-src` for `http:`, `https:`, `blob:`, and `data:`. The
policy keeps `script-src` self-hosted and currently keeps `unsafe-inline` for
existing inline styles.
Angular production builds must not rely on inline event handlers for stylesheet
activation. Keep `web:build:production` and `web:build:pwa` configured without
+5 -3
View File
@@ -209,8 +209,9 @@ the frame-copy canvas.
embedded MPV experiment flag) switches macOS/arm64, Linux and Windows to a
second rendering engine that replaces the native-view compositing entirely
(gate: `isFrameCopyPlatformSupported()` in
`embedded-mpv-frame-copy-platform.util.ts`, shared by `main.ts`, the
service and the adapter):
`embedded-mpv-frame-copy-platform.util.ts`; the adapter imports it directly,
while `main.ts` and the service call it transitively through the same util
module's `isFrameCopyRuntimeUsable()` / `getFrameCopyRuntimeAvailability()`):
- `apps/electron-backend/native/helper/` — `iptvnator_mpv_helper`, a
one-process-per-session libmpv host. It decodes (hwdec), renders
@@ -259,7 +260,8 @@ service and the adapter):
bounds sync.
Enabling it: the `Settings > Playback > Embedded MPV: frame-copy engine`
checkbox (shown only when support reports `frameCopyAvailable`) persists to
checkbox (shown when support reports `frameCopyAvailable` or the option is
already enabled, so it stays visible for turning off) persists to
the main-process config store (`electron-conf`), which `main.ts` reads before
creating the window and translates into the env flag; an explicitly set env
var (including `0`) wins over the stored preference, but cannot bypass the
+6 -5
View File
@@ -88,10 +88,10 @@ Do not add extra badges, left rails, or second selection systems unless there is
## Detail Views
VOD and series detail screens share the `detail-view` Sass mixin from
`libs/ui/styles/_detail-view.scss`. Feature-local `styles/detail-view.scss`
files should only import that mixin and pass small typography overrides when a
provider needs them.
VOD and series detail screens share the detail-view Sass mixin (`@mixin base`)
from `libs/ui/styles/_detail-view.scss`. Feature-local `styles/detail-view.scss`
files should only `@use` that module and `@include detail-view.base(...)` with
small typography overrides when a provider needs them.
Do not copy the full detail-view stylesheet into feature libraries. Add shared
layout changes to the mixin, and keep provider-specific differences explicit in
@@ -201,7 +201,8 @@ The shared row should be reused instead of rebuilding channel markup per view.
### EPG Card
- Radius:
`14px`
`11px` (`.epg-timeline__block` in
`libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.scss`)
- Neutral cards use low-contrast surface treatment
- Current card uses selection surface and selection border
- Description should clamp rather than overflow
+55 -50
View File
@@ -60,31 +60,23 @@ The behavioral contract is guarded by `apps/web/src/app/iptv-playlist-parser.con
### State Structure
```typescript
// libs/m3u-state/src/lib/state.ts
interface PlaylistState {
// Active channel being played
active: Channel | undefined;
active: Channel | undefined; // Active channel being played
activePlaybackUrl: string | null;
activeEpgProgram: EpgProgram | undefined;
currentEpgProgram: EpgProgram | undefined;
epgAvailable: boolean;
channelsLoading: boolean; // Route still resolving channel data
channels: Channel[]; // All channels from current playlist
playlists: PlaylistMetaState; // Playlist metadata (entity adapter)
}
// 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[];
};
// libs/m3u-state/src/lib/playlists.state.ts
interface PlaylistMetaState extends EntityState<PlaylistMeta> {
selectedId: string;
allPlaylistsLoaded: boolean;
selectedFilters: string[]; // 'm3u' | 'xtream' | 'stalker'
}
```
@@ -114,7 +106,7 @@ selectFavorites; // Favorite channel URLs
// Playlist selectors
selectAllPlaylistsMeta; // All playlists
selectActivePlaylistId; // Selected playlist ID
selectCurrentPlaylist; // Active playlist object
selectActivePlaylist; // Active playlist object
selectPlaylistTitle; // Title with "Global favorites" fallback
// EPG selectors
@@ -134,20 +126,25 @@ channel-list-container/
├── 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
├── all-channels-view/ # Virtual scroll + debounced search
│ ├── all-channels-view.component.ts
│ ├── all-channels-view.component.html
│ └── all-channels-view.component.scss
│
├── groups-tab/ # Expansion panels + infinite scroll
│ ├── groups-tab.component.ts
│ ├── groups-tab.component.html
│ └── groups-tab.component.scss
├── groups-view/ # Expansion panels + infinite scroll
│ ├── groups-view.component.ts
│ ├── groups-view.component.html
│ └── groups-view.component.scss
│
├── favorites-tab/ # Drag-drop reordering
│ ├── favorites-tab.component.ts
│ ├── favorites-tab.component.html
│ └── favorites-tab.component.scss
├── favorites-view/ # Drag-drop reordering
│ ├── favorites-view.component.ts
│ ├── favorites-view.component.html
│ └── favorites-view.component.scss
│
├── recent-view/ # Recently viewed channels
│ ├── recent-view.component.ts
│ ├── recent-view.component.html
│ └── recent-view.component.scss
│
└── channel-list-item/ # Individual channel display
├── channel-list-item.component.ts
@@ -235,14 +232,17 @@ channel-list-container/
rendering. Playlist order avoids cloning the full list when no search term is
active.
### EnrichedChannel Pattern
### ChannelEpgMetadata Pattern
For performance optimization, channels are pre-enriched with EPG data:
For performance optimization, EPG data is kept in a side-car map instead of
being cloned onto every channel (the older `EnrichedChannel` pattern that
spread-cloned every channel on every ~30 s tick was removed —
`channel-list-container/epg-enrichment.util.ts`):
```typescript
interface EnrichedChannel extends Channel {
// libs/ui/components/src/lib/channel-list-container/epg-enrichment.util.ts
interface ChannelEpgMetadata {
epgProgram: EpgProgram | null | undefined;
logo: string; // Playlist tvg-logo first, XMLTV icon fallback second
progressPercentage: number; // Pre-computed by parent
}
```
@@ -354,7 +354,7 @@ activation, and the details dialog behave identically to the timeline.
`isLivePlayback`, `loading`, `emptyReason`, `selectedDate`, `collapsed`,
`summary` and emits `programActivated`, `returnToLive`, `selectedDateChange`,
`openEpgSettings`, `retry`, `collapsedChange`. The host layout owns playback,
persists the collapse state (`liveEpgPanelState` in localStorage), and (for
persists the collapse state (`live-epg-panel-state` in localStorage), and (for
the M3U player) the `EpgActions.setCurrentEpgProgram` / `setEpgAvailableFlag`
/ `setActiveEpgProgram` dispatches. The timeline owns the **single** panel
bar — collapse chevron + channel name on the left, return-to-live / jump /
@@ -557,25 +557,25 @@ These URLs are playlist-scoped by default:
#### AllChannelsViewComponent
- **Inputs**: `channels`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds`
- **Outputs**: `channelSelected`, `favoriteToggled`
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `favoriteToggled`, `sidebarToggleRequested`
- **Features**: Workspace search, persisted channel sorting, virtual scrolling, no-results placeholder
#### GroupsViewComponent
- **Inputs**: Same as AllChannelsTab + `groupedChannels`
- **Outputs**: `channelSelected`, `favoriteToggled`
- **Inputs**: Same as AllChannelsViewComponent + `groupedChannels`
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `favoriteToggled`, `hiddenGroupTitlesChanged`, sidebar sizing outputs
- **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`
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `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`
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `removeRecent`
- **Features**: Read-only channel details context menu, row-level and context-menu removal
## EPG Integration
@@ -736,16 +736,21 @@ interface EpgProgram {
## Routes
Routes live in `libs/playlist/m3u/feature-player/src/lib/m3u-workspace.routes.ts`
(`createM3uWorkspaceRoutes()`), nested under the workspace shell:
```
/playlists/:id # Video player with playlist
/iptv # Default IPTV route
/workspace/playlists/:id # M3U player (redirects to .../all)
/workspace/playlists/:id/favorites # Favorites collection view
/workspace/playlists/:id/recent # Recently viewed collection view
/workspace/playlists/:id/:view # Video player with channel list view
```
## Adding New Features
### To add a new tab to channel list:
### To add a new view to channel list:
1. Create component in `channel-list-container/new-tab/`
1. Create component in `channel-list-container/new-view/`
2. Accept inputs: `channels`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl`
3. Emit `channelSelected` output
4. Add to parent template and imports
@@ -635,6 +635,7 @@ The guarded ArtPlayer integration lives in:
```text
libs/ui/playback/src/lib/art-player/
├── art-player-audio-tracks.ts
├── art-player-setup.ts
├── art-player-source-session.ts
├── art-player-video-session.ts
+6 -2
View File
@@ -5,7 +5,9 @@ settings screen.
## Entry Points
- UI: `/Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings.component.ts`
- UI: `/Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings-backup-section.component.ts`
(embedded in `settings.component.html`), with the file read/handoff in
`/Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings-backup.facade.ts`
- Backup service: `/Users/4gray/Code/iptvnator/libs/services/src/lib/playlist-backup.service.ts`
- Manifest types: `/Users/4gray/Code/iptvnator/libs/shared/interfaces/src/lib/playlist-backup.interface.ts`
- Xtream pending restore storage:
@@ -102,7 +104,9 @@ Only EPG source URLs are backed up at the app-settings level.
## Import Flow
The settings component hands file contents to `PlaylistBackupService`.
The settings backup facade (`settings-backup.facade.ts`, driven by
`settings-backup-section.component.ts`) reads the file (`file.text()`) and
hands its contents to `PlaylistBackupService.importBackup()`.
The service:
@@ -50,7 +50,7 @@ Implication:
Current code paths:
- `libs/portal/xtream/feature/src/lib/favorites/favorites.component.ts`
- `libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts` (favorites + recent, with shared UI from `libs/portal/shared/ui/src/lib/components/favorites-layout/`)
- `libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts`
- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts`
@@ -113,8 +113,7 @@ Implication:
Current code paths:
- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-collection-route.component.ts` (favorites + recent via `mode` route data) -> `stalker-collection-detail.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts` (Stalker branch)
+19 -5
View File
@@ -56,6 +56,15 @@ Keep SQL-heavy logic here so the worker entry remains a thin dispatcher:
2. `apps/electron-backend/src/app/database/operations/content.operations.ts`
3. `apps/electron-backend/src/app/database/operations/playlist.operations.ts`
4. `apps/electron-backend/src/app/database/operations/xtream.operations.ts`
5. `apps/electron-backend/src/app/database/operations/favorites.operations.ts`
6. `apps/electron-backend/src/app/database/operations/recently-viewed.operations.ts`
7. `apps/electron-backend/src/app/database/operations/playback-position.operations.ts`
8. `apps/electron-backend/src/app/database/operations/content-backdrop.operations.ts`
9. `apps/electron-backend/src/app/database/operations/title-match.operations.ts`
10. `apps/electron-backend/src/app/database/operations/tmdb.operations.ts`
11. `apps/electron-backend/src/app/database/operations/epg-mapping.operations.ts`
(plus the shared cancellation helper `operation-control.ts` in the same directory)
## Worker Architecture
@@ -675,11 +684,16 @@ CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --s
These are intentionally still out of scope for this first cut:
1. request cancellation
2. moving network-heavy Xtream fetches off the current path
3. migrating every remaining small SQLite IPC handler to the worker
4. richer delete progress reporting for bulk destructive operations
5. repo-wide Angular/Jest cleanup for the currently failing web test baseline
1. moving network-heavy Xtream fetches off the current path
2. migrating every remaining small SQLite IPC handler to the worker
3. richer delete progress reporting for bulk destructive operations
4. repo-wide Angular/Jest cleanup for the currently failing web test baseline
(Request cancellation, originally listed here, has since shipped — see the
"Cancellation contract" section above: `DB_CANCEL_OPERATION` in
`apps/electron-backend/src/app/api/main.preload.ts`, `AbortError` production in
`database.worker.ts`, and `DatabaseService.cancelOperation` in
`libs/services/src/lib/database-electron.service.ts`.)
## Extending The Worker
+14 -4
View File
@@ -22,7 +22,7 @@ The mock server enables:
Per-request random data would break navigation: if category IDs change between calls, content fetched under a category ID won't match the category list. Instead:
- Data is generated **once per MAC address** on first request, then cached in memory.
- `@faker-js/faker` is seeded with a numeric value derived from the MAC address before generation.
- `@faker-js/faker` is seeded with the scenario's `seed` value before generation: predefined scenario MACs use fixed seeds from `scenarios.ts`; unknown MACs derive the seed from the MAC via `macToSeed()`.
- Same MAC → identical data on every server restart.
- Restart the server to reshuffle all data.
@@ -41,7 +41,7 @@ No files or databases are written. All state (generated content + favorites) liv
## Data Generation Pipeline
```
faker.seed(macToNumber(mac))
faker.seed(config.seed) // scenario seed; unknown MACs: macToSeed(mac)
│
├── generateCategories('itv', N) → itvCategories[]
│ └── generateChannels() → channels Map<categoryId, channel[]>
@@ -123,7 +123,7 @@ marker and an `ffrt4://radio/...` command.
"id": "30001-s1",
"name": "Season 1",
"cmd": "ffrt4://series/30001/season/1",
"series": ["30001-s1-e1", "30001-s1-e2", ...],
"series": ["1", "2", "3", ...],
"screenshot_uri": "https://picsum.photos/seed/30001-s1/300/200",
"director": "...",
"actors": "...",
@@ -217,9 +217,19 @@ interface ScenarioConfig {
episodesPerSeason: number;
isSeriesFraction: number; // 0–1: fraction of VOD with is_series=1
embeddedSeriesFraction: number; // 0–1: fraction of VOD with embedded series[]
supportsGetAllChannels?: boolean; // default true; false mimics legacy portals
// without the ITV get_all_channels action
}
```
The `legacy-pagination` scenario (`00:1A:79:00:00:06`) sets
`supportsGetAllChannels: false`: `get_all_channels` then answers with an error
payload so clients fall back to the paginated `get_ordered_list` crawl. For
supporting scenarios, `get_all_channels` (`get-all-channels.handler.ts`,
`type=itv` only) returns the complete ITV channel list in one
`{ js: { data, total_items } }` response, excluding channels from censored
(adult) genres.
### Adding a New Scenario
1. Add an entry to the `SCENARIOS` map in `src/app/scenarios.ts`.
@@ -257,7 +267,7 @@ Playwright waits for both servers to be healthy before starting tests. If either
Each stalker e2e test calls `POST http://localhost:3210/reset` in `beforeEach` to clear in-memory state. This ensures tests don't bleed favorites or other mutable state into each other.
The generated content (categories, items) is **not** cleared on reset — it's deterministic and doesn't need to be. Only in-memory favorites are cleared.
`resetAll()` clears both the generated-content cache and in-memory favorites (`data-store.ts`). Because generation is seed-deterministic, the next request regenerates identical content, so the observable data does not change across resets.
### Recommended Test Structure
+12 -11
View File
@@ -32,14 +32,15 @@ Stalker support covers:
Primary route tree lives in
`libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`.
- `/stalker/:id/vod`
- `/stalker/:id/series`
- `/stalker/:id/vod` (plus `vod/:categoryId` child)
- `/stalker/:id/series` (plus `series/:categoryId` child)
- `/stalker/:id/itv`
- `/stalker/:id/radio`
- `/stalker/:id/favorites`
- `/stalker/:id/recent`
- `/stalker/:id/search`
- `/stalker/:id/downloads` (shared downloads module from Xtream UI)
- `/stalker/:id/actor/:personId`
- `/stalker/:id/downloads` (shared `DownloadsComponent` from `@iptvnator/portal/downloads/feature`)
## Runtime Architecture
@@ -48,20 +49,20 @@ Primary route tree lives in
3. Requests go through `DataService.sendIpcEvent(STALKER_REQUEST, ...)` or `StalkerSessionService` (full portal auth).
4. Electron main process handles `STALKER_REQUEST` in
`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.
5. Axios calls Stalker `load.php` API with required headers/cookies and returns the raw `response.data` to the renderer; normalization happens in the store feature slices.
## Main UI Components
- `libs/portal/stalker/feature/src/lib/stalker-main-container.component.ts`
- Category + content layout for `vod` and `series`
- `CategoryContentViewComponent` from `@iptvnator/portal/catalog/feature` (`libs/portal/catalog/feature`)
- Shared category + content layout used by the `vod` and `series` routes (wired in `stalker-feature.routes.ts` via `loadCategoryContentViewComponent`)
- `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
- ITV live playback, radio playback, channel/station navigation, EPG panel integration
- `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
- Shared inline audio player used by M3U radio channels and Stalker radio stations
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
- Season/episode UI for all Stalker series modes
- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-collection-route.component.ts`
- Favorites and recently-viewed collection views (`mode = 'favorites' | 'recent'` route data), rendering `stalker-collection-detail.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
## Store and Data Flow
@@ -342,9 +343,9 @@ Current implementation is shared via Stalker-specific helpers:
Where this is used:
- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts` (favorites + recently viewed)
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
- `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
Navigation rule to preserve:
@@ -408,7 +409,7 @@ See full backend and web-remote flow in [Remote Control Architecture](./remote-c
Stalker ITV now splits EPG usage:
- active channel panel: bulk `get_epg_info` cached once per playlist and rendered
through shared `app-epg-list`
through the shared EPG panel (`app-epg-timeline`, or `app-epg-list-view` in list mode)
- channel row preview: no pre-playback network requests; previews are derived
from cached bulk EPG only after the first active-channel fetch succeeds
- active panel fallback: `get_short_epg` when bulk EPG is missing or unsupported
@@ -215,7 +215,7 @@ Single table with two row kinds discriminated by `lookup_key` prefix:
```
tmdb_metadata (
media_type 'movie' | 'tv' | 'person',
lookup_key 'id:<tmdbId>' -- details payload row
lookup_key 'id:<tmdbId>|v2' -- details payload row
'title:<normalized>|year:<y>|v2' -- search resolution row
'person:<personId>' -- person payload row
language TEXT, -- TMDB language code
@@ -229,8 +229,11 @@ tmdb_metadata (
TTLs (enforced at read time in `TmdbCacheService.isFresh`): details and
positive matches 30 days, negative matches 7 days.
Search keys carry a version suffix so normalization changes cannot reuse stale
positive or negative resolutions. Database startup deletes the obsolete
Search and details keys carry a `|v2` version suffix (`buildDetailsLookupKey`
in `tmdb-matcher.ts`): for search rows so normalization changes cannot reuse
stale positive or negative resolutions, for details rows because payloads now
include videos via `append_to_response` and pre-videos cache rows had to be
invalidated. Database startup deletes the obsolete
unversioned search rows once and records
`migration:tmdb-search-lookup-v2-cache-cleanup:v1` in `app_state`; details and
person cache rows are unaffected.
+37 -16
View File
@@ -12,8 +12,12 @@ Related:
- The dashboard is the default `/workspace` landing page.
- It is a **rail-based** content surface (Netflix / Apple TV pattern), not a
customizable widget grid.
- Layout is static and curated — there is no edit mode, drag-drop, size
stepper, show/hide toggle, or persisted layout. Rails auto-hide when empty.
- Layout order is static and curated — there is no edit mode, drag-drop, or
size stepper. Each rail has a persisted show/hide toggle
(`Settings.dashboardRails`, `DashboardRailsSettings` in
`libs/shared/interfaces/src/lib/settings.interface.ts`, surfaced under
Settings → Dashboard); every template rail is gated by
`dashboardRails().<key>`. Rails additionally auto-hide when empty.
- First-run users see the shared welcome empty-state with a single primary
CTA to add their first playlist.
@@ -38,14 +42,23 @@ Core implementation:
│ Continue Watching · See all → │
│ [poster][poster][poster][poster] →→ │
├─────────────────────────────────────────────────────────────────────┤
│ Live now on your favorites / Continue with live TV · See all → │
│ Live now on your favorites · See all → │
│ [channel][channel][channel][channel] →→ │
├─────────────────────────────────────────────────────────────────────┤
│ Recently watched live TV · See all → │
│ [channel][channel][channel][channel] →→ │
├─────────────────────────────────────────────────────────────────────┤
│ Favorite movies & series · See all → │
│ [poster][poster][poster][poster] →→ │
├─────────────────────────────────────────────────────────────────────┤
│ Recently Used Sources · See all → │
│ [tile][tile][tile][tile] →→ │
├─────────────────────────────────────────────────────────────────────┤
│ Recently Added on Xtream (aggregated across providers) │
│ [poster][poster][poster] →→ │
├─────────────────────────────────────────────────────────────────────┤
│ Trending this week (TMDB, opt-in, Electron-only) │
│ [poster][poster][poster] →→ │
└─────────────────────────────────────────────────────────────────────┘
```
@@ -55,7 +68,7 @@ Render rules:
page no longer uses `dashboardReady()` as a page-wide skeleton gate.
Initial hero/recent/favorites loading states render scoped skeletons so one
slow rail does not hide already available content.
2. `hasPlaylists() === false` → render `<app-empty-state type="welcome">`
2. `hasPlaylists() === false` → render `<app-empty-state [type]="'welcome-dashboard'">`
full-bleed. All rails and the hero are skipped.
3. `hero()` = `globalRecentItems()[0]`. If present, render the hero panel.
4. Each rail is emitted via `@if (cards.length > 0)`. Empty rails are hidden
@@ -63,9 +76,11 @@ Render rules:
5. The continue-watching hero prefers a stored Xtream `backdrop_url`; when it
is missing the UI falls back to a blurred poster treatment instead of
showing a flat panel.
6. The mixed global favorites rail is not rendered on the dashboard. Live
favorites are promoted into the live rail, while mixed favorites stay on
`/workspace/global-favorites`.
6. Live favorites are promoted into their own live rail; movie/series
favorites render in a separate `Favorite movies & series` rail
(`favoriteMoviesAndSeriesCards`, `data-test-id="dashboard-favorite-vod-rail"`,
mapped from `globalFavoriteItems()` filtered to movie/series). Full mixed
favorites management stays on `/workspace/global-favorites`.
7. The live favorites rail keeps its scoped skeleton until the initial global
favorites load has completed for both Xtream-backed and playlist-backed
favorites. This avoids first-paint partial counts such as a single Stalker
@@ -106,9 +121,10 @@ Render rules:
metadata and playback positions load, the detail player consumes the
target once and resumes the saved episode. Opening the same item normally
from the global recent grid remains a detail-only action.
3. `liveOnFavoritesCardsEnriched` — maps favorited live channels first,
falling back to recently watched live channels when no live favorites
exist. M3U cards carry an `epg_lookup_key` using the app-wide XMLTV
3. `liveFavoriteCardsEnriched` and `recentLiveCardsEnriched` — two
independent rails (`dashboard-live-favorites-rail` and
`dashboard-recent-live-rail`); there is no fallback from one to the
other. M3U cards carry an `epg_lookup_key` using the app-wide XMLTV
fallback order (`tvg-id` -> `tvg-name` -> channel name); EPG enrichment
must use that key before falling back to the card title.
4. `xtreamRecentlyAddedCards` — maps `xtreamRecentlyAddedItems()` to rail
@@ -130,7 +146,10 @@ Render rules:
3. `DashboardDataService` is passive on construction. The dashboard feature
owns the initial reloads for recent items, favorites, and Xtream recently
added rows on page entry.
4. No `Layout` state, no localStorage keys, no migrations.
4. No dashboard-local `Layout` state, no localStorage keys, no migrations.
Per-rail visibility is the one persisted preference, and it lives in the
global settings store (`Settings.dashboardRails`), not in a
dashboard-owned layout blob.
5. Navigation state + deep-link targets come from the existing
`getRecentItemLink()` / `getGlobalFavoriteLink()` / `getPlaylistLink()`
helpers on `DashboardDataService` and reuse the workspace navigation
@@ -162,7 +181,7 @@ Render rules:
## Empty State
The welcome state is rendered via the existing
`EmptyStateComponent` (`type="welcome"`) from
`EmptyStateComponent` (`type="welcome-dashboard"`) from
`libs/playlist/shared/ui`:
1. Illustration + headline + description from the existing M3U welcome
@@ -189,8 +208,9 @@ The welcome state is rendered via the existing
7. `Recently Used Sources` reflects recent source usage across all provider
types, not just recent imports.
8. The live rail title key must match the rendered source: favorites use
`WORKSPACE.DASHBOARD.LIVE_FAVORITES`; recently watched fallback uses
`WORKSPACE.DASHBOARD.LIVE_RECENT`.
`WORKSPACE.DASHBOARD.LIVE_FAVORITES`; the recently-watched-live rail uses
`WORKSPACE.DASHBOARD.RECENTLY_WATCHED_LIVE_TV`
(`liveRailTitleKeyForSource` in `rails/dashboard-rail.utils.ts`).
## Adding Or Changing Rails
@@ -210,8 +230,9 @@ Current workflow:
Intentionally out of scope:
1. Customizable layout (drag/drop, resize, show/hide toggles, layout
persistence). Removed in favor of a curated, opinionated order.
1. Customizable layout (drag/drop, resize, freeform reordering). The rail
order stays curated and opinionated. (Per-rail show/hide toggles have
since shipped via `Settings.dashboardRails` — see Summary.)
2. Freeform widget grid with collision management.
3. External data rails such as RSS, sports, or news adapters.
4. Per-user A/B variants of rail ordering.
+7 -2
View File
@@ -38,10 +38,15 @@ Core implementation:
Current workspace routes:
1. `/` -> `/workspace`
2. `/workspace` -> `/workspace/dashboard`
2. `/workspace` -> functional redirect `workspaceEntryRedirect`
(`WorkspaceStartupPreferencesService.resolveInitialWorkspacePath()`;
`/workspace/dashboard` by default, `/workspace/sources` when the dashboard
is disabled, or the last restorable route under
`StartupBehavior.RestoreLastView` — `dashboard` itself is guarded by
`dashboardAccessGuard`)
3. `/workspace/dashboard`
4. `/workspace/sources`
5. `/workspace/playlists/:id/:view`
5. `/workspace/playlists/:id/:view` (plus `favorites` and `recent` siblings)
6. `/workspace/global-favorites`
7. `/workspace/global-recent`
8. `/workspace/search`