diff --git a/core/boxart_itch.gd b/core/boxart_itch.gd index 723a94e..080d27d 100644 --- a/core/boxart_itch.gd +++ b/core/boxart_itch.gd @@ -22,6 +22,7 @@ const LOG_URL_MAX_LEN := 80 const SETTING_SECTION := "plugin.artprovider" const SETTING_ANIMATED_GIFS := "animated_gifs" const GIF_DEFAULT_FRAME_DELAY_MS := 100 +const CACHE_DIR := "images" @export var use_caching: bool = true var http_image := HTTPImageFetcher.new() @@ -118,6 +119,71 @@ func _is_gif_url(url: String) -> bool: return _sanitize_url(url).to_lower().ends_with(".gif") +## Returns whether a URL points to a JPEG image. +func _is_jpg_url(url: String) -> bool: + var clean := _sanitize_url(url).to_lower() + return clean.ends_with(".jpg") or clean.ends_with(".jpeg") + + +## Removes and frees an HTTPRequest node. +func _remove_http(http: HTTPRequest) -> void: + remove_child(http) + http.queue_free() + + +## Downloads an image and detects the actual format from content-type headers. +## Used as a fallback when HTTPImageFetcher fails due to extension mismatch. +func _fetch_image_with_format_detection(url: String, cache_flags: int) -> Texture2D: + var http := HTTPRequest.new() + http.timeout = HTTP_TIMEOUT_MS / 1000.0 + add_child.call_deferred(http) + await http.ready + + if http.request(url) != OK: + _remove_http(http) + return null + + var args: Array = await http.request_completed + var result: int = args[0] + var response_code: int = args[1] + var headers: PackedStringArray = args[2] + var body: PackedByteArray = args[3] + _remove_http(http) + + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: + return null + + # Detect actual format from Content-Type header. + var content_type := "" + for h in headers: + if h.to_lower().begins_with("content-type:"): + content_type = h.split(":", true, 1)[1].strip_edges().to_lower() + break + + var image := Image.new() + var err: int = ERR_INVALID_DATA + if content_type.find("png") != -1: + err = image.load_png_from_buffer(body) + elif content_type.find("jpeg") != -1 or content_type.find("jpg") != -1: + err = image.load_jpg_from_buffer(body) + elif content_type.find("webp") != -1: + err = image.load_webp_from_buffer(body) + else: + # Unknown type — try PNG first, then JPEG. + err = image.load_png_from_buffer(body) + if err != OK: + err = image.load_jpg_from_buffer(body) + + if err != OK: + logger.warn("Format detection failed for %s (content-type: %s)" % [url.left(LOG_URL_MAX_LEN), content_type]) + return null + + var texture := ImageTexture.create_from_image(image) + if cache_flags & Cache.FLAGS.SAVE: + Cache.save_image(CACHE_DIR, url, texture) + return texture + + func get_boxart(item: LibraryItem, kind: LAYOUT) -> Texture2D: if not kind in layout_map: logger.error("Unsupported boxart layout: {0}".format([kind])) @@ -373,7 +439,13 @@ func _fetch_image(url: String, cache_flags: int) -> Texture2D: _last_animated_setting = animated logger.info("Animated GIF setting changed to %s — cache cleared" % str(animated)) return await _fetch_gif_as_texture(url, cache_flags) - return await http_image.fetch(url, cache_flags) + # HTTPImageFetcher uses the URL extension to pick the decoder, but + # itch.zone often serves PNG data for .jpg URLs. If the normal fetch + # fails for a .jpg/.jpeg URL, retry by detecting the actual format. + var texture := await http_image.fetch(url, cache_flags) + if texture == null and _is_jpg_url(url): + texture = await _fetch_image_with_format_detection(url, cache_flags) + return texture ## Downloads a GIF and converts it to a Godot texture. When animated GIFs