Files
gnommo/gnommo/handoff.py
T

171 lines
6.1 KiB
Python

"""Hand off a finished video to the gnommoweb server.
Works for any gnommo project type: parent videos and shorts alike.
Usage:
gnommo handoff -p video1
gnommo handoff -p short_pixelated_universe
gnommo handoff -p video1 --file /path/to/render.mp4
Reads project.json for the 'output_video' field (path relative to the
project directory). Override with --file.
On success:
- Uploads the video to MinIO via POST /api/projects/:handle/handoff
- For shorts: server auto-advances status to 'processed'
- Bumps video_version on every upload
- Updates .gnommo_sync.json with new video_version
Configuration (from .env or environment):
GNOMMOWEB_URL Base URL (e.g. http://localhost:3001)
GNOMMOWEB_API_KEY Bearer token (CONTENT_API_KEY from gnommoweb)
"""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
try:
import requests
except ImportError:
print("Error: 'requests' package is required. Run: pip install requests", file=sys.stderr)
sys.exit(1)
SYNC_FILE = ".gnommo_sync.json"
def _load_env_file():
env_path = Path(__file__).parent.parent / ".env"
if not env_path.exists():
return
with open(env_path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key not in os.environ:
os.environ[key] = value
def _read_sync(project_path: Path) -> dict:
sync_file = project_path / SYNC_FILE
if sync_file.exists():
with open(sync_file) as f:
return json.load(f)
return {}
def _write_sync(project_path: Path, data: dict):
with open(project_path / SYNC_FILE, "w") as f:
json.dump(data, f, indent=2)
def cmd_handoff(project_path: Path, verbose: bool = False, file_override: str | None = None, prod: bool = False) -> int:
_load_env_file()
if prod:
api_url = os.environ.get("GNOMMOWEB_PROD_URL", "").rstrip("/")
api_key = os.environ.get("GNOMMOWEB_PROD_API_KEY", "")
if not api_url: print("Error: GNOMMOWEB_PROD_URL is not set.", file=sys.stderr); return 1
if not api_key: print("Error: GNOMMOWEB_PROD_API_KEY is not set.", file=sys.stderr); return 1
else:
api_url = os.environ.get("GNOMMOWEB_URL", "").rstrip("/")
api_key = os.environ.get("GNOMMOWEB_API_KEY", "")
if not api_url: print("Error: GNOMMOWEB_URL is not set.", file=sys.stderr); return 1
if not api_key: print("Error: GNOMMOWEB_API_KEY is not set.", file=sys.stderr); return 1
if verbose:
target = "production" if prod else "local"
print(f"{target}: {api_url}")
project_file = project_path / "project.json"
if not project_file.exists():
print(f"Error: {project_file} not found", file=sys.stderr)
return 1
with open(project_file) as f:
project = json.load(f)
project_id = project.get("id")
if not project_id:
print("Error: project.json must have an 'id' field.", file=sys.stderr)
return 1
# ── Resolve video file ─────────────────────────────────────────────────────
if file_override:
video_path = Path(file_override)
else:
output_video = project.get("output_video")
if not output_video:
print(
"Error: no 'output_video' field in project.json and no --file provided.",
file=sys.stderr,
)
return 1
video_path = project_path / output_video
if not video_path.exists():
print(f"Error: video file not found: {video_path}", file=sys.stderr)
return 1
file_size_mb = video_path.stat().st_size / (1024 * 1024)
if verbose:
print(f"Handing off {project_id}{api_url}")
print(f" File: {video_path} ({file_size_mb:.1f} MB)")
# ── Upload ─────────────────────────────────────────────────────────────────
try:
with open(video_path, "rb") as vf:
r = requests.post(
f"{api_url}/api/projects/{project_id}/handoff",
files={"video": (video_path.name, vf, _mime_type(video_path))},
headers={"Authorization": f"Bearer {api_key}"},
timeout=None, # large files may take a while
)
r.raise_for_status()
except requests.exceptions.ConnectionError:
print(f"Error: Could not connect to {api_url}", file=sys.stderr)
return 1
except requests.exceptions.HTTPError as e:
print(f"Error: Server returned {e.response.status_code}", file=sys.stderr)
try:
print(f" {e.response.json()}", file=sys.stderr)
except Exception:
pass
return 1
result = r.json()
video_version = result.get("video_version", "?")
video_url = result.get("video_url", "")
# ── Write sync state ───────────────────────────────────────────────────────
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
existing_sync = _read_sync(project_path)
_write_sync(project_path, {
**existing_sync,
"last_handoff_at": now_iso,
"video_version": video_version,
"server_updated_at": result.get("asset", {}).get("updated_at", existing_sync.get("server_updated_at")),
})
print(f"{project_id} → v{video_version} [processed]")
if video_url:
print(f" {video_url}")
return 0
def _mime_type(path: Path) -> str:
ext = path.suffix.lower()
return {
".mp4": "video/mp4",
".mov": "video/quicktime",
".webm": "video/webm",
".mkv": "video/x-matroska",
}.get(ext, "application/octet-stream")