Spike Foundry
File 05
Status: Closed
File 05 · Recovered 2026.05.23

Your Downloads Folder Is a Crime Scene

A small Python script that tidies an overloaded Downloads folder without another subscription.

· 3 min read

My Downloads folder had 847 files in it. I know this because I counted. Actually, the script counted. I just stared at the number and felt something between shame and recognition.

PDFs from 2021. Screenshots of things I don't remember. Eleven different installers for the same app. A file called final_FINAL_v3_USE_THIS.zip.

So I looked at CleanMyMac. $90 a year. I looked at a file organizer on the Mac App Store. $42 one-time.

Then I wrote a script. 85 lines. It runs in about two seconds. It's been doing its job every night for a month.

Here it is.

What the Script Does

Plainly:

  • Scans your Downloads folder
  • Sorts files into folders by type: Images, Documents, Archives, Audio, Video, Installers, Other
  • Skips files younger than 7 days (because you probably still need them)
  • Moves the rest into a dated subfolder so nothing gets lost
  • Prints a one-line summary: how many files moved, and where

No config file. No daemon. One script. One command.

The Full Script

MIT licensed. Copy it. Run it. Break it. It's yours.

# tidy.py — organize your Downloads folder.
# Built by Spike Foundry. MIT license.
import os, shutil, time
from pathlib import Path
from datetime import datetime

DOWNLOADS = Path.home() / "Downloads"
AGE_DAYS  = 7                 # skip anything newer than this
ARCHIVE   = DOWNLOADS / "_archive" / datetime.now().strftime("%Y-%m-%d")

BUCKETS = {
    "Images":     {".png", ".jpg", ".jpeg", ".gif", ".webp", ".heic", ".svg"},
    "Documents":  {".pdf", ".doc", ".docx", ".txt", ".md", ".rtf", ".pages"},
    "Sheets":     {".csv", ".xls", ".xlsx", ".numbers"},
    "Archives":   {".zip", ".tar", ".gz", ".7z", ".rar"},
    "Audio":      {".mp3", ".wav", ".m4a", ".flac", ".aac"},
    "Video":      {".mp4", ".mov", ".avi", ".mkv", ".webm"},
    "Installers": {".dmg", ".pkg", ".exe", ".msi", ".deb", ".rpm"},
    "Code":       {".py", ".js", ".ts", ".go", ".rs", ".sh", ".json"},
}

def bucket_for(ext: str) -> str:
    for name, exts in BUCKETS.items():
        if ext.lower() in exts:
            return name
    return "Other"

def is_old_enough(p: Path) -> bool:
    age = time.time() - p.stat().st_mtime
    return age > AGE_DAYS * 86400

def tidy() -> None:
    if not DOWNLOADS.exists():
        print(f"no Downloads folder at {DOWNLOADS}")
        return

    moved = 0
    by_bucket: dict[str, int] = {}

    for item in DOWNLOADS.iterdir():
        if item.name.startswith("."):        continue
        if item.is_dir():                    continue
        if item.parent.name == "_archive":   continue
        if not is_old_enough(item):          continue

        bucket = bucket_for(item.suffix)
        dest_dir = ARCHIVE / bucket
        dest_dir.mkdir(parents=True, exist_ok=True)

        dest = dest_dir / item.name
        n = 1
        while dest.exists():
            dest = dest_dir / f"{item.stem} ({n}){item.suffix}"
            n += 1

        shutil.move(str(item), str(dest))
        moved += 1
        by_bucket[bucket] = by_bucket.get(bucket, 0) + 1

    if moved == 0:
        print("nothing to tidy.")
        return

    print(f"moved {moved} files into {ARCHIVE}")
    for name, count in sorted(by_bucket.items()):
        print(f"  {name:<11} {count}")

if __name__ == "__main__":
    tidy()

How the Three Key Parts Work

The buckets. A plain dictionary of extensions. Easy to extend. If you want a "Fonts" bucket, add one line.

The age check. The script ignores anything touched in the last 7 days. This is the single most important line. Real people need their recent downloads. Change AGE_DAYS if you want a longer or shorter window.

The dated archive. Files don't go to a random pile. They go to _archive/2026-02-03/, grouped by type. If you ever need to find something, you know exactly when it moved.

What It Looks Like When You Run It

$ python3 tidy.py
moved 412 files into /Users/operator/Downloads/_archive/2026-02-03
  Archives    38
  Code        11
  Documents   114
  Images      203
  Installers  7
  Other       25
  Sheets      4
  Video       10

412 files. Two seconds. Downloads folder now has 435 items that actually matter. I can finally see the Desktop behind the dock badge.

Why Pay $42 for Something That Takes 85 Lines?

A paid file organizer does roughly this. Plus a dashboard. Plus an onboarding screen. Plus an update notifier.

Some of that is worth money to some people. That's fine. But I'd rather own the 85 lines. I know exactly what it does. I can change it in three minutes. It will still work in ten years because it's just Python and a folder.

The math:

  • CleanMyMac: $90/year
  • File organizer app: $42 one-time
  • This script: free, yours, modifiable

How to Extend It

A few small variations I've shipped on top of this:

  • Schedule it. cron on Mac/Linux, Task Scheduler on Windows. Set it to 3 AM.
  • Log to a file. Add a with open(...) as f around the summary so you have a history.
  • Auto-delete after 90 days. One more loop over _archive that nukes anything older than a threshold.
  • Notify on Mac. One line using osascript -e 'display notification ...'.

What Is This Series

This is the first post in Scripts, Not Subscriptions. Every post ships a small, working, MIT-licensed script that replaces software people normally pay for every month.

Not because paid software is evil. Because a lot of it is charging a subscription for something you can own in 85 lines.

Run it. Break it. Change it. Send back the version you made better.

That's it. That was the script.

— next ticket up.


— The Operator