fix(xtream): sync pwa playlist details cache

This commit is contained in:
4gray committed 2026-06-13 12:25:24 +02:00
1 parent 4303496d63
commit a2d6f8ef1c
3 files changed
+155 -1

No files matched your search

@@ -1,5 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -33,6 +33,9 @@ describe('PlaylistInfoComponent', () => {
let store: { let store: {
dispatch: jest.Mock; dispatch: jest.Mock;
}; };
let dialogRef: {
close: jest.Mock;
};
const originalElectron = window.electron; const originalElectron = window.electron;
const playlist = { const playlist = {
@@ -63,6 +66,9 @@ describe('PlaylistInfoComponent', () => {
store = { store = {
dispatch: jest.fn(), dispatch: jest.fn(),
}; };
dialogRef = {
close: jest.fn(),
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PlaylistInfoComponent], imports: [PlaylistInfoComponent],
@@ -87,6 +93,10 @@ describe('PlaylistInfoComponent', () => {
provide: MatSnackBar, provide: MatSnackBar,
useValue: snackBar, useValue: snackBar,
}, },
{
provide: MatDialogRef,
useValue: dialogRef,
},
{ {
provide: TranslateService, provide: TranslateService,
useValue: { useValue: {
@@ -146,6 +156,7 @@ describe('PlaylistInfoComponent', () => {
'CLOSE', 'CLOSE',
{ duration: 3000 } { duration: 3000 }
); );
expect(dialogRef.close).toHaveBeenCalledTimes(1);
}); });
it('uses the Electron save dialog when desktop file saving is available', async () => { it('uses the Electron save dialog when desktop file saving is available', async () => {
@@ -4,12 +4,17 @@ import {
XtreamApiService, XtreamApiService,
XtreamCredentials, XtreamCredentials,
} from '../services/xtream-api.service'; } from '../services/xtream-api.service';
import { PlaylistsService } from '@iptvnator/services';
import { of } from 'rxjs';
describe('PwaXtreamDataSource', () => { describe('PwaXtreamDataSource', () => {
let dataSource: PwaXtreamDataSource; let dataSource: PwaXtreamDataSource;
let apiService: { let apiService: {
getStreams: jest.Mock; getStreams: jest.Mock;
}; };
let playlistsService: {
getPlaylistById: jest.Mock;
};
const credentials: XtreamCredentials = { const credentials: XtreamCredentials = {
serverUrl: 'http://localhost:3211', serverUrl: 'http://localhost:3211',
@@ -23,6 +28,9 @@ describe('PwaXtreamDataSource', () => {
apiService = { apiService = {
getStreams: jest.fn(), getStreams: jest.fn(),
}; };
playlistsService = {
getPlaylistById: jest.fn(() => of(undefined)),
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
@@ -31,6 +39,10 @@ describe('PwaXtreamDataSource', () => {
provide: XtreamApiService, provide: XtreamApiService,
useValue: apiService, useValue: apiService,
}, },
{
provide: PlaylistsService,
useValue: playlistsService,
},
], ],
}); });
@@ -66,6 +78,62 @@ describe('PwaXtreamDataSource', () => {
); );
}); });
it('uses current playlist metadata before stale PWA storage when fetching playlist details', async () => {
await dataSource.createPlaylist({
id: 'playlist-1',
name: 'Old Xtream',
title: 'Old Xtream',
serverUrl: 'http://old.example:8080',
username: 'old-user',
password: 'old-pass',
type: 'xtream',
});
playlistsService.getPlaylistById.mockReturnValue(
of({
_id: 'playlist-1',
title: 'Updated Xtream',
importDate: '2026-04-01T00:00:00.000Z',
lastUsage: '2026-04-01T00:00:00.000Z',
count: 0,
autoRefresh: false,
updateDate: 123,
serverUrl: 'http://new.example:8080',
username: 'new-user',
password: 'new-pass',
userAgent: 'new-agent',
referrer: 'https://referrer.example',
origin: 'https://origin.example',
})
);
await expect(dataSource.getPlaylist('playlist-1')).resolves.toEqual(
expect.objectContaining({
id: 'playlist-1',
name: 'Updated Xtream',
title: 'Updated Xtream',
updateDate: 123,
serverUrl: 'http://new.example:8080',
username: 'new-user',
password: 'new-pass',
userAgent: 'new-agent',
referrer: 'https://referrer.example',
origin: 'https://origin.example',
})
);
expect(
JSON.parse(localStorage.getItem('xtream-playlists') || '[]')
).toEqual([
expect.objectContaining({
id: 'playlist-1',
name: 'Updated Xtream',
title: 'Updated Xtream',
serverUrl: 'http://new.example:8080',
username: 'new-user',
type: 'xtream',
}),
]);
});
it('normalizes Xtream API stream identifiers for PWA catalog navigation', async () => { it('normalizes Xtream API stream identifiers for PWA catalog navigation', async () => {
apiService.getStreams.mockImplementation( apiService.getStreams.mockImplementation(
(_credentials: XtreamCredentials, type: string) => { (_credentials: XtreamCredentials, type: string) => {
@@ -481,6 +549,10 @@ describe('PwaXtreamDataSource', () => {
provide: XtreamApiService, provide: XtreamApiService,
useValue: apiService, useValue: apiService,
}, },
{
provide: PlaylistsService,
useValue: playlistsService,
},
], ],
}); });
dataSource = TestBed.inject(PwaXtreamDataSource); dataSource = TestBed.inject(PwaXtreamDataSource);
@@ -1,5 +1,6 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { import {
Playlist,
PlaybackPositionData, PlaybackPositionData,
XtreamPendingRestoreState, XtreamPendingRestoreState,
XtreamCategory, XtreamCategory,
@@ -14,6 +15,8 @@ import {
XtreamApiService, XtreamApiService,
XtreamCredentials, XtreamCredentials,
} from '../services/xtream-api.service'; } from '../services/xtream-api.service';
import { PlaylistsService } from '@iptvnator/services';
import { firstValueFrom } from 'rxjs';
import { import {
DbCategoryType, DbCategoryType,
IXtreamDataSource, IXtreamDataSource,
@@ -76,6 +79,7 @@ type StoredXtreamPlaylistData = Omit<XtreamPlaylistData, 'password'> & {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class PwaXtreamDataSource implements IXtreamDataSource { export class PwaXtreamDataSource implements IXtreamDataSource {
private readonly apiService = inject(XtreamApiService); private readonly apiService = inject(XtreamApiService);
private readonly playlistsService = inject(PlaylistsService);
private readonly logger = createLogger('PwaXtreamDataSource'); private readonly logger = createLogger('PwaXtreamDataSource');
private readonly contentTypes = ['live', 'movie', 'series'] as const; private readonly contentTypes = ['live', 'movie', 'series'] as const;
@@ -89,6 +93,12 @@ export class PwaXtreamDataSource implements IXtreamDataSource {
// ========================================================================= // =========================================================================
async getPlaylist(playlistId: string): Promise<XtreamPlaylistData | null> { async getPlaylist(playlistId: string): Promise<XtreamPlaylistData | null> {
const currentPlaylist =
await this.getPlaylistFromCurrentMetadata(playlistId);
if (currentPlaylist) {
return currentPlaylist;
}
const playlists = this.getPlaylistsFromStorage(); const playlists = this.getPlaylistsFromStorage();
const playlist = playlists.find((p) => p.id === playlistId); const playlist = playlists.find((p) => p.id === playlistId);
return playlist?.password ? playlist : null; return playlist?.password ? playlist : null;
@@ -157,6 +167,67 @@ export class PwaXtreamDataSource implements IXtreamDataSource {
); );
} }
private async getPlaylistFromCurrentMetadata(
playlistId: string
): Promise<XtreamPlaylistData | null> {
try {
const playlist = await firstValueFrom(
this.playlistsService.getPlaylistById(playlistId)
);
const xtreamPlaylist = this.toXtreamPlaylistData(playlist);
if (xtreamPlaylist) {
this.upsertPlaylistInStorage(xtreamPlaylist);
}
return xtreamPlaylist;
} catch {
return null;
}
}
private toXtreamPlaylistData(
playlist: Playlist | null | undefined
): XtreamPlaylistData | null {
if (
!playlist?._id ||
!playlist.serverUrl ||
!playlist.username ||
!playlist.password
) {
return null;
}
return {
id: playlist._id,
name: playlist.title,
title: playlist.title,
updateDate: playlist.updateDate,
serverUrl: playlist.serverUrl,
username: playlist.username,
password: playlist.password,
type: 'xtream',
userAgent: playlist.userAgent,
referrer: playlist.referrer,
origin: playlist.origin,
};
}
private upsertPlaylistInStorage(playlist: XtreamPlaylistData): void {
this.rememberPlaylistPassword(playlist);
const playlists = this.getPlaylistsFromStorage();
const index = playlists.findIndex((item) => item.id === playlist.id);
if (index === -1) {
this.savePlaylistsToStorage([...playlists, playlist]);
return;
}
playlists[index] = playlist;
this.savePlaylistsToStorage(playlists);
}
private toStoredPlaylist( private toStoredPlaylist(
playlist: XtreamPlaylistData playlist: XtreamPlaylistData
): StoredXtreamPlaylistData { ): StoredXtreamPlaylistData {