fix(import): no bogus Stalker expiry notice for unlimited accounts (#1851)

* docs(website): say the Stalker validated notice needs an expiry date

The import shows "Portal validated" only when the portal profile
carries expire_date; an account without a fixed expiry is added
silently. The guide described the notice as unconditional (Codex
review on #1808).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(import): read the Stalker import expiry through parseStalkerDate

The validated-import snackbar multiplied the raw account_info.expire_date
by 1000. Portals encode an unlimited account as -1, "0" or a zero date,
which announced a 1969/1970 expiry, and a date-string expiry rendered as
"Invalid Date". Every other consumer already reads the value through
parseStalkerDate; the import now does too, so the guide's "no notice
without a fixed expiry" holds (Codex review on #1851).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-10-08 20:02:43 +02:00
1 parent f3c576d182
commit 932876fbd9
4 files changed
+67 -6

No files matched your search

@@ -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".
@@ -94,8 +94,9 @@ setups, so IPTVnator no longer guesses:
that address, and if a provider migrates the portal, IPTVnator re-probes once on the
first failure and repairs the stored connection.
On a full portal the profile also carries your subscription facts, and IPTVnator confirms
the import with "Portal validated" and the expiry date. A portal that cannot be reached
On a full portal the profile also carries your subscription facts. When they include an
expiry date, IPTVnator confirms the import with "Portal validated" and that date; an account
without a fixed expiry is added without that notice. A portal that cannot be reached
during the import is still added when the address looks like a panel, with a notice that it
was added without validation, so a temporarily offline provider does not block you; the next
successful request completes the setup.
@@ -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',
@@ -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',