DEV Community

Cover image for How An Upload Tracker And Three Platform CSVs Kept My Stock Portfolio From Falling Apart

How An Upload Tracker And Three Platform CSVs Kept My Stock Portfolio From Falling Apart

Last March I almost lost a week of work to a copy-paste mistake. I had 40 finished assets ready for Adobe Stock, and I pasted a Vecteezy CSV header into the Adobe upload by accident. The whole batch came back rejected in under a minute. Not because the art was bad. Because the header was wrong.

That was the moment I stopped trusting my memory and started writing everything down. This post is about the two boring tools that saved my portfolio: a plain-text upload tracker, and one script that writes three different platform CSVs with the exact headers each platform demands.

Platform CSVs are unforgiving, and that is fair

Every stock platform has a fixed upload CSV format, and their ingest systems check the header row before they look at a single file.

Adobe Stock wants a 5-column CSV: filename, title, keywords, category, releases. Vecteezy wants 4 columns: filename, title, keywords, category. Dreamstime is wider, 15 columns, because it carries more metadata per asset. If your header does not match their expected columns, the upload fails before review even starts. If your columns are right but the content is malformed, for example keywords separated wrong or a category number out of range, you get rejected at review and wait days to find out.

My first mistake was keeping one "master" CSV and hand-editing it per platform. That works for ten files. At forty files across three platforms it produced exactly the failure I described above. So I wrote make_metadata.py. You point it at a folder of files, name the platform, and it writes the CSV with the exact header that platform expects and nothing else:

python make_metadata.py ./my-assets --platform adobe      --out metadata.csv
python make_metadata.py ./my-assets --platform vecteezy   --out vecteezy.csv
python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv
Enter fullscreen mode Exit fullscreen mode

It takes an optional titles.json if you have real titles and keywords per asset. Without it, the filename gets title-cased and used as the title. That is fine for a first pass, but you should replace it before submitting, because reviewers do notice generic titles and it affects acceptance.

The important design decision: the script writes the header the platform demands and no extra columns. I was tempted to add "helpful" columns like date or collection. Do not. Extra columns are how you get an instant reject.

Double-posting is the fastest way to get flagged

Here is the failure nobody warns you about. You run a batch upload. Your connection drops halfway. You rerun the script. Now you have posted 40 assets twice, or worse, the same asset under two filenames because your generation step renumbered things.

Platforms treat duplicate submissions as a quality signal, and repeated duplicates get accounts flagged. The fix is embarrassingly simple. upload_tracker.py is a ledger, a plain file that remembers what you 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
Enter fullscreen mode Exit fullscreen mode

pending is the command that matters. My upload script only ever reads from the pending list, so a re-run physically cannot double-post. It uses the Python standard library only, no install, no database. The whole thing is less clever than it sounds, which is why it works.

The folder layout stopped my catalogue from becoming a junk drawer

Once you have 39 finished products, filenames alone stop carrying enough information. This is the layout everything assumes now:

project/
  assets/      raw output, one file per asset
  output/      ready-to-submit files + metadata CSVs
  packs/       finished product zips + covers
  research/    notes, scrape logs, sources
  scripts/     the tools
Enter fullscreen mode Exit fullscreen mode

One rule I learned the hard way: keep research/ forever. I deleted my notes once to "clean up" and lost the source material for three product ideas. Scrape logs and research notes are raw material, not clutter.

All of this ran on a tiny server, and that shaped every choice

The whole pipeline runs on 1 vCPU and 2 GB RAM, no GPU. That constraint forced habits worth keeping even on bigger machines:

  • One process at a time. Never fan out image jobs in parallel on a single core.
  • Call gc.collect() after each asset to free memory between items.
  • Measure peak RSS, not average, with resource.getrusage(RUSAGE_SELF).ru_maxrss. That is your real ceiling.
  • One file per subprocess for heavy render steps, so memory returns to the OS.
  • Inspect previews at 60 dpi instead of rendering at 300 when you only need to check composition.

The metadata and tracking scripts fit this easily because they touch text files only. No third-party packages at all. The rest of my pipeline uses exactly two: Pillow for product covers, reportlab for PDFs.

What this added up to

The pipeline that these two scripts anchor has packaged 39 products, about 856 MB of output, from roughly 19,000 lines of pipeline code, backed by over 300 pages of research behind the rules the scripts enforce. The rules in the CSV script are not guesses. They are the headers the platforms actually reject you for getting wrong.

I put the working scripts together as the compliance metadata kit. It is the same make_metadata.py covering all three platforms, the same upload_tracker.py, plus the packaging tools, with the folder layout and the small-server habits documented. If you are building a similar pipeline, it is the tool built from this work, not a course about it.

Get compliance metadata kit


Building digital products? Free guides & kits for digital product sellers.

Top comments (0)