From 4603bd83d516094daa88cd716b4d458b0e6a30e5 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 22 May 2026 04:19:05 +0300 Subject: [PATCH] refactor(runtime): gate playlist refresh by capability --- docs/architecture/pwa-self-hosted.md | 3 +- .../playlist-refresh-action.service.spec.ts | 24 ++++++-- .../lib/playlist-refresh-action.service.ts | 18 ++++-- .../playlist-item.component.html | 4 +- .../playlist-item.component.spec.ts | 57 ++++++++++++++++-- .../playlist-item/playlist-item.component.ts | 4 +- .../recent-playlists.component.spec.ts | 59 ++++++++++++------- .../recent-playlists.component.ts | 21 +++++-- .../lib/runtime-capabilities.service.spec.ts | 25 ++++++++ .../src/lib/runtime-capabilities.service.ts | 8 +++ 10 files changed, 177 insertions(+), 46 deletions(-) diff --git a/docs/architecture/pwa-self-hosted.md b/docs/architecture/pwa-self-hosted.md index c9aba55c7..612c98168 100644 --- a/docs/architecture/pwa-self-hosted.md +++ b/docs/architecture/pwa-self-hosted.md @@ -71,7 +71,8 @@ feature decisions expressed as capabilities such as `supportsEpg`, `supportsSqlite`, `supportsXtreamSqliteDataSource`, `supportsDownloads`, or `supportsManagedExternalPlayers` so PWA and Electron behavior stays auditable from one shared boundary. `supportsDownloads` requires the complete downloads -preload API surface used by `DownloadsService`, and +preload API surface used by `DownloadsService`, `supportsPlaylistRefresh` +requires the native playlist refresh/cancel/progress bridge, and `supportsManagedExternalPlayers` requires the MPV and VLC preload launch methods (`openInMpv` and `openInVlc`); a partial Electron bridge must not expose desktop-only actions in the PWA/shared UI. diff --git a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts index 1e5821d0a..7a92a7c94 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts @@ -77,7 +77,8 @@ describe('PlaylistRefreshActionService', () => { getAllPlaybackPositions: jest.Mock; }; let runtime: { - isElectron: boolean; + supportsPlaylistRefresh: boolean; + supportsXtreamSqliteDataSource: boolean; }; let routeProvider: ReturnType< typeof signal<'playlists' | 'xtreams' | null> @@ -131,7 +132,8 @@ describe('PlaylistRefreshActionService', () => { getAllPlaybackPositions: jest.fn().mockResolvedValue([]), }; runtime = { - isElectron: true, + supportsPlaylistRefresh: true, + supportsXtreamSqliteDataSource: true, }; routeProvider = signal<'playlists' | 'xtreams' | null>('xtreams'); resolvedPlaylistId = signal(null); @@ -195,8 +197,8 @@ describe('PlaylistRefreshActionService', () => { localStorage.clear(); }); - it('treats file-backed M3U playlists as refreshable in Electron', () => { - runtime.isElectron = true; + it('treats file-backed M3U playlists as refreshable when the refresh bridge is available', () => { + runtime.supportsPlaylistRefresh = true; expect( service.canRefresh( @@ -210,8 +212,8 @@ describe('PlaylistRefreshActionService', () => { ).toBe(true); }); - it('does not expose filesystem refresh outside Electron', () => { - runtime.isElectron = false; + it('does not expose file-backed M3U refresh without the refresh bridge', () => { + runtime.supportsPlaylistRefresh = false; expect( service.canRefresh( @@ -225,6 +227,16 @@ describe('PlaylistRefreshActionService', () => { ).toBe(false); }); + it('treats Xtream playlists as refreshable only when the SQLite data source is available', () => { + runtime.supportsXtreamSqliteDataSource = true; + + expect(service.canRefresh(createPlaylistMeta())).toBe(true); + + runtime.supportsXtreamSqliteDataSource = false; + + expect(service.canRefresh(createPlaylistMeta())).toBe(false); + }); + it('stores Xtream restore data before updating playlist meta and navigating', async () => { const item = createPlaylistMeta(); const executionOrder: string[] = []; diff --git a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts index 1d9e113fe..aa5a5b099 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts @@ -48,11 +48,18 @@ export class PlaylistRefreshActionService { readonly refreshPreparation = this.refreshPreparationState.asReadonly(); canRefresh(playlist: PlaylistMeta | null): boolean { - if (!playlist || !this.runtime.isElectron) { + if (!playlist) { return false; } - return Boolean(playlist.serverUrl || playlist.url || playlist.filePath); + if (playlist.serverUrl) { + return this.runtime.supportsXtreamSqliteDataSource; + } + + return ( + this.runtime.supportsPlaylistRefresh && + Boolean(playlist.url || playlist.filePath) + ); } refresh(playlist: PlaylistMeta): void { @@ -60,9 +67,12 @@ export class PlaylistRefreshActionService { return; } - if (playlist.serverUrl) { + if (playlist.serverUrl && this.runtime.supportsXtreamSqliteDataSource) { this.refreshXtream(playlist); - } else if (playlist.url || playlist.filePath) { + } else if ( + this.runtime.supportsPlaylistRefresh && + (playlist.url || playlist.filePath) + ) { void this.refreshM3u(playlist); } } diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html index b9856fc0d..371a490e2 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html @@ -167,7 +167,7 @@ close } - @if (item.url || item.filePath) { + @if (item.url || (item.filePath && supportsPlaylistRefresh)) {