"""Batch operations service — perform operations across multiple notebooks.""" from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Any from ..core.client import NotebookLMClient from . import chat as chat_service from . import notebooks as notebooks_service from . import sources as sources_service from . import studio as studio_service from ._compat import TypedDict from .errors import ValidationError class BatchItemResult(TypedDict): """Result for a single item in a batch operation.""" notebook_id: str notebook_title: str success: bool result: Any error: str | None class BatchResult(TypedDict): """Aggregated batch operation result.""" operation: str items: list[BatchItemResult] total: int succeeded: int failed: int def _resolve_targets( client: NotebookLMClient, notebook_names: list[str] | None = None, tags: list[str] | None = None, all_notebooks: bool = False, ) -> list[tuple[str, str]]: """Resolve batch targets to (id, title) tuples. Reuses cross_notebook logic.""" from . import cross_notebook as cross_notebook_service return cross_notebook_service._resolve_notebook_ids(client, notebook_names, tags, all_notebooks) def _run_batch( operation: str, targets: list[tuple[str, str]], fn, max_concurrent: int = 5, ) -> BatchResult: """Execute a function across multiple targets in parallel.""" results: list[BatchItemResult] = [] with ThreadPoolExecutor(max_workers=min(max_concurrent, len(targets))) as executor: futures = {} for nb_id, nb_title in targets: future = executor.submit(fn, nb_id, nb_title) futures[future] = (nb_id, nb_title) for future in as_completed(futures): nb_id, nb_title = futures[future] try: result = future.result() results.append( { "notebook_id": nb_id, "notebook_title": nb_title, "success": True, "result": result, "error": None, } ) except Exception as e: results.append( { "notebook_id": nb_id, "notebook_title": nb_title, "success": False, "result": None, "error": str(e), } ) results.sort(key=lambda r: (not r["success"], r["notebook_title"])) succeeded = sum(1 for r in results if r["success"]) return { "operation": operation, "items": results, "total": len(targets), "succeeded": succeeded, "failed": len(targets) - succeeded, } def batch_query( client: NotebookLMClient, query_text: str, notebook_names: list[str] | None = None, tags: list[str] | None = None, all_notebooks: bool = False, max_concurrent: int = 5, ) -> BatchResult: """Query multiple notebooks with the same question. This is similar to cross_notebook_query but returns raw per-notebook results without aggregation. Args: client: Authenticated client query_text: Question to ask notebook_names: Specific notebook names or IDs tags: Select by tags all_notebooks: Query all max_concurrent: Max parallel queries """ if not query_text or not query_text.strip(): raise ValidationError("Query text is required.", user_message="Please provide a question.") targets = _resolve_targets(client, notebook_names, tags, all_notebooks) if not targets: return {"operation": "batch_query", "items": [], "total": 0, "succeeded": 0, "failed": 0} def query_fn(nb_id, nb_title): return chat_service.query(client, nb_id, query_text) return _run_batch("batch_query", targets, query_fn, max_concurrent) def batch_add_source( client: NotebookLMClient, source_url: str, notebook_names: list[str] | None = None, tags: list[str] | None = None, all_notebooks: bool = False, max_concurrent: int = 3, ) -> BatchResult: """Add the same source URL to multiple notebooks. Args: client: Authenticated client source_url: URL to add as source notebook_names: Target notebooks tags: Select by tags all_notebooks: All notebooks max_concurrent: Max parallel ops (lower default for writes) """ if not source_url or not source_url.strip(): raise ValidationError("Source URL is required.", user_message="Please provide a URL.") targets = _resolve_targets(client, notebook_names, tags, all_notebooks) if not targets: return { "operation": "batch_add_source", "items": [], "total": 0, "succeeded": 0, "failed": 0, } def add_fn(nb_id, nb_title): return sources_service.add_source(client, nb_id, source_type="url", url=source_url) return _run_batch("batch_add_source", targets, add_fn, max_concurrent) def batch_create( client: NotebookLMClient, titles: list[str], ) -> BatchResult: """Create multiple notebooks at once. Args: client: Authenticated client titles: List of notebook titles to create """ if not titles: raise ValidationError( "At least one title is required.", user_message="Please provide notebook titles." ) targets = [(f"new-{i}", title) for i, title in enumerate(titles)] def create_fn(nb_id, nb_title): return notebooks_service.create_notebook(client, nb_title) return _run_batch("batch_create", targets, create_fn, max_concurrent=3) def batch_delete( client: NotebookLMClient, notebook_names: list[str] | None = None, tags: list[str] | None = None, confirm: bool = False, max_concurrent: int = 3, ) -> BatchResult: """Delete multiple notebooks. Args: client: Authenticated client notebook_names: Notebooks to delete tags: Select by tags confirm: Must be True (safety check) max_concurrent: Max parallel ops """ if not confirm: raise ValidationError( "Batch delete requires confirm=True.", user_message="Batch delete is IRREVERSIBLE. Set confirm=True after user approval.", ) targets = _resolve_targets(client, notebook_names, tags, all_notebooks=False) if not targets: return {"operation": "batch_delete", "items": [], "total": 0, "succeeded": 0, "failed": 0} def delete_fn(nb_id, nb_title): return notebooks_service.delete_notebook(client, nb_id) return _run_batch("batch_delete", targets, delete_fn, max_concurrent) def batch_studio( client: NotebookLMClient, artifact_type: str = "audio", notebook_names: list[str] | None = None, tags: list[str] | None = None, all_notebooks: bool = False, max_concurrent: int = 2, ) -> BatchResult: """Generate studio artifacts across multiple notebooks. Args: client: Authenticated client artifact_type: Type of artifact (audio, video, report, etc.) notebook_names: Target notebooks tags: Select by tags all_notebooks: All notebooks max_concurrent: Max parallel (lower for heavy ops) """ targets = _resolve_targets(client, notebook_names, tags, all_notebooks) if not targets: return {"operation": "batch_studio", "items": [], "total": 0, "succeeded": 0, "failed": 0} def studio_fn(nb_id, nb_title): return studio_service.create_artifact(client, nb_id, artifact_type) return _run_batch("batch_studio", targets, studio_fn, max_concurrent)