Files
shpm/scripts/amazon_sync.py
T
nmemmertandClaude Sonnet 4.6 4ff5a6c8ca feat: photo sync (iCloud + Amazon import), larger gear icon, Luma branding
- Settings: iCloud Photos section with icloudpd-backed sync — Apple ID,
  password, download path, cookie dir, live log output via SSE
- Settings: Amazon Photos one-time import — path to extracted data export,
  Add & Index button wires it directly into Watch Folders
- scripts/amazon_sync.py: stdlib-only downloader using Netscape cookie file
- server: POST /api/sync/icloud, /api/sync/amazon, GET /api/sync/stream
  (SSE log streaming matching ingest stream pattern)
- db: icloud/amazon setting keys; getSettings handles bool + string fields
- Gear icon: fontSize 16→20, button 38×38, color #555→#888
- favicon.svg: 6-blade aperture icon in Luma blue
- Browser tab title: shpm → Luma
- Dockerfile: installs icloudpd via pip3, copies scripts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 09:30:43 -04:00

135 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Download new photos from Amazon Photos using browser-exported cookies.
Usage:
amazon_sync.py <cookies.txt> <dest_dir>
cookies.txt must be in Netscape/Mozilla format. Export it from your browser
using an extension like "Get cookies.txt LOCALLY" while logged into amazon.com.
"""
import sys
import json
import time
import pathlib
import http.cookiejar
import urllib.request
import urllib.parse
import urllib.error
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
"Accept": "application/json",
}
def build_opener(cookie_file):
jar = http.cookiejar.MozillaCookieJar()
try:
jar.load(cookie_file, ignore_discard=True, ignore_expires=True)
except FileNotFoundError:
print(f"Error: cookie file not found: {cookie_file}", file=sys.stderr, flush=True)
sys.exit(1)
return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
def api_get(opener, url):
req = urllib.request.Request(url, headers=HEADERS)
with opener.open(req, timeout=30) as r:
return json.loads(r.read().decode())
def list_photos(opener, offset=0, limit=200):
params = urllib.parse.urlencode({
"filters": "kind:PHOTOS and contentProperties.contentType:(image*)",
"sort": '["modifiedDate DESC"]',
"limit": limit,
"offset": offset,
"asset": "ALL",
"tempLink": "false",
})
return api_get(opener, f"https://www.amazon.com/drive/v1/nodes?{params}")
def download_node(opener, node_id, dest_path):
url = f"https://www.amazon.com/drive/v1/nodes/{node_id}/content"
req = urllib.request.Request(url, headers=HEADERS)
dest_path.parent.mkdir(parents=True, exist_ok=True)
with opener.open(req, timeout=120) as r, open(dest_path, "wb") as f:
while True:
chunk = r.read(65536)
if not chunk:
break
f.write(chunk)
def main():
if len(sys.argv) < 3:
print("Usage: amazon_sync.py <cookies.txt> <dest_dir>", file=sys.stderr, flush=True)
sys.exit(1)
cookie_file = sys.argv[1]
dest_dir = pathlib.Path(sys.argv[2])
dest_dir.mkdir(parents=True, exist_ok=True)
opener = build_opener(cookie_file)
downloaded = 0
skipped = 0
offset = 0
print(f"Destination: {dest_dir}", flush=True)
while True:
print(f"Fetching photo list (offset={offset})…", flush=True)
try:
data = list_photos(opener, offset=offset)
except urllib.error.HTTPError as e:
if e.code == 401:
print(
"Authentication failed. Re-export your cookies from a logged-in amazon.com session.",
file=sys.stderr, flush=True,
)
else:
print(f"HTTP {e.code}: {e.reason}", file=sys.stderr, flush=True)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr, flush=True)
sys.exit(1)
nodes = data.get("data", [])
total_count = data.get("count", 0)
if not nodes:
break
for node in nodes:
name = node.get("name") or f"{node['id']}.jpg"
dest = dest_dir / name
if dest.exists():
skipped += 1
continue
print(f" Downloading {name}", flush=True)
try:
download_node(opener, node["id"], dest)
downloaded += 1
time.sleep(0.1)
except Exception as e:
print(f" Error: {e}", file=sys.stderr, flush=True)
offset += len(nodes)
if offset >= total_count:
break
time.sleep(0.5)
print(f"\nDone — {downloaded} downloaded, {skipped} already present.", flush=True)
if __name__ == "__main__":
main()