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
|
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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user