{
  "version": "1.0.0",
  "exported_at": "2026-05-31T13:45:00.000Z",
  "project": {
    "name": "TikTok Search Scraper Login Required",
    "description": "Best-effort TikTok search-results scraper for the keyword URL shown in the Octoparse template. Login is required: run with a UScraper/Chrome profile already signed in to TikTok. The workflow opens TikTok search, switches to the Videos tab, performs infinite scrolling, normalizes loaded video links into scraper rows, and exports video/profile metadata to CSV. If TikTok blocks access, shows skeleton cards, CAPTCHA, or the session is not logged in, the template writes a diagnostic fallback row instead of failing.",
    "color": "bg-[#ff0050]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser viewport size",
      "position_x": 100,
      "position_y": 260,
      "config": {
        "width": 1920,
        "height": 1080,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 460,
      "position_y": 260,
      "config": {
        "url": "https://www.tiktok.com/search?lang=en&q=dancing&t=1761204190553",
        "color": "bg-[#4589ff]",
        "tags": [
          "tiktok",
          "search",
          "login-required"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 860,
      "position_y": 260,
      "config": {
        "timeout": 45,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1220,
      "position_y": 260,
      "config": {
        "selector": "body",
        "timeout": 45,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "click-1",
      "block_type": "process",
      "title": "Click",
      "description": "Click on element",
      "position_x": 1580,
      "position_y": 260,
      "config": {
        "selector": "//button[.//*[normalize-space()='Videos'] or normalize-space()='Videos']",
        "timeout": 20,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1940,
      "position_y": 260,
      "config": {
        "duration": 4,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 2300,
      "position_y": 260,
      "config": {
        "jsCode": "(async () => {\n  const sleep = (ms) => new Promise(r => setTimeout(r, ms));\n  let lastHeight = 0;\n  let stableRounds = 0;\n  const maxScrolls = 45;\n  for (let i = 0; i < maxScrolls; i++) {\n    window.scrollTo(0, document.body.scrollHeight);\n    await sleep(1600);\n    const height = document.body.scrollHeight;\n    const videoCount = document.querySelectorAll('a[href*=\"/video/\"]').length;\n    if (height === lastHeight) stableRounds++; else stableRounds = 0;\n    lastHeight = height;\n    if (videoCount >= 80) break;\n    if (stableRounds >= 4) break;\n  }\n\n  const existing = document.querySelector('#uscraper-tiktok-normalized-results');\n  if (existing) existing.remove();\n\n  const container = document.createElement('div');\n  container.id = 'uscraper-tiktok-normalized-results';\n  container.setAttribute('data-uscraper-generated', 'true');\n  container.style.display = 'none';\n  document.body.appendChild(container);\n\n  const keyword = new URLSearchParams(location.search).get('q') || '';\n  const seen = new Set();\n  const anchors = Array.from(document.querySelectorAll('a[href*=\"/video/\"]'));\n\n  function cleanText(value) {\n    return String(value || '').replace(/\\s+/g, ' ').trim();\n  }\n\n  function closestCard(anchor) {\n    return anchor.closest('[data-e2e*=\"search-card\"], [data-e2e*=\"video\"], [class*=\"DivItemContainer\"], [class*=\"DivSearchVideoContainer\"], [class*=\"SearchVideo\"], [class*=\"DivWrapper\"], div') || anchor.parentElement || anchor;\n  }\n\n  function usernameFromUrl(url) {\n    try {\n      const part = new URL(url, location.href).pathname.split('/').find(p => p.startsWith('@'));\n      return part ? part.replace('@', '') : '';\n    } catch (e) {\n      return '';\n    }\n  }\n\n  function profileFromUrl(url) {\n    const name = usernameFromUrl(url);\n    return name ? 'https://www.tiktok.com/@' + name + '?lang=en' : '';\n  }\n\n  function makeRowFromAnchor(anchor) {\n    const href = new URL(anchor.getAttribute('href') || anchor.href || '', location.href).href;\n    if (!href || seen.has(href)) return;\n    seen.add(href);\n\n    const card = closestCard(anchor);\n    const cardText = cleanText(card.innerText || anchor.innerText || '');\n    const title = cleanText(anchor.getAttribute('title') || anchor.getAttribute('aria-label') || card.querySelector('img[alt]')?.getAttribute('alt') || cardText);\n    const hashtags = Array.from(new Set((cardText.match(/#[\\p{L}\\p{N}_]+/gu) || []))).join(' ');\n    const viewMatch = cardText.match(/(?:^|\\s)(\\d+(?:\\.\\d+)?[KMB]?)(?=\\s*(?:views?|View|$))/i) || cardText.match(/\\b\\d+(?:\\.\\d+)?[KMB]\\b/);\n    const dateMatch = cardText.match(/\\b\\d+\\s?(?:s|m|h|d|w|mo|y)\\s+ago\\b/i) || cardText.match(/\\b\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}\\b/);\n    const avatar = card.querySelector('img[src*=\"avatar\"], img[src*=\"avt\"], img')?.src || '';\n    const followerMatch = cardText.match(/\\b\\d+(?:\\.\\d+)?[KMB]?\\s+Followers?\\b/i);\n    const verified = card.querySelector('[aria-label*=\"verified\" i], svg[aria-label*=\"verified\" i], [data-e2e*=\"verified\"]') ? 'Yes' : 'No';\n    const userName = usernameFromUrl(href);\n\n    const row = document.createElement('div');\n    row.setAttribute('data-uscraper-tiktok-row', 'true');\n    row.dataset.scrapeStatus = 'ok';\n    row.dataset.inputKeyword = keyword;\n    row.dataset.pageTitle = document.title || '';\n    row.dataset.pageUrl = location.href;\n    row.dataset.videoTitle = title;\n    row.dataset.videoUrl = href;\n    row.dataset.viewCount = viewMatch ? viewMatch[1] || viewMatch[0] : '';\n    row.dataset.hashtags = hashtags;\n    row.dataset.tiktoker = userName;\n    row.dataset.tiktokerUrl = profileFromUrl(href);\n    row.dataset.datePublished = dateMatch ? dateMatch[0] : '';\n    row.dataset.currentTime = new Date().toISOString();\n    row.dataset.userName = userName;\n    row.dataset.userProfileUrl = profileFromUrl(href);\n    row.dataset.userAvatarUrl = avatar;\n    row.dataset.userIntro = '';\n    row.dataset.userSubtitle = '';\n    row.dataset.followerCount = followerMatch ? followerMatch[0] : '';\n    row.dataset.ifVerified = verified;\n    container.appendChild(row);\n  }\n\n  anchors.forEach(makeRowFromAnchor);\n\n  if (!container.querySelector('[data-uscraper-tiktok-row]')) {\n    const diagnostic = document.createElement('div');\n    diagnostic.setAttribute('data-uscraper-tiktok-row', 'true');\n    diagnostic.dataset.scrapeStatus = 'blocked_or_login_required_no_video_links_found';\n    diagnostic.dataset.inputKeyword = keyword;\n    diagnostic.dataset.pageTitle = document.title || '';\n    diagnostic.dataset.pageUrl = location.href;\n    diagnostic.dataset.videoTitle = '';\n    diagnostic.dataset.videoUrl = '';\n    diagnostic.dataset.viewCount = '';\n    diagnostic.dataset.hashtags = '';\n    diagnostic.dataset.tiktoker = '';\n    diagnostic.dataset.tiktokerUrl = '';\n    diagnostic.dataset.datePublished = '';\n    diagnostic.dataset.currentTime = new Date().toISOString();\n    diagnostic.dataset.userName = '';\n    diagnostic.dataset.userProfileUrl = '';\n    diagnostic.dataset.userAvatarUrl = '';\n    diagnostic.dataset.userIntro = cleanText(document.body.innerText || '').slice(0, 500);\n    diagnostic.dataset.userSubtitle = 'TikTok did not expose search result video links. Confirm the browser profile is logged in and not blocked by CAPTCHA/region restrictions.';\n    diagnostic.dataset.followerCount = '';\n    diagnostic.dataset.ifVerified = '';\n    container.appendChild(diagnostic);\n  }\n\n  window.scrollTo(0, 0);\n  return container.querySelectorAll('[data-uscraper-tiktok-row]').length;\n})()",
        "waitForCompletion": true,
        "timeout": 150,
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 2660,
      "position_y": 260,
      "config": {
        "selector": "[data-uscraper-tiktok-row]",
        "timeout": 20,
        "visible": false,
        "color": "bg-[#42be65]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 3020,
      "position_y": 260,
      "config": {
        "rowSelector": "[data-uscraper-tiktok-row]",
        "fileName": "tiktok-busqueda-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "create",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "scrape_status",
            "selector": "ROW.dataset.scrapeStatus || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "input_keyword",
            "selector": "ROW.dataset.inputKeyword || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "page_title",
            "selector": "ROW.dataset.pageTitle || document.title || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "page_url",
            "selector": "ROW.dataset.pageUrl || location.href",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "video_title",
            "selector": "ROW.dataset.videoTitle || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "video_url",
            "selector": "ROW.dataset.videoUrl || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "view_count",
            "selector": "ROW.dataset.viewCount || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "hashtags",
            "selector": "ROW.dataset.hashtags || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "tiktoker",
            "selector": "ROW.dataset.tiktoker || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "tiktoker_url",
            "selector": "ROW.dataset.tiktokerUrl || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "date_published",
            "selector": "ROW.dataset.datePublished || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "current_time",
            "selector": "ROW.dataset.currentTime || new Date().toISOString()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "user_name",
            "selector": "ROW.dataset.userName || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "user_profile_url",
            "selector": "ROW.dataset.userProfileUrl || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "user_avatar_url",
            "selector": "ROW.dataset.userAvatarUrl || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "user_intro",
            "selector": "ROW.dataset.userIntro || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "user_subtitle",
            "selector": "ROW.dataset.userSubtitle || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "follower_count",
            "selector": "ROW.dataset.followerCount || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "if_verified",
            "selector": "ROW.dataset.ifVerified || ''",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 3380,
      "position_y": 260,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "click-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "click-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-2",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "end-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 28,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 388,
      "position_y": 156,
      "width": 2520,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1508,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "click-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 2228,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2948,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-control",
      "element_type": "group",
      "title": "Control Flow",
      "color": "#8d8d8d",
      "position_x": 3308,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort TikTok search-results scraper for the keyword URL shown in the Octoparse template. Login is required: run with a UScraper/Chrome profile already signed in to TikTok. The workflow opens TikTok search, switches to the Videos tab, performs infinite scrolling, normalizes loaded video links into scraper rows, and exports video/profile metadata to CSV. If TikTok blocks access, shows skeleton cards, CAPTCHA, or the session is not logged in, the template writes a diagnostic fallback row instead of failing.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-click-1",
      "element_type": "note",
      "title": "Note: Click",
      "content": "Uses XPath `//button[.//*[normalize-space()='Videos'] or normalize-space()='Videos']`. XPath breaks easily if DOM structure changes.",
      "color": "#ee5396",
      "position_x": 1780,
      "position_y": 240,
      "width": 340,
      "height": 124,
      "z_index": 22,
      "data": {
        "block_id": "click-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(async () => {\n  const sleep = (ms) => new Promise(r => setTimeout(r, ms));\n  let lastHeight = 0;\n  ...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2500,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (scrape_status, input_keyword, page_title, page_url, video_title). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 3220,
      "position_y": 240,
      "width": 340,
      "height": 135,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    }
  ]
}