simpleshop_theme/lib/simpleshop_theme/images/variant_cache.ex
jamey 1b49b470f2 feat: add product image download pipeline for PageSpeed 100%
Downloads Printify CDN images via ImageDownloadWorker, processes
through Media pipeline (WebP conversion, AVIF/WebP variant generation),
and links to ProductImage via new image_id FK.

- Add image_id to product_images table
- ImageDownloadWorker downloads and processes external images
- sync_product_images preserves image_id when URL unchanged
- PreviewData uses local images for responsive <picture> elements
- VariantCache enqueues pending downloads on startup
- mix simpleshop.download_images backfill task

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 00:26:19 +00:00

118 lines
3.3 KiB
Elixir

defmodule SimpleshopTheme.Images.VariantCache do
@moduledoc """
Ensures all image variants exist on startup.
This GenServer runs at startup and checks for:
1. Database images with incomplete variants_status or missing disk files
2. Mockup source files missing their generated variants
For any images missing variants, it enqueues Oban jobs to regenerate them.
"""
use GenServer
require Logger
alias SimpleshopTheme.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema
alias SimpleshopTheme.Images.{Optimizer, OptimizeWorker}
alias SimpleshopTheme.Products
alias SimpleshopTheme.Sync.ImageDownloadWorker
import Ecto.Query
@mockup_dir "priv/static/mockups"
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
Task.start(fn -> ensure_all_variants() end)
{:ok, %{}}
end
defp ensure_all_variants do
Logger.info("[VariantCache] Checking image variant cache...")
File.mkdir_p!(Optimizer.cache_dir())
ensure_database_image_variants()
ensure_mockup_variants()
ensure_product_image_downloads()
end
defp ensure_database_image_variants do
incomplete =
ImageSchema
|> where([i], i.variants_status != "complete" or is_nil(i.variants_status))
|> where([i], i.is_svg == false)
|> Repo.all()
complete_missing =
ImageSchema
|> where([i], i.variants_status == "complete")
|> where([i], i.is_svg == false)
|> where([i], not is_nil(i.source_width))
|> Repo.all()
|> Enum.reject(fn img ->
Optimizer.disk_variants_exist?(img.id, img.source_width)
end)
to_process = incomplete ++ complete_missing
if to_process == [] do
Logger.info("[VariantCache] All database image variants up to date")
else
Logger.info(
"[VariantCache] Enqueueing #{length(to_process)} database images for processing"
)
Enum.each(to_process, fn image ->
image
|> ImageSchema.changeset(%{variants_status: "pending"})
|> Repo.update!()
OptimizeWorker.enqueue(image.id)
end)
end
end
defp ensure_mockup_variants do
if File.dir?(@mockup_dir) do
sources =
Path.wildcard(Path.join(@mockup_dir, "*.webp"))
|> Enum.reject(&is_variant?/1)
missing = Enum.reject(sources, &mockup_variants_exist?/1)
if missing == [] do
Logger.info("[VariantCache] All mockup variants up to date")
else
Logger.info("[VariantCache] Enqueueing #{length(missing)} mockups for processing")
Enum.each(missing, &OptimizeWorker.enqueue_mockup/1)
end
end
end
defp is_variant?(path) do
basename = Path.basename(path) |> Path.rootname()
String.match?(basename, ~r/-(400|800|1200|thumb)$/)
end
defp mockup_variants_exist?(source_path) do
basename = Path.basename(source_path) |> Path.rootname()
dir = Path.dirname(source_path)
File.exists?(Path.join(dir, "#{basename}-800.webp"))
end
defp ensure_product_image_downloads do
pending = Products.list_pending_downloads(limit: 500)
if pending == [] do
Logger.info("[VariantCache] All product images downloaded")
else
Logger.info("[VariantCache] Enqueueing #{length(pending)} product images for download")
Enum.each(pending, fn image -> ImageDownloadWorker.enqueue(image.id) end)
end
end
end