Last February I lost an entire Saturday to a rejected upload. I had 42 JPEGs ready for Adobe Stock, a CSV I'd written by hand, and a review rejection that came back in under an hour. The reason was boring: my header had five columns in the wrong order. Not bad art, not bad keywords. A header row. That was the moment I stopped treating packaging as an afterthought and started writing scripts for it.
That batch eventually became 39 finished products, 856 MB of output, all processed on a 1 vCPU, 2 GB server with no GPU. Here is what actually made the difference.
The metadata header is a gate, not a suggestion
Every stock platform validates your CSV header before it even looks at your files. Adobe Stock wants a 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. Get one column name wrong or shuffle the order and the whole upload is rejected instantly, before a human ever sees your work.
So the first script I wrote, make_metadata.py, does exactly one thing: it walks a folder of files and writes the exact header each platform demands, nothing else. It takes a platform flag and an output path:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
You can pass --titles titles.json if you have real titles and keywords ready. Without it, the filename gets title-cased and used as the title. That is fine for a first pass, but replace it before you submit, because keyword quality is where the actual discoverability lives. The script uses only the Python standard library. No pip install, no framework. On a 2 GB box, that matters.
Double-posting is the fastest way to get flagged
The second failure I avoided only because I got lucky. I re-ran a batch script at 1 a.m. because I thought it had crashed. It hadn't. I nearly posted the same 40 files twice, which is exactly the kind of behavior that gets an account reviewed and flagged.
The fix is upload_tracker.py, a plain ledger. You mark an asset as done and it stays done:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
A re-run now physically cannot double-post, because the tracker checks the ledger first. It also uses the standard library alone. This is the least glamorous script in the set and the one I would keep if I could only keep one.
Turn a document into a product in one command
A lot of digital products are just documents: guides, checklists, pattern books, license explainers. The gap between "I have the content" and "I have a product" is formatting, and formatting by hand does not scale when you have 39 of them.
md2pdf.py takes a Markdown file with headings, tables, lists, and links and renders a clean, styled PDF:
python md2pdf.py content.md product.pdf "My Product Title"
It depends on exactly one third-party package, reportlab. That is it. Two packages across the whole kit, Pillow being the other one. I deliberately kept the dependency list that short because every extra package is another thing that can break on a small server.
Packaging: a zip with a real cover sells, a bare file does not
The last script, pack_product.py, bundles everything:
python pack_product.py my-product "My Product" "A short subtitle" 9
It writes the zip, a 1280×720 cover, and a 600×600 thumbnail. Covers go to a _system folder next to where you run it, and you can redirect that with --dir or the PACK_ROOT environment variable. I made that configurable after the third time I nearly committed gigabytes of covers into a git repo.
The folder layout and the RAM habits
None of the scripts work well without a fixed structure. This is the one the whole pipeline assumes:
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 my scrape logs and notes to "clean up" and immediately regretted it when I sat down to build the next product. That folder is raw material.
On the server side, the whole thing ran on 1 vCPU and 2 GB RAM, no GPU. The habits that made it possible are simple. One process at a time, never fan out image jobs in parallel on a single core. Call gc.collect() after each asset. Measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss, not average, because peak is what crashes you. Run heavy render steps one file per subprocess so memory actually returns to the OS. And render at the resolution you need, inspecting a preview at 60 dpi instead of 300.
What this added up to
The parent project behind these scripts is about 19,000 lines of pipeline code, backed by 300+ pages of research into the platform rules the scripts enforce. The four scripts here are just the packaging layer, the part that kept failing when I did it by hand. They validated on the smallest server I could rent, which means they will run on whatever you have.
I put the exact scripts, with the Adobe 5-column, Vecteezy 4-column, and Dreamstime 15-column headers already baked in, into a product called the compliance metadata kit. It is not a course and there is no upsell inside. It is the tooling from this work, with the quick start and install notes above included as documentation. If your pipeline keeps stalling at packaging the way mine did, it will save you the Saturday I lost.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)