feat: enhance iPhone photo inbox receiver with notification, reveal, and clipboard copy options

This commit is contained in:
2026-05-11 11:27:42 -06:00
parent e01b59c065
commit 97ef0be216
2 changed files with 116 additions and 0 deletions

View File

@@ -30,6 +30,54 @@ IPHONE_PHOTO_OUTPUT_DIR="$HOME/Pictures/iPhone Inbox" \
python3 scripts/iphone-photo-inbox/receiver.py 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: Find the Mac IP address on the current network:
```bash ```bash

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
import argparse import argparse
import datetime as dt import datetime as dt
import json
import os import os
import subprocess
from http import HTTPStatus from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path 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") 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): class UploadHandler(BaseHTTPRequestHandler):
server_version = "iPhonePhotoInbox/1.0" server_version = "iPhonePhotoInbox/1.0"
@@ -81,6 +121,16 @@ class UploadHandler(BaseHTTPRequestHandler):
temp_path = Path(tmp.name) temp_path = Path(tmp.name)
temp_path.replace(target) 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") self.send_text(HTTPStatus.CREATED, f"{target}\n")
print(f"saved {target}", flush=True) print(f"saved {target}", flush=True)
@@ -104,11 +154,17 @@ class UploadServer(ThreadingHTTPServer):
output_dir: Path, output_dir: Path,
upload_token: str, upload_token: str,
max_bytes: int, max_bytes: int,
notify_on_upload: bool,
reveal_on_upload: bool,
copy_on_upload: bool,
) -> None: ) -> None:
super().__init__(server_address, handler_class) super().__init__(server_address, handler_class)
self.output_dir = output_dir self.output_dir = output_dir
self.upload_token = upload_token self.upload_token = upload_token
self.max_bytes = max_bytes 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: 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("--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("--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("--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() return parser.parse_args()
@@ -129,9 +188,18 @@ def main() -> None:
args.output_dir.expanduser().resolve(), args.output_dir.expanduser().resolve(),
args.token, args.token,
args.max_mb * 1024 * 1024, 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"listening on http://{args.host}:{args.port}/upload", flush=True)
print(f"saving JPEGs to {server.output_dir}", 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: if not args.token:
print("warning: no token configured; anyone on this network can upload", flush=True) print("warning: no token configured; anyone on this network can upload", flush=True)
server.serve_forever() server.serve_forever()