Download itch.io cover art into OGPU's local boxart dir so games show real covers instead of the placeholder
This commit is contained in:
parent
c4fdd2f974
commit
a9371bdbc2
2 changed files with 122 additions and 1 deletions
|
|
@ -77,6 +77,7 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
var items := [] as Array[LibraryLaunchItem]
|
||||
for i in json_items:
|
||||
items.append(LibraryLaunchItem.from_dict(i))
|
||||
_queue_boxart(items)
|
||||
return items
|
||||
|
||||
if not itch.is_logged_in:
|
||||
|
|
@ -133,9 +134,129 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
if Cache.save_json(_cache_dir, _apps_cache_file, json_items) != OK:
|
||||
logger.warn("Unable to save itch.io apps cache")
|
||||
|
||||
_queue_boxart(items)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
## Queues background downloads of each game's cover art into OGPU's local
|
||||
## boxart directory (user://boxart/local/). The built-in "local" BoxArtProvider
|
||||
## picks those files up by <game name>-<layout>.png, so games show their real
|
||||
## cover instead of the placeholder. Covers are only downloaded once (the task
|
||||
## bails out if a layout file already exists).
|
||||
func _queue_boxart(items: Array) -> void:
|
||||
for i in items:
|
||||
var item: LibraryLaunchItem = i
|
||||
var game: Dictionary = item.metadata.get("game", {})
|
||||
var title: String = game.get("title", "")
|
||||
var cover_url: String = game.get("coverUrl", "")
|
||||
if title.is_empty() or cover_url.is_empty():
|
||||
continue
|
||||
if FileAccess.file_exists("/".join([_boxart_dir(), title + "-portrait.png"])):
|
||||
continue
|
||||
itch.thread_group.scheduled_exec(_ensure_boxart.bind(game), 0)
|
||||
|
||||
|
||||
## Downloads the given game's cover (blocking, runs on the shared thread) and
|
||||
## writes it to every boxart layout. itch.zone only serves one fixed landscape
|
||||
## image per game, and OGPU's boxart slots all keep-aspect (scale + crop), so a
|
||||
## single image is reused for portrait, landscape, banner and logo.
|
||||
func _ensure_boxart(game: Dictionary) -> void:
|
||||
var title: String = game.get("title", "")
|
||||
var cover_url: String = game.get("coverUrl", "")
|
||||
if title.is_empty() or cover_url.is_empty():
|
||||
return
|
||||
var dir := _boxart_dir()
|
||||
if FileAccess.file_exists("/".join([dir, title + "-portrait.png"])):
|
||||
return
|
||||
|
||||
var body := _download_cover(cover_url)
|
||||
if body.is_empty():
|
||||
logger.warn("Unable to download cover art for '" + title + "'")
|
||||
return
|
||||
if not _is_raster_image(body):
|
||||
# OGPU's local provider only loads png/jpg; animated gif covers (rare)
|
||||
# can't be rendered, so skip them rather than write an unloadable file.
|
||||
logger.warn("Skipping unsupported cover format for '" + title + "'")
|
||||
return
|
||||
|
||||
DirAccess.make_dir_recursive_absolute(dir)
|
||||
for layout in ["portrait", "landscape", "banner", "logo"]:
|
||||
var path := "/".join([dir, title + "-" + layout + ".png"])
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_buffer(body)
|
||||
file.close()
|
||||
logger.info("Downloaded boxart for '" + title + "'")
|
||||
|
||||
|
||||
func _boxart_dir() -> String:
|
||||
return ProjectSettings.globalize_path("user://boxart/local")
|
||||
|
||||
|
||||
## Blocking HTTPS GET of a single cover image, for use on a background thread.
|
||||
func _download_cover(url: String) -> PackedByteArray:
|
||||
var parts := url.split("/")
|
||||
var use_tls := parts[0] == "https:"
|
||||
var http := HTTPClient.new()
|
||||
var err: int = http.connect_to_host(parts[2], 443 if use_tls else 80, TLSOptions.client() if use_tls else null)
|
||||
if err != OK:
|
||||
return PackedByteArray()
|
||||
var deadline := Time.get_ticks_msec() + 15000
|
||||
while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
|
||||
http.poll()
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
return PackedByteArray()
|
||||
OS.delay_msec(50)
|
||||
if http.get_status() != HTTPClient.STATUS_CONNECTED:
|
||||
http.close()
|
||||
return PackedByteArray()
|
||||
|
||||
http.request(HTTPClient.METHOD_GET, "/" + "/".join(parts.slice(3)), PackedStringArray())
|
||||
deadline = Time.get_ticks_msec() + 15000
|
||||
while http.get_status() == HTTPClient.STATUS_REQUESTING:
|
||||
http.poll()
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
return PackedByteArray()
|
||||
OS.delay_msec(10)
|
||||
|
||||
var body := PackedByteArray()
|
||||
while http.get_status() == HTTPClient.STATUS_BODY:
|
||||
http.poll()
|
||||
var chunk: PackedByteArray = http.read_response_body_chunk()
|
||||
if chunk.is_empty():
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
return PackedByteArray()
|
||||
OS.delay_msec(10)
|
||||
continue
|
||||
body.append_array(chunk)
|
||||
|
||||
var code: int = http.get_response_code()
|
||||
http.close()
|
||||
if code != 200:
|
||||
return PackedByteArray()
|
||||
return body
|
||||
|
||||
|
||||
## Returns true if the bytes look like a PNG or JPEG (the only formats OGPU's
|
||||
## local boxart provider can load).
|
||||
func _is_raster_image(body: PackedByteArray) -> bool:
|
||||
if body.size() < 12:
|
||||
return false
|
||||
var png_magic := [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
||||
var is_png := true
|
||||
for i in png_magic.size():
|
||||
if body[i] != png_magic[i]:
|
||||
is_png = false
|
||||
break
|
||||
if is_png:
|
||||
return true
|
||||
return body[0] == 0xff and body[1] == 0xd8 and body[2] == 0xff
|
||||
|
||||
|
||||
## Best-effort discovery of the game's launch executable inside its install
|
||||
## folder. butlerd doesn't hand back a ready-to-exec command the way a
|
||||
## "gog://" or "steam://" URI does, so we look at what's actually on disk.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue