{
  "name": "Nexscope - n8n YouTube Transcript API: Create Timestamped Review Evidence (manual pilot)",
  "nodes": [
    {
      "name": "Manual start",
      "id": "manual-start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        300
      ]
    },
    {
      "name": "Research input",
      "id": "research-input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const request = {\"url\": \"https://www.youtube.com/watch?v=REPLACE_VIDEO_ID\", \"language\": \"en\", \"originalAudio\": false};\nif (Object.values(request).some(v=>typeof v==='string'&&v.includes('REPLACE'))) throw new Error('Replace all example placeholders before calling the API.');\nif(!(/^https:\\/\\/(?:www\\.)?youtube\\.com\\/watch\\?[^\\s]*$/.test(request.url)&&/[?&]v=[A-Za-z0-9_-]{11}(?:[&#]|$)/.test(request.url))&&!/^https:\\/\\/youtu\\.be\\/[A-Za-z0-9_-]{11}(?:[?#][^\\s]*)?$/.test(request.url))throw new Error('Use a public YouTube watch or youtu.be HTTPS URL with an 11-character video ID.');\nreturn [{json:request}];"
      },
      "position": [
        220,
        300
      ]
    },
    {
      "name": "Nexscope Data",
      "id": "nexscope-data",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "parameters": {
        "method": "POST",
        "url": "https://api.nexscope.ai/youtube/transcript",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 90000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "position": [
        440,
        300
      ],
      "retryOnFail": false
    },
    {
      "name": "Validate and map",
      "id": "validate-and-map",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const body = $input.first().json;\nconst request = $('Research input').first().json;\nif (typeof body.code !== 'number' || body.code !== 0) throw new Error('API business error; inspect HTTP output before any repeat request.');\nconst d = body.data;\nif (!d || !['AVAILABLE','PARTIAL'].includes(d.resourceStatus)) throw new Error('Resource unavailable or unknown; no rows exported.');\nconst cell = v => {\n  if(v == null) return '';\n  if(typeof v === 'object') v=JSON.stringify(v);\n  return typeof v === 'string' && (/^[\\t\\r\\n]/.test(v) || /^[=+\\-@]/.test(v.trimStart())) ? \"'\"+v : v;\n};\nconst sourceUrl = v => { if(typeof v!=='string' || !/^https:\\/\\/[A-Za-z0-9.-]+(?::[0-9]+)?(?:[/?#][^\\s]*)?$/.test(v)) throw new Error('Missing or invalid HTTPS source URL.'); return v; };\nconst common = {\n  resource_status:d.resourceStatus, fetched_at:d.fetchedAt ?? '', source_fetched_at:d.sourceFetchedAt ?? '',\n  observed_at_utc:new Date().toISOString(),execution_id:String($execution.id),\n  query_or_input:request.keyword ?? request.url ?? request.handle ?? request.query,\n  has_more: d.hasMore ?? '', next_cursor:d.nextCursor ?? ''\n};\nconst finish = (rows, returned) => rows.map(row=>{\n  const missing=Object.keys(row).filter(k=>row[k]==null);\n  const output={...row,...common,returned_count:returned,exported_count:rows.length,missing_fields:missing.join(';')};\n  return {json:Object.fromEntries(Object.entries(output).map(([k,v])=>[k,cell(v)]))};\n});\nconst t=d.transcript;\nif(!t || !Array.isArray(t.segments))throw new Error('No transcript segments.');\nconst languageCode = value => {\n if(typeof value!=='string')return null;\n const v=value.trim().toLowerCase().replace(/_/g,'-');\n if(v==='english')return 'en';\n return /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/.test(v)?v:null;\n};\nconst returnedLanguage=languageCode(t.language);\nif(!returnedLanguage || returnedLanguage!==languageCode(request.language))throw new Error('Returned caption language differs from request or is unknown; review before exporting.');\nconst rows=[];const seen=new Set();\nfor(const s of t.segments.slice(0,100)) {\n if(!s || typeof s.text!=='string' || !s.text.trim() || !Number.isFinite(s.startMs) || !Number.isFinite(s.endMs) || s.startMs<0 || s.endMs<s.startMs)throw new Error('Invalid caption segment.');\n const id=request.url+'|'+s.startMs+'|'+s.endMs;if(seen.has(id))continue;seen.add(id);\n const videoId=request.url.match(/[?&]v=([A-Za-z0-9_-]{11})(?:[&#]|$)/)?.[1] ?? request.url.match(/^https:\\/\\/youtu\\.be\\/([A-Za-z0-9_-]{11})(?:[?#]|$)/)?.[1];\n if(!videoId)throw new Error('Expected a watch or youtu.be URL with an 11-character video ID.');\n const link='https://www.youtube.com/watch?v='+videoId+'&t='+Math.floor(s.startMs/1000);\n rows.push({source_id:id,source_url:link,text:s.text,start_ms:s.startMs,end_ms:s.endMs,caption_language:t.language,caption_kind:t.kind});\n}\nreturn finish(rows,t.segments.length);"
      },
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Research review",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "sheets",
      "name": "Append to Google Sheets - configure then enable",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "disabled": true,
      "position": [
        880,
        300
      ]
    },
    {
      "name": "Setup notes",
      "id": "setup-notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Manual research pilot\nSelect your own Header Auth credential: Authorization = Bearer YOUR_KEY. No key belongs in this JSON. Edit Research input, leave Sheets disabled, inspect the HTTP and mapping results. Configure a scratch sheet using the companion headers before enabling writes. No scheduling, automatic pagination or retries. Reruns may charge credits and append duplicates. One API request per manual run.",
        "height": 320,
        "width": 650
      },
      "position": [
        150,
        -100
      ]
    }
  ],
  "connections": {
    "Manual start": {
      "main": [
        [
          {
            "node": "Research input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Research input": {
      "main": [
        [
          {
            "node": "Nexscope Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Nexscope Data": {
      "main": [
        [
          {
            "node": "Validate and map",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate and map": {
      "main": [
        [
          {
            "node": "Append to Google Sheets - configure then enable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
