mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
fix(xtream): sync pwa playlist details cache
This commit is contained in:
1 parent
4303496d63
commit
a2d6f8ef1c
3 files changed
+155
-1
No files matched your search
+12
-1
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user