feat: enhance iPhone photo inbox receiver with notification, reveal, and clipboard copy options
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user