# Media Sort Starter: free preview

**See where every photo and video would land before anything moves.** One read-only script and the starter taxonomy from the full kit.

## What it does

`sort_preview.py` walks a folder and counts how many photos and videos would land in each of your folders. It moves nothing, copies nothing and writes nothing. Files that match a private category are counted as "left alone" and never listed.

## Run it

1. Save the two files below.
2. Edit the folder names and words in `taxonomy.json` to match how you think about your media.
3. `python3 sort_preview.py ~/Downloads taxonomy.json`

Prompt for Grok Bot, Claude Code, Codex or Hermes:
> Run python3 sort_preview.py on ~/Downloads with taxonomy.json and tell me which folder is biggest and how many files land in _review. Do not open, move or list any files.

## The full kit adds

A plan you approve by typing a code at your own terminal (an agent cannot approve for you), duplicate detection by hash, copy before move, a byte-for-byte verify, undo, agent rules for Grok Bot, Claude Code, Codex and Hermes, and 13 tests. $14.

#### file: mini/sort_preview.py
~~~~python
#!/usr/bin/env python3
"""Media Sort Starter free sample: read-only preview. Counts what would land in each folder of your taxonomy.
Moves nothing, copies nothing, writes nothing. Python 3.8+.  Usage: python3 sort_preview.py FOLDER taxonomy.json"""
import json, os, sys
MEDIA = {".jpg",".jpeg",".png",".heic",".heif",".webp",".gif",".tif",".tiff",".dng",".mp4",".mov",".m4v",".avi",".mkv",".webm"}
def main(src, taxp):
    tax = json.load(open(taxp, encoding="utf-8")); counts = {}; off = 0
    for r, ds, fs in os.walk(src):
        ds[:] = [d for d in ds if not d.startswith(".")]
        for f in fs:
            if f.startswith(".") or os.path.splitext(f)[1].lower() not in MEDIA: continue
            rel = os.path.relpath(os.path.join(r, f), src).lower(); base = os.path.basename(rel); folder = os.path.dirname(rel)
            cat = "_review"
            for c in tax["categories"]:
                m = c.get("match", {})
                if any(w.lower() in base for w in m.get("name_words", [])) or any(w.lower() in folder for w in m.get("folder_words", [])):
                    cat = None if (c.get("private") and not c.get("enabled")) else c["name"]; break
            if cat is None: off += 1; continue
            counts[cat] = counts.get(cat, 0) + 1
    for k, v in sorted(counts.items()): print(f"{v:6}  {k}")
    print(f"{off:6}  left alone (private folder is off)")
    return counts, off
if __name__ == "__main__":
    if len(sys.argv) != 3: print(__doc__); sys.exit(2)
    main(sys.argv[1], sys.argv[2])
~~~~

#### file: taxonomy.json
~~~~json
{
 "about": "Edit the names and words to match how you think about your own media. First matching rule wins, top to bottom.",
 "categories": [
  {"name": "Private", "private": true, "enabled": false, "match": {"name_words": ["kids", "family", "home", "doctor", "passport", "id-card"], "folder_words": ["family", "personal", "private"]}},
  {"name": "Talking and performance", "private": false, "match": {"name_words": ["talk", "speech", "set", "live", "podcast", "interview"], "folder_words": ["shows", "talks"]}},
  {"name": "Training", "private": false, "match": {"name_words": ["gym", "run", "workout", "training", "match"], "folder_words": ["training", "sport"]}},
  {"name": "Food and places", "private": false, "match": {"name_words": ["food", "cafe", "dinner", "lunch", "restaurant"]}},
  {"name": "Work and business", "private": false, "match": {"name_words": ["event", "meeting", "booth", "client-demo", "conference", "whiteboard"], "folder_words": ["work"]}},
  {"name": "Screenshots and documents", "private": false, "match": {"name_words": ["screenshot", "screen shot", "scan", "receipt", "invoice"]}}
 ]
}
~~~~

Tested 2026-11-01 on Linux, Python 3.8 to 3.13: counts match, private files left alone, folder unchanged.
