mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
Merge remote-tracking branch 'origin/master' into codex/linux-embedded-mpv-ci-cleanup
# Conflicts: # docs/architecture/embedded-mpv-native.md
This commit is contained in:
commit
46a2a7f1ae
127 files changed
+7274
-1576
No files matched your search
@@ -4,17 +4,25 @@ The download manager is a desktop-only feature that layers a curated queue, prog
|
||||
|
||||
## Backend responsibilities
|
||||
|
||||
- **Queue control (apps/electron-backend/src/app/events/downloads.events.ts)**
|
||||
`DownloadTask` mirrors the shared `DownloadItem` table plus transient cancel/progress helpers. `enqueueDownload()` resolves a unique file path, persists a `queued` row in `downloads`, 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()` now 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. The handler throttles progress broadcast, saves `filePath`/`fileName` from `electron-dl`, and marks failures/cancellations cleanly. Errors and cancellations delete partial files.
|
||||
- **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()`.
|
||||
- **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**
|
||||
Existing destination files are never overwritten. Before starting Electron's
|
||||
download, the backend atomically reserves a free numbered filename with an
|
||||
exclusive filesystem create. Electron may overwrite that empty reservation,
|
||||
but cannot overwrite a file that existed before the reservation. The selected
|
||||
`filePath` and `fileName` are persisted before transfer begins. Errors,
|
||||
cancellations, and startup recovery remove that exact partial path and clear
|
||||
it from the row; completed downloads replace it with Electron's final values.
|
||||
- **IPC surface**
|
||||
The backend exposes `DOWNLOADS_*` handlers for list retrieval, start/cancel/retry/remove operations, folder selection/reveal, and the `DOWNLOADS_UPDATE_EVENT` emitter that the renderer listens to in order to refresh its signal store.
|
||||
|
||||
## Renderer architecture
|
||||
|
||||
- **Downloads service** (`apps/web/src/app/services/downloads.service.ts`)
|
||||
Signals back the current download list while `hasDownloads` and `isAvailable` gates UI rendering. Before each download the service resolves a download folder (stored in `SettingsStore` or fetched via `downloadsGetDefaultFolder`) 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 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`.
|
||||
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.
|
||||
@@ -33,9 +41,12 @@ The download manager is a desktop-only feature that layers a curated queue, prog
|
||||
|
||||
## Queuing, persistence, and UX notes
|
||||
|
||||
- Every download row writes to the shared `downloads` table with statuses (`queued`, `downloading`, `completed`, `failed`, `canceled`) plus metadata such as `bytesDownloaded`, `totalBytes`, `errorMessage`, and Xtream identifiers. Stale downloads reset to `failed` on startup.
|
||||
- Queue cancellation removes the task or calls `downloadItem.cancel()` if the item is active; retries reuse the same database entry, preventing duplicate rows.
|
||||
- Folder selection first checks stored preferences, falls back to the OS default downloads path, and finally prompts the user to pick a folder. The downloads service persists the chosen path via `SettingsStore`.
|
||||
- Every download row writes to the shared `downloads` table with statuses (`queued`, `downloading`, `completed`, `failed`, `canceled`) plus metadata such as `bytesDownloaded`, `totalBytes`, `errorMessage`, and Xtream identifiers. On startup, `download-recovery.ts` deletes persisted partial reservations before stale queued/downloading rows become `failed`.
|
||||
- Queue cancellation removes a queued task or records an active cancellation request and calls `downloadItem.cancel()` when the item is available; retries reuse the same database entry, preventing duplicate rows.
|
||||
- The OS downloads path is always authorized. A custom folder becomes
|
||||
authorized only after native folder selection, and the main process persists
|
||||
that selection under Electron `userData`. Renderer settings may display the
|
||||
path, but they are not trusted as authorization.
|
||||
- The new UI leverages CSS variables for theme-specific backgrounds/borders, ensures `.downloads__list` can scroll inside its panel, and brings consistent badge/typography treatments to each card.
|
||||
|
||||
Keeping the backend queue, IPC handlers, shared schema, and renderer signals synchronized minimizes drift between platform rules and the UI. Future work might cover download list filters, cancel-all actions, or integration with upcoming playback analytics.
|
||||
@@ -103,3 +103,55 @@ Rules:
|
||||
When changing this flow, keep stale header cleanup covered. Switching from a
|
||||
channel or playlist with custom headers to one without custom headers must clear
|
||||
the previous override.
|
||||
|
||||
## Main-Process Remote Requests
|
||||
|
||||
Renderer-triggered HTTP requests must pass through the URL policy in
|
||||
`apps/electron-backend/src/app/events/url-safety.ts`. The policy rejects
|
||||
non-HTTP(S) URLs and embedded credentials, and strict callers also reject
|
||||
loopback, private, reserved, and DNS-resolved private addresses. IPv4-mapped
|
||||
IPv6 literals are decoded before classification, including hexadecimal forms
|
||||
such as `::ffff:7f00:1`, so alternate IPv6 spelling cannot bypass IPv4 rules.
|
||||
|
||||
Remote request callers must use the validated Axios redirect helper so every
|
||||
redirect target is checked before the main process follows it. Under the strict
|
||||
policy, the helper pins the socket lookup to the IP addresses that passed
|
||||
validation while retaining the original hostname for TLS SNI, certificate
|
||||
validation, and virtual hosting. This prevents DNS rebinding between validation
|
||||
and connection. Callers with custom TLS policy provide a typed agent factory;
|
||||
the validated request layer supplies the pinned lookup instead of copying
|
||||
private Node `Agent.options` state. Cross-origin redirects must not forward `Authorization`,
|
||||
`Cookie`, `Proxy-Authorization`, Axios `params`, or request bodies.
|
||||
|
||||
EPG URLs are strict by default because an M3U playlist can supply them through
|
||||
`url-tvg`. Operators who intentionally use a LAN-hosted EPG source should prefer
|
||||
the renderer's source-scoped “Allow source” action, which persists the exact EPG
|
||||
URL in settings and retries that source only. The
|
||||
`IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS=1` environment flag remains an
|
||||
emergency/development process-wide override for strict EPG fetches. Directly
|
||||
configured Xtream, Stalker, and playlist providers retain private-network
|
||||
support, but still require HTTP(S), reject embedded credentials, and validate
|
||||
redirects.
|
||||
|
||||
Remote playlist TLS certificates are validated by default. The
|
||||
renderer can persist a host-scoped invalid-certificate trust decision for a
|
||||
playlist or EPG source host. The `IPTVNATOR_ALLOW_INSECURE_TLS=1` escape hatch
|
||||
is only for explicitly trusted providers with invalid or self-signed
|
||||
certificates when the host-scoped UI path is not available.
|
||||
|
||||
## Filesystem Capabilities
|
||||
|
||||
Renderer IPC payloads are not filesystem authorization.
|
||||
|
||||
- `write-file` accepts only a path returned to the same renderer by the native
|
||||
save dialog. The capability is single-use and is consumed before the write,
|
||||
including when the filesystem operation fails.
|
||||
- Download folders are owned by the Electron main process. The OS downloads
|
||||
directory is always allowed; a custom directory is accepted only after the
|
||||
native folder dialog selects it.
|
||||
- The selected download directory is persisted under Electron `userData` and
|
||||
returned by `DOWNLOADS_GET_DEFAULT_FOLDER`, so renderer-managed settings
|
||||
cannot substitute an arbitrary host path.
|
||||
- Downloads do not overwrite an existing destination file.
|
||||
- Reveal and playback handlers accept only file paths recorded in IPTVnator's
|
||||
downloads database.
|
||||
@@ -143,6 +143,33 @@ The click path must continue through each detail host's normal episode playback
|
||||
method so recent-item updates, inline/external player selection, resume offsets,
|
||||
and playback-position saving keep the same behavior as manual episode clicks.
|
||||
|
||||
## Series Inline Episode Continuation
|
||||
|
||||
Xtream and Stalker series detail views own current-episode state and pass a
|
||||
`SeriesPlaybackNavigation` payload through `PortalInlinePlayerComponent` and
|
||||
`WebPlayerViewComponent` to the active embedded player.
|
||||
|
||||
Current contract:
|
||||
|
||||
- Video.js, HTML5, ArtPlayer, and embedded MPV emit `playbackEnded` only for
|
||||
real media EOF/`ended` events.
|
||||
- Teardown, replacement, manual close, player reload, idle state, and playback
|
||||
errors must not emit `playbackEnded`.
|
||||
- Series previous/next controls render only when the series navigation payload is
|
||||
present. Movies, live streams, radio streams, and non-series VOD must not show
|
||||
those buttons.
|
||||
- The shared navigation payload contains `canPrevious`, `canNext`, and
|
||||
`autoplayEnabled`. Player controls disable previous on the first episode and
|
||||
next on the last episode of the current season.
|
||||
- Autoplay is enabled by default for series playback. On `playbackEnded`, the
|
||||
portal detail host starts the next episode only when `canNext` is true for the
|
||||
current season.
|
||||
- Autoplay and Next never cross season boundaries or lazy-load another season.
|
||||
Quick start remains the flow that may choose an episode from another loaded or
|
||||
newly loaded season before playback begins.
|
||||
- Previous switches directly to the previous episode in the current season. It
|
||||
does not implement a restart-threshold behavior.
|
||||
|
||||
## Codec And Container Diagnostics
|
||||
|
||||
The shared `WebPlayerViewComponent` is the central browser-player viewport for M3U, Xtream, and Stalker inline playback, including live streams opened from favorites and recently viewed collections. Video.js, HTML5, and ArtPlayer report native media errors, HLS.js errors, mpegts.js errors, and HLS manifest codec metadata into the shared diagnostics classifier.
|
||||
|
||||
@@ -101,11 +101,14 @@ The renderer learns which features the loaded addon binary supports through the
|
||||
|
||||
- `ended` when the end-file reason is `MPV_END_FILE_REASON_EOF`
|
||||
- `error` when MPV reports an end-file error
|
||||
- `loading` when MPV reports `MPV_END_FILE_REASON_REDIRECT`, because playback continues with the redirected playlist contents
|
||||
- `idle` for other successful end-file reasons such as replacement/stop
|
||||
- `closed` only for dispose/manual teardown
|
||||
|
||||
Renderer autoplay must use `ended` only. It must not treat `closed`, `idle`, or `error` as a request to continue to the next episode.
|
||||
|
||||
The native addon also observes mpv's `eof-reached` property and maps a true value to `ended`. This is required because embedded sessions run with `keep-open=yes`; MPV can pause at EOF while keeping the file loaded, so relying only on `MPV_EVENT_END_FILE` can leave the renderer in a paused-at-end state and block series autoplay.
|
||||
|
||||
Series episode navigation is owned by the portal feature components and passed through the shared inline player to `EmbeddedMpvPlayerComponent`. The embedded MPV controls show `skip_previous` and `skip_next` buttons only for non-live series playback. The shared navigation payload contains `canPrevious`, `canNext`, and `autoplayEnabled`; the component disables previous/next at the current-season boundaries and guards the output handlers as well as the button disabled state.
|
||||
|
||||
Autoplay is enabled by default for series playback in embedded MPV. On `ended`, Xtream and Stalker series detail views start the next episode only when the current episode has a next item in the same season. Playback stops on the last episode of the current season. Previous always switches to the previous episode in the current season; it does not implement a restart-threshold behavior.
|
||||
|
||||
@@ -184,8 +184,8 @@ Series inline playback behavior is shared across all three modes:
|
||||
|
||||
- `StalkerSeriesViewComponent` maps every mode into `mappedSeasons()` and derives the currently playing episode from `inlinePlayback.contentInfo.contentXtreamId`.
|
||||
- The inline player header shows the current episode metadata below the title, for example `S01E03 - Episode title`.
|
||||
- When experimental embedded MPV is active, the player receives previous/next episode state for the current season only.
|
||||
- Embedded MPV autoplay is enabled by default. On MPV EOF (`ended`), Stalker starts the next episode only when it already exists in the current season's mapped episode list.
|
||||
- Embedded players receive previous/next episode state for the current season only.
|
||||
- Inline series autoplay is enabled by default. On player EOF (`ended`), Stalker starts the next episode only when it already exists in the current season's mapped episode list.
|
||||
- Autoplay and Next stop at the last episode of the current season. They do not jump to the next season and do not lazy-load an unloaded `is_series=1` season. Quick start remains the only flow that may load another VOD-series season before playback.
|
||||
- Previous is disabled on the first episode of the current season and otherwise switches directly to the previous episode.
|
||||
|
||||
|
||||
@@ -74,20 +74,20 @@ Provider route integration:
|
||||
The shell is intentionally split into four persistent regions:
|
||||
|
||||
1. Left rail:
|
||||
1. Static workspace links for dashboard, sources, global favorites, and recently viewed.
|
||||
2. Provider-aware context links derived from the active or current playlist.
|
||||
3. Settings remains a persistent footer shortcut in the rail.
|
||||
1. Static workspace links for dashboard, sources, global favorites, and recently viewed.
|
||||
2. Provider-aware context links derived from the active or current playlist.
|
||||
3. Settings remains a persistent footer shortcut in the rail.
|
||||
2. Top header:
|
||||
1. Playlist switcher.
|
||||
2. Route-aware search input and command palette trigger.
|
||||
3. Add source action.
|
||||
4. Optional playlist refresh and route-specific shortcut actions.
|
||||
5. Downloads shortcut in Electron.
|
||||
1. Playlist switcher.
|
||||
2. Route-aware search input and command palette trigger.
|
||||
3. Add source action.
|
||||
4. Optional playlist refresh and route-specific shortcut actions.
|
||||
5. Downloads shortcut in Electron.
|
||||
3. Main body:
|
||||
1. Optional left context panel.
|
||||
2. Main router outlet content.
|
||||
1. Optional left context panel.
|
||||
2. Main router outlet content.
|
||||
4. Optional footer:
|
||||
1. External playback session bar when a docked session is visible.
|
||||
1. External playback session bar when a docked session is visible.
|
||||
|
||||
`WorkspaceShellComponent` binds only to `WorkspaceShellFacade`. The facade is
|
||||
kept as a thin template-facing API and delegates ownership to component-scoped
|
||||
@@ -116,15 +116,15 @@ for the template unless the template contract itself intentionally changes.
|
||||
The shell decides which secondary panel to show from the current route:
|
||||
|
||||
1. `/workspace/sources`
|
||||
1. `WorkspaceSourcesFiltersPanelComponent`
|
||||
1. `WorkspaceSourcesFiltersPanelComponent`
|
||||
2. Xtream category sections (`live`, `vod`, `series`)
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
3. Stalker category sections (`itv`, `radio`, `vod`, `series`)
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
4. `/workspace/settings`
|
||||
1. `WorkspaceSettingsContextPanelComponent`
|
||||
1. `WorkspaceSettingsContextPanelComponent`
|
||||
5. Downloads sections
|
||||
1. `WorkspaceCollectionContextPanelComponent`
|
||||
1. `WorkspaceCollectionContextPanelComponent`
|
||||
|
||||
The context panel is part of the shell contract. New workspace-level routes
|
||||
should explicitly decide whether they need one rather than adding local
|
||||
@@ -197,6 +197,91 @@ Keyboard shortcut help is shell-owned:
|
||||
added. Do not include native browser/editor behavior such as `Tab` or
|
||||
platform text editing shortcuts.
|
||||
|
||||
## Window Chrome And Custom Title Bar
|
||||
|
||||
The Electron window hides the native title bar on all desktop platforms
|
||||
(`titleBarStyle: 'hidden'` in `apps/electron-backend/src/app/app.ts`):
|
||||
|
||||
1. macOS keeps the native traffic lights (`titleBarOverlay: true`,
|
||||
`trafficLightPosition`); the renderer draws no window buttons.
|
||||
2. Windows and Linux use renderer-drawn window controls
|
||||
(`app-window-controls`, `libs/ui/components/src/lib/window-controls/`).
|
||||
`frame` is intentionally left untouched so native resize borders and
|
||||
window snapping keep working.
|
||||
|
||||
The controls are mounted once in `app-root` (not inside the workspace
|
||||
header) as a `position: fixed` top-right overlay so they stay clickable
|
||||
above full-window content such as the multi-EPG cdk overlay and Material
|
||||
dialog backdrops — the same behavior as the macOS traffic lights. Because
|
||||
CDK overlays render as popovers in the browser top layer (above any
|
||||
z-index), the component host is itself a `popover="manual"` element: it
|
||||
enters the top layer on init and re-enters it (hide + show) whenever
|
||||
another popover opens, so the controls always paint last. The
|
||||
`z-index: 10000` remains only as a fallback when the popover API is
|
||||
unavailable. They render only when
|
||||
`RuntimeCapabilitiesService.usesCustomWindowControls` is true (Windows/Linux
|
||||
Electron with the window-control bridge methods available); the PWA and
|
||||
macOS never mount them.
|
||||
|
||||
IPC contract (constants in `libs/shared/interfaces/src/lib/ipc-commands.ts`,
|
||||
handlers in `apps/electron-backend/src/app/events/window.events.ts`):
|
||||
|
||||
1. `WINDOW:MINIMIZE`, `WINDOW:TOGGLE_MAXIMIZE`, `WINDOW:CLOSE`,
|
||||
`WINDOW:GET_STATE` are `ipcMain.handle` channels resolved from the sender
|
||||
WebContents. Close goes through `win.close()` so the existing
|
||||
window-bounds persistence in `app.ts` still runs.
|
||||
2. `WINDOW:STATE_CHANGED` is pushed main → renderer on
|
||||
maximize/unmaximize/enter-full-screen/leave-full-screen so the
|
||||
maximize/restore glyph stays correct for externally triggered changes
|
||||
(double-click on a drag region, OS snap, F11). The controls hide
|
||||
themselves while the window is fullscreen.
|
||||
|
||||
Layout integration:
|
||||
|
||||
1. `document.body` gets a `frameless-platform` class (set in
|
||||
`AppComponent`, same mechanism as `dark-theme`) — body-level so rules
|
||||
also reach cdk-overlay content rendered outside `app-root`.
|
||||
2. `apps/web/src/styles.scss` reserves `padding-right: 150px` in
|
||||
top-aligned drag regions (`.workspace-header`, multi-EPG
|
||||
`#epg-navigation`) for the 3 × 46px button strip.
|
||||
3. Button colors follow the theme via CSS variables (`--app-on-surface`,
|
||||
`--app-hover-overlay`); the close button uses the Windows-style red
|
||||
hover (`#e81123`). No theme IPC is involved.
|
||||
|
||||
Window decorations on Linux (shadows, corners):
|
||||
|
||||
1. Hiding the title bar removes the window manager's decorations, so the
|
||||
shadow/rounded corners must come from client-side decorations (CSD).
|
||||
Electron only draws CSD on native Wayland, and frameless-window CSD
|
||||
(GTK drop shadow + extended resize boundaries, `hasShadow: true` by
|
||||
default) requires **Electron >= 41** — the reason the dependency was
|
||||
bumped from 39. Electron picks Wayland automatically on Wayland
|
||||
sessions since 38.2.
|
||||
2. On X11 sessions frameless windows stay undecorated (square, no
|
||||
shadow) — an upstream platform limitation shared by e.g. VS Code.
|
||||
3. Rounded corners for frameless Linux windows are not yet supported by
|
||||
Electron (tracked upstream as planned work); Windows 11 keeps its DWM
|
||||
rounded corners and shadow because the standard frame is retained.
|
||||
|
||||
Toolchain notes for the Electron 41 upgrade:
|
||||
|
||||
1. `better-sqlite3` is pinned to exactly `12.9.0` — the last release that
|
||||
ships prebuilt binaries for BOTH Node 20 (ABI 115, used by Jest) and
|
||||
Electron 41 (ABI 145, used at runtime). `12.10.0` dropped the Node 20
|
||||
prebuilds, which forces a from-source build that fails on machines
|
||||
without a C++ toolchain.
|
||||
2. The pnpm override `node-abi@3.85.0 -> 3.92.0` is required so
|
||||
`@electron/rebuild` (via `electron-builder install-app-deps`) can map
|
||||
Electron 41 to its ABI.
|
||||
|
||||
Known caveats:
|
||||
|
||||
1. DIY buttons cannot show the Windows 11 Snap Layouts flyout (only native
|
||||
caption buttons or the Window Controls Overlay get that).
|
||||
2. Double-click-to-maximize on drag regions is handled natively by
|
||||
Electron/Chromium; on Linux the exact behavior depends on the window
|
||||
manager.
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
Use this document as the source of truth when changing workspace shell behavior.
|
||||
|
||||
Reference in new issue
Block a user