I lost an afternoon of submissions to a single header row.
It was a Tuesday. I had 40 JPGs ready for Adobe Stock, a CSV I'd built by hand the night before, and I pasted it into the uploader feeling pretty good about myself. The rejection was instant. Wrong column count. Adobe wants five columns in a specific order, and I had the keywords column in the wrong place. So I fixed it, resubmitted, then opened Vecteezy and realized their CSV is four columns in a different order. Then Dreamstime, which is fifteen columns. I rewrote the same metadata three times for the same files, by hand, in a spreadsheet, at midnight.
That was the night I decided I would never type a metadata row again. Here is what actually works.
One folder in, three CSVs out
The core insight is boring: the asset never changes, only the wrapper does. Every platform is asking for the same underlying data, title, keywords, category, just shaped differently. So the fix is a single script that reads one folder of files and emits the exact CSV each platform demands.
make_metadata.py does this. You point it at a folder:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv --ext .jpg
python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv --ext .jpg
Same folder, three outputs, each with the header that platform's uploader checks first. The review process rejects a wrong header before a human ever looks at your art, so getting the header right is not optional polish, it is the gate.
For titles and keywords, the script accepts a titles.json file. Without it, it title-cases the filename and uses that. That is fine for a first pass but replace it before you submit, filenames like floral_wreath_v2.jpg make terrible stock titles and worse keywords.
The whole thing is standard library only. No pip install, no API keys, no cloud account. That matters more than it sounds, because the tools you actually use on a deadline are the ones with zero setup friction.
The double-post problem nobody warns you about
Here is a failure mode I learned about the stressful way: run your upload batch twice, and you can flag your own account. Contributors get reviewed for duplicate submissions, and an automated pipeline makes duplicates trivially easy. You re-run a script, the tracker state is stale, and now the same file is in the queue twice.
So the second script is a dumb, boring ledger. upload_tracker.py remembers what you have already posted:
python upload_tracker.py pending items.txt # what is NOT done yet
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
It is not clever. That is the point. A sqlite database with an admin panel would be more impressive and less used. A text ledger you can read with cat is something you will actually trust before a submission batch.
The rest of the pipeline runs on almost nothing
I want to push back on the assumption that you need a real server for this. The entire pipeline that produced my current catalogue, 39 finished products, 856 MB of output, ran on 1 vCPU, 2 GB RAM, no GPU. The habits that make that possible:
- One process at a time. Never fan out image jobs in parallel on a single core, it just thrashes.
- Call
gc.collect()after each asset. On a 2 GB box, leaked memory between items is the difference between finishing and dying at asset 30. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrssgives you the real ceiling, average memory lies to you. - For heavy render steps, one file per subprocess, so memory actually returns to the OS.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 saves more time and RAM than any optimization I tried.
The metadata and tracking scripts fit in this budget with room to spare because they barely use any.
Packaging is a separate problem, and it deserves its own scripts
Two more things stalled me repeatedly, so they got scripts too.
Turning a Markdown document into a sellable PDF: md2pdf.py content.md product.pdf "My Product Title" gives you a styled PDF with real headings, tables, lists and links, using reportlab. One command, no fiddling with Word.
Bundling a product: pack_product.py zips the files and renders a 1280x720 cover and a 600x600 thumbnail. A zip with a real cover sells, a bare file does not, and making covers by hand for 39 products was never going to happen. This one needs Pillow, which is one of exactly two third-party dependencies in the whole kit.
The folder layout matters too, more than I expected:
project/
assets/ raw output
output/ ready-to-submit files + metadata.csv
packs/ finished zips + covers
research/ notes and sources
scripts/ the tools
Keep research/ forever. I once deleted old scrape logs and notes to "clean up" and then needed exactly that material for a follow-up product. I will not make that mistake twice.
Where to get it
Everything above is the packaging layer of a larger pipeline, roughly 19,000 lines of pipeline code backed by 300+ pages of research into how these platforms actually review and reject submissions. I pulled the four scripts that solved my real bottlenecks into a standalone kit, the same ones that packaged those 39 products. If you are also tired of hand-writing CSVs at midnight, it is here:
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)