410 lines
14 KiB
Python
Executable File
410 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Notebook Library Management for NotebookLM
|
|
Manages a library of NotebookLM notebooks with metadata
|
|
Based on the MCP server implementation
|
|
"""
|
|
|
|
import json
|
|
import argparse
|
|
import uuid
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any
|
|
from datetime import datetime
|
|
|
|
|
|
class NotebookLibrary:
|
|
"""Manages a collection of NotebookLM notebooks with metadata"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the notebook library"""
|
|
# Store data within the skill directory
|
|
skill_dir = Path(__file__).parent.parent
|
|
self.data_dir = skill_dir / "data"
|
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.library_file = self.data_dir / "library.json"
|
|
self.notebooks: Dict[str, Dict[str, Any]] = {}
|
|
self.active_notebook_id: Optional[str] = None
|
|
|
|
# Load existing library
|
|
self._load_library()
|
|
|
|
def _load_library(self):
|
|
"""Load library from disk"""
|
|
if self.library_file.exists():
|
|
try:
|
|
with open(self.library_file, 'r') as f:
|
|
data = json.load(f)
|
|
self.notebooks = data.get('notebooks', {})
|
|
self.active_notebook_id = data.get('active_notebook_id')
|
|
print(f"📚 Loaded library with {len(self.notebooks)} notebooks")
|
|
except Exception as e:
|
|
print(f"⚠️ Error loading library: {e}")
|
|
self.notebooks = {}
|
|
self.active_notebook_id = None
|
|
else:
|
|
self._save_library()
|
|
|
|
def _save_library(self):
|
|
"""Save library to disk"""
|
|
try:
|
|
data = {
|
|
'notebooks': self.notebooks,
|
|
'active_notebook_id': self.active_notebook_id,
|
|
'updated_at': datetime.now().isoformat()
|
|
}
|
|
with open(self.library_file, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
except Exception as e:
|
|
print(f"❌ Error saving library: {e}")
|
|
|
|
def add_notebook(
|
|
self,
|
|
url: str,
|
|
name: str,
|
|
description: str,
|
|
topics: List[str],
|
|
content_types: Optional[List[str]] = None,
|
|
use_cases: Optional[List[str]] = None,
|
|
tags: Optional[List[str]] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Add a new notebook to the library
|
|
|
|
Args:
|
|
url: NotebookLM notebook URL
|
|
name: Display name for the notebook
|
|
description: What's in this notebook
|
|
topics: Topics covered
|
|
content_types: Types of content (optional)
|
|
use_cases: When to use this notebook (optional)
|
|
tags: Additional tags for organization (optional)
|
|
|
|
Returns:
|
|
The created notebook object
|
|
"""
|
|
# Generate ID from name
|
|
notebook_id = name.lower().replace(' ', '-').replace('_', '-')
|
|
|
|
# Check for duplicates
|
|
if notebook_id in self.notebooks:
|
|
raise ValueError(f"Notebook with ID '{notebook_id}' already exists")
|
|
|
|
# Create notebook object
|
|
notebook = {
|
|
'id': notebook_id,
|
|
'url': url,
|
|
'name': name,
|
|
'description': description,
|
|
'topics': topics,
|
|
'content_types': content_types or [],
|
|
'use_cases': use_cases or [],
|
|
'tags': tags or [],
|
|
'created_at': datetime.now().isoformat(),
|
|
'updated_at': datetime.now().isoformat(),
|
|
'use_count': 0,
|
|
'last_used': None
|
|
}
|
|
|
|
# Add to library
|
|
self.notebooks[notebook_id] = notebook
|
|
|
|
# Set as active if it's the first notebook
|
|
if len(self.notebooks) == 1:
|
|
self.active_notebook_id = notebook_id
|
|
|
|
self._save_library()
|
|
|
|
print(f"✅ Added notebook: {name} ({notebook_id})")
|
|
return notebook
|
|
|
|
def remove_notebook(self, notebook_id: str) -> bool:
|
|
"""
|
|
Remove a notebook from the library
|
|
|
|
Args:
|
|
notebook_id: ID of notebook to remove
|
|
|
|
Returns:
|
|
True if removed, False if not found
|
|
"""
|
|
if notebook_id in self.notebooks:
|
|
del self.notebooks[notebook_id]
|
|
|
|
# Clear active if it was removed
|
|
if self.active_notebook_id == notebook_id:
|
|
self.active_notebook_id = None
|
|
# Set new active if there are other notebooks
|
|
if self.notebooks:
|
|
self.active_notebook_id = list(self.notebooks.keys())[0]
|
|
|
|
self._save_library()
|
|
print(f"✅ Removed notebook: {notebook_id}")
|
|
return True
|
|
|
|
print(f"⚠️ Notebook not found: {notebook_id}")
|
|
return False
|
|
|
|
def update_notebook(
|
|
self,
|
|
notebook_id: str,
|
|
name: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
topics: Optional[List[str]] = None,
|
|
content_types: Optional[List[str]] = None,
|
|
use_cases: Optional[List[str]] = None,
|
|
tags: Optional[List[str]] = None,
|
|
url: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Update notebook metadata
|
|
|
|
Args:
|
|
notebook_id: ID of notebook to update
|
|
Other args: Fields to update (None = keep existing)
|
|
|
|
Returns:
|
|
Updated notebook object
|
|
"""
|
|
if notebook_id not in self.notebooks:
|
|
raise ValueError(f"Notebook not found: {notebook_id}")
|
|
|
|
notebook = self.notebooks[notebook_id]
|
|
|
|
# Update fields if provided
|
|
if name is not None:
|
|
notebook['name'] = name
|
|
if description is not None:
|
|
notebook['description'] = description
|
|
if topics is not None:
|
|
notebook['topics'] = topics
|
|
if content_types is not None:
|
|
notebook['content_types'] = content_types
|
|
if use_cases is not None:
|
|
notebook['use_cases'] = use_cases
|
|
if tags is not None:
|
|
notebook['tags'] = tags
|
|
if url is not None:
|
|
notebook['url'] = url
|
|
|
|
notebook['updated_at'] = datetime.now().isoformat()
|
|
|
|
self._save_library()
|
|
print(f"✅ Updated notebook: {notebook['name']}")
|
|
return notebook
|
|
|
|
def get_notebook(self, notebook_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Get a specific notebook by ID"""
|
|
return self.notebooks.get(notebook_id)
|
|
|
|
def list_notebooks(self) -> List[Dict[str, Any]]:
|
|
"""List all notebooks in the library"""
|
|
return list(self.notebooks.values())
|
|
|
|
def search_notebooks(self, query: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Search notebooks by query
|
|
|
|
Args:
|
|
query: Search query (searches name, description, topics, tags)
|
|
|
|
Returns:
|
|
List of matching notebooks
|
|
"""
|
|
query_lower = query.lower()
|
|
results = []
|
|
|
|
for notebook in self.notebooks.values():
|
|
# Search in various fields
|
|
searchable = [
|
|
notebook['name'].lower(),
|
|
notebook['description'].lower(),
|
|
' '.join(notebook['topics']).lower(),
|
|
' '.join(notebook['tags']).lower(),
|
|
' '.join(notebook.get('use_cases', [])).lower()
|
|
]
|
|
|
|
if any(query_lower in field for field in searchable):
|
|
results.append(notebook)
|
|
|
|
return results
|
|
|
|
def select_notebook(self, notebook_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Set a notebook as active
|
|
|
|
Args:
|
|
notebook_id: ID of notebook to activate
|
|
|
|
Returns:
|
|
The activated notebook
|
|
"""
|
|
if notebook_id not in self.notebooks:
|
|
raise ValueError(f"Notebook not found: {notebook_id}")
|
|
|
|
self.active_notebook_id = notebook_id
|
|
self._save_library()
|
|
|
|
notebook = self.notebooks[notebook_id]
|
|
print(f"✅ Activated notebook: {notebook['name']}")
|
|
return notebook
|
|
|
|
def get_active_notebook(self) -> Optional[Dict[str, Any]]:
|
|
"""Get the currently active notebook"""
|
|
if self.active_notebook_id:
|
|
return self.notebooks.get(self.active_notebook_id)
|
|
return None
|
|
|
|
def increment_use_count(self, notebook_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Increment usage counter for a notebook
|
|
|
|
Args:
|
|
notebook_id: ID of notebook that was used
|
|
|
|
Returns:
|
|
Updated notebook
|
|
"""
|
|
if notebook_id not in self.notebooks:
|
|
raise ValueError(f"Notebook not found: {notebook_id}")
|
|
|
|
notebook = self.notebooks[notebook_id]
|
|
notebook['use_count'] += 1
|
|
notebook['last_used'] = datetime.now().isoformat()
|
|
|
|
self._save_library()
|
|
return notebook
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""Get library statistics"""
|
|
total_notebooks = len(self.notebooks)
|
|
total_topics = set()
|
|
total_use_count = 0
|
|
|
|
for notebook in self.notebooks.values():
|
|
total_topics.update(notebook['topics'])
|
|
total_use_count += notebook['use_count']
|
|
|
|
# Find most used
|
|
most_used = None
|
|
if self.notebooks:
|
|
most_used = max(
|
|
self.notebooks.values(),
|
|
key=lambda n: n['use_count']
|
|
)
|
|
|
|
return {
|
|
'total_notebooks': total_notebooks,
|
|
'total_topics': len(total_topics),
|
|
'total_use_count': total_use_count,
|
|
'active_notebook': self.get_active_notebook(),
|
|
'most_used_notebook': most_used,
|
|
'library_path': str(self.library_file)
|
|
}
|
|
|
|
|
|
def main():
|
|
"""Command-line interface for notebook management"""
|
|
parser = argparse.ArgumentParser(description='Manage NotebookLM library')
|
|
|
|
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
|
|
|
# Add command
|
|
add_parser = subparsers.add_parser('add', help='Add a notebook')
|
|
add_parser.add_argument('--url', required=True, help='NotebookLM URL')
|
|
add_parser.add_argument('--name', required=True, help='Display name')
|
|
add_parser.add_argument('--description', required=True, help='Description')
|
|
add_parser.add_argument('--topics', required=True, help='Comma-separated topics')
|
|
add_parser.add_argument('--use-cases', help='Comma-separated use cases')
|
|
add_parser.add_argument('--tags', help='Comma-separated tags')
|
|
|
|
# List command
|
|
subparsers.add_parser('list', help='List all notebooks')
|
|
|
|
# Search command
|
|
search_parser = subparsers.add_parser('search', help='Search notebooks')
|
|
search_parser.add_argument('--query', required=True, help='Search query')
|
|
|
|
# Activate command
|
|
activate_parser = subparsers.add_parser('activate', help='Set active notebook')
|
|
activate_parser.add_argument('--id', required=True, help='Notebook ID')
|
|
|
|
# Remove command
|
|
remove_parser = subparsers.add_parser('remove', help='Remove a notebook')
|
|
remove_parser.add_argument('--id', required=True, help='Notebook ID')
|
|
|
|
# Stats command
|
|
subparsers.add_parser('stats', help='Show library statistics')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Initialize library
|
|
library = NotebookLibrary()
|
|
|
|
# Execute command
|
|
if args.command == 'add':
|
|
topics = [t.strip() for t in args.topics.split(',')]
|
|
use_cases = [u.strip() for u in args.use_cases.split(',')] if args.use_cases else None
|
|
tags = [t.strip() for t in args.tags.split(',')] if args.tags else None
|
|
|
|
notebook = library.add_notebook(
|
|
url=args.url,
|
|
name=args.name,
|
|
description=args.description,
|
|
topics=topics,
|
|
use_cases=use_cases,
|
|
tags=tags
|
|
)
|
|
print(json.dumps(notebook, indent=2))
|
|
|
|
elif args.command == 'list':
|
|
notebooks = library.list_notebooks()
|
|
if notebooks:
|
|
print("\n📚 Notebook Library:")
|
|
for notebook in notebooks:
|
|
active = " [ACTIVE]" if notebook['id'] == library.active_notebook_id else ""
|
|
print(f"\n 📓 {notebook['name']}{active}")
|
|
print(f" ID: {notebook['id']}")
|
|
print(f" Topics: {', '.join(notebook['topics'])}")
|
|
print(f" Uses: {notebook['use_count']}")
|
|
else:
|
|
print("📚 Library is empty. Add notebooks with: notebook_manager.py add")
|
|
|
|
elif args.command == 'search':
|
|
results = library.search_notebooks(args.query)
|
|
if results:
|
|
print(f"\n🔍 Found {len(results)} notebooks:")
|
|
for notebook in results:
|
|
print(f"\n 📓 {notebook['name']} ({notebook['id']})")
|
|
print(f" {notebook['description']}")
|
|
else:
|
|
print(f"🔍 No notebooks found for: {args.query}")
|
|
|
|
elif args.command == 'activate':
|
|
notebook = library.select_notebook(args.id)
|
|
print(f"Now using: {notebook['name']}")
|
|
|
|
elif args.command == 'remove':
|
|
if library.remove_notebook(args.id):
|
|
print("Notebook removed from library")
|
|
|
|
elif args.command == 'stats':
|
|
stats = library.get_stats()
|
|
print("\n📊 Library Statistics:")
|
|
print(f" Total notebooks: {stats['total_notebooks']}")
|
|
print(f" Total topics: {stats['total_topics']}")
|
|
print(f" Total uses: {stats['total_use_count']}")
|
|
if stats['active_notebook']:
|
|
print(f" Active: {stats['active_notebook']['name']}")
|
|
if stats['most_used_notebook']:
|
|
print(f" Most used: {stats['most_used_notebook']['name']} ({stats['most_used_notebook']['use_count']} uses)")
|
|
print(f" Library path: {stats['library_path']}")
|
|
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |