eagle-viewer/eagle_api_import.py

212 lines
6.8 KiB
Python

#!/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()