mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
fix(stalker): unwrap nested js.account_info envelope in get_main_info
Ministra-style portals nest the account block — fetchStalkerExpireDate() in stalker-player-request.utils already consumes exactly that shape, so the flat-only mapper silently discarded valid responses and legacy imports (which have no cached snapshot) got an empty account panel. Merge nested fields over flat aliases, send the JsHttpRequest parameter the existing get_main_info caller sends, switch the mock server to the nested envelope so the E2E covers the realistic shape, and document the account-info feature in CLAUDE.md (review feedback from Greptile and Codex on #1330). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
582785ee8e
commit
32a259aea4
4 files changed
+101
-24
No files matched your search
@@ -1016,6 +1016,12 @@ engine` (restart required) or
|
||||
- TMDB attribution (logo + disclaimer) is required and shown in the settings TMDB section and About
|
||||
- See `docs/architecture/tmdb-metadata-enrichment.md`
|
||||
|
||||
**Portal Account Info**:
|
||||
|
||||
- Both portal types expose an account-info dialog through the same entry points: header playlist switcher (bottom section for the active playlist + per-row ⋮ menu), dashboard source card ⋮ menu, and the command palette. Gates use the shared predicates in `libs/shared/interfaces/src/lib/portal-account-playlist.utils.ts`; `WorkspaceShellHeaderService.openAccountInfoFor()` picks the dialog by playlist type.
|
||||
- Xtream: `AccountInfoComponent` (`libs/portal/xtream/feature/src/lib/account-info/`), queries `get_account_info` live.
|
||||
- Stalker: `StalkerAccountInfoComponent` (`libs/portal/stalker/feature/src/lib/stalker-account-info/`), cached-first — renders the import-time `stalkerAccountInfo` snapshot instantly, then `StalkerAccountInfoService` refreshes (full portals: handshake+`get_profile`; `portal.php`: best-effort `account_info/get_main_info`, nested `js.account_info` envelope or flat fields). Details: `docs/architecture/stalker-portal.md` ("Account Info Dialog").
|
||||
|
||||
**Favorites and Recently Viewed**:
|
||||
|
||||
- Per-playlist favorites and global favorites
|
||||
|
||||
@@ -2,22 +2,25 @@ import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* Stalker `account_info/get_main_info` — subscription facts for the
|
||||
* account-info dialog. Field names deliberately mirror the loosely
|
||||
* standardized panels in the wild (end_date as a date string, tariff_plan,
|
||||
* numeric status where 1 = active).
|
||||
* account-info dialog. Uses the nested `js.account_info` envelope that
|
||||
* Ministra-style portals answer with (the shape `fetchStalkerExpireDate()`
|
||||
* already consumes), plus flat aliases some panels emit alongside it.
|
||||
*/
|
||||
export function handleGetMainInfo(req: Request, res: Response): void {
|
||||
const expiry = new Date();
|
||||
expiry.setDate(expiry.getDate() + 180);
|
||||
const mac = (req.query['mac'] as string) || '00:1A:79:00:00:01';
|
||||
|
||||
res.json({
|
||||
js: {
|
||||
mac: (req.query['mac'] as string) || '00:1A:79:00:00:01',
|
||||
mac,
|
||||
phone: '10042',
|
||||
login: 'mockuser',
|
||||
status: 1,
|
||||
tariff_plan: 'Mock Premium 180',
|
||||
end_date: expiry.toISOString().slice(0, 10),
|
||||
account_info: {
|
||||
login: 'mockuser',
|
||||
status: 1,
|
||||
tariff_plan: 'Mock Premium 180',
|
||||
end_date: expiry.toISOString().slice(0, 10),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -115,7 +115,10 @@ describe('StalkerAccountInfoService', () => {
|
||||
expect.objectContaining({
|
||||
url: portalPlaylist.portalUrl,
|
||||
macAddress: portalPlaylist.macAddress,
|
||||
params: { type: 'account_info', action: 'get_main_info' },
|
||||
params: expect.objectContaining({
|
||||
type: 'account_info',
|
||||
action: 'get_main_info',
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(snapshot).toEqual({
|
||||
@@ -128,6 +131,48 @@ describe('StalkerAccountInfoService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('unwraps the nested js.account_info envelope used by Ministra-style portals', async () => {
|
||||
// Same envelope fetchStalkerExpireDate() consumes in
|
||||
// stalker-player-request.utils — nested facts must win over
|
||||
// flat aliases.
|
||||
dataService.sendIpcEvent.mockResolvedValue({
|
||||
js: {
|
||||
mac: '00:1A:79:00:00:01',
|
||||
tariff_plan: 'Flat Alias Plan',
|
||||
account_info: {
|
||||
login: 'nested-user',
|
||||
expire_date: 1_790_000_000,
|
||||
tariff_plan_name: 'Nested Premium',
|
||||
status: '1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = await service.fetchAccountInfo(portalPlaylist);
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
login: 'nested-user',
|
||||
expireDate: 1_790_000_000,
|
||||
tariffPlanName: 'Nested Premium',
|
||||
status: 1,
|
||||
mac: '00:1A:79:00:00:01',
|
||||
phone: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends the JsHttpRequest parameter legacy panels expect', async () => {
|
||||
dataService.sendIpcEvent.mockResolvedValue({ js: {} });
|
||||
|
||||
await service.fetchAccountInfo(portalPlaylist);
|
||||
|
||||
expect(dataService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({ JsHttpRequest: '1-xml' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null when get_main_info yields no usable facts', async () => {
|
||||
dataService.sendIpcEvent.mockResolvedValue({ js: {} });
|
||||
|
||||
|
||||
@@ -24,19 +24,30 @@ export interface StalkerAccountSnapshot {
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
interface StalkerMainInfoFields {
|
||||
mac?: string;
|
||||
phone?: string;
|
||||
login?: string;
|
||||
fname?: string;
|
||||
status?: number | string;
|
||||
tariff_plan?: string;
|
||||
tariff_plan_name?: string;
|
||||
end_date?: number | string;
|
||||
expire_date?: number | string;
|
||||
expire_billing_date?: number | string;
|
||||
}
|
||||
|
||||
interface StalkerMainInfoResponse {
|
||||
js?: {
|
||||
mac?: string;
|
||||
phone?: string;
|
||||
login?: string;
|
||||
fname?: string;
|
||||
status?: number | string;
|
||||
tariff_plan?: string;
|
||||
tariff_plan_name?: string;
|
||||
end_date?: number | string;
|
||||
expire_date?: number | string;
|
||||
expire_billing_date?: number | string;
|
||||
} | null;
|
||||
js?:
|
||||
| (StalkerMainInfoFields & {
|
||||
/**
|
||||
* Ministra-style portals nest the account block —
|
||||
* `fetchStalkerExpireDate()` in stalker-player-request.utils
|
||||
* reads this exact envelope. Other panels answer flat.
|
||||
*/
|
||||
account_info?: StalkerMainInfoFields | null;
|
||||
})
|
||||
| null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,14 +112,26 @@ export class StalkerAccountInfoService {
|
||||
stalkerSession: this.stalkerSession,
|
||||
},
|
||||
playlist,
|
||||
{ type: 'account_info', action: 'get_main_info' }
|
||||
{
|
||||
type: 'account_info',
|
||||
action: 'get_main_info',
|
||||
JsHttpRequest: '1-xml',
|
||||
}
|
||||
);
|
||||
|
||||
const info = response?.js;
|
||||
if (!info || typeof info !== 'object') {
|
||||
const flat = response?.js;
|
||||
if (!flat || typeof flat !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Nested envelope wins field-by-field; flat top-level values remain
|
||||
// as aliases for panels that answer without the account_info block.
|
||||
const nested =
|
||||
flat.account_info && typeof flat.account_info === 'object'
|
||||
? flat.account_info
|
||||
: {};
|
||||
const info: StalkerMainInfoFields = { ...flat, ...nested };
|
||||
|
||||
return normalizeSnapshot({
|
||||
login: info.login || info.fname || undefined,
|
||||
expireDate:
|
||||
|
||||
Reference in new issue
Block a user