Last March I ran my upload batch twice. The first run hung halfway through on a flaky connection, so I did what anyone does: I ran it again. Adobe accepted the duplicates without complaint. Vecteezy did not. I spent a Sunday afternoon deleting 60-some submissions one by one, and I got a warning email explaining that resubmitting identical content violates their contributor terms. That was the moment I stopped trusting memory and wrote a ledger.
This post is about the boring fix: a plain-text ledger and strict CSV headers. It is not exciting, but it is the difference between a pipeline you trust and one you babysit.
The CSV header is a gate, not a suggestion
All three stock platforms parse your upload CSV before anything else, and a wrong header means instant rejection. Not a warning. A rejection. The headers are different on each platform, and they are not close to each other:
- Adobe wants a 5-column header. Title, keywords, category, releases, and the file reference. Five columns, exact order.
- Vecteezy wants 4 columns. Fewer fields, but if you hand it Adobe's format it fails on the first row.
- Dreamstime wants 15 columns. Half of them can be empty, but all of them must exist in the right position.
My first attempt at this was one spreadsheet I edited by hand for each platform. That lasted about two weeks before I fat-fingered a column and lost a submission window. So make_metadata.py now writes all three formats from one folder of files. You point it at ./my-assets, pick the platform, and it emits the exact header that platform expects, nothing extra. Adobe gets 5 columns, Vecteezy gets 4, Dreamstime gets 15. No template drift, no hand editing.
One honest caveat: without a titles file, the script title-cases your filename and uses that as the title. That is fine for a first pass, but replace it with real titles and keywords before you submit. Reviewers notice keyword spam and filename titles, and a rejection costs you days of review queue time.
The ledger that makes double-posting impossible
The fix for my March failure is embarrassingly simple. upload_tracker.py keeps a plain text ledger of what you have already submitted. Three commands:
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
Before every batch, I run pending and feed its output to the uploader. If a run dies halfway, the ledger still knows which files finished. The re-run only touches what is left. There is no state to corrupt, no database to maintain, it is a text file you can read and back up with everything else.
Why does this matter beyond convenience? Because duplicate submissions are one of the fastest ways to get a contributor account flagged. Vecteezy rejected my duplicates outright, Adobe let them through and then held them in review. Either way, the reviewer sees a contributor who submitted the same asset twice, and that is not the impression you want when you are building volume.
The script uses the Python standard library alone. No pip install, no dependencies. If you only ever use the metadata builder and the tracker, you need nothing but Python 3.9 or newer.
One process at a time on a small box
The whole pipeline behind these scripts runs on a 1 vCPU, 2 GB RAM server with no GPU. That constraint shaped every habit in it:
- Never fan out image jobs in parallel on one core. Sequential is faster in practice because you stop thrashing.
- Call
gc.collect()after each asset so memory actually frees between items. - Measure peak RSS with
resource.getrusage(RUSAGE_SELF).ru_maxrss, not your average usage. The peak is what gets you OOM-killed. - Run heavy render steps one file per subprocess, so the OS reclaims the memory when the subprocess exits.
- Inspect previews at 60 dpi, render finals at 300. Inspecting at full resolution is the most common waste I see.
I learned the subprocess rule after a render step ate the box and the kernel killed my batch at asset 30 of 90. The ledger meant the re-run started at 31, which is the other reason I am loyal to it.
The folder layout that keeps a catalogue sane
Once you pass a few hundred assets, structure stops being optional. The layout this kit assumes:
project/
assets/ raw output, one file per asset
output/ ready-to-submit files + metadata.csv
packs/ finished product zips + covers
research/ notes, scrape logs, sources
scripts/ the tools
The one I would underline is research/. I deleted my early scrape logs once to "clean up" and then needed them two months later when I rebuilt a product line. Keep them forever. They are the raw material for the next product.
Where the ledger fits in the bigger picture
The tracker and metadata builder are the submission layer. Around them, the parent pipeline packs products: md2pdf.py turns a Markdown doc into a styled PDF via reportlab, and pack_product.py zips a product and renders a 1280x720 cover plus a 600x600 thumbnail with Pillow. Two third-party packages total, and only for those two scripts. The whole thing packaged 39 products, about 856 MB of output, on that same 2 GB box, backed by roughly 19,000 lines of pipeline code and 300+ pages of research behind the rules the scripts enforce.
None of that is clever. It is a header written correctly, a text file that remembers what shipped, and a machine that does one thing at a time. That combination is what let me stop re-checking submissions by hand.
I put the four scripts, the folder layout, and the RAM notes together as the pipeline starter kit, because those are the exact tools I use on every batch. If you are building your own pipeline, it will save you the duplicate-submission Sunday I lost.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)