mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
refactor(portal): gate activity storage by runtime capability
This commit is contained in:
1 parent
03586d934a
commit
790dbb7062
7 files changed
+179
-38
No files matched your search
+49
@@ -17,8 +17,19 @@ describe('UnifiedFavoritesDataService', () => {
|
||||
let service: UnifiedFavoritesDataService;
|
||||
let electronApi: {
|
||||
dbAddFavorite: jest.Mock;
|
||||
dbAddRecentItem: jest.Mock;
|
||||
dbClearPlaylistRecentItems: jest.Mock;
|
||||
dbClearRecentlyViewed: jest.Mock;
|
||||
dbGetAllGlobalFavorites: jest.Mock;
|
||||
dbGetContentByXtreamId: jest.Mock;
|
||||
dbGetFavorites: jest.Mock;
|
||||
dbGetGlobalRecentlyAdded: jest.Mock;
|
||||
dbGetRecentItems: jest.Mock;
|
||||
dbGetRecentlyViewed: jest.Mock;
|
||||
dbReorderGlobalFavorites: jest.Mock;
|
||||
dbRemoveFavorite: jest.Mock;
|
||||
dbRemoveRecentItem: jest.Mock;
|
||||
dbRemoveRecentItemsBatch: jest.Mock;
|
||||
};
|
||||
let databaseService: {
|
||||
getAllGlobalFavorites: jest.Mock;
|
||||
@@ -99,8 +110,21 @@ describe('UnifiedFavoritesDataService', () => {
|
||||
beforeEach(() => {
|
||||
electronApi = {
|
||||
dbAddFavorite: jest.fn().mockResolvedValue({ success: true }),
|
||||
dbAddRecentItem: jest.fn(),
|
||||
dbClearPlaylistRecentItems: jest.fn(),
|
||||
dbClearRecentlyViewed: jest.fn(),
|
||||
dbGetAllGlobalFavorites: jest.fn(),
|
||||
dbGetContentByXtreamId: jest.fn(),
|
||||
dbGetFavorites: jest.fn(),
|
||||
dbGetGlobalRecentlyAdded: jest.fn(),
|
||||
dbGetRecentItems: jest.fn(),
|
||||
dbGetRecentlyViewed: jest.fn(),
|
||||
dbReorderGlobalFavorites: jest.fn().mockResolvedValue({
|
||||
success: true,
|
||||
}),
|
||||
dbRemoveFavorite: jest.fn().mockResolvedValue(undefined),
|
||||
dbRemoveRecentItem: jest.fn(),
|
||||
dbRemoveRecentItemsBatch: jest.fn(),
|
||||
};
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: electronApi as Window['electron'],
|
||||
@@ -522,6 +546,31 @@ describe('UnifiedFavoritesDataService', () => {
|
||||
expect(electronApi.dbAddFavorite).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the active Xtream data source when the Electron bridge lacks activity storage methods', async () => {
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: {} as Window['electron'],
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await service.removeFavorite({
|
||||
uid: 'xtream::xtream-1::movie:101',
|
||||
name: 'Partial Bridge Movie',
|
||||
contentType: 'movie',
|
||||
sourceType: 'xtream',
|
||||
playlistId: 'xtream-1',
|
||||
playlistName: 'Xtream One',
|
||||
posterUrl: 'movie.png',
|
||||
xtreamId: 101,
|
||||
contentId: 1010,
|
||||
} satisfies UnifiedCollectionItem);
|
||||
|
||||
expect(xtreamDataSource.removeFavorite).toHaveBeenCalledWith(
|
||||
1010,
|
||||
'xtream-1'
|
||||
);
|
||||
expect(electronApi.dbRemoveFavorite).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds Stalker favorites through portal favorites', async () => {
|
||||
await service.addFavorite({
|
||||
uid: 'stalker::stalker-1::101',
|
||||
|
||||
+33
-19
@@ -3,7 +3,11 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistActions, selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
|
||||
import { firstValueFrom, map } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
|
||||
import {
|
||||
DatabaseService,
|
||||
PlaylistsService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
Channel,
|
||||
extractStalkerItemId,
|
||||
@@ -47,6 +51,7 @@ export class UnifiedFavoritesDataService {
|
||||
private readonly dbService = inject(DatabaseService);
|
||||
private readonly playlistsService = inject(PlaylistsService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly xtreamDataSource = inject(XTREAM_DATA_SOURCE);
|
||||
|
||||
async getFavorites(
|
||||
@@ -90,12 +95,13 @@ export class UnifiedFavoritesDataService {
|
||||
await this.setM3uFavorites(item.playlistId, filtered);
|
||||
break;
|
||||
}
|
||||
case 'xtream':
|
||||
case 'xtream': {
|
||||
if (item.contentId == null) {
|
||||
return;
|
||||
}
|
||||
if (window.electron) {
|
||||
await window.electron.dbRemoveFavorite(
|
||||
const electron = this.electronActivityBridge;
|
||||
if (electron) {
|
||||
await electron.dbRemoveFavorite(
|
||||
item.contentId,
|
||||
item.playlistId
|
||||
);
|
||||
@@ -106,6 +112,7 @@ export class UnifiedFavoritesDataService {
|
||||
item.playlistId
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'stalker': {
|
||||
const sourceItemId = item.uid.split('::')[2];
|
||||
await firstValueFrom(
|
||||
@@ -157,7 +164,8 @@ export class UnifiedFavoritesDataService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
const electron = this.electronActivityBridge;
|
||||
if (!electron) {
|
||||
await this.xtreamDataSource.addFavorite(
|
||||
contentId,
|
||||
item.playlistId,
|
||||
@@ -166,7 +174,7 @@ export class UnifiedFavoritesDataService {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electron.dbAddFavorite(
|
||||
await electron.dbAddFavorite(
|
||||
contentId,
|
||||
item.playlistId,
|
||||
item.posterUrl ?? item.logo ?? undefined
|
||||
@@ -287,21 +295,22 @@ export class UnifiedFavoritesDataService {
|
||||
}
|
||||
|
||||
const xtreamUpdates = this.buildXtreamPositionUpdates(items);
|
||||
const electron = this.electronActivityBridge;
|
||||
|
||||
if (
|
||||
options?.scope === 'playlist' &&
|
||||
options.playlistId &&
|
||||
options.portalType === 'xtream'
|
||||
) {
|
||||
if (xtreamUpdates.length > 0 && window.electron) {
|
||||
await window.electron.dbReorderGlobalFavorites(xtreamUpdates);
|
||||
if (xtreamUpdates.length > 0 && electron) {
|
||||
await electron.dbReorderGlobalFavorites(xtreamUpdates);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
xtreamUpdates.length > 0 && window.electron
|
||||
? window.electron.dbReorderGlobalFavorites(xtreamUpdates)
|
||||
xtreamUpdates.length > 0 && electron
|
||||
? electron.dbReorderGlobalFavorites(xtreamUpdates)
|
||||
: Promise.resolve(),
|
||||
this.saveOrder(items.map((item) => item.uid)),
|
||||
]);
|
||||
@@ -356,7 +365,7 @@ export class UnifiedFavoritesDataService {
|
||||
private async clearXtreamFavorites(
|
||||
items: UnifiedCollectionItem[]
|
||||
): Promise<void> {
|
||||
const electron = window.electron;
|
||||
const electron = this.electronActivityBridge;
|
||||
if (!electron) {
|
||||
await Promise.all(
|
||||
items
|
||||
@@ -514,7 +523,7 @@ export class UnifiedFavoritesDataService {
|
||||
}
|
||||
|
||||
private async getXtreamAllFavorites(): Promise<UnifiedCollectionItem[]> {
|
||||
if (!window.electron) {
|
||||
if (!this.electronActivityBridge) {
|
||||
const allMeta = await this.getAllMeta();
|
||||
const results: UnifiedCollectionItem[] = [];
|
||||
for (const meta of allMeta.filter(
|
||||
@@ -527,7 +536,6 @@ export class UnifiedFavoritesDataService {
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!window.electron?.dbGetAllGlobalFavorites) return [];
|
||||
try {
|
||||
const rows =
|
||||
(await this.dbService.getAllGlobalFavorites()) as XtreamFavoriteRow[];
|
||||
@@ -542,7 +550,7 @@ export class UnifiedFavoritesDataService {
|
||||
): Promise<UnifiedCollectionItem[]> {
|
||||
try {
|
||||
const meta = await this.getPlaylistMeta(playlistId);
|
||||
if (!window.electron) {
|
||||
if (!this.electronActivityBridge) {
|
||||
const rows =
|
||||
await this.xtreamDataSource.getFavorites(playlistId);
|
||||
return rows.map((row) =>
|
||||
@@ -568,7 +576,7 @@ export class UnifiedFavoritesDataService {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
if (!this.electronActivityBridge) {
|
||||
const content = await this.xtreamDataSource.getContentByXtreamId(
|
||||
item.xtreamId,
|
||||
item.playlistId,
|
||||
@@ -721,9 +729,9 @@ export class UnifiedFavoritesDataService {
|
||||
}
|
||||
|
||||
private async getSavedOrder(): Promise<string[]> {
|
||||
if (!window.electron?.dbGetAppState) return [];
|
||||
if (!this.runtime.hasElectronMethod('dbGetAppState')) return [];
|
||||
try {
|
||||
const raw = await window.electron.dbGetAppState(
|
||||
const raw = await window.electron?.dbGetAppState(
|
||||
GLOBAL_FAVORITES_ORDER_KEY
|
||||
);
|
||||
return raw ? (JSON.parse(raw) as string[]) : [];
|
||||
@@ -733,9 +741,9 @@ export class UnifiedFavoritesDataService {
|
||||
}
|
||||
|
||||
private async saveOrder(uidOrder: string[]): Promise<void> {
|
||||
if (!window.electron?.dbSetAppState) return;
|
||||
if (!this.runtime.hasElectronMethod('dbSetAppState')) return;
|
||||
try {
|
||||
await window.electron.dbSetAppState(
|
||||
await window.electron?.dbSetAppState(
|
||||
GLOBAL_FAVORITES_ORDER_KEY,
|
||||
JSON.stringify(uidOrder)
|
||||
);
|
||||
@@ -845,4 +853,10 @@ export class UnifiedFavoritesDataService {
|
||||
|
||||
return extractStalkerItemId(favorite);
|
||||
}
|
||||
|
||||
private get electronActivityBridge(): Window['electron'] | undefined {
|
||||
return this.runtime.supportsPortalActivityStorage
|
||||
? window.electron
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
+43
-1
@@ -94,9 +94,27 @@ describe('UnifiedRecentDataService', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const createPortalActivityElectronApi = (): Window['electron'] =>
|
||||
({
|
||||
dbGetRecentlyViewed: jest.fn(),
|
||||
dbClearRecentlyViewed: jest.fn(),
|
||||
dbGetAllGlobalFavorites: jest.fn(),
|
||||
dbGetGlobalRecentlyAdded: jest.fn(),
|
||||
dbAddFavorite: jest.fn(),
|
||||
dbRemoveFavorite: jest.fn(),
|
||||
dbGetFavorites: jest.fn(),
|
||||
dbReorderGlobalFavorites: jest.fn(),
|
||||
dbGetRecentItems: jest.fn(),
|
||||
dbAddRecentItem: jest.fn(),
|
||||
dbClearPlaylistRecentItems: jest.fn(),
|
||||
dbRemoveRecentItem: jest.fn(),
|
||||
dbRemoveRecentItemsBatch: jest.fn(),
|
||||
dbGetContentByXtreamId: jest.fn(),
|
||||
}) as unknown as Window['electron'];
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: {} as Window['electron'],
|
||||
value: createPortalActivityElectronApi(),
|
||||
configurable: true,
|
||||
});
|
||||
store = {
|
||||
@@ -295,6 +313,30 @@ describe('UnifiedRecentDataService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the active Xtream data source when the Electron bridge lacks activity storage methods', async () => {
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: {} as Window['electron'],
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await service.removeRecentItem({
|
||||
uid: 'xtream::xtream-1::movie:290',
|
||||
name: 'Partial Bridge Movie',
|
||||
contentType: 'movie',
|
||||
sourceType: 'xtream',
|
||||
playlistId: 'xtream-1',
|
||||
playlistName: 'Xtream One',
|
||||
xtreamId: 290,
|
||||
contentId: 444,
|
||||
} satisfies UnifiedCollectionItem);
|
||||
|
||||
expect(xtreamDataSource.removeRecentItem).toHaveBeenCalledWith(
|
||||
444,
|
||||
'xtream-1'
|
||||
);
|
||||
expect(dbService.removeRecentItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('builds distinct Xtream recent UIDs when live and series share an xtream id', async () => {
|
||||
store.select.mockReturnValue(
|
||||
of([
|
||||
|
||||
@@ -2,7 +2,11 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { PlaylistActions, selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
|
||||
import { firstValueFrom, map } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
|
||||
import {
|
||||
DatabaseService,
|
||||
PlaylistsService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
Channel,
|
||||
extractStalkerItemId,
|
||||
@@ -40,6 +44,7 @@ export class UnifiedRecentDataService {
|
||||
private readonly store = inject(Store);
|
||||
private readonly dbService = inject(DatabaseService);
|
||||
private readonly playlistsService = inject(PlaylistsService);
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly xtreamDataSource = inject(XTREAM_DATA_SOURCE);
|
||||
|
||||
async getRecentItems(
|
||||
@@ -60,15 +65,15 @@ export class UnifiedRecentDataService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
await this.xtreamDataSource.removeRecentItem(
|
||||
if (this.hasPortalActivityStorage) {
|
||||
await this.dbService.removeRecentItem(
|
||||
item.contentId,
|
||||
item.playlistId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dbService.removeRecentItem(
|
||||
await this.xtreamDataSource.removeRecentItem(
|
||||
item.contentId,
|
||||
item.playlistId
|
||||
);
|
||||
@@ -139,7 +144,7 @@ export class UnifiedRecentDataService {
|
||||
const tasks: Promise<unknown>[] = [];
|
||||
|
||||
if (xtreamBatch.length > 0) {
|
||||
if (window.electron) {
|
||||
if (this.hasPortalActivityStorage) {
|
||||
tasks.push(this.dbService.removeRecentItemsBatch(xtreamBatch));
|
||||
} else {
|
||||
tasks.push(
|
||||
@@ -177,7 +182,7 @@ export class UnifiedRecentDataService {
|
||||
playlistId?: string
|
||||
): Promise<void> {
|
||||
if (scope === 'playlist' && playlistId) {
|
||||
if (window.electron) {
|
||||
if (this.hasPortalActivityStorage) {
|
||||
await this.dbService.clearPlaylistRecentItems(playlistId);
|
||||
} else {
|
||||
await this.xtreamDataSource.clearRecentItems(playlistId);
|
||||
@@ -189,7 +194,7 @@ export class UnifiedRecentDataService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.electron) {
|
||||
if (this.hasPortalActivityStorage) {
|
||||
await this.dbService.clearGlobalRecentlyViewed();
|
||||
} else {
|
||||
const allMeta = await this.getAllMeta();
|
||||
@@ -264,7 +269,7 @@ export class UnifiedRecentDataService {
|
||||
: null);
|
||||
|
||||
if (contentId != null) {
|
||||
if (window.electron) {
|
||||
if (this.hasPortalActivityStorage) {
|
||||
await this.dbService.addRecentItem(
|
||||
contentId,
|
||||
item.playlistId
|
||||
@@ -349,8 +354,12 @@ export class UnifiedRecentDataService {
|
||||
return this.getM3uPlaylistRecent(playlistId);
|
||||
}
|
||||
|
||||
private get hasPortalActivityStorage(): boolean {
|
||||
return this.runtime.supportsPortalActivityStorage;
|
||||
}
|
||||
|
||||
private async getXtreamGlobalRecent(): Promise<UnifiedCollectionItem[]> {
|
||||
if (!window.electron) {
|
||||
if (!this.hasPortalActivityStorage) {
|
||||
const allMeta = await this.getAllMeta();
|
||||
const results: UnifiedCollectionItem[] = [];
|
||||
for (const meta of allMeta.filter(
|
||||
@@ -393,7 +402,7 @@ export class UnifiedRecentDataService {
|
||||
): Promise<UnifiedCollectionItem[]> {
|
||||
try {
|
||||
const meta = await this.getPlaylistMeta(playlistId);
|
||||
if (!window.electron) {
|
||||
if (!this.hasPortalActivityStorage) {
|
||||
const rows =
|
||||
await this.xtreamDataSource.getRecentItems(playlistId);
|
||||
return rows.map((row) =>
|
||||
@@ -434,7 +443,7 @@ export class UnifiedRecentDataService {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
if (!this.hasPortalActivityStorage) {
|
||||
const content = await this.xtreamDataSource.getContentByXtreamId(
|
||||
item.xtreamId,
|
||||
item.playlistId,
|
||||
|
||||
@@ -39,10 +39,19 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
dbGetAppState: jest.fn(),
|
||||
dbSetAppState: jest.fn(),
|
||||
dbGetRecentlyViewed: jest.fn(),
|
||||
dbClearRecentlyViewed: jest.fn(),
|
||||
dbGetAllGlobalFavorites: jest.fn(),
|
||||
dbGetGlobalRecentlyAdded: jest.fn(),
|
||||
dbAddFavorite: jest.fn(),
|
||||
dbRemoveRecentItem: jest.fn(),
|
||||
dbRemoveFavorite: jest.fn(),
|
||||
dbGetFavorites: jest.fn(),
|
||||
dbReorderGlobalFavorites: jest.fn(),
|
||||
dbGetRecentItems: jest.fn(),
|
||||
dbAddRecentItem: jest.fn(),
|
||||
dbClearPlaylistRecentItems: jest.fn(),
|
||||
dbRemoveRecentItemsBatch: jest.fn(),
|
||||
dbGetContentByXtreamId: jest.fn(),
|
||||
downloadsGetList: jest.fn(),
|
||||
prepareEmbeddedMpv: jest.fn(),
|
||||
saveFileDialog: jest.fn(),
|
||||
|
||||
@@ -49,13 +49,22 @@ export class RuntimeCapabilitiesService {
|
||||
}
|
||||
|
||||
get supportsPortalActivityStorage(): boolean {
|
||||
return (
|
||||
this.hasElectronMethod('dbGetRecentlyViewed') &&
|
||||
this.hasElectronMethod('dbGetAllGlobalFavorites') &&
|
||||
this.hasElectronMethod('dbGetGlobalRecentlyAdded') &&
|
||||
this.hasElectronMethod('dbRemoveRecentItem') &&
|
||||
this.hasElectronMethod('dbRemoveFavorite')
|
||||
);
|
||||
return [
|
||||
'dbGetRecentlyViewed',
|
||||
'dbClearRecentlyViewed',
|
||||
'dbGetAllGlobalFavorites',
|
||||
'dbGetGlobalRecentlyAdded',
|
||||
'dbAddFavorite',
|
||||
'dbRemoveFavorite',
|
||||
'dbGetFavorites',
|
||||
'dbReorderGlobalFavorites',
|
||||
'dbGetRecentItems',
|
||||
'dbAddRecentItem',
|
||||
'dbClearPlaylistRecentItems',
|
||||
'dbRemoveRecentItem',
|
||||
'dbRemoveRecentItemsBatch',
|
||||
'dbGetContentByXtreamId',
|
||||
].every((methodName) => this.hasElectronMethod(methodName));
|
||||
}
|
||||
|
||||
get supportsManagedExternalPlayers(): boolean {
|
||||
|
||||
@@ -159,10 +159,19 @@ describe('DashboardDataService', () => {
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: {
|
||||
dbGetRecentlyViewed: jest.fn(),
|
||||
dbClearRecentlyViewed: jest.fn(),
|
||||
dbGetAllGlobalFavorites: jest.fn(),
|
||||
dbGetGlobalRecentlyAdded: jest.fn(),
|
||||
dbAddFavorite: jest.fn(),
|
||||
dbRemoveRecentItem: jest.fn(),
|
||||
dbRemoveFavorite: jest.fn(),
|
||||
dbGetFavorites: jest.fn(),
|
||||
dbReorderGlobalFavorites: jest.fn(),
|
||||
dbGetRecentItems: jest.fn(),
|
||||
dbAddRecentItem: jest.fn(),
|
||||
dbClearPlaylistRecentItems: jest.fn(),
|
||||
dbRemoveRecentItemsBatch: jest.fn(),
|
||||
dbGetContentByXtreamId: jest.fn(),
|
||||
} as unknown as Window['electron'],
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user