add Printful mockup generator and post-sync angle enrichment
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>
This commit is contained in:
@@ -1,32 +1,23 @@
|
||||
defmodule Mix.Tasks.GenerateMockups do
|
||||
@moduledoc """
|
||||
Generates product mockups using the Printify API.
|
||||
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 Printify,
|
||||
creates products, and downloads the generated mockups.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Printify account with API access
|
||||
- The PRINTIFY_API_TOKEN environment variable must be set
|
||||
sample content. It downloads artwork from Unsplash, uploads it to the
|
||||
print-on-demand provider, creates products, and downloads the generated mockups.
|
||||
|
||||
## Usage
|
||||
|
||||
# Generate mockups (keeps products in Printify)
|
||||
# Printify (default)
|
||||
mix generate_mockups
|
||||
|
||||
# Delete existing products first, then generate fresh ones
|
||||
mix generate_mockups --cleanup
|
||||
mix generate_mockups --replace
|
||||
|
||||
# Generate mockups and delete products afterwards
|
||||
mix generate_mockups --cleanup
|
||||
|
||||
# Search for available blueprints
|
||||
mix generate_mockups --search "poster"
|
||||
|
||||
# List all blueprints
|
||||
mix generate_mockups --list-blueprints
|
||||
# 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
|
||||
|
||||
@@ -35,13 +26,13 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
|
||||
use Mix.Task
|
||||
|
||||
alias SimpleshopTheme.Mockups.Generator, as: MockupGenerator
|
||||
alias SimpleshopTheme.Mockups.Generator, as: PrintifyGenerator
|
||||
alias SimpleshopTheme.Mockups.PrintfulGenerator
|
||||
|
||||
@shortdoc "Generates product mockups using Printify API"
|
||||
@shortdoc "Generates product mockups using Printify or Printful API"
|
||||
|
||||
@impl Mix.Task
|
||||
def run(args) do
|
||||
# Start required applications
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
{opts, _, _} =
|
||||
@@ -51,21 +42,30 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
replace: :boolean,
|
||||
search: :string,
|
||||
list_blueprints: :boolean,
|
||||
help: :boolean
|
||||
help: :boolean,
|
||||
provider: :string,
|
||||
mockups_only: :boolean,
|
||||
products_only: :boolean
|
||||
],
|
||||
aliases: [
|
||||
c: :cleanup,
|
||||
r: :replace,
|
||||
s: :search,
|
||||
l: :list_blueprints,
|
||||
h: :help
|
||||
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()
|
||||
|
||||
@@ -73,48 +73,51 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
search_blueprints(opts[:search])
|
||||
|
||||
true ->
|
||||
generate_mockups(opts)
|
||||
run_printify(opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp print_help do
|
||||
Mix.shell().info("""
|
||||
|
||||
Printify Mockup Generator
|
||||
=========================
|
||||
Mockup Generator
|
||||
================
|
||||
|
||||
Generates product mockups using the Printify API.
|
||||
Generates product mockups using Printify or Printful APIs.
|
||||
|
||||
Usage:
|
||||
mix generate_mockups [options]
|
||||
|
||||
Options:
|
||||
--replace, -r Delete ALL existing products from Printify before generating (with confirmation)
|
||||
--cleanup, -c Delete created products from Printify after downloading mockups
|
||||
--search, -s TERM Search for blueprints by name
|
||||
--list-blueprints List all available blueprint IDs and names
|
||||
--help, -h Show this help message
|
||||
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. Your Printify API token.
|
||||
PRINTIFY_API_TOKEN Required for Printify provider
|
||||
PRINTFUL_API_TOKEN Required for Printful provider
|
||||
|
||||
Examples:
|
||||
# Generate all mockups
|
||||
export PRINTIFY_API_TOKEN="your-token"
|
||||
mix generate_mockups
|
||||
|
||||
# Generate and cleanup
|
||||
mix generate_mockups --cleanup
|
||||
|
||||
# Find blueprint IDs
|
||||
mix generate_mockups --search "poster"
|
||||
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 MockupGenerator.list_blueprints() do
|
||||
case PrintifyGenerator.list_blueprints() do
|
||||
blueprints when is_list(blueprints) ->
|
||||
Mix.shell().info("\nAvailable Blueprints:\n")
|
||||
|
||||
@@ -133,7 +136,7 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
defp search_blueprints(term) do
|
||||
Mix.shell().info("Searching for blueprints matching '#{term}'...")
|
||||
|
||||
case MockupGenerator.search_blueprints(term) do
|
||||
case PrintifyGenerator.search_blueprints(term) do
|
||||
results when is_list(results) ->
|
||||
if results == [] do
|
||||
Mix.shell().info("No blueprints found matching '#{term}'")
|
||||
@@ -153,7 +156,11 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_mockups(opts) do
|
||||
# ===========================================================================
|
||||
# Printify
|
||||
# ===========================================================================
|
||||
|
||||
defp run_printify(opts) do
|
||||
cleanup = Keyword.get(opts, :cleanup, false)
|
||||
replace = Keyword.get(opts, :replace, false)
|
||||
|
||||
@@ -168,39 +175,27 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
|
||||
""")
|
||||
|
||||
# Verify API token is set
|
||||
case System.get_env("PRINTIFY_API_TOKEN") do
|
||||
nil ->
|
||||
Mix.shell().error("""
|
||||
Error: PRINTIFY_API_TOKEN environment variable is not set.
|
||||
|
||||
To get your API token:
|
||||
1. Log in to Printify
|
||||
2. Go to Settings > API tokens
|
||||
3. Create a new token with required permissions
|
||||
|
||||
Then run:
|
||||
Set it and retry:
|
||||
export PRINTIFY_API_TOKEN="your-token"
|
||||
mix generate_mockups
|
||||
""")
|
||||
|
||||
_token ->
|
||||
if replace, do: purge_existing_products()
|
||||
if replace, do: purge_existing_printify_products()
|
||||
|
||||
results = MockupGenerator.generate_all(cleanup: cleanup)
|
||||
results = PrintifyGenerator.generate_all(cleanup: cleanup)
|
||||
|
||||
# Summary
|
||||
successful = Enum.count(results, &match?({:ok, _, _, _}, &1))
|
||||
failed = Enum.count(results, &match?({:error, _, _}, &1))
|
||||
|
||||
Mix.shell().info("""
|
||||
|
||||
═══════════════════════════════════════════
|
||||
Summary
|
||||
═══════════════════════════════════════════
|
||||
Successful: #{successful}
|
||||
Failed: #{failed}
|
||||
═══════════════════════════════════════════
|
||||
Summary: #{successful} succeeded, #{failed} failed
|
||||
""")
|
||||
|
||||
if failed > 0 do
|
||||
@@ -211,7 +206,7 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
end
|
||||
end
|
||||
|
||||
defp purge_existing_products do
|
||||
defp purge_existing_printify_products do
|
||||
alias SimpleshopTheme.Clients.Printify, as: Client
|
||||
|
||||
Mix.shell().info("Fetching existing products...")
|
||||
@@ -226,11 +221,71 @@ defmodule Mix.Tasks.GenerateMockups do
|
||||
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)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user