diff --git a/core/boxart_itch.gd b/core/boxart_itch.gd index 1687132..136e8fc 100644 --- a/core/boxart_itch.gd +++ b/core/boxart_itch.gd @@ -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) diff --git a/plugin.json b/plugin.json index 6e96472..7924c75 100644 --- a/plugin.json +++ b/plugin.json @@ -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",