diff --git a/gnommo/transfer.py b/gnommo/transfer.py index 04c35ab..baef40c 100644 --- a/gnommo/transfer.py +++ b/gnommo/transfer.py @@ -57,6 +57,17 @@ def _build_manifest(project_path: Path) -> list[str]: if (project_path / fixed).exists(): files.add(fixed) + # Keynote presentation(s) + for key_file in project_path.glob("*.key"): + files.add(key_file.name) + + # Rendered output (mp4 + srt — not low-res previews or transcripts) + out_dir = project_path / "out" + if out_dir.is_dir(): + for f in out_dir.iterdir(): + if f.is_file() and f.suffix in (".mp4", ".srt"): + files.add(str(f.relative_to(project_path))) + # Manuscript (may be at a non-standard path) manuscript_rel = project.get("manuscript", "manuscript.txt") if (project_path / manuscript_rel).exists(): @@ -131,6 +142,90 @@ def _build_manifest(project_path: Path) -> list[str]: return sorted(files) +# --------------------------------------------------------------------------- +# Shared assets manifest +# --------------------------------------------------------------------------- + +def _find_shared_assets_root(project_path: Path) -> Optional[Path]: + """Return the local shared_assets directory, or None if not found.""" + candidate = project_path.parent / "shared_assets" + if candidate.is_dir(): + return candidate + try: + from .cache import load_assets_config + assets = load_assets_config() + if assets: + candidate = assets / "shared_assets" + if candidate.is_dir(): + return candidate + except Exception: + pass + return None + + +def _build_shared_manifest(project_path: Path) -> tuple: + """Return (shared_assets_root, sorted list of root-relative paths) for shared files used by this project. + + Layout conventions discovered from the actual filesystem: + - Shared videos: shared_assets/{source_file} (e.g. Logo6sec.mov, pexels/14923961.mp4) + - Shared audio: shared_assets/media/audio/{file} + """ + shared_root = _find_shared_assets_root(project_path) + if shared_root is None: + return None, [] + + files: set[str] = set() + + # shared_assets/videos.json so the render pipeline can read metadata + if (shared_root / "videos.json").exists(): + files.add("videos.json") + + project: dict = {} + proj_json = project_path / "project.json" + if proj_json.exists(): + try: + project = json.loads(proj_json.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + pass + + # Shared video source files + videos_rel = project.get("videos", "media/videos/videos.json") + videos_json = project_path / videos_rel + if videos_json.exists(): + try: + data = json.loads(videos_json.read_text(encoding="utf-8")) + for entry in data.values(): + if not entry.get("is_shared"): + continue + src = entry.get("source_file") + if not src: + continue + # source_file is relative to shared_assets root + if (shared_root / src).exists(): + files.add(src) + except (json.JSONDecodeError, OSError): + pass + + # Shared audio source files (live under media/audio/ within shared_assets) + audio_json = project_path / "media" / "audio" / "audio.json" + if audio_json.exists(): + try: + data = json.loads(audio_json.read_text(encoding="utf-8")) + for entry in data.values(): + if not entry.get("is_shared"): + continue + src = entry.get("file") + if not src: + continue + rel = f"media/audio/{src}" + if (shared_root / rel).exists(): + files.add(rel) + except (json.JSONDecodeError, OSError): + pass + + return shared_root, sorted(files) + + # --------------------------------------------------------------------------- # Commit log helpers # --------------------------------------------------------------------------- @@ -203,15 +298,22 @@ def cmd_up(project_path: Path, verbose: bool, dry_run: bool) -> int: for f in manifest: print(f" {f}") + # Build shared assets manifest + shared_root, shared_files = _build_shared_manifest(project_path) + remote_shared = f"{server['path']}/shared_assets" + if dry_run: - print("\n[DRY RUN] Would push:") + print("\n[DRY RUN] Would push project files:") for f in manifest: print(f" {f}") + if shared_files: + print(f"\n[DRY RUN] Would push shared assets ({len(shared_files)} files):") + for f in shared_files: + print(f" {f}") return 0 - # Ensure remote dir exists + # Pass 1: project files subprocess.run([*ssh_cmd, f"mkdir -p {remote_project}"], check=True) - rsync_cmd = [ "rsync", "-av", "--progress", "--files-from=-", @@ -221,10 +323,26 @@ def cmd_up(project_path: Path, verbose: bool, dry_run: bool) -> int: ] result = subprocess.run(rsync_cmd, input="\n".join(manifest), text=True) if result.returncode != 0: - print(f"Error: rsync failed (code {result.returncode})") + print(f"Error: rsync failed for project files (code {result.returncode})") return 1 - print(f"\nDone. Pushed {total} files.") + # Pass 2: shared assets referenced by this project + if shared_files and shared_root: + print(f"\nPushing shared assets ({len(shared_files)} files)...") + subprocess.run([*ssh_cmd, f"mkdir -p {remote_shared}"], check=True) + rsync_shared = [ + "rsync", "-av", "--progress", + "--files-from=-", + "-e", f"ssh -p {server['port']}", + f"{shared_root}/", + f"{server['user']}@{server['host']}:{remote_shared}/", + ] + result = subprocess.run(rsync_shared, input="\n".join(shared_files), text=True) + if result.returncode != 0: + print(f"Error: rsync failed for shared assets (code {result.returncode})") + return 1 + + print(f"\nDone. Pushed {total} project files, {len(shared_files)} shared assets.") return 0 @@ -267,20 +385,45 @@ def cmd_down(project_path: Path, verbose: bool, dry_run: bool) -> int: f"{project_path}/", ] + remote_shared = f"{server['path']}/shared_assets" + local_shared = project_path.parent / "shared_assets" + rsync_shared_cmd = [ + "rsync", "-av", "--progress", + "-e", f"ssh -p {server['port']}", + f"{server['user']}@{server['host']}:{remote_shared}/", + f"{local_shared}/", + ] + if dry_run: rsync_cmd.insert(1, "--dry-run") - print("\n[DRY RUN] Would execute:") + print("\n[DRY RUN] Would pull project:") print(f" {' '.join(rsync_cmd)}") + print("\n[DRY RUN] Would pull shared assets:") + print(f" {' '.join(rsync_shared_cmd)}") return 0 if verbose: print(f" {' '.join(rsync_cmd)}") + # Pass 1: project files result = subprocess.run(rsync_cmd) if result.returncode != 0: - print(f"Error: rsync failed (code {result.returncode})") + print(f"Error: rsync failed for project files (code {result.returncode})") return 1 + # Pass 2: shared assets (only if server has any) + check = subprocess.run( + [*ssh_cmd, f"test -d {remote_shared} && echo ok"], + capture_output=True, text=True, + ) + if check.stdout.strip() == "ok": + print("\nPulling shared assets...") + local_shared.mkdir(parents=True, exist_ok=True) + result = subprocess.run(rsync_shared_cmd) + if result.returncode != 0: + print(f"Error: rsync failed for shared assets (code {result.returncode})") + return 1 + print("\nDone.") return 0 diff --git a/up-all.sh b/up-all.sh new file mode 100755 index 0000000..594d929 --- /dev/null +++ b/up-all.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +./gnommo.sh -p video0 up +./gnommo.sh -p video1 up +./gnommo.sh -p video2 up +./gnommo.sh -p video3 up +./gnommo.sh -p video4 up +./gnommo.sh -p video5 up +./gnommo.sh -p video6 up +