Fix srcset-polluted URLs from butlerd, sanitize cover and screenshot URLs
All checks were successful
Build plugin / build (push) Successful in 1m13s
All checks were successful
Build plugin / build (push) Successful in 1m13s
butlerd sometimes passes itch.io coverUrl/stillCoverUrl with trailing
srcset descriptors (e.g. ' 1x, h' or ' 1'). These polluted URLs cause
HTTPImageFetcher to fail because the URL is invalid and the extension
extraction breaks.
Changes:
- Add _sanitize_url() to strip srcset artifacts from URLs
- Add _is_gif_url() helper that works with both clean and polluted URLs
- Fix SCREENSHOT_REGEX_PATTERN to use [\w+/=]* instead of [^"]* to
avoid crossing srcset boundaries in HTML
- Replace all .ends_with('.gif') checks with _is_gif_url()
- Sanitize all URLs from game metadata and HTML scraping
- Add IMAGE_EXTS constant for extension validation
Also bump to v0.1.7.
This commit is contained in:
parent
730b600ec0
commit
1bde072983
2 changed files with 34 additions and 12 deletions
|
|
@ -13,7 +13,8 @@ extends BoxArtProvider
|
|||
const BOXART_DIR := "user://boxart/itch"
|
||||
const META_CACHE_FILE := "itch_art_meta.json"
|
||||
const HTTP_TIMEOUT_MS := 15000
|
||||
const SCREENSHOT_REGEX_PATTERN := "https://img\\.itch\\.zone/aW1hZ2Uv[^\"]*/original/[^\"]*"
|
||||
const SCREENSHOT_REGEX_PATTERN := "https://img\\.itch\\.zone/aW1hZ2Uv[\\w+/=]*/original/[\\w+/=]*\\.[a-z]+"
|
||||
const IMAGE_EXTS := ["png", "jpg", "jpeg", "gif", "webp", "bmp"]
|
||||
const CONNECT_POLL_DELAY_MS := 50
|
||||
const READ_POLL_DELAY_MS := 10
|
||||
const LOG_URL_MAX_LEN := 80
|
||||
|
|
@ -45,6 +46,27 @@ func _ready() -> void:
|
|||
add_child(http_image)
|
||||
|
||||
|
||||
## Strips HTML srcset artifacts from a URL. Butlerd sometimes passes through
|
||||
## itch.io cover URLs with trailing srcset descriptors like " 1x, h" or " 2x".
|
||||
## These cause HTTPImageFetcher to fail because the URL is no longer valid.
|
||||
func _sanitize_url(url: String) -> String:
|
||||
if url.is_empty():
|
||||
return url
|
||||
# Srcset descriptors are separated by spaces — take only the URL portion.
|
||||
var clean: String = url.split(" ")[0]
|
||||
# Validate that the cleaned URL ends with a known image extension.
|
||||
var ext: String = clean.get_extension().to_lower()
|
||||
if ext not in IMAGE_EXTS:
|
||||
logger.warn("Sanitized URL has unexpected extension '%s': %s" % [ext, clean.left(LOG_URL_MAX_LEN)])
|
||||
return clean
|
||||
|
||||
|
||||
## Returns whether a URL points to a GIF image. Handles both clean URLs and
|
||||
## URLs contaminated with srcset artifacts (e.g. "image.gif 1x, h").
|
||||
func _is_gif_url(url: String) -> bool:
|
||||
return _sanitize_url(url).to_lower().ends_with(".gif")
|
||||
|
||||
|
||||
func get_boxart(item: LibraryItem, kind: LAYOUT) -> Texture2D:
|
||||
if not kind in layout_map:
|
||||
logger.error("Unsupported boxart layout: {0}".format([kind]))
|
||||
|
|
@ -81,15 +103,15 @@ func get_boxart(item: LibraryItem, kind: LAYOUT) -> Texture2D:
|
|||
else:
|
||||
logger.warn("No URL in game dict for: %s" % title)
|
||||
|
||||
var screenshots: Array = meta.get("screenshots", [])
|
||||
var screenshots: Array = meta.get("screenshots", []).map(_sanitize_url)
|
||||
# Filter out GIF screenshots — Godot can't decode them.
|
||||
var non_gif_screenshots: Array = screenshots.filter(
|
||||
func(url): return not url.to_lower().ends_with(".gif")
|
||||
func(url): return not _is_gif_url(url)
|
||||
)
|
||||
if screenshots.size() != non_gif_screenshots.size():
|
||||
logger.info("Filtered %d GIF screenshots for: %s" % [screenshots.size() - non_gif_screenshots.size(), title])
|
||||
|
||||
var cover_url := _best_portrait_url(game, non_gif_screenshots)
|
||||
var cover_url := _sanitize_url(_best_portrait_url(game, non_gif_screenshots))
|
||||
if cover_url.is_empty():
|
||||
logger.warn("No cover URL for: %s (id=%d)" % [title, game_id])
|
||||
return null
|
||||
|
|
@ -150,14 +172,14 @@ func _url_for_layout(kind: LAYOUT, cover_url: String, screenshots: Array) -> Str
|
|||
LAYOUT.GRID_LANDSCAPE:
|
||||
# Prefer a non-GIF screenshot
|
||||
for url in screenshots:
|
||||
if not url.to_lower().ends_with(".gif"):
|
||||
if not _is_gif_url(url):
|
||||
return url
|
||||
return cover_url
|
||||
LAYOUT.BANNER:
|
||||
# Single screenshot fallback (two-screenshot case handled in get_boxart).
|
||||
# Prefer a non-GIF screenshot.
|
||||
for url in screenshots:
|
||||
if not url.to_lower().ends_with(".gif"):
|
||||
if not _is_gif_url(url):
|
||||
return url
|
||||
return cover_url
|
||||
return ""
|
||||
|
|
@ -169,15 +191,15 @@ func _url_for_layout(kind: LAYOUT, cover_url: String, screenshots: Array) -> Str
|
|||
func _best_portrait_url(game: Dictionary, screenshots: Array) -> String:
|
||||
# Prefer stillCoverUrl if it's not a GIF
|
||||
var still: String = game.get("stillCoverUrl", "")
|
||||
if not still.is_empty() and not still.to_lower().ends_with(".gif"):
|
||||
if not still.is_empty() and not _is_gif_url(still):
|
||||
return still
|
||||
# Try coverUrl if it's not a GIF
|
||||
var cover: String = game.get("coverUrl", "")
|
||||
if not cover.is_empty() and not cover.to_lower().ends_with(".gif"):
|
||||
if not cover.is_empty() and not _is_gif_url(cover):
|
||||
return cover
|
||||
# Try the first non-GIF screenshot
|
||||
for url in screenshots:
|
||||
if not url.to_lower().ends_with(".gif"):
|
||||
if not _is_gif_url(url):
|
||||
return url
|
||||
# Last resort: use whatever we have (GIF)
|
||||
return still if not still.is_empty() else cover
|
||||
|
|
@ -274,8 +296,8 @@ func _fetch_game_page_metadata(game: Dictionary) -> Dictionary:
|
|||
shot_re.compile(SCREENSHOT_REGEX_PATTERN)
|
||||
var screenshots: Array = []
|
||||
for m in shot_re.search_all(html):
|
||||
var url: String = m.get_string(0)
|
||||
if url.to_lower().ends_with(".gif"):
|
||||
var url: String = _sanitize_url(m.get_string(0))
|
||||
if _is_gif_url(url):
|
||||
logger.debug("Skipping GIF screenshot: " + url)
|
||||
continue
|
||||
screenshots.append(url)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"plugin.id": "itch-artprovider",
|
||||
"plugin.name": "itch.io Art Provider",
|
||||
"plugin.version": "0.1.6",
|
||||
"plugin.version": "0.1.7",
|
||||
"plugin.min-api-version": "1.1.0",
|
||||
"plugin.link": "https://forge.thergic.ar/jose/itchio-opengamepadui-artprovider-plugin",
|
||||
"plugin.source": "https://forge.thergic.ar/jose/itchio-opengamepadui-artprovider-plugin",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue