diff --git a/scripts/iphone-photo-inbox/README.md b/scripts/iphone-photo-inbox/README.md index 0619ad9..f5425e8 100644 --- a/scripts/iphone-photo-inbox/README.md +++ b/scripts/iphone-photo-inbox/README.md @@ -30,6 +30,54 @@ IPHONE_PHOTO_OUTPUT_DIR="$HOME/Pictures/iPhone Inbox" \ python3 scripts/iphone-photo-inbox/receiver.py ``` +Useful receive modes: + +```bash +# Show a macOS notification when a photo arrives. +IPHONE_PHOTO_NOTIFY=1 IPHONE_PHOTO_TOKEN="choose-a-token" python3 scripts/iphone-photo-inbox/receiver.py + +# Reveal each received photo in Finder. +IPHONE_PHOTO_REVEAL=1 IPHONE_PHOTO_TOKEN="choose-a-token" python3 scripts/iphone-photo-inbox/receiver.py + +# Copy each received photo file to the Mac clipboard for pasting into apps. +IPHONE_PHOTO_COPY=1 IPHONE_PHOTO_TOKEN="choose-a-token" python3 scripts/iphone-photo-inbox/receiver.py +``` + +These can be combined: + +```bash +IPHONE_PHOTO_NOTIFY=1 \ +IPHONE_PHOTO_COPY=1 \ +IPHONE_PHOTO_OUTPUT_DIR="$HOME/Pictures/iPhone Inbox" \ +IPHONE_PHOTO_TOKEN="choose-a-token" \ +python3 scripts/iphone-photo-inbox/receiver.py +``` + +The flags also accept `true`, `yes`, or `on`: + +```bash +IPHONE_PHOTO_NOTIFY=true IPHONE_PHOTO_COPY=true ... +``` + +When these modes are active, the receiver startup log should include: + +```text +notifications enabled +clipboard copy enabled +``` + +After each upload, it should also print: + +```text +notification sent +copied file to clipboard +``` + +If those startup lines do not appear, the environment variables were not passed +to the running receiver process. If the startup lines appear but the post-upload +lines do not, check the printed `macOS action failed:` error and macOS privacy +permissions for Terminal/Codex automation and notifications. + Find the Mac IP address on the current network: ```bash diff --git a/scripts/iphone-photo-inbox/receiver.py b/scripts/iphone-photo-inbox/receiver.py index 5bf449f..858af5b 100755 --- a/scripts/iphone-photo-inbox/receiver.py +++ b/scripts/iphone-photo-inbox/receiver.py @@ -5,7 +5,9 @@ from __future__ import annotations import argparse import datetime as dt +import json import os +import subprocess from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path @@ -33,6 +35,44 @@ def looks_like_jpeg(data: bytes) -> bool: return data.startswith(b"\xff\xd8") and data.endswith(b"\xff\xd9") +def env_flag(name: str) -> bool: + return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} + + +def run_macos_action(command: list[str]) -> bool: + try: + result = subprocess.run(command, check=False, capture_output=True, text=True) + except OSError as error: + print(f"macOS action failed: {error}", flush=True) + return False + + if result.returncode != 0: + error = result.stderr.strip() or result.stdout.strip() or f"exit {result.returncode}" + print(f"macOS action failed: {error}", flush=True) + return False + + return True + + +def notify(title: str, message: str) -> bool: + script = f"display notification {json.dumps(message)} with title {json.dumps(title)}" + return run_macos_action(["osascript", "-e", script]) + + +def reveal_in_finder(path: Path) -> bool: + return run_macos_action(["open", "-R", str(path)]) + + +def copy_file_to_clipboard(path: Path) -> bool: + script = f""" +set theFile to POSIX file {json.dumps(str(path))} as alias +tell application "Finder" + set the clipboard to {{theFile}} +end tell +""" + return run_macos_action(["osascript", "-e", script]) + + class UploadHandler(BaseHTTPRequestHandler): server_version = "iPhonePhotoInbox/1.0" @@ -81,6 +121,16 @@ class UploadHandler(BaseHTTPRequestHandler): temp_path = Path(tmp.name) temp_path.replace(target) + if self.server.notify_on_upload: + if notify("iPhone Photo Inbox", target.name): + print("notification sent", flush=True) + if self.server.reveal_on_upload: + if reveal_in_finder(target): + print("revealed in Finder", flush=True) + if self.server.copy_on_upload: + if copy_file_to_clipboard(target): + print("copied file to clipboard", flush=True) + self.send_text(HTTPStatus.CREATED, f"{target}\n") print(f"saved {target}", flush=True) @@ -104,11 +154,17 @@ class UploadServer(ThreadingHTTPServer): output_dir: Path, upload_token: str, max_bytes: int, + notify_on_upload: bool, + reveal_on_upload: bool, + copy_on_upload: bool, ) -> None: super().__init__(server_address, handler_class) self.output_dir = output_dir self.upload_token = upload_token self.max_bytes = max_bytes + self.notify_on_upload = notify_on_upload + self.reveal_on_upload = reveal_on_upload + self.copy_on_upload = copy_on_upload def parse_args() -> argparse.Namespace: @@ -118,6 +174,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--output-dir", type=Path, default=Path(os.getenv("IPHONE_PHOTO_OUTPUT_DIR", DEFAULT_OUTPUT_DIR))) parser.add_argument("--token", default=os.getenv("IPHONE_PHOTO_TOKEN", "")) parser.add_argument("--max-mb", type=int, default=int(os.getenv("IPHONE_PHOTO_MAX_MB", "30"))) + parser.add_argument("--notify", action="store_true", default=env_flag("IPHONE_PHOTO_NOTIFY")) + parser.add_argument("--reveal", action="store_true", default=env_flag("IPHONE_PHOTO_REVEAL")) + parser.add_argument("--copy", action="store_true", default=env_flag("IPHONE_PHOTO_COPY")) return parser.parse_args() @@ -129,9 +188,18 @@ def main() -> None: args.output_dir.expanduser().resolve(), args.token, args.max_mb * 1024 * 1024, + args.notify, + args.reveal, + args.copy, ) print(f"listening on http://{args.host}:{args.port}/upload", flush=True) print(f"saving JPEGs to {server.output_dir}", flush=True) + if args.notify: + print("notifications enabled", flush=True) + if args.reveal: + print("Finder reveal enabled", flush=True) + if args.copy: + print("clipboard copy enabled", flush=True) if not args.token: print("warning: no token configured; anyone on this network can upload", flush=True) server.serve_forever()