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 { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
@@ -33,6 +33,9 @@ describe('PlaylistInfoComponent', () => {
let store: {
dispatch: jest.Mock;
};
let dialogRef: {
close: jest.Mock;
};
const originalElectron = window.electron;
const playlist = {
@@ -63,6 +66,9 @@ describe('PlaylistInfoComponent', () => {
store = {
dispatch: jest.fn(),
};
dialogRef = {
close: jest.fn(),
};
await TestBed.configureTestingModule({
imports: [PlaylistInfoComponent],
@@ -87,6 +93,10 @@ describe('PlaylistInfoComponent', () => {
provide: MatSnackBar,
useValue: snackBar,
},
{
provide: MatDialogRef,
useValue: dialogRef,
},
{
provide: TranslateService,
useValue: {
@@ -146,6 +156,7 @@ describe('PlaylistInfoComponent', () => {
'CLOSE',
{ duration: 3000 }
);
expect(dialogRef.close).toHaveBeenCalledTimes(1);
});
it('uses the Electron save dialog when desktop file saving is available', async () => {
@@ -4,12 +4,17 @@ import {
XtreamApiService,
XtreamCredentials,
} from '../services/xtream-api.service';
import { PlaylistsService } from '@iptvnator/services';
import { of } from 'rxjs';
describe('PwaXtreamDataSource', () => {
let dataSource: PwaXtreamDataSource;
let apiService: {
getStreams: jest.Mock;
};
let playlistsService: {
getPlaylistById: jest.Mock;
};
const credentials: XtreamCredentials = {
serverUrl: 'http://localhost:3211',
@@ -23,6 +28,9 @@ describe('PwaXtreamDataSource', () => {
apiService = {
getStreams: jest.fn(),
};
playlistsService = {
getPlaylistById: jest.fn(() => of(undefined)),
};
TestBed.configureTestingModule({
providers: [
@@ -31,6 +39,10 @@ describe('PwaXtreamDataSource', () => {
provide: XtreamApiService,
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 () => {
apiService.getStreams.mockImplementation(
(_credentials: XtreamCredentials, type: string) => {
@@ -481,6 +549,10 @@ describe('PwaXtreamDataSource', () => {
provide: XtreamApiService,
useValue: apiService,
},
{
provide: PlaylistsService,
useValue: playlistsService,
},
],
});
dataSource = TestBed.inject(PwaXtreamDataSource);
@@ -1,5 +1,6 @@
import { inject, Injectable } from '@angular/core';
import {
Playlist,
PlaybackPositionData,
XtreamPendingRestoreState,
XtreamCategory,
@@ -14,6 +15,8 @@ import {
XtreamApiService,
XtreamCredentials,
} from '../services/xtream-api.service';
import { PlaylistsService } from '@iptvnator/services';
import { firstValueFrom } from 'rxjs';
import {
DbCategoryType,
IXtreamDataSource,
@@ -76,6 +79,7 @@ type StoredXtreamPlaylistData = Omit<XtreamPlaylistData, 'password'> & {
@Injectable({ providedIn: 'root' })
export class PwaXtreamDataSource implements IXtreamDataSource {
private readonly apiService = inject(XtreamApiService);
private readonly playlistsService = inject(PlaylistsService);
private readonly logger = createLogger('PwaXtreamDataSource');
private readonly contentTypes = ['live', 'movie', 'series'] as const;
@@ -89,6 +93,12 @@ export class PwaXtreamDataSource implements IXtreamDataSource {
// =========================================================================
async getPlaylist(playlistId: string): Promise<XtreamPlaylistData | null> {
const currentPlaylist =
await this.getPlaylistFromCurrentMetadata(playlistId);
if (currentPlaylist) {
return currentPlaylist;
}
const playlists = this.getPlaylistsFromStorage();
const playlist = playlists.find((p) => p.id === playlistId);
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(
playlist: XtreamPlaylistData
): StoredXtreamPlaylistData {