From 59c15493a700dde93b987c1f0332ad295a2f9018 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 24 Jul 2026 08:13:43 +0200 Subject: [PATCH] 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:|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 --- AGENTS.md | 6 - CLAUDE.md | 7 +- README.md | 7 -- apps/stalker-mock-server/README.md | 27 ++--- docs/architecture/category-management.md | 32 +++--- docs/architecture/download-manager.md | 10 +- docs/architecture/electron-security.md | 16 ++- docs/architecture/embedded-mpv-native.md | 8 +- docs/architecture/iptvnator-ui-guidelines.md | 11 +- docs/architecture/m3u-playlist-module.md | 105 +++++++++--------- docs/architecture/player-controls-contract.md | 1 + docs/architecture/playlist-backup-restore.md | 8 +- docs/architecture/portal-detail-navigation.md | 5 +- docs/architecture/sqlite-db-worker.md | 24 +++- docs/architecture/stalker-mock-server.md | 18 ++- docs/architecture/stalker-portal.md | 23 ++-- docs/architecture/tmdb-metadata-enrichment.md | 9 +- docs/architecture/workspace-dashboard.md | 53 ++++++--- docs/architecture/workspace-shell.md | 9 +- 19 files changed, 217 insertions(+), 162 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 01f664e84..a910c9b14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 659ab4a3a..b5d2fedc3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/README.md b/README.md index d06f5ae1a..b4b810a7b 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/apps/stalker-mock-server/README.md b/apps/stalker-mock-server/README.md index fbcda2e2e..8a299a823 100644 --- a/apps/stalker-mock-server/README.md +++ b/apps/stalker-mock-server/README.md @@ -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 diff --git a/docs/architecture/category-management.md b/docs/architecture/category-management.md index c9aaeaddb..77e360629 100644 --- a/docs/architecture/category-management.md +++ b/docs/architecture/category-management.md @@ -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 diff --git a/docs/architecture/download-manager.md b/docs/architecture/download-manager.md index 1db7fca8c..0608f1fb1 100644 --- a/docs/architecture/download-manager.md +++ b/docs/architecture/download-manager.md @@ -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 diff --git a/docs/architecture/electron-security.md b/docs/architecture/electron-security.md index 0f33d310f..d5c2f16db 100644 --- a/docs/architecture/electron-security.md +++ b/docs/architecture/electron-security.md @@ -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 diff --git a/docs/architecture/embedded-mpv-native.md b/docs/architecture/embedded-mpv-native.md index 6adbbaabf..c143db405 100644 --- a/docs/architecture/embedded-mpv-native.md +++ b/docs/architecture/embedded-mpv-native.md @@ -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 diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index d0d3f9f29..6fb85b0b2 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -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 diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index 85847412d..d4c8ba9ea 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -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; - selectedId: string | undefined; - allPlaylistsLoaded: boolean; - selectedFilters: PlaylistSourceFilter[]; - }; +// libs/m3u-state/src/lib/playlists.state.ts +interface PlaylistMetaState extends EntityState { + 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 diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index dd565c991..4ee51bd55 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -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 diff --git a/docs/architecture/playlist-backup-restore.md b/docs/architecture/playlist-backup-restore.md index 3a4589bf8..1a88331e6 100644 --- a/docs/architecture/playlist-backup-restore.md +++ b/docs/architecture/playlist-backup-restore.md @@ -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: diff --git a/docs/architecture/portal-detail-navigation.md b/docs/architecture/portal-detail-navigation.md index 6b684b99f..8b2d08717 100644 --- a/docs/architecture/portal-detail-navigation.md +++ b/docs/architecture/portal-detail-navigation.md @@ -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) diff --git a/docs/architecture/sqlite-db-worker.md b/docs/architecture/sqlite-db-worker.md index 4aaa17c61..1083053f6 100644 --- a/docs/architecture/sqlite-db-worker.md +++ b/docs/architecture/sqlite-db-worker.md @@ -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 diff --git a/docs/architecture/stalker-mock-server.md b/docs/architecture/stalker-mock-server.md index 60458d092..a15bb061a 100644 --- a/docs/architecture/stalker-mock-server.md +++ b/docs/architecture/stalker-mock-server.md @@ -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 @@ -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 diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index c37c44d59..c75d97f4f 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -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 diff --git a/docs/architecture/tmdb-metadata-enrichment.md b/docs/architecture/tmdb-metadata-enrichment.md index b28de8208..dfe689ef7 100644 --- a/docs/architecture/tmdb-metadata-enrichment.md +++ b/docs/architecture/tmdb-metadata-enrichment.md @@ -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:' -- details payload row + lookup_key 'id:|v2' -- details payload row 'title:|year:|v2' -- search resolution row 'person:' -- 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. diff --git a/docs/architecture/workspace-dashboard.md b/docs/architecture/workspace-dashboard.md index 7e0cb4ee7..79942c6e7 100644 --- a/docs/architecture/workspace-dashboard.md +++ b/docs/architecture/workspace-dashboard.md @@ -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().`. 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 `` +2. `hasPlaylists() === false` → render `` 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. diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index f5a7d14a8..5a0b6c9c0 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -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`