1+ import axios from 'axios'
12import { 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
55export 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+
1648export 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