fix(catalog): Hyundai/Kia/Nissan schema images (image-board ticket URL)

These three brands rendered parts with NO schema illustration (and no hotspots):
schema_pics=0 vs 56/29/19 parts on dev. Root cause: extractPsaImageTicketUrl only
read imageViewerParamsUrl from the id="jsinitparams" data-params attribute (where
Ford/PSA/Opel/Volvo put it). Hyundai/Kia/Nissan's jsinitparams carries only a
localization dict ({"commonTxt":{"ok":"TAMAM"}}); their imageViewerParamsUrl lives
in a separate <script> JSON blob → extractor returned null → image pipeline skipped.

Fix: when the jsinitparams-attribute parse yields no URL, fall back to a whole-HTML
scan for "imageViewerParamsUrl":"…" (decoding &). Brand-agnostic and additive;
Ford/PSA/Opel/Volvo unchanged (attribute path still wins). Proven live via the de
client: Hyundai 600x820/3 hotspots, Kia 600x820/11, Nissan 1024x560/13 — all download OK.

Also: drop a pre-existing noParameterAssign in fetchP4Page (derive `account` const
from `accountParam` instead of reassigning the param). +3 extractor unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 21:19:19 +03:00
parent 20cd4fda01
commit 2040686e4d
2 changed files with 63 additions and 16 deletions

View File

@@ -102,3 +102,34 @@ describe("PL24FordLegacyService.parseFordGroupsFromHtml — nav-crumb junk filte
expect(names).not.toContain("Geri");
});
});
describe("PL24FordLegacyService.extractPsaImageTicketUrl — image-board ticket URL", () => {
const ex = svc as unknown as { extractPsaImageTicketUrl(html: string): string | null };
it("reads imageViewerParamsUrl from the id=jsinitparams attribute (Ford/PSA/Opel/Volvo)", () => {
const html =
'<div id="jsinitparams" data-params="{&quot;imageViewerParamsUrl&quot;:&quot;/ford/fordp_parts/json-image-ticket.action?x=1&amp;y=2&quot;}"></div>';
expect(ex.extractPsaImageTicketUrl(html)).toBe(
"/ford/fordp_parts/json-image-ticket.action?x=1&y=2",
);
});
it("falls back to the <script> JSON blob when jsinitparams only has localization (Hyundai/Kia/Nissan)", () => {
// Real shape: jsinitparams carries commonTxt only; imageViewerParamsUrl lives in
// a separate script blob with raw quotes and &-escaped ampersands.
const html =
'<div id="jsinitparams" data-params="{&quot;commonTxt&quot;:{&quot;ok&quot;:&quot;TAMAM&quot;}}"></div>' +
'<script>var p = {"qty":"Miktar","imageViewerParamsUrl":"/hyundai-kia-automotive-group/hyundai_parts/json-image-ticket.action?catalog=HMT1B0PA00\\u0026illustration=1\\u0026signature=abc"};</script>';
expect(ex.extractPsaImageTicketUrl(html)).toBe(
"/hyundai-kia-automotive-group/hyundai_parts/json-image-ticket.action?catalog=HMT1B0PA00&illustration=1&signature=abc",
);
});
it("returns null when no image ticket url is present", () => {
expect(
ex.extractPsaImageTicketUrl(
'<div id="jsinitparams" data-params="{&quot;commonTxt&quot;:{}}"></div>',
),
).toBeNull();
});
});

View File

@@ -2833,23 +2833,38 @@ export class PL24FordLegacyService {
}
/**
* Extract the json-image-ticket.action URL from image-board.action HTML jsinitparams.
* Extract the json-image-ticket.action URL from image-board.action HTML.
*
* PSA / Ford / Opel / Volvo embed it in the `id="jsinitparams"` data-params
* attribute (HTML-encoded JSON). Hyundai / Kia / Nissan instead carry only a
* localization dict in jsinitparams and put `imageViewerParamsUrl` in a separate
* <script> JSON blob (raw quotes, &-escaped &) — so when the attribute parse
* yields nothing we scan the whole document as a brand-agnostic fallback.
* Without this fallback those three brands silently render parts with no schema
* image (and no hotspots).
*/
private extractPsaImageTicketUrl(html: string): string | null {
const m = html.match(/id="jsinitparams"[^>]+data-params="([^"]+)"/);
if (!m) return null;
try {
const dataParams = JSON.parse(m[1].replace(/&quot;/g, '"').replace(/&amp;/g, "&"));
// PSA: top-level imageViewerParamsUrl
// Ford/Hyundai/Opel/Volvo: nested under jsIlluData[0].imageViewerParamsUrl
return (
(dataParams.imageViewerParamsUrl as string) ||
(dataParams.jsIlluData?.[0]?.imageViewerParamsUrl as string) ||
null
);
} catch {
return null;
if (m) {
try {
const dataParams = JSON.parse(m[1].replace(/&quot;/g, '"').replace(/&amp;/g, "&"));
const url =
(dataParams.imageViewerParamsUrl as string) ||
(dataParams.jsIlluData?.[0]?.imageViewerParamsUrl as string);
if (url) return url;
} catch {
// fall through to the script-blob scan below
}
}
// Hyundai/Kia/Nissan: imageViewerParamsUrl lives in a <script> JSON blob.
const blob = html.match(/"imageViewerParamsUrl"\s*:\s*"([^"]+)"/);
if (blob) {
return blob[1]
.replace(/\\u0026/g, "&")
.replace(/&quot;/g, '"')
.replace(/&amp;/g, "&");
}
return null;
}
/**
@@ -3284,7 +3299,7 @@ export class PL24FordLegacyService {
url: string,
serviceName: string,
isFullUrl = false,
account: "tr" | "de" = "tr",
accountParam: "tr" | "de" = "tr",
retried = false,
): Promise<string | null> {
// Hyundai/Kia/Nissan parts are licensed ONLY on the de account — the tr
@@ -3292,8 +3307,9 @@ export class PL24FordLegacyService {
// fetch for these brands to de (which carries the Korean/Japanese license),
// and ensure the de service token is authorized. Verified on dev: de yields
// 266 Hyundai / 169 Kia models, non-demo, with real drill content.
if (LEGACY_DE_SERVICES.has(serviceName)) {
account = "de";
const isDeOnly = LEGACY_DE_SERVICES.has(serviceName);
const account = isDeOnly ? "de" : accountParam;
if (isDeOnly) {
await this.authService.authorizeServiceForAccount(serviceName, "de");
}
// Some catalogs (Volvo vin-group.action) store hrefs relative to the catalog