Skip to content

Conversation

@Anderson-RC
Copy link

Fix: GetNotes API Response Parsing

Summary

The nlm notes command crashes with a nil pointer dereference when listing notes from a NotebookLM notebook. This PR fixes the crash and restores full functionality by implementing correct parsing of the API response, including extraction of note content.

This issue seems to have been caused by the (undocumented) NotebookLM API changing the schema of the data returned by this function. The default unmarshaling couldn't parse the response, and so the nil pointer caused a full program crash. This fix adds a schema to the data (instead of inline assignment) to place the notes content in.

The note's content was also being discarded in the response. See other PR for possible extension to make use of it.

Issue

When running nlm notes <notebook-id>, the command panics:

panic: runtime error: invalid memory address or nil pointer dereference
goroutine 1 [running]:
main.listNotes(...)
    cmd/nlm/main.go:1032

Root Cause

The NotebookLM API returns note data in a custom nested array format that differs from the expected protobuf structure. The beprotojson unmarshaller was unable to correctly parse this format, resulting in:

  1. note.GetMetadata() returning nil
  2. note.Title being empty (populated from wrong field position)
  3. note.Content not being extracted at all

API Response Structure

The API returns notes in this nested array structure:

[
  [                                    <- notes array (position 0)
    [                                  <- NoteEntry
      "note-uuid",                     <- position 0: source_id
      [                                <- position 1: NoteDetails
        "note-uuid",                   <- details[0]: id (duplicate)
        "content text...",             <- details[1]: content
        [type, "id", [sec, nanos]],    <- details[2]: NoteTimestampMetadata
        null,                          <- details[3]: reserved
        "Note Title"                   <- details[4]: title
      ]
    ],
    ...
  ],
  [additional-metadata]                <- ignored
]

The protobuf Source message expects Title at field 2, but the API places it at position 4 of the nested details array. Content is at position 1.

Changes

1. gen/notebooklm/v1alpha1/notes_response.pb.go - API Structure Documentation

Created new file documenting the expected API message types with proper position-to-field mappings:

  • NoteEntry - Outer array [source_id, details]
  • NoteDetails - Inner array [id, content, metadata, null, title]
  • NoteTimestampMetadata - Metadata [type, id, timestamp]
  • TimestampPair - Timestamp [seconds, nanos]

This serves as documentation for the API structure and could be extended for beprotojson compatibility in the future.

2. gen/notebooklm/v1alpha1/notebooklm.pb.go - Content field

Added Content field to the Source struct to store note body text:

type Source struct {
    // ... existing fields ...
    Content  string `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"`
}

3. gen/service/LabsTailwindOrchestrationService_client.go - Refactored parser

Refactored GetNotes with:

  1. Detailed inline documentation of the API response structure
  2. Extracted parseNoteEntry helper function for cleaner, maintainable code
  3. Content extraction from position 1 of the details array
  4. Fallback to beprotojson.Unmarshal if API format changes
// parseNoteEntry parses a single note entry from the API response.
// This matches the structure documented in notes_response.pb.go.
func parseNoteEntry(entry interface{}) *notebooklmv1alpha1.Source {
    noteArr, ok := entry.([]interface{})
    if !ok || len(noteArr) < 2 {
        return nil
    }

    note := &notebooklmv1alpha1.Source{}

    // Position 0: Source ID
    if sourceID, ok := noteArr[0].(string); ok {
        note.SourceId = &notebooklmv1alpha1.SourceId{SourceId: sourceID}
    }

    // Position 1: Details array [id, content, metadata, null, title]
    detailsArr, ok := noteArr[1].([]interface{})
    if !ok || len(detailsArr) < 5 {
        return note
    }

    // details[1]: Content
    if content, ok := detailsArr[1].(string); ok {
        note.Content = content
    }

    // details[4]: Title
    if title, ok := detailsArr[4].(string); ok {
        note.Title = title
    }

    // details[2]: Metadata with timestamp
    // ... (see full implementation)

    return note
}

4. cmd/nlm/main.go - Nil guards in listNotes

Added defensive nil checks when accessing note metadata to prevent crashes:

lastModified := "unknown"
if meta := note.GetMetadata(); meta != nil && meta.LastModifiedTime != nil {
    lastModified = meta.LastModifiedTime.AsTime().Format(time.RFC3339)
}

Also improved output formatting:

  • Extract proper UUID from SourceId wrapper
  • Show "(untitled)" for notes without titles
  • Added "No notes found" message for empty notebooks

Testing

Before Fix

$ nlm notes <notebook-id>
panic: runtime error: invalid memory address or nil pointer dereference

After Fix

$ nlm notes <notebook-id>
ID                                   TITLE                    LAST MODIFIED
abc12345-6789-abcd-ef01-234567890abc Research Notes           2025-12-01T10:00:00Z
def67890-abcd-1234-5678-90abcdef1234 Literature Review        2025-12-02T14:30:00Z
ghi11111-2222-3333-4444-555566667777 Project Outline          2025-12-03T09:15:00Z
jkl22222-3333-4444-5555-666677778888 (untitled)               unknown

Notes are correctly displayed with titles from the right API position, timestamps parsed correctly, and draft notes show "(untitled)" with "unknown" timestamp.

Files Changed

File Change
gen/notebooklm/v1alpha1/notes_response.pb.go NEW - API structure documentation
gen/notebooklm/v1alpha1/notebooklm.pb.go Added Content field to Source
gen/service/LabsTailwindOrchestrationService_client.go Refactored with parseNoteEntry helper, content extraction
cmd/nlm/main.go Nil guards in listNotes

Notes

  • The fix includes a fallback to the standard beprotojson.Unmarshal in case the API format changes
  • Draft content shows as "unknown" with "unknown timestamp, at least on my own Notebook.
  • Note content is now extracted and stored in Source.Content for downstream use (see proceeding PR)

Related

  • This appears to be caused by a NotebookLM API update
  • No official API documentation exists (that I could find) for the consumer NotebookLM version; this fix was developed by observing actual API responses. Let me know if this documentation actually exists because I couldn't find it anywhere.

…cks to improve `listNotes` command robustness.
- Create notes_response.pb.go documenting expected API message types
- Extract parseNoteEntry helper function for cleaner code
- Add Content field to Source struct for note body text
- Add detailed API structure comments in GetNotes client
- Keep fallback to beprotojson for API format changes
- Update FIX_GETNOTES_PARSING.md with implementation details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant