mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
fix: enhance playlist management by adding user agent and referrer checks, and streamline bootstrap logic
This commit is contained in:
1 parent
062002e91c
commit
58f0d95972
3 files changed
+134
-28
No files matched your search
@@ -29,6 +29,7 @@ export class XtreamCatalogFacadeService
|
||||
{
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
private savedPageBeforeDetail: number | null = null;
|
||||
private loadedPositionsPlaylistId: string | null = null;
|
||||
private readonly routeSearchQuery = signal('');
|
||||
|
||||
readonly provider = 'xtream' as const;
|
||||
@@ -135,7 +136,8 @@ export class XtreamCatalogFacadeService
|
||||
}
|
||||
|
||||
const playlistId = this.xtreamStore.currentPlaylist()?.id;
|
||||
if (playlistId) {
|
||||
if (playlistId && this.loadedPositionsPlaylistId !== playlistId) {
|
||||
this.loadedPositionsPlaylistId = playlistId;
|
||||
this.xtreamStore.loadAllPositions(playlistId);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ const XTREAM_PLAYLIST: XtreamPlaylistData = {
|
||||
password: 'secret',
|
||||
type: 'xtream',
|
||||
};
|
||||
const XTREAM_PLAYLIST_WITH_NULL_OPTIONALS = {
|
||||
...XTREAM_PLAYLIST,
|
||||
origin: null,
|
||||
referrer: null,
|
||||
userAgent: null,
|
||||
} as XtreamPlaylistData;
|
||||
|
||||
async function flushEffects(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
@@ -59,6 +65,7 @@ describe('XtreamWorkspaceRouteSession', () => {
|
||||
const portalStatus = signal<PortalStatusType>('active');
|
||||
const selectedContentType = signal<'live' | 'vod' | 'series'>('vod');
|
||||
const selectedCategoryId = signal<number | null>(null);
|
||||
const isContentInitialized = signal(false);
|
||||
const contentInitBlockReason =
|
||||
signal<XtreamContentInitBlockReason | null>(null);
|
||||
let hasUsableOfflineCache = false;
|
||||
@@ -78,6 +85,7 @@ describe('XtreamWorkspaceRouteSession', () => {
|
||||
playlistId.set(nextPlaylistId ?? null);
|
||||
currentPlaylist.set(null);
|
||||
selectedContentType.set('vod');
|
||||
isContentInitialized.set(false);
|
||||
}),
|
||||
setCurrentPlaylist: jest.fn((playlist: XtreamPlaylistData | null) => {
|
||||
currentPlaylist.set(playlist);
|
||||
@@ -87,8 +95,11 @@ describe('XtreamWorkspaceRouteSession', () => {
|
||||
hasUsableOfflineCache: jest.fn().mockImplementation(async () => {
|
||||
return hasUsableOfflineCache;
|
||||
}),
|
||||
isContentInitialized,
|
||||
contentInitBlockReason,
|
||||
initializeContent: jest.fn().mockResolvedValue(undefined),
|
||||
initializeContent: jest.fn().mockImplementation(async () => {
|
||||
isContentInitialized.set(true);
|
||||
}),
|
||||
setSelectedContentType: jest.fn(
|
||||
(type: 'live' | 'vod' | 'series') => {
|
||||
selectedContentType.set(type);
|
||||
@@ -114,11 +125,12 @@ describe('XtreamWorkspaceRouteSession', () => {
|
||||
routeProvider.set('xtreams');
|
||||
routePlaylistId.set(PLAYLIST_ID);
|
||||
activePlaylist.set(ACTIVE_PLAYLIST);
|
||||
currentPlaylist.set(XTREAM_PLAYLIST);
|
||||
playlistId.set(PLAYLIST_ID);
|
||||
currentPlaylist.set(null);
|
||||
playlistId.set(null);
|
||||
portalStatus.set('active');
|
||||
selectedContentType.set('vod');
|
||||
selectedCategoryId.set(null);
|
||||
isContentInitialized.set(false);
|
||||
contentInitBlockReason.set(null);
|
||||
hasUsableOfflineCache = false;
|
||||
|
||||
@@ -198,6 +210,73 @@ describe('XtreamWorkspaceRouteSession', () => {
|
||||
).toBeGreaterThan(xtreamStore.resetStore.mock.invocationCallOrder[0]);
|
||||
});
|
||||
|
||||
it('does not bootstrap again when the store already holds the active playlist', async () => {
|
||||
router.url = `/workspace/xtreams/${PLAYLIST_ID}/vod/101`;
|
||||
currentPlaylist.set(XTREAM_PLAYLIST);
|
||||
playlistId.set(PLAYLIST_ID);
|
||||
isContentInitialized.set(true);
|
||||
|
||||
TestBed.inject(XtreamWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
|
||||
expect(xtreamStore.setSelectedContentType).toHaveBeenCalledWith('vod');
|
||||
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledWith(101);
|
||||
expect(xtreamStore.resetStore).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.fetchXtreamPlaylist).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.checkPortalStatus).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.initializeContent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not treat null and undefined Xtream connection metadata as a playlist change', async () => {
|
||||
router.url = `/workspace/xtreams/${PLAYLIST_ID}/vod/101`;
|
||||
currentPlaylist.set(XTREAM_PLAYLIST_WITH_NULL_OPTIONALS);
|
||||
playlistId.set(PLAYLIST_ID);
|
||||
isContentInitialized.set(true);
|
||||
|
||||
TestBed.inject(XtreamWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
|
||||
expect(xtreamStore.resetStore).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.fetchXtreamPlaylist).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.checkPortalStatus).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.initializeContent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not reinitialize content when switching categories in an initialized playlist', async () => {
|
||||
router.url = `/workspace/xtreams/${PLAYLIST_ID}/vod/101`;
|
||||
xtreamStore.checkPortalStatus.mockImplementation(async () => {
|
||||
portalStatus.set('active');
|
||||
return 'active';
|
||||
});
|
||||
|
||||
TestBed.inject(XtreamWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
await flushEffects();
|
||||
|
||||
expect(isContentInitialized()).toBe(true);
|
||||
|
||||
xtreamStore.resetStore.mockClear();
|
||||
xtreamStore.fetchXtreamPlaylist.mockClear();
|
||||
xtreamStore.checkPortalStatus.mockClear();
|
||||
xtreamStore.initializeContent.mockClear();
|
||||
xtreamStore.setSelectedContentType.mockClear();
|
||||
xtreamStore.setSelectedCategory.mockClear();
|
||||
|
||||
router.url = `/workspace/xtreams/${PLAYLIST_ID}/vod/202`;
|
||||
routerEvents.next(
|
||||
new NavigationEnd(1, router.url, router.url)
|
||||
);
|
||||
await flushEffects();
|
||||
|
||||
expect(xtreamStore.setSelectedContentType).toHaveBeenCalledWith('vod');
|
||||
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledWith(202);
|
||||
expect(selectedCategoryId()).toBe(202);
|
||||
expect(xtreamStore.resetStore).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.fetchXtreamPlaylist).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.checkPortalStatus).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.initializeContent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['expired', 'inactive', 'unavailable'] as const)(
|
||||
'blocks %s portals before import-driven initialization starts',
|
||||
async (status) => {
|
||||
@@ -239,6 +318,8 @@ describe('XtreamWorkspaceRouteSession', () => {
|
||||
|
||||
it('rebootstraps the current Xtream playlist when its connection details change', async () => {
|
||||
activePlaylist.set(UPDATED_ACTIVE_PLAYLIST);
|
||||
currentPlaylist.set(XTREAM_PLAYLIST);
|
||||
playlistId.set(PLAYLIST_ID);
|
||||
xtreamStore.checkPortalStatus.mockImplementation(async () => {
|
||||
portalStatus.set('active');
|
||||
return 'active';
|
||||
|
||||
@@ -76,13 +76,39 @@ function hasPlaylistConnectionChanges(
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUserAgent = currentPlaylist.userAgent ?? null;
|
||||
const nextUserAgent = nextPlaylist.userAgent ?? null;
|
||||
const currentReferrer = currentPlaylist.referrer ?? null;
|
||||
const nextReferrer = nextPlaylist.referrer ?? null;
|
||||
const currentOrigin = currentPlaylist.origin ?? null;
|
||||
const nextOrigin = nextPlaylist.origin ?? null;
|
||||
|
||||
return (
|
||||
currentPlaylist.serverUrl !== nextPlaylist.serverUrl ||
|
||||
currentPlaylist.username !== nextPlaylist.username ||
|
||||
currentPlaylist.password !== nextPlaylist.password ||
|
||||
currentPlaylist.userAgent !== nextPlaylist.userAgent ||
|
||||
currentPlaylist.referrer !== nextPlaylist.referrer ||
|
||||
currentPlaylist.origin !== nextPlaylist.origin
|
||||
currentUserAgent !== nextUserAgent ||
|
||||
currentReferrer !== nextReferrer ||
|
||||
currentOrigin !== nextOrigin
|
||||
);
|
||||
}
|
||||
|
||||
function shouldBootstrapXtreamPlaylist(
|
||||
playlistId: string | null,
|
||||
routePlaylist: XtreamPlaylistData | null,
|
||||
storePlaylistId: string | null,
|
||||
currentPlaylist: XtreamPlaylistData | null
|
||||
): boolean {
|
||||
const currentPlaylistUpdateDate = currentPlaylist?.updateDate ?? null;
|
||||
const routePlaylistUpdateDate = routePlaylist?.updateDate ?? null;
|
||||
|
||||
return Boolean(
|
||||
playlistId &&
|
||||
routePlaylist &&
|
||||
(storePlaylistId !== playlistId ||
|
||||
currentPlaylist?.id !== playlistId ||
|
||||
currentPlaylistUpdateDate !== routePlaylistUpdateDate ||
|
||||
hasPlaylistConnectionChanges(currentPlaylist, routePlaylist))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,8 +143,6 @@ export class XtreamWorkspaceRouteSession {
|
||||
private readonly router = inject(Router);
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
|
||||
private currentPlaylistId: string | null = null;
|
||||
private currentPlaylistUpdateDate: number | null = null;
|
||||
private syncInFlight = false;
|
||||
private syncPending = false;
|
||||
|
||||
@@ -186,26 +210,20 @@ export class XtreamWorkspaceRouteSession {
|
||||
routeContext.provider === 'xtreams'
|
||||
? toXtreamPlaylistData(this.playlistContext.activePlaylist())
|
||||
: null;
|
||||
const storePlaylistId = this.xtreamStore.playlistId();
|
||||
const currentPlaylist = this.xtreamStore.currentPlaylist();
|
||||
const routePlaylistUpdateDate = routePlaylist?.updateDate ?? null;
|
||||
const needsPlaylistBootstrap = Boolean(
|
||||
playlistId &&
|
||||
routePlaylist &&
|
||||
(currentPlaylist?.id !== playlistId ||
|
||||
this.currentPlaylistUpdateDate !== routePlaylistUpdateDate ||
|
||||
hasPlaylistConnectionChanges(currentPlaylist, routePlaylist))
|
||||
const shouldBootstrapPlaylist = shouldBootstrapXtreamPlaylist(
|
||||
playlistId,
|
||||
routePlaylist,
|
||||
storePlaylistId,
|
||||
currentPlaylist
|
||||
);
|
||||
let portalStatus = this.xtreamStore.portalStatus();
|
||||
let didBootstrapPlaylist = false;
|
||||
|
||||
if (
|
||||
playlistId &&
|
||||
(this.currentPlaylistId !== playlistId || needsPlaylistBootstrap)
|
||||
) {
|
||||
if (this.currentPlaylistId !== playlistId || needsPlaylistBootstrap) {
|
||||
this.xtreamStore.resetStore(playlistId);
|
||||
this.currentPlaylistId = playlistId;
|
||||
this.currentPlaylistUpdateDate = routePlaylistUpdateDate;
|
||||
}
|
||||
if (playlistId && shouldBootstrapPlaylist) {
|
||||
this.xtreamStore.resetStore(playlistId);
|
||||
didBootstrapPlaylist = true;
|
||||
|
||||
this.xtreamStore.setCurrentPlaylist(routePlaylist);
|
||||
|
||||
@@ -245,7 +263,10 @@ export class XtreamWorkspaceRouteSession {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.initializeCurrentSectionContent(section);
|
||||
await this.initializeCurrentSectionContent(
|
||||
section,
|
||||
didBootstrapPlaylist
|
||||
);
|
||||
}
|
||||
|
||||
private syncRouteState(
|
||||
@@ -267,7 +288,8 @@ export class XtreamWorkspaceRouteSession {
|
||||
}
|
||||
|
||||
private async initializeCurrentSectionContent(
|
||||
section: PortalRailSection | null
|
||||
section: PortalRailSection | null,
|
||||
didBootstrapPlaylist: boolean
|
||||
): Promise<void> {
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
const playlistId = this.xtreamStore.playlistId();
|
||||
@@ -277,7 +299,8 @@ export class XtreamWorkspaceRouteSession {
|
||||
}
|
||||
|
||||
if (
|
||||
isImportDrivenSection(section)
|
||||
isImportDrivenSection(section) &&
|
||||
(didBootstrapPlaylist || !this.xtreamStore.isContentInitialized())
|
||||
) {
|
||||
await this.xtreamStore.initializeContent();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user