#!/usr/bin/env python3 """ import_masterclass.py Creates a Masterclass.library in Eagle format from /volume1/steve/Courses/masterclass/ Run on the Synology NAS. Eagle requirements confirmed from working library inspection: - Each .info dir must contain: , _thumbnail.png, metadata.json - metadata.json "name" must match filename stem EXACTLY - Library root needs: metadata.json, mtime.json, actions.json, tags.json, saved-filters.json - mtime.json must have all item IDs -> modificationTime, plus "all" -> count """ import os import json import time import subprocess import sys from pathlib import Path from collections import defaultdict SOURCE = Path("/volume1/steve/Courses/masterclass") LIBRARY = Path("/volume1/steve/Eagle_Libraries/Masterclass.library") IMAGES = LIBRARY / "images" SUPPORTED_EXTS = { "mp4", "mov", "m4v", "avi", "mkv", "webm", "mp3", "m4a", "aac", "wav", "flac", "jpg", "jpeg", "png", "gif", "webp", "pdf", } VIDEO_EXTS = {"mp4", "mov", "m4v", "avi", "mkv", "webm"} IMAGE_EXTS = {"jpg", "jpeg", "png", "gif", "webp"} _id_counter = 0 _id_prefix = f"MC{int(time.time()) % 100000000:08d}" def gen_id(): global _id_counter _id_counter += 1 return f"{_id_prefix}{_id_counter:05d}" def now_ms(): return int(time.time() * 1000) def generate_thumbnail(filepath, thumb_path): """Generate thumbnail PNG. Returns True if successful.""" ext = filepath.suffix.lower().lstrip(".") try: if ext in VIDEO_EXTS: # Extract frame at 10 seconds in (or start if short) result = subprocess.run( ["ffmpeg", "-v", "quiet", "-ss", "10", "-i", str(filepath), "-vframes", "1", "-vf", "scale=320:-1", str(thumb_path)], timeout=30, capture_output=True ) if not thumb_path.exists(): # Try from start subprocess.run( ["ffmpeg", "-v", "quiet", "-i", str(filepath), "-vframes", "1", "-vf", "scale=320:-1", str(thumb_path)], timeout=30, capture_output=True ) elif ext in IMAGE_EXTS: subprocess.run( ["ffmpeg", "-v", "quiet", "-i", str(filepath), "-vf", "scale=320:-1", str(thumb_path)], timeout=30, capture_output=True ) return thumb_path.exists() except Exception: return False def collect_files(): """Walk source dir, return list of (filepath, category, course) tuples.""" 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 build_folder_tree(files): """Build Eagle folder hierarchy. Returns (folders_json, folder_ids dict).""" tree = defaultdict(set) for _, category, course in files: if category: tree[category].add(course or "") folder_ids = {} folders_json = [] for category in sorted(tree): cat_id = gen_id() folder_ids[(category, None)] = cat_id children = [] for course in sorted(tree[category]): if not course: continue course_id = gen_id() folder_ids[(category, course)] = course_id children.append({ "id": course_id, "name": course, "description": "", "children": [], "modificationTime": now_ms(), "tags": [], "password": "", "passwordTips": "", }) folders_json.append({ "id": cat_id, "name": category, "description": "", "children": children, "modificationTime": now_ms(), "tags": [], "password": "", "passwordTips": "", }) return folders_json, folder_ids def main(): print(f"Source: {SOURCE}", flush=True) print(f"Library: {LIBRARY}", flush=True) if not SOURCE.exists(): print("ERROR: Source directory not found.", file=sys.stderr) sys.exit(1) print("Scanning files...", flush=True) files = collect_files() print(f"Found {len(files)} files to import.", flush=True) folders_json, folder_ids = build_folder_tree(files) # Create library structure IMAGES.mkdir(parents=True, exist_ok=True) # Write all required library root files meta = { "folders": folders_json, "smartFolders": [], "quickAccess": [], "tagsGroups": [], "modificationTime": now_ms(), "applicationVersion": "4.0.0", } (LIBRARY / "metadata.json").write_text(json.dumps(meta)) (LIBRARY / "actions.json").write_text("[]") (LIBRARY / "tags.json").write_text("[]") (LIBRARY / "saved-filters.json").write_text("[]") (LIBRARY / "mtime.json").write_text("{}") # Will be written at end print("Library root files written.", flush=True) mtime_map = {} errors = [] for i, (filepath, category, course) in enumerate(files, 1): try: ext = filepath.suffix.lower().lstrip(".") stem = filepath.stem # MUST match filename exactly for Eagle item_id = gen_id() item_dir = IMAGES / f"{item_id}.info" item_dir.mkdir(parents=True, exist_ok=True) # Hardlink the file into .info dir dest_file = item_dir / filepath.name try: os.link(filepath, dest_file) except OSError: import shutil shutil.copy2(filepath, dest_file) # Generate thumbnail thumb_path = item_dir / f"{stem}_thumbnail.png" generate_thumbnail(filepath, thumb_path) # Get file stats stat = filepath.stat() size = stat.st_size btime_ms = int(stat.st_ctime * 1000) mtime_ms = int(stat.st_mtime * 1000) mod_ms = now_ms() # Folder assignment folder_id = None if category and course: folder_id = folder_ids.get((category, course)) if not folder_id and category: folder_id = folder_ids.get((category, None)) img_meta = { "id": item_id, "name": stem, # Exact filename stem — Eagle matches this to the file "size": size, "btime": btime_ms, "mtime": mtime_ms, "ext": ext, "tags": [], "folders": [folder_id] if folder_id else [], "isDeleted": False, "url": "", "annotation": "", "modificationTime": mod_ms, "width": 0, "height": 0, "resolutionWidth": 0, "resolutionHeight": 0, "lastModified": mod_ms, } (item_dir / "metadata.json").write_text(json.dumps(img_meta)) mtime_map[item_id] = mod_ms if i % 50 == 0 or i == len(files): print(f"Progress: {i}/{len(files)}", flush=True) except Exception as e: errors.append(f"{filepath}: {e}") print(f"ERROR: {filepath}: {e}", file=sys.stderr) # Write final mtime.json with all item IDs mtime_map["all"] = len(mtime_map) (LIBRARY / "mtime.json").write_text(json.dumps(mtime_map)) print(f"\n=== Import complete ===", flush=True) print(f"Items imported: {len(mtime_map) - 1}", flush=True) print(f"Errors: {len(errors)}", flush=True) if errors: for e in errors[:10]: print(f" {e}", flush=True) if __name__ == "__main__": main()