New PrintfulGenerator module creates demo products in Printful with multi-variant support (multiple colours/sizes per product type). Mix task gets --provider flag to choose between Printify and Printful. After syncing Printful products, MockupEnricher Oban worker calls the legacy mockup generator API to produce extra angle images (back, left, right) and appends them as product images. Jobs are staggered 45s apart with snooze-based 429 handling. Flat products (canvas, poster) get no extras — apparel and 3D products get 1-5 extra angles each. Also fixes: - cross-provider slug uniqueness (appends -2, -3 suffix) - category mapping order (Accessories before Canvas Prints) - image dedup by URL instead of colour (fixes canvas variants) - artwork URL stored in provider_data for enricher access Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
292 lines
9.1 KiB
Elixir
292 lines
9.1 KiB
Elixir
defmodule Mix.Tasks.GenerateMockups do
|
|
@moduledoc """
|
|
Generates product mockups using Printify or Printful APIs.
|
|
|
|
This task automates the creation of product mockups for the SimpleshopTheme
|
|
sample content. It downloads artwork from Unsplash, uploads it to the
|
|
print-on-demand provider, creates products, and downloads the generated mockups.
|
|
|
|
## Usage
|
|
|
|
# Printify (default)
|
|
mix generate_mockups
|
|
mix generate_mockups --cleanup
|
|
mix generate_mockups --replace
|
|
|
|
# Printful
|
|
mix generate_mockups --provider printful
|
|
mix generate_mockups --provider printful --mockups-only
|
|
mix generate_mockups --provider printful --products-only
|
|
mix generate_mockups --provider printful --cleanup
|
|
|
|
## Output
|
|
|
|
Mockup images are saved to: priv/static/mockups/
|
|
"""
|
|
|
|
use Mix.Task
|
|
|
|
alias SimpleshopTheme.Mockups.Generator, as: PrintifyGenerator
|
|
alias SimpleshopTheme.Mockups.PrintfulGenerator
|
|
|
|
@shortdoc "Generates product mockups using Printify or Printful API"
|
|
|
|
@impl Mix.Task
|
|
def run(args) do
|
|
Mix.Task.run("app.start")
|
|
|
|
{opts, _, _} =
|
|
OptionParser.parse(args,
|
|
switches: [
|
|
cleanup: :boolean,
|
|
replace: :boolean,
|
|
search: :string,
|
|
list_blueprints: :boolean,
|
|
help: :boolean,
|
|
provider: :string,
|
|
mockups_only: :boolean,
|
|
products_only: :boolean
|
|
],
|
|
aliases: [
|
|
c: :cleanup,
|
|
r: :replace,
|
|
s: :search,
|
|
l: :list_blueprints,
|
|
h: :help,
|
|
p: :provider
|
|
]
|
|
)
|
|
|
|
provider = Keyword.get(opts, :provider, "printify")
|
|
|
|
cond do
|
|
opts[:help] ->
|
|
print_help()
|
|
|
|
provider == "printful" ->
|
|
run_printful(opts)
|
|
|
|
opts[:list_blueprints] ->
|
|
list_blueprints()
|
|
|
|
opts[:search] ->
|
|
search_blueprints(opts[:search])
|
|
|
|
true ->
|
|
run_printify(opts)
|
|
end
|
|
end
|
|
|
|
defp print_help do
|
|
Mix.shell().info("""
|
|
|
|
Mockup Generator
|
|
================
|
|
|
|
Generates product mockups using Printify or Printful APIs.
|
|
|
|
Usage:
|
|
mix generate_mockups [options]
|
|
|
|
Common options:
|
|
--provider, -p NAME Provider to use: "printify" (default) or "printful"
|
|
--cleanup, -c Delete created products after downloading mockups
|
|
--help, -h Show this help message
|
|
|
|
Printify options:
|
|
--replace, -r Delete ALL existing products before generating (with confirmation)
|
|
--search, -s TERM Search for blueprints by name
|
|
--list-blueprints List all available blueprint IDs and names
|
|
|
|
Printful options:
|
|
--mockups-only Only generate mockup images (skip product creation)
|
|
--products-only Only create sync products (skip mockup images)
|
|
|
|
Environment:
|
|
PRINTIFY_API_TOKEN Required for Printify provider
|
|
PRINTFUL_API_TOKEN Required for Printful provider
|
|
|
|
Examples:
|
|
mix generate_mockups
|
|
mix generate_mockups --provider printful
|
|
mix generate_mockups --provider printful --mockups-only
|
|
mix generate_mockups --provider printful --cleanup
|
|
""")
|
|
end
|
|
|
|
defp list_blueprints do
|
|
Mix.shell().info("Fetching blueprints from Printify...")
|
|
|
|
case PrintifyGenerator.list_blueprints() do
|
|
blueprints when is_list(blueprints) ->
|
|
Mix.shell().info("\nAvailable Blueprints:\n")
|
|
|
|
blueprints
|
|
|> Enum.each(fn {id, title} ->
|
|
Mix.shell().info(" #{id}: #{title}")
|
|
end)
|
|
|
|
Mix.shell().info("\nTotal: #{length(blueprints)} blueprints")
|
|
|
|
{:error, reason} ->
|
|
Mix.shell().error("Error fetching blueprints: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
defp search_blueprints(term) do
|
|
Mix.shell().info("Searching for blueprints matching '#{term}'...")
|
|
|
|
case PrintifyGenerator.search_blueprints(term) do
|
|
results when is_list(results) ->
|
|
if results == [] do
|
|
Mix.shell().info("No blueprints found matching '#{term}'")
|
|
else
|
|
Mix.shell().info("\nMatching Blueprints:\n")
|
|
|
|
results
|
|
|> Enum.each(fn {id, title} ->
|
|
Mix.shell().info(" #{id}: #{title}")
|
|
end)
|
|
|
|
Mix.shell().info("\nFound: #{length(results)} blueprints")
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Mix.shell().error("Error searching blueprints: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
# ===========================================================================
|
|
# Printify
|
|
# ===========================================================================
|
|
|
|
defp run_printify(opts) do
|
|
cleanup = Keyword.get(opts, :cleanup, false)
|
|
replace = Keyword.get(opts, :replace, false)
|
|
|
|
Mix.shell().info("""
|
|
|
|
╔═══════════════════════════════════════════╗
|
|
║ Printify Mockup Generator ║
|
|
╠═══════════════════════════════════════════╣
|
|
║ Replace mode: #{if replace, do: "ON ", else: "OFF"} ║
|
|
║ Cleanup mode: #{if cleanup, do: "ON ", else: "OFF"} ║
|
|
╚═══════════════════════════════════════════╝
|
|
|
|
""")
|
|
|
|
case System.get_env("PRINTIFY_API_TOKEN") do
|
|
nil ->
|
|
Mix.shell().error("""
|
|
Error: PRINTIFY_API_TOKEN environment variable is not set.
|
|
|
|
Set it and retry:
|
|
export PRINTIFY_API_TOKEN="your-token"
|
|
mix generate_mockups
|
|
""")
|
|
|
|
_token ->
|
|
if replace, do: purge_existing_printify_products()
|
|
|
|
results = PrintifyGenerator.generate_all(cleanup: cleanup)
|
|
|
|
successful = Enum.count(results, &match?({:ok, _, _, _}, &1))
|
|
failed = Enum.count(results, &match?({:error, _, _}, &1))
|
|
|
|
Mix.shell().info("""
|
|
|
|
Summary: #{successful} succeeded, #{failed} failed
|
|
""")
|
|
|
|
if failed > 0 do
|
|
Mix.shell().error(
|
|
"Some products failed to generate. Check the output above for details."
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp purge_existing_printify_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 = PrintifyGenerator.purge_all_products(shop_id)
|
|
Mix.shell().info("Deleted #{deleted} products.\n")
|
|
else
|
|
Mix.shell().info("Skipping purge.\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
# ===========================================================================
|
|
# Printful
|
|
# ===========================================================================
|
|
|
|
defp run_printful(opts) do
|
|
mockups_only = Keyword.get(opts, :mockups_only, false)
|
|
products_only = Keyword.get(opts, :products_only, false)
|
|
cleanup = Keyword.get(opts, :cleanup, false)
|
|
|
|
do_mockups = !products_only
|
|
do_products = !mockups_only
|
|
|
|
Mix.shell().info("""
|
|
|
|
╔═══════════════════════════════════════════╗
|
|
║ Printful Mockup Generator ║
|
|
╠═══════════════════════════════════════════╣
|
|
║ Mockups: #{if do_mockups, do: "ON ", else: "OFF"} ║
|
|
║ Products: #{if do_products, do: "ON ", else: "OFF"} ║
|
|
║ Cleanup: #{if cleanup, do: "ON ", else: "OFF"} ║
|
|
╚═══════════════════════════════════════════╝
|
|
|
|
""")
|
|
|
|
case System.get_env("PRINTFUL_API_TOKEN") do
|
|
nil ->
|
|
Mix.shell().error("""
|
|
Error: PRINTFUL_API_TOKEN environment variable is not set.
|
|
|
|
Set it and retry:
|
|
export PRINTFUL_API_TOKEN="your-token"
|
|
mix generate_mockups --provider printful
|
|
""")
|
|
|
|
_token ->
|
|
results =
|
|
PrintfulGenerator.generate_all(
|
|
mockups: do_mockups,
|
|
products: do_products,
|
|
cleanup: cleanup
|
|
)
|
|
|
|
mockup_ok = Enum.count(results.mockups, &match?({:ok, _, _}, &1))
|
|
mockup_fail = Enum.count(results.mockups, &match?({:error, _, _}, &1))
|
|
product_ok = Enum.count(results.products, &match?({:ok, _, _}, &1))
|
|
product_fail = Enum.count(results.products, &match?({:error, _, _}, &1))
|
|
|
|
Mix.shell().info("""
|
|
|
|
Summary:
|
|
Mockups: #{mockup_ok} succeeded, #{mockup_fail} failed
|
|
Products: #{product_ok} succeeded, #{product_fail} failed
|
|
""")
|
|
|
|
if mockup_fail + product_fail > 0 do
|
|
Mix.shell().error("Some operations failed. Check the output above for details.")
|
|
end
|
|
end
|
|
end
|
|
end
|