@maixio/pstore 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2107 @@
1
+ // src/api/category-names.ts
2
+ var CATEGORY_DISPLAY_NAMES = {
3
+ "cat.gma.AllDeals": {
4
+ "zh-hans-HK": "所有优惠",
5
+ "zh-hant-HK": "所有優惠",
6
+ "zh-hant-TW": "所有優惠",
7
+ "en-US": "All Deals",
8
+ "en-HK": "All Deals",
9
+ "en-TW": "All Deals",
10
+ "ja-JP": "すべてのセール",
11
+ default: "All Deals"
12
+ },
13
+ "cat.gma.x_All_games": {
14
+ "zh-hans-HK": "所有游戏",
15
+ "zh-hant-HK": "所有遊戲",
16
+ "zh-hant-TW": "所有遊戲",
17
+ "en-US": "All Games",
18
+ "en-HK": "All Games",
19
+ "en-TW": "All Games",
20
+ "ja-JP": "すべてのゲーム",
21
+ default: "All Games"
22
+ },
23
+ "cat.gma.NewGames": {
24
+ "zh-hans-HK": "新的游戏",
25
+ "zh-hant-HK": "新遊戲",
26
+ "zh-hant-TW": "新遊戲",
27
+ "en-US": "New Games",
28
+ "en-HK": "New Games",
29
+ "en-TW": "New Games",
30
+ "ja-JP": "新着ゲーム",
31
+ default: "New Games"
32
+ },
33
+ "cat.gma.Pre-Orders": {
34
+ "zh-hans-HK": "预购",
35
+ "zh-hant-HK": "預購",
36
+ "zh-hant-TW": "預購",
37
+ "en-US": "Pre-Orders",
38
+ "en-HK": "Pre-Orders",
39
+ "en-TW": "Pre-Orders",
40
+ "ja-JP": "予約注文",
41
+ default: "Pre-Orders"
42
+ },
43
+ "cat.gma.x_All_PS4_games": {
44
+ "zh-hans-HK": "所有 PS4 游戏",
45
+ "zh-hant-HK": "所有 PS4 遊戲",
46
+ "zh-hant-TW": "所有 PS4 遊戲",
47
+ "en-US": "All PS4 Games",
48
+ "en-HK": "All PS4 Games",
49
+ "en-TW": "All PS4 Games",
50
+ "ja-JP": "すべてのPS4ゲーム",
51
+ default: "All PS4 Games"
52
+ },
53
+ "cat.gma.x_All_PS5_games": {
54
+ "zh-hans-HK": "所有 PS5 游戏",
55
+ "zh-hant-HK": "所有 PS5 遊戲",
56
+ "zh-hant-TW": "所有 PS5 遊戲",
57
+ "en-US": "All PS5 Games",
58
+ "en-HK": "All PS5 Games",
59
+ "en-TW": "All PS5 Games",
60
+ "ja-JP": "すべてのPS5ゲーム",
61
+ default: "All PS5 Games"
62
+ },
63
+ "cat.gma.x_Top_10_games_in_your_country": {
64
+ "zh-hans-HK": "本地十大热门游戏",
65
+ "zh-hant-HK": "本地十大熱門遊戲",
66
+ "zh-hant-TW": "本地十大熱門遊戲",
67
+ "en-US": "Top 10 Games in Your Country",
68
+ "en-HK": "Top 10 Games in Your Country",
69
+ "en-TW": "Top 10 Games in Your Country",
70
+ "ja-JP": "あなたの国のトップ10ゲーム",
71
+ default: "Top 10 Games in Your Country"
72
+ }
73
+ };
74
+ function resolveCategoryDisplayName(localizedName, locale) {
75
+ if (!localizedName)
76
+ return;
77
+ const names = CATEGORY_DISPLAY_NAMES[localizedName];
78
+ if (names) {
79
+ return (locale ? names[locale] : undefined) ?? names.default;
80
+ }
81
+ return formatCategoryKey(localizedName);
82
+ }
83
+ function formatCategoryKey(localizedName) {
84
+ const key = localizedName.replace(/^cat\.gma\./, "");
85
+ if (!key || key === localizedName)
86
+ return;
87
+ return key.replace(/^x_/, "").replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
88
+ }
89
+
90
+ // src/api/catalog-registry.ts
91
+ function catalog(alias, id, localizedName, zhHansHkName, source = "built-in", visibility = "visible", notes) {
92
+ return {
93
+ alias,
94
+ id,
95
+ localizedName,
96
+ displayName: zhHansHkName ? { "zh-hans-HK": zhHansHkName, default: zhHansHkName } : undefined,
97
+ visibility: { default: visibility },
98
+ source,
99
+ notes
100
+ };
101
+ }
102
+ var BUILTIN_CATALOGS = [
103
+ catalog("all-deals", "3f772501-f6f8-49b7-abac-874a88ca4897", "cat.gma.AllDeals", "所有优惠", "manual", "hidden", "Globally valid catalog; not exposed from current storefront navigation."),
104
+ catalog("best-sellers", "132bb05e-89dc-4e66-966d-1297b045e21d", "cat.gma.Best_Sellers", "最畅销"),
105
+ catalog("ps5-pro-enhanced-games", "1d443305-2dcf-4543-8f7e-8c6ec409ecbf", "cat.gma.PS5_Pro_Enhanced_Games", "PS5 Pro Enhanced Games"),
106
+ catalog("all-ps4-games", "30e3fe35-8f2d-4496-95bc-844f56952e3c", "cat.gma.x_All_PS4_games", "所有 PS4 游戏"),
107
+ catalog("pre-orders", "3bf499d7-7acf-4931-97dd-2667494ee2c9", "cat.gma.Pre-Orders", "预购"),
108
+ catalog("free-to-play", "4dfd67ab-4ed7-40b0-a937-a549aece13d0", "cat.gma.FreeToPlay", "基本免费游戏"),
109
+ catalog("add-ons", "51c9aa7a-c0c7-4b68-90b4-328ad11bf42e", "cat.gma.add-onsbygame", "所有追加内容"),
110
+ catalog("play-station-vr2-games", "7b0ad209-dadd-4575-9e51-09ccc803deeb", "cat.gma.PlayStation_VR2_Games", "PlayStation VR2游戏"),
111
+ catalog("all-ps5-games", "d0446d4b-dc9a-4f1e-86ec-651f099c9b29", "cat.gma.x_All_PS5_games", "所有 PS5 游戏"),
112
+ catalog("new-games", "e1699f77-77e1-43ca-a296-26d08abacb0f", "cat.gma.NewGames", "新的游戏"),
113
+ catalog("top-10-games-in-your-country", "fbb563aa-c602-476d-bb92-fe7f35080205", "cat.gma.x_Top_10_games_in_your_country", "本地十大热门游戏"),
114
+ catalog("beginner-friendly", "0a1e21f5-2bf6-45aa-8105-82e0a18940f6", "cat.gma.EDITBeginnerFriendly", "适合新手"),
115
+ catalog("fighting", "0f6fc626-a05d-4378-8537-83f515a6d64e", "cat.gma.Fighting", "战斗"),
116
+ catalog("monthly-picks", "1dfb1512-2d95-4288-aebb-c5355a5083be", "cat.gma.DiscoverMonthlyPicks", "每月精选游戏"),
117
+ catalog("accessibility", "26790261-5100-4c8e-93f2-2f38b189ed20", "cat.gma.EditAccessibility", "协助工具"),
118
+ catalog("all-games", "28c9c2b2-cecc-415c-9a08-482a605cb104", "cat.gma.x_All_games", "所有游戏"),
119
+ catalog("action", "298b428c-0c39-4ec8-abd5-237484e5a2ea", "cat.gma.Action", "动作"),
120
+ catalog("great-games-2026", "2afbc717-a46f-4527-885b-0287dabf9786", "cat.gma.EDIT_26_Great_Games", "2026年绝佳游戏"),
121
+ catalog("robot-games", "2c53964f-8525-4ecc-ad9f-7dd9e87080e4", "cat.gma.RobotGames", "机器人游戏"),
122
+ catalog("open-world", "30cbd819-fe39-4a71-b26b-7bd5e585e81a", "cat.gma.x_Open_World", "开放世界"),
123
+ catalog("family-friendly", "38848c2a-a417-4e79-8fda-b499d1c7621b", "cat.gma.forallages", "合家欢游戏"),
124
+ catalog("story-driven", "3f61c5cb-2a7c-4870-a7ed-73d0eca710c1", "cat.gma.StoryDriven", "剧情导向"),
125
+ catalog("party-music-dance", "53563c94-a8bc-44b5-829e-6b2faa22e858", "cat.gma.PartyMusicandDance", "物品"),
126
+ catalog("play-list", "5d607247-7a7f-49d9-a39f-004183d6ec1c", "cat.gma.x_Explore_The_Play_List", "探索 The Play List"),
127
+ catalog("ea-play", "607787d5-1e83-4183-b028-e2ba3b873e57", "cat.gma.x_EA_Play_", "查看最新 EA Play 游戏试玩版"),
128
+ catalog("shooter", "64ee024b-7644-468a-92c6-370269075d5c", "cat.gma.Shooter", "射击"),
129
+ catalog("horror", "6ec578f6-d6b7-423c-8b93-14e14a5a43f2", "cat.gma.Horror", "体验恐怖"),
130
+ catalog("arcade", "7754f7e7-0f77-4cc6-9329-327f99558f63", "cat.gma.k.PlayStation_Plus_Game_Cata", "街机游戏"),
131
+ catalog("gta-plus-games", "83449c85-2cb2-419e-9f5f-640a925fa213", "cat.gma.GAMES_AVAILABLE_WITH_GTA_PLUS", "GTA+内含的游戏"),
132
+ catalog("heroine", "86d14731-40cd-40ec-b4f5-cdee3ff2b38a", "cat.gma.Heroine2020", "物品"),
133
+ catalog("demos", "95601a70-7564-4771-b305-0283fe3593e4", "cat.gma.Demos", "试玩版游戏"),
134
+ catalog("ps5-essentials", "97d3ba9f-58cb-4b68-b77c-c67744a52df3", "cat.gma.EditPS5Essentials", "PS5必玩游戏"),
135
+ catalog("brain-teasers", "a1bcd7d4-2bcb-44cb-934c-cc1f4ef33ffa", "cat.gma.Edit_BrainTeasers", "脑力大考验"),
136
+ catalog("platformer", "a6c1a446-52a9-4372-89ad-1662708d437c", "cat.gma.Platform", "物品"),
137
+ catalog("most-anticipated", "a7c97306-69bd-45cb-a44f-c9ffd9eaa7d3", "cat.gma.MostAnticipated", "万众期待"),
138
+ catalog("ps-plus-exclusive-discounts", "b3915b25-f581-43dd-95dd-a4ec50dbabe6", "cat.gma..x_PS_Plus_exclusive_discounts", "PS Plus专属折扣"),
139
+ catalog("superheroes", "b5823083-b523-44cf-9b47-5bb14235f256", "cat.gma.Superheroes", "超级英雄"),
140
+ catalog("racing", "b78c6346-2a31-4cf7-978b-134d61ce584d", "cat.gma.i.PlayStation_Plus_Game_Cata", "驾驶与赛车游戏"),
141
+ catalog("sports", "b86f0f65-cf49-4f96-8cdd-3991ca17eadc", "cat.gma.f.PlayStation_Plus_Game_Cata", "体育游戏"),
142
+ catalog("simulation", "bb42a4e0-2d0e-40e5-9714-ae4e10320f24", "cat.gma.Simulation", "模拟"),
143
+ catalog("cozy-games", "bf015f98-c056-442e-ace3-d37794adb358", "cat.gma.EditCozyGames", "享受闲适"),
144
+ catalog("best-companions", "c2ce2847-6c6f-41f5-9a57-e50fff2a9055", "cat.gma.BestCompanions", "最佳伙伴"),
145
+ catalog("play-together", "c6a44ef3-fb6e-4a5d-88b8-1235d77a3d34", "cat.gma.PlayTogether", "结伴同游"),
146
+ catalog("games-to-wishlist", "ca30992e-0de1-4092-b514-e00438fbc0d9", "cat.gma.Games_to_wishlist", "值得收藏的游戏"),
147
+ catalog("jrpgs", "d6fa0b3e-8da9-4990-a215-77be705b3555", "cat.gma.JRPGs", "日系RPG"),
148
+ catalog("kids-family", "d7095c22-4e0b-458c-bb50-fc79c0d4b5a8", "cat.gma.l.PlayStation_Plus_Game_Cata", "儿童和家庭游戏"),
149
+ catalog("ubisoft-plus", "db65f8d8-e606-49af-9a9a-23a05f55bd9a", "cat.gma.UbisoftPlus", "Ubisoft+ Classics"),
150
+ catalog("long-haul-legends", "dbe740ab-97a8-4809-97b0-73c89836f78b", "cat.gma.EDIT_LongHaulLegends_Category", "长篇传奇杰作"),
151
+ catalog("ps5-pro-enhanced-games-alt", "defeaa4c-377c-418e-8642-a0fc39fbdbe4", "cat.gma.PS5_Pro_Enhanced_Games", "PS5 Pro Enhanced Games"),
152
+ catalog("rpg", "e0b1cde3-a7ea-4d7a-960a-fa5edbafae8f", "cat.gma.j.PlayStation_Plus_Game_Cata", "角色扮演游戏"),
153
+ catalog("play-station-studios", "e6562951-eee2-4964-a5e0-176b28bb4603", "cat.gma.EDIT_PlayStationStudios", "Discover PlayStation Studios™"),
154
+ catalog("gta-online-add-ons", "e7132a49-c17f-42e6-968e-0bc4e9e834e0", "cat.am.GTA_Online_Add_Ons", "Grand Theft Auto在线模式(PlayStation®5)追加内容"),
155
+ catalog("online-multiplayer", "edaeb3f9-aeaf-45d4-81ab-67a1f18d5c36", "cat.gma.EditOnline_Multiplayer", "在线多人游玩"),
156
+ catalog("play-station-indies", "f073e967-8952-4b28-908f-35476499178a", "cat.gma.PlayStationIndies", "独立游戏"),
157
+ catalog("yakuza-games", "f0c9b0e6-a9ac-4556-84cc-f449f7cb6994", "cat.gma.EDIT_YakuzaGames_Franchise_Collection_Category", "人中之龙"),
158
+ catalog("editors-choice", "f0e0aa50-998f-4fc7-881b-064f10f0c2cc", "cat.gma.EditorsChoice", "编辑精选"),
159
+ catalog("roguelikes", "f5cebe8a-c63f-4b51-bf00-e786307d6373", "cat.gma.Roguelikes", "Roguelike游戏"),
160
+ catalog("anime-games", "f744ea6a-5367-45d9-ba28-39c0ae624018", "cat.gma.ANIMEgames", "动漫游戏")
161
+ ];
162
+ var CATALOG_ALIASES = Object.fromEntries(BUILTIN_CATALOGS.map((entry) => [entry.alias, entry.id]));
163
+ function resolveCatalogAlias(input) {
164
+ const normalized = input.toLowerCase();
165
+ return BUILTIN_CATALOGS.find((entry) => entry.alias === normalized || entry.id === input);
166
+ }
167
+ function resolveCatalogDisplayName(entry, locale) {
168
+ return (locale ? entry.displayName?.[locale] : undefined) ?? (entry.localizedName ? resolveCategoryDisplayName(entry.localizedName, locale) : undefined) ?? entry.displayName?.default;
169
+ }
170
+ function resolveCatalogVisibility(entry, locale) {
171
+ return (locale ? entry.visibility?.[locale] : undefined) ?? entry.visibility?.default ?? "unknown";
172
+ }
173
+
174
+ // src/types.ts
175
+ var LOCALE_MAP = {
176
+ "zh-hans-HK": { locale: "zh-hans-HK", language: "zh", country: "HK", region: "SIE-ASIA", currency: "HKD" },
177
+ "zh-hant-HK": { locale: "zh-hant-HK", language: "zh", country: "HK", region: "SIE-ASIA", currency: "HKD" },
178
+ "en-HK": { locale: "en-HK", language: "en", country: "HK", region: "SIE-ASIA", currency: "HKD" },
179
+ "zh-hant-TW": { locale: "zh-hant-TW", language: "zh", country: "TW", region: "SIE-ASIA", currency: "TWD" },
180
+ "en-TW": { locale: "en-TW", language: "en", country: "TW", region: "SIE-ASIA", currency: "TWD" },
181
+ "ja-JP": { locale: "ja-JP", language: "ja", country: "JP", region: "SIEJ", currency: "JPY" },
182
+ "en-US": { locale: "en-US", language: "en", country: "US", region: "SIEA", currency: "USD" },
183
+ "en-GB": { locale: "en-GB", language: "en", country: "GB", region: "SIEE", currency: "GBP" },
184
+ "zh-hans-CN": { locale: "zh-hans-CN", language: "zh", country: "CN", region: "SIEC", currency: "CNY" }
185
+ };
186
+
187
+ class PsnApiError extends Error {
188
+ statusCode;
189
+ operationName;
190
+ retryAfterMs;
191
+ constructor(message, statusCode, operationName, retryAfterMs) {
192
+ super(message);
193
+ this.statusCode = statusCode;
194
+ this.operationName = operationName;
195
+ this.retryAfterMs = retryAfterMs;
196
+ this.name = "PsnApiError";
197
+ }
198
+ }
199
+
200
+ // src/locale.ts
201
+ var DEFAULT_LOCALE = "zh-hans-HK";
202
+ var TITLE_LOCALE_PRESETS = {
203
+ "cjk-en": ["zh-hans-HK", "zh-hant-HK", "ja-JP", "en-US"],
204
+ "zh-ja-en": ["zh-hans-HK", "zh-hant-HK", "ja-JP", "en-US"],
205
+ all: ["zh-hans-HK", "zh-hant-HK", "en-HK", "zh-hant-TW", "en-TW", "ja-JP", "en-US", "en-GB", "zh-hans-CN"]
206
+ };
207
+ function isPsnLocale(value) {
208
+ return typeof value === "string" && value in LOCALE_MAP;
209
+ }
210
+ function normalizeLocale(value) {
211
+ if (!value)
212
+ return DEFAULT_LOCALE;
213
+ if (isPsnLocale(value))
214
+ return value;
215
+ const lower = value.toLowerCase();
216
+ const match = Object.keys(LOCALE_MAP).find((locale) => locale.toLowerCase() === lower);
217
+ return match ?? DEFAULT_LOCALE;
218
+ }
219
+ function getLocaleInfo(locale) {
220
+ return LOCALE_MAP[normalizeLocale(locale)];
221
+ }
222
+ function localeToSearchParams(locale) {
223
+ const info = getLocaleInfo(locale);
224
+ return {
225
+ countryCode: info.country,
226
+ languageCode: info.language
227
+ };
228
+ }
229
+ function localeToStorePath(locale) {
230
+ return normalizeLocale(locale).toLowerCase();
231
+ }
232
+ function parseLocaleList(input) {
233
+ if (!input)
234
+ return [];
235
+ const preset = TITLE_LOCALE_PRESETS[input.trim().toLowerCase()];
236
+ if (preset)
237
+ return [...preset];
238
+ const seen = new Set;
239
+ const locales = [];
240
+ for (const item of input.split(",")) {
241
+ const raw = item.trim();
242
+ if (!raw)
243
+ continue;
244
+ if (!isPsnLocale(raw) && !Object.keys(LOCALE_MAP).some((locale2) => locale2.toLowerCase() === raw.toLowerCase())) {
245
+ continue;
246
+ }
247
+ const locale = normalizeLocale(raw);
248
+ if (!seen.has(locale)) {
249
+ seen.add(locale);
250
+ locales.push(locale);
251
+ }
252
+ }
253
+ return locales;
254
+ }
255
+
256
+ // src/fetch.ts
257
+ var API_BASE = "https://web.np.playstation.com/api/graphql/v1/op";
258
+ var TIMEOUT_MS = 1e4;
259
+ var MAX_RETRIES = 2;
260
+ async function fetchPsnApi(operationName, sha256Hash, variables, locale) {
261
+ const url = new URL(API_BASE);
262
+ url.searchParams.set("operationName", operationName);
263
+ url.searchParams.set("variables", JSON.stringify(variables));
264
+ url.searchParams.set("extensions", JSON.stringify({
265
+ persistedQuery: { version: 1, sha256Hash }
266
+ }));
267
+ const headers = {
268
+ "apollo-require-preflight": "true",
269
+ "Accept-Encoding": "gzip, deflate",
270
+ Accept: "application/json"
271
+ };
272
+ if (locale) {
273
+ headers["x-psn-store-locale-override"] = normalizeLocale(locale);
274
+ }
275
+ let lastError = null;
276
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
277
+ try {
278
+ const controller = new AbortController;
279
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
280
+ const response = await fetch(url.toString(), {
281
+ headers,
282
+ signal: controller.signal
283
+ });
284
+ clearTimeout(timeoutId);
285
+ if (!response.ok) {
286
+ const body = await response.text().catch(() => "");
287
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
288
+ throw new PsnApiError(`HTTP ${response.status}: ${body.slice(0, 200)}`, response.status, operationName, retryAfterMs);
289
+ }
290
+ const data = await response.json();
291
+ if (data.errors && data.errors.length > 0) {
292
+ throw new PsnApiError(`GraphQL error: ${data.errors[0]?.message ?? "Unknown error"}`, undefined, operationName);
293
+ }
294
+ return data.data;
295
+ } catch (err) {
296
+ lastError = err instanceof Error ? err : new Error(String(err));
297
+ if (err instanceof PsnApiError && err.statusCode) {
298
+ if (err.statusCode !== 429 && err.statusCode < 500) {
299
+ throw err;
300
+ }
301
+ }
302
+ if (attempt === MAX_RETRIES) {
303
+ throw lastError;
304
+ }
305
+ const retryAfterMs = err instanceof PsnApiError ? err.retryAfterMs : undefined;
306
+ const delayMs = retryAfterMs ?? 1000 * (attempt + 1);
307
+ await new Promise((r) => setTimeout(r, delayMs));
308
+ }
309
+ }
310
+ throw lastError ?? new Error("Unexpected error");
311
+ }
312
+ async function fetchHtml(url) {
313
+ const controller = new AbortController;
314
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS * 2);
315
+ try {
316
+ const response = await fetch(url, {
317
+ headers: {
318
+ "User-Agent": "Mozilla/5.0 (compatible; PsnStoreCLI/1.0)",
319
+ "Accept-Encoding": "gzip"
320
+ },
321
+ signal: controller.signal
322
+ });
323
+ clearTimeout(timeoutId);
324
+ if (!response.ok) {
325
+ throw new PsnApiError(`HTML fetch failed: HTTP ${response.status}`, response.status);
326
+ }
327
+ return await response.text();
328
+ } catch (err) {
329
+ clearTimeout(timeoutId);
330
+ if (err instanceof PsnApiError)
331
+ throw err;
332
+ throw new PsnApiError(`HTML fetch error: ${err.message}`);
333
+ }
334
+ }
335
+ function parseRetryAfter(value) {
336
+ if (!value)
337
+ return;
338
+ const seconds = Number(value);
339
+ if (Number.isFinite(seconds) && seconds >= 0) {
340
+ return Math.min(seconds * 1000, 30000);
341
+ }
342
+ const dateMs = Date.parse(value);
343
+ if (Number.isFinite(dateMs)) {
344
+ return Math.min(Math.max(dateMs - Date.now(), 0), 30000);
345
+ }
346
+ return;
347
+ }
348
+
349
+ // src/cache.ts
350
+ class Cache {
351
+ store = new Map;
352
+ defaultTtl;
353
+ constructor(defaultTtlMs = 5 * 60 * 1000) {
354
+ this.defaultTtl = defaultTtlMs;
355
+ }
356
+ get(key) {
357
+ const entry = this.store.get(key);
358
+ if (!entry)
359
+ return;
360
+ if (Date.now() > entry.expires) {
361
+ this.store.delete(key);
362
+ return;
363
+ }
364
+ return entry.value;
365
+ }
366
+ set(key, value, ttlMs) {
367
+ this.store.set(key, {
368
+ value,
369
+ expires: Date.now() + (ttlMs ?? this.defaultTtl)
370
+ });
371
+ }
372
+ has(key) {
373
+ const entry = this.store.get(key);
374
+ if (!entry)
375
+ return false;
376
+ if (Date.now() > entry.expires) {
377
+ this.store.delete(key);
378
+ return false;
379
+ }
380
+ return true;
381
+ }
382
+ delete(key) {
383
+ this.store.delete(key);
384
+ }
385
+ clear() {
386
+ this.store.clear();
387
+ }
388
+ get size() {
389
+ return this.store.size;
390
+ }
391
+ }
392
+ var TTL = {
393
+ GAME_LOOKUP: 5 * 60 * 1000,
394
+ PRODUCT_TO_CONCEPT: 30 * 60 * 1000,
395
+ SEARCH: 2 * 60 * 1000,
396
+ CATEGORY: 2 * 60 * 1000,
397
+ BATARANG: 10 * 60 * 1000
398
+ };
399
+
400
+ // src/config.ts
401
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
402
+ import { homedir } from "node:os";
403
+ import { join } from "node:path";
404
+ var DEFAULT_CONFIG = {
405
+ locale: "zh-hans-HK",
406
+ format: "text",
407
+ pageSize: 24,
408
+ verbose: false,
409
+ color: true
410
+ };
411
+ var cachedConfig = null;
412
+ function loadConfigFile() {
413
+ const searchPaths = [
414
+ join(process.cwd(), "pstore.config.json"),
415
+ join(process.cwd(), "psn-config.json"),
416
+ join(homedir(), ".config", "pstore", "config.json"),
417
+ join(homedir(), ".config", "psn-cli", "config.json")
418
+ ];
419
+ for (const filePath of searchPaths) {
420
+ try {
421
+ if (existsSync(filePath)) {
422
+ const raw = readFileSync(filePath, "utf-8");
423
+ return JSON.parse(raw);
424
+ }
425
+ } catch {}
426
+ }
427
+ return {};
428
+ }
429
+ function getConfig() {
430
+ if (cachedConfig)
431
+ return cachedConfig;
432
+ const fileConfig = loadConfigFile();
433
+ cachedConfig = {
434
+ ...DEFAULT_CONFIG,
435
+ ...fileConfig,
436
+ locale: normalizeLocale(fileConfig.locale ?? DEFAULT_CONFIG.locale),
437
+ pageSize: clampNumber(fileConfig.pageSize, 1, 100, DEFAULT_CONFIG.pageSize ?? 24)
438
+ };
439
+ return cachedConfig;
440
+ }
441
+ function clampNumber(value, min, max, fallback) {
442
+ if (typeof value !== "number" || !Number.isFinite(value))
443
+ return fallback;
444
+ return Math.min(Math.max(Math.floor(value), min), max);
445
+ }
446
+
447
+ // src/utils.ts
448
+ function detectIdType(id) {
449
+ return /^\d+$/.test(id) ? "concept" : "product";
450
+ }
451
+ function calcDiscountPercent(listPrice, salePrice) {
452
+ if (listPrice <= 0 || salePrice >= listPrice || salePrice < 0)
453
+ return null;
454
+ return Math.round((1 - salePrice / listPrice) * 100);
455
+ }
456
+ function isFreePrice(basePriceValue, discountedValue) {
457
+ return basePriceValue === 0 && discountedValue === 0;
458
+ }
459
+ function isIncludedPrice(basePriceValue, discountedValue, discountText, discountedPrice) {
460
+ if (basePriceValue === null || basePriceValue === undefined || basePriceValue <= 0)
461
+ return false;
462
+ if (discountedValue !== 0)
463
+ return false;
464
+ const text = `${discountText ?? ""} ${discountedPrice ?? ""}`;
465
+ return /included|包含/i.test(text);
466
+ }
467
+ function htmlEntityDecode(str) {
468
+ return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&nbsp;/g, " ").replace(/<br\s*\/?>/gi, `
469
+ `);
470
+ }
471
+ function stripHtml(html) {
472
+ return html.replace(/<[^>]+>/g, "").trim();
473
+ }
474
+
475
+ // src/api/graphql.ts
476
+ var HASH = {
477
+ TELEMETRY_CONCEPT: "906eef4a71644e7c6c9c7b300b68754e385f01149240c1767bbca493c5d92485",
478
+ TELEMETRY_PRODUCT: "71375f8f3dba0de83520ecd474069e037f8ea23b4efcb6778aef58894cd4452d",
479
+ PRICE_CONCEPT: "4ec6effdcdb6e041936c79acecd44aeea347ae3055d2b23ee2c794084b6e9c60",
480
+ PRICE_PRODUCT: "737838e0e3fe50986b4087b51327970a71c80497576bea07904e9ecf4a2dab02",
481
+ RATING_CONCEPT: "6c476325b232d51aca55ce143d6f946860e22174a263967fbb9e1da4f78489fa",
482
+ RATING_PRODUCT: "799fa113378f699281e0eda3154c54e03d763f6a98ad9a1378d58b1c2cb76cec",
483
+ UPSELL_CONCEPT: "20883918a853e2b01a1b73d5ee0bc23f337f00e4d1e07d5cbc8efde9223c460e",
484
+ UPSELL_PRODUCT: "a110672db9e20dc4f4d655fffd2f3a09730914ec3458cfb53de70cb2b526af53",
485
+ SEARCH: "6ef5e809c35a056a1150fdcf513d9c505484dd1a946b6208888435c3182f105a",
486
+ CATEGORY_GRID: "4e41660b6732f35c99fc5541926b7502a09557924e8c2cfebd1beb1a5c8c8f81"
487
+ };
488
+ async function fetchTelemetry(conceptId, locale) {
489
+ const data = await fetchPsnApi("queryRetrieveTelemetryDataPDPConcept", HASH.TELEMETRY_CONCEPT, { conceptId }, locale);
490
+ if (!data?.conceptRetrieve)
491
+ return null;
492
+ const cr = data.conceptRetrieve;
493
+ const dp = cr.defaultProduct;
494
+ return {
495
+ idType: "concept",
496
+ conceptId: cr.id,
497
+ productId: dp?.id,
498
+ name: cr.name,
499
+ productName: dp?.name,
500
+ edition: dp?.edition?.name ?? undefined,
501
+ genres: dp?.localizedGenres?.map((g) => g.value) ?? [],
502
+ skus: dp?.skus,
503
+ starRating: dp?.starRating ? { averageRating: dp.starRating.averageRating } : undefined,
504
+ price: extractBestPrice(dp?.webctas),
505
+ npTitleId: dp?.npTitleId,
506
+ classification: dp?.storeDisplayClassification,
507
+ topCategory: dp?.topCategory,
508
+ platforms: dp?.platforms,
509
+ releaseDate: cr.releaseDate?.value ?? undefined,
510
+ contentRating: dp?.contentRating ?? undefined
511
+ };
512
+ }
513
+ async function fetchTelemetryByProduct(productId, locale) {
514
+ const data = await fetchPsnApi("queryRetrieveTelemetryDataPDPProduct", HASH.TELEMETRY_PRODUCT, { productId }, locale);
515
+ const pr = data?.productRetrieve;
516
+ if (!pr)
517
+ return null;
518
+ return {
519
+ idType: "product",
520
+ conceptId: "",
521
+ productId,
522
+ productName: pr.name,
523
+ edition: pr.edition?.name ?? undefined,
524
+ genres: pr.localizedGenres?.map((g) => g.value) ?? [],
525
+ skus: pr.skus,
526
+ starRating: pr.starRating ? { averageRating: pr.starRating.averageRating } : undefined,
527
+ npTitleId: pr.npTitleId,
528
+ classification: pr.storeDisplayClassification,
529
+ topCategory: pr.topCategory,
530
+ platforms: pr.platforms,
531
+ releaseDate: pr.releaseDate?.value ?? undefined,
532
+ contentRating: pr.contentRating ?? undefined
533
+ };
534
+ }
535
+ function extractBestPrice(webctas) {
536
+ if (!webctas || webctas.length === 0)
537
+ return;
538
+ const numericPrice = webctas.find((w) => {
539
+ const val = w?.price?.basePriceValue ?? 0;
540
+ return val > 0;
541
+ });
542
+ if (numericPrice?.price)
543
+ return numericPrice.price;
544
+ const currencyMatch = webctas.find((w) => {
545
+ const bp = w?.price?.basePrice ?? "";
546
+ return bp.includes("HK$") || bp.includes("¥") || bp.startsWith("$") || bp.includes("€") || bp.includes("£");
547
+ });
548
+ if (currencyMatch?.price)
549
+ return currencyMatch.price;
550
+ const nonTrial = webctas.find((w) => {
551
+ const svc = w?.price?.serviceBranding;
552
+ if (svc?.includes("PS_PLUS"))
553
+ return false;
554
+ const bp = w?.price?.basePrice ?? "";
555
+ return !bp.includes("试玩") && !bp.includes("Free") && !bp.includes("Trial");
556
+ });
557
+ if (nonTrial?.price)
558
+ return nonTrial.price;
559
+ const fallback = webctas[0]?.price;
560
+ if (fallback)
561
+ return fallback;
562
+ return;
563
+ }
564
+ function toPrice(priceInfo) {
565
+ if (!priceInfo)
566
+ return;
567
+ return {
568
+ basePrice: priceInfo.basePrice ?? null,
569
+ basePriceValue: priceInfo.basePriceValue ?? null,
570
+ discountedPrice: priceInfo.discountedPrice ?? null,
571
+ discountedValue: priceInfo.discountedValue ?? null,
572
+ discountText: priceInfo.discountText ?? null,
573
+ currencyCode: priceInfo.currencyCode ?? null,
574
+ endTime: priceInfo.endTime ?? null,
575
+ isFree: isFreePrice(priceInfo.basePriceValue, priceInfo.discountedValue),
576
+ isIncluded: isIncludedPrice(priceInfo.basePriceValue, priceInfo.discountedValue, priceInfo.discountText, priceInfo.discountedPrice)
577
+ };
578
+ }
579
+ async function fetchPrice(conceptId, locale) {
580
+ const data = await fetchPsnApi("conceptRetrieveForCtasWithPrice", HASH.PRICE_CONCEPT, { conceptId }, locale);
581
+ if (!data?.conceptRetrieve)
582
+ return null;
583
+ const priceInfo = extractBestPrice(data.conceptRetrieve.defaultProduct?.webctas);
584
+ return {
585
+ price: toPrice(priceInfo),
586
+ conceptId: data.conceptRetrieve.id,
587
+ productId: data.conceptRetrieve.defaultProduct?.id,
588
+ products: data.conceptRetrieve.products?.map((p) => ({
589
+ id: p.id,
590
+ name: p.name,
591
+ edition: p.edition?.name
592
+ }))
593
+ };
594
+ }
595
+ async function fetchPriceByProduct(productId, locale) {
596
+ const data = await fetchPsnApi("productRetrieveForCtasWithPrice", HASH.PRICE_PRODUCT, { productId }, locale);
597
+ const priceInfo = extractBestPrice(data?.productRetrieve?.webctas);
598
+ if (!priceInfo)
599
+ return null;
600
+ return {
601
+ price: toPrice(priceInfo),
602
+ conceptId: data?.productRetrieve?.concept?.id,
603
+ productId: data?.productRetrieve?.id ?? productId
604
+ };
605
+ }
606
+ async function fetchRating(conceptId, locale) {
607
+ const data = await fetchPsnApi("wcaConceptStarRatingRetrive", HASH.RATING_CONCEPT, { conceptId }, locale);
608
+ if (!data?.conceptRetrieve)
609
+ return null;
610
+ return {
611
+ conceptId: data.conceptRetrieve.id,
612
+ name: data.conceptRetrieve.defaultProduct?.name ?? data.conceptRetrieve.defaultProduct?.name,
613
+ starRating: data.conceptRetrieve.defaultProduct?.starRating,
614
+ classification: data.conceptRetrieve.defaultProduct?.storeDisplayClassification,
615
+ topCategory: data.conceptRetrieve.defaultProduct?.topCategory,
616
+ releaseDate: data.conceptRetrieve.releaseDate?.value ?? undefined
617
+ };
618
+ }
619
+ async function fetchRatingByProduct(productId, locale) {
620
+ const data = await fetchPsnApi("wcaProductStarRatingRetrive", HASH.RATING_PRODUCT, { productId }, locale);
621
+ return {
622
+ conceptId: data?.productRetrieve?.concept?.id,
623
+ productId: data?.productRetrieve?.id ?? productId,
624
+ name: data?.productRetrieve?.name,
625
+ starRating: data?.productRetrieve?.starRating,
626
+ classification: data?.productRetrieve?.storeDisplayClassification,
627
+ topCategory: data?.productRetrieve?.topCategory,
628
+ releaseDate: data?.productRetrieve?.releaseDate?.value ?? undefined
629
+ };
630
+ }
631
+ async function fetchUpsell(conceptId, locale) {
632
+ const data = await fetchPsnApi("conceptRetrieveForUpsellWithCtas", HASH.UPSELL_CONCEPT, { conceptId }, locale);
633
+ if (!data?.conceptRetrieve)
634
+ return null;
635
+ return {
636
+ conceptId: data.conceptRetrieve.id,
637
+ media: data.conceptRetrieve.media?.filter((m) => m.type === "IMAGE").map((m) => ({
638
+ role: m.role,
639
+ type: m.type,
640
+ url: m.url
641
+ })),
642
+ products: data.conceptRetrieve.products?.map((p) => ({
643
+ id: p.id,
644
+ name: p.name,
645
+ edition: p.edition?.name,
646
+ editionType: p.edition?.type,
647
+ platforms: p.platforms,
648
+ classification: p.storeDisplayClassification,
649
+ price: toPrice(extractBestPrice(p.webctas)),
650
+ media: p.media?.filter((m) => m.type === "IMAGE").map((m) => ({
651
+ role: m.role,
652
+ type: m.type,
653
+ url: m.url
654
+ }))
655
+ }))
656
+ };
657
+ }
658
+ async function fetchUpsellByProduct(productId, locale) {
659
+ const data = await fetchPsnApi("productRetrieveForUpsellWithCtas", HASH.UPSELL_PRODUCT, { productId }, locale);
660
+ const concept = data?.productRetrieve?.concept;
661
+ if (!concept)
662
+ return null;
663
+ return {
664
+ conceptId: concept.id,
665
+ productId: data.productRetrieve?.id ?? productId,
666
+ media: concept.media?.filter((m) => m.type === "IMAGE").map((m) => ({
667
+ role: m.role,
668
+ type: m.type,
669
+ url: m.url
670
+ })),
671
+ products: concept.products?.map((p) => ({
672
+ id: p.id,
673
+ name: p.name,
674
+ edition: p.edition?.name,
675
+ editionType: p.edition?.type,
676
+ platforms: p.platforms,
677
+ classification: p.storeDisplayClassification,
678
+ price: toPrice(extractBestPrice(p.webctas))
679
+ }))
680
+ };
681
+ }
682
+ function parseSearchResults(data) {
683
+ if (!data)
684
+ return [];
685
+ const universalSearch = data.universalSearch;
686
+ if (Array.isArray(universalSearch?.results)) {
687
+ return universalSearch.results.map((item) => extractSearchHit(item)).filter((hit) => hit !== null);
688
+ }
689
+ for (const path of ["conceptRetrieve", "searchResults", "results"]) {
690
+ const section = data[path];
691
+ if (section && typeof section === "object") {
692
+ const items = extractItemsFromSection(section);
693
+ if (items.length > 0)
694
+ return items;
695
+ }
696
+ }
697
+ return [];
698
+ }
699
+ function extractItemsFromSection(section) {
700
+ const hits = [];
701
+ for (const value of Object.values(section)) {
702
+ if (!Array.isArray(value))
703
+ continue;
704
+ for (const item of value) {
705
+ if (typeof item !== "object" || item === null)
706
+ continue;
707
+ const hit = extractSearchHit(item);
708
+ if (hit)
709
+ hits.push(hit);
710
+ }
711
+ }
712
+ return hits;
713
+ }
714
+ function extractSearchHit(item) {
715
+ const id = item.id ?? item.productId ?? item.conceptId ?? "";
716
+ const name = item.name ?? item.title ?? "";
717
+ if (!id || !name)
718
+ return null;
719
+ const type = item.__typename?.includes("Product") ? "product" : "concept";
720
+ let price;
721
+ const priceObj = item.price;
722
+ if (priceObj?.displayPrice) {
723
+ price = priceObj.displayPrice;
724
+ } else if (priceObj?.basePrice) {
725
+ price = priceObj.basePrice;
726
+ }
727
+ return {
728
+ id: String(id),
729
+ name: String(name),
730
+ type,
731
+ price,
732
+ platforms: item.platforms,
733
+ classification: item.localizedStoreDisplayClassification ?? item.storeDisplayClassification
734
+ };
735
+ }
736
+ var cfg = getConfig();
737
+ var categoryCache = new Cache(cfg.cacheTtl?.category ?? TTL.CATEGORY);
738
+ async function fetchCategoryGrid(params) {
739
+ const result = await fetchCategoryGridResult(params);
740
+ return result?.items ?? null;
741
+ }
742
+ async function fetchCategoryGridResult(params) {
743
+ const sortBy = typeof params.sortBy === "string" && params.sortBy.length > 2 ? JSON.parse(params.sortBy) : params.sortBy ?? null;
744
+ const variables = {
745
+ id: params.id,
746
+ pageArgs: {
747
+ offset: params.offset ?? 0,
748
+ size: params.size ?? 24
749
+ },
750
+ sortBy,
751
+ filterBy: params.filterBy ?? [],
752
+ maxResults: params.maxResults ?? null,
753
+ facetOptions: [
754
+ "ageRating",
755
+ "conceptCompatibilityNotices",
756
+ "conceptGenres",
757
+ "conceptReleaseDate",
758
+ "conceptVrCompatibility",
759
+ "webBasePrice",
760
+ "productCompatibilityNotices",
761
+ "productGenres",
762
+ "productReleaseDate",
763
+ "productVrCompatibility",
764
+ "storeDisplayClassification",
765
+ "targetPlatforms"
766
+ ]
767
+ };
768
+ const cacheKey = JSON.stringify({ locale: params.locale, variables });
769
+ const cached = categoryCache.get(cacheKey);
770
+ if (cached)
771
+ return cached;
772
+ const data = await fetchPsnApi("categoryGridRetrieve", HASH.CATEGORY_GRID, variables, params.locale);
773
+ const grid = data?.categoryGridRetrieve;
774
+ if (!grid)
775
+ return null;
776
+ const result = parseCategoryGridResult(params.id, grid, params.locale);
777
+ categoryCache.set(cacheKey, result);
778
+ return result;
779
+ }
780
+ function parseCategoryGridResult(id, grid, locale) {
781
+ return {
782
+ id,
783
+ localizedName: grid.localizedName,
784
+ displayName: resolveCategoryDisplayName(grid.localizedName, locale),
785
+ reportingName: grid.reportingName,
786
+ pageInfo: grid.pageInfo ? {
787
+ totalCount: grid.pageInfo.totalCount,
788
+ offset: grid.pageInfo.offset,
789
+ size: grid.pageInfo.size,
790
+ isLast: grid.pageInfo.isLast
791
+ } : undefined,
792
+ sortedBy: grid.sortedBy ? parseSortingOption(grid.sortedBy) : undefined,
793
+ sortingOptions: grid.sortingOptions?.map(parseSortingOption),
794
+ facets: grid.facetOptions?.map((facet) => ({
795
+ name: facet.name,
796
+ displayName: facet.displayName,
797
+ values: (facet.values ?? []).map((value) => ({
798
+ key: value.key,
799
+ displayName: value.displayName,
800
+ count: value.count
801
+ }))
802
+ })),
803
+ items: parseCategoryGridItems(grid)
804
+ };
805
+ }
806
+ function parseSortingOption(option) {
807
+ return {
808
+ name: option.name,
809
+ displayName: option.displayName,
810
+ isAscending: option.isAscending
811
+ };
812
+ }
813
+ function parseCategoryGridItems(grid) {
814
+ const nodes = grid.products?.length ? grid.products : grid.concepts ?? [];
815
+ return nodes.map((node) => ({
816
+ id: node.id,
817
+ name: node.name ?? "Unknown",
818
+ type: node.__typename?.includes("Product") ? "product" : "concept",
819
+ price: node.displayPrice ?? node.price?.discountedPrice ?? node.price?.basePrice,
820
+ platforms: node.platforms,
821
+ classification: node.localizedStoreDisplayClassification ?? node.storeDisplayClassification
822
+ }));
823
+ }
824
+ function clearCategoryCache() {
825
+ categoryCache.clear();
826
+ }
827
+
828
+ // src/api/browse.ts
829
+ var HASH2 = {
830
+ EXPERIENCE: "ca9089d2e37af4044e054283ba4a84a6099a35b30d56d4c6bd00fa44eca2f07a",
831
+ DEFAULT_VIEW: "b359cfcf086857a5828ac75ac43cb863934f540df1b92cceb65f4b62bc61dd48"
832
+ };
833
+ var BROWSE_ALIAS = "browse";
834
+ var BROWSE_CLIENT_ID = "b6de8d4d-bf9b-11ee-ad2a-aea73dc1ea43";
835
+ var BROWSE_EXPERIENCE_ID = "7bbceafe-bfa8-11ee-b375-5e45f4e139ac";
836
+ var BROWSE_CATEGORY_ID = "28c9c2b2-cecc-415c-9a08-482a605cb104";
837
+ var BROWSE_LOCALIZED_KEY_ID = "cat.gma.x_All_games";
838
+ function normalizeBrowseWindow(input) {
839
+ const value = (input ?? "all").toLowerCase().replace(/_/g, "-");
840
+ if (value === "next" || value === "next-thirty-days" || value === "next-30-days") {
841
+ return "next-thirty-days";
842
+ }
843
+ if (value === "last" || value === "last-thirty-days" || value === "last-30-days" || value === "latest") {
844
+ return "last-thirty-days";
845
+ }
846
+ return "all";
847
+ }
848
+ function resolveBrowseSortDefaults(window) {
849
+ switch (window) {
850
+ case "next-thirty-days":
851
+ return { sortBy: "conceptReleaseDate", order: "asc" };
852
+ case "last-thirty-days":
853
+ return { sortBy: "conceptReleaseDate", order: "desc" };
854
+ default:
855
+ return { sortBy: "sales30", order: "desc" };
856
+ }
857
+ }
858
+ function resolveBrowseQuery(query) {
859
+ const window = normalizeBrowseWindow(query.window);
860
+ const defaults = resolveBrowseSortDefaults(window);
861
+ const sortInput = query.sortBy?.toLowerCase();
862
+ let sortBy = normalizeBrowseSort(query.sortBy) ?? defaults.sortBy;
863
+ let order = normalizeBrowseOrder(query.order) ?? defaults.order;
864
+ if (sortInput === "price-asc") {
865
+ sortBy = "webBasePrice";
866
+ order = "asc";
867
+ } else if (sortInput === "price-desc") {
868
+ sortBy = "webBasePrice";
869
+ order = "desc";
870
+ } else if (sortInput === "name-desc") {
871
+ sortBy = "conceptName";
872
+ order = "desc";
873
+ } else if (sortInput === "release-new") {
874
+ sortBy = "conceptReleaseDate";
875
+ order = "desc";
876
+ } else if (sortInput === "release-old") {
877
+ sortBy = "conceptReleaseDate";
878
+ order = "asc";
879
+ }
880
+ const filters = resolveBrowseFilters(query);
881
+ return {
882
+ window,
883
+ sortBy,
884
+ order,
885
+ page: Math.max(1, query.page ?? 1),
886
+ size: Math.min(Math.max(1, query.size ?? 24), 100),
887
+ sample: query.sample && query.sample > 0 ? query.sample : undefined,
888
+ filters
889
+ };
890
+ }
891
+ function sampleItems(items, count, rng = Math.random) {
892
+ if (count <= 0)
893
+ return [];
894
+ if (count >= items.length)
895
+ return [...items];
896
+ const pool = [...items];
897
+ for (let i = pool.length - 1;i > 0; i--) {
898
+ const j = Math.floor(rng() * (i + 1));
899
+ const current = pool[i];
900
+ const target = pool[j];
901
+ if (current !== undefined && target !== undefined) {
902
+ pool[i] = target;
903
+ pool[j] = current;
904
+ }
905
+ }
906
+ return pool.slice(0, count);
907
+ }
908
+ async function fetchBrowseGrid(query) {
909
+ const resolved = resolveBrowseQuery(query);
910
+ const categoryId = await resolveBrowseCategoryId(query.locale);
911
+ const filterBy = resolved.filters;
912
+ const items = await fetchCategoryGrid({
913
+ id: categoryId,
914
+ offset: (resolved.page - 1) * resolved.size,
915
+ size: resolved.size,
916
+ sortBy: { name: resolved.sortBy, isAscending: resolved.order === "asc" },
917
+ filterBy,
918
+ maxResults: null,
919
+ locale: query.locale
920
+ });
921
+ if (!items)
922
+ return null;
923
+ return {
924
+ categoryId,
925
+ items,
926
+ query: resolved
927
+ };
928
+ }
929
+ async function resolveBrowseCategoryId(locale) {
930
+ try {
931
+ await fetchPsnApi("getExperience", HASH2.EXPERIENCE, { clientId: BROWSE_CLIENT_ID, alias: BROWSE_ALIAS }, locale);
932
+ const defaultView = await fetchPsnApi("getDefaultView", HASH2.DEFAULT_VIEW, {
933
+ experienceId: BROWSE_EXPERIENCE_ID,
934
+ categoryId: BROWSE_CATEGORY_ID,
935
+ localizedKeyId: BROWSE_LOCALIZED_KEY_ID
936
+ }, locale);
937
+ const candidate = pickString([
938
+ defaultView?.categoryId,
939
+ defaultView?.gridCategoryId,
940
+ defaultView?.defaultView?.categoryId,
941
+ defaultView?.getDefaultView?.categoryId,
942
+ defaultView?.data?.categoryId,
943
+ defaultView?.data?.gridCategoryId
944
+ ]);
945
+ return candidate ?? BROWSE_CATEGORY_ID;
946
+ } catch {
947
+ return BROWSE_CATEGORY_ID;
948
+ }
949
+ }
950
+ function normalizeBrowseSort(input) {
951
+ if (!input)
952
+ return null;
953
+ const value = input.toLowerCase();
954
+ if (value === "conceptreleasedate" || value === "release-date" || value === "releasedate") {
955
+ return "conceptReleaseDate";
956
+ }
957
+ if (value === "conceptname" || value === "name") {
958
+ return "conceptName";
959
+ }
960
+ if (value === "sales30" || value === "best-selling" || value === "bestselling") {
961
+ return "sales30";
962
+ }
963
+ if (value === "downloads30" || value === "best-downloaded" || value === "bestdownloaded") {
964
+ return "downloads30";
965
+ }
966
+ if (value === "webbaseprice") {
967
+ return "webBasePrice";
968
+ }
969
+ if (value === "contentcollections.contentcollectionstartdate" || value === "content-collection-start-date") {
970
+ return "contentCollections.contentCollectionStartDate";
971
+ }
972
+ return input;
973
+ }
974
+ function normalizeBrowseOrder(input) {
975
+ if (!input)
976
+ return null;
977
+ const value = input.toLowerCase();
978
+ if (value === "asc" || value === "ascending")
979
+ return "asc";
980
+ if (value === "desc" || value === "descending")
981
+ return "desc";
982
+ return null;
983
+ }
984
+ function resolveBrowseFilters(query) {
985
+ const filters = [];
986
+ const window = normalizeBrowseWindow(query.window);
987
+ if (query.filters) {
988
+ for (const filter of query.filters) {
989
+ const normalized = filter.trim();
990
+ if (normalized) {
991
+ filters.push(normalized);
992
+ }
993
+ }
994
+ }
995
+ const pushFacet = (facet, values) => {
996
+ for (const value of values ?? []) {
997
+ const normalized = value.trim();
998
+ if (normalized) {
999
+ filters.push(`${facet}:${normalized}`);
1000
+ }
1001
+ }
1002
+ };
1003
+ pushFacet("targetPlatforms", query.platforms);
1004
+ pushFacet("conceptGenres", query.genres);
1005
+ pushFacet("webBasePrice", query.prices);
1006
+ pushFacet("storeDisplayClassification", query.classifications);
1007
+ pushFacet("ageRating", query.ageRatings);
1008
+ pushFacet("productVrCompatibility", query.vrCompatibilities);
1009
+ pushFacet("conceptCompatibilityNotices", query.notices);
1010
+ if (window === "next-thirty-days") {
1011
+ filters.unshift("conceptReleaseDate:next_thirty_days");
1012
+ } else if (window === "last-thirty-days") {
1013
+ filters.unshift("conceptReleaseDate:last_thirty_days");
1014
+ }
1015
+ return filters;
1016
+ }
1017
+ function pickString(values) {
1018
+ for (const value of values) {
1019
+ if (typeof value === "string" && value.length > 0) {
1020
+ return value;
1021
+ }
1022
+ }
1023
+ return;
1024
+ }
1025
+
1026
+ // src/api/search.ts
1027
+ var SEARCH_HASH = "6ef5e809c35a056a1150fdcf513d9c505484dd1a946b6208888435c3182f105a";
1028
+ var cfg2 = getConfig();
1029
+ var searchCache = new Cache(cfg2.cacheTtl?.search ?? TTL.SEARCH);
1030
+ async function searchGames(options) {
1031
+ const localeInfo = localeToSearchParams(options.locale);
1032
+ const page = options.page ?? 1;
1033
+ const size = options.size ?? 24;
1034
+ const variables = {
1035
+ searchTerm: options.term,
1036
+ countryCode: localeInfo.countryCode,
1037
+ languageCode: localeInfo.languageCode,
1038
+ pageSize: size,
1039
+ pageOffset: (page - 1) * size
1040
+ };
1041
+ if (options.nextCursor) {
1042
+ variables.nextCursor = options.nextCursor;
1043
+ }
1044
+ const cacheKey = JSON.stringify({ locale: options.locale, variables });
1045
+ const cached = searchCache.get(cacheKey);
1046
+ if (cached)
1047
+ return cached;
1048
+ const rawData = await fetchPsnApi("getSearchResults", SEARCH_HASH, variables, options.locale);
1049
+ const response = parseSearchResponse(rawData, page, size);
1050
+ searchCache.set(cacheKey, response);
1051
+ return response;
1052
+ }
1053
+ function parseSearchResponse(rawData, page, size) {
1054
+ const hits = parseSearchResults(rawData);
1055
+ const nextCursor = extractNextCursor(rawData);
1056
+ const pageInfo = extractPageInfo(rawData);
1057
+ return {
1058
+ hits,
1059
+ page,
1060
+ size,
1061
+ nextCursor,
1062
+ isLast: pageInfo?.isLast,
1063
+ total: pageInfo?.totalCount
1064
+ };
1065
+ }
1066
+ function extractNextCursor(data) {
1067
+ if (!data)
1068
+ return;
1069
+ const universalSearch = data.universalSearch;
1070
+ return universalSearch?.next;
1071
+ }
1072
+ function extractPageInfo(data) {
1073
+ if (!data)
1074
+ return;
1075
+ const universalSearch = data.universalSearch;
1076
+ const pageInfo = universalSearch?.pageInfo;
1077
+ if (!pageInfo)
1078
+ return;
1079
+ return {
1080
+ isLast: typeof pageInfo.isLast === "boolean" ? pageInfo.isLast : undefined,
1081
+ totalCount: typeof pageInfo.totalCount === "number" ? pageInfo.totalCount : undefined
1082
+ };
1083
+ }
1084
+ function clearSearchCache() {
1085
+ searchCache.clear();
1086
+ }
1087
+
1088
+ // src/api/status.ts
1089
+ var STATUS_CONFIG_URL = "https://status.playstation.com/config/app.json";
1090
+ var DEFAULT_STATUS_BASE_URL = "https://status.playstation.com/data/";
1091
+ var DEFAULT_STATUS_SUFFIX = ".json";
1092
+ async function fetchPsnStatus(options = {}) {
1093
+ const locale = normalizeLocale(options.locale);
1094
+ const country = (options.country ?? getLocaleInfo(locale).country).toUpperCase();
1095
+ const region = options.region ?? regionForCountry(country);
1096
+ const config = await fetchStatusConfig().catch(() => null);
1097
+ const baseUrl = config?.status_base_url ?? DEFAULT_STATUS_BASE_URL;
1098
+ const suffix = config?.status_base_url_suffix ?? DEFAULT_STATUS_SUFFIX;
1099
+ const url = `${baseUrl}statuses/region/${region}${suffix}`;
1100
+ const response = await fetch(url, {
1101
+ headers: {
1102
+ Accept: "application/json",
1103
+ "User-Agent": "pstore/0.0 status-check"
1104
+ }
1105
+ });
1106
+ if (!response.ok) {
1107
+ throw new Error(`PlayStation status request failed: HTTP ${response.status}`);
1108
+ }
1109
+ const raw = await response.json();
1110
+ return parsePsnStatus(raw, {
1111
+ locale,
1112
+ country,
1113
+ region,
1114
+ refreshIntervalMs: config?.status_refresh_interval
1115
+ });
1116
+ }
1117
+ function parsePsnStatus(raw, context) {
1118
+ const country = raw.countries?.find((item) => item.countryCode?.toUpperCase() === context.country);
1119
+ if (!country) {
1120
+ throw new Error(`Country ${context.country} not found in PlayStation status region ${context.region}`);
1121
+ }
1122
+ const incidents = normalizeIncidents(country.status ?? raw.status ?? [], context.locale);
1123
+ const services = (country.services ?? []).map((service) => {
1124
+ const serviceIncidents = normalizeIncidents(service.status ?? [], context.locale);
1125
+ const resources = (service.resources ?? []).map((resource) => {
1126
+ const resourceIncidents = normalizeIncidents(resource.status ?? [], context.locale);
1127
+ return {
1128
+ name: pickLocalizedText(resource.i18n, context.locale) ?? resource.resourceName ?? "Unknown",
1129
+ status: statusFromIncidents(resourceIncidents),
1130
+ incidents: resourceIncidents
1131
+ };
1132
+ });
1133
+ return {
1134
+ id: service.serviceId ?? service.serviceName ?? "unknown",
1135
+ name: pickLocalizedText(service.i18n, context.locale) ?? service.serviceName ?? "Unknown",
1136
+ status: statusFromIncidents(serviceIncidents),
1137
+ incidents: serviceIncidents,
1138
+ resources
1139
+ };
1140
+ });
1141
+ return {
1142
+ region: context.region,
1143
+ country: context.country,
1144
+ locale: context.locale,
1145
+ overallStatus: statusFromStatuses([
1146
+ statusFromIncidents(incidents),
1147
+ ...services.map((service) => service.status)
1148
+ ]),
1149
+ generatedAt: raw.generatedAt,
1150
+ updatedAt: latestModifiedDate([...incidents, ...services.flatMap((service) => service.incidents)]),
1151
+ refreshIntervalMs: context.refreshIntervalMs,
1152
+ services,
1153
+ incidents
1154
+ };
1155
+ }
1156
+ function regionForCountry(country) {
1157
+ const code = country.toUpperCase();
1158
+ if (["US", "CA", "MX", "BR", "AR", "CL", "CO", "PE"].includes(code))
1159
+ return "SCEA";
1160
+ if (["HK", "TW", "JP", "CN", "KR", "SG", "TH", "MY", "ID"].includes(code))
1161
+ return "SCEJA";
1162
+ return "SCEE";
1163
+ }
1164
+ async function fetchStatusConfig() {
1165
+ const response = await fetch(STATUS_CONFIG_URL, {
1166
+ headers: {
1167
+ Accept: "application/json",
1168
+ "User-Agent": "pstore/0.0 status-check"
1169
+ }
1170
+ });
1171
+ if (!response.ok) {
1172
+ throw new Error(`PlayStation status config request failed: HTTP ${response.status}`);
1173
+ }
1174
+ return response.json();
1175
+ }
1176
+ function normalizeIncidents(statuses, locale) {
1177
+ return statuses.filter((status) => normalizeStatusType(status.statusType) !== "ok").map((status) => ({
1178
+ id: status.statusId,
1179
+ statusType: status.statusType ?? "Unknown",
1180
+ message: pickLocalizedText(status.message?.messages, locale) ?? status.message?.messageKey,
1181
+ startDate: status.startDate,
1182
+ modifiedDate: status.modifiedDate
1183
+ }));
1184
+ }
1185
+ function statusFromIncidents(incidents) {
1186
+ return statusFromStatuses(incidents.map((incident) => normalizeStatusType(incident.statusType)));
1187
+ }
1188
+ function statusFromStatuses(statuses) {
1189
+ if (statuses.includes("outage"))
1190
+ return "outage";
1191
+ if (statuses.includes("degraded"))
1192
+ return "degraded";
1193
+ if (statuses.includes("maintenance"))
1194
+ return "maintenance";
1195
+ return "ok";
1196
+ }
1197
+ function normalizeStatusType(statusType) {
1198
+ const value = statusType?.toLowerCase();
1199
+ if (value === "outage")
1200
+ return "outage";
1201
+ if (value === "degraded")
1202
+ return "degraded";
1203
+ if (value === "maintenance")
1204
+ return "maintenance";
1205
+ return "ok";
1206
+ }
1207
+ function pickLocalizedText(values, locale) {
1208
+ if (!values)
1209
+ return;
1210
+ if (values[locale])
1211
+ return values[locale];
1212
+ const language = locale.split("-")[0]?.toLowerCase();
1213
+ const sameLanguage = Object.entries(values).find(([key]) => key.toLowerCase().startsWith(`${language}-`));
1214
+ if (sameLanguage)
1215
+ return sameLanguage[1];
1216
+ return values["en-US"] ?? values["en-GB"] ?? Object.values(values)[0];
1217
+ }
1218
+ function latestModifiedDate(incidents) {
1219
+ return incidents.map((incident) => incident.modifiedDate ?? incident.startDate).filter((value) => Boolean(value)).sort().at(-1);
1220
+ }
1221
+
1222
+ // src/api/batarang.ts
1223
+ var ENV_SCRIPT_RE = /<script\s+id="env:[^"]*"\s+type="application\/json">([\s\S]*?)<\/script>/gi;
1224
+ async function fetchBatarang(conceptId, locale) {
1225
+ const localePath = localeToStorePath(locale);
1226
+ const url = `https://store.playstation.com/${localePath}/concept/${conceptId}`;
1227
+ try {
1228
+ const html = await fetchHtml(url);
1229
+ return parseBatarangAll(html);
1230
+ } catch {
1231
+ return null;
1232
+ }
1233
+ }
1234
+ async function fetchBatarangByProduct(productId, locale) {
1235
+ const localePath = localeToStorePath(locale);
1236
+ const url = `https://store.playstation.com/${localePath}/product/${productId}`;
1237
+ try {
1238
+ const html = await fetchHtml(url);
1239
+ return parseBatarangAll(html);
1240
+ } catch {
1241
+ return null;
1242
+ }
1243
+ }
1244
+ function extractEnvScripts(html) {
1245
+ const scripts = [];
1246
+ let match;
1247
+ while ((match = ENV_SCRIPT_RE.exec(html)) !== null) {
1248
+ const script = match[1];
1249
+ if (script)
1250
+ scripts.push(script);
1251
+ }
1252
+ return scripts;
1253
+ }
1254
+ function parseBatarangAll(html) {
1255
+ const scripts = extractEnvScripts(html);
1256
+ const merged = {};
1257
+ const conceptEntries = {};
1258
+ for (const script of scripts) {
1259
+ try {
1260
+ const data = JSON.parse(script);
1261
+ const cache = data.cache ?? {};
1262
+ for (const [key, value] of Object.entries(cache)) {
1263
+ if (typeof value !== "object" || value === null)
1264
+ continue;
1265
+ const record = value;
1266
+ if (key.startsWith("Product:")) {
1267
+ const id = key.split(":").slice(1).join(":");
1268
+ merged[id] = smartMerge(merged[id], record);
1269
+ } else if (key.startsWith("Concept:")) {
1270
+ const id = key.split(":").slice(1).join(":");
1271
+ conceptEntries[id] = smartMerge(conceptEntries[id], record);
1272
+ }
1273
+ }
1274
+ } catch {}
1275
+ }
1276
+ const result = {};
1277
+ for (const product of Object.values(merged)) {
1278
+ if (!result.name && typeof product.name === "string") {
1279
+ result.name = product.name;
1280
+ }
1281
+ if (!result.edition && product.edition && typeof product.edition === "object") {
1282
+ const edition = product.edition;
1283
+ if (typeof edition.name === "string") {
1284
+ result.edition = edition.name;
1285
+ }
1286
+ }
1287
+ if (!result.genres && Array.isArray(product.localizedGenres)) {
1288
+ result.genres = product.localizedGenres.map((g) => g.value).filter((value) => typeof value === "string" && value.length > 0);
1289
+ }
1290
+ if (!result.platforms && Array.isArray(product.platforms)) {
1291
+ result.platforms = product.platforms.filter((platform) => typeof platform === "string" && platform.length > 0);
1292
+ }
1293
+ if (!result.classification && typeof product.storeDisplayClassification === "string") {
1294
+ result.classification = product.storeDisplayClassification;
1295
+ }
1296
+ if (!result.topCategory && typeof product.topCategory === "string") {
1297
+ result.topCategory = product.topCategory;
1298
+ }
1299
+ if (!result.npTitleId && typeof product.npTitleId === "string") {
1300
+ result.npTitleId = product.npTitleId;
1301
+ }
1302
+ if (!result.starRating && product.starRating && typeof product.starRating === "object") {
1303
+ const sr = product.starRating;
1304
+ const averageRating = typeof sr.averageRating === "number" ? sr.averageRating : undefined;
1305
+ if (typeof averageRating === "number") {
1306
+ result.starRating = {
1307
+ averageRating,
1308
+ averageRatingForDisplay: typeof sr.averageRatingForDisplay === "string" ? sr.averageRatingForDisplay : undefined,
1309
+ totalRatingsCount: typeof sr.totalRatingsCount === "number" ? sr.totalRatingsCount : undefined,
1310
+ ratingsDistribution: Array.isArray(sr.ratingsDistribution) ? sr.ratingsDistribution : undefined
1311
+ };
1312
+ }
1313
+ }
1314
+ if (product.descriptions) {
1315
+ result.descriptions = product.descriptions;
1316
+ }
1317
+ if (product.publisherName) {
1318
+ result.publisherName = product.publisherName;
1319
+ }
1320
+ if (result.descriptions && result.publisherName) {
1321
+ break;
1322
+ }
1323
+ }
1324
+ for (const concept of Object.values(conceptEntries)) {
1325
+ if (!result.name && typeof concept.name === "string") {
1326
+ result.name = concept.name;
1327
+ }
1328
+ if (!result.genres && Array.isArray(concept.localizedGenres)) {
1329
+ result.genres = concept.localizedGenres.map((g) => g.value).filter((value) => typeof value === "string" && value.length > 0);
1330
+ }
1331
+ if (!result.platforms && Array.isArray(concept.platforms)) {
1332
+ result.platforms = concept.platforms.filter((platform) => typeof platform === "string" && platform.length > 0);
1333
+ }
1334
+ if (!result.classification && typeof concept.storeDisplayClassification === "string") {
1335
+ result.classification = concept.storeDisplayClassification;
1336
+ }
1337
+ if (!result.topCategory && typeof concept.topCategory === "string") {
1338
+ result.topCategory = concept.topCategory;
1339
+ }
1340
+ if (!result.npTitleId && typeof concept.npTitleId === "string") {
1341
+ result.npTitleId = concept.npTitleId;
1342
+ }
1343
+ if (!result.starRating && concept.starRating && typeof concept.starRating === "object") {
1344
+ const sr = concept.starRating;
1345
+ const averageRating = typeof sr.averageRating === "number" ? sr.averageRating : undefined;
1346
+ if (typeof averageRating === "number") {
1347
+ result.starRating = {
1348
+ averageRating,
1349
+ averageRatingForDisplay: typeof sr.averageRatingForDisplay === "string" ? sr.averageRatingForDisplay : undefined,
1350
+ totalRatingsCount: typeof sr.totalRatingsCount === "number" ? sr.totalRatingsCount : undefined,
1351
+ ratingsDistribution: Array.isArray(sr.ratingsDistribution) ? sr.ratingsDistribution : undefined
1352
+ };
1353
+ }
1354
+ }
1355
+ if (!result.descriptions && concept.descriptions) {
1356
+ result.descriptions = concept.descriptions;
1357
+ }
1358
+ if (!result.publisherName && concept.publisherName) {
1359
+ result.publisherName = concept.publisherName;
1360
+ }
1361
+ }
1362
+ for (const product of Object.values(merged)) {
1363
+ if (!result.releaseDate) {
1364
+ const releaseDate = extractReleaseDate(product.releaseDate);
1365
+ if (releaseDate) {
1366
+ result.releaseDate = releaseDate;
1367
+ }
1368
+ }
1369
+ }
1370
+ for (const concept of Object.values(conceptEntries)) {
1371
+ if (!result.releaseDate) {
1372
+ const releaseDate = extractReleaseDate(concept.releaseDate);
1373
+ if (releaseDate) {
1374
+ result.releaseDate = releaseDate;
1375
+ }
1376
+ }
1377
+ }
1378
+ for (const product of Object.values(merged)) {
1379
+ if (product.compatibilityNoticesByPlatform && !result.compatibilityNoticesByPlatform) {
1380
+ result.compatibilityNoticesByPlatform = product.compatibilityNoticesByPlatform;
1381
+ }
1382
+ if (product.screenLanguages && !result.screenLanguages) {
1383
+ result.screenLanguages = product.screenLanguages;
1384
+ }
1385
+ if (product.spokenLanguages && !result.spokenLanguages) {
1386
+ result.spokenLanguages = product.spokenLanguages;
1387
+ }
1388
+ if (product.contentRating && !result.contentRating) {
1389
+ result.contentRating = product.contentRating;
1390
+ }
1391
+ }
1392
+ for (const concept of Object.values(conceptEntries)) {
1393
+ if (!result.contentRating && concept.contentRating) {
1394
+ result.contentRating = concept.contentRating;
1395
+ }
1396
+ }
1397
+ return result;
1398
+ }
1399
+ function extractReleaseDate(value) {
1400
+ if (typeof value === "string" && value.length > 0) {
1401
+ return value;
1402
+ }
1403
+ if (value && typeof value === "object") {
1404
+ const record = value;
1405
+ if (typeof record.value === "string" && record.value.length > 0) {
1406
+ return record.value;
1407
+ }
1408
+ }
1409
+ return;
1410
+ }
1411
+ function smartMerge(existing, incoming) {
1412
+ if (!existing)
1413
+ return { ...incoming };
1414
+ const result = { ...existing };
1415
+ for (const [key, value] of Object.entries(incoming)) {
1416
+ if (value === null || value === undefined)
1417
+ continue;
1418
+ const current = result[key];
1419
+ if (current === undefined || current === null) {
1420
+ result[key] = value;
1421
+ } else if (Array.isArray(value) && Array.isArray(current)) {
1422
+ if (value.length > current.length) {
1423
+ result[key] = value;
1424
+ }
1425
+ } else if (typeof value === "object" && typeof current === "object" && !Array.isArray(value) && !Array.isArray(current)) {
1426
+ if (Object.keys(value).length > Object.keys(current).length) {
1427
+ result[key] = value;
1428
+ }
1429
+ }
1430
+ }
1431
+ return result;
1432
+ }
1433
+
1434
+ // src/build-record.ts
1435
+ function buildLookupRecord(data) {
1436
+ const { inputId, telemetry, price, rating, upsell, batarang, concept, locale, timing, includeProducts, warnings } = data;
1437
+ const idType = data.idType ?? telemetry?.idType;
1438
+ const conceptId = telemetry?.conceptId || price?.conceptId || rating?.conceptId || upsell?.conceptId || undefined;
1439
+ const productId = telemetry?.productId ?? price?.productId ?? rating?.productId ?? upsell?.productId;
1440
+ const resolvedPrice = resolvePrice(price, telemetry);
1441
+ const resolvedName = telemetry?.name ?? telemetry?.productName ?? rating?.name ?? batarang?.name ?? "";
1442
+ const resolvedRating = rating?.starRating ?? telemetry?.starRating ?? batarang?.starRating ?? undefined;
1443
+ const resolvedReleaseDate = batarang?.releaseDate ?? telemetry?.releaseDate ?? rating?.releaseDate ?? undefined;
1444
+ const resolvedMedia = upsell?.media;
1445
+ const resolvedProducts = includeProducts ? upsell?.products?.map((p) => ({
1446
+ id: p.id,
1447
+ name: p.name ?? "",
1448
+ edition: p.edition ? { name: p.edition } : undefined,
1449
+ price: p.price,
1450
+ platforms: p.platforms,
1451
+ storeDisplayClassification: p.classification
1452
+ })) : undefined;
1453
+ const result = {
1454
+ id: idType === "product" ? productId ?? inputId ?? "" : conceptId ?? inputId ?? "",
1455
+ idType,
1456
+ conceptId,
1457
+ productId,
1458
+ name: resolvedName,
1459
+ locale: locale ?? "zh-hans-HK",
1460
+ productName: telemetry?.productName,
1461
+ edition: telemetry?.edition ?? batarang?.edition,
1462
+ genres: telemetry?.genres ?? batarang?.genres,
1463
+ publisher: batarang?.publisherName ?? undefined,
1464
+ releaseDate: resolvedReleaseDate,
1465
+ price: resolvedPrice,
1466
+ starRating: resolvedRating,
1467
+ classification: telemetry?.classification ?? rating?.classification ?? batarang?.classification,
1468
+ topCategory: telemetry?.topCategory ?? rating?.topCategory ?? batarang?.topCategory,
1469
+ platforms: telemetry?.platforms ?? batarang?.platforms,
1470
+ skus: telemetry?.skus,
1471
+ descriptions: batarang?.descriptions,
1472
+ media: resolvedMedia,
1473
+ products: resolvedProducts,
1474
+ npTitleId: telemetry?.npTitleId ?? batarang?.npTitleId,
1475
+ contentRating: batarang?.contentRating ?? telemetry?.contentRating ?? undefined,
1476
+ sources: {
1477
+ name: telemetry?.name ? "telemetry" : telemetry?.productName ? "telemetry" : rating?.name ? "rating" : batarang?.name ? "batarang" : "none",
1478
+ edition: telemetry?.edition ? "telemetry" : batarang?.edition ? "batarang" : "none",
1479
+ genres: telemetry?.genres?.length ? "telemetry" : batarang?.genres?.length ? "batarang" : "none",
1480
+ publisher: batarang?.publisherName ? "batarang" : "none",
1481
+ releaseDate: batarang?.releaseDate ? "batarang" : telemetry?.releaseDate ? "telemetry" : rating?.releaseDate ? "rating" : "none",
1482
+ price: price?.price ? "price" : telemetry?.price ? "telemetry" : "none",
1483
+ starRating: rating?.starRating ? "rating" : telemetry?.starRating ? "telemetry" : batarang?.starRating ? "batarang" : "none",
1484
+ classification: telemetry?.classification ? "telemetry" : rating?.classification ? "rating" : batarang?.classification ? "batarang" : "none",
1485
+ topCategory: telemetry?.topCategory ? "telemetry" : rating?.topCategory ? "rating" : batarang?.topCategory ? "batarang" : "none",
1486
+ platforms: telemetry?.platforms?.length ? "telemetry" : batarang?.platforms?.length ? "batarang" : "none",
1487
+ skus: telemetry?.skus?.length ? "telemetry" : "none",
1488
+ descriptions: batarang?.descriptions?.length ? "batarang" : "none",
1489
+ media: resolvedMedia?.length ? "upsell" : "none",
1490
+ products: resolvedProducts?.length ? "upsell" : "none",
1491
+ npTitleId: telemetry?.npTitleId ? "telemetry" : batarang?.npTitleId ? "batarang" : "none",
1492
+ contentRating: batarang?.contentRating ? "batarang" : telemetry?.contentRating ? "telemetry" : "none"
1493
+ },
1494
+ timing,
1495
+ warnings: warnings && warnings.length > 0 ? warnings : undefined,
1496
+ concept: concept ?? undefined
1497
+ };
1498
+ return result;
1499
+ }
1500
+ function resolvePrice(priceResult, telemetry) {
1501
+ if (priceResult?.price) {
1502
+ const p = priceResult.price;
1503
+ const discountPercent = p.basePriceValue && p.discountedValue && p.basePriceValue > 0 ? calcDiscountPercent(p.basePriceValue, p.discountedValue) : null;
1504
+ return {
1505
+ ...p,
1506
+ discountPercent,
1507
+ isFree: isFreePrice(p.basePriceValue, p.discountedValue),
1508
+ isIncluded: p.isIncluded ?? isIncludedPrice(p.basePriceValue, p.discountedValue, p.discountText, p.discountedPrice)
1509
+ };
1510
+ }
1511
+ if (telemetry?.price) {
1512
+ const p = telemetry.price;
1513
+ const discountPercent = p.basePriceValue && p.discountedValue && p.basePriceValue > 0 ? calcDiscountPercent(p.basePriceValue, p.discountedValue) : null;
1514
+ return {
1515
+ basePrice: p.basePrice ?? null,
1516
+ basePriceValue: p.basePriceValue ?? null,
1517
+ discountedPrice: p.discountedPrice ?? null,
1518
+ discountedValue: p.discountedValue ?? null,
1519
+ discountText: null,
1520
+ currencyCode: null,
1521
+ endTime: null,
1522
+ discountPercent,
1523
+ isFree: isFreePrice(p.basePriceValue, p.discountedValue),
1524
+ isIncluded: isIncludedPrice(p.basePriceValue, p.discountedValue, p.discountText, p.discountedPrice)
1525
+ };
1526
+ }
1527
+ return;
1528
+ }
1529
+
1530
+ // src/perf.ts
1531
+ class PerfTracer {
1532
+ startTime;
1533
+ spans = new Map;
1534
+ label;
1535
+ constructor(label) {
1536
+ this.startTime = performance.now();
1537
+ this.label = label ?? "trace";
1538
+ }
1539
+ async span(name, fn) {
1540
+ const start = performance.now();
1541
+ try {
1542
+ return await fn();
1543
+ } finally {
1544
+ this.spans.set(name, performance.now() - start);
1545
+ }
1546
+ }
1547
+ spanSync(name, fn) {
1548
+ const start = performance.now();
1549
+ try {
1550
+ return fn();
1551
+ } finally {
1552
+ this.spans.set(name, performance.now() - start);
1553
+ }
1554
+ }
1555
+ elapsed() {
1556
+ return performance.now() - this.startTime;
1557
+ }
1558
+ getTiming() {
1559
+ const result = {};
1560
+ for (const [name, duration] of this.spans) {
1561
+ result[name] = Math.round(duration);
1562
+ }
1563
+ result.total = Math.round(this.elapsed());
1564
+ return result;
1565
+ }
1566
+ report() {
1567
+ const total = this.elapsed();
1568
+ console.log(`
1569
+ ${this.label}:`);
1570
+ console.log("Phase".padEnd(30), "Time".padEnd(10), "%");
1571
+ console.log("-".repeat(50));
1572
+ for (const [name, duration] of this.spans) {
1573
+ const pct = total > 0 ? (duration / total * 100).toFixed(1) : "0.0";
1574
+ console.log(name.padEnd(30), `${Math.round(duration)}ms`.padEnd(10), `${pct}%`);
1575
+ }
1576
+ console.log("-".repeat(50));
1577
+ console.log("total".padEnd(30), `${Math.round(total)}ms`);
1578
+ }
1579
+ }
1580
+
1581
+ // src/provider.ts
1582
+ var cfg3 = getConfig();
1583
+ var lookupCache = new Cache(cfg3.cacheTtl?.lookup ?? TTL.GAME_LOOKUP);
1584
+ var conceptCache = new Cache(TTL.PRODUCT_TO_CONCEPT);
1585
+ async function lookupGame(id, options = {}) {
1586
+ const perf = new PerfTracer("lookup");
1587
+ const locale = normalizeLocale(options.locale);
1588
+ const idType = detectIdType(id);
1589
+ const warnings = [];
1590
+ const warn = (source, err) => {
1591
+ const message = err instanceof Error ? err.message : String(err);
1592
+ warnings.push({ source, message: message.slice(0, 200) });
1593
+ return null;
1594
+ };
1595
+ const titleLocales = normalizeTitleLocales(options.titleLocales);
1596
+ const requestPlan = resolveLookupRequestPlan(options);
1597
+ const cacheKey = [
1598
+ id,
1599
+ locale,
1600
+ requestPlan.profile,
1601
+ requestPlan.includeTelemetry,
1602
+ requestPlan.includePrice,
1603
+ requestPlan.includeRating,
1604
+ requestPlan.includeUpsell,
1605
+ requestPlan.includeProducts,
1606
+ requestPlan.includeConcept,
1607
+ requestPlan.includeBatarang,
1608
+ titleLocales.join(",")
1609
+ ].join(":");
1610
+ const cached = lookupCache.get(cacheKey);
1611
+ if (cached) {
1612
+ return { result: cached, fromCache: true };
1613
+ }
1614
+ const useProductApi = idType === "product";
1615
+ const queryId = id;
1616
+ const [telemetry, price, rating, upsell, batarang] = await Promise.all([
1617
+ requestPlan.includeTelemetry ? perf.span("telemetry", () => useProductApi ? fetchTelemetryByProduct(queryId, locale) : fetchTelemetry(queryId, locale)).catch((err) => warn("telemetry", err)) : Promise.resolve(null),
1618
+ requestPlan.includePrice ? perf.span("price", () => useProductApi ? fetchPriceByProduct(queryId, locale) : fetchPrice(queryId, locale)).catch((err) => warn("price", err)) : Promise.resolve(null),
1619
+ requestPlan.includeRating ? perf.span("rating", () => useProductApi ? fetchRatingByProduct(queryId, locale) : fetchRating(queryId, locale)).catch((err) => warn("rating", err)) : Promise.resolve(null),
1620
+ requestPlan.includeUpsell || requestPlan.includeProducts ? perf.span("upsell", () => useProductApi ? fetchUpsellByProduct(queryId, locale) : fetchUpsell(queryId, locale)).catch((err) => warn("upsell", err)) : Promise.resolve(null),
1621
+ requestPlan.includeBatarang ? perf.span("batarang", async () => {
1622
+ return useProductApi ? fetchBatarangByProduct(queryId, locale) : fetchBatarang(queryId, locale);
1623
+ }).catch((err) => warn("batarang", err)) : Promise.resolve(null)
1624
+ ]);
1625
+ const conceptId = telemetry?.conceptId || price?.conceptId || rating?.conceptId || upsell?.conceptId || (useProductApi ? conceptCache.get(id) : id);
1626
+ if (useProductApi && conceptId) {
1627
+ conceptCache.set(id, conceptId);
1628
+ }
1629
+ const concept = useProductApi && requestPlan.includeConcept && conceptId ? await buildConceptSupplement(conceptId, locale, warn) : null;
1630
+ const result = buildLookupRecord({
1631
+ inputId: id,
1632
+ idType,
1633
+ telemetry,
1634
+ price,
1635
+ rating,
1636
+ upsell,
1637
+ batarang,
1638
+ concept,
1639
+ locale,
1640
+ timing: perf.getTiming(),
1641
+ includeProducts: requestPlan.includeProducts,
1642
+ warnings
1643
+ });
1644
+ if (titleLocales.length > 0) {
1645
+ result.localizedTitles = await fetchLocalizedTitles(queryId, useProductApi, titleLocales, warn);
1646
+ }
1647
+ lookupCache.set(cacheKey, result);
1648
+ return { result, fromCache: false };
1649
+ }
1650
+ function resolveLookupRequestPlan(options) {
1651
+ const profile = options.profile ?? "default";
1652
+ const profileDefaults = profile === "title-maintenance" ? {
1653
+ includeTelemetry: true,
1654
+ includePrice: false,
1655
+ includeRating: false,
1656
+ includeUpsell: false,
1657
+ includeProducts: false,
1658
+ includeConcept: false,
1659
+ includeBatarang: true
1660
+ } : {
1661
+ includeTelemetry: true,
1662
+ includePrice: true,
1663
+ includeRating: true,
1664
+ includeUpsell: true,
1665
+ includeProducts: options.includeProducts ?? false,
1666
+ includeConcept: options.includeConcept ?? false,
1667
+ includeBatarang: options.includeBatarang ?? false
1668
+ };
1669
+ return {
1670
+ profile,
1671
+ includeTelemetry: options.includeTelemetry ?? profileDefaults.includeTelemetry,
1672
+ includePrice: options.includePrice ?? profileDefaults.includePrice,
1673
+ includeRating: options.includeRating ?? profileDefaults.includeRating,
1674
+ includeUpsell: options.includeUpsell ?? profileDefaults.includeUpsell,
1675
+ includeProducts: options.includeProducts ?? profileDefaults.includeProducts,
1676
+ includeConcept: options.includeConcept ?? profileDefaults.includeConcept,
1677
+ includeBatarang: options.includeBatarang ?? profileDefaults.includeBatarang
1678
+ };
1679
+ }
1680
+ function normalizeTitleLocales(locales) {
1681
+ const seen = new Set;
1682
+ const result = [];
1683
+ for (const locale of locales ?? []) {
1684
+ const normalized = normalizeLocale(locale);
1685
+ if (!seen.has(normalized)) {
1686
+ seen.add(normalized);
1687
+ result.push(normalized);
1688
+ }
1689
+ }
1690
+ return result;
1691
+ }
1692
+ async function fetchLocalizedTitles(id, useProductApi, locales, warn) {
1693
+ const entries = await Promise.all(locales.map(async (locale) => {
1694
+ const telemetry = await (useProductApi ? fetchTelemetryByProduct(id, locale) : fetchTelemetry(id, locale)).catch((err) => warn("telemetry", err));
1695
+ const title = telemetry?.name ?? telemetry?.productName;
1696
+ return [locale, title];
1697
+ }));
1698
+ const titles = {};
1699
+ for (const [locale, title] of entries) {
1700
+ if (title)
1701
+ titles[locale] = title;
1702
+ }
1703
+ return titles;
1704
+ }
1705
+ async function buildConceptSupplement(conceptId, locale, warn) {
1706
+ const [telemetry, price, rating, upsell] = await Promise.all([
1707
+ fetchTelemetry(conceptId, locale).catch((err) => warn("telemetry", err)),
1708
+ fetchPrice(conceptId, locale).catch((err) => warn("price", err)),
1709
+ fetchRating(conceptId, locale).catch((err) => warn("rating", err)),
1710
+ fetchUpsell(conceptId, locale).catch((err) => warn("upsell", err))
1711
+ ]);
1712
+ return buildLookupRecord({
1713
+ inputId: conceptId,
1714
+ idType: "concept",
1715
+ telemetry,
1716
+ price,
1717
+ rating,
1718
+ upsell,
1719
+ locale,
1720
+ includeProducts: true
1721
+ });
1722
+ }
1723
+ function clearCaches() {
1724
+ lookupCache.clear();
1725
+ conceptCache.clear();
1726
+ clearSearchCache();
1727
+ clearCategoryCache();
1728
+ }
1729
+
1730
+ // src/client.ts
1731
+ var defaultDeps = {
1732
+ lookupGame,
1733
+ searchGames,
1734
+ fetchBrowseGrid,
1735
+ fetchCategoryGrid,
1736
+ fetchCategoryGridResult,
1737
+ fetchPsnStatus
1738
+ };
1739
+ function createPstoreClient(options = {}) {
1740
+ return createPstoreClientWithDeps(options, defaultDeps);
1741
+ }
1742
+ function createPstoreClientWithDeps(options = {}, deps) {
1743
+ const defaultLocale = normalizeLocale(options.locale ?? DEFAULT_LOCALE);
1744
+ return {
1745
+ locale: defaultLocale,
1746
+ lookup: (id, lookupOptions = {}) => deps.lookupGame(id, {
1747
+ ...lookupOptions,
1748
+ locale: normalizeLocale(lookupOptions.locale ?? defaultLocale)
1749
+ }),
1750
+ search: (searchOptions) => deps.searchGames({
1751
+ ...searchOptions,
1752
+ locale: normalizeLocale(searchOptions.locale ?? defaultLocale)
1753
+ }),
1754
+ browse: (query = {}) => deps.fetchBrowseGrid({
1755
+ ...query,
1756
+ locale: normalizeLocale(query.locale ?? defaultLocale)
1757
+ }),
1758
+ category: (params) => deps.fetchCategoryGrid({
1759
+ ...params,
1760
+ id: resolveCatalogAlias(params.id)?.id ?? params.id,
1761
+ locale: normalizeLocale(params.locale ?? defaultLocale)
1762
+ }),
1763
+ categoryGrid: (params) => deps.fetchCategoryGridResult({
1764
+ ...params,
1765
+ id: resolveCatalogAlias(params.id)?.id ?? params.id,
1766
+ locale: normalizeLocale(params.locale ?? defaultLocale)
1767
+ }),
1768
+ status: (statusOptions = {}) => deps.fetchPsnStatus({
1769
+ ...statusOptions,
1770
+ locale: normalizeLocale(statusOptions.locale ?? defaultLocale)
1771
+ })
1772
+ };
1773
+ }
1774
+ // src/api/catalog-validation.ts
1775
+ async function validateCatalog(entry, options = {}) {
1776
+ const locale = normalizeLocale(options.locale);
1777
+ try {
1778
+ const result = await fetchCategoryGridResult({
1779
+ id: entry.id,
1780
+ offset: 0,
1781
+ size: options.size ?? 1,
1782
+ locale
1783
+ });
1784
+ return {
1785
+ alias: entry.alias,
1786
+ id: entry.id,
1787
+ locale,
1788
+ ok: Boolean(result),
1789
+ totalCount: result?.pageInfo?.totalCount,
1790
+ itemCount: result?.items.length,
1791
+ localizedName: result?.localizedName,
1792
+ displayName: result?.displayName,
1793
+ reportingName: result?.reportingName,
1794
+ error: result ? undefined : "empty response"
1795
+ };
1796
+ } catch (error) {
1797
+ return {
1798
+ alias: entry.alias,
1799
+ id: entry.id,
1800
+ locale,
1801
+ ok: false,
1802
+ error: error.message
1803
+ };
1804
+ }
1805
+ }
1806
+ async function validateCatalogs(options = {}) {
1807
+ const entries = options.entries ?? BUILTIN_CATALOGS;
1808
+ return Promise.all(entries.map((entry) => validateCatalog(entry, options)));
1809
+ }
1810
+ // src/api/catalog-discovery.ts
1811
+ var PAGE_ALIAS_RE = /href="[^"]*\/pages\/([a-z0-9-]+)"/gi;
1812
+ var CATEGORY_HREF_RE = /href="[^"]*\/category\/([0-9a-f-]{36})(?:\/[0-9]+)?"/gi;
1813
+ var CATEGORY_EMS_RE = /(?:&quot;|")(?:emsCategoryId|interactLink)(?:&quot;|"):(?:&quot;|")?(?:EMS_CATEGORY:)?([0-9a-f-]{36})(?:&quot;|")?/gi;
1814
+ var CATEGORY_APOLLO_KEY_RE = /(?:CategoryGrid|CategoryStrand):([0-9a-f-]{36})(?=[:"])/gi;
1815
+ var CATEGORY_LINK_TARGET_RE = /(?:&quot;|")target(?:&quot;|"):(?:&quot;|")([0-9a-f-]{36})(?:&quot;|")[\s\S]{0,160}?(?:&quot;|")type(?:&quot;|"):(?:&quot;|")EMS_CATEGORY(?:&quot;|")/gi;
1816
+ var CATEGORY_LINK_TYPE_RE = /(?:&quot;|")type(?:&quot;|"):(?:&quot;|")EMS_CATEGORY(?:&quot;|")[\s\S]{0,160}?(?:&quot;|")target(?:&quot;|"):(?:&quot;|")([0-9a-f-]{36})(?:&quot;|")/gi;
1817
+ var CATEGORY_TITLE_RE = /id="title-([0-9a-f-]{36})"[^>]*>([\s\S]*?)<\/h2>/gi;
1818
+ var PAGE_TITLE_RE = /<title>([\s\S]*?)<\/title>/i;
1819
+ var PAGE_H1_RE = /<h1[^>]*class="[^"]*psw-sr-only[^"]*"[^>]*>([\s\S]*?)<\/h1>/i;
1820
+ var GRID_TITLE_RE = /data-qa="ems-sdk-grid-title"[^>]*>([\s\S]*?)<\/h2>/i;
1821
+ async function discoverCatalogs(opts = {}) {
1822
+ const locale = opts.locale;
1823
+ const localePath = localeToStorePath(locale);
1824
+ const maxDepth = opts.maxDepth ?? 2;
1825
+ const concurrency = Math.max(1, opts.concurrency ?? 4);
1826
+ const maxPages = Math.max(1, opts.maxPages ?? 25);
1827
+ const maxCatalogs = Math.max(1, opts.maxCatalogs ?? 300);
1828
+ const pages = [];
1829
+ const catalogs = [];
1830
+ const catalogsById = new Map;
1831
+ const seenPages = new Set;
1832
+ const seenCatalogs = new Set;
1833
+ const queue = [];
1834
+ const enqueuePage = (alias, url, depth, sourcePageAlias) => {
1835
+ if (seenPages.has(url))
1836
+ return;
1837
+ seenPages.add(url);
1838
+ queue.push({
1839
+ kind: "page",
1840
+ alias,
1841
+ url,
1842
+ depth,
1843
+ sourcePageAlias
1844
+ });
1845
+ };
1846
+ const enqueueCategory = (id, url, depth, sourcePageAlias) => {
1847
+ if (seenCatalogs.has(id))
1848
+ return false;
1849
+ seenCatalogs.add(id);
1850
+ queue.push({
1851
+ kind: "category",
1852
+ id,
1853
+ url,
1854
+ depth,
1855
+ sourcePageAlias
1856
+ });
1857
+ return true;
1858
+ };
1859
+ enqueuePage("latest", `https://store.playstation.com/${localePath}/pages/latest`, 0);
1860
+ while (queue.length > 0 && (pages.length < maxPages || catalogs.length < maxCatalogs)) {
1861
+ const batch = queue.splice(0, concurrency);
1862
+ const results = await Promise.all(batch.map(async (item) => {
1863
+ try {
1864
+ const html = await fetchHtml(item.url);
1865
+ return { item, html };
1866
+ } catch {
1867
+ return { item, html: null };
1868
+ }
1869
+ }));
1870
+ for (const { item, html } of results) {
1871
+ if (!html)
1872
+ continue;
1873
+ if (item.kind === "page") {
1874
+ if (pages.length < maxPages) {
1875
+ pages.push({
1876
+ alias: item.alias ?? item.url,
1877
+ url: item.url,
1878
+ depth: item.depth,
1879
+ title: extractPageTitle(html)
1880
+ });
1881
+ }
1882
+ }
1883
+ const categoryTitles = extractCategoryTitles(html);
1884
+ const categoryMetadata = extractCategoryMetadata(html, locale);
1885
+ if (item.kind === "category" && item.id) {
1886
+ const title = extractPageTitle(html);
1887
+ if (title) {
1888
+ const existing = categoryMetadata.get(item.id) ?? { id: item.id };
1889
+ existing.displayName = title;
1890
+ categoryMetadata.set(item.id, existing);
1891
+ }
1892
+ }
1893
+ for (const metadata of categoryMetadata.values()) {
1894
+ const catalog2 = catalogsById.get(metadata.id);
1895
+ if (!catalog2)
1896
+ continue;
1897
+ catalog2.localizedName ??= metadata.localizedName;
1898
+ catalog2.displayName ??= metadata.displayName;
1899
+ catalog2.reportingName ??= metadata.reportingName;
1900
+ }
1901
+ const pageAliases = extractPageAliases(html);
1902
+ const categoryIds = extractCategoryIds(html);
1903
+ for (const id of categoryIds) {
1904
+ if (catalogs.length >= maxCatalogs)
1905
+ break;
1906
+ const nextUrl = `https://store.playstation.com/${localePath}/category/${id}/1`;
1907
+ const added = enqueueCategory(id, nextUrl, item.depth + 1, item.alias ?? item.sourcePageAlias);
1908
+ if (!added)
1909
+ continue;
1910
+ const label = categoryTitles.get(id);
1911
+ const metadata = categoryMetadata.get(id);
1912
+ const catalog2 = {
1913
+ id,
1914
+ url: nextUrl,
1915
+ depth: item.depth + 1,
1916
+ label,
1917
+ displayName: label ?? metadata?.displayName,
1918
+ localizedName: metadata?.localizedName,
1919
+ reportingName: metadata?.reportingName,
1920
+ sourcePageAlias: item.alias
1921
+ };
1922
+ catalogs.push(catalog2);
1923
+ catalogsById.set(id, catalog2);
1924
+ if (catalogs.length >= maxCatalogs)
1925
+ break;
1926
+ }
1927
+ if (item.depth >= maxDepth)
1928
+ continue;
1929
+ for (const alias of pageAliases) {
1930
+ const nextUrl = `https://store.playstation.com/${localePath}/pages/${alias}`;
1931
+ enqueuePage(alias, nextUrl, item.depth + 1, item.alias);
1932
+ }
1933
+ }
1934
+ }
1935
+ catalogs.sort((a, b) => {
1936
+ const depthDiff = a.depth - b.depth;
1937
+ if (depthDiff !== 0)
1938
+ return depthDiff;
1939
+ return a.id.localeCompare(b.id);
1940
+ });
1941
+ return { pages, catalogs };
1942
+ }
1943
+ function extractPageAliases(html) {
1944
+ const aliases = new Set;
1945
+ PAGE_ALIAS_RE.lastIndex = 0;
1946
+ let match;
1947
+ while ((match = PAGE_ALIAS_RE.exec(html)) !== null) {
1948
+ const alias = match[1];
1949
+ if (alias)
1950
+ aliases.add(alias);
1951
+ }
1952
+ return [...aliases];
1953
+ }
1954
+ function extractCategoryIds(html) {
1955
+ const ids = new Set;
1956
+ CATEGORY_HREF_RE.lastIndex = 0;
1957
+ CATEGORY_EMS_RE.lastIndex = 0;
1958
+ CATEGORY_APOLLO_KEY_RE.lastIndex = 0;
1959
+ CATEGORY_LINK_TARGET_RE.lastIndex = 0;
1960
+ CATEGORY_LINK_TYPE_RE.lastIndex = 0;
1961
+ let match;
1962
+ while ((match = CATEGORY_HREF_RE.exec(html)) !== null) {
1963
+ const id = match[1];
1964
+ if (id)
1965
+ ids.add(id);
1966
+ }
1967
+ while ((match = CATEGORY_EMS_RE.exec(html)) !== null) {
1968
+ const id = match[1];
1969
+ if (id)
1970
+ ids.add(id);
1971
+ }
1972
+ while ((match = CATEGORY_APOLLO_KEY_RE.exec(html)) !== null) {
1973
+ const id = match[1];
1974
+ if (id)
1975
+ ids.add(id);
1976
+ }
1977
+ while ((match = CATEGORY_LINK_TARGET_RE.exec(html)) !== null) {
1978
+ const id = match[1];
1979
+ if (id)
1980
+ ids.add(id);
1981
+ }
1982
+ while ((match = CATEGORY_LINK_TYPE_RE.exec(html)) !== null) {
1983
+ const id = match[1];
1984
+ if (id)
1985
+ ids.add(id);
1986
+ }
1987
+ return [...ids];
1988
+ }
1989
+ function extractCategoryTitles(html) {
1990
+ const titles = new Map;
1991
+ CATEGORY_TITLE_RE.lastIndex = 0;
1992
+ let match;
1993
+ while ((match = CATEGORY_TITLE_RE.exec(html)) !== null) {
1994
+ const id = match[1];
1995
+ const raw = match[2];
1996
+ if (!id || !raw)
1997
+ continue;
1998
+ const title = decodeText(raw);
1999
+ if (title)
2000
+ titles.set(id, title);
2001
+ }
2002
+ return titles;
2003
+ }
2004
+ function extractCategoryMetadata(html, locale) {
2005
+ const metadata = new Map;
2006
+ const apolloState = extractApolloState(html);
2007
+ if (!apolloState)
2008
+ return metadata;
2009
+ for (const [key, value] of Object.entries(apolloState)) {
2010
+ if (!key.startsWith("CategoryGrid:") || !isRecord(value))
2011
+ continue;
2012
+ const id = key.split(":")[1];
2013
+ if (!id)
2014
+ continue;
2015
+ const localizedName = stringValue(value.localizedName);
2016
+ const reportingName = stringValue(value.reportingName);
2017
+ metadata.set(id, {
2018
+ id,
2019
+ localizedName,
2020
+ reportingName,
2021
+ displayName: resolveCategoryDisplayName(localizedName, locale)
2022
+ });
2023
+ }
2024
+ return metadata;
2025
+ }
2026
+ function extractPageTitle(html) {
2027
+ PAGE_H1_RE.lastIndex = 0;
2028
+ const h1 = PAGE_H1_RE.exec(html)?.[1];
2029
+ if (h1) {
2030
+ const title = decodeText(h1);
2031
+ if (title)
2032
+ return title;
2033
+ }
2034
+ GRID_TITLE_RE.lastIndex = 0;
2035
+ const gridTitle = GRID_TITLE_RE.exec(html)?.[1];
2036
+ if (gridTitle) {
2037
+ const title = decodeText(gridTitle);
2038
+ if (title)
2039
+ return title;
2040
+ }
2041
+ PAGE_TITLE_RE.lastIndex = 0;
2042
+ const pageTitle = PAGE_TITLE_RE.exec(html)?.[1];
2043
+ if (pageTitle) {
2044
+ const title = decodeText(pageTitle).split("|")[0]?.trim();
2045
+ if (title)
2046
+ return title;
2047
+ }
2048
+ return;
2049
+ }
2050
+ function decodeText(html) {
2051
+ return stripHtml(htmlEntityDecode(html)).replace(/\s+/g, " ").trim();
2052
+ }
2053
+ function extractApolloState(html) {
2054
+ const match = /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/i.exec(html);
2055
+ if (!match?.[1])
2056
+ return null;
2057
+ try {
2058
+ const data = JSON.parse(match[1]);
2059
+ const props = data.props;
2060
+ if (!isRecord(props))
2061
+ return null;
2062
+ const apolloState = props.apolloState;
2063
+ return isRecord(apolloState) ? apolloState : null;
2064
+ } catch {
2065
+ return null;
2066
+ }
2067
+ }
2068
+ function isRecord(value) {
2069
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2070
+ }
2071
+ function stringValue(value) {
2072
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2073
+ }
2074
+ export {
2075
+ validateCatalogs,
2076
+ validateCatalog,
2077
+ searchGames,
2078
+ sampleItems,
2079
+ resolveCategoryDisplayName,
2080
+ resolveCatalogVisibility,
2081
+ resolveCatalogDisplayName,
2082
+ resolveCatalogAlias,
2083
+ resolveBrowseSortDefaults,
2084
+ resolveBrowseQuery,
2085
+ regionForCountry,
2086
+ parsePsnStatus,
2087
+ parseLocaleList,
2088
+ normalizeLocale,
2089
+ normalizeBrowseWindow,
2090
+ lookupGame,
2091
+ localeToStorePath,
2092
+ localeToSearchParams,
2093
+ isPsnLocale,
2094
+ getLocaleInfo,
2095
+ fetchPsnStatus,
2096
+ fetchCategoryGridResult,
2097
+ fetchCategoryGrid,
2098
+ fetchBrowseGrid,
2099
+ discoverCatalogs,
2100
+ createPstoreClient,
2101
+ clearCaches as clearPstoreCaches,
2102
+ TITLE_LOCALE_PRESETS,
2103
+ LOCALE_MAP,
2104
+ DEFAULT_LOCALE,
2105
+ CATALOG_ALIASES,
2106
+ BUILTIN_CATALOGS
2107
+ };