howlongtobeat-core 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/LICENSE +21 -0
- package/README.md +157 -0
- package/esm/mod.d.ts +30 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +31 -0
- package/esm/package.json +3 -0
- package/esm/src/HowLongToBeat.d.ts +63 -0
- package/esm/src/HowLongToBeat.d.ts.map +1 -0
- package/esm/src/HowLongToBeat.js +142 -0
- package/esm/src/http/client.d.ts +29 -0
- package/esm/src/http/client.d.ts.map +1 -0
- package/esm/src/http/client.js +250 -0
- package/esm/src/parser/json.d.ts +17 -0
- package/esm/src/parser/json.d.ts.map +1 -0
- package/esm/src/parser/json.js +113 -0
- package/esm/src/types.d.ts +179 -0
- package/esm/src/types.d.ts.map +1 -0
- package/esm/src/types.js +19 -0
- package/esm/src/utils/similarity.d.ts +28 -0
- package/esm/src/utils/similarity.d.ts.map +1 -0
- package/esm/src/utils/similarity.js +127 -0
- package/package.json +38 -0
- package/script/mod.d.ts +30 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +39 -0
- package/script/package.json +3 -0
- package/script/src/HowLongToBeat.d.ts +63 -0
- package/script/src/HowLongToBeat.d.ts.map +1 -0
- package/script/src/HowLongToBeat.js +146 -0
- package/script/src/http/client.d.ts +29 -0
- package/script/src/http/client.d.ts.map +1 -0
- package/script/src/http/client.js +258 -0
- package/script/src/parser/json.d.ts +17 -0
- package/script/src/parser/json.d.ts.map +1 -0
- package/script/src/parser/json.js +118 -0
- package/script/src/types.d.ts +179 -0
- package/script/src/types.d.ts.map +1 -0
- package/script/src/types.js +22 -0
- package/script/src/utils/similarity.d.ts +28 -0
- package/script/src/utils/similarity.d.ts.map +1 -0
- package/script/src/utils/similarity.js +133 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for HowLongToBeat API
|
|
3
|
+
*/
|
|
4
|
+
const HLTB_BASE_URL = "https://howlongtobeat.com";
|
|
5
|
+
const HLTB_SEARCH_URL = `${HLTB_BASE_URL}/api/search`;
|
|
6
|
+
const HLTB_GAME_URL = `${HLTB_BASE_URL}/game`;
|
|
7
|
+
/**
|
|
8
|
+
* List of User-Agent strings for rotation
|
|
9
|
+
*/
|
|
10
|
+
const USER_AGENTS = [
|
|
11
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
12
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
13
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|
14
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
|
|
15
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0",
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Get a random User-Agent string
|
|
20
|
+
*/
|
|
21
|
+
function getRandomUserAgent() {
|
|
22
|
+
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Cached auth token and its expiry
|
|
26
|
+
*/
|
|
27
|
+
let cachedToken = null;
|
|
28
|
+
let tokenExpiry = 0;
|
|
29
|
+
const TOKEN_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
30
|
+
/**
|
|
31
|
+
* Cached search API URL
|
|
32
|
+
*/
|
|
33
|
+
let cachedSearchUrl = null;
|
|
34
|
+
/**
|
|
35
|
+
* Fetch auth token from HLTB
|
|
36
|
+
*/
|
|
37
|
+
async function fetchAuthToken() {
|
|
38
|
+
// Check cache first
|
|
39
|
+
if (cachedToken && Date.now() < tokenExpiry) {
|
|
40
|
+
return cachedToken;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const timestamp = Date.now();
|
|
44
|
+
const response = await fetch(`${HLTB_BASE_URL}/api/search/init?t=${timestamp}`, {
|
|
45
|
+
headers: {
|
|
46
|
+
"User-Agent": getRandomUserAgent(),
|
|
47
|
+
"Referer": HLTB_BASE_URL,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
// TODO: implement proper error handling
|
|
52
|
+
console.error(`Failed to fetch auth token: ${response.status}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const data = (await response.json());
|
|
56
|
+
cachedToken = data.token || null;
|
|
57
|
+
tokenExpiry = Date.now() + TOKEN_TTL_MS;
|
|
58
|
+
return cachedToken;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// TODO: implement proper error handling
|
|
62
|
+
console.error("Error fetching auth token:", error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Discover the dynamic search API URL from HLTB's JavaScript bundles
|
|
68
|
+
*/
|
|
69
|
+
async function discoverSearchUrl() {
|
|
70
|
+
if (cachedSearchUrl) {
|
|
71
|
+
return cachedSearchUrl;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// Fetch the main page
|
|
75
|
+
const response = await fetch(HLTB_BASE_URL, {
|
|
76
|
+
headers: {
|
|
77
|
+
"User-Agent": getRandomUserAgent(),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
// TODO: implement proper error handling
|
|
82
|
+
console.error(`Failed to fetch main page: ${response.status}`);
|
|
83
|
+
return HLTB_SEARCH_URL;
|
|
84
|
+
}
|
|
85
|
+
const html = await response.text();
|
|
86
|
+
// Find _app-*.js bundle URLs
|
|
87
|
+
const scriptPattern = /_app-[a-zA-Z0-9]+\.js/g;
|
|
88
|
+
const scripts = html.match(scriptPattern);
|
|
89
|
+
if (!scripts || scripts.length === 0) {
|
|
90
|
+
return HLTB_SEARCH_URL;
|
|
91
|
+
}
|
|
92
|
+
// Try to find the search endpoint in the scripts
|
|
93
|
+
for (const script of scripts) {
|
|
94
|
+
try {
|
|
95
|
+
const scriptUrl = `${HLTB_BASE_URL}/_next/static/chunks/pages/${script}`;
|
|
96
|
+
const scriptResponse = await fetch(scriptUrl, {
|
|
97
|
+
headers: {
|
|
98
|
+
"User-Agent": getRandomUserAgent(),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (!scriptResponse.ok)
|
|
102
|
+
continue;
|
|
103
|
+
const scriptContent = await scriptResponse.text();
|
|
104
|
+
// Look for the search API path pattern
|
|
105
|
+
// Common patterns: "/api/search/", "api/search", etc.
|
|
106
|
+
const searchPathPattern = /["']([^"']*api\/search[^"']*)["']/g;
|
|
107
|
+
const matches = [...scriptContent.matchAll(searchPathPattern)];
|
|
108
|
+
for (const match of matches) {
|
|
109
|
+
const path = match[1];
|
|
110
|
+
if (path && !path.includes("init") && path.startsWith("/")) {
|
|
111
|
+
cachedSearchUrl = `${HLTB_BASE_URL}${path}`;
|
|
112
|
+
return cachedSearchUrl;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// TODO: implement proper error handling
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// TODO: implement proper error handling
|
|
124
|
+
console.error("Error discovering search URL:", error);
|
|
125
|
+
}
|
|
126
|
+
// Fall back to known URL
|
|
127
|
+
cachedSearchUrl = HLTB_SEARCH_URL;
|
|
128
|
+
return cachedSearchUrl;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build search request body
|
|
132
|
+
*/
|
|
133
|
+
export function buildSearchRequest(searchTerms, modifier = "", page = 1, size = 20) {
|
|
134
|
+
return {
|
|
135
|
+
searchType: "games",
|
|
136
|
+
searchTerms,
|
|
137
|
+
searchPage: page,
|
|
138
|
+
size,
|
|
139
|
+
searchOptions: {
|
|
140
|
+
games: {
|
|
141
|
+
userId: 0,
|
|
142
|
+
platform: "",
|
|
143
|
+
sortCategory: "popular",
|
|
144
|
+
rangeCategory: "main",
|
|
145
|
+
rangeTime: { min: 0, max: 0 },
|
|
146
|
+
gameplay: {
|
|
147
|
+
perspective: "",
|
|
148
|
+
flow: "",
|
|
149
|
+
genre: "",
|
|
150
|
+
difficulty: "",
|
|
151
|
+
},
|
|
152
|
+
rangeYear: { max: "", min: "" },
|
|
153
|
+
modifier,
|
|
154
|
+
},
|
|
155
|
+
users: { sortCategory: "postcount" },
|
|
156
|
+
lists: { sortCategory: "follows" },
|
|
157
|
+
filter: "",
|
|
158
|
+
sort: 0,
|
|
159
|
+
randomizer: 0,
|
|
160
|
+
},
|
|
161
|
+
useCache: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Execute a search request to HLTB API
|
|
166
|
+
*/
|
|
167
|
+
export async function executeSearch(gameName, modifier = "") {
|
|
168
|
+
const token = await fetchAuthToken();
|
|
169
|
+
if (!token) {
|
|
170
|
+
// TODO: implement proper error handling
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const searchUrl = await discoverSearchUrl();
|
|
174
|
+
const searchTerms = gameName.split(" ").filter((term) => term.length > 0);
|
|
175
|
+
const requestBody = buildSearchRequest(searchTerms, modifier);
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(searchUrl, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: {
|
|
180
|
+
"Content-Type": "application/json",
|
|
181
|
+
"User-Agent": getRandomUserAgent(),
|
|
182
|
+
"Referer": HLTB_BASE_URL,
|
|
183
|
+
"x-auth-token": token,
|
|
184
|
+
},
|
|
185
|
+
body: JSON.stringify(requestBody),
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
// TODO: implement proper error handling
|
|
189
|
+
console.error(`Search request failed: ${response.status}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return await response.json();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// TODO: implement proper error handling
|
|
196
|
+
console.error("Error executing search:", error);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Fetch game page HTML to extract game title
|
|
202
|
+
*/
|
|
203
|
+
export async function fetchGamePage(gameId) {
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(`${HLTB_GAME_URL}/${gameId}`, {
|
|
206
|
+
headers: {
|
|
207
|
+
"User-Agent": getRandomUserAgent(),
|
|
208
|
+
"Referer": HLTB_BASE_URL,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
// TODO: implement proper error handling
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return await response.text();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
// TODO: implement proper error handling
|
|
219
|
+
console.error("Error fetching game page:", error);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Extract game title from HLTB game page HTML
|
|
225
|
+
*/
|
|
226
|
+
export function extractGameTitle(html) {
|
|
227
|
+
// Title format: "How long is {GAME_NAME}? | HowLongToBeat"
|
|
228
|
+
// Note: title tag may have attributes like data-next-head=""
|
|
229
|
+
const titlePattern = /<title[^>]*>How long is ([^?]+)\?[^<]*<\/title>/i;
|
|
230
|
+
const match = html.match(titlePattern);
|
|
231
|
+
if (match && match[1]) {
|
|
232
|
+
return match[1].trim();
|
|
233
|
+
}
|
|
234
|
+
// TODO: implement proper error handling
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get base URL for constructing image and web links
|
|
239
|
+
*/
|
|
240
|
+
export function getBaseUrl() {
|
|
241
|
+
return HLTB_BASE_URL;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Clear cached token and search URL (useful for testing)
|
|
245
|
+
*/
|
|
246
|
+
export function clearCache() {
|
|
247
|
+
cachedToken = null;
|
|
248
|
+
tokenExpiry = 0;
|
|
249
|
+
cachedSearchUrl = null;
|
|
250
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON response parser for HowLongToBeat API
|
|
3
|
+
*/
|
|
4
|
+
import type { HLTBRawGame, HowLongToBeatEntry } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Convert a raw HLTB game object to HowLongToBeatEntry
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseGameEntry(raw: HLTBRawGame, similarity?: number, autoFilterTimes?: boolean): HowLongToBeatEntry;
|
|
9
|
+
/**
|
|
10
|
+
* Parse an array of raw games into HowLongToBeatEntry array
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseGameEntries(rawGames: HLTBRawGame[], searchQuery: string, autoFilterTimes: boolean, calculateSimilarity: (a: string, b: string) => number): HowLongToBeatEntry[];
|
|
13
|
+
/**
|
|
14
|
+
* Filter entries by minimum similarity threshold
|
|
15
|
+
*/
|
|
16
|
+
export declare function filterBySimilarity(entries: HowLongToBeatEntry[], minimumSimilarity: number): HowLongToBeatEntry[];
|
|
17
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../../src/src/parser/json.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA2BnE;;GAEG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,UAAU,GAAE,MAAU,EACtB,eAAe,GAAE,OAAe,GAC/B,kBAAkB,CAyEpB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,WAAW,EAAE,EACvB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,OAAO,EACxB,mBAAmB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GACpD,kBAAkB,EAAE,CAgBtB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAAE,EAC7B,iBAAiB,EAAE,MAAM,GACxB,kBAAkB,EAAE,CAItB"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON response parser for HowLongToBeat API
|
|
3
|
+
*/
|
|
4
|
+
import { getBaseUrl } from "../http/client.js";
|
|
5
|
+
const HLTB_IMAGE_URL = "https://howlongtobeat.com/games";
|
|
6
|
+
/**
|
|
7
|
+
* Convert seconds to hours with one decimal place
|
|
8
|
+
*/
|
|
9
|
+
function secondsToHours(seconds) {
|
|
10
|
+
if (seconds <= 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return Math.round((seconds / 3600) * 10) / 10;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse platforms string into array
|
|
17
|
+
*/
|
|
18
|
+
function parsePlatforms(platformString) {
|
|
19
|
+
if (!platformString || platformString.trim() === "") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return platformString.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert a raw HLTB game object to HowLongToBeatEntry
|
|
26
|
+
*/
|
|
27
|
+
export function parseGameEntry(raw, similarity = 0, autoFilterTimes = false) {
|
|
28
|
+
const baseUrl = getBaseUrl();
|
|
29
|
+
// Determine game type flags
|
|
30
|
+
const complexityLvlCombine = raw.comp_lvl_combine === 1;
|
|
31
|
+
const complexityLvlSp = raw.comp_lvl_sp === 1;
|
|
32
|
+
const complexityLvlCo = raw.comp_lvl_co === 1;
|
|
33
|
+
const complexityLvlMp = raw.comp_lvl_mp === 1;
|
|
34
|
+
// Parse times
|
|
35
|
+
let mainStory = secondsToHours(raw.comp_main);
|
|
36
|
+
let mainExtra = secondsToHours(raw.comp_plus);
|
|
37
|
+
let completionist = secondsToHours(raw.comp_100);
|
|
38
|
+
let allStyles = secondsToHours(raw.comp_all);
|
|
39
|
+
let coopTime = secondsToHours(raw.invested_co);
|
|
40
|
+
let mpTime = secondsToHours(raw.invested_mp);
|
|
41
|
+
// Auto-filter times based on game type
|
|
42
|
+
if (autoFilterTimes) {
|
|
43
|
+
// If no singleplayer, nullify SP times
|
|
44
|
+
if (!complexityLvlSp && !complexityLvlCombine) {
|
|
45
|
+
mainStory = null;
|
|
46
|
+
mainExtra = null;
|
|
47
|
+
completionist = null;
|
|
48
|
+
allStyles = null;
|
|
49
|
+
}
|
|
50
|
+
// If no co-op, nullify co-op time
|
|
51
|
+
if (!complexityLvlCo && !complexityLvlCombine) {
|
|
52
|
+
coopTime = null;
|
|
53
|
+
}
|
|
54
|
+
// If no multiplayer, nullify MP time
|
|
55
|
+
if (!complexityLvlMp && !complexityLvlCombine) {
|
|
56
|
+
mpTime = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Build image URL
|
|
60
|
+
let gameImageUrl = null;
|
|
61
|
+
if (raw.game_image) {
|
|
62
|
+
gameImageUrl = `${HLTB_IMAGE_URL}/${raw.game_image}`;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
gameId: raw.game_id,
|
|
66
|
+
gameName: raw.game_name || null,
|
|
67
|
+
gameAlias: raw.game_alias || null,
|
|
68
|
+
gameType: raw.game_type || null,
|
|
69
|
+
gameImageUrl,
|
|
70
|
+
gameWebLink: `${baseUrl}/game/${raw.game_id}`,
|
|
71
|
+
reviewScore: raw.review_score > 0 ? raw.review_score : null,
|
|
72
|
+
profileDev: raw.profile_dev || null,
|
|
73
|
+
profilePlatforms: parsePlatforms(raw.profile_platform),
|
|
74
|
+
releaseWorld: raw.release_world > 0 ? raw.release_world : null,
|
|
75
|
+
mainStory,
|
|
76
|
+
mainExtra,
|
|
77
|
+
completionist,
|
|
78
|
+
allStyles,
|
|
79
|
+
coopTime,
|
|
80
|
+
mpTime,
|
|
81
|
+
complexityLvlCombine,
|
|
82
|
+
complexityLvlSp,
|
|
83
|
+
complexityLvlCo,
|
|
84
|
+
complexityLvlMp,
|
|
85
|
+
similarity,
|
|
86
|
+
jsonContent: raw,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse an array of raw games into HowLongToBeatEntry array
|
|
91
|
+
*/
|
|
92
|
+
export function parseGameEntries(rawGames, searchQuery, autoFilterTimes, calculateSimilarity) {
|
|
93
|
+
const normalizedQuery = searchQuery.toLowerCase().trim();
|
|
94
|
+
return rawGames.map((raw) => {
|
|
95
|
+
// Calculate similarity against game name and alias
|
|
96
|
+
const nameSimilarity = raw.game_name
|
|
97
|
+
? calculateSimilarity(normalizedQuery, raw.game_name.toLowerCase())
|
|
98
|
+
: 0;
|
|
99
|
+
const aliasSimilarity = raw.game_alias
|
|
100
|
+
? calculateSimilarity(normalizedQuery, raw.game_alias.toLowerCase())
|
|
101
|
+
: 0;
|
|
102
|
+
const similarity = Math.max(nameSimilarity, aliasSimilarity);
|
|
103
|
+
return parseGameEntry(raw, similarity, autoFilterTimes);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Filter entries by minimum similarity threshold
|
|
108
|
+
*/
|
|
109
|
+
export function filterBySimilarity(entries, minimumSimilarity) {
|
|
110
|
+
return entries
|
|
111
|
+
.filter((entry) => entry.similarity >= minimumSimilarity)
|
|
112
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
113
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HowLongToBeat TypeScript types
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Search modifiers for filtering game results
|
|
6
|
+
*/
|
|
7
|
+
export declare enum SearchModifiers {
|
|
8
|
+
/** No modifier - include all results */
|
|
9
|
+
NONE = "",
|
|
10
|
+
/** Only show DLC content */
|
|
11
|
+
ISOLATE_DLC = "only_dlc",
|
|
12
|
+
/** Only show mods */
|
|
13
|
+
ISOLATE_MODS = "only_mods",
|
|
14
|
+
/** Only show hacks */
|
|
15
|
+
ISOLATE_HACKS = "only_hacks",
|
|
16
|
+
/** Hide DLC from results */
|
|
17
|
+
HIDE_DLC = "hide_dlc"
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Similarity algorithm to use for filtering results
|
|
21
|
+
*/
|
|
22
|
+
export type SimilarityAlgorithm = "gestalt" | "levenshtein";
|
|
23
|
+
/**
|
|
24
|
+
* Options for HowLongToBeat constructor
|
|
25
|
+
*/
|
|
26
|
+
export interface HowLongToBeatOptions {
|
|
27
|
+
/** Minimum similarity threshold (0-1). Default: 0.4 */
|
|
28
|
+
minimumSimilarity?: number;
|
|
29
|
+
/** Auto-nullify irrelevant time fields based on game type. Default: false */
|
|
30
|
+
autoFilterTimes?: boolean;
|
|
31
|
+
/** Similarity algorithm to use. Default: "gestalt" */
|
|
32
|
+
similarityAlgorithm?: SimilarityAlgorithm;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Represents a game entry from HowLongToBeat
|
|
36
|
+
*/
|
|
37
|
+
export interface HowLongToBeatEntry {
|
|
38
|
+
/** Unique game ID on HLTB */
|
|
39
|
+
gameId: number;
|
|
40
|
+
/** Game name */
|
|
41
|
+
gameName: string | null;
|
|
42
|
+
/** Alternative name/alias */
|
|
43
|
+
gameAlias: string | null;
|
|
44
|
+
/** Type of content: "game" or "dlc" */
|
|
45
|
+
gameType: string | null;
|
|
46
|
+
/** URL to the game's cover image */
|
|
47
|
+
gameImageUrl: string | null;
|
|
48
|
+
/** Direct link to the game's HLTB page */
|
|
49
|
+
gameWebLink: string;
|
|
50
|
+
/** Review score (0-100) */
|
|
51
|
+
reviewScore: number | null;
|
|
52
|
+
/** Developer name */
|
|
53
|
+
profileDev: string | null;
|
|
54
|
+
/** Available platforms */
|
|
55
|
+
profilePlatforms: string[] | null;
|
|
56
|
+
/** Release year */
|
|
57
|
+
releaseWorld: number | null;
|
|
58
|
+
/** Main story completion time */
|
|
59
|
+
mainStory: number | null;
|
|
60
|
+
/** Main story + extras completion time */
|
|
61
|
+
mainExtra: number | null;
|
|
62
|
+
/** 100% completionist time */
|
|
63
|
+
completionist: number | null;
|
|
64
|
+
/** Average of all playstyles */
|
|
65
|
+
allStyles: number | null;
|
|
66
|
+
/** Co-op completion time */
|
|
67
|
+
coopTime: number | null;
|
|
68
|
+
/** Multiplayer time */
|
|
69
|
+
mpTime: number | null;
|
|
70
|
+
/** Combined single/multi game */
|
|
71
|
+
complexityLvlCombine: boolean;
|
|
72
|
+
/** Has singleplayer content */
|
|
73
|
+
complexityLvlSp: boolean;
|
|
74
|
+
/** Has co-op content */
|
|
75
|
+
complexityLvlCo: boolean;
|
|
76
|
+
/** Has multiplayer content */
|
|
77
|
+
complexityLvlMp: boolean;
|
|
78
|
+
/** Similarity score to search query (0-1) */
|
|
79
|
+
similarity: number;
|
|
80
|
+
/** Full raw JSON response from HLTB */
|
|
81
|
+
jsonContent: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Raw game data from HLTB API response
|
|
85
|
+
*/
|
|
86
|
+
export interface HLTBRawGame {
|
|
87
|
+
game_id: number;
|
|
88
|
+
game_name: string;
|
|
89
|
+
game_name_date: number;
|
|
90
|
+
game_alias: string;
|
|
91
|
+
game_type: string;
|
|
92
|
+
game_image: string;
|
|
93
|
+
comp_lvl_combine: number;
|
|
94
|
+
comp_lvl_sp: number;
|
|
95
|
+
comp_lvl_co: number;
|
|
96
|
+
comp_lvl_mp: number;
|
|
97
|
+
comp_lvl_spd: number;
|
|
98
|
+
comp_main: number;
|
|
99
|
+
comp_plus: number;
|
|
100
|
+
comp_100: number;
|
|
101
|
+
comp_all: number;
|
|
102
|
+
comp_main_count: number;
|
|
103
|
+
comp_plus_count: number;
|
|
104
|
+
comp_100_count: number;
|
|
105
|
+
comp_all_count: number;
|
|
106
|
+
invested_co: number;
|
|
107
|
+
invested_mp: number;
|
|
108
|
+
invested_co_count: number;
|
|
109
|
+
invested_mp_count: number;
|
|
110
|
+
count_comp: number;
|
|
111
|
+
count_speedrun: number;
|
|
112
|
+
count_backlog: number;
|
|
113
|
+
count_review: number;
|
|
114
|
+
review_score: number;
|
|
115
|
+
count_playing: number;
|
|
116
|
+
count_retired: number;
|
|
117
|
+
profile_dev: string;
|
|
118
|
+
profile_popular: number;
|
|
119
|
+
profile_steam: number;
|
|
120
|
+
profile_platform: string;
|
|
121
|
+
release_world: number;
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* HLTB API search response structure
|
|
126
|
+
*/
|
|
127
|
+
export interface HLTBSearchResponse {
|
|
128
|
+
color: string;
|
|
129
|
+
title: string;
|
|
130
|
+
category: string;
|
|
131
|
+
count: number;
|
|
132
|
+
pageCurrent: number;
|
|
133
|
+
pageTotal: number;
|
|
134
|
+
pageSize: number;
|
|
135
|
+
data: HLTBRawGame[];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Search request body for HLTB API
|
|
139
|
+
*/
|
|
140
|
+
export interface HLTBSearchRequest {
|
|
141
|
+
searchType: string;
|
|
142
|
+
searchTerms: string[];
|
|
143
|
+
searchPage: number;
|
|
144
|
+
size: number;
|
|
145
|
+
searchOptions: {
|
|
146
|
+
games: {
|
|
147
|
+
userId: number;
|
|
148
|
+
platform: string;
|
|
149
|
+
sortCategory: string;
|
|
150
|
+
rangeCategory: string;
|
|
151
|
+
rangeTime: {
|
|
152
|
+
min: number;
|
|
153
|
+
max: number;
|
|
154
|
+
};
|
|
155
|
+
gameplay: {
|
|
156
|
+
perspective: string;
|
|
157
|
+
flow: string;
|
|
158
|
+
genre: string;
|
|
159
|
+
difficulty: string;
|
|
160
|
+
};
|
|
161
|
+
rangeYear: {
|
|
162
|
+
max: string;
|
|
163
|
+
min: string;
|
|
164
|
+
};
|
|
165
|
+
modifier: string;
|
|
166
|
+
};
|
|
167
|
+
users: {
|
|
168
|
+
sortCategory: string;
|
|
169
|
+
};
|
|
170
|
+
lists: {
|
|
171
|
+
sortCategory: string;
|
|
172
|
+
};
|
|
173
|
+
filter: string;
|
|
174
|
+
sort: number;
|
|
175
|
+
randomizer: number;
|
|
176
|
+
};
|
|
177
|
+
useCache: boolean;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/src/types.ts"],"names":[],"mappings":"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"}
|
package/esm/src/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HowLongToBeat TypeScript types
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Search modifiers for filtering game results
|
|
6
|
+
*/
|
|
7
|
+
export var SearchModifiers;
|
|
8
|
+
(function (SearchModifiers) {
|
|
9
|
+
/** No modifier - include all results */
|
|
10
|
+
SearchModifiers["NONE"] = "";
|
|
11
|
+
/** Only show DLC content */
|
|
12
|
+
SearchModifiers["ISOLATE_DLC"] = "only_dlc";
|
|
13
|
+
/** Only show mods */
|
|
14
|
+
SearchModifiers["ISOLATE_MODS"] = "only_mods";
|
|
15
|
+
/** Only show hacks */
|
|
16
|
+
SearchModifiers["ISOLATE_HACKS"] = "only_hacks";
|
|
17
|
+
/** Hide DLC from results */
|
|
18
|
+
SearchModifiers["HIDE_DLC"] = "hide_dlc";
|
|
19
|
+
})(SearchModifiers || (SearchModifiers = {}));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String similarity algorithms for HowLongToBeat
|
|
3
|
+
*
|
|
4
|
+
* Provides both Gestalt pattern matching (like Python's difflib.SequenceMatcher)
|
|
5
|
+
* and Levenshtein distance-based similarity.
|
|
6
|
+
*/
|
|
7
|
+
import type { SimilarityAlgorithm } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Calculate similarity using the specified algorithm
|
|
10
|
+
*/
|
|
11
|
+
export declare function calculateSimilarity(a: string, b: string, algorithm?: SimilarityAlgorithm): number;
|
|
12
|
+
/**
|
|
13
|
+
* Gestalt Pattern Matching similarity (like Python's difflib.SequenceMatcher)
|
|
14
|
+
*
|
|
15
|
+
* This implements the Ratcliff/Obershelp algorithm which finds the longest
|
|
16
|
+
* common substring and recursively processes the remaining parts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function gestaltSimilarity(a: string, b: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Calculate similarity based on Levenshtein distance
|
|
21
|
+
* Returns a value between 0 and 1, where 1 means identical strings
|
|
22
|
+
*/
|
|
23
|
+
export declare function levenshteinSimilarity(a: string, b: string): number;
|
|
24
|
+
/**
|
|
25
|
+
* Create a similarity calculator function with a specific algorithm
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSimilarityCalculator(algorithm?: SimilarityAlgorithm): (a: string, b: string) => number;
|
|
28
|
+
//# sourceMappingURL=similarity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similarity.d.ts","sourceRoot":"","sources":["../../../src/src/utils/similarity.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,SAAS,GAAE,mBAA+B,GACzC,MAAM,CAKR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAM9D;AAuGD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAOlE;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,GAAE,mBAA+B,GACzC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAElC"}
|