I lost about two hours of upload work last month because of a filename. Not a rejected file, not a corrupt zip. A filename. I had a pack of 40 vectors ready for Adobe Stock and Vecteezy, ran my batch twice in one afternoon, and the second run posted duplicates because my filename scheme had no way to tell the tracker what was already done. That was the day I stopped treating filenames as decoration and started treating them as data.
Here is what I learned, written down so you skip the two hours.
The filename is not just a filename
On both platforms, the filename field does more work than people assume. When you build a CSV for Adobe Stock, the script has to match each row to a file on disk. Adobe's upload CSV format is 5 columns, and the first column is the filename. If that string does not exactly match the file you uploaded, the metadata row attaches to nothing. The asset sits there untitled, untitled assets get rejected in review, and you find out days later.
Vecteezy's CSV is 4 columns and the same logic applies. The filename is the join key between your CSV and your actual upload. Get it wrong and the whole row is dead weight.
So the first rule of my pipeline: the filename must be stable, unique, and identical across every platform CSV I generate. One canonical name per asset, generated once, never hand-edited.
What a good filename contains
My scheme is simple: pack-name_asset-number.svg, all lowercase, no spaces. Two reasons.
First, make_metadata.py has a fallback where, if you do not supply a titles file, it takes the filename, title-cases it, and uses it as the title. That is fine for a first pass but it produces titles like "Botanical Set 003", which are weak. The filename should carry the pack name so even the fallback title has some search value. Then you replace it with real titles before you submit. I keep a titles.json per pack and pass it with --titles.
Second, the asset number is what upload_tracker.py keys on. When I run upload_tracker.py done "botanical-set_012.svg", the ledger records that exact string. If I regenerate the pack and the filenames shift even slightly, the tracker sees them as new assets and a re-run double-posts. Double-posting is the fastest way I know to get an account flagged, so this is not a cosmetic concern.
The headers are unforgiving, so stop typing them
All three channels I upload to reject a wrong CSV header instantly. Adobe wants its exact 5-column header, Vecteezy wants its exact 4-column header, Dreamstime wants 15 columns. No partial credit, no helpful error message pointing at which column you broke.
I used to keep three template spreadsheets. That lasted until I edited one cell in a header row by accident and burned an upload cycle. Now make_metadata.py writes the header the platform demands and nothing else. I never touch a header by hand again.
The workflow is one command per platform:
python make_metadata.py ./output --platform adobe --out metadata.csv --ext .jpg
python make_metadata.py ./output --platform vecteezy --out vecteezy.csv --ext .jpg
Same folder, same files, three CSVs, one canonical filename per asset in each. The filename field is the only thing that has to agree across all of them, which is exactly why it needs to be generated, not typed.
The folder layout keeps filenames honest
A catalogue of packs turns into a junk drawer without fixed structure. Mine looks like this:
project/
assets/ raw output, one file per asset
output/ ready-to-submit files + metadata.csv
packs/ finished zips + covers
research/ notes and sources
scripts/ the tools
Assets never leave assets/ until they are final. Renames happen exactly once, when a file moves from assets/ to output/. After that the name is frozen. If I need to fix an asset, it goes back through the rename step so the tracker stays in sync. This one habit eliminated every duplicate and orphaned-metadata problem I had.
One mistake I made early and will not repeat: do not delete your research/ folder to "clean up". Those notes and logs are the raw material for the next pack. Disk is cheaper than redoing research.
A note on running this on cheap hardware
None of this needs a big machine. The whole pipeline, 39 finished products and about 856 MB of output, ran on a 1 vCPU, 2 GB RAM server with no GPU. The habits that make it work: one process at a time, never parallelize image jobs on one core, call gc.collect() after each asset, and measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss rather than guessing. For metadata work specifically, you need almost nothing. make_metadata.py and upload_tracker.py run on the Python standard library alone, no install at all.
The tool this became
Everything above is baked into the pipeline starter kit: the four scripts I actually use, including make_metadata.py with the exact Adobe, Vecteezy, and Dreamstime headers, and upload_tracker.py so a re-run can never double-post. It is the same tooling that packaged those 39 products, and it runs fine on the smallest server you can rent.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)