nblm-core 0.2.2

Core library for NotebookLM Enterprise API client
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NotebookSource {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<NotebookSourceMetadata>,
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub settings: Option<NotebookSourceSettings>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_id: Option<NotebookSourceId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NotebookSourceMetadata {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_added_timestamp: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub word_count: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub youtube_metadata: Option<NotebookSourceYoutubeMetadata>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NotebookSourceYoutubeMetadata {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub channel_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub video_id: Option<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NotebookSourceSettings {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NotebookSourceId {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserContent {
    Web {
        #[serde(rename = "webContent")]
        web_content: WebContent,
    },
    Text {
        #[serde(rename = "textContent")]
        text_content: TextContent,
    },
    GoogleDrive {
        #[serde(rename = "googleDriveContent")]
        google_drive_content: GoogleDriveContent,
    },
    Video {
        #[serde(rename = "videoContent")]
        video_content: VideoContent,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct WebContent {
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TextContent {
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_name: Option<String>,
}

/// Google Drive content for adding sources.
///
/// # Requirements
///
/// - Use credentials initialized with `gcloud auth login --enable-gdrive-access`
/// - Ensure the authenticated account has at least viewer access to the document
/// - Provide the MIME type returned by the Drive API (e.g. `application/pdf`)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GoogleDriveContent {
    pub document_id: String,
    pub mime_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VideoContent {
    #[serde(rename = "youtubeUrl")]
    pub url: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn user_content_untagged_web() {
        let json = r#"{"webContent":{"url":"https://example.com"}}"#;
        let content: UserContent = serde_json::from_str(json).unwrap();
        match content {
            UserContent::Web { web_content } => {
                assert_eq!(web_content.url, "https://example.com");
            }
            _ => panic!("expected Web variant"),
        }
    }

    #[test]
    fn user_content_untagged_text() {
        let json = r#"{"textContent":{"content":"sample text"}}"#;
        let content: UserContent = serde_json::from_str(json).unwrap();
        match content {
            UserContent::Text { text_content } => {
                assert_eq!(text_content.content, "sample text");
            }
            _ => panic!("expected Text variant"),
        }
    }

    #[test]
    fn user_content_untagged_google_drive() {
        let json = r#"{"googleDriveContent":{"documentId":"123","mimeType":"application/vnd.google-apps.document"}}"#;
        let content: UserContent = serde_json::from_str(json).unwrap();
        match content {
            UserContent::GoogleDrive {
                google_drive_content,
            } => {
                assert_eq!(google_drive_content.document_id, "123");
                assert_eq!(
                    google_drive_content.mime_type,
                    "application/vnd.google-apps.document"
                );
            }
            _ => panic!("expected GoogleDrive variant"),
        }
    }

    #[test]
    fn user_content_untagged_video() {
        let json = r#"{"videoContent":{"youtubeUrl":"https://youtube.com/watch?v=123"}}"#;
        let content: UserContent = serde_json::from_str(json).unwrap();
        match content {
            UserContent::Video { video_content } => {
                assert_eq!(video_content.url, "https://youtube.com/watch?v=123");
            }
            _ => panic!("expected Video variant"),
        }
    }

    #[test]
    fn user_content_video_serializes_correctly() {
        let content = UserContent::Video {
            video_content: VideoContent {
                url: "https://youtube.com/watch?v=123".to_string(),
            },
        };
        let json = serde_json::to_string(&content).unwrap();
        assert!(
            json.contains("videoContent"),
            "JSON should contain videoContent, got: {}",
            json
        );
        assert!(
            json.contains(r#""youtubeUrl":"https://youtube.com/watch?v=123""#),
            "JSON should contain youtubeUrl field, got: {}",
            json
        );
    }
}