Skip to content

Commit 214f6a7

Browse files
authored
[FIX] Fix How Long To Beat implementation (#5334)
* tech: remove hltb-js and patches * fix: fix HLTB by using native implementation --------- Co-authored-by: Flavio F Lima <flavioislima@users.noreply.github.com>
1 parent a4da046 commit 214f6a7

5 files changed

Lines changed: 104 additions & 71 deletions

File tree

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
"fs-extra": "^11.3.0",
7575
"fuse.js": "^6.6.2",
7676
"graceful-fs": "^4.2.11",
77-
"howlongtobeat-js": "^1.0.2",
7877
"i18next": "^22.5.1",
7978
"i18next-fs-backend": "^2.6.0",
8079
"i18next-http-backend": "^2.7.3",

patches/howlongtobeat-js@1.0.2.patch

Lines changed: 0 additions & 12 deletions
This file was deleted.

pnpm-lock.yaml

Lines changed: 1 addition & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
patchedDependencies:
22
'@types/node@22.19.3': patches/@types__node@22.19.3.patch
3-
howlongtobeat-js@1.0.2: patches/howlongtobeat-js@1.0.2.patch
43

54
onlyBuiltDependencies:
65
- '@fortawesome/fontawesome-common-types'
Lines changed: 103 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import axios from 'axios'
12
import { logError, logInfo, LogPrefix } from 'backend/logger'
2-
import { HowLongToBeat, HowLongToBeatEntry } from 'howlongtobeat-js'
33

44
// this is a subset of the HowLongToBeatEntry type without some fields
55
export interface HeroicHowLongToBeatEntry {
@@ -13,42 +13,118 @@ export interface HeroicHowLongToBeatEntry {
1313
gameWebLink?: string
1414
}
1515

16+
interface HltbGameData {
17+
game_id: number
18+
game_name: string
19+
game_image: string
20+
comp_main: number
21+
comp_plus: number
22+
comp_100: number
23+
}
24+
25+
interface HltbSearchResponse {
26+
data: HltbGameData[]
27+
}
28+
29+
const HLTB_BASE_URL = 'https://howlongtobeat.com'
30+
31+
async function getHltbToken(): Promise<string | null> {
32+
const url = `${HLTB_BASE_URL}/api/finder/init?t=${Date.now()}`
33+
try {
34+
const response = await axios.get(url, {
35+
headers: {
36+
referer: HLTB_BASE_URL,
37+
'User-Agent':
38+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
39+
}
40+
})
41+
return response.data?.token || null
42+
} catch (error) {
43+
logError(['Error fetching HLTB token:', error], LogPrefix.ExtraGameInfo)
44+
return null
45+
}
46+
}
47+
1648
export async function getHowLongToBeat(
1749
title: string
1850
): Promise<HeroicHowLongToBeatEntry | null> {
1951
logInfo(`Getting HowLongToBeat data for ${title}`, LogPrefix.ExtraGameInfo)
20-
const hltb = new HowLongToBeat(0.4)
21-
let info: HowLongToBeatEntry[] | null = null
22-
try {
23-
info = await hltb.search(title)
24-
} catch (error) {
25-
logError(
26-
[`Error searching HLTB data for ${title}:`, error],
27-
LogPrefix.ExtraGameInfo
28-
)
52+
53+
const token = await getHltbToken()
54+
if (!token) {
2955
return null
3056
}
3157

32-
if (!info || info.length === 0) {
58+
const searchUrl = `${HLTB_BASE_URL}/api/finder`
59+
const payload = {
60+
searchType: 'games',
61+
searchTerms: title.split(' '),
62+
searchPage: 1,
63+
size: 20,
64+
searchOptions: {
65+
games: {
66+
userId: 0,
67+
platform: '',
68+
sortCategory: 'popular',
69+
rangeCategory: 'main',
70+
rangeTime: { min: 0, max: 0 },
71+
gameplay: { perspective: '', flow: '', genre: '', difficulty: '' },
72+
rangeYear: { min: '', max: '' },
73+
modifier: ''
74+
},
75+
users: { sortCategory: 'postcount' },
76+
lists: { sortCategory: 'follows' },
77+
filter: '',
78+
sort: 0,
79+
randomizer: 0
80+
}
81+
}
82+
83+
try {
84+
const response = await axios.post<HltbSearchResponse>(searchUrl, payload, {
85+
headers: {
86+
'Content-Type': 'application/json',
87+
'x-auth-token': token,
88+
referer: HLTB_BASE_URL,
89+
'User-Agent':
90+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
91+
}
92+
})
93+
94+
const info = response.data?.data
95+
if (!info || info.length === 0) {
96+
logError(
97+
`No HowLongToBeat data found for ${title}`,
98+
LogPrefix.ExtraGameInfo
99+
)
100+
return null
101+
}
102+
103+
const game: HltbGameData = info[0]
104+
105+
// New API returns values in seconds, converting to hours
106+
const mainStory = game.comp_main ? Math.round(game.comp_main / 3600) : 0
107+
const mainExtra = game.comp_plus ? Math.round(game.comp_plus / 3600) : 0
108+
const completionist = game.comp_100 ? Math.round(game.comp_100 / 3600) : 0
109+
110+
return {
111+
mainStory,
112+
mainExtra,
113+
completionist,
114+
gameId: game.game_id,
115+
gameName: game.game_name || undefined,
116+
gameImageUrl: game.game_image
117+
? `${HLTB_BASE_URL}/games/${game.game_image}`
118+
: undefined,
119+
gameWebLink: game.game_id
120+
? `${HLTB_BASE_URL}/game/${game.game_id}`
121+
: undefined
122+
}
123+
} catch (error) {
33124
logError(
34-
`No HowLongToBeat data found for ${title}`,
125+
[`Error searching HLTB data for ${title}:`, error],
35126
LogPrefix.ExtraGameInfo
36127
)
37128
return null
38129
}
39-
const game = info[0]
40-
41-
const mainStory = game.mainStory ? Math.round(game.mainStory) : 0
42-
const mainExtra = game.mainExtra ? Math.round(game.mainExtra) : 0
43-
const completionist = game.completionist ? Math.round(game.completionist) : 0
44-
45-
return {
46-
mainStory,
47-
mainExtra,
48-
completionist,
49-
gameId: game.gameId,
50-
gameName: game.gameName || undefined,
51-
gameImageUrl: game.gameImageUrl || undefined,
52-
gameWebLink: game.gameWebLink || undefined
53-
}
54130
}

0 commit comments

Comments
 (0)