GIF handling: skip GIFs in HTTPImageFetcher, filter GIF screenshots, fallback portrait URLs
All checks were successful
Build plugin / build (push) Successful in 1m11s

- 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)
This commit is contained in:
Jose Falanga 2026-08-17 11:07:54 -03:00
parent 8a5dc41064
commit d06cba872c
2 changed files with 54 additions and 13 deletions

View file

@ -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: