mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
* spike(embedded-mpv): frame-copy pipeline prototype (helper + shm ring + Electron viewer) Standalone macOS spike for the frame-copy unification direction from the 2026-07-10 analysis: a helper process renders mpv offscreen into a GL FBO, reads frames back through an async PBO ring, and publishes BGRA frames into a 3-slot POSIX shm seqlock ring; a minimal Electron viewer copies the newest frame via a plain-C N-API addon and uploads it to a WebGL canvas per rAF. First numbers on M1 Pro (see spike README): 4K60 HEVC hwdec sustained at 60 fps end to end, ~1.2 ms shm copy + ~3.5 ms texture upload, ~10 ms produce-to-upload age, zero torn frames. Remaining gates: weak hardware, long-run pacing, HDR, latency flash test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): add RESULTS.md measurement log with M1 Pro baseline Structured per-machine table with repro commands so the pending Intel Mac and Windows iGPU runs can be appended and compared one-to-one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): pacing/judder instrumentation + 50/25 fps and HDR gate results Viewer now measures inter-frame intervals on both clocks (present side and producer side): stddev/p99/max, late-frame counters vs the producer's median interval, and a cumulative LONGRUN summary every 30 s. The addon exposes the producer timestamp (produceMs) for this. Measured on M1 Pro: 50 fps and 25 fps cadences are clean (late frames only at startup; residual jitter is 120 Hz rAF grid quantization, bounded by one display tick), and 4K25 HDR10 PQ/BT.2020 is tonemapped to SDR by mpv before readback at full rate with unchanged copy costs. RESULTS.md carries the tables and HDR-clip repro commands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): record 10-minute 4K60 HEVC long-run results Zero dropped frames and zero torn reads after the first-minute warmup over ~8.5 minutes; steady-state late frames (~0.4%) track the 12 s test clip's --loop restarts, not the copy pipeline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): viewport-scaling measurement + integration design draft Confirms the render-at-viewport-size claim (4K source in a 720p FBO costs 720p: 0.17 ms readback / 0.16 ms copy / 0.17 ms upload at 60 fps) and adds DESIGN.md — the draft integration architecture: per-session helper process linking bundled libmpv on all platforms (finally full-featured + Wayland- agnostic Linux), JSON-over-stdio control evolving the Linux wid protocol, unchanged EmbeddedMpvSession renderer contract, shm generations for resize, packaging via the existing vendored-runtime tooling, rollout behind its own flag with the docked path as default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): auto-detect Homebrew prefix and Node headers for Intel Macs BREW_PREFIX was hardcoded to /opt/homebrew (Apple Silicon) and NODE_INC to one nvm version; both now resolve via brew --prefix and the PATH node's execPath, so the pending Intel Mac run needs no Makefile edits. README gets a fresh-machine checklist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): self-contained measurement bundle for machines without Node/pnpm make-bundle.sh assembles a tarball with the spike sources, vendored N-API headers (Makefile prefers them when present, so no Node install is needed), pre-generated 4K HEVC/HDR10 test clips, and an official Electron dist download for the target arch. collect-results.sh builds and runs the full RESULTS.md scenario suite automatically (plus an optional --long 10-minute run) and writes one results-<host>-<date>.txt to send back. Target-machine prerequisites shrink to Xcode CLT + brew mpv — built for the pending Intel Mac baseline run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): support MacPorts libmpv and legacy-macOS bundles Makefile and collect-results.sh now detect libmpv in the Homebrew prefix or MacPorts /opt/local (Homebrew is unsupported on legacy macOS like High Sierra; 'sudo port install mpv +libmpv' provides libmpv there). make-bundle takes ELECTRON_VERSION/BUNDLE_SUFFIX overrides — Electron 27+ needs macOS 10.15, so High Sierra bundles ship Electron 26.6.10 (LSMinimumSystemVersion 10.13). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * spike(embedded-mpv): scope macOS frame-copy engine to Apple Silicon only Owner decision 2026-07-10: skip Intel Mac measurements and gate the future frame-copy engine on arm64. Intel Macs able to run the app at all are a shrinking 2015-2020 cohort and keep the docked/external/web player paths; the macOS hardware gate closes with the M1 Pro numbers, and remaining hardware risk moves to the Windows/Linux ports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): frame-copy helper process and shm frame reader (native layer) iptvnator_mpv_helper: one-process-per-session libmpv host that renders offscreen at viewport size (headless CGL + async PBO ring, validated in spikes/mpv-frame-copy), publishes BGRA frames into a seqlock shm ring with resize generations, plays audio directly, and speaks a stdio protocol — tab-separated commands in, JSON events out. The snapshot event mirrors NativeEmbeddedMpvSessionSnapshot; status semantics (END_FILE reasons, eof-reached with keep-open, pause gated on loaded path, fatal-only status flips) are ported from embedded_mpv.mm. embedded_mpv_frame_reader.node: plain-C N-API reader the preload script uses to memcpy the newest complete frame into a V8 ArrayBuffer (Electron's memory cage forbids zero-copy). Stub exports off macOS. Both build as extra binding.gyp targets through build-embedded-mpv.js; the helper gets the same libmpv dependency-path rewrite + ad-hoc re-sign as the addon and is validated by the forbidden-link check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): frame-copy engine wiring in main process and preload EmbeddedMpvFrameCopyAdapter implements the NativeEmbeddedMpvAddon surface over a per-session helper process (spawn, stdio protocol, snapshot cache, graceful quit->SIGTERM->SIGKILL teardown), so EmbeddedMpvNativeService reuses its polling/diff/power-blocker/recording logic unchanged. The IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY flag (darwin/arm64 only) routes getAddon() to the adapter and reports engine: 'frame-copy' in support. The preload frame pump loads the shm reader addon, copies the newest frame once per rAF into a reused buffer, and uploads it to WebGL2 on the renderer's canvas — no frame data crosses the contextBridge; the bridge only gains attachEmbeddedMpvFrameView/detachEmbeddedMpvFrameView. The experiment flag relaxes the window sandbox for that native require; contextIsolation and nodeIntegration:false stay on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): frame-copy canvas mode in the player component + docs EmbeddedMpvPlayerComponent renders <canvas data-embedded-mpv-frame> when support reports engine 'frame-copy' and the session controller starts/stops the preload frame pump around the session lifecycle. The bounds provider skips HIDDEN_BOUNDS and the popover cutout for this engine — the canvas is ordinary DOM, dialogs and popovers stack above it natively; bounds sync still drives the helper's render size. Adapter unit tests cover spawn args, snapshot caching, shm generations, protocol encoding, unexpected-exit mapping, and dispose escalation. Architecture doc and CLAUDE.md describe the engine, its flag, and the sandbox trade-off. Verified end to end in the built app (M1 Pro): engine detection, helper spawn, lavfi playback onto the canvas via CDP-injected smoke — including an orientation fix (helper FLIP_Y already yields texture-order rows; the pump shader must not flip uv again). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): close helper stdin on dispose + lifecycle logging Live testing surfaced a stray idle helper that survived a session switch; until the root cause is pinned down, dispose now also closes the child's stdin (the helper exits on EOF) as a second kill path besides quit -> SIGTERM -> SIGKILL, and spawn/dispose/exit are logged with the session id so leaks are attributable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): reap sessions when the renderer reloads or crashes Root cause of the stray idle helper found during live testing: session teardown lives in the renderer's Angular lifecycle, which never runs on a renderer crash or hard reload — the main process kept the session (and its frame-copy helper process / native mpv handle) alive until app shutdown. EmbeddedMpvNativeService now watches the main window's webContents for render-process-gone and did-navigate (full reloads only; in-app Angular routing emits did-navigate-in-page) and disposes every session. Applies to both engines. Verified live: location.reload() during frame-copy playback logs 'Disposing 1 session(s): renderer reloaded' and the helper exits cleanly. Regression test drives both events against the service. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): Settings toggle for the frame-copy engine New 'Embedded MPV: frame-copy engine' checkbox in Settings > Playback, shown only when the machine can run it (macOS arm64 with the helper binary present — support now reports frameCopyAvailable). The choice persists to the main-process config store because the engine relaxes the window sandbox for the preload frame pump, which is fixed at window creation: main.ts reads the store before creating the window and sets the engine env var; an explicitly set env var (including '0') always wins, and the UI shows a restart hint while the saved choice differs from the active engine. Localized in all 18 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): aspect-fit rendering in the frame-copy helper The helper now observes dwidth/dheight and renders its FBO at the aspect-fit size of the video inside the requested viewport, bumping a shm generation on change — letterbox bars are never baked into frames (the VOD watch shell's ~2:1 box no longer shows black side bars; the canvas background is transparent so the sides show the app surface, while fullscreen keeps its black backdrop). Frames also get smaller than the viewport when aspects differ, trimming copy cost. Aspect override changes refit automatically. Snapshots now carry videoWidth/videoHeight, and the adapter forwards IPTVNATOR_EMBEDDED_MPV_AUDIO_DELAY to mpv's audio-delay for lip-sync tuning until proper calibration lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): require frame-copy artifacts in macOS package validation + docs macOS packages that ship embedded_mpv.node must also ship the iptvnator_mpv_helper binary and the embedded_mpv_frame_reader.node addon — they come out of the same binding.gyp run, and a package missing them would silently lose the frame-copy engine. Covered in the package-identity test. Architecture doc and CLAUDE.md document the Settings toggle, aspect-fit rendering, audio-delay passthrough, and the renderer-reload session reaping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(electron): inline TS helpers so the sandboxed preload keeps working The frame pump's async/await (target es2015 + importHelpers) made webpack externalize tslib in main.preload.js. Sandboxed preloads can only require Electron's built-in module whitelist, so the entire preload script failed to load and window.electron disappeared for every run without the frame-copy flag. importHelpers:false for electron-backend keeps the preload bundle self-contained — and future async code in preload can no longer silently reintroduce the breakage. Verified live: sandboxed run now has the bridge, reports engine 'native' and frameCopyAvailable true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): commit the frame-copy analysis handoff + source inventory The 2026-07-10 analysis that led to this branch now lives next to the spike (spikes/mpv-frame-copy/ANALYSIS.md), and the architecture doc's What To Commit section lists the frame-copy engine sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address review findings on the frame-copy engine - Stale pump attach can no longer win over a newer session: attach/detach bump a shared epoch and async attach waits re-check it after every await, so an attach for a replaced session aborts instead of installing itself (greptile P1). - A failed frame-view attach (no canvas, no WebGL2, reader missing) now disposes the session and surfaces the error UI instead of leaving audio playing behind a black canvas (codex P2). - A stale frame-copy opt-in without the helper binary falls back to the native engine instead of reporting embedded MPV unsupported, and the Settings checkbox stays visible while a saved opt-in exists so it can always be cleared (codex P2). Regression test covers the fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(packaging): make the darwin frame-copy packaging test host-agnostic On non-macOS CI hosts validatePackagedEmbeddedMpv also reports that macOS link validation needs a macOS host, so the success-path assertion now checks only the frame-copy artifact requirement instead of expecting an empty error list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): Windows/Linux porting handoff for the frame-copy engine Self-contained entry point for porting sessions on other machines: current state and coordination constraints, per-OS task lists (Linux EGL first, then Windows WGL + named shm — the decisive iGPU perf gate), the hard-won gotchas from the macOS integration (preload/tslib sandbox breakage, V8 memory cage, frame orientation, stale-attach epoch, dispose escalation, node-gyp naming, snapshot protocol semantics), testing recipes, and the suggested milestone order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): branching and merge strategy in the porting handoff Port work goes to stacked branches off the frame-copy branch (PR base = frame-copy branch, sequential merges, stack depth one), never into the frozen PR #1169 branch itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): drop stale uncommitted note from porting handoff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): harden frame-copy helper startup --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
797 lines
48 KiB
Markdown
797 lines
48 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
> The process sections below (Plan Mode, Documentation After Changes, Regression Prevention, Agent Bootstrap, Electron CDP Debugging) are mirrored in `AGENTS.md`, which is the canonical copy for agent workflows. When updating one, keep the other in sync.
|
||
|
||
## Plan Mode
|
||
|
||
- When Claude Code is in Plan Mode and produces a final `<proposed_plan>`, it must also save that finalized plan as a Markdown file in the repo-root `.plans/` directory.
|
||
- Save only finalized plans. Do not write interim exploration, question turns, or draft revisions to `.plans/`.
|
||
- Use the filename pattern `YYYY-MM-DD-short-topic.md` such as `.plans/2026-03-12-channel-filtering.md`.
|
||
- If the intended filename already exists, append a numeric suffix such as `-2`, `-3`, and so on.
|
||
|
||
## Documentation After Changes
|
||
|
||
- After implementing a meaningful change, Claude Code must assess whether canonical repo docs need updates before considering the task complete.
|
||
- Meaningful changes include new or changed user-visible behavior, architecture or data-flow changes, non-obvious maintenance workflows, new setup/debugging steps, and new subsystem contracts or boundaries.
|
||
- Skip doc updates for trivial refactors with unchanged behavior, formatting-only edits, and isolated test-only changes.
|
||
- Prefer updating an existing authoritative doc before creating a new one:
|
||
1. `README.md` for top-level developer or user workflows
|
||
2. `docs/architecture/` for architecture, ownership, and behavior contracts
|
||
3. the nearest module `README.md` for local usage or behavior
|
||
- Keep this file (`CLAUDE.md`) itself up to date. It is a living document: whenever a change touches something it describes — monorepo structure (new/moved/renamed apps or libs), routes, database schema/tables, stores and their features, key components, commands, environment behavior, or coding conventions — update the affected `CLAUDE.md` sections as part of the same task, and keep the mirrored process sections in `AGENTS.md` in sync.
|
||
- When adding a new feature area, check whether the Architecture or Key Features sections of `CLAUDE.md` describe the surrounding area; if they do, reflect the addition there instead of leaving the description stale.
|
||
- Do not let `CLAUDE.md` drift: a stale path or route in this file poisons the context of every future agent session. If you notice an outdated claim while working, fix it (or flag it in the final summary) even if it is unrelated to the current task.
|
||
- Repo docs are canonical even when they were originally drafted by an LLM.
|
||
- Final task summaries should state whether docs were updated and which doc changed.
|
||
|
||
## Regression Prevention And Test Updates
|
||
|
||
- Before the final summary for any feature, behavior change, bug fix, data-flow change, Electron IPC/database change, or user-visible UI workflow change, Claude Code must complete a test impact pass. Identify the affected projects and decide whether unit, integration, E2E, build, lint, or manual/CDP verification is required.
|
||
- Bug fixes must normally include regression coverage that fails on the old behavior and passes with the fix. If automated coverage is not practical, document why in the final summary and include the strongest manual validation performed.
|
||
- Feature work and behavior changes must update existing tests when assertions, fixtures, mocks, routes, or E2E flows are now stale, incomplete, or missing. Prefer extending the closest existing spec or E2E file before adding a new suite.
|
||
- Default validation ladder:
|
||
1. Run targeted unit tests for directly affected projects with `pnpm nx test <project>` or existing scripts such as `pnpm run test:frontend`, `pnpm run test:backend`, or `pnpm run test:unit:ci` when the scope is broader.
|
||
2. Run affected E2E coverage when changing user-visible workflows, routing, persistence, playback, portals, settings, import flows, or Electron-only behavior.
|
||
3. Use `pnpm nx show projects --withTarget test` and `pnpm nx show projects --withTarget e2e` when project ownership or available validation targets are unclear.
|
||
4. Prefer specific atomized E2E targets before broad suites when they cover the changed behavior, for example `pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts` or `pnpm nx run electron-backend-e2e:e2e-ci--src/search.e2e.ts`.
|
||
- Electron-specific changes affecting IPC, SQLite, packaged runtime, external players, native file access, or Electron-only routes require Electron E2E coverage where available, or CDP/manual verification with `agent-browser` and the tracing flags documented below.
|
||
- Final task summaries must list tests added or updated, validation commands run with results, and any skipped validation with the reason. For docs-only changes, state that unit/E2E validation was not required and verify the changed Markdown instead.
|
||
|
||
## Project Overview
|
||
|
||
IPTVnator is a cross-platform IPTV player application built with Angular and Electron, supporting M3U/M3U8 playlists, Xtream Codes API, and Stalker portals.
|
||
|
||
**Dual Environment Support**: The application is designed to work in both Electron and as a Progressive Web App (PWA). The architecture uses a factory pattern to inject environment-specific services at runtime, ensuring the same codebase works in both contexts.
|
||
|
||
## Development Commands
|
||
|
||
### Agent Bootstrap
|
||
|
||
```bash
|
||
pnpm install --frozen-lockfile
|
||
pnpm nx show projects
|
||
```
|
||
|
||
- Run the install step in a fresh worktree before relying on Nx discovery, lint, test, or build commands. Without `node_modules`, local Nx modules are unavailable.
|
||
- Use scoped path aliases from `tsconfig.base.json` such as `@iptvnator/services`, `@iptvnator/shared/interfaces`, and `@iptvnator/ui/components`.
|
||
- Do not add new imports from legacy bare aliases such as `services`, `shared-interfaces`, `components`, `m3u-state`, or `database`.
|
||
- Every Nx project should keep `scope:*`, `domain:*`, and `type:*` tags in `project.json`.
|
||
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
|
||
- Repository-specific skills are committed under `.codex/skills/`. If Claude Code does not load skills directly, treat those files as concise ownership docs.
|
||
|
||
### Building and Serving
|
||
|
||
```bash
|
||
# Serve the Angular web app only (development mode, baseHref="/")
|
||
pnpm run serve:frontend
|
||
# or
|
||
nx serve web
|
||
|
||
# Serve with PWA configuration (optimized, baseHref="/")
|
||
pnpm run serve:frontend:pwa
|
||
# or
|
||
nx serve web --configuration=pwa
|
||
|
||
# Serve the Electron app (starts both frontend and backend)
|
||
pnpm run serve:backend
|
||
# or
|
||
nx serve electron-backend
|
||
|
||
# Build frontend for Electron (baseHref="./")
|
||
pnpm run build:frontend
|
||
# or
|
||
nx build web
|
||
|
||
# Build frontend for PWA deployment (baseHref="/")
|
||
pnpm run build:frontend:pwa
|
||
# or
|
||
nx build web --configuration=pwa
|
||
|
||
# Build backend (Electron)
|
||
pnpm run build:backend
|
||
# or
|
||
nx build electron-backend
|
||
|
||
# Package the app (creates distributable without installers)
|
||
pnpm run package:app
|
||
# or
|
||
nx run electron-backend:package
|
||
|
||
# Create installers/executables
|
||
pnpm run make:app
|
||
# or
|
||
nx run electron-backend:make
|
||
```
|
||
|
||
### Electron CDP Debugging
|
||
|
||
- Start Electron in dev mode with: `nx serve electron-backend`
|
||
- Package-script equivalent: `pnpm run serve:backend`
|
||
- The workspace is configured to always launch Electron with: `--remote-debugging-port=9222`
|
||
- Use CDP clients (Chrome DevTools Protocol tools) against: `127.0.0.1:9222`
|
||
- When the task is Electron automation/debugging, use the `electron` skill
|
||
- Do not auto-open DevTools during normal CDP automation. In development, DevTools is opt-in via `ELECTRON_OPEN_DEVTOOLS=1`.
|
||
- If DevTools is open, `agent-browser --cdp 9222 ...` may attach to the DevTools page instead of the IPTVnator window (symptoms: `tab list` shows `about:blank`, empty snapshots, black screenshots). Inspect targets with `curl http://127.0.0.1:9222/json/list` and connect directly to the app page's `webSocketDebuggerUrl`.
|
||
|
||
For startup tracing or white-screen debugging:
|
||
|
||
```bash
|
||
IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
|
||
```
|
||
|
||
Useful narrower flags:
|
||
|
||
- `IPTVNATOR_TRACE_IPC=1` traces renderer `window.electron.*` bridge calls
|
||
- `IPTVNATOR_TRACE_DB=1` traces DB worker requests and DB progress events
|
||
- `IPTVNATOR_TRACE_SQL=1` traces SQLite statements in both main and worker connections
|
||
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow navigation/load lifecycle
|
||
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player launch/reuse/polling debug output
|
||
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console logs into the Electron terminal
|
||
|
||
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
|
||
pnpm nx reset
|
||
```
|
||
|
||
Use global `agent-browser` (preferred):
|
||
|
||
```bash
|
||
# Verify CDP targets
|
||
agent-browser --cdp 9222 tab list
|
||
|
||
# Switch to the app tab and inspect interactive elements
|
||
agent-browser --cdp 9222 tab 1
|
||
agent-browser --cdp 9222 snapshot -i -c -d 4
|
||
|
||
# Capture debug artifacts
|
||
agent-browser --cdp 9222 screenshot /tmp/iptvnator-cdp.png
|
||
agent-browser --cdp 9222 trace start /tmp/iptvnator.trace.zip
|
||
agent-browser --cdp 9222 wait 1500
|
||
agent-browser --cdp 9222 trace stop /tmp/iptvnator.trace.zip
|
||
```
|
||
|
||
If `agent-browser` is not in PATH, use:
|
||
|
||
```bash
|
||
npx --yes agent-browser --cdp 9222 tab list
|
||
```
|
||
|
||
### Testing
|
||
|
||
```bash
|
||
# Run frontend tests
|
||
pnpm run test:frontend
|
||
# or
|
||
pnpm nx test web
|
||
|
||
# Run backend tests
|
||
pnpm run test:backend
|
||
# or
|
||
pnpm nx test electron-backend
|
||
|
||
# Run targeted E2E tests (Playwright)
|
||
pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts
|
||
pnpm nx run electron-backend-e2e:e2e-ci--src/search.e2e.ts
|
||
|
||
# Run broad E2E suites only when the impact justifies it
|
||
pnpm nx e2e web-e2e
|
||
pnpm nx e2e electron-backend-e2e
|
||
|
||
# Run tests with coverage when needed
|
||
pnpm nx test web --configuration=ci
|
||
```
|
||
|
||
Before finishing behavior changes or bug fixes, follow `Regression Prevention And Test Updates` above and report the test impact decision in the final summary.
|
||
|
||
### Linting
|
||
|
||
```bash
|
||
# Lint all projects (what CI enforces on every PR)
|
||
pnpm run lint
|
||
|
||
# Lint a single project
|
||
nx lint web
|
||
nx lint electron-backend
|
||
```
|
||
|
||
CI runs lint for every project (`.github/workflows/ci.yml`). This enforces the
|
||
Nx module-boundary tags, the legacy bare-alias ban, and a `max-lines` ESLint
|
||
rule (hard maximum 400 lines per TypeScript file). Pre-existing oversized files
|
||
are baselined in `tools/eslint/max-lines-baseline.mjs`; regenerate the baseline
|
||
with `node tools/eslint/generate-max-lines-baseline.mjs` after splitting a file.
|
||
Never add new files to the baseline.
|
||
|
||
## Architecture
|
||
|
||
### Monorepo Structure (Nx Workspace)
|
||
|
||
This is an Nx monorepo with the following structure:
|
||
|
||
- **apps/web** - Angular application (frontend, shared by Electron and PWA)
|
||
- **apps/electron-backend** - Electron main process
|
||
- **apps/web-backend** - HTTP backend for the self-hosted PWA (`/parse`, `/parse-xml`, `/xtream`, `/stalker` CORS proxy endpoints)
|
||
- **apps/remote-control-web** - Mobile remote-control web app served by the Electron backend
|
||
- **apps/web-e2e** - Playwright E2E tests against the web app
|
||
- **apps/electron-backend-e2e** - Playwright E2E tests against the Electron app
|
||
- **apps/stalker-mock-server** - Mock Stalker/Ministra portal for dev and E2E
|
||
- **apps/xtream-mock-server** - Mock Xtream Codes API for dev and E2E
|
||
- **apps/website** - Astro + Tailwind landing page and blog
|
||
- **libs/** - Shared libraries:
|
||
- **epg/data-access** - EPG services, runtime bridge, program normalization
|
||
- **m3u-state** - NgRx state management for M3U playlists
|
||
- **playlist/import/feature** - Playlist import flows (file/URL/text upload, Xtream and Stalker import dialogs)
|
||
- **playlist/m3u/feature-player** - M3U video player page and `/workspace/playlists/:id` routes
|
||
- **playlist/shared/{ui,util}** - Shared playlist UI and utilities
|
||
- **portal/xtream/{data-access,feature}** - XtreamStore, services, data sources; routed Xtream components
|
||
- **portal/stalker/{data-access,feature}** - StalkerStore and routed Stalker components
|
||
- **portal/catalog/feature** - Portal catalog UI
|
||
- **portal/downloads/feature** - Download manager UI
|
||
- **portal/shared/{data-access,ui,util}** - Cross-portal shared code
|
||
- **services** - Abstract DataService contract and shared app services (incl. the TMDB metadata enrichment module in `lib/tmdb/`)
|
||
- **shared/interfaces** - TypeScript interfaces and types (incl. `ElectronBridgeApi`)
|
||
- **shared/database** - Canonical Drizzle schema and DB connection (used by the Electron backend)
|
||
- **shared/m3u-utils** - M3U playlist utilities
|
||
- **shared/testing** - Shared test helpers
|
||
- **ui/components** - Reusable UI components (incl. channel list)
|
||
- **ui/epg** - EPG UI (timeline ribbon, multi-EPG, progress panel, program dialogs)
|
||
- **ui/playback** - Player UI (video/audio players)
|
||
- **ui/pipes** - Angular pipes
|
||
- **ui/remote-control** - Remote-control UI pieces
|
||
- **ui/shared-portals** - Shared portal types (`LiveEpgPanelSummary`)
|
||
- **ui/styles** - Shared styles/theme
|
||
- **workspace/{shell,dashboard}** - Workspace shell (layout/navigation) and dashboard
|
||
|
||
### Frontend Architecture (Angular)
|
||
|
||
**State Management**: Uses NgRx for playlist state management:
|
||
|
||
- Store configuration in `apps/web/src/app/app.config.ts`
|
||
- Playlist state, actions, effects, and reducers in `libs/m3u-state/`
|
||
- Entity adapter pattern for managing playlists collection
|
||
- Router store integration for route-based state
|
||
|
||
**XtreamStore Architecture** (Signal Store with Feature Composition):
|
||
|
||
The Xtream Codes module uses NgRx Signal Store with a layered architecture:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ PRESENTATION LAYER │
|
||
│ Components use XtreamStore (facade) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ FACADE LAYER │
|
||
│ XtreamStore │
|
||
│ (Composes feature stores, unified API) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌────────────┬────────────┼────────────┬────────────┐
|
||
▼ ▼ ▼ ▼ ▼
|
||
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
|
||
│ withPortal│ │withContent │ │withSelection│ │ withSearch │ │ withPlayer │
|
||
└────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘
|
||
│ │ │
|
||
└───────────────────────────┼──────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ DATA SOURCE LAYER │
|
||
│ IXtreamDataSource │
|
||
│ ┌───────────────────┬───────────────────┐ │
|
||
│ ▼ ▼ │
|
||
│ ElectronDataSource PwaDataSource │
|
||
│ (DB-first + API) (API-only) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
File structure:
|
||
|
||
```
|
||
libs/portal/xtream/
|
||
├── data-access/src/lib/
|
||
│ ├── stores/
|
||
│ │ ├── features/
|
||
│ │ │ ├── with-portal.feature.ts # Playlist & portal status
|
||
│ │ │ ├── with-content.feature.ts # Categories & streams
|
||
│ │ │ ├── with-selection.feature.ts # UI selection & pagination
|
||
│ │ │ ├── with-search.feature.ts # Search functionality
|
||
│ │ │ ├── with-epg.feature.ts # EPG data
|
||
│ │ │ ├── with-player.feature.ts # Stream URLs & player
|
||
│ │ │ ├── with-playback-positions.feature.ts # Resume/playback positions
|
||
│ │ │ └── index.ts
|
||
│ │ ├── xtream.store.ts # Facade composing all features
|
||
│ │ └── index.ts
|
||
│ ├── services/
|
||
│ │ ├── xtream-api.service.ts # Xtream Codes API calls
|
||
│ │ ├── xtream-url.service.ts # Stream URL construction
|
||
│ │ ├── favorites.service.ts # Favorites persistence
|
||
│ │ ├── epg-queue.service.ts # EPG fetch queueing
|
||
│ │ ├── xtream-xmltv-fallback.service.ts # XMLTV fallback EPG
|
||
│ │ └── index.ts
|
||
│ ├── data-sources/
|
||
│ │ ├── xtream-data-source.interface.ts # Abstract interface + types
|
||
│ │ ├── electron-xtream-data-source.ts # DB-first implementation
|
||
│ │ ├── pwa-xtream-data-source.ts # API-only implementation
|
||
│ │ └── index.ts # provideXtreamDataSource() factory
|
||
│ ├── with-favorites.feature.ts # Favorites feature
|
||
│ └── with-recent-items.ts # Recently viewed feature
|
||
└── feature/src/lib/ # Routed components
|
||
├── xtream-feature.routes.ts # createXtreamRoutes(): /workspace/xtreams/:id tree
|
||
├── live-stream-layout/, vod-details/, serial-details/, ...
|
||
└── global-search-results/ # Global search (Electron-only route)
|
||
```
|
||
|
||
Key patterns:
|
||
|
||
- **Feature stores**: Each `with*.feature.ts` uses `signalStoreFeature()` for focused functionality
|
||
- **Facade pattern**: `XtreamStore` composes all features, maintaining backward compatibility
|
||
- **Data source abstraction**: `IXtreamDataSource` interface with environment-specific implementations
|
||
- **Factory injection**: `provideXtreamDataSource()` selects Electron or PWA implementation at runtime
|
||
|
||
Data strategies by environment:
|
||
| Environment | Strategy |
|
||
|-------------|----------|
|
||
| **Electron** | DB-first: Check DB → fetch API if missing → cache to DB |
|
||
| **PWA** | API-only: Always fetch from API, store in memory |
|
||
|
||
**M3U Playlist Module Architecture**:
|
||
|
||
The M3U playlist module handles traditional M3U/M3U8 playlists with support for 90,000+ channels.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ VIDEO PLAYER PAGE │
|
||
│ libs/playlist/m3u/feature-player/src/lib/video-player/ │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ ┌─────────────┐ ┌───────────────────────────────────────────────┐│
|
||
│ │ Sidebar │ │ Video Player (ArtPlayer/Video.js) ││
|
||
│ │ ┌─────────┐ │ │ ││
|
||
│ │ │Channel │ │ ├───────────────────────────────────────────────┤│
|
||
│ │ │List │ │ │ EPG timeline ribbon (app-epg-timeline) ││
|
||
│ │ │Container│ │ │ horizontal, under the player ││
|
||
│ │ └─────────┘ │ └───────────────────────────────────────────────┘│
|
||
│ └─────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
The live EPG panel is a horizontal **timeline ribbon** under the player (`app-epg-timeline`, `libs/ui/epg/src/lib/epg-timeline/`), not a right-side drawer (reworked in PR #1102). See `docs/architecture/m3u-playlist-module.md` for the timeline's controllers and scroll behavior.
|
||
|
||
**Radio Channel Layout** (when `channel.radio === 'true'`):
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ ┌─────────────┐ ┌────────────────────────────────────────────────┐│
|
||
│ │ Sidebar │ │ Blurred backdrop (station logo) ││
|
||
│ │ │ │ ┌──────────┐ ││
|
||
│ │ │ │ │ Artwork │ ← cinematic hero layout ││
|
||
│ │ │ │ └──────────┘ ││
|
||
│ │ │ │ Station Name ││
|
||
│ │ │ │ [LIVE] badge ││
|
||
│ │ │ │ ⏮ ▶/⏸ ⏭ ← transport controls ││
|
||
│ │ │ │ 🔊 ━━━━━━━━━ ← volume slider ││
|
||
│ │ │ │ (no EPG panel) ││
|
||
│ └─────────────┘ └────────────────────────────────────────────────┘│
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
Key radio behavior:
|
||
|
||
- Detection: `channel.radio === 'true'` (string from M3U `radio` attribute)
|
||
- The audio player always renders inline — `shouldShowInlinePlayer` is bypassed for radio
|
||
- EPG panel is conditionally hidden in the template when radio is active
|
||
- Volume is shared with video player via `localStorage` key `'volume'`
|
||
- Keyboard: ArrowUp/Down adjusts volume by 5%, M toggles mute
|
||
- Component: `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
||
|
||
Channel List Component Structure (parent coordinator pattern):
|
||
|
||
```
|
||
libs/ui/components/src/lib/channel-list-container/
|
||
├── channel-list-container.component.ts # Parent - shared state coordinator
|
||
├── all-channels-view/ # Virtual scroll + debounced search
|
||
├── groups-view/ # Expansion panels + infinite scroll
|
||
├── favorites-view/ # CDK drag-drop reordering
|
||
├── recent-view/ # Recently viewed channels
|
||
└── channel-list-item/ # Individual channel display
|
||
```
|
||
|
||
Key patterns:
|
||
|
||
- **EnrichedChannel**: Pre-computed EPG data attached to channels for performance
|
||
- **Parent coordinator**: Manages shared signals (`channelEpgMap`, `progressTick`, `favoriteIds`)
|
||
- **Virtual scrolling**: CDK virtual scroll for 90,000+ channel lists
|
||
- **Infinite scroll**: IntersectionObserver in groups view loads 50 items at a time
|
||
- **Global progress tick**: Single 30s interval instead of per-item intervals
|
||
|
||
State management via NgRx (`libs/m3u-state/`):
|
||
|
||
- `PlaylistActions`: loadPlaylists, addPlaylist, removePlaylist, parsePlaylist
|
||
- `ChannelActions`: setChannels, setActiveChannel, setAdjacentChannelAsActive
|
||
- `EpgActions`: setActiveEpgProgram, setCurrentEpgProgram, setEpgAvailableFlag
|
||
- `FavoritesActions`: updateFavorites, setFavorites
|
||
|
||
See `docs/architecture/m3u-playlist-module.md` for complete documentation.
|
||
|
||
**Routing**: Lazy-loaded routes in `apps/web/src/app/app.routes.ts`. All user-facing routes are nested under the workspace shell (`/workspace/...`); `/` redirects into the workspace.
|
||
|
||
- Dashboard: `/workspace/dashboard`; sources overview: `/workspace/sources`
|
||
- M3U player: `/workspace/playlists/:id` (children: `favorites`, `recent`, `:view`) — routes in `libs/playlist/m3u/feature-player`
|
||
- Xtream Codes: `/workspace/xtreams/:id` (children: `live`, `vod`, `series`, `search`, `actor/:personId`, `recently-added`, `favorites`, `recent`, `downloads`) — `libs/portal/xtream/feature/src/lib/xtream-feature.routes.ts`
|
||
- Stalker portal: `/workspace/stalker/:id` (children: `itv`, `vod`, `radio`, `series`, `favorites`, `recent`, `search`, `actor/:personId`, `downloads`) — `libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`
|
||
- Global collections: `/workspace/global-favorites`, `/workspace/global-recent`
|
||
- Global search: `/workspace/search` (Electron-only; a guard redirects the PWA to `/workspace/sources`)
|
||
- Downloads: `/workspace/downloads`
|
||
- Settings: `/workspace/settings` (`/settings` redirects there)
|
||
|
||
**Service Architecture** (Factory Pattern):
|
||
|
||
- Abstract `DataService` class in `libs/services/src/lib/data.service.ts` defines the contract
|
||
- Two environment-specific implementations:
|
||
- `ElectronService` (`apps/web/src/app/services/electron.service.ts`) - Uses IPC to communicate with Electron backend
|
||
- `PwaService` (`apps/web/src/app/services/pwa.service.ts`) - Uses HTTP API and IndexedDB for standalone web version
|
||
- Factory function `DataFactory()` in `apps/web/src/app/app.config.ts` determines which implementation to inject:
|
||
```typescript
|
||
if (window.electron) {
|
||
return inject(ElectronService);
|
||
}
|
||
return inject(PwaService);
|
||
```
|
||
|
||
**Data Storage (Environment-Specific)**:
|
||
|
||
- **Electron**: SQLite database via Drizzle ORM (`better-sqlite3` driver)
|
||
- Location: `~/.iptvnator/databases/iptvnator.db`
|
||
- Full-featured relational database with foreign keys and indexes
|
||
- Canonical schema and connection live in `libs/shared/database`
|
||
- **PWA (Web)**: IndexedDB via `ngx-indexed-db`
|
||
- Browser-based NoSQL storage
|
||
- Same schema structure but implemented in IndexedDB
|
||
- Limited by browser storage quotas
|
||
|
||
**TypeScript File Size Rule**:
|
||
|
||
Keep TypeScript files under **300 lines**. Hard maximum is **350–400 lines**.
|
||
|
||
- When creating new files, design them to stay within this limit from the start.
|
||
- When adding a feature to an existing file that would push it past 350 lines, **refactor first**: extract helpers, sub-services, or feature modules before adding the new code.
|
||
- When you notice a file already exceeds 350 lines, **proactively suggest a refactoring** (or perform it if the change is straightforward) — even if the immediate task is small.
|
||
|
||
Typical split strategies:
|
||
|
||
- Angular components: extract child components, move logic to a dedicated service or store feature
|
||
- Signal store features: split into smaller `with*` feature functions in separate files
|
||
- Services: split by responsibility (e.g. separate API, transformation, and state concerns)
|
||
- Utility files: group by domain and export from a barrel `index.ts`
|
||
|
||
This rule exists to keep the codebase navigable and reviewable. A 150-line file is always preferable to a 500-line file.
|
||
|
||
---
|
||
|
||
**Angular Coding Standards**:
|
||
|
||
This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use the following:
|
||
|
||
- **Component Queries**: Use `viewChild()`, `viewChildren()`, `contentChild()`, `contentChildren()` instead of `@ViewChild`, `@ViewChildren`, `@ContentChild`, `@ContentChildren` decorators
|
||
|
||
```typescript
|
||
// ✅ Correct - Signal-based
|
||
readonly menu = viewChild.required<MatMenu>('menuRef');
|
||
readonly items = viewChildren<ElementRef>('item');
|
||
|
||
// ❌ Incorrect - Old decorator syntax
|
||
@ViewChild('menuRef') menu!: MatMenu;
|
||
@ViewChildren('item') items!: QueryList<ElementRef>;
|
||
```
|
||
|
||
**Important**: When using signals in templates with properties that expect non-signal values, unwrap the signal by calling it:
|
||
|
||
```html
|
||
<!-- ✅ Correct - Unwrap the signal -->
|
||
<button [matMenuTriggerFor]="menu()">Open Menu</button>
|
||
|
||
<!-- ❌ Incorrect - Signal not unwrapped -->
|
||
<button [matMenuTriggerFor]="menu">Open Menu</button>
|
||
```
|
||
|
||
- **Component Inputs/Outputs**: Use `input()` and `output()` functions instead of `@Input()` and `@Output()` decorators
|
||
|
||
```typescript
|
||
// ✅ Correct - Signal-based
|
||
readonly title = input.required<string>();
|
||
readonly size = input<number>(10); // with default value
|
||
readonly clicked = output<string>();
|
||
|
||
// ❌ Incorrect - Old decorator syntax
|
||
@Input({ required: true }) title!: string;
|
||
@Input() size = 10;
|
||
@Output() clicked = new EventEmitter<string>();
|
||
```
|
||
|
||
- **Reactive State**: Use signal primitives for reactive state management
|
||
|
||
```typescript
|
||
// ✅ Use signal(), computed(), effect(), linkedSignal()
|
||
readonly count = signal(0);
|
||
readonly doubled = computed(() => this.count() * 2);
|
||
|
||
constructor() {
|
||
effect(() => {
|
||
console.log('Count changed:', this.count());
|
||
});
|
||
}
|
||
```
|
||
|
||
- **Host Bindings**: Use `@HostBinding()` and `@HostListener()` decorators (these don't have signal equivalents yet)
|
||
|
||
```typescript
|
||
@HostBinding('class.active') get isActive() { return this.active(); }
|
||
@HostListener('click') onClick() { /* ... */ }
|
||
```
|
||
|
||
- **Control Flow**: Use `@if`, `@for`, `@switch` instead of `*ngIf`, `*ngFor`, `*ngSwitch`
|
||
|
||
```typescript
|
||
// ✅ Correct - Modern syntax
|
||
@if (isLoggedIn()) {
|
||
<p>Welcome!</p>
|
||
}
|
||
|
||
@for (item of items(); track item.id) {
|
||
<li>{{ item.name }}</li>
|
||
}
|
||
|
||
// ❌ Incorrect - Old syntax
|
||
<p *ngIf="isLoggedIn">Welcome!</p>
|
||
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
|
||
```
|
||
|
||
### Backend Architecture (Electron)
|
||
|
||
**Main Entry**: `apps/electron-backend/src/main.ts`
|
||
|
||
- Bootstraps Electron app and initializes database
|
||
- Registers event handlers for IPC communication
|
||
|
||
**Database**:
|
||
|
||
- **ORM**: Drizzle ORM with `better-sqlite3` (local SQLite file)
|
||
- **Location**: `~/.iptvnator/databases/iptvnator.db` (avoids spaces in path)
|
||
- **Schema** (`libs/shared/database/src/lib/schema.ts` — canonical; `apps/electron-backend/src/app/database/schema.ts` is a backwards-compat re-export shim):
|
||
- `playlists` - Playlist metadata (M3U, Xtream, Stalker)
|
||
- `categories` - Content categories (live, movies, series)
|
||
- `content` - Streams/VOD/series items
|
||
- `favorites` - User favorites
|
||
- `recentlyViewed` - Watch history
|
||
- `epgChannels`, `epgPrograms` - Persisted EPG data
|
||
- `playbackPositions` - Resume positions
|
||
- `downloads` - Download manager state
|
||
- `appState` - Key-value app state (also tracks one-off data migrations)
|
||
- `tmdbMetadata` - TMDB enrichment cache (details payloads + search match resolutions, keyed by media type/lookup key/language)
|
||
- **Connection**: `libs/shared/database/src/lib/connection.ts`
|
||
- `createTables()` auto-creates tables on init (`CREATE TABLE IF NOT EXISTS`)
|
||
- Provides full read-write access for `electron-backend` and a read-only mode
|
||
- A root `drizzle.config.ts` configures Drizzle Kit tooling (points at the schema via the compat shim)
|
||
|
||
**IPC Communication**:
|
||
|
||
- **Preload script**: `apps/electron-backend/src/app/api/main.preload.ts`
|
||
- Exposes `window.electron` API via `contextBridge`
|
||
- All IPC channels defined here (playlist operations, EPG, database CRUD, external players, etc.)
|
||
- The canonical TypeScript contract is `ElectronBridgeApi` in `libs/shared/interfaces/src/lib/electron-api.interface.ts`; `global.d.ts`, `apps/web/src/typings.d.ts`, and `main.preload.ts` must reference this shared type instead of maintaining separate method lists.
|
||
- **Event handlers**: `apps/electron-backend/src/app/events/`
|
||
- `database.events.ts` - Database CRUD operations
|
||
- `playlist.events.ts` - Playlist import/update
|
||
- `epg.events.ts` - EPG IPC registration and freshness/fetch orchestration; worker lifecycle lives in `epg-worker.service.ts`, DB lookups in `epg-query.service.ts`
|
||
- `xtream.events.ts` - Xtream Codes API
|
||
- `stalker.events.ts` - Stalker portal API
|
||
- `player.events.ts` - External player IPC registration; MPV/VLC lifecycle logic lives in `mpv-session.service.ts`, `vlc-session.service.ts`, and shared `external-player-*` helpers
|
||
- `settings.events.ts` - App settings
|
||
- `electron.events.ts` - App version, etc.
|
||
|
||
**Workers** (`apps/electron-backend/src/app/workers/`):
|
||
|
||
- EPG parsing: `epg-parser.worker.ts`; main-process worker lifecycle is coordinated from `apps/electron-backend/src/app/events/epg-worker.service.ts`
|
||
- Non-EPG SQLite work: `database.worker.ts` (see `docs/architecture/sqlite-db-worker.md`)
|
||
- Playlist refresh: `playlist-refresh.worker.ts`
|
||
|
||
### Key Features
|
||
|
||
**Playlist Support**:
|
||
|
||
- M3U/M3U8 files (local or URL)
|
||
- Xtream Codes API (`username`, `password`, `serverUrl`)
|
||
- Stalker portal (`macAddress`, `url`)
|
||
|
||
**Video Players**:
|
||
|
||
- Built-in HTML5 player with HLS.js or Video.js
|
||
- External players: MPV, VLC (via IPC to Electron backend)
|
||
- Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=<x11-window>` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`.
|
||
- Embedded MPV frame-copy engine (experimental, macOS Apple Silicon only; enabled via `Settings > Playback > Embedded MPV: frame-copy engine` (restart required) or `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1` on top of the embedded MPV experiment flag): a per-session `iptvnator_mpv_helper` process renders mpv offscreen at viewport size and publishes BGRA frames into a shm ring; the preload frame pump uploads them onto a renderer `<canvas data-embedded-mpv-frame>`, so controls/dialogs are ordinary DOM above the video (no native-surface compositing workarounds; the flag relaxes the window sandbox for the preload's native reader addon). Stored and explicit opt-ins relax the sandbox only while the base embedded-MPV feature is enabled and a platform-supported runtime contains both an executable helper and readable regular frame-reader addon; packaged discovery is restricted to packaged resources. A disabled base experiment keeps embedded MPV unavailable with the sandbox intact, while a missing, mode-stripped, or incomplete frame-copy runtime falls back to the native engine without relaxing the sandbox. Adapter: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy.adapter.ts`; helper: `apps/electron-backend/native/helper/`; details in `docs/architecture/embedded-mpv-native.md` ("Frame-Copy Engine").
|
||
|
||
**VOD/Series Detail Pages (two-state layout)**:
|
||
|
||
- Xtream and Stalker detail pages use the shared `PortalDetailShellComponent` (`libs/ui/components/src/lib/portal-detail-shell/`) with two states: **Browse** (hero with poster/metadata/actions, episodes below) and **Watch** (hero collapses with a ~300ms morph, the inline player takes the full content width, metadata moves to an About block below the episodes)
|
||
- Watch state derives from `inlinePlayback() !== null` only; external MPV/VLC playback keeps the browse layout. Esc and "Close player" exit to browse without navigation; the now-playing back arrow is route-level back (straight to the list via the host's `goBack()`)
|
||
- Hosts pass hero chips/meta/actions as `*appDetailTags`/`*appDetailMeta`/`*appDetailActions` templates; the shell stamps them into both the hero and the About block
|
||
- Seasons are tabs (`SeasonTabsComponent`, dropdown beyond 6 seasons) with auto-selection (playing episode's season → resume season → first) that fires the same `seasonSelected` lazy-load/enrichment hooks as manual clicks; grid/list episode view toggle persists to localStorage; season descriptions come from `get_series_info` (Xtream) or TMDB (Stalker)
|
||
- See `docs/architecture/embedded-inline-playback.md` ("Two-State Detail Layout")
|
||
|
||
**Radio Player**:
|
||
|
||
- Dedicated audio player for channels with `radio="true"` M3U attribute
|
||
- Cinematic layout: blurred station logo as backdrop, floating artwork card, transport controls
|
||
- Always uses the built-in inline player — external player settings (MPV/VLC) are ignored for radio
|
||
- EPG panel is hidden for radio channels (radio streams have no EPG data)
|
||
- Volume synced with video player via shared `localStorage` key `'volume'`
|
||
- Keyboard shortcuts: ArrowUp/ArrowDown (volume), M (mute)
|
||
- Component: `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
||
|
||
**EPG (Electronic Program Guide)**:
|
||
|
||
- XMLTV format support
|
||
- Background parsing in worker thread
|
||
- Stored in database for quick lookup
|
||
|
||
**TMDB Metadata Enrichment** (opt-in):
|
||
|
||
- Enriches Xtream and Stalker VOD/series detail views with TMDB data (plot, cast with avatar chips, director, genres, rating, artwork, YouTube trailers) via a field-level merge — the provider stays authoritative for stream data and any field TMDB can't fill; Cyrillic titles are searched with `ru-RU` so exact-title matching works
|
||
- "Similar" rail in ALL detail views: TMDB recommendations matched against the provider catalog by normalized title, two-tier — exact form first, year-stripped fallback gated on year compatibility (`libs/portal/xtream/feature/src/lib/tmdb-similar.util.ts`, `normalizeTitleKeys`); cross-portal matches from other imported Xtream playlists supplement the Xtream rail and fully power the Stalker rail (`CrossPortalSimilarService` in `libs/services`, batched `DB_MATCH_TITLES`, Electron only); detail components re-initialize on route param changes since the router reuses them for detail→detail navigation
|
||
- Season/episode enrichment: opening a season lazily fetches `/tv/{id}/season/{n}` and overlays real episode names, overviews and stills via `mergeEpisodesWithTmdb` (Xtream: `XtreamStore.enrichSelectedSerialSeason`; Stalker: overlay in the series view's `mappedSeasons`)
|
||
- Dashboard: opt-in "Trending this week" rail (weekly TMDB trending matched against imported Xtream playlists via one batched `DB_MATCH_TITLES` request; Electron-only, `dashboardRails.tmdbTrending` toggle) and hero TMDB extras (backdrop fallback, rating + genre badges, memoized per session; series heroes show the tracked S/E badge from playback positions) — `DashboardTrendingService` in `libs/workspace/dashboard/data-access`, `DashboardHeroTmdbService` in `libs/workspace/dashboard/feature`; both load async after first paint
|
||
- Actor pages: cast avatar chips are clickable (TMDB person id) and open `actor/:personId` inside the current portal — TMDB person bio + full filmography; Xtream matches titles against the loaded catalog (direct navigation), unmatched titles and all Stalker titles open the portal search prefilled (`?q=`); the in-portal search page shows a Back button (`SearchLayoutComponent.showBackButton` → `Location.back()`) so users can return to the actor page; shared UI in `libs/ui/shared-portals` (`ActorViewComponent`)
|
||
- Actor page "All portals" scope (Electron only): batched `DB_MATCH_TITLES` worker op (trigram FTS over all imported Xtream playlists, `apps/electron-backend/src/app/database/operations/title-match.operations.ts`); `normalizeTitle` is shared renderer/worker via `libs/shared/interfaces/src/lib/title-normalization.util.ts`
|
||
- Opt-in via `Settings > Metadata (TMDB)` (sends titles to TMDB); optional user API key overrides the embedded default (`DEFAULT_TMDB_API_KEY` in `libs/services/src/lib/tmdb/tmdb-config.ts` — an empty placeholder in the repo by design; the real key lives in the `TMDB_API_KEY` GitHub Actions secret and is injected at CI build time by `tools/tmdb/inject-tmdb-key.mjs`)
|
||
- Match confidence: provider `tmdb_id` trusted fully; otherwise normalized-title + year (±1) search with a strict gate — no confident match means no enrichment
|
||
- Detail views render provider data immediately; enrichment patches the selection asynchronously (staleness-guarded)
|
||
- Cached in SQLite `tmdb_metadata` (Electron, via DB worker ops `DB_GET/SET_TMDB_METADATA`) or in-memory (PWA); localized via the app language setting
|
||
- Service layer: `libs/services/src/lib/tmdb/`; store glue: `libs/portal/xtream/data-access/src/lib/stores/xtream-tmdb-enrichment.ts` and `libs/portal/stalker/data-access/src/lib/stores/stalker-tmdb-enrichment.ts` (hooked in `withStalkerSelection().setSelectedItem`)
|
||
- TMDB attribution (logo + disclaimer) is required and shown in the settings TMDB section and About
|
||
- See `docs/architecture/tmdb-metadata-enrichment.md`
|
||
|
||
**Favorites and Recently Viewed**:
|
||
|
||
- Per-playlist favorites and global favorites
|
||
- Recently viewed tracks watch history
|
||
|
||
**Internationalization**:
|
||
|
||
- Uses `@ngx-translate` with 18 language files in `apps/web/src/assets/i18n/`
|
||
|
||
## Development Notes
|
||
|
||
### Environment Detection and Dual-Mode Architecture
|
||
|
||
The app determines whether it's running in Electron or as a PWA by checking:
|
||
|
||
```typescript
|
||
window.electron; // truthy in Electron, undefined in browser
|
||
```
|
||
|
||
**Why Dual Mode?**
|
||
IPTVnator supports both Electron (desktop app) and PWA (web browser) to provide flexibility:
|
||
|
||
- **Electron**: Full-featured desktop experience with local database, external player support (MPV/VLC), and native file system access
|
||
- **PWA**: Lightweight web version that runs in any browser without installation
|
||
|
||
**Environment-Specific Behavior**:
|
||
|
||
- `app.config.ts` - `DataFactory()` selects DataService implementation based on environment
|
||
- `app.routes.ts` - Same `/workspace/...` route tree in both environments; guards keep Electron-only routes (e.g. global search) out of the PWA
|
||
- Storage layer switches automatically:
|
||
- Electron → SQLite/Drizzle ORM → `~/.iptvnator/databases/iptvnator.db`
|
||
- PWA → IndexedDB → Browser storage
|
||
- External player support (MPV/VLC) only available in Electron
|
||
- File system operations only available in Electron (uploading playlists from disk)
|
||
|
||
**Base Href Configuration**:
|
||
The app uses different base href values depending on the build target:
|
||
|
||
- **Development & PWA**: `baseHref="/"` (from `index.html`)
|
||
- Used by: `pnpm run serve:frontend`, `pnpm run build:frontend:pwa`
|
||
- For web servers with proper routing
|
||
- **Electron Production**: `baseHref="./"` (overridden in build config)
|
||
- Used by: `pnpm run build:backend`, `pnpm run make:app`
|
||
- Required for `file://` protocol in Electron
|
||
|
||
Build configurations in `apps/web/project.json`:
|
||
|
||
- `production`: Electron build with `baseHref="./"`
|
||
- `pwa`: Web deployment with `baseHref="/"`
|
||
- `development`: Dev mode with `baseHref="/"` from index.html
|
||
|
||
**Factory Pattern Implementation**:
|
||
The factory pattern ensures a single codebase works in both environments without conditional checks scattered throughout the application. All environment-specific logic is encapsulated in the service implementations.
|
||
|
||
### Testing Strategy
|
||
|
||
- **Unit tests**: Jest with `jest-preset-angular` and `ng-mocks`
|
||
- **E2E tests**: Playwright testing the web app and Electron app
|
||
- Backend tests use standard Jest
|
||
- Bug fixes should add focused regression coverage unless there is a documented reason not to.
|
||
- Use the impact-based validation policy in `Regression Prevention And Test Updates` to choose targeted unit tests, atomized E2E targets, broad suites, or CDP/manual verification.
|
||
|
||
### Nx Commands
|
||
|
||
Use `nx` CLI for better performance:
|
||
|
||
```bash
|
||
pnpm nx run <project>:<target>
|
||
# Example: pnpm nx run web:build
|
||
# Example: pnpm nx run electron-backend:serve
|
||
```
|
||
|
||
To run multiple projects:
|
||
|
||
```bash
|
||
pnpm nx run-many --target=test --all
|
||
```
|
||
|
||
### Electron Build Process
|
||
|
||
The Electron backend depends on the web app being built first:
|
||
|
||
- `electron-backend:build` depends on `web:build`
|
||
- Output goes to `dist/apps/electron-backend` (backend) and `dist/apps/web` (frontend)
|
||
- Packaging combines both into distributable
|
||
|
||
### Database Migrations
|
||
|
||
No formal migration system yet. Schema changes are applied via raw SQL in the `createTables()` function in `libs/shared/database/src/lib/connection.ts` using `CREATE TABLE IF NOT EXISTS`. One-off data migrations run guarded by keys stored in the `appState` table.
|
||
|
||
### Common Patterns
|
||
|
||
**IPC Communication**:
|
||
|
||
1. Define handler in appropriate events file (e.g., `database.events.ts`)
|
||
2. Register with `ipcMain.handle()` in the event bootstrap function
|
||
3. Expose in preload script via `contextBridge.exposeInMainWorld()`
|
||
4. Call from Angular via `window.electron.<methodName>()`
|
||
|
||
**Adding New Playlist Source**:
|
||
|
||
1. Add type to `libs/shared/interfaces/src/lib/playlist.interface.ts`
|
||
2. Create event handler in `apps/electron-backend/src/app/events/`
|
||
3. Add the import flow in `libs/playlist/import/feature/` (add-playlist dialog + per-source import components) and surface it on the dashboard (`libs/workspace/dashboard/`) if needed
|
||
4. Update database schema if needed
|
||
|
||
**State Management**:
|
||
|
||
- Use NgRx for global application state (M3U playlists, `libs/m3u-state`)
|
||
- Use NgRx Signal Store with `signalStoreFeature()` composition for portal/feature state (XtreamStore, StalkerStore)
|
||
- Use NgRx signals for reactive data streams
|
||
|
||
<!-- nx configuration start-->
|
||
<!-- Leave the start & end comments to automatically receive updates. -->
|
||
|
||
## General Guidelines for working with Nx
|
||
|
||
- For navigating/exploring the workspace, invoke the `nx-workspace` skill first when it is available - it has patterns for querying projects, targets, and dependencies. If it is unavailable, use `pnpm nx show projects`, `pnpm nx graph`, and project `project.json` files directly.
|
||
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
|
||
- Prefix nx commands with the workspace's package manager (e.g., `pnpm nx build`, `npm exec nx test`) - avoids using globally installed CLI
|
||
- You have access to the Nx MCP server and its tools, use them to help the user
|
||
- For Nx plugin best practices, check `node_modules/@nx/<plugin>/PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable.
|
||
- NEVER guess CLI flags - always check nx_docs or `--help` first when unsure
|
||
|
||
## Scaffolding & Generators
|
||
|
||
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the `nx-generate` skill FIRST before exploring or calling MCP tools
|
||
|
||
## When to use nx_docs
|
||
|
||
- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
|
||
- DON'T USE for: basic generator syntax (`nx g @nx/react:app`), standard commands, things you already know
|
||
- The `nx-generate` skill handles generator discovery internally - don't call nx_docs just to look up generator syntax
|
||
|
||
<!-- nx configuration end-->
|