From 20aa5f87bd70c945c1be79632a1f1cb6ab27247c Mon Sep 17 00:00:00 2001 From: "Steven (Zen)" Date: Tue, 11 Aug 2026 14:46:12 -0600 Subject: [PATCH] Add Eagle API import scripts (proper import via Eagle local REST API) --- eagle_api_import.py | 211 ++++++++++++++++++++++++++++++++++++++++++++ eagle_api_import.sh | 87 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 eagle_api_import.py create mode 100644 eagle_api_import.sh diff --git a/eagle_api_import.py b/eagle_api_import.py new file mode 100644 index 0000000..f92bc36 --- /dev/null +++ b/eagle_api_import.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +eagle_api_import.py +Run this on your Mac while Eagle is open with the Masterclass library active. +Uses Eagle's local REST API to properly import all Masterclass files. + +Usage: + python3 eagle_api_import.py + +Requirements: +- Eagle must be open +- Masterclass library must be the active library in Eagle +- NAS must be mounted at /Volumes/steve/ +""" + +import os +import json +import time +import urllib.request +import urllib.error +from pathlib import Path +from collections import defaultdict + +EAGLE_API = "http://localhost:41595" +SOURCE = Path("/Volumes/steve/Courses/masterclass") + +SUPPORTED_EXTS = { + "mp4", "mov", "m4v", "avi", "mkv", "webm", + "mp3", "m4a", "aac", "wav", "flac", + "jpg", "jpeg", "png", "gif", "webp", + "pdf", +} + +def api(method, endpoint, data=None): + url = f"{EAGLE_API}{endpoint}" + body = json.dumps(data).encode() if data else None + req = urllib.request.Request( + url, data=body, + headers={"Content-Type": "application/json"}, + method=method + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + return json.loads(resp.read()) + except Exception as e: + return {"status": "error", "message": str(e)} + +def get(endpoint): + return api("GET", endpoint) + +def post(endpoint, data): + return api("POST", endpoint, data) + +def collect_files(): + files = [] + for root, dirs, filenames in os.walk(SOURCE): + dirs[:] = sorted([d for d in dirs if d != "@eaDir"]) + root_path = Path(root) + rel = root_path.relative_to(SOURCE) + parts = rel.parts + category = parts[0] if len(parts) >= 1 else None + course = parts[1] if len(parts) >= 2 else None + if category is None: + continue + for fname in sorted(filenames): + ext = fname.rsplit(".", 1)[-1].lower() if "." in fname else "" + if ext not in SUPPORTED_EXTS: + continue + files.append((root_path / fname, category, course)) + return files + +def create_folders(): + """Create folder hierarchy in Eagle and return {(category, course): folder_id}.""" + # First get existing folders + lib_info = get("/api/library/info") + existing = {} + if lib_info.get("status") == "success": + def extract(folders, parent=None): + for f in folders: + existing[f["name"]] = f["id"] + extract(f.get("children", []), f["id"]) + extract(lib_info.get("data", {}).get("library", {}).get("folders", [])) + + print(f"Existing folders: {len(existing)}") + + folder_ids = {} + + # Get all categories and courses from files + tree = defaultdict(set) + for _, category, course in collect_files(): + if category: + tree[category].add(course or "") + + for category in sorted(tree): + # Create category folder if needed + if category not in existing: + resp = post("/api/folder/create", {"folderName": category}) + if resp.get("status") == "success": + cat_id = resp["data"]["id"] + existing[category] = cat_id + print(f" Created folder: {category}") + time.sleep(0.1) + else: + print(f" ERROR creating {category}: {resp}") + continue + else: + cat_id = existing[category] + + folder_ids[(category, None)] = cat_id + + for course in sorted(tree[category]): + if not course: + continue + if course not in existing: + resp = post("/api/folder/create", { + "folderName": course, + "parent": cat_id + }) + if resp.get("status") == "success": + course_id = resp["data"]["id"] + existing[course] = course_id + print(f" Created subfolder: {category}/{course}") + time.sleep(0.1) + else: + print(f" ERROR creating {category}/{course}: {resp}") + continue + else: + course_id = existing[course] + + folder_ids[(category, course)] = course_id + + return folder_ids + +def main(): + # Check Eagle is running + info = get("/api/application/info") + if info.get("status") != "success": + print("ERROR: Eagle API not reachable. Make sure Eagle is open.") + return + + print(f"Eagle version: {info['data']['version']}") + + # Check current library + lib_info = get("/api/library/info") + if lib_info.get("status") == "success": + lib_name = lib_info.get("data", {}).get("library", {}).get("name", "unknown") + lib_path = lib_info.get("data", {}).get("library", {}).get("path", "") + print(f"Current library: {lib_name}") + print(f"Library path: {lib_path}") + if "masterclass" not in lib_name.lower() and "masterclass" not in lib_path.lower(): + confirm = input("WARNING: Active library doesn't look like Masterclass. Continue? (y/n): ") + if confirm.lower() != "y": + return + + print("\nCreating folder structure...") + folder_ids = create_folders() + print(f"Folders ready: {len(folder_ids)}") + + print("\nCollecting files...") + files = collect_files() + print(f"Files to import: {len(files)}") + input("Press Enter to start import (or Ctrl+C to cancel)...") + + done = 0 + errors = [] + BATCH_SIZE = 20 + + # Import in batches using addFromPaths + batch = [] + for i, (filepath, category, course) in enumerate(files, 1): + stem = filepath.stem + folder_id = folder_ids.get((category, course)) or folder_ids.get((category, None)) + + item = { + "path": str(filepath), + "name": stem, + "tags": [], + "annotation": "", + } + if folder_id: + item["folderId"] = folder_id + + batch.append(item) + + if len(batch) >= BATCH_SIZE or i == len(files): + resp = post("/api/item/addFromPaths", {"items": batch}) + if resp.get("status") == "success": + done += len(batch) + else: + # Fall back to individual adds + for item in batch: + r = post("/api/item/addFromPath", item) + if r.get("status") == "success": + done += 1 + else: + errors.append(f"{item['path']}: {r.get('message', r)}") + batch = [] + + if done % 50 == 0 or i == len(files): + print(f"Progress: {done}/{len(files)} done, {len(errors)} errors") + time.sleep(0.05) # Gentle rate limiting + + print(f"\n=== Import complete ===") + print(f"Imported: {done}") + print(f"Errors: {len(errors)}") + if errors: + for e in errors[:10]: + print(f" {e}") + +if __name__ == "__main__": + main() diff --git a/eagle_api_import.sh b/eagle_api_import.sh new file mode 100644 index 0000000..d7d7b00 --- /dev/null +++ b/eagle_api_import.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# eagle_api_import.sh +# Run this on your Mac while Eagle is open with the Masterclass library active. +# Uses Eagle's local API to properly import all files from /Volumes/steve/Courses/masterclass/ +# +# Usage: bash eagle_api_import.sh +# +# Eagle API docs: https://api.eagle.cool/item/add-from-path + +EAGLE_API="http://localhost:41595" +SOURCE="/Volumes/steve/Courses/masterclass" + +SUPPORTED_EXTS="mp4 mov m4v mp3 m4a jpg jpeg png gif webp pdf" + +is_supported() { + local ext="${1##*.}" + ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]') + for e in $SUPPORTED_EXTS; do + [[ "$e" == "$ext" ]] && return 0 + done + return 1 +} + +# Check Eagle API is up +info=$(curl -s "$EAGLE_API/api/application/info") +if [[ -z "$info" ]]; then + echo "ERROR: Eagle API not reachable at $EAGLE_API. Make sure Eagle is open." + exit 1 +fi +echo "Eagle API: OK" + +# Check which library is open +lib_info=$(curl -s "$EAGLE_API/api/library/info") +lib_name=$(echo "$lib_info" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['library']['name'])" 2>/dev/null) +echo "Current library: $lib_name" +echo "Make sure 'Masterclass' library is open in Eagle before continuing." +echo "Press Enter to continue or Ctrl+C to cancel..." +read + +DONE=0 +ERRORS=0 + +# Walk the source directory +find "$SOURCE" -type f | sort | while read filepath; do + is_supported "$filepath" || continue + + filename=$(basename "$filepath") + ext="${filename##*.}" + name="${filename%.*}" + + # Get relative path to determine folder: category/course + rel="${filepath#$SOURCE/}" + category=$(echo "$rel" | cut -d'/' -f1) + course=$(echo "$rel" | cut -d'/' -f2) + + # Build folder path tag for Eagle + folder_tag="$category/$course" + + # Call Eagle API to add item from path + response=$(curl -s -X POST "$EAGLE_API/api/item/addFromPath" \ + -H "Content-Type: application/json" \ + -d "{ + \"path\": \"$filepath\", + \"name\": \"$name\", + \"tags\": [], + \"annotation\": \"\", + \"folderId\": \"\" + }") + + status=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status','unknown'))" 2>/dev/null) + + if [[ "$status" == "success" ]]; then + DONE=$((DONE + 1)) + else + ERRORS=$((ERRORS + 1)) + echo "ERROR: $filepath -> $response" + fi + + if (( (DONE + ERRORS) % 50 == 0 )); then + echo "Progress: done=$DONE errors=$ERRORS" + fi +done + +echo "" +echo "=== Complete ===" +echo "Imported: $DONE" +echo "Errors: $ERRORS"