--- name: photo-archive description: Move years of photos out of iCloud Photos, Google Photos and loose drives into one dated folder archive (Year/Months, Year/Trips), prove every file reached Google Drive (or any cloud) by name and exact size, and only then free the source - with fingerprinted, user-approved, reversible deletes. Use for "get my old photos out of iCloud", "my iCloud/Google storage is full", "back up and clean my Google Photos", "check my upload to Drive and tell me what I can delete", "sort this old hard drive of photos into my archive". --- # photo-archive One pipeline for every source: **index -> download/extract -> sort into the archive layout -> upload (user) -> verify on the cloud -> user approves -> delete at the source (recoverable) -> re-verify.** Learned on a real ~26,000-item iCloud + 12,000-item Google Photos + 3,400-file SSD clean-up; every rule below exists because skipping it nearly lost something. ## Archive layout (default; keep whatever layout the user already has) ``` \\Months\\ everyday photos (2023\Months\04_Apr) \\Trips\\ a trip's date range (2023\Trips\2023-12_Lisbon; year = start year) \_undated\ no capture date anywhere \_log\ logs (paths, dates) - keep locally, do not upload ``` Trips come from `trips.csv` (`Name,Start,End`, inclusive local dates; see `templates/trips.example.csv`). `analyse_index.py` proposes candidates from GPS/timezones; the user confirms them. A trip spanning New Year stays whole in its start year. Users often rearrange folders on the cloud side afterwards - verify by folder mapping, never assume. ## Non-negotiable safety rules (details: reference/safe-deletion.md) 1. **Never delete before the copy is verified** on the destination by name + exact size, per folder, with misses re-checked by a title search. A sort log or "upload finished" message is not proof. 2. **Deletes need an explicit user OK for the exact count**, and go to a recoverable bin (iCloud Recently Deleted, Google Photos Bin, Drive trash, Windows Recycle Bin). Test with ONE item first, confirm it, then the rest. 3. **Protected by default**: the iCloud Hidden album (never download, move or delete), the user's own albums, shared albums (deleting there removes it for everyone - ask), anything the user calls out. Phone-only deletes are the user's job. Borrowed/shared drives: write only inside the user's own folder. 4. **The user types passwords and 2FA codes themselves** (in their own terminal or the browser), never in chat. Don't send GPS, emails or library metadata to any external service. 5. Local media is never deleted by the agent: it moves files, the user deletes after verification. 6. Re-verify right before any delete: the source must not have changed since the check (same file count and sizes). ## Pick the route (scripts in `scripts/`, run Python as `python -I`) | Source | Steps | Reference | |---|---|---| | iCloud Photos | `icloud_index.js` -> `analyse_index.py` (size it, trips) -> per phase `icloud_urls.js` -> `direct_download.py` -> `sort_archive.py` | reference/icloud.md | | Google Photos | Google Takeout zips -> `takeout_survey.py` -> `takeout_sort.py` (skips what iCloud already gave) | reference/google-photos.md | | Old drive / SSD / zips / phone backup | `media_dates.py` -> `match_saved.py` (what is NOT yet archived) -> `place_plan.py` | reference/loose-dumps.md | | Verify upload | `drive_check.py manifest` + cloud listings -> `drive_check.py check` | reference/drive-verification.md | | Free iCloud | `icloud_delete.js` DRY -> `build_delete_list.py` (vs verified backup) -> user OK -> LIMIT=1 -> all | reference/safe-deletion.md | | Free Google Photos | `gphotos.js` (list/info) -> `gphotos_match.py` (vs Takeout) -> user OK -> `gpk.trash` 1 -> all | reference/google-photos.md | | Tidy up | `remove_empty_dirs.py ` (dry run, then `--apply`) | - | Work in **phases** (a year, a quarter, a trip) so each phase is downloaded, sorted, checked and reported before the next: big single runs get throttled, and a small phase is easy to re-verify. ## Browser-side scripts iCloud and Google Photos are read through the user's own signed-in browser tab (Playwright MCP `browser_evaluate`, or paste into DevTools). The snippets are self-contained async functions; edit the `EDIT ME` block, run, save the returned JSON to a file (Playwright: `filename` option; it can only write inside the project folder) and feed the file to the Python step. These are the web apps' own internal calls: unofficial, may change; if a call returns an error object, stop and re-inspect instead of guessing. ## Status discipline Keep a `STATUS.md` (what is sorted, uploaded, verified, deleted, still open) and a dated `CHANGELOG.md` entry for every session that moves, uploads or deletes anything; keep every log CSV - they are the undo information. ## Report to the user Counts per folder, GB, what was verified and how (name+size, per folder), what is missing and why, what is safe to delete (exact paths/counts) and what is NOT, and the next action that is theirs (upload X, approve Y, empty bin Z).