From d06cba872c63bc7a5b3154191c41ee8d4b7f488e Mon Sep 17 00:00:00 2001 From: Jose Falanga Date: Mon, 17 Aug 2026 11:07:54 -0300 Subject: [PATCH] GIF handling: skip GIFs in HTTPImageFetcher, filter GIF screenshots, fallback portrait URLs - HTTPImageFetcher: skip GIF requests early (Godot can't decode), add webp support, try fallback formats - Art provider: filter GIF screenshots from banners, prefer non-GIF cover URLs via _best_portrait_url - Fix variable ordering bug (meta used before definition) --- core/boxart_itch.gd | 65 ++++++++++++++++++++++++++++++++++++--------- plugin.json | 2 +- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/core/boxart_itch.gd b/core/boxart_itch.gd index 104c326..18f2292 100644 --- a/core/boxart_itch.gd +++ b/core/boxart_itch.gd @@ -52,14 +52,10 @@ func get_boxart(item: LibraryItem, kind: LAYOUT) -> Texture2D: var game_id := int(game.get("id", 0)) var title: String = game.get("title", "") - var cover_url := _cover_url(game) - logger.info("get_boxart: title=%s id=%d cover_url=%s layout=%s" % [title, game_id, cover_url, str(kind)]) + logger.info("get_boxart: title=%s id=%d layout=%s" % [title, game_id, str(kind)]) if title.is_empty(): logger.warn("Game title is empty for id %d" % game_id) return null - if cover_url.is_empty(): - logger.warn("No cover URL for: %s (id=%d)" % [title, game_id]) - return null # Enrich on first access: scrape the itch.io page for screenshots. var meta: Dictionary = _enriched.get(game_id, {}) @@ -77,15 +73,28 @@ func get_boxart(item: LibraryItem, kind: LAYOUT) -> Texture2D: logger.warn("No URL in game dict for: %s" % title) var screenshots: Array = meta.get("screenshots", []) + # 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") + ) + 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) + if cover_url.is_empty(): + logger.warn("No cover URL for: %s (id=%d)" % [title, game_id]) + return null + logger.info("cover_url=%s" % cover_url.left(80)) + var cache_flags := Cache.FLAGS.NONE if use_caching: cache_flags = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE # Banner: combine two screenshots side-by-side when available. - if kind == LAYOUT.BANNER and screenshots.size() >= 2: + if kind == LAYOUT.BANNER and non_gif_screenshots.size() >= 2: logger.info("Fetching itch.io banner (2 screenshots) for: " + item.name) - var tex_a := await http_image.fetch(screenshots[0], cache_flags) - var tex_b := await http_image.fetch(screenshots[1], cache_flags) + var tex_a := await http_image.fetch(non_gif_screenshots[0], cache_flags) + var tex_b := await http_image.fetch(non_gif_screenshots[1], cache_flags) if tex_a != null and tex_b != null: return _combine_side_by_side(tex_a, tex_b) # Fall through to single-image path if either failed. @@ -95,7 +104,7 @@ func get_boxart(item: LibraryItem, kind: LAYOUT) -> Texture2D: if tex_b != null: return tex_b - var url := _url_for_layout(kind, cover_url, screenshots) + var url := _url_for_layout(kind, cover_url, non_gif_screenshots) if url.is_empty(): logger.warn("URL for layout %s is empty (cover=%s)" % [str(kind), cover_url]) return null @@ -130,20 +139,52 @@ func _url_for_layout(kind: LAYOUT, cover_url: String, screenshots: Array) -> Str LAYOUT.GRID_PORTRAIT, LAYOUT.LOGO: return cover_url LAYOUT.GRID_LANDSCAPE: - return screenshots[0] if not screenshots.is_empty() else cover_url + # Prefer a non-GIF screenshot + for url in screenshots: + if not url.to_lower().ends_with(".gif"): + return url + return cover_url LAYOUT.BANNER: # Single screenshot fallback (two-screenshot case handled in get_boxart). - return screenshots[0] if not screenshots.is_empty() else cover_url + # Prefer a non-GIF screenshot. + for url in screenshots: + if not url.to_lower().ends_with(".gif"): + return url + return cover_url return "" func _cover_url(game: Dictionary) -> String: var still: String = game.get("stillCoverUrl", "") - if not still.is_empty(): + if not still.is_empty() and not still.to_lower().ends_with(".gif"): return still + # stillCoverUrl is empty or is a GIF — prefer it if non-GIF alternatives + # aren't available, but log a warning. + if not still.is_empty(): + logger.debug("stillCoverUrl is a GIF, will try alternatives: " + still) return game.get("coverUrl", "") +## Returns a non-GIF URL suitable for portrait/logo from the game data. +## Falls back through: stillCoverUrl → coverUrl (non-GIF) → first non-GIF +## screenshot. +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"): + 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"): + return cover + # Try the first non-GIF screenshot + for url in screenshots: + if not url.to_lower().ends_with(".gif"): + return url + # Last resort: use whatever we have (GIF) + return still if not still.is_empty() else cover + + ## Combines two textures side-by-side into a single banner image. ## Both images are scaled to the same height, then placed left and right. func _combine_side_by_side(a: Texture2D, b: Texture2D) -> Texture2D: diff --git a/plugin.json b/plugin.json index 887ea6b..c0e577f 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.3", + "plugin.version": "0.1.4", "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",