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