09e9f3545f
CodeQL / Analyze (push) Waiting to run
dependency-audit / pip-audit (push) Waiting to run
Test / Code Quality (push) Waiting to run
Test / Test (macos-latest, Python 3.10) (push) Blocked by required conditions
Test / Test (macos-latest, Python 3.11) (push) Blocked by required conditions
Test / Test (macos-latest, Python 3.12) (push) Blocked by required conditions
Test / Test (macos-latest, Python 3.13) (push) Blocked by required conditions
Test / Test (macos-latest, Python 3.14) (push) Blocked by required conditions
Test / Test (ubuntu-latest, Python 3.10) (push) Blocked by required conditions
Test / Test (ubuntu-latest, Python 3.11) (push) Blocked by required conditions
Test / Test (ubuntu-latest, Python 3.12) (push) Blocked by required conditions
Test / Test (ubuntu-latest, Python 3.13) (push) Blocked by required conditions
Test / Test (ubuntu-latest, Python 3.14) (push) Blocked by required conditions
Test / Test (windows-latest, Python 3.10) (push) Blocked by required conditions
Test / Test (windows-latest, Python 3.11) (push) Blocked by required conditions
Test / Test (windows-latest, Python 3.12) (push) Blocked by required conditions
Test / Test (windows-latest, Python 3.13) (push) Blocked by required conditions
Test / Test (windows-latest, Python 3.14) (push) Blocked by required conditions
231 lines
7.9 KiB
Python
231 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Diagnose GET_NOTEBOOK failures (Issue #114).
|
|
|
|
Compares LIST_NOTEBOOKS and GET_NOTEBOOK raw responses to help diagnose
|
|
why GET_NOTEBOOK returns "No result found" while LIST_NOTEBOOKS works.
|
|
|
|
This script makes the exact same HTTP requests as the main client and
|
|
prints the raw responses, parsed chunks, and found RPC IDs.
|
|
|
|
Usage:
|
|
python scripts/diagnose_get_notebook.py <notebook_id>
|
|
python scripts/diagnose_get_notebook.py # uses first notebook from list
|
|
|
|
Environment variables:
|
|
NOTEBOOKLM_AUTH_JSON - Playwright storage state JSON (for CI)
|
|
Or run 'notebooklm login' first for local auth.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
from typing import Any
|
|
from urllib.parse import urlencode
|
|
|
|
import httpx
|
|
|
|
from notebooklm.auth import AuthTokens, fetch_tokens, load_auth_from_storage
|
|
from notebooklm.rpc import (
|
|
BATCHEXECUTE_URL,
|
|
RPCMethod,
|
|
build_request_body,
|
|
encode_rpc_request,
|
|
)
|
|
from notebooklm.rpc.decoder import (
|
|
collect_rpc_ids,
|
|
decode_response,
|
|
parse_chunked_response,
|
|
strip_anti_xssi,
|
|
)
|
|
|
|
|
|
def load_auth() -> dict[str, str]:
|
|
"""Load auth cookies from storage."""
|
|
try:
|
|
return load_auth_from_storage()
|
|
except FileNotFoundError:
|
|
print(
|
|
"ERROR: No authentication found.\n"
|
|
"Set NOTEBOOKLM_AUTH_JSON env var or run 'notebooklm login'",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
except ValueError as e:
|
|
print(f"ERROR: Invalid authentication: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
async def make_rpc_request(
|
|
client: httpx.AsyncClient,
|
|
auth: AuthTokens,
|
|
method: RPCMethod,
|
|
params: list,
|
|
source_path: str = "/",
|
|
) -> str:
|
|
"""Make an RPC request and return raw response text."""
|
|
query = urlencode(
|
|
{
|
|
"rpcids": method.value,
|
|
"source-path": source_path,
|
|
"f.sid": auth.session_id,
|
|
"rt": "c",
|
|
}
|
|
)
|
|
url = f"{BATCHEXECUTE_URL}?{query}"
|
|
|
|
rpc_request = encode_rpc_request(method, params)
|
|
body = build_request_body(rpc_request, auth.csrf_token)
|
|
|
|
cookie_header = "; ".join(f"{k}={v}" for k, v in auth.cookies.items())
|
|
headers = {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Cookie": cookie_header,
|
|
}
|
|
|
|
response = await client.post(url, content=body, headers=headers)
|
|
response.raise_for_status()
|
|
return response.text
|
|
|
|
|
|
def diagnose_response(label: str, raw: str, rpc_id: str) -> tuple[list, list, Any]:
|
|
"""Print diagnostic info about a raw RPC response.
|
|
|
|
Returns:
|
|
Tuple of (chunks, found_ids, decoded_result_or_None).
|
|
"""
|
|
print(f"\n{'=' * 60}")
|
|
print(f" {label}")
|
|
print(f"{'=' * 60}")
|
|
print(f" Raw response length: {len(raw)} bytes")
|
|
print(f" First 200 chars: {raw[:200]!r}")
|
|
|
|
cleaned = strip_anti_xssi(raw)
|
|
chunks = parse_chunked_response(cleaned)
|
|
found_ids = collect_rpc_ids(chunks)
|
|
|
|
print(f" Chunks parsed: {len(chunks)}")
|
|
print(f" Found RPC IDs: {found_ids}")
|
|
print(f" Target RPC ID: {rpc_id}")
|
|
print(f" Target in found_ids: {rpc_id in found_ids}")
|
|
|
|
# Show each chunk's structure
|
|
for i, chunk in enumerate(chunks):
|
|
if isinstance(chunk, list) and len(chunk) >= 2:
|
|
tag = chunk[0] if isinstance(chunk[0], str) else "?"
|
|
cid = chunk[1] if len(chunk) > 1 else "?"
|
|
has_data = chunk[2] is not None if len(chunk) > 2 else False
|
|
data_type = type(chunk[2]).__name__ if len(chunk) > 2 else "N/A"
|
|
print(f" Chunk {i}: tag={tag}, id={cid}, has_data={has_data}, data_type={data_type}")
|
|
else:
|
|
preview = repr(chunk)[:100]
|
|
print(f" Chunk {i}: {preview}")
|
|
|
|
# Try full decode_response (uses the same parsing internally)
|
|
decoded_result = None
|
|
try:
|
|
decoded_result = decode_response(raw, rpc_id)
|
|
preview = repr(decoded_result)[:200]
|
|
print(f" decode_response: OK - {preview}")
|
|
except Exception as e:
|
|
print(f" decode_response: raised {type(e).__name__}: {e}")
|
|
|
|
return chunks, found_ids, decoded_result
|
|
|
|
|
|
async def run_diagnosis(notebook_id: str | None = None) -> None:
|
|
"""Run the diagnostic comparison."""
|
|
cookies = load_auth()
|
|
|
|
print("Fetching auth tokens...")
|
|
try:
|
|
csrf_token, session_id = await fetch_tokens(cookies)
|
|
except (ValueError, httpx.HTTPError) as e:
|
|
print(f"ERROR: Failed to fetch auth tokens: {e}")
|
|
print("Try running: notebooklm login")
|
|
sys.exit(1)
|
|
auth = AuthTokens(cookies=cookies, csrf_token=csrf_token, session_id=session_id)
|
|
print(f"Auth OK (CSRF length: {len(auth.csrf_token)})")
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, read=60.0)) as client:
|
|
# 1. LIST_NOTEBOOKS
|
|
print("\n--- LIST_NOTEBOOKS ---")
|
|
list_raw = await make_rpc_request(client, auth, RPCMethod.LIST_NOTEBOOKS, [])
|
|
list_chunks, list_ids, list_result = diagnose_response(
|
|
"LIST_NOTEBOOKS", list_raw, RPCMethod.LIST_NOTEBOOKS.value
|
|
)
|
|
|
|
# Extract notebook ID if not provided
|
|
if not notebook_id and list_result:
|
|
try:
|
|
if isinstance(list_result, list) and len(list_result) > 0:
|
|
first_nb = list_result[0]
|
|
if isinstance(first_nb, list):
|
|
# Nested: [[notebook_data, ...], ...]
|
|
if isinstance(first_nb[0], list):
|
|
notebook_id = first_nb[0][2] if len(first_nb[0]) > 2 else None
|
|
else:
|
|
notebook_id = first_nb[2] if len(first_nb) > 2 else None
|
|
except (IndexError, TypeError) as e:
|
|
print(f" Warning: Could not extract notebook ID from LIST_NOTEBOOKS result: {e}")
|
|
|
|
if not notebook_id:
|
|
print("\nERROR: Could not extract notebook ID from LIST_NOTEBOOKS.")
|
|
print("Please provide a notebook ID as argument:")
|
|
print(f" python {sys.argv[0]} <notebook_id>")
|
|
sys.exit(1)
|
|
|
|
print(f"\nUsing first notebook: {notebook_id}")
|
|
|
|
# 2. GET_NOTEBOOK
|
|
print("\n--- GET_NOTEBOOK ---")
|
|
get_raw = await make_rpc_request(
|
|
client,
|
|
auth,
|
|
RPCMethod.GET_NOTEBOOK,
|
|
[notebook_id, None, [2], None, 0],
|
|
source_path=f"/notebook/{notebook_id}",
|
|
)
|
|
get_chunks, get_ids, _ = diagnose_response(
|
|
"GET_NOTEBOOK", get_raw, RPCMethod.GET_NOTEBOOK.value
|
|
)
|
|
|
|
# 3. Side-by-side comparison
|
|
print(f"\n{'=' * 60}")
|
|
print(" COMPARISON")
|
|
print(f"{'=' * 60}")
|
|
print(
|
|
f" LIST_NOTEBOOKS: {len(list_raw)} bytes, {len(list_chunks)} chunks, IDs: {list_ids}"
|
|
)
|
|
print(f" GET_NOTEBOOK: {len(get_raw)} bytes, {len(get_chunks)} chunks, IDs: {get_ids}")
|
|
|
|
list_ok = RPCMethod.LIST_NOTEBOOKS.value in list_ids
|
|
get_ok = RPCMethod.GET_NOTEBOOK.value in get_ids
|
|
print(f"\n LIST_NOTEBOOKS working: {list_ok}")
|
|
print(f" GET_NOTEBOOK working: {get_ok}")
|
|
|
|
if list_ok and not get_ok:
|
|
print("\n DIAGNOSIS: LIST_NOTEBOOKS works but GET_NOTEBOOK does not.")
|
|
print(" This matches Issue #114. The GET_NOTEBOOK RPC may be:")
|
|
print(" - Returning null result data (server-side issue)")
|
|
print(" - Requiring different parameters than expected")
|
|
print(" - Affected by a server-side change not yet reflected in method IDs")
|
|
elif list_ok and get_ok:
|
|
print("\n DIAGNOSIS: Both RPCs are working. Issue may be intermittent.")
|
|
else:
|
|
print("\n DIAGNOSIS: Both RPCs are failing. Check authentication.")
|
|
|
|
|
|
def main() -> None:
|
|
"""Main entry point."""
|
|
notebook_id = sys.argv[1] if len(sys.argv) > 1 else None
|
|
|
|
print("GET_NOTEBOOK Diagnostic Tool (Issue #114)")
|
|
print("=" * 60)
|
|
|
|
asyncio.run(run_diagnosis(notebook_id))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|