171 lines
6.1 KiB
Python
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")
|