defmodule CobblemonUi.SpriteCache do @moduledoc """ Downloads and caches Pokémon sprite images locally. Sprites are stored under `$CACHE_DIR/sprites/.png`. Use `ensure_sprite/1` to lazily download a sprite on first access, or `sprite_path/1` to get the expected local filesystem path. """ use GenServer require Logger @source_url "https://img.rankedboost.com/wp-content/plugins/k-Pokemon/assets/sprites-official" # --------------------------------------------------------------------------- # Client API # --------------------------------------------------------------------------- def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @doc """ Ensures the sprite for the given species is downloaded locally. Returns `:ok` if the file exists (or was just fetched), or `{:error, reason}`. """ @spec ensure_sprite(String.t()) :: :ok | {:error, term()} def ensure_sprite(species) when is_binary(species) do key = String.downcase(species) path = sprite_path(key) if File.exists?(path) do :ok else GenServer.call(__MODULE__, {:download, key}, 30_000) end end @doc """ Returns the local URL path for a species sprite (for use in templates). Always returns the path even if the file hasn't been downloaded yet. """ @spec sprite_url(String.t()) :: String.t() def sprite_url(species) do "/sprites/#{String.downcase(species)}.png" end @doc "Returns the filesystem path where a sprite would be stored." @spec sprite_path(String.t()) :: String.t() def sprite_path(species) do Path.join(sprites_dir(), "#{String.downcase(species)}.png") end @doc "Returns the base sprites directory." @spec sprites_dir() :: String.t() def sprites_dir do dir = System.get_env("CACHE_DIR", ".") Path.join(dir, "sprites") end # --------------------------------------------------------------------------- # Server callbacks # --------------------------------------------------------------------------- @impl true def init(_opts) do File.mkdir_p!(sprites_dir()) {:ok, MapSet.new()} end @impl true def handle_call({:download, species}, _from, in_progress) do path = sprite_path(species) if File.exists?(path) do {:reply, :ok, in_progress} else result = download_sprite(species, path) {:reply, result, in_progress} end end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp download_sprite(species, dest) do url = "#{@source_url}/#{species}.png" case Req.get(url, decode_body: false) do {:ok, %Req.Response{status: 200, body: body}} when is_binary(body) -> File.mkdir_p!(Path.dirname(dest)) File.write!(dest, body) Logger.debug("[SpriteCache] Downloaded sprite for #{species}") :ok {:ok, %Req.Response{status: status}} -> Logger.warning("[SpriteCache] Failed to download #{species}: HTTP #{status}") {:error, {:http_error, status}} {:error, reason} -> Logger.warning("[SpriteCache] Failed to download #{species}: #{inspect(reason)}") {:error, reason} end end end