diff --git a/.changes/import-stalker-unlimited-expiry.md b/.changes/import-stalker-unlimited-expiry.md new file mode 100644 index 000000000..3e5fd151f --- /dev/null +++ b/.changes/import-stalker-unlimited-expiry.md @@ -0,0 +1,6 @@ +--- +type: fix +area: import +--- + +Adding a Stalker portal with an unlimited subscription no longer announces a bogus 1970 expiry date, and a portal that reports its expiry as a date shows that date instead of "Invalid Date". diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts index 8b4e0b34f..fc96814f9 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts @@ -449,6 +449,56 @@ describe('StalkerPortalImportComponent identity handling', () => { ); }); + // Portals encode an unlimited account as -1, "0" or a zero date, and + // the wire value is often a string despite the API's number type. + it.each([-1, '0', '-1', '0000-00-00'])( + 'announces no expiry for the unlimited sentinel %p', + async (expireDate) => { + portalDiscovery.discover.mockResolvedValue({ + status: 'resolved', + portalUrl: + 'https://portal.example.com/stalker_portal/server/load.php', + isFullStalkerPortal: true, + token: 'token-1', + accountInfo: { login: 'demo', expire_date: expireDate }, + }); + component.form.patchValue({ + title: 'Unlimited Portal', + macAddress: '00:1A:79:AA:BB:CC', + portalUrl: 'https://portal.example.com/stalker_portal/c', + }); + + await component.addPlaylist(); + + expect(snackBar.open).not.toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledTimes(1); + } + ); + + it('announces a date-string expiry as that calendar day', async () => { + const translate = TestBed.inject(TranslateService); + portalDiscovery.discover.mockResolvedValue({ + status: 'resolved', + portalUrl: + 'https://portal.example.com/stalker_portal/server/load.php', + isFullStalkerPortal: true, + token: 'token-1', + accountInfo: { login: 'demo', expire_date: '2030-01-15' }, + }); + component.form.patchValue({ + title: 'Dated Portal', + macAddress: '00:1A:79:AA:BB:CC', + portalUrl: 'https://portal.example.com/stalker_portal/c', + }); + + await component.addPlaylist(); + + expect(translate.instant).toHaveBeenCalledWith( + 'HOME.STALKER_PORTAL.VALIDATED_EXPIRES', + { date: new Date(2030, 0, 15).toLocaleDateString() } + ); + }); + it('drops a refusal once the user edits the form or retries', async () => { portalDiscovery.discover.mockResolvedValue({ status: 'auth-rejected', diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts index 8cd65d5fd..1a19d0403 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts @@ -28,6 +28,7 @@ import { PlaylistActions } from '@iptvnator/m3u-state'; import { legacyTransformStalkerPortalUrl, normalizeStalkerPortalInputUrl, + parseStalkerDate, STALKER_WATCHDOG_DEFAULT_PERIOD_SECONDS, StalkerPortalDiscoveryService, normalizeStalkerPortalIdentity, @@ -462,10 +463,13 @@ export class StalkerPortalImportComponent { }; } - if (stalkerAccountInfo?.expireDate) { - const expireDate = new Date( - stalkerAccountInfo.expireDate * 1000 - ); + // The raw value may be a date string or an "unlimited" + // sentinel (-1, "0"), so read it like every other consumer. + const expiresAt = parseStalkerDate( + stalkerAccountInfo?.expireDate + ); + if (expiresAt) { + const expireDate = new Date(expiresAt * 1000); this.snackBar.open( this.translate.instant( 'HOME.STALKER_PORTAL.VALIDATED_EXPIRES',