{
  "name": "YouTube RAG Pipeline - Ingestion & Vectorization",
  "nodes": [
    {
      "parameters": {},
      "id": "trigger-manual",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "videoId": "={{ $json.videoId }}"
      },
      "id": "get-metadata",
      "name": "Get Metadata",
      "type": "n8n-nodes-base.youTube",
      "typeVersion": 1,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "resource": "commentThread",
        "videoId": "={{ $json.id }}",
        "limit": 20
      },
      "id": "get-comments",
      "name": "Get Comments",
      "type": "n8n-nodes-base.youTube",
      "typeVersion": 1,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const inputItem = $input.first().json;\nlet videoId = inputItem.id;\n\nif (!videoId) {\n  return [{ json: { error: \"No video ID found\" } }];\n}\n\nconst youtubePageUrl = `https://www.youtube.com/watch?v=${videoId}`;\n\ntry {\n  const response = await fetch(youtubePageUrl);\n  if (!response.ok) throw new Error(\"Failed to fetch page\");\n  const body = await response.text();\n\n  const playerResponseMatch = body.match(/ytInitialPlayerResponse = ({.*?});/);\n  if (!playerResponseMatch) throw new Error(\"No player response found\");\n\n  const ytInitialPlayerResponse = JSON.parse(playerResponseMatch[1]);\n  const captionTracks = ytInitialPlayerResponse?.captions?.playerCaptionsTracklistRenderer?.captionTracks;\n\n  if (!captionTracks || captionTracks.length === 0) {\n    return [{ json: { ...inputItem, transcript: \"[No Captions Available]\" } }];\n  }\n\n  const preferredTrack = captionTracks.find(track => track.languageCode === 'en') || captionTracks[0];\n  const transcriptResponse = await fetch(preferredTrack.baseUrl + '&fmt=json3');\n  const transcriptData = await transcriptResponse.json();\n\n  const transcript = transcriptData.events\n    .filter(event => event.segs)\n    .map(event => event.segs.map(seg => seg.utf8).join(' ')\n    .join(' ')\n    .replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n    .replace(/\\s+/g, ' ')\n    .trim();\n\n  return [{ json: { ...inputItem, transcript } }];\n\n} catch (error) {\n  return [{ json: { ...inputItem, transcript: \"[Error Fetching Transcript: \" + error.message + \"]\" } }];\n}"
      },
      "id": "fetch-transcript",
      "name": "Fetch Transcript (Code)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "content",
              "name": "content",
              "value": "={{ $json.snippet.title }}\n\n{{ $json.snippet.description }}\n\nTRANSCRIPT:\n{{ $json.transcript }}\n\nCOMMENTS:\n{{ $json.comments }}",
              "type": "string"
            }
          ]
        }
      },
      "id": "format-document",
      "name": "Format Document",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "chunkSize": 1000,
        "chunkOverlap": 200
      },
      "id": "text-splitter",
      "name": "Text Splitter",
      "type": "n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
      "typeVersion": 1,
      "position": [
        1100,
        200
      ]
    },
    {
      "parameters": {
        "model": "text-embedding-3-small"
      },
      "id": "embeddings",
      "name": "OpenAI Embeddings",
      "type": "n8n-nodes-langchain.embeddingsOpenAi",
      "typeVersion": 1,
      "position": [
        1100,
        400
      ]
    },
    {
      "parameters": {
        "mode": "insert",
        "collection": "youtube_knowledge_base",
        "options": {}
      },
      "id": "qdrant-store",
      "name": "Qdrant Vector Store",
      "type": "n8n-nodes-langchain.vectorStoreQdrant",
      "typeVersion": 1,
      "position": [
        1100,
        0
      ]
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Get Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Metadata": {
      "main": [
        [
          {
            "node": "Get Comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Comments": {
      "main": [
        [
          {
            "node": "Fetch Transcript (Code)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Transcript (Code)": {
      "main": [
        [
          {
            "node": "Format Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Document": {
      "main": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text Splitter": {
      "ai_textSplitter": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "ai_textSplitter",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Embeddings": {
      "ai_embedding": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}