--- name: sort-trip-photos description: Sort a messy trip dump (phone copies, shared/forwarded images, screenshots, videos, duplicates) into per-day folders D1..Dn by when each photo was really taken, with honest holding folders (unrelated, unsorted, duplicates) and logged, reversible EXIF date fixes. Use for "sort my trip photos", "add the new batch to the trip folders", "copy the trip off my iPhone and sort it". Windows (PowerShell 7 + Windows PowerShell 5.1 for the iPhone copy). --- # sort-trip-photos Sorts a trip dump into day folders by **when (and where) each photo really was**, not by what the filename or the save time says. Built and battle-tested on a ~3,200-file, 8-day trip arriving in several batches. ## Inputs (read the project's CLAUDE.md first; ask only for what it does not answer) - **TripRoot**: the folder holding the batch folders and the day folders (e.g. `D:\Photos\Japan2026`). - **D1** (first trip day, yyyy-mm-dd), **number of days**, home timezone, the route (cities/flights with times) if the user knows it - it is what lets you place undated images. - Which batch is NEW. Only the new batch is sorted; day folders already hold earlier work. Never re-survey them. - The user's rules for what counts as trip content (defaults below). ## Fast path (scripts in `scripts/` next to this file; work files in the session scratch dir, never in TripRoot) 0. **Phone copy (optional)** - `copy_from_iphone.ps1` (Windows PowerShell 5.1, `-STA`, run in the background). Filters by the phone's own Modified date, copies only missing/broken photos, **skips videos** (MTP hangs on them - see below) and lists what it skipped. Re-run = resume. 1. **Day folders** - `make_day_folders.ps1 -Root -D1 -Days ` (D1 gets `_Travel`, the last `_Return`). 2. **Survey** - `extract_meta.ps1 -Root -Base -Out meta.csv`. Adds `Day` and `Kind` (aae | exif | video | png | jpg-noexif). ~25 s per 2,600 files. Reports 0-byte files (failed copies). Videos need `ffprobe` on PATH for their date (else they fall back to file time). 3. **Duplicates** - `find_dups.ps1 -Root -Batch -Out verdicts.csv`: name match against the day folders **and** size -> MD5. (Name match matters: an earlier sort rewrote EXIF, so the same image hashes differently.) 4. **Judge undated files in bulk with contact sheets** - put `Kind=jpg-noexif` and `Kind=png` rows inside the trip window (sorted by `Mod`) into list CSVs and run `contact_sheet.ps1`: photos `-Cols 8 -Rows 5 -Tile 200 -TileH 260` (40 per sheet); screenshots `-Cols 9 -Rows 4 -Tile 165 -TileH 300`. Read the sheet images, then turn **numbers** into verdicts: `verdicts_from_index.ps1 -Index \sheet_index.csv -Out verdicts.csv -Unrelated "4,5,11-26" -Unsorted "10" -Append`. Anything not listed = trip content. Open a single image only for real doubts. Dated camera photos need no viewing. 5. **Plan** - `build_plan.ps1 -Root -Meta meta.csv -Out plan.csv -D1 -Verdicts verdicts.csv`. Plan rows must equal survey rows. Handles 0-byte -> `_sort_log\skipped_copy_failed`, out-of-window -> unrelated, AAE sidecars follow their photo, no-EXIF JPEGs get `NewExifDT` = save time. 6. **Dry run, then apply** - `apply_plan.ps1 -Root -PlanFile plan.csv -DryRun`, then without `-DryRun`. Writes EXIF losslessly (piexif in a throwaway venv), sets file times, moves, logs `_sort_log\{move_plan,move_log,metadata_changes}_.csv`. Locked files are retried, reported, not logged; re-run the same command to finish. 7. **Verify** - every plan row exists at its destination; the batch folder is empty (leave it in place unless the user wants empty folders removed). Report. ## Classification rules (defaults - the user's own rules in CLAUDE.md win) | Content | Destination | |---|---| | Trip content dated D1..Dn | `D#_DDMon` | | Not about the trip: chat/AI/social screenshots, memes, work/school, home-country photos others sent, anything before D1 or after Dn | `unrelated` | | Can't tell, or in-transit edge cases (e.g. 02:00 night bus after landing) | `unsorted` (user decides) | | Exact duplicate / same image already sorted | `duplicates_safe_to_delete_later` (never delete) | | 0-byte or wrong-content files from a failed phone copy | `_sort_log\skipped_copy_failed` | | `.AAE` sidecars | follow their photo; random-named ones follow their own date | Screenshots *about* the trip (tickets, receipts in the local currency, bookings, maps, hotel chats) stay in the day folder. ## Dates: what to trust - Phone camera `IMG_####.JPG/HEIC` EXIF = the phone's local clock (switches timezone on arrival). Trust it. - Videos: metadata `creation_time` is UTC; the file Modified time is the local clock. Use Modified. - **Random-named files without EXIF (`ABCD1234.JPG`) are shared/forwarded/saved images.** Modified = the SAVE time (home clock if saved at home). Day = save date is the working rule; the clock time is an estimate. Judge trip vs not by content. - Screenshots (PNG): Modified = real screenshot time; the status-bar clock in the image confirms it. - Never "fix" dates the user has confirmed. ## Metadata correction For undated/mis-dated trip photos placed in a day: write `DateTimeOriginal` = verified **date** + **estimated clock time**, keep their order, set file Created/Modified to match, and log old -> new in `_sort_log\metadata_changes_*.csv`. Never touch EXIF of already-dated files. PNG/video: file times only. ## iPhone over MTP (Windows "This PC > Apple iPhone") - Unlock the phone, tap Trust; if it is not listed under This PC enumeration fails (the script stops and says so). - Items show without extension; the Type column gives it. "Modified" = phone date -> filter BEFORE copying. - Photos/PNG/AAE copy at ~1 s each. **Videos come back 0-byte and `CopyHere` then hangs for good** (a timeout in the same process cannot interrupt it). Skip them; on the phone set Settings > Photos > Transfer to Mac or PC = Keep Originals, or copy videos in Explorer / via iCloud. - An interrupted copy leaves 0-byte files and sometimes a file with ANOTHER photo's bytes. Compare sizes with the phone (+-3%) and re-copy through a temp folder (the script does). ## Pitfalls (each cost real time once) - `Import-Csv` + `[datetime]` swaps day/month on d/M locales. Keep ISO strings; `ParseExact` with InvariantCulture. - PowerShell variables are case-insensitive: a local `$meta` silently overwrote the `-Meta` parameter. `$Matches` set inside `Where-Object` is not visible outside it. - Batch folders can be nested (`202610_a\202610_a`): `Folder` is relative to TripRoot. - Contact-sheet numbers are positions in that sheet's list - always map through `sheet_index.csv`. - Run Python with `python -I` on photo folders. ## Report Per-folder counts; which dates were corrected (and which clock times are estimates); what is in `unsorted` and why; judgement calls the user may want to overrule; duplicates and what they duplicate; skipped/failed phone copies; confirmation the batch folder is empty. Add new trip facts (timeline anchors) to the project's CLAUDE.md.