howlongtobeat 0.2.3 → 0.2.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/howlongtobeat/html_requests.rb +22 -13
- data/lib/howlongtobeat/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e8314a8423ed470f6b0f5f80097b903ece915d2a6eabc356cdb34104dacb929
|
|
4
|
+
data.tar.gz: 7e6c9c045ee96345593fa961007988a96b78ffd4dd1f44b0f4529f79c14f2ecd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6cb621ce224ad454077852c33e4295f8bab8394c81865703c397a93f6d83d710142cd426b43f14e9d94bca24e2f9e9aafb1ab800db66709864f7a7531d29b27
|
|
7
|
+
data.tar.gz: e59d86c29009a8e2759874ef021c0ff49285d797778454ef60cb639d8d8899412ce445d5cf6180c3d6cfc756b6590b60324abffda6f305f6ca5648b61c6eb630
|
data/CHANGELOG.md
CHANGED
|
@@ -25,6 +25,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
25
25
|
### Security
|
|
26
26
|
- None
|
|
27
27
|
|
|
28
|
+
## [0.2.4] - 2026-05-07
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- HLTB renamed the search endpoint from `/api/finder` to `/api/bleed`. Runtime discovery already adapted, but the hardcoded fallbacks didn't — refresh `SEARCH_URL`, the default in `fetch_search_token`, and the candidate list to put `/api/bleed` first.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Drop the `_app-*.js` script-name filter in `send_website_request_getcode`. The modern HLTB build (Turbopack) ships chunks with opaque names like `0-~-0up.q3_p0.js`, so the filter never matched and forced every search through a redundant retry. Single-pass discovery now iterates all `<script src>` tags directly.
|
|
35
|
+
- `send_website_request_getcode` only returns when a `search_url` is found; finding only an `api_key` no longer short-circuits the loop.
|
|
36
|
+
|
|
28
37
|
## [0.2.3] - 2026-04-15
|
|
29
38
|
|
|
30
39
|
### Fixed
|
|
@@ -8,7 +8,10 @@ module HowLongToBeat
|
|
|
8
8
|
BASE_URL = 'https://howlongtobeat.com'
|
|
9
9
|
REFERER_HEADER = BASE_URL
|
|
10
10
|
GAME_URL = "#{BASE_URL}/game"
|
|
11
|
-
|
|
11
|
+
# HLTB renames this endpoint periodically (most recent: /api/find -> /api/finder -> /api/bleed).
|
|
12
|
+
# The runtime discovery in `send_website_request_getcode` is the source of truth;
|
|
13
|
+
# this constant is the fallback when discovery fails.
|
|
14
|
+
SEARCH_URL = "#{BASE_URL}/api/bleed"
|
|
12
15
|
|
|
13
16
|
class SearchModifiers
|
|
14
17
|
NONE = ""
|
|
@@ -48,7 +51,8 @@ module HowLongToBeat
|
|
|
48
51
|
|
|
49
52
|
def extract_search_url_script(script_content)
|
|
50
53
|
# Prefer the search endpoint used in POST fetch calls, which is more stable
|
|
51
|
-
# than hardcoding "/api/search" and works with variants like "/api/finder"
|
|
54
|
+
# than hardcoding "/api/search" and works with variants like "/api/finder"
|
|
55
|
+
# and "/api/bleed" (current as of 2026-05).
|
|
52
56
|
post_fetch_pattern = /fetch\s*\(\s*["']\/api\/([a-zA-Z0-9_\/-]+)[^"']*["']\s*,\s*{[^}]*method:\s*["']POST["'][^}]*}/mi
|
|
53
57
|
if (match = script_content.match(post_fetch_pattern))
|
|
54
58
|
path_suffix = match[1]
|
|
@@ -141,10 +145,7 @@ module HowLongToBeat
|
|
|
141
145
|
end
|
|
142
146
|
|
|
143
147
|
def send_web_request(game_name, search_modifiers = SearchModifiers::NONE, page = 1)
|
|
144
|
-
search_info = send_website_request_getcode
|
|
145
|
-
if search_info.nil? || search_info.search_url.nil?
|
|
146
|
-
search_info = send_website_request_getcode(true)
|
|
147
|
-
end
|
|
148
|
+
search_info = send_website_request_getcode
|
|
148
149
|
|
|
149
150
|
endpoint_candidates = build_endpoint_candidates(search_info&.search_url)
|
|
150
151
|
|
|
@@ -181,7 +182,7 @@ module HowLongToBeat
|
|
|
181
182
|
|
|
182
183
|
def fetch_search_token(parsed_search_url = nil)
|
|
183
184
|
base_endpoint = parsed_search_url.to_s.strip
|
|
184
|
-
base_endpoint = '/api/
|
|
185
|
+
base_endpoint = '/api/bleed' if base_endpoint.empty?
|
|
185
186
|
base_endpoint = "/#{base_endpoint}" unless base_endpoint.start_with?('/')
|
|
186
187
|
base_endpoint = base_endpoint.sub(%r{/+\z}, '')
|
|
187
188
|
base_endpoint = base_endpoint.sub(%r{/init\z}, '')
|
|
@@ -216,7 +217,8 @@ module HowLongToBeat
|
|
|
216
217
|
preferred = parsed_endpoint.to_s.strip
|
|
217
218
|
candidates = []
|
|
218
219
|
candidates << preferred unless preferred.empty?
|
|
219
|
-
|
|
220
|
+
# Known historical endpoints, newest first. HLTB rotates this name periodically.
|
|
221
|
+
candidates.concat(['/api/bleed', '/api/finder', '/api/search', '/api/s'])
|
|
220
222
|
|
|
221
223
|
normalized = candidates.map do |endpoint|
|
|
222
224
|
next nil if endpoint.nil? || endpoint.strip.empty?
|
|
@@ -228,7 +230,13 @@ module HowLongToBeat
|
|
|
228
230
|
normalized.compact.uniq
|
|
229
231
|
end
|
|
230
232
|
|
|
231
|
-
|
|
233
|
+
# Walks every <script src> tag on the homepage looking for one that
|
|
234
|
+
# contains a `fetch("/api/<name>", { method: "POST" })` call. HLTB used to
|
|
235
|
+
# bundle the relevant code under `_app-*.js`, but the modern (Turbopack)
|
|
236
|
+
# build emits opaque chunk names like `0-~-0up.q3_p0.js`, so a name-based
|
|
237
|
+
# filter is no longer reliable — we just iterate and stop on the first
|
|
238
|
+
# script that yields a `search_url`.
|
|
239
|
+
def send_website_request_getcode
|
|
232
240
|
headers = get_title_request_headers
|
|
233
241
|
response = make_get_request(BASE_URL, headers)
|
|
234
242
|
return nil unless response
|
|
@@ -236,15 +244,16 @@ module HowLongToBeat
|
|
|
236
244
|
doc = Nokogiri::HTML(response)
|
|
237
245
|
script_urls = doc.css('script[src]').map { |script| script['src'] }
|
|
238
246
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
scripts.each do |script_url|
|
|
247
|
+
script_urls.each do |script_url|
|
|
242
248
|
url = script_url.start_with?('http') ? script_url : "#{BASE_URL}#{script_url}"
|
|
243
249
|
script_content = make_get_request(url, headers)
|
|
244
250
|
next unless script_content
|
|
245
251
|
|
|
246
252
|
search_info = SearchInfo.new(script_content)
|
|
247
|
-
return
|
|
253
|
+
# Only return on a search_url match — an api_key without a search_url
|
|
254
|
+
# leaves us with no idea where to POST, and the loop should keep
|
|
255
|
+
# looking for a chunk that gives us the endpoint.
|
|
256
|
+
return search_info if search_info.search_url && !search_info.search_url.empty?
|
|
248
257
|
end
|
|
249
258
|
|
|
250
259
|
nil
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: howlongtobeat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitrii Pashutskii
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|