perf(playback): keep the history gate off the initial bundle

The `@iptvnator/services` barrel ships in the initial bundle, so adding
PlaybackHistoryGate there (and subscribing to it from the app-wide
ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes.

- Move the gate to a new lazy-only `playback-data-access` project
  (`@iptvnator/playback/data-access`; scope:shared, domain:playback,
  type:data-access) and register it in the coverage policy.
- The gate subscribes to MPV/VLC session updates itself; it is created by
  the first deferred write, which precedes the launch it waits for.
  ExternalPlaybackService is back to master.
- The Xtream "playlist switched before confirmation" check moves to the
  lazy helper; the initial-path store only takes a `skipListRefresh` flag.

Net effect on this branch: +27 bytes over master (master itself is
108 bytes over the ratchet baseline already).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 13:41:31 +02:00
1 parent 671a0cb1ec
commit e41bcb9f65
34 files changed
+250 -128

No files matched your search

@@ -1,5 +1,3 @@
import { TestBed } from '@angular/core/testing';
import { PlaybackHistoryGate } from '@iptvnator/services';
import { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
import { ExternalPlaybackService } from './external-playback.service';
@@ -42,36 +40,7 @@ describe('ExternalPlaybackService', () => {
},
});
service = TestBed.runInInjectionContext(
() => new ExternalPlaybackService()
);
});
it('commits deferred history once the external player has opened', () => {
const commit = jest.fn();
TestBed.inject(PlaybackHistoryGate).defer(
{ streamUrls: ['https://example.com/video.m3u8'] },
commit
);
listener?.(createSession({ status: 'launching' }));
expect(commit).not.toHaveBeenCalled();
listener?.(createSession({ status: 'opened' }));
expect(commit).toHaveBeenCalledTimes(1);
});
it('does not commit history for a launch that failed', () => {
const commit = jest.fn();
TestBed.inject(PlaybackHistoryGate).defer(
{ streamUrls: ['https://example.com/video.m3u8'] },
commit
);
listener?.(createSession({ status: 'launching' }));
listener?.(createSession({ status: 'error', error: 'not found' }));
expect(commit).not.toHaveBeenCalled();
service = new ExternalPlaybackService();
});
it('tracks the latest launch and hides dismissed sessions until the next launch', () => {
@@ -1,5 +1,4 @@
import { computed, inject, Injectable, signal } from '@angular/core';
import { PlaybackHistoryGate } from '@iptvnator/services';
import { computed, Injectable, signal } from '@angular/core';
import {
ExternalPlayerSession,
PlayerContentInfo,
@@ -9,7 +8,6 @@ import {
providedIn: 'root',
})
export class ExternalPlaybackService {
private readonly historyGate = inject(PlaybackHistoryGate);
readonly activeSession = signal<ExternalPlayerSession | null>(null);
private readonly dismissedSessionId = signal<string | null>(null);
@@ -104,12 +102,6 @@ export class ExternalPlaybackService {
}
private handleSessionUpdate(session: ExternalPlayerSession): void {
// MPV/VLC cannot report whether a live stream really plays, so a
// successful launch is what commits the deferred history write.
if (session.status === 'opened' || session.status === 'playing') {
this.historyGate.confirm({ streamUrls: [session.streamUrl] });
}
const current = this.activeSession();
const restoresCurrentReplacement =
current?.id === session.restoredFromSessionId;
@@ -1137,9 +1137,9 @@ its stream has really played, so a stream that fails straight away never
reaches history.
- Writers do not persist on selection. They hand the write to
`PlaybackHistoryGate` (`@iptvnator/services`) with `defer(target, commit)`,
where the target is what the playback will be known by: the host's
`playbackSessionKey` (M3U) and/or stream URLs. The Stalker resolver defers
`PlaybackHistoryGate` (`@iptvnator/playback/data-access`) with
`defer(target, commit)`, where the target is what the playback will be
known by: the host's `playbackSessionKey` (M3U) and/or stream URLs. The Stalker resolver defers
by the resolved (possibly temporary) link; the persisted row still stores
the portal `cmd`, never that link. Writers capture the item and its
playlist when they defer, so navigating meanwhile cannot misfile it; an
@@ -1160,9 +1160,11 @@ reaches history.
engine or format swap of the same stream keeps its progress. The radio
`AudioPlayerComponent` confirms the same way (with the host's session key
when given).
- MPV/VLC cannot report whether a live stream plays, so the Electron
`ExternalPlaybackService` confirms a session's `streamUrl` once it is
`opened` or `playing`; a launch that ends in `error` is not recorded. That
- MPV/VLC cannot report whether a live stream plays, so the gate itself
subscribes to the Electron external-player session updates and confirms a
session's `streamUrl` once it is `opened` or `playing`; a launch that ends
in `error` is not recorded. (Subscribing in the gate, which the first
deferred write creates, keeps it off the initial bundle.) That
confirmation carries no session key, so an "Open in MPV/VLC" recovery
launch is also confirmed by the `WebPlayerViewComponent` that requested
it, under its own session key, once the launch has opened. M3U keeps
@@ -189,6 +189,14 @@ other utility projects, including shared interface contracts, while
`ui-playback` and feature hosts may depend on it to render and execute
session-local recovery actions.
`libs/playback/data-access` is the `playback-data-access` Nx project,
imported through `@iptvnator/playback/data-access` (tags `scope:shared`,
`domain:playback`, `type:data-access`). It owns renderer playback state that
store, feature and player layers share — currently `PlaybackHistoryGate`,
which holds "recently viewed" writes until playback is confirmed. It is kept
out of `@iptvnator/services` on purpose: that barrel ships in the initial
bundle, and this project is only reached from lazy player and portal code.
## Project Tags
Every Nx project keeps one tag from each family in `project.json`:
+22
View File
@@ -0,0 +1,22 @@
export default {
displayName: 'playback-data-access',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/playback/data-access',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
moduleFileExtensions: ['ts', 'js', 'html'],
};
+21
View File
@@ -0,0 +1,21 @@
{
"name": "playback-data-access",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/playback/data-access/src",
"prefix": "lib",
"projectType": "library",
"tags": ["scope:shared", "domain:playback", "type:data-access"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/playback/data-access/jest.config.ts",
"tsConfig": "libs/playback/data-access/tsconfig.spec.json"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/playback-history-gate.service';
@@ -1,3 +1,4 @@
import type { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
import { PlaybackHistoryGate } from './playback-history-gate.service';
describe('PlaybackHistoryGate', () => {
@@ -137,6 +138,58 @@ describe('PlaybackHistoryGate', () => {
expect(oldest).not.toHaveBeenCalled();
});
describe('MPV/VLC sessions', () => {
const originalElectron = window.electron;
let emit: (session: Partial<ExternalPlayerSession>) => void;
beforeEach(() => {
Object.defineProperty(window, 'electron', {
configurable: true,
value: {
onExternalPlayerSessionUpdate: (
callback: (session: ExternalPlayerSession) => void
) => {
emit = (session) =>
callback({
streamUrl: 'http://vod/1.mkv',
...session,
} as ExternalPlayerSession);
return () => undefined;
},
},
});
gate = new PlaybackHistoryGate();
});
afterEach(() => {
Object.defineProperty(window, 'electron', {
configurable: true,
value: originalElectron,
});
});
it('confirms a stream once its external player has opened', () => {
const commit = jest.fn();
gate.defer({ streamUrls: ['http://vod/1.mkv'] }, commit);
emit({ status: 'launching' });
expect(commit).not.toHaveBeenCalled();
emit({ status: 'opened' });
expect(commit).toHaveBeenCalledTimes(1);
});
it('does not confirm a launch that failed', () => {
const commit = jest.fn();
gate.defer({ streamUrls: ['http://vod/1.mkv'] }, commit);
emit({ status: 'launching' });
emit({ status: 'error' });
expect(commit).not.toHaveBeenCalled();
});
});
it('keeps committing the other writes when one throws', () => {
const consoleError = jest
.spyOn(console, 'error')
@@ -53,6 +53,19 @@ const MAX_PENDING_HISTORY_WRITES = 20;
export class PlaybackHistoryGate {
private pending: PendingHistoryWrite[] = [];
constructor() {
// MPV/VLC cannot report whether a live stream plays, so a launch
// that opened is the confirmation. Subscribed here rather than in
// the app-wide external-playback service so the gate stays off the
// initial bundle: it is created by the first deferred write, which
// always precedes the launch it waits for.
window.electron?.onExternalPlayerSessionUpdate?.((session) => {
if (session.status === 'opened' || session.status === 'playing') {
this.confirm({ streamUrls: [session.streamUrl] });
}
});
}
defer(target: PlaybackHistoryTarget, commit: () => void): void {
const normalized = normalizeTarget(target);
if (!normalized.sessionKey && normalized.streamUrls.size === 0) {
@@ -0,0 +1,9 @@
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+29
View File
@@ -0,0 +1,29 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"isolatedModules": true,
"target": "es2022",
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
}
@@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": false,
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"],
"moduleResolution": "node10"
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
@@ -10,12 +10,12 @@ import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
import { PORTAL_EXTERNAL_PLAYBACK } from '@iptvnator/portal/shared/util';
import {
DataService,
PlaybackHistoryGate,
PlaylistsService,
RuntimeCapabilitiesService,
SettingsStore,
TmdbEnrichmentService,
} from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import {
PlaylistMeta,
Settings,
@@ -114,13 +114,13 @@ import { createPlaybackSessionKey } from '@iptvnator/playback/util';
import { ChannelListLoadingStateComponent } from '@iptvnator/ui/components';
import {
DataService,
PlaybackHistoryGate,
PlaylistsService,
RecordingsService,
RuntimeCapabilitiesService,
SettingsStore,
TmdbEnrichmentService,
} from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import {
Channel,
createDevLogger,
@@ -8,7 +8,7 @@ import {
PORTAL_PLAYER,
UnifiedCollectionItem,
} from '@iptvnator/portal/shared/util';
import { PlaybackHistoryGate } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { ElectronStreamHeadersService } from '@iptvnator/ui/playback';
import { UnifiedLiveTimeshift } from './unified-live-catchup';
import { UnifiedLiveSelectionGeneration } from './unified-live-selection-generation';
@@ -26,11 +26,8 @@ import {
} from '@iptvnator/ui/epg';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ResizableDirective } from '@iptvnator/ui/components';
import {
PlaybackHistoryGate,
RuntimeCapabilitiesService,
SettingsStore,
} from '@iptvnator/services';
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { EpgItem, EpgProgram, VideoPlayer } from '@iptvnator/shared/interfaces';
import {
PORTAL_PLAYER,
@@ -9,11 +9,8 @@ import {
} from '@iptvnator/ui/playback';
import { EpgListViewComponent, EpgTimelineComponent } from '@iptvnator/ui/epg';
import { ResizableDirective } from '@iptvnator/ui/components';
import {
PlaybackHistoryGate,
RuntimeCapabilitiesService,
SettingsStore,
} from '@iptvnator/services';
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { EpgProgram, VideoPlayer } from '@iptvnator/shared/interfaces';
import {
PORTAL_PLAYER,
@@ -4,11 +4,8 @@ import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { PORTAL_PLAYER } from '@iptvnator/portal/shared/util';
import {
DataService,
PlaybackHistoryGate,
PlaylistsService,
} from '@iptvnator/services';
import { DataService, PlaylistsService } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { of } from 'rxjs';
import {
PlaylistMeta,
@@ -4,11 +4,8 @@ import { signalStoreFeature, withMethods } from '@ngrx/signals';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { PORTAL_PLAYER, createLogger } from '@iptvnator/portal/shared/util';
import {
DataService,
PlaybackHistoryGate,
PlaylistsService,
} from '@iptvnator/services';
import { DataService, PlaylistsService } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import {
PlaylistMeta,
ResolvedPortalPlayback,
@@ -1,6 +1,6 @@
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { signalStore, withState } from '@ngrx/signals';
import { signalStore } from '@ngrx/signals';
import { of } from 'rxjs';
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
import { XTREAM_DATA_SOURCE } from './data-sources/xtream-data-source.interface';
@@ -16,11 +16,6 @@ jest.mock('@iptvnator/portal/shared/util', () => ({
}));
const TestRecentItemsStore = signalStore(withRecentItems());
/** Like `XtreamStore`: `withPortal` (current playlist) precedes the feature. */
const TestCurrentPlaylistStore = signalStore(
withState({ currentPlaylist: { id: 'playlist-2' } }),
withRecentItems()
);
describe('withRecentItems', () => {
const originalElectron = window.electron;
@@ -100,7 +95,6 @@ describe('withRecentItems', () => {
TestBed.configureTestingModule({
providers: [
TestRecentItemsStore,
TestCurrentPlaylistStore,
{
provide: DatabaseService,
useValue: databaseService,
@@ -163,9 +157,7 @@ describe('withRecentItems', () => {
]);
});
it('saves a late write to its playlist without replacing the current playlist list', async () => {
// Playback confirmed after the user moved on to playlist-2.
const currentStore = TestBed.inject(TestCurrentPlaylistStore);
it('saves without replacing the list when the playlist is no longer current', async () => {
dataSource.getContentByXtreamId.mockResolvedValue({
id: 3941697,
title: 'Krypton',
@@ -173,10 +165,11 @@ describe('withRecentItems', () => {
xtream_id: 290,
});
currentStore.addRecentItem({
store.addRecentItem({
xtreamId: 290,
contentType: 'series',
playlist: signal({ id: 'playlist-1' }),
skipListRefresh: true,
});
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -186,7 +179,7 @@ describe('withRecentItems', () => {
undefined
);
expect(dataSource.getRecentItems).not.toHaveBeenCalled();
expect(currentStore.recentItems()).toEqual([]);
expect(store.recentItems()).toEqual([]);
});
it('uses the Xtream ID as the PWA recent key when cached content is cold', async () => {
@@ -50,21 +50,6 @@ function mapDbRecentItem(
};
}
/**
* Whether `playlistId` is the store's current playlist. `withPortal`, composed
* before this feature in `XtreamStore`, owns it; a store without one (a
* feature-only test store) treats every playlist as current.
*/
function isCurrentPlaylist(store: object, playlistId: string): boolean {
const currentPlaylist = (
store as {
currentPlaylist?: () => { id?: string } | null | undefined;
}
).currentPlaylist;
const currentId = currentPlaylist?.()?.id;
return !currentId || currentId === playlistId;
}
export const withRecentItems = function () {
const logger = createLogger('withRecentItems');
return signalStoreFeature(
@@ -100,6 +85,8 @@ export const withRecentItems = function () {
contentType: 'live' | 'movie' | 'series';
playlist: Signal<{ id: string } | null | undefined>;
backdropUrl?: string;
/** Save only: `playlist` is no longer the one in state. */
skipListRefresh?: boolean;
}>(
pipe(
switchMap(
@@ -108,6 +95,7 @@ export const withRecentItems = function () {
contentType,
playlist,
backdropUrl,
skipListRefresh,
}) => {
const playlistId = playlist()?.id;
const normalizedXtreamId = Number(xtreamId);
@@ -137,13 +125,10 @@ export const withRecentItems = function () {
playlistId,
backdropUrl
);
// A write confirmed after the user switched
// playlists is saved to its own playlist,
// but the list in state is the current one's.
if (!isCurrentPlaylist(store, playlistId)) {
if (skipListRefresh) {
return;
}
// Reload after add/update so re-watched items
// immediately move to the top in recently-viewed.
const items =
@@ -25,10 +25,8 @@ import {
} from '@iptvnator/portal/shared/util';
import type { SeasonEpisodeDownloadAdapter } from '@iptvnator/portal/shared/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import {
PlaybackHistoryGate,
PlaybackPositionRuntimeBridgeService,
} from '@iptvnator/services';
import { PlaybackPositionRuntimeBridgeService } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { PlaybackPositionData } from '@iptvnator/shared/interfaces';
import { PortalInlinePlayerComponent } from '@iptvnator/ui/playback';
import { BehaviorSubject, EMPTY, of } from 'rxjs';
@@ -577,6 +575,7 @@ describe('SerialDetailsComponent', () => {
contentType: 'series',
playlist: expect.any(Function),
backdropUrl: undefined,
skipListRefresh: false,
});
expect(addRecentItem.mock.calls[0][0].playlist()).toEqual(
currentPlaylist()
@@ -6,10 +6,8 @@ import {
PORTAL_PLAYER,
} from '@iptvnator/portal/shared/util';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import {
PlaybackHistoryGate,
PlaybackPositionRuntimeBridgeService,
} from '@iptvnator/services';
import { PlaybackPositionRuntimeBridgeService } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import type {
PlaybackPositionData,
PlayerContentInfo,
@@ -173,6 +171,8 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
expect.objectContaining({
xtreamId: ROUTE_VOD_ID,
contentType: 'movie',
// Saved to its own playlist; the store's list is now another's.
skipListRefresh: true,
})
);
expect(recentItem.playlist()).toEqual({ id: ROUTE_PLAYLIST });
@@ -1,6 +1,6 @@
import { inject, signal } from '@angular/core';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { PlaybackHistoryGate } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
export interface XtreamRecentItemRequest {
readonly xtreamId: number | string;
@@ -13,7 +13,8 @@ export interface XtreamRecentItemRequest {
* played — inline for a couple of seconds, or launched in MPV/VLC — so a
* source that fails straight away never reaches history or the dashboard
* hero. The playlist is captured when playback starts, so navigating to
* another one meanwhile cannot misfile the item.
* another one meanwhile cannot misfile the item — nor replace the recent
* list the store now holds for the playlist the user switched to.
*
* Must run in an injection context.
*/
@@ -27,7 +28,11 @@ export function injectXtreamRecentHistory(): (
return (streamUrl, request) => {
const playlist = signal(store.currentPlaylist()).asReadonly();
gate.defer({ streamUrls: [streamUrl] }, () =>
store.addRecentItem({ ...request, playlist })
store.addRecentItem({
...request,
playlist,
skipListRefresh: store.currentPlaylist()?.id !== playlist()?.id,
})
);
};
}
-1
View File
@@ -6,7 +6,6 @@ export * from './lib/database-electron.service';
export * from './lib/downloads.service';
export * from './lib/recordings.service';
export * from './lib/playback-position-runtime-bridge.service';
export * from './lib/playback-history-gate.service';
export * from './lib/playback-position.service';
export * from './lib/playlist-delete-cleanup.token';
export * from './lib/playlist-delete-action.service';
@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { PlaybackHistoryGate } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { AudioPlayerComponent } from './audio-player.component';
describe('AudioPlayerComponent', () => {
@@ -22,7 +22,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { ChannelActions } from '@iptvnator/m3u-state';
import { PlaybackHistoryGate } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import { PlaybackHistoryConfirmation } from '../playback-history/playback-history-confirmation';
@Component({
@@ -1,4 +1,4 @@
import type { PlaybackHistoryTarget } from '@iptvnator/services';
import type { PlaybackHistoryTarget } from '@iptvnator/playback/data-access';
import { PlaybackHistoryConfirmation } from './playback-history-confirmation';
describe('PlaybackHistoryConfirmation', () => {
@@ -1,7 +1,7 @@
import type {
PlaybackHistoryGate,
PlaybackHistoryTarget,
} from '@iptvnator/services';
} from '@iptvnator/playback/data-access';
import { PlaybackProgressConfirmation } from './playback-progress-confirmation';
export interface PlaybackHistoryConfirmationOptions {
@@ -11,11 +11,8 @@ import {
PlaybackDiagnosticCode,
PlaybackDiagnosticSource,
} from '@iptvnator/playback/util';
import {
PlaybackHistoryGate,
RuntimeCapabilitiesService,
SettingsStore,
} from '@iptvnator/services';
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import {
STORE_KEY,
VideoPlayer,
@@ -22,11 +22,8 @@ import {
type PlaybackRecommendationTarget,
} from '@iptvnator/playback/util';
import { PORTAL_EXTERNAL_PLAYBACK } from '@iptvnator/portal/shared/util';
import {
PlaybackHistoryGate,
RuntimeCapabilitiesService,
SettingsStore,
} from '@iptvnator/services';
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
import {
VideoPlayer,
type Channel,
+7
View File
@@ -89,6 +89,13 @@
"validationCommand": "pnpm nx test epg-data-access",
"e2eTags": ["@epg"]
},
{
"name": "playback-data-access",
"root": "libs/playback/data-access",
"sourceRoot": "libs/playback/data-access/src",
"validationCommand": "pnpm nx test playback-data-access",
"e2eTags": ["@persistence"]
},
{
"name": "m3u-state",
"root": "libs/m3u-state",
+3
View File
@@ -33,6 +33,9 @@
"libs/portal/shared/util/src/index.ts"
],
"@iptvnator/epg/data-access": ["libs/epg/data-access/src/index.ts"],
"@iptvnator/playback/data-access": [
"libs/playback/data-access/src/index.ts"
],
"@iptvnator/playback/util": ["libs/playback/util/src/index.ts"],
"@iptvnator/playlist/shared/ui": [
"libs/playlist/shared/ui/src/index.ts"