GDScript coroutines cannot be stored without await like JS Promises.
Removed _pending_gif_requests and _do_fetch_gif_as_texture — the race
condition is not a practical issue since Godot is single-threaded and
the cache check prevents duplicate work after the first request completes.
Bump to v0.1.15
- Replace manual HTTP polling with _download_url_bytes
- Extract _is_cloudflare_challenge and _extract_screenshots_from_html
- All functions now under 50 lines
- Rename goos/goarch to os_name/arch_name (no domain jargon)
- Split _install_ffmpeg into _detect_os, _detect_arch, _build_ffmpeg_url, _download_url_bytes, _extract_ffmpeg
- Split _build_animated_gif_texture into _extract_gif_frames and _assemble_animated_texture
- Split _parse_gif_frame_delays into _skip_gif_header, _parse_gif_extension, _skip_gif_image_descriptor
- Replace _download_gif_bytes with _download_url_bytes (shared helper)
- All functions now under 50 lines
- Add settings menu with 'Animate GIF covers' toggle (default: off)
- When enabled: extract all GIF frames via ffmpeg, build AnimatedTexture
- When disabled: extract first frame only (static ImageTexture)
- AnimatedTexture starts paused — unpauses only when card has focus
- Focus-based pausing via gui_focus_changed signal (walks up to GameCard)
- Parse GIF89a frame delays for accurate animation speed
- Cache invalidation when setting changes (no restart needed)
- Cache AnimatedTextures in memory to avoid re-extraction
- Bump to v0.1.13
- Add ffmpeg download and installation (mirrors butler pattern)
- Add _fetch_gif_first_frame() that downloads GIF and extracts first frame
- Add _ensure_ffmpeg() that checks system PATH, bundled location, or downloads
- Add _fetch_image() that routes GIF URLs through ffmpeg, non-GIF through HTTPImageFetcher
- Remove GIF filtering from screenshots and cover URLs
- Simplify _url_for_layout since _fetch_image handles GIFs transparently
- Bump to v0.1.12
_bset_portrait_url was filtering GIF URLs, but stillCoverUrl is the
still image version by definition and should always be used when
available. This restores the original behavior where games with GIF
capsules get their still cover image.
_best_portrait_url was falling back to non-GIF screenshots when both
coverUrl and stillCoverUrl were GIFs. Screenshots are not capsule art,
so this produced wrong imagery. Now the function only returns cover
URLs and returns empty when only GIFs are available.
When all available cover URLs are GIFs (coverUrl, stillCoverUrl, and
screenshots), return empty instead of a GIF URL. HTTPImageFetcher cannot
decode GIFs, so attempting to fetch one always returns null. Returning
empty early avoids unnecessary network requests and confusing log noise.
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.