Add Eagle API import scripts (proper import via Eagle local REST API)

This commit is contained in:
Steven (Zen) 2026-08-11 14:46:12 -06:00
parent 9fe61f6b7a
commit 20aa5f87bd
2 changed files with 298 additions and 0 deletions

211
eagle_api_import.py Normal file
View File

@ -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()

87
eagle_api_import.sh Normal file
View File

@ -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"