switch mockup generator to UK print providers and add --replace flag

- Prefer Print Clever (72) for canvas, Monster Digital (29) for
  apparel and mugs where available, fall back to default providers
- Rename Art Print products to Canvas (new Satin Canvas blueprint)
- Add Canvas Prints category in Printify tag extraction
- Add --replace/-r flag to purge existing Printify products before
  generating (with interactive confirmation)
- Add purge_all_products/1 to generator module
- Remove old art print mockups, add new canvas + extra provider mockups

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-14 13:00:59 +00:00
parent 5c2f70ce44
commit af207d7a35
78 changed files with 100 additions and 45 deletions

View File

@ -16,6 +16,9 @@ defmodule Mix.Tasks.GenerateMockups do
# Generate mockups (keeps products in Printify) # Generate mockups (keeps products in Printify)
mix generate_mockups mix generate_mockups
# Delete existing products first, then generate fresh ones
mix generate_mockups --replace
# Generate mockups and delete products afterwards # Generate mockups and delete products afterwards
mix generate_mockups --cleanup mix generate_mockups --cleanup
@ -45,12 +48,14 @@ defmodule Mix.Tasks.GenerateMockups do
OptionParser.parse(args, OptionParser.parse(args,
switches: [ switches: [
cleanup: :boolean, cleanup: :boolean,
replace: :boolean,
search: :string, search: :string,
list_blueprints: :boolean, list_blueprints: :boolean,
help: :boolean help: :boolean
], ],
aliases: [ aliases: [
c: :cleanup, c: :cleanup,
r: :replace,
s: :search, s: :search,
l: :list_blueprints, l: :list_blueprints,
h: :help h: :help
@ -84,6 +89,7 @@ defmodule Mix.Tasks.GenerateMockups do
mix generate_mockups [options] mix generate_mockups [options]
Options: Options:
--replace, -r Delete ALL existing products from Printify before generating (with confirmation)
--cleanup, -c Delete created products from Printify after downloading mockups --cleanup, -c Delete created products from Printify after downloading mockups
--search, -s TERM Search for blueprints by name --search, -s TERM Search for blueprints by name
--list-blueprints List all available blueprint IDs and names --list-blueprints List all available blueprint IDs and names
@ -149,12 +155,14 @@ defmodule Mix.Tasks.GenerateMockups do
defp generate_mockups(opts) do defp generate_mockups(opts) do
cleanup = Keyword.get(opts, :cleanup, false) cleanup = Keyword.get(opts, :cleanup, false)
replace = Keyword.get(opts, :replace, false)
Mix.shell().info(""" Mix.shell().info("""
Printify Mockup Generator Printify Mockup Generator
Replace mode: #{if replace, do: "ON ", else: "OFF"} ║
Cleanup mode: #{if cleanup, do: "ON ", else: "OFF"} ║ Cleanup mode: #{if cleanup, do: "ON ", else: "OFF"} ║
@ -177,6 +185,8 @@ defmodule Mix.Tasks.GenerateMockups do
""") """)
_token -> _token ->
if replace, do: purge_existing_products()
results = MockupGenerator.generate_all(cleanup: cleanup) results = MockupGenerator.generate_all(cleanup: cleanup)
# Summary # Summary
@ -200,4 +210,27 @@ defmodule Mix.Tasks.GenerateMockups do
end end
end end
end end
defp purge_existing_products do
alias SimpleshopTheme.Clients.Printify, as: Client
Mix.shell().info("Fetching existing products...")
{:ok, shop_id} = Client.get_shop_id()
{:ok, %{"data" => products}} = Client.list_products(shop_id)
count = length(products)
if count == 0 do
Mix.shell().info("No existing products to delete.\n")
else
Mix.shell().info("Found #{count} existing products in Printify.")
if Mix.shell().yes?("Delete all #{count} products before generating new ones?") do
deleted = MockupGenerator.purge_all_products(shop_id)
Mix.shell().info("Deleted #{deleted} products.\n")
else
Mix.shell().info("Skipping purge.\n")
end
end
end
end end

View File

@ -21,43 +21,43 @@ defmodule SimpleshopTheme.Mockups.Generator do
def product_definitions do def product_definitions do
[ [
%{ %{
name: "Mountain Sunrise Art Print", name: "Mountain Sunrise Canvas",
slug: "mountain-sunrise-print", slug: "mountain-sunrise-canvas",
category: "Art Prints", category: "Canvas Prints",
artwork_url: unsplash_download_url("UweNcthlmDc"), artwork_url: unsplash_download_url("UweNcthlmDc"),
product_type: :poster, product_type: :canvas,
price: 2400 price: 2400
}, },
%{ %{
name: "Ocean Waves Art Print", name: "Ocean Waves Canvas",
slug: "ocean-waves-print", slug: "ocean-waves-canvas",
category: "Art Prints", category: "Canvas Prints",
artwork_url: unsplash_download_url("XRhUTUVuXAE"), artwork_url: unsplash_download_url("XRhUTUVuXAE"),
product_type: :poster, product_type: :canvas,
price: 2400 price: 2400
}, },
%{ %{
name: "Wildflower Meadow Art Print", name: "Wildflower Meadow Canvas",
slug: "wildflower-meadow-print", slug: "wildflower-meadow-canvas",
category: "Art Prints", category: "Canvas Prints",
artwork_url: unsplash_download_url("QvjL4y7SF9k"), artwork_url: unsplash_download_url("QvjL4y7SF9k"),
product_type: :poster, product_type: :canvas,
price: 2400 price: 2400
}, },
%{ %{
name: "Geometric Abstract Art Print", name: "Geometric Abstract Canvas",
slug: "geometric-abstract-print", slug: "geometric-abstract-canvas",
category: "Art Prints", category: "Canvas Prints",
artwork_url: unsplash_download_url("-6GvTDpkkPU"), artwork_url: unsplash_download_url("-6GvTDpkkPU"),
product_type: :poster, product_type: :canvas,
price: 2800 price: 2800
}, },
%{ %{
name: "Botanical Illustration Print", name: "Botanical Illustration Canvas",
slug: "botanical-illustration-print", slug: "botanical-illustration-canvas",
category: "Art Prints", category: "Canvas Prints",
artwork_url: unsplash_download_url("FNtNIDQWUZY"), artwork_url: unsplash_download_url("FNtNIDQWUZY"),
product_type: :poster, product_type: :canvas,
price: 2400 price: 2400
}, },
%{ %{
@ -157,25 +157,20 @@ defmodule SimpleshopTheme.Mockups.Generator do
""" """
def blueprint_config do def blueprint_config do
%{ %{
# Search terms matched to Printify's actual blueprint titles (partial match) # Search terms matched to Printify's actual blueprint titles (partial match).
poster: %{blueprint_id: nil, print_provider_id: nil, search_term: "Matte Posters"}, # preferred_provider_id selects a UK-based provider where available:
tshirt: %{blueprint_id: nil, print_provider_id: nil, search_term: "Softstyle T-Shirt"}, # 29 = Monster Digital (UK) — apparel, mugs
hoodie: %{blueprint_id: nil, print_provider_id: nil, search_term: "Pullover Hoodie"}, # 72 = Print Clever (UK) — canvas prints
tote: %{blueprint_id: nil, print_provider_id: nil, search_term: "Cotton Tote Bag"}, canvas: %{search_term: "Satin Canvas, Stretched", preferred_provider_id: 72},
mug: %{blueprint_id: nil, print_provider_id: nil, search_term: "Mug 11oz"}, tshirt: %{search_term: "Softstyle T-Shirt", preferred_provider_id: 29},
cushion: %{ hoodie: %{search_term: "Heavy Blend™ Hooded Sweatshirt", preferred_provider_id: 29},
blueprint_id: nil, tote: %{search_term: "Cotton Tote Bag", preferred_provider_id: nil},
print_provider_id: nil, mug: %{search_term: "Ceramic Mug", preferred_provider_id: 29},
search_term: "Spun Polyester Square Pillow" cushion: %{search_term: "Spun Polyester Square Pillow", preferred_provider_id: nil},
}, blanket: %{search_term: "Sherpa Fleece Blanket", preferred_provider_id: nil},
blanket: %{blueprint_id: nil, print_provider_id: nil, search_term: "Sherpa Fleece Blanket"}, notebook: %{search_term: "Hardcover Journal Matte", preferred_provider_id: nil},
notebook: %{ phone_case: %{search_term: "Tough Phone Cases", preferred_provider_id: nil},
blueprint_id: nil, laptop_sleeve: %{search_term: "Laptop Sleeve", preferred_provider_id: nil}
print_provider_id: nil,
search_term: "Hardcover Journal Matte"
},
phone_case: %{blueprint_id: nil, print_provider_id: nil, search_term: "Tough Phone Cases"},
laptop_sleeve: %{blueprint_id: nil, print_provider_id: nil, search_term: "Laptop Sleeve"}
} }
end end
@ -210,13 +205,19 @@ defmodule SimpleshopTheme.Mockups.Generator do
@doc """ @doc """
Find a suitable print provider for a blueprint. Find a suitable print provider for a blueprint.
Prefers providers with good ratings and reasonable pricing.
When `preferred_provider_id` is given, uses that provider if available
for the blueprint. Falls back to the first provider otherwise.
""" """
def find_print_provider(blueprint_id) do def find_print_provider(blueprint_id, preferred_provider_id \\ nil) do
case Client.get_print_providers(blueprint_id) do case Client.get_print_providers(blueprint_id) do
{:ok, providers} when is_list(providers) and providers != [] -> {:ok, providers} when is_list(providers) and providers != [] ->
# Just pick the first provider for simplicity provider =
{:ok, hd(providers)} if preferred_provider_id do
Enum.find(providers, fn p -> p["id"] == preferred_provider_id end)
end
{:ok, provider || hd(providers)}
{:ok, []} -> {:ok, []} ->
{:error, :no_providers} {:error, :no_providers}
@ -384,6 +385,26 @@ defmodule SimpleshopTheme.Mockups.Generator do
end) end)
end end
@doc """
Deletes all products from the Printify shop.
Returns the number of products deleted.
"""
def purge_all_products(shop_id) do
case Client.list_products(shop_id) do
{:ok, %{"data" => products}} when is_list(products) ->
Enum.each(products, fn p ->
IO.puts(" Deleting: #{p["title"]} (#{p["id"]})")
Client.delete_product(shop_id, p["id"])
Process.sleep(200)
end)
length(products)
_ ->
0
end
end
@doc """ @doc """
Generate mockups for all products. Generate mockups for all products.
""" """
@ -450,7 +471,7 @@ defmodule SimpleshopTheme.Mockups.Generator do
with {:ok, blueprint} <- find_blueprint(config.search_term), with {:ok, blueprint} <- find_blueprint(config.search_term),
blueprint_id = blueprint["id"], blueprint_id = blueprint["id"],
_ = IO.puts(" Found blueprint: #{blueprint["title"]} (#{blueprint_id})"), _ = IO.puts(" Found blueprint: #{blueprint["title"]} (#{blueprint_id})"),
{:ok, provider} <- find_print_provider(blueprint_id), {:ok, provider} <- find_print_provider(blueprint_id, config[:preferred_provider_id]),
provider_id = provider["id"], provider_id = provider["id"],
_ = IO.puts(" Using provider: #{provider["title"]} (#{provider_id})"), _ = IO.puts(" Using provider: #{provider["title"]} (#{provider_id})"),
{:ok, variants} <- get_variant_info(blueprint_id, provider_id), {:ok, variants} <- get_variant_info(blueprint_id, provider_id),

View File

@ -418,10 +418,11 @@ defmodule SimpleshopTheme.Providers.Printify do
cond do cond do
has_tag?(tags, ~w[t-shirt tshirt shirt hoodie sweatshirt apparel clothing]) -> "Apparel" has_tag?(tags, ~w[t-shirt tshirt shirt hoodie sweatshirt apparel clothing]) -> "Apparel"
has_tag?(tags, ~w[canvas]) -> "Canvas Prints"
has_tag?(tags, ~w[mug cup blanket pillow cushion homeware homewares home]) -> "Homewares" has_tag?(tags, ~w[mug cup blanket pillow cushion homeware homewares home]) -> "Homewares"
has_tag?(tags, ~w[notebook journal stationery]) -> "Stationery" has_tag?(tags, ~w[notebook journal stationery]) -> "Stationery"
has_tag?(tags, ~w[phone case bag tote accessories]) -> "Accessories" has_tag?(tags, ~w[phone case bag tote accessories]) -> "Accessories"
has_tag?(tags, ~w[art print poster canvas wall]) -> "Art Prints" has_tag?(tags, ~w[art print poster wall]) -> "Art Prints"
true -> List.first(raw["tags"]) true -> List.first(raw["tags"])
end end
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB