diff --git a/.changes/dashboard-live-epg-timestamp-units.md b/.changes/dashboard-live-epg-timestamp-units.md deleted file mode 100644 index a5b267810..000000000 --- a/.changes/dashboard-live-epg-timestamp-units.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -type: internal -area: dashboard ---- - -The dashboard's live "now playing" time range and progress bar now read -pre-computed EPG timestamps as unix seconds, matching the rest of the EPG -code. Today's dashboard lookups never carry those fields, so nothing changes -on screen; this closes the gap before a future data path supplies them. diff --git a/.changes/database-catalog-write-batches.md b/.changes/database-catalog-write-batches.md deleted file mode 100644 index ce695f522..000000000 --- a/.changes/database-catalog-write-batches.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: perf -area: database -issues: [1292] ---- - -Refreshing or removing a large Xtream playlist is several times faster: the -"Removing cached content" stage and the re-import now commit around 5,000 -rows at a time instead of 100, and progress updates arrive at most ten times a -second instead of once per batch. diff --git a/.changes/database-skipped-release-upgrades.md b/.changes/database-skipped-release-upgrades.md deleted file mode 100644 index 2e972dc95..000000000 --- a/.changes/database-skipped-release-upgrades.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: database -issues: [1580] ---- - -Updating directly from older versions no longer blocks playlist loading with a missing database column error. Existing sources, favorites, and playback history are preserved. diff --git a/.changes/deps-transitive-cve-overrides.md b/.changes/deps-transitive-cve-overrides.md deleted file mode 100644 index 4a1234242..000000000 --- a/.changes/deps-transitive-cve-overrides.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: internal -area: deps ---- - -Closes the four open Dependabot alerts on transitive npm dependencies via -pinned pnpm overrides: browserslist (crash on untrusted stats), @xmldom/xmldom -(XML fragment injection — its existing override target had itself fallen into -the advisory range), @humanfs/node (recursive copy follows symlinks out of the -tree) and postcss-selector-parser (AST recursion DoS). diff --git a/.changes/downloads-xtream-archive.md b/.changes/downloads-xtream-archive.md deleted file mode 100644 index 3d0c4b0c2..000000000 --- a/.changes/downloads-xtream-archive.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: downloads ---- - -Download completed Xtream catch-up programmes as TS files from Live TV programme details. Find them in Downloads with the channel and broadcast date and play them offline. Interrupted archive downloads restart from the beginning. diff --git a/.changes/embedded-mpv-extra-options-reconnect.md b/.changes/embedded-mpv-extra-options-reconnect.md deleted file mode 100644 index 8e5d60152..000000000 --- a/.changes/embedded-mpv-extra-options-reconnect.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -type: feature -area: embedded-mpv -highlight: Embedded MPV reconnects dropped streams ---- - -The embedded MPV player now reloads a live stream that drops mid-playback on -its own, with increasing delays and an "attempt N of 6" line instead of a -dead error screen; a new Settings > Playback toggle turns this off. The same -section gains an advanced field for extra libmpv options (one key=value per -line) that applies on every engine, with a short network timeout on by default. diff --git a/.changes/embedded-mpv-no-ytdl-hook.md b/.changes/embedded-mpv-no-ytdl-hook.md deleted file mode 100644 index 273e28c7d..000000000 --- a/.changes/embedded-mpv-no-ytdl-hook.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: fix -area: embedded-mpv ---- - -When a stream refuses the connection, the embedded MPV player (frame-copy -engine) no longer hands the URL to yt-dlp before giving up: the failure -shows up right away and its error no longer reads "youtube-dl failed: -unexpected error occurred", matching the native-view engines and the -external MPV player. diff --git a/.changes/epg-copy-archive-url.md b/.changes/epg-copy-archive-url.md deleted file mode 100644 index 36ef5fd2f..000000000 --- a/.changes/epg-copy-archive-url.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: epg ---- - -Copy a catch-up programme’s URL from its EPG details in Xtream and supported M3U sources, including Favorites and Recent, without interrupting playback. The link can be used in an external player or download tool. diff --git a/.changes/epg-display-offset.md b/.changes/epg-display-offset.md deleted file mode 100644 index f90a8987a..000000000 --- a/.changes/epg-display-offset.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -area: epg -issues: [50] ---- - -EPG settings now include a display-only time offset, making it easy to correct provider feeds with missing or incorrect timezone information without re-importing the guide. diff --git a/.changes/epg-double-gzip.md b/.changes/epg-double-gzip.md deleted file mode 100644 index 51f35e458..000000000 --- a/.changes/epg-double-gzip.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: epg -issues: [1586] ---- - -EPG guides now load when a server adds gzip compression to an already compressed XMLTV file. Sources using a single gzip layer continue to work. diff --git a/.changes/epg-programme-guide.md b/.changes/epg-programme-guide.md deleted file mode 100644 index 03da9d351..000000000 --- a/.changes/epg-programme-guide.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -type: feature -area: epg -issues: [171] -highlight: Programme guide, rebuilt ---- - -The programme guide now shows your playlist's own channels in their order, -with the current group or favorites one click away. Click a channel to -switch playback while the player stays on screen above the grid; -double-click switches and closes. Open it from the new Guide button, the -header, the command palette, or the G key. Hide channels without EPG and -pick comfortable or compact rows. diff --git a/.changes/epg-removed-source-cache.md b/.changes/epg-removed-source-cache.md deleted file mode 100644 index b2f7bd281..000000000 --- a/.changes/epg-removed-source-cache.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: epg ---- - -Removing and saving an EPG source now clears its cached programmes, including data left by previously removed sources on restart. Other configured sources and playlist guides are preserved, and Live TV stops showing programmes from the removed source. diff --git a/.changes/epg-timeline-compact-controls.md b/.changes/epg-timeline-compact-controls.md deleted file mode 100644 index 6acf207c3..000000000 --- a/.changes/epg-timeline-compact-controls.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: epg ---- - -The EPG timeline toolbar is more compact: "Now" is an icon button, and the zoom slider is replaced by a single button that cycles day overview → by hour → detailed. Ctrl/⌘ + scroll (or a trackpad pinch) over the timeline zooms smoothly around the cursor. The channel and programme title now use all the space the controls free up. diff --git a/.changes/host-health-live-trial.md b/.changes/host-health-live-trial.md deleted file mode 100644 index 482435dff..000000000 --- a/.changes/host-health-live-trial.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: host-health -issues: [1439] ---- - -Portal recovery now waits for an active probe to finish before sending another request, even when redirects or a slow response take longer than 45 seconds. Completed and cancelled requests release the probe slot reliably. diff --git a/.changes/live-tv-hidden-channel-list.md b/.changes/live-tv-hidden-channel-list.md deleted file mode 100644 index 108e7fee0..000000000 --- a/.changes/live-tv-hidden-channel-list.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: live-tv -issues: [1458] ---- - -A hidden channel list no longer looks like a playlist that lost its channels: the player now says the list is hidden and offers a "Show channels list" button, a toggle in the workspace header stays in place in both states, and hiding the list in one place (M3U player, portal Live TV, favorites/recent) no longer hides it everywhere else. diff --git a/.changes/m3u-clearkey-base64.md b/.changes/m3u-clearkey-base64.md deleted file mode 100644 index 245c4f153..000000000 --- a/.changes/m3u-clearkey-base64.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: m3u ---- - -ClearKey channels with ordinary Base64 keys now play when imported or refreshed, including JSON playlist exports that use + and / characters. Refresh an existing playlist once to apply the fix. diff --git a/.changes/m3u-collection-drm.md b/.changes/m3u-collection-drm.md deleted file mode 100644 index 76318a744..000000000 --- a/.changes/m3u-collection-drm.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: m3u -issues: [1590] ---- - -DASH channels, including ClearKey-protected streams, now play from Recently Viewed and Favorites using the compatible built-in player. Existing imports keep their DRM keys without needing to re-import the playlist. diff --git a/.changes/m3u-url-user-agent.md b/.changes/m3u-url-user-agent.md deleted file mode 100644 index bcc5e04b1..000000000 --- a/.changes/m3u-url-user-agent.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: m3u -issues: [465, 1120] ---- - -M3U URL imports now accept a custom User-Agent for providers that require it before downloading the playlist. The value is saved and reused for playlist refreshes in the desktop app and self-hosted web app. diff --git a/.changes/m3u-vod-playback-mode.md b/.changes/m3u-vod-playback-mode.md deleted file mode 100644 index 3bfe2dd46..000000000 --- a/.changes/m3u-vod-playback-mode.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: m3u ---- - -Movies and episodes recognized as video files in M3U playlists now offer playback time and seeking in built-in players, even when TMDB or movie details are disabled. Seeking remains dependent on the source's support. diff --git a/.changes/migration-legacy-electron-profiles.md b/.changes/migration-legacy-electron-profiles.md deleted file mode 100644 index d8d3b00dc..000000000 --- a/.changes/migration-legacy-electron-profiles.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: migration -issues: [1504] ---- - -Upgrades from older desktop versions retain all sources from the legacy profile. Already-upgraded users can choose to recover missing sources without replacing current sources or settings. Migration keeps the original data and retries safely after a failed write. diff --git a/.changes/migration-startup-feedback.md b/.changes/migration-startup-feedback.md deleted file mode 100644 index 97abad880..000000000 --- a/.changes/migration-startup-feedback.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: migration ---- - -Startup now shows a theme-aware preparation screen while loading sources and checking a large programme guide. Temporary source-read failures retry automatically; if loading still fails, you can retry without restarting the app. Recovery also completes any previously failed programme-guide cleanup before opening the library. diff --git a/.changes/packaging-appmanager-metadata.md b/.changes/packaging-appmanager-metadata.md deleted file mode 100644 index bd052761a..000000000 --- a/.changes/packaging-appmanager-metadata.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: packaging ---- - -AppImage builds now include an update source for AppManager, allowing it to discover new GitHub releases and download full application updates without manually configuring the repository. diff --git a/.changes/playback-controls-hide-after-button-click.md b/.changes/playback-controls-hide-after-button-click.md deleted file mode 100644 index 39a6cc15b..000000000 --- a/.changes/playback-controls-hide-after-button-click.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: fix -area: playback ---- - -The built-in player controls now hide on their own after you click a button -in the bar with the mouse, such as fullscreen or mute. Previously the clicked -button kept the bar pinned on screen until you clicked the video, which also -paused it. Applies to the shared controls in the web players and the -Embedded MPV frame-copy engine. diff --git a/.changes/playback-diagnostic-details.md b/.changes/playback-diagnostic-details.md deleted file mode 100644 index 9be70d132..000000000 --- a/.changes/playback-diagnostic-details.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: playback ---- - -Playback errors now explain confirmed access and DRM license failures, with stream codecs, DRM systems and failure stages in the details. Copy diagnostics creates a support report without stream URLs or credentials. No extra requests are made to your provider. diff --git a/.changes/playback-embedded-mpv-seek-steps.md b/.changes/playback-embedded-mpv-seek-steps.md deleted file mode 100644 index d8dd4bb7e..000000000 --- a/.changes/playback-embedded-mpv-seek-steps.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: fix -area: playback ---- - -Arrow keys and the ±10 s buttons in the Embedded MPV player now move by their -full step every time. Pressing an arrow repeatedly, or holding it, used to -advance only about a second per press because each step was computed from a -stale position; steps are now relative seeks executed by mpv itself, so rapid -presses add up. diff --git a/.changes/playback-fullscreen-channel-panel.md b/.changes/playback-fullscreen-channel-panel.md deleted file mode 100644 index 4ab8066fc..000000000 --- a/.changes/playback-fullscreen-channel-panel.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -type: feature -area: playback -issues: [48] -highlight: Channel list in fullscreen ---- - -Rest the mouse on the left edge (tap it on touch) or press C while a live -channel plays fullscreen: the channel list (M3U all/groups/favorites/recent, -Xtream, Stalker, global favorites) slides over the video with search, so you -can zap without leaving fullscreen. Nothing covers the video while it is -closed. M3U playlists also zap with PageUp/PageDown. diff --git a/.changes/playback-fullscreen-panel-hover.md b/.changes/playback-fullscreen-panel-hover.md deleted file mode 100644 index 36b9c34f1..000000000 --- a/.changes/playback-fullscreen-panel-hover.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: playback ---- - -The fullscreen channel panel stays open under the pointer when its opening animation is delayed, instead of closing before the list appears. diff --git a/.changes/playback-fullscreen-survives-episode-switch.md b/.changes/playback-fullscreen-survives-episode-switch.md deleted file mode 100644 index 861bf88db..000000000 --- a/.changes/playback-fullscreen-survives-episode-switch.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: fix -area: playback ---- - -With the default shared player controls, the built-in player now stays in -fullscreen when you switch to another episode, when the next episode starts -automatically, and when a live channel or an alternative movie source is -switched — previously every switch dropped back to the page. The legacy -vendor-controls opt-out keeps its previous behavior. diff --git a/.changes/playback-legacy-pip-teardown.md b/.changes/playback-legacy-pip-teardown.md deleted file mode 100644 index a0ef28472..000000000 --- a/.changes/playback-legacy-pip-teardown.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: playback ---- - -Switching channels or leaving playback now closes the old picture-in-picture window in HTML5, Video.js, and ArtPlayer even when shared player controls are disabled, preventing frozen or outdated video from remaining on top. This includes Safari’s legacy picture-in-picture mode. diff --git a/.changes/playback-light-theme-panels.md b/.changes/playback-light-theme-panels.md deleted file mode 100644 index 908c75598..000000000 --- a/.changes/playback-light-theme-panels.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: playback -issues: [1530] ---- - -Embedded MPV controls and programme-guide panels now follow the visual theme and stay readable in light and dark mode. Video overlays retain their contrasting dark backgrounds. diff --git a/.changes/playback-native-container-routing.md b/.changes/playback-native-container-routing.md deleted file mode 100644 index a70f3312a..000000000 --- a/.changes/playback-native-container-routing.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -type: fix -area: playback ---- - -The built-in HTML5 player now plays `.mkv`, `.webm`, `.avi`, `.mov` and other -non-HLS video files directly instead of handing them to the HLS engine, which -failed with a "network or provider loading error" on many Xtream episodes and -movies. ArtPlayer and the HTML5 player now choose their engine by the same rule. diff --git a/.changes/playback-shortcuts-after-button-click.md b/.changes/playback-shortcuts-after-button-click.md deleted file mode 100644 index 0067f1a7a..000000000 --- a/.changes/playback-shortcuts-after-button-click.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: fix -area: playback ---- - -Keyboard shortcuts work again right after you click a button in the built-in -player controls. Previously the clicked button kept the keyboard: after -clicking fullscreen, Space left fullscreen instead of pausing, and the seek, -volume and mute keys did nothing until you clicked the video. Applies to the -shared controls in the web players and the Embedded MPV frame-copy engine. diff --git a/.changes/playback-videojs-shortcuts-after-button-click.md b/.changes/playback-videojs-shortcuts-after-button-click.md deleted file mode 100644 index 6a9e38625..000000000 --- a/.changes/playback-videojs-shortcuts-after-button-click.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: fix -area: playback ---- - -With IPTVnator's shared controls turned off, the Video.js player's keyboard -shortcuts now work again right after you click a button in its control bar. -Previously the clicked button kept the keyboard: after clicking fullscreen, -Space left fullscreen instead of pausing, and the seek, volume and mute keys -did nothing until you clicked the video. diff --git a/.changes/playback-vlc-windows-console.md b/.changes/playback-vlc-windows-console.md deleted file mode 100644 index c29a9b86a..000000000 --- a/.changes/playback-vlc-windows-console.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: playback -issues: [871] ---- - -VLC now opens without an extra console window on Windows when playback progress tracking or Reuse VLC instance is enabled. diff --git a/.changes/player-stream-info-popover.md b/.changes/player-stream-info-popover.md deleted file mode 100644 index 9c1ed8b46..000000000 --- a/.changes/player-stream-info-popover.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -type: feature -area: playback -highlight: See what a stream really is while it plays ---- - -The player overlay now shows stream information: resolution, playback and source -frame rates, stream and codec bitrates, audio details, buffer and dropped frames. -Available in built-in web players and experimental frame-copy Embedded MPV. -Unknown or unavailable values stay hidden; measured frame rates reflect dropped frames and -stalls. diff --git a/.changes/playlist-inactive-source-cleanup.md b/.changes/playlist-inactive-source-cleanup.md deleted file mode 100644 index 0c1703673..000000000 --- a/.changes/playlist-inactive-source-cleanup.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: playlist ---- - -Desktop users can review inactive sources and delete selected ones together. The dialog checks the whole library, preselects confirmed expired or disabled accounts, and lets you keep individual sources. Busy sources are skipped, and deletion results are reported individually. diff --git a/.changes/playlist-network-source-status.md b/.changes/playlist-network-source-status.md deleted file mode 100644 index d5efc01db..000000000 --- a/.changes/playlist-network-source-status.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: playlist ---- - -Desktop playlist lists now show availability for Stalker portals and M3U links alongside Xtream accounts. Checks share cached results, run in the background, and explain when a source could not be verified. diff --git a/.changes/playlists-refresh-catalog-read.md b/.changes/playlists-refresh-catalog-read.md deleted file mode 100644 index aaa852144..000000000 --- a/.changes/playlists-refresh-catalog-read.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: playlists ---- - -Opening an M3U playlist immediately after refreshing now waits for its pending save, so the channel list does not revert to the previous catalog. diff --git a/.changes/portals-concurrent-connectivity-failures.md b/.changes/portals-concurrent-connectivity-failures.md deleted file mode 100644 index 8b4324169..000000000 --- a/.changes/portals-concurrent-connectivity-failures.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: portals -issues: [1438] ---- - -Xtream and Stalker portals no longer enter a connection cooldown merely because several parallel requests fail in the same millisecond. This fixes false unavailability in both the desktop app and the self-hosted web version. diff --git a/.changes/portals-connectivity-setting.md b/.changes/portals-connectivity-setting.md deleted file mode 100644 index 65acde25c..000000000 --- a/.changes/portals-connectivity-setting.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: feature -area: portals ---- - -Desktop settings now let you disable the temporary pause after repeated Xtream or Stalker connection failures, without restarting. Account info explains when requests are paused and offers an immediate retry. diff --git a/.changes/portals-live-channel-return.md b/.changes/portals-live-channel-return.md deleted file mode 100644 index a5e093a3e..000000000 --- a/.changes/portals-live-channel-return.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: portals -issues: [1520] ---- - -Xtream and Stalker keep remote channel order while you browse other categories or search. Use Show playing channel to return to the current channel without restarting playback. Stalker radio supports the same behavior. diff --git a/.changes/portals-live-panel-collapse-levels.md b/.changes/portals-live-panel-collapse-levels.md deleted file mode 100644 index bec26b05d..000000000 --- a/.changes/portals-live-panel-collapse-levels.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: feature -area: portals ---- - -Live TV panels now fold in steps: hide only the categories rail and keep the -channel list, or hide both for a player-only view. With categories hidden, the -list header becomes a category dropdown, so switching categories stays one -click away, and hiding both panels then bringing them back returns to the -level you had before. diff --git a/.changes/shell-startup-window-mode.md b/.changes/shell-startup-window-mode.md deleted file mode 100644 index fbe45baa0..000000000 --- a/.changes/shell-startup-window-mode.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -type: feature -area: shell -issues: [1455] ---- - -Settings → General has a new "Window on startup" option: open the desktop app -at its last size, maximized, or fullscreen — handy on a TV or HTPC. Starting -with `--fullscreen` forces a single fullscreen launch without changing the -setting, and F11 now toggles fullscreen anywhere in the app. diff --git a/.changes/stalker-category-search.md b/.changes/stalker-category-search.md deleted file mode 100644 index a0a52e132..000000000 --- a/.changes/stalker-category-search.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: stalker -issues: [1543] ---- - -Stalker Live TV search stays within the selected category in the sidebar and fullscreen panel, including channels beyond the first page. All Items searches the whole catalog, and the two search fields work independently. diff --git a/.changes/stalker-keep-live-playback-on-category-switch.md b/.changes/stalker-keep-live-playback-on-category-switch.md deleted file mode 100644 index a6ae14ae9..000000000 --- a/.changes/stalker-keep-live-playback-on-category-switch.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -type: fix -area: stalker ---- - -In Stalker portals, switching the Live TV or radio category in the sidebar no -longer stops the channel that is playing. The category only changes which -channels are listed, matching how Xtream portals and M3U playlists already -behave. diff --git a/.changes/stalker-season-marker-display.md b/.changes/stalker-season-marker-display.md deleted file mode 100644 index 0b0a1f4ce..000000000 --- a/.changes/stalker-season-marker-display.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: stalker ---- - -Stalker series with season markers such as “s02” or “2 сезон” now show the correct season in tabs and episode labels while preserving watch progress. TMDB updates no longer reset loaded episodes, and delayed portal responses cannot mix episodes from different series. diff --git a/.changes/stalker-seek-byte-ranges.md b/.changes/stalker-seek-byte-ranges.md deleted file mode 100644 index f95b140ee..000000000 --- a/.changes/stalker-seek-byte-ranges.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: stalker ---- - -Seeking in Stalker movies and series now reaches the selected position instead of jumping forward or skipping to the next episode after resuming playback. diff --git a/.changes/ui-keyboard-scroll.md b/.changes/ui-keyboard-scroll.md deleted file mode 100644 index 2a5ca3bf1..000000000 --- a/.changes/ui-keyboard-scroll.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: ui -issues: [1506] ---- - -Live TV now supports keyboard movement between categories and channels, and channel lists keep scrolling after mouse selection. Channel scrollbars and resize handles are independently accessible. Movie and series details support immediate keyboard scrolling and no longer hide their scrollbars. diff --git a/.changes/ui-light-detail-surfaces.md b/.changes/ui-light-detail-surfaces.md deleted file mode 100644 index 4602cd79a..000000000 --- a/.changes/ui-light-detail-surfaces.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: ui ---- - -Detail buttons, episode cards and episode rows now keep visible backgrounds and borders in the light theme. The grid/list switch also clearly highlights the selected view in both themes. diff --git a/.changes/ui-sticky-detail-back.md b/.changes/ui-sticky-detail-back.md deleted file mode 100644 index e943bed2d..000000000 --- a/.changes/ui-sticky-detail-back.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: ui -issues: [1570] ---- - -Movie and series detail pages keep their Back button visible while scrolling. Escape closes the inline player to the description, then returns to the previous view. Open menus, dialogs and fullscreen retain priority. diff --git a/.changes/web-backend-validated-redirects.md b/.changes/web-backend-validated-redirects.md deleted file mode 100644 index 83c83a659..000000000 --- a/.changes/web-backend-validated-redirects.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: web-backend -issues: [1436] ---- - -The self-hosted backend now checks every provider redirect and pins connections to validated addresses, preventing redirects or DNS changes from bypassing private-network restrictions. Trusted LAN access remains available through the existing opt-in. diff --git a/.changes/xtream-auto-live-ts-fallback.md b/.changes/xtream-auto-live-ts-fallback.md deleted file mode 100644 index 097814030..000000000 --- a/.changes/xtream-auto-live-ts-fallback.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: xtream -issues: [1513] ---- - -Xtream Live TV in Auto can try advertised TS once when HLS initially fails with an HTTP error in a web player. The selected player and stream headers are kept. Settings explain the manual TS workaround for external players, Embedded MPV, and HLS retries that never report a terminal failure. diff --git a/.changes/xtream-catchup-server-timezone.md b/.changes/xtream-catchup-server-timezone.md deleted file mode 100644 index 405f92f4d..000000000 --- a/.changes/xtream-catchup-server-timezone.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: xtream -issues: [1562] ---- - -Catch-up (timeshift) from the Favorites and Recent tabs now asks the panel for the programme you clicked: the start time is rendered in the panel's own timezone instead of your computer's. The panel's timezone is remembered per source, survives restarts, and panels that report an unusual timezone name are handled through their clock. diff --git a/.changes/xtream-filtered-category-selection.md b/.changes/xtream-filtered-category-selection.md deleted file mode 100644 index 0cfa027ac..000000000 --- a/.changes/xtream-filtered-category-selection.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: fix -area: xtream -issues: [812, 816] ---- - -Xtream category management now selects or deselects only matching categories while searching, preserving the visibility of all other categories. Button labels and availability reflect the search results, and the counter clearly shows the total selected. diff --git a/.changes/xtream-http-connection-test.md b/.changes/xtream-http-connection-test.md deleted file mode 100644 index ddb44bc45..000000000 --- a/.changes/xtream-http-connection-test.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: xtream ---- - -The explicit HTTPS and HTTP connection test now detects Xtream portals that accept HTTP when HTTPS is unavailable and fills in the working address before you save. The saved address is used for catalog updates, provider EPG and playback. Connection failures now show a more specific explanation. diff --git a/.changes/xtream-sync-overlay-contrast.md b/.changes/xtream-sync-overlay-contrast.md deleted file mode 100644 index 52d3a31ad..000000000 --- a/.changes/xtream-sync-overlay-contrast.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -area: xtream ---- - -The Xtream sync panel now keeps its status text, labels and Stop sync button clearly readable in both light and dark themes. diff --git a/.claude/skills/release-cut/SKILL.md b/.claude/skills/release-cut/SKILL.md index 0f1e76d9c..9a2b15540 100644 --- a/.claude/skills/release-cut/SKILL.md +++ b/.claude/skills/release-cut/SKILL.md @@ -7,6 +7,8 @@ description: Use when preparing, cutting, tagging, publishing, or verifying an I Full contract, asset table and rationale: `docs/architecture/release-pipeline.md`. +For imagegen announcement covers, reuse [the approved artwork prompt](../../../docs/development/release-cover-artwork.md). + The tag workflow authors the public GitHub body with `node tools/release/extract-changelog-section.mjs --public "${VERSION}"`. Keep the full changelog, including internal notes, committed before tagging. diff --git a/.codex/skills/release-cut/SKILL.md b/.codex/skills/release-cut/SKILL.md index 0f1e76d9c..9a2b15540 100644 --- a/.codex/skills/release-cut/SKILL.md +++ b/.codex/skills/release-cut/SKILL.md @@ -7,6 +7,8 @@ description: Use when preparing, cutting, tagging, publishing, or verifying an I Full contract, asset table and rationale: `docs/architecture/release-pipeline.md`. +For imagegen announcement covers, reuse [the approved artwork prompt](../../../docs/development/release-cover-artwork.md). + The tag workflow authors the public GitHub body with `node tools/release/extract-changelog-section.mjs --public "${VERSION}"`. Keep the full changelog, including internal notes, committed before tagging. diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index fc7e8bb27..d901a20ee 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -13,6 +13,14 @@ name: Build and Make Electron App # cleanup-pr-draft.yml deletes the draft when the PR closes, so the stale # window is visible and bounded; refreshing drafts on skipped runs is not # worth a separate workflow. +# +# Master pushes are the nightly channel: the nightly-version job computes +# one -nightly.. version for the whole run +# (tools/release/nightly-version.mjs), every build job writes it into +# package.json so electron-updater treats the build as newer than the +# released version, and the release job publishes the artifacts as a +# prerelease of 4gray/iptvnator-nightly instead of the rolling test-master +# draft. Contract: docs/architecture/release-pipeline.md ("Nightly channel"). on: push: branches: @@ -42,6 +50,39 @@ permissions: contents: read jobs: + # One version for the whole run. Computed here rather than in each build + # job because the rule depends on whether the base tag exists on origin: + # a tag pushed while the matrix runs would otherwise give one run two + # different versions. Empty output means "not a nightly build". + nightly-version: + name: Resolve nightly version + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.resolve.outputs.version }} + steps: + - name: Checkout code + if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == '4gray/iptvnator' + uses: actions/checkout@v7 + + - name: Resolve nightly version + id: resolve + shell: bash + env: + NIGHTLY: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == '4gray/iptvnator' }} + run: | + set -euo pipefail + + if [ "${NIGHTLY}" != "true" ]; then + echo "version=" >> "${GITHUB_OUTPUT}" + echo "Not a master push; no nightly version." + exit 0 + fi + + VERSION="$(node tools/release/nightly-version.mjs)" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + echo "Nightly version: ${VERSION}" + linux-embedded-mpv-runtime: name: Build pinned Linux Embedded MPV runtime runs-on: ubuntu-22.04 @@ -61,7 +102,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' - name: Resolve Linux runtime toolchain cache key id: linux-runtime-cache-key @@ -340,6 +381,7 @@ jobs: build-cross-platform: name: Build on ${{ matrix.os }} ${{ matrix.arch }} + needs: nightly-version runs-on: ${{ matrix.runner }} timeout-minutes: 120 concurrency: @@ -379,7 +421,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' cache: 'pnpm' - name: Install Linux system dependencies @@ -445,6 +487,23 @@ jobs: BUILD_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} run: node tools/build/inject-build-commit.mjs + - name: Apply nightly version + # Master merges feed the nightly update channel. The version + # must be greater than the released one for electron-updater to + # offer it, and it must be in package.json before the frontend + # and backend builds and electron-builder read it. The value + # comes from the nightly-version job so every job of this run + # builds the same version. The script also sets the electron- + # builder publish channel to "nightly", which names the updater + # metadata nightly-mac.yml / nightly.yml / nightly-linux.yml. + if: needs.nightly-version.outputs.version != '' + # Windows runners default to PowerShell, where "${NIGHTLY_VERSION}" + # expands to nothing and the script rejects the empty version. + shell: bash + env: + NIGHTLY_VERSION: ${{ needs.nightly-version.outputs.version }} + run: node tools/release/nightly-version.mjs --apply --version "${NIGHTLY_VERSION}" + - name: Build frontend run: pnpm nx build web --skip-nx-cache @@ -1091,6 +1150,16 @@ jobs: electron-backend-e2e:packaged-frame-copy-smoke \ --skip-nx-cache + - name: Upload packaged frame-copy smoke diagnostics + if: always() && matrix.os == 'linux' && matrix.linux_profile == 'portable' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: packaged-frame-copy-smoke + path: | + dist/playwright-report/electron-backend-e2e/packaged-frame-copy-smoke/ + dist/test-results/electron-backend-e2e/packaged-frame-copy-smoke/ + retention-days: 7 + - name: Diagnose packaged x64 frame-copy hardware path if: matrix.os == 'linux' && matrix.linux_profile == 'portable' continue-on-error: true @@ -1188,6 +1257,7 @@ jobs: dist/executables/**/*.dmg dist/executables/**/*.zip dist/executables/**/latest-mac.yml + dist/executables/**/nightly-mac.yml dist/executables/**/*.blockmap retention-days: 7 @@ -1212,6 +1282,7 @@ jobs: dist/executables/*.AppImage dist/executables/*.snap dist/executables/**/latest-linux*.yml + dist/executables/**/nightly-linux*.yml dist/executables/**/*.blockmap retention-days: 7 @@ -1234,12 +1305,15 @@ jobs: dist/executables/**/*.msi dist/executables/**/*.zip dist/executables/**/latest.yml + dist/executables/**/nightly.yml dist/executables/**/*.blockmap retention-days: 7 build-linux: name: Build on ${{ matrix.os }} ${{ matrix.arch }} (${{ matrix.linux_profile }}) - needs: linux-embedded-mpv-runtime + needs: + - linux-embedded-mpv-runtime + - nightly-version runs-on: ${{ matrix.runner }} timeout-minutes: 120 concurrency: @@ -1276,6 +1350,7 @@ jobs: create-release: name: Create Draft Release needs: + - nightly-version - build-cross-platform - build-linux if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} @@ -1285,6 +1360,12 @@ jobs: cancel-in-progress: false permissions: contents: write + env: + # Master pushes publish to the nightly repository (steps at the + # end of this job) instead of the rolling draft. + NIGHTLY: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == '4gray/iptvnator' }} + NIGHTLY_REPOSITORY: 4gray/iptvnator-nightly + NIGHTLY_KEEP_RELEASES: '20' steps: - name: Checkout code @@ -1306,10 +1387,20 @@ jobs: node <<'NODE' const fs = require('fs'); - const candidates = [ - 'artifacts/macos-x64-artifacts/latest-mac.yml', - 'artifacts/macos-arm64-artifacts/latest-mac.yml', - ].filter((filePath) => fs.existsSync(filePath)); + // electron-builder names the updater metadata after the + // channel: latest-mac.yml for releases, nightly-mac.yml for + // the prerelease versions master builds carry. + const channelFile = ['latest-mac.yml', 'nightly-mac.yml'].find( + (name) => + fs.existsSync(`artifacts/macos-x64-artifacts/${name}`) || + fs.existsSync(`artifacts/macos-arm64-artifacts/${name}`) + ); + const candidates = channelFile + ? [ + `artifacts/macos-x64-artifacts/${channelFile}`, + `artifacts/macos-arm64-artifacts/${channelFile}`, + ].filter((filePath) => fs.existsSync(filePath)) + : []; if (candidates.length === 0) { console.log('No macOS update metadata found; skipping merge.'); @@ -1407,8 +1498,8 @@ jobs: mergedEntries ); - fs.writeFileSync('artifacts/latest-mac.yml', merged); - console.log(`Merged ${candidates.length} macOS update metadata files.`); + fs.writeFileSync(`artifacts/${channelFile}`, merged); + console.log(`Merged ${candidates.length} macOS update metadata files into ${channelFile}.`); NODE - name: Get version from package.json @@ -1422,6 +1513,7 @@ jobs: # for every push. PR builds must not use github.sha here: that is the # ephemeral merge-commit SHA, which resolves to nothing in the repo. - name: Compose release metadata + if: env.NIGHTLY != 'true' id: release-meta shell: bash env: @@ -1499,7 +1591,7 @@ jobs: # full current set right after. Only drafts are pruned; published # releases are never touched. - name: Prune stale draft assets - if: github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open' + if: env.NIGHTLY != 'true' && (github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ steps.release-meta.outputs.tag }} @@ -1522,7 +1614,7 @@ jobs: - name: Create Draft Release id: draft-release - if: github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open' + if: env.NIGHTLY != 'true' && (github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open') uses: softprops/action-gh-release@v3 with: draft: true @@ -1569,7 +1661,7 @@ jobs: # body is left as the action set it and only title/commitish are # re-asserted. - name: Ensure draft metadata is current - if: steps.draft-release.outputs.id != '' + if: env.NIGHTLY != 'true' && steps.draft-release.outputs.id != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_ID: ${{ steps.draft-release.outputs.id }} @@ -1612,3 +1704,196 @@ jobs: --arg body "${FULL_BODY}" \ '{tag_name: $tag, name: $name, target_commitish: $commitish, body: ($body | .[0:120000])}' | gh api -X PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --input - > /dev/null + + # ── Nightly channel ────────────────────────────────────────── + # Drafts are invisible to anyone without write access and to + # electron-updater, so master builds are published as prereleases + # of the nightly repository, which the desktop app's Nightly update + # channel follows. The repository needs one commit on its default + # branch (gh creates the release tag there) and a fine-grained PAT + # with Contents: read/write on it, stored as NIGHTLY_RELEASE_TOKEN. + # Without the token the build still succeeds and only warns. + - name: Resolve nightly release metadata + if: env.NIGHTLY == 'true' + id: nightly-meta + shell: bash + env: + NIGHTLY_RELEASE_TOKEN: ${{ secrets.NIGHTLY_RELEASE_TOKEN }} + VERSION: ${{ needs.nightly-version.outputs.version }} + run: | + set -euo pipefail + + if [ -z "${VERSION}" ]; then + echo "::error::The nightly-version job produced no version for this master push." + exit 1 + fi + { + echo "version=${VERSION}" + echo "tag=v${VERSION}" + } >> "${GITHUB_OUTPUT}" + + if [ -z "${NIGHTLY_RELEASE_TOKEN}" ]; then + echo "::warning::NIGHTLY_RELEASE_TOKEN is not configured; the nightly release for ${VERSION} is skipped." + echo "publish=false" >> "${GITHUB_OUTPUT}" + else + echo "publish=true" >> "${GITHUB_OUTPUT}" + fi + + # The notes list the master commits since the previous nightly. + # That nightly's source commit is read back from the marker its + # own notes carry, because the nightly repository has no copy of + # the app history to compare against. The main-repository compare + # uses GITHUB_TOKEN; only the nightly repository is read with the + # PAT. + - name: Compose nightly release notes + if: steps.nightly-meta.outputs.publish == 'true' + id: nightly-notes + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NIGHTLY_RELEASE_TOKEN: ${{ secrets.NIGHTLY_RELEASE_TOKEN }} + VERSION: ${{ steps.nightly-meta.outputs.version }} + HEAD_SHA: ${{ github.sha }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + + SHORT_SHA="${HEAD_SHA:0:7}" + NOTES_FILE="${RUNNER_TEMP}/nightly-notes.md" + + PREVIOUS_TAG="$(GH_TOKEN="${NIGHTLY_RELEASE_TOKEN}" gh release list \ + --repo "${NIGHTLY_REPOSITORY}" --exclude-drafts --limit 1 \ + --json tagName --jq '.[0].tagName // ""')" + PREVIOUS_SHA="" + if [ -n "${PREVIOUS_TAG}" ]; then + PREVIOUS_SHA="$(GH_TOKEN="${NIGHTLY_RELEASE_TOKEN}" gh release view "${PREVIOUS_TAG}" \ + --repo "${NIGHTLY_REPOSITORY}" --json body --jq '.body // ""' | + sed -n 's/.*.*/\1/p' | head -n 1)" + fi + + COMMITS="" + if [ -n "${PREVIOUS_SHA}" ] && [ "${PREVIOUS_SHA}" != "${HEAD_SHA}" ]; then + # A rewritten history makes the compare fail; the notes + # then just omit the list rather than failing the release. + COMMITS="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PREVIOUS_SHA}...${HEAD_SHA}" \ + --jq '.commits[] | "- [`\(.sha[0:7])`](\(.html_url)) \(.commit.message | split("\n")[0])"' || true)" + fi + + { + printf '🌙 Nightly build from `master` — commit [`%s`](%s/commit/%s) · [workflow run](%s)\n\n' \ + "${SHORT_SHA}" "${REPO_URL}" "${HEAD_SHA}" "${RUN_URL}" + printf 'Untested snapshot of master for the desktop app'"'"'s **Nightly** update channel (Settings → About → Update channel). Nightly builds may break, and their database changes are permanent: switching back to Stable keeps this build installed until the next stable release is newer. Back up your playlists first.\n\n' + if [ -n "${COMMITS}" ]; then + printf '## Changes since %s\n\n%s\n\n' "${PREVIOUS_TAG}" "${COMMITS}" + else + printf 'See the [commit history](%s/commits/master) for what changed.\n\n' "${REPO_URL}" + fi + printf '\n' "${HEAD_SHA}" + } > "${NOTES_FILE}" + + echo "notes-file=${NOTES_FILE}" >> "${GITHUB_OUTPUT}" + + # Created as a draft, assets uploaded, then published in one edit, + # so electron-updater never sees a release whose channel file is + # still missing. A published release is never deleted here: a + # rerun after a successful publish is a no-op, and only a draft + # left behind by a failed run is replaced. The release job is + # serialized per ref but two master runs can still finish out of + # order, so a nightly that is older than the newest published one + # is dropped instead of becoming the feed's newest entry. + - name: Publish nightly release + if: steps.nightly-meta.outputs.publish == 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.NIGHTLY_RELEASE_TOKEN }} + TAG: ${{ steps.nightly-meta.outputs.tag }} + VERSION: ${{ steps.nightly-meta.outputs.version }} + HEAD_SHA: ${{ github.sha }} + NOTES_FILE: ${{ steps.nightly-notes.outputs.notes-file }} + run: | + set -euo pipefail + shopt -s nullglob + + NEWEST_PUBLISHED="$(gh release list --repo "${NIGHTLY_REPOSITORY}" --exclude-drafts --limit 200 \ + --json tagName --jq '.[].tagName | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+-nightly\\."))' | + sort -V | tail -n 1)" + if [ -n "${NEWEST_PUBLISHED}" ] && [ "${NEWEST_PUBLISHED}" != "${TAG}" ] && + [ "$(printf '%s\n%s\n' "${NEWEST_PUBLISHED}" "${TAG}" | sort -V | tail -n 1)" != "${TAG}" ]; then + echo "::notice::${NEWEST_PUBLISHED} is already published and newer than ${TAG}; not publishing this superseded build." + exit 0 + fi + + for required in \ + artifacts/nightly-mac.yml \ + artifacts/windows-artifacts/nightly.yml \ + artifacts/linux-portable-artifacts/nightly-linux.yml; do + if [ ! -f "${required}" ]; then + echo "::error::Missing updater metadata ${required}; the nightly channel would be unable to install this build." + exit 1 + fi + done + + assets=( + artifacts/macos-x64-artifacts/*-x64.dmg + artifacts/macos-x64-artifacts/*-x64.zip + artifacts/macos-x64-artifacts/*.blockmap + artifacts/macos-arm64-artifacts/*-arm64.dmg + artifacts/macos-arm64-artifacts/*-arm64.zip + artifacts/macos-arm64-artifacts/*.blockmap + artifacts/nightly-mac.yml + artifacts/linux-system-artifacts/*.deb + artifacts/linux-system-artifacts/*.rpm + artifacts/linux-system-artifacts/*.pacman + artifacts/linux-system-artifacts/*.pkg.tar.* + artifacts/linux-portable-artifacts/*.AppImage + artifacts/linux-portable-artifacts/*.snap + artifacts/linux-portable-artifacts/nightly-linux*.yml + artifacts/linux-portable-artifacts/*.blockmap + artifacts/linux-flatpak-artifacts/*.flatpak + artifacts/linux-frame-copy-runtime-sources/linux-frame-copy-runtime-sources.tar.xz + artifacts/windows-artifacts/*-setup.exe + artifacts/windows-artifacts/*.msi + artifacts/windows-artifacts/*.zip + artifacts/windows-artifacts/nightly.yml + artifacts/windows-artifacts/*.blockmap + ) + + EXISTING="$(gh api "repos/${NIGHTLY_REPOSITORY}/releases?per_page=100" --paginate | + jq -c --arg tag "${TAG}" 'map(select(.tag_name == $tag)) | first // empty')" + if [ -n "${EXISTING}" ]; then + if [ "$(jq -r '.draft' <<< "${EXISTING}")" != "true" ]; then + echo "::notice::${TAG} is already published in ${NIGHTLY_REPOSITORY}; nothing to do for this re-run." + exit 0 + fi + DRAFT_ID="$(jq -r '.id' <<< "${EXISTING}")" + echo "Removing the draft ${TAG} (id ${DRAFT_ID}) a failed run left behind." + gh api -X DELETE "repos/${NIGHTLY_REPOSITORY}/releases/${DRAFT_ID}" + fi + + gh release create "${TAG}" "${assets[@]}" \ + --repo "${NIGHTLY_REPOSITORY}" \ + --draft \ + --prerelease \ + --title "Nightly ${VERSION} (${HEAD_SHA:0:7})" \ + --notes-file "${NOTES_FILE}" + gh release edit "${TAG}" --repo "${NIGHTLY_REPOSITORY}" --draft=false --prerelease + + echo "Published ${TAG} to ${NIGHTLY_REPOSITORY} with ${#assets[@]} assets." + + - name: Prune old nightly releases + if: steps.nightly-meta.outputs.publish == 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.NIGHTLY_RELEASE_TOKEN }} + run: | + set -euo pipefail + + gh release list --repo "${NIGHTLY_REPOSITORY}" --exclude-drafts --limit 200 \ + --json tagName --jq '.[].tagName | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+-nightly\\."))' | + sort -V -r | tail -n "+$((NIGHTLY_KEEP_RELEASES + 1))" | + while read -r tag; do + [ -n "${tag}" ] || continue + echo "Deleting nightly ${tag} (keeping the newest ${NIGHTLY_KEEP_RELEASES})." + gh release delete "${tag}" --repo "${NIGHTLY_REPOSITORY}" --cleanup-tag --yes + done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b0434924..f0b1910c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' cache: 'pnpm' - name: Install dependencies @@ -141,12 +141,15 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Validate agent guidance + run: pnpm run agents:validate + - name: Validate Nx dependency version policy run: pnpm run deps:nx:validate diff --git a/.github/workflows/cleanup-pr-draft.yml b/.github/workflows/cleanup-pr-draft.yml index 695685255..4b442cfe3 100644 --- a/.github/workflows/cleanup-pr-draft.yml +++ b/.github/workflows/cleanup-pr-draft.yml @@ -3,12 +3,20 @@ name: Cleanup PR Draft Release on: pull_request: types: [closed] + # The event above is the fast path, not a guarantee: GitHub does not run a + # `pull_request: closed` workflow when the head ref is already gone at + # event time, which is exactly what Dependabot does when it supersedes one + # of its own PRs (closes it and deletes the branch in a single operation). + # Those drafts — and any the event path missed for other reasons — are + # collected by the scheduled sweep below. + schedule: + - cron: '17 4 * * *' + workflow_dispatch: -# contents: write — delete the draft release; actions: write — cancel the -# closed PR's still-running build workflow before deleting. +# contents: write — delete the draft release. The `actions: write` needed to +# cancel a closed PR's still-running build is scoped to the event job. permissions: - actions: write - contents: write + contents: read jobs: delete-draft: @@ -16,8 +24,11 @@ jobs: # Fork PRs never get a draft (the release job skips them) and their # GITHUB_TOKEN is read-only regardless of the permissions block, so # there is nothing to cancel or delete. - if: github.event.pull_request.head.repo.full_name == github.repository + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + permissions: + actions: write + contents: write steps: # A closed PR can be reopened while this job is still queued or # waiting; a reopened PR's fresh build must not be cancelled and @@ -90,3 +101,94 @@ jobs: gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --paginate \ --jq ".[] | select(.draft and .tag_name == \"test-pr-${PR_NUMBER}\") | .id" | xargs -r -n1 -I{} gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/{}" + + sweep-drafts: + name: Sweep orphaned PR draft releases + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + # Two sweeps must not race each other into a double delete; a manual + # dispatch during the nightly cron would otherwise produce a spurious + # failure on an already-deleted draft. + concurrency: + group: cleanup-pr-draft-sweep + cancel-in-progress: false + steps: + # Unlike the event job this one does not cancel in-progress builds: + # the build's draft steps are themselves gated on the live PR state + # being `open` ("Check PR is still open" in build-and-make.yaml), so + # a run that outlives the close cannot recreate what was swept. A + # build that passed that check just before the PR closed is caught + # by the next sweep. + # + # Draft releases have no real git tag, so they are invisible to + # `gh release view ` and to the tags API. Enumerate releases + # and match on the stored tag_name, exactly as the event job does. + # The `test-` drafts and every other release are left + # alone by the ^test-pr-$ shape. + - name: Delete drafts whose PR is closed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + # Assigned rather than piped: a failed or partially paginated + # listing must fail the job instead of silently sweeping a + # short list. + drafts="$( + gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --paginate \ + --jq '.[] | select(.draft and (.tag_name | test("^test-pr-[0-9]+$"))) | "\(.id) \(.tag_name)"' + )" + + if [ -z "${drafts}" ]; then + echo "No test-pr- drafts found." + exit 0 + fi + + failed=0 + + while IFS=' ' read -r release_id tag; do + pr_number="${tag#test-pr-}" + + # Fail closed: a lookup error (404, rate limit, outage) + # leaves `state` empty and the draft untouched. Only a + # PR GitHub currently reports as closed loses its draft, + # so a reopened PR and an open PR mid-build keep theirs. + # gh's own stderr is left visible on purpose — a draft + # kept as `unknown` should say why in the job log. + state="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.state' || true)" + if [ "${state}" != "closed" ]; then + echo "Keeping ${tag}: PR #${pr_number} is ${state:-unknown}." + continue + fi + + if gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/${release_id}" >/dev/null; then + echo "Deleted ${tag} (release ${release_id}) for closed PR #${pr_number}." + continue + fi + + # The delete failed. Only a release GitHub confirms is + # gone (404) excuses that — the event job racing us to + # the same draft. `gh api` exits 1 for every failure + # alike, so a rate limit or outage hitting both calls + # would otherwise read as "already deleted" and leave a + # green sweep behind an undeleted draft. Read the status + # line instead: `-i` prints it even on an error status, + # and a request that never got a response leaves it + # empty, which is not 404 and so stays a failure. + recheck_status="$( + gh api -i "repos/${GITHUB_REPOSITORY}/releases/${release_id}" 2>/dev/null | + sed -n '1s#^HTTP/[0-9.]* \([0-9]\{3\}\).*#\1#p' || true + )" + + if [ "${recheck_status}" = "404" ]; then + echo "Draft ${tag} (release ${release_id}) was already gone." + else + echo "::error::Failed to delete draft ${tag} (release ${release_id}); re-check returned ${recheck_status:-no HTTP status}." + failed=1 + fi + done <<< "${drafts}" + + exit "${failed}" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5f87782aa..93b8e120f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,6 +6,7 @@ name: "CodeQL" on: + workflow_dispatch: push: branches: [master] pull_request: diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index f7e1b2c52..a8cf6f076 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -32,7 +32,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' cache: 'pnpm' - name: Install dependencies diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index b2f4e2582..74ff1e198 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -59,7 +59,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' cache: 'pnpm' - name: Install Dependencies @@ -68,6 +68,9 @@ jobs: - name: Build Backend run: pnpm nx build electron-backend + - name: Verify Electron process cleanup + run: pnpm exec tsx --test apps/electron-backend-e2e/src/performance/electron-process-lifecycle.spec.ts apps/electron-backend-e2e/src/performance/electron-process-termination.spec.ts + - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps @@ -113,7 +116,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: '.nvmrc' cache: 'pnpm' - name: Install Dependencies diff --git a/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml b/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml index fafdab266..a944acea0 100644 --- a/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml +++ b/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml @@ -35,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: - node-version: '22' + node-version-file: '.nvmrc' - name: Refresh pin when it approaches upstream retention id: refresh diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..c94711948 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.23.2 diff --git a/.plans/2026-09-20-agent-guidance-reorganization.md b/.plans/2026-09-20-agent-guidance-reorganization.md new file mode 100644 index 000000000..4a7677f69 --- /dev/null +++ b/.plans/2026-09-20-agent-guidance-reorganization.md @@ -0,0 +1,67 @@ +# Agent guidance reorganization — issue #1643 + +Approved implementation plan, 2026-09-20. + +## Outcome + +One source of common instructions: AGENTS.md (at most 200 lines / 16 KiB). +CLAUDE.md imports @AGENTS.md and contains only Claude-specific guidance +(at most 30 lines / 2 KiB). Do not increase Codex loading limits. No runtime +or public API changes. + +## Knowledge preservation + +Inventory both original files at the starting commit in +`docs/maintenance/agent-guidance-migration.md`. Record source section and line +ranges, destination document and heading, and whether each contract was moved, +merged with an existing equivalent, or corrected with evidence. Split long +player sections into individual contracts. Preserve exceptions, commands, +rationale and platform constraints. Do not create a required monolithic archive. + +## Destinations + +Use existing authoritative docs first: Nx boundaries for structure/dependencies; +validation-map for tests/lint; release-pipeline and release skills for releases; +sqlite-db-worker and the database README for IPC/migrations; m3u-playlist-module +for M3U/XMLTV/startup/source health; Xtream/Stalker compatibility docs for portals; +player-controls-contract for web controls/radio/sleep; embedded-mpv-native for +native runtime/packaging; UI guidelines, detail navigation and remote control for +navigation; PWA/host connectivity/security docs for networking; existing download, +TMDB, multi-source, workspace and backup docs for their domains; website README +for website policy. + +Create docs/development/agent-workflow.md for documentation/skill maintenance and +Angular conventions, and docs/development/electron-debugging.md for CDP/tracing. +Add a developer navigation link in README.md. + +## Root guidance and navigation + +Retain project purpose, essential commands, .nvmrc/frozen install/Nx bootstrap, +scoped imports and boundaries, migration safety, credential redaction, regression +coverage, release-note/doc requirements, protected Markdown formatting and plan +storage. Preserve the Nx-managed block/markers, conditional on available tools. +Replace mandatory root-file updates with updates to each subsystem's canonical +doc. Root instructions hold only universal rules and a compact topic routing table. +Create docs/maintenance/agent-context-map.md with topics, code paths, docs and +skills. Read affected contracts only; cross-domain work reads each relevant one. +Update existing skills rather than proliferating copies; preserve byte-identical +release mirrors. No mass nested instructions in this change. + +## Tooling + +Extend repository-skills (no new Nx project) with agents:validate and node:test +coverage. Check UTF-8 bytes/line budgets, one standalone @AGENTS.md import in +CLAUDE.md and no other root imports, local navigation/map/migration links and +anchors, and literal repository paths without treating globs/commands as paths. +Add an unconditional CI validation step and correct Nx test inputs/lint commands. + +## Acceptance + +Tests cover exact/over budgets, UTF-8, LF/CRLF, missing/duplicate/extra imports, +missing local files and anchors. Run frozen install, Nx discovery, repository-skills +test/lint, agents:validate, skills:validate, release:notes:validate, git diff --check +and workflow validation. Audit every source block to a destination, with no +unresolved or lost unique contract. Walk navigation for XMLTV, Xtream, MPV, +migrations and releases. App unit/E2E is unnecessary (no runtime changes); no +release note for docs/tooling validation. Do not run whole-file Prettier on docs, +AGENTS.md or CLAUDE.md. diff --git a/AGENTS.md b/AGENTS.md index 504ea352c..3119f7d6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,1176 +1,128 @@ -# AGENTS.md - -This file provides guidance to coding agents working in this repository. - -## Plan Mode - -- When an agent is in Plan Mode and produces a final ``, 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, questions, 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. - -## Agent Bootstrap - -- In a fresh worktree, run `pnpm install --frozen-lockfile` before relying on Nx project discovery, lint, test, or build commands. Without `node_modules`, `pnpm nx show projects` will fail because the local Nx modules are unavailable. -- Re-run the install whenever the checkout moves — `git pull`, `git reset --hard`, a rebase, or a worktree branch being re-pointed. Git rewrites `pnpm-lock.yaml` but never re-links `node_modules`, so a tree installed at an older commit keeps serving the old dependency versions and tests fail locally while CI stays green. Check with `cmp pnpm-lock.yaml node_modules/.pnpm/lock.yaml`; any difference means the tree is stale, and a plain `pnpm install --frozen-lockfile` in that directory repairs it. Each worktree needs its own install — with no local `node_modules`, Nx aborts with `Could not find ".modules.yaml"`. -- After dependencies are installed, verify workspace discovery with `pnpm nx show projects`. -- 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` so `@nx/enforce-module-boundaries` remains useful for humans and agents. -- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy. -- Keep `nx` and every official `@nx/*` package on the same exact version; run - `pnpm run deps:nx:validate` after dependency updates. -- Vite `7.3.6`, resolved through Angular's build tooling, is patched with - bounded transform prefilters and the upstream precise matchers in - `patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling - resolves a Vite version containing the fix, and run `pnpm run deps:vite:test` - after related dependency updates. -- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in - `patches/app-builder-lib@26.15.7.patch` with the upstream backport - electron-userland/electron-builder#10172: `security set-key-partition-list -k` - must receive the temporary keychain's own password, not the `.p12` import - password. macOS runner images since `macos-26-arm64` 20260831 verify that - password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user -name or passphrase you entered is not correct`. Keep the patch until - electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+), - and run `pnpm run deps:electron-builder:test` after related dependency - updates — the test fails when the patched version no longer matches the - installed one. -- A directory holding files consumed by other projects must be an Nx project. - Nx builds its graph from TypeScript imports only, so a relative SCSS `@use` - across project roots creates no edge and the imported file lands in no task - hash — edits then return a cache hit instead of rebuilding. Shared partials - live in `libs/ui/styles` (project `ui-styles`), and each consumer declares - `"implicitDependencies": ["ui-styles"]`. Run `pnpm run styles:inputs:validate` - after adding a cross-project stylesheet import. -- Update Nx with `pnpm nx migrate nx@ --skipInstall`, regenerate the - lockfile, run generated migrations when present, and validate before opening - a PR. Major updates are always manual. Replace incomplete Dependabot security - PRs with a coordinated update instead of editing the bot branch. -- ESLint enforces `max-lines` on TypeScript files: production code targets under 300 with a hard maximum of 400, while tests (`**/*.spec.ts`, `**/*.spec-data.ts`, `**/*.e2e.ts`, `apps/*-e2e/**`) are held to 1200 — a long spec signals coverage, not the design debt the production limit catches. Blank lines and comments are not counted, so a docblock never forces a split. Limits live in `tools/eslint/max-lines-config.mjs`, imported by both `eslint.config.mjs` and the generator so the rule and the baseline cannot drift. Files that predate the rule are baselined in `tools/eslint/max-lines-baseline.mjs`; after splitting a file, regenerate it with `node tools/eslint/generate-max-lines-baseline.mjs` (it runs ESLint's own rule rather than counting lines itself). Never add new files to the baseline — the list must only shrink. A new file that genuinely cannot be split (for example a function serialized into another process) instead carries its own file-wide `/* eslint-disable max-lines -- */`; the generator skips those files, so a justified exemption never lands in the baseline. Remove such a directive once ESLint reports it as unused. -- Project `lint` targets that shell out to eslint must quote the glob, e.g. `eslint "apps//**/*.ts"`. An unquoted `**` is expanded by the POSIX shell on Linux and macOS (which has no `globstar`, so it matches only a shallow subset of files) while Windows passes the literal pattern to ESLint, which expands it recursively — the two hosts then lint different file sets. The target still reports success either way, so a broken glob hides missing coverage instead of failing. After changing such a target, compare the linted file count against `find -name '*.ts' | wc -l`. -- Repository-specific skills live under `.codex/skills/`. -- Frontmatter descriptions are trigger-only and begin with `Use when`; keep - each skill at or below 500 words. -- Run `pnpm run skills:validate` after editing a committed skill or a literal - path it documents. -- Keep `.codex` and `.claude` copies of `release-notes` and `release-cut` - byte-identical. - -## Documentation After Changes - -- After implementing a meaningful change, agents 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 the root `CLAUDE.md` and this file up to date. They are living documents: whenever a change touches something they describe — 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 sections as part of the same task, and keep the process sections mirrored between `AGENTS.md` and `CLAUDE.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` or `AGENTS.md` drift: a stale path or route in these files 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. - -## Release Notes For User-Visible Changes - -- Any change a user could notice — new behavior, changed behavior, bug fix, performance win, breaking change — must add one note file under `.changes/` in the same PR. Format, field table, and writing rules: `.changes/README.md`. -- Name it `-.md`; `area` matches the conventional-commit scope. There is no version field — the release version is chosen at release time. -- Write the body for a user, not a reviewer: "the player now remembers volume between episodes", not "hoist volume state into the session". Max 400 characters; depth belongs in the release blog post. -- `type: internal` records invisible maintenance. Internal notes stay collapsed in `CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the authored public GitHub body by `extract-changelog-section.mjs --public`; GitHub's generated commit list remains separate, so an internal-only release can have an empty authored body. -- `highlight: ` (max 60 characters, rejected on `type: internal`) marks a note as one of the release's two or three headline changes. Highlights lead the Telegram/Reddit announcement drafts, become ready-made blog section headings, and are the input the highlight-card generator renders from. A release where everything is a highlight has none. -- Skip the note for test-only changes, docs, CI/workflow plumbing, and pure refactors with no behavior change. When skipping on a PR that touches `apps/**` or `libs/**`, apply the `no-release-note` label. -- CI enforces this: the "Release note gate" job in `.github/workflows/ci.yml` fails PRs that change runtime code without an added `.changes/*.md` or the label (policy in `tools/release/check-release-note-gate.mjs`; tests/e2e/website/mock-server/docs paths are auto-exempt). -- The `release-notes` skill covers writing notes; the `release-cut` skill covers the full release sequence. Canonical contract — surfaces, ordering constraints, the required draft asset set: `docs/architecture/release-pipeline.md`. -- Validate before finishing: `pnpm run release:notes:validate`. -- Announcement drafts and highlight cards are built from the same notes: `pnpm --silent run release:notes:telegram` and `pnpm --silent run release:notes:reddit` print paste-ready posts to stdout (Telegram is guaranteed to fit its 4096-character limit; `--silent` keeps pnpm's lifecycle banner out of a redirected post), and `pnpm run release:cards:generate` renders branded 1200×630 highlight cards plus a release hero into `dist/release-highlight-cards/v/`. All three read `highlight:` metadata that exists only in the note files, so they must run before `build-release-notes.mjs --consume`; the cards additionally need `release:screenshots` to have run. Nothing is posted or copied into the website tree automatically. -- Pushes to `master` and `v*` can publish Docker images. A `v*` tag build creates a draft GitHub release. -- `pnpm run release:verify:draft` waits for that tag build (polling until the run is indexed, then `gh run watch`) and verifies the draft's status, authored body, and complete required asset set. It is read-only and deliberately fails on an already-published release, because it is the gate that runs before publication. -- Publishing the GitHub release verifies its Snap assets and automatically uploads them to `edge`; installed-Snap smoke and candidate/stable promotion remain manual. -- Release-post screenshots come only from the release capture script running against the mock servers. Never add a screenshot taken from a real playlist or account to `apps/website/public/blog/**` — real streams, logos, and metadata are copyrighted, and credentials must never reach a published image. -- Final task summaries should state whether a release note was added or why it was skipped. - -## AppImage Manager Metadata - -AppManager full-download discovery uses `appImage.desktop.entry` URL fields. -Electron Builder generates the version; `extraMetadata.desktopName=iptvnator` -preserves Linux window identity without a shared `linux.desktop.entry` object -(builder's nested merge would leak AppImage fields into Snap). This does not -enable AppImageUpdate/zsync. Contract: `docs/architecture/release-pipeline.md` -(AppImage external-manager metadata). - -## Upgrade And Migration Compatibility - -- Users may skip releases. The application must apply all required migrations in dependency order when upgrading directly from an older release; never assume that users installed or launched every intermediate version. -- Preserve migration paths for existing persisted data. Do not make deleting a database/profile or reinstalling the application a normal upgrade requirement. Any unavoidable intermediate-version requirement must be an explicitly documented exception. -- Create required tables first, add missing columns before dependent indexes/triggers/queries, and make startup migrations safe to run again. `CREATE TABLE IF NOT EXISTS` does not update an existing table's columns. -- For persistence changes, test real SQLite initialization with representative historical schemas and data, including skipped releases, the previous release, a fresh database, and repeated startup. Assert preservation of user data as well as the resulting schema; SQL mocks alone cannot verify upgrade compatibility. Cover equivalent persisted-state transitions for non-SQLite stores. -- See `libs/shared/database/README.md` for SQLite migration ownership and validation guidance. - -## 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, 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 ` 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. - -## Legacy Desktop Profile Migration - -`electron-profile-bootstrap.ts` selects the known v0.19 `electron-backend` -profile before eager main-process imports only when current Chromium storage -is unused. Existing profiles retain their settings and offer explicit recovery -of missing sources from a disposable legacy snapshot. Playlist rows and a -completion receipt commit atomically in the DB worker; original IndexedDB is -retained, current payload rows are preserved, and completed imports never -replay deleted sources. Contract and recovery limits: -`docs/architecture/m3u-playlist-module.md` (Desktop upgrades from legacy profiles). - -Startup shows `AppStartupStatusComponent` until the initial route and source -inventory are ready, including XMLTV reconciliation. Inventory reads retry once; -failed reads show an explicit Retry action instead of an empty library. Successful -inventory reads first await settings loading, then pending XMLTV reconciliation, -and retry failed cleanup -with its last committed URLs before exposing the workspace. See the -same contract for startup readiness and error handling. - -## Electron Debugging (CDP) - -- Start the Electron development app with: `nx serve electron-backend` -- Package-script equivalent: `pnpm run serve:backend` -- Electron is configured to start with: `--remote-debugging-port=9222` -- Connect Chrome DevTools Protocol tools to: `127.0.0.1:9222` -- For Electron automation/debugging tasks, 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`, snapshots are empty, and screenshots are black. -- If that happens, inspect targets with `curl http://127.0.0.1:9222/json/list` and connect directly to the IPTVnator page websocket from the `webSocketDebuggerUrl` field. -- The app holds a single-instance lock (`acquireSingleInstanceLock` in `apps/electron-backend/src/app/services/single-instance.ts`): a second launch against the same `userData` quits immediately and focuses the running window. To attach a second CDP-enabled instance to the same profile, set `IPTVNATOR_ALLOW_MULTIPLE_INSTANCES=1` — knowing that only one of the two processes will own the renderer's IndexedDB, so settings written by the other are lost. Before focusing, the guard forwards the second launch's argv to `onSecondInstance`, which is how a playlist path handed to an already-running app reaches the open queue. - -### Trace / Debug Startup - -- Full startup tracing: - -```bash -IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend -``` - -- Narrower trace flags: - - `IPTVNATOR_TRACE_IPC=1` traces renderer `window.electron.*` bridge calls - - `IPTVNATOR_TRACE_DB=1` traces DB worker requests and request-scoped DB events - - `IPTVNATOR_TRACE_SQL=1` traces SQLite statements in the main process and DB worker - - `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events - - `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr - - `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal - - `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset - - `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset - -- 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. - -- If local Nx state gets weird before a rerun: - -```bash -pnpm nx reset -``` - -### agent-browser (global install) - -```bash -agent-browser --cdp 9222 tab list -agent-browser --cdp 9222 tab 1 -agent-browser --cdp 9222 snapshot -i -c -d 4 -agent-browser --cdp 9222 screenshot /tmp/iptvnator-cdp.png -``` - -### Fallback - -```bash -npx --yes agent-browser --cdp 9222 tab list -``` - -### DevTools Workaround - -```bash -ELECTRON_OPEN_DEVTOOLS=1 nx serve electron-backend -curl http://127.0.0.1:9222/json/list -agent-browser connect ws://127.0.0.1:9222/devtools/page/ -agent-browser screenshot /tmp/iptvnator-cdp.png -``` - -## Xtream Category Management - -The Electron Live TV, Movies, and Series category dialog applies Select/Deselect -to search results while a filter is active and to the whole type otherwise. -Button states use the matching group; "Total selected" counts the whole catalog. -Save persists the complete draft, Close discards it, and refresh restores hidden -categories by provider ID and type. See `docs/architecture/category-management.md`. - -## XMLTV Response Compression - -Electron decodes HTTP compression before the gzip file layer. For `.gz`/gzip -metadata plus HTTP gzip, a streaming signature check unwraps one remaining -file layer while preserving single-layer providers. Errors and cancellation -close the decoding chain. Contract: `docs/architecture/m3u-playlist-module.md` -("XMLTV response compression"). - -## XMLTV Source Removal - -Saving Settings → EPG reconciles cached XMLTV with committed global URLs and -all enabled M3U playlist sources. Startup runs the same reconciliation after -settings load and playlist migration. Ordinary saves skip unchanged normalized -source sets; an explicitly edited EPG field can retry a failed cleanup. -A cleanup failure after persistence still mirrors committed settings to Electron; -the form stays dirty for retry. Failed storage writes never mirror to main. -Failed settings reads and incomplete playlist migration never authorize pruning. -Removed sources retire queued/running imports and dismiss retained error rows -before worker-owned deletion. Retry waits for reconciliation and rechecks its -error row, including after trust-setting writes. Shared channel IDs survive while -another source has programmes or per-source channel metadata. The additive -`epg_channel_sources` table preserves each imported source's name, logo, URL and -timestamp plus transaction-ordered `write_order`, so removal restores the latest -surviving snapshot even when import timestamps tie; ambiguous legacy metadata -falls back to the XMLTV ID until reimport. Manual mappings remain user preferences, but no -longer resolve deleted data. Renderer lookup generations, Xtream previews and Stalker mapping-cache -invalidation prevent late results from restoring removed programmes. Provider -EPG is independent. See `docs/architecture/m3u-playlist-module.md` -("XMLTV source lifecycle"). - -## Web Backend Provider Redirects - -All four provider proxy routes use `ValidatedHttpClient`: automatic redirects -are disabled, the initial URL and at most five redirect hops pass full URL/DNS -validation, and fresh agents pin each connection to that hop's validated IPs. -Host/SNI and TLS verification remain intact; outbound environment proxies are -disabled. Private-network opt-in applies to the chain. Cross-origin redirects -strip session headers; original query params are not replayed. One portal -admission owns the entire chain and final body, with explicit redirect evidence -preventing destination failures from penalizing the initial endpoint. Contracts: -`docs/architecture/pwa-self-hosted.md` and -`docs/architecture/host-connectivity-guard.md`. - -## Portal Connectivity Preference - -- Half-open trial slots follow the complete request lifetime with no elapsed-time - expiry. All four Electron/web-backend portal handlers release in `finally`, - independently of outcome reporting; cleanup preserves trial/epoch ownership - and works while the environment override is disabled. Contract: - `docs/architecture/host-connectivity-guard.md` ("Trial ownership follows the - request lifetime"). -- Desktop Settings > General > Portal connections exposes default-on - `Settings.portalConnectivityGuard`. Only explicit false opts out. Save mirrors - the value to Electron `PORTAL_CONNECTIVITY_GUARD` and applies it without restart; - settings bootstrap restores it before the renderer loads. It controls Xtream - and Stalker together. Preference transitions clear cooldowns and invalidate old - request completions; unchanged saves preserve evidence. The environment switch - `IPTVNATOR_DISABLE_CONNECTIVITY_GUARD=1` remains authoritative. PWA clients do not - control the shared backend's guard. -- Both account-info dialogs explain guard refusals with localized paused-request - copy and Retry now; Stalker preserves cached account data on a failed refresh. - Contract: `docs/architecture/host-connectivity-guard.md`. - -## Live Channel Return - -Xtream and Stalker (including radio) capture displayed playback order on explicit -selection. Remote up/down, numbers and status use that queue while browsing -categories or search. Stalker commits after successful current URL resolution -and extends only loaded pages of the original scope. The conditional channel -header action clears search, returns to the accessible playing category and -focuses its row without changing playback. Contract: -`docs/architecture/remote-control.md` (Live channel return and playback order). - -## Stalker Live Search - -ITV sidebar and fullscreen searches independently filter the complete selected -category; only All Items searches the whole public catalog. Cached categories -search before windowing; missing/censored genres keep provider pagination, -including automatic continuation for short or empty search results. ITV search -never narrows shared provider pages or resets their index. Category changes -reset list windows and retain playback/active EPG. Contract: -`docs/architecture/stalker-portal.md` (Full ITV Channel List Cache). - -## Live TV Panel Levels - -Portal live layouts (Xtream `live`, Stalker `itv`/`radio`) fold their panels -from the outside in, in three nested levels owned by `LiveSidebarState` -(`@iptvnator/portal/shared/util`): `expanded` (categories rail + channels rail -+ player), `categories-hidden` (channels rail + player) and `collapsed` -(player only). `LiveLayoutSidebarStateService` is the single source of truth, per -surface (`m3u` / `portal` / `collection`; the levels apply to `portal`); the -shell context sidebar folds the categories rail on -`areCategoriesHiddenFor('portal')` (at level 2 only while the portal store has -a selected category — the live root has no channels header to host the way -back — and always at level 3), the channels rail folds on -`isCollapsedFor('portal')`. While the rail is folded the -channels header turns its title into a category dropdown that opens the same -`WorkspaceContextPanelComponent` as a CDK popover through the -`LIVE_CATEGORIES_POPOVER` token: the workspace shell provides -`WorkspaceLiveCategoriesPopoverService` (focus-trapped `role="dialog"`, -closed by backdrop, Escape, selection, its footer and any `NavigationStart`), -the live layouts reach it through `createLivePanelsController()` (level -flags, dropdown bridge and focus handoff in one shared object; the token is -optional). `Cmd/Ctrl+B`, the header toggle and the -floating restore handle return to the level the user collapsed from (the -target is session-only; every level is restored as stored per surface). -Folded rails carry `inert`, and -`handoffFocusOnLiveSidebarChange()` / `focusIfFocusLost()` move focus to the -replacement affordance only when the activated button was removed or inerted. -M3U and the unified live tab have no categories rail and treat level 2 like -level 1. Contract: `docs/architecture/iptvnator-ui-guidelines.md` -("Collapsible Live Sidebar"). - -## Channel and Detail Keyboard Scrolling - -Channel scroll owners use `ChannelScrollFocusDirective`; pointer selection -focuses the viewport, native scrolling survives virtual row recycling, and -row Enter/Space activation stays separate from focus movement. Portal Live TV -uses ArrowRight from the selected category and ArrowLeft from the channels -pane to move between columns. Shared live sidebars reserve scrollbar space -beside the resize handle. `PortalDetailShellComponent` owns a visible native -scrollbar and guarded initial page focus. Its sticky control and Escape close -inline playback to browse, then invoke the host's existing Back action; the -now-playing bar retains its separate direct route Back. Browse Escape requires -focus inside the shell; watch preserves the global close shortcut. Menus, -dialogs, fullscreen, editable fields, repeats and hidden/inert surfaces retain -their keys. M3U and collection bootstrap shells set `backAvailable=false` when -there is no browse return action. Contracts: -`docs/architecture/iptvnator-ui-guidelines.md` and -`docs/architecture/portal-detail-navigation.md`. - -## Xtream Connection Test - -Add/Edit source Test HTTPS and HTTP discloses plaintext credential use before -the click and can replace an unavailable HTTPS base with a -verified active HTTP base in the form. Only initial refused-port or TLS -wrong-version evidence permits the same-host attempt; HTTP errors, certificate -failures and redirect failures do not. Add/Save persists `serverUrl`, and the -routed session observes the metadata change. Passive checks never change the -protocol. Separate XMLTV and already-issued media/download URLs stay independent. -Contract: `docs/architecture/xtream-portal-compatibility.md` -("Explicit protocol discovery"). - -## Xtream Live Auto Format - -The routed Xtream live host supplies `liveAutoTsUrl` only for Auto with explicit -HLS+TS account evidence, using the canonical URL builder and original headers. -The same web player may try TS once after an owned initial terminal HTTP failure, -before `playing`; the old transport unmounts before the guarded render callback -starts TS. No player preference or playlist cache changes. Manual formats, -unknown formats, DRM, VOD/catch-up and stale sessions are excluded. External -MPV/VLC and Embedded MPV retain manual TS; Video.js segment retry cycles without -a terminal diagnostic also need manual TS. Contract and full support matrix: -`docs/architecture/xtream-portal-compatibility.md` (Initial Auto HLS failure). - -## Xtream Catch-Up Server Timezone - -The `{Y-m-d:H-M}` segment of a timeshift URL is read by the panel in ITS -timezone (`server_info.timezone`), never the viewer's (issue #1562). -`withPortal.checkPortalStatus()` normalizes it with -`resolveXtreamServerTimezone()` (`libs/shared/interfaces`, an ICU-resolvable -name, else a `UTC±HH:MM` derived from the `time_now`/`timestamp_now` clock -pair) and persists it on the playlist row through -`IXtreamDataSource.rememberServerTimezone` — Electron: one conditional -`json_set` UPDATE (`DB_SET_PLAYLIST_SERVER_TIMEZONE`) guarded by the row's -current connection; PWA: `PlaylistsService.transformPlaylistMeta` — because -the Favorites / Recent resolver reads the STORED row, not the store, and the -worker interleaves requests, so no read may precede the write. -`DB_GET_PLAYLIST` projects it back from the row payload, and a server URL -change drops it until the next account-info check. The same value converts -timestamp-less EPG -`start`/`end` strings. Contract: -`docs/architecture/xtream-portal-compatibility.md` ("Start time is the -panel's clock, not the viewer's"). - -## Radio / Audio Player - -M3U playlists can contain radio channels identified by the `radio="true"` attribute on `#EXTINF` lines. When a radio channel is selected: - -- The dedicated `AudioPlayerComponent` (`libs/ui/playback/src/lib/audio-player/`) renders instead of a video player -- The audio player always uses the built-in inline player — external player settings (MPV/VLC) are ignored -- The EPG panel is hidden (radio streams have no EPG data) -- The layout uses a cinematic hero pattern: the station logo is blurred as a full-area backdrop with a vignette overlay, and the artwork card + controls float above it -- Volume is shared with the video player via `localStorage` key `'volume'` -- Keyboard shortcuts: ArrowUp/ArrowDown (volume +/-5%), M (mute toggle) -- Radio detection in the video player template: `activeChannel.radio === 'true'` — this is a string comparison, not boolean - -Key files: - -- `libs/ui/playback/src/lib/audio-player/audio-player.component.ts` — the audio player component -- `libs/ui/playback/src/lib/audio-player/audio-player.component.scss` — cinematic hero styling -- `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html` — template conditionals for radio vs video -- `libs/shared/interfaces/src/lib/channel.interface.ts` — `radio: string` field on Channel interface - -## M3U Playback Mode - -`isLikelyM3uVod` in `libs/shared/m3u-utils` recognizes video-file extensions -and exact `/movie/`, `/movies/`, `/vod/`, `/series/` URL pathname segments, -independently of TMDB and `Settings.m3uVodDetails`. The M3U host's -`embeddedPlayback()` sets `isLive: false` for those entries or a catch-up URL; -the movie detail host forwards the same payload. Movie metadata recognition -still excludes episodes. Ordinary HLS/TS and unknown URLs without VOD evidence, -DASH and radio retain their existing behavior. Actual seeking requires source -support. Xtream/Stalker, external MPV/VLC payloads and session identity are -unchanged. Contract: `docs/architecture/m3u-playlist-module.md` -(M3U Playback Mode). - -## M3U URL User-Agent - -- `PlaylistsService.getPlaylist()` joins the per-playlist mutation queue so a - route opened during refresh reads after its pending save. Mutation-internal - reads keep using `getPlaylistById()` directly to avoid queue re-entry. -- The URL import form accepts an optional User-Agent and stores it as - `Playlist.userAgent`. Electron sends it on initial download, manual refresh, - and startup auto-update. The self-hosted PWA sends it through the registered - target `/parse` backend proxy for import and refresh; a matching backend is - required, and browser playback-header restrictions still apply. -- Reuse the existing source editor and channel-over-playlist playback header - precedence. Contract: `docs/architecture/m3u-playlist-module.md` - ("User-Agent for URL sources"). - -## Shared Player Controls - -- Stream info popover: an `info` button in the top-right corner of the shared - controls overlay shows live stream data — resolution + aspect ratio, measured - frame rate, video/audio codec and bitrate, audio channels and sample rate, - container, buffer, and dropped frames. Rendered only when the engine reports - something (`capabilities.streamStats`), sampled once a second and only while - the popover is open. Web engines read the `