{
  "version": "1.0.0",
  "exported_at": "2026-06-03T12:55:00.000Z",
  "project": {
    "name": "TikTok Video Details Scraper",
    "description": "Scrapes TikTok video detail pages by URL, exporting poster, nickname, post date, content, hashtags, engagement counts, region, post type, cover URL, duration, download URL, video ID, and music details. Uses a multi-URL navigation loop so all URLs listed in navigate.urls are processed. Best-effort: TikTok may block scraping with login, CAPTCHA, geo restrictions, rate limits, or changing page data structures.",
    "color": "bg-[#25f4ee]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://www.tiktok.com/@whit.laur/video/7446455969858506015"
        ],
        "color": "bg-[#4589ff]",
        "tags": [
          "tiktok",
          "video-detail",
          "multi-url"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 480,
      "position_y": 220,
      "config": {
        "timeout": 45
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "selector": "article",
        "timeout": 45,
        "visible": true
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "waitForCompletion": true,
        "timeout": 15,
        "jsCode": "(() => {\n  const text = (el) => (el && (el.innerText || el.textContent) || '').trim();\n  const attr = (sel, name) => document.querySelector(sel)?.getAttribute(name) || '';\n  const meta = (name) => document.querySelector(`meta[name=\"${name}\"]`)?.content || document.querySelector(`meta[property=\"${name}\"]`)?.content || '';\n  const url = location.href;\n  const videoId = (url.match(/\\/video\\/(\\d+)/) || [])[1] || '';\n  const usernameFromUrl = (location.pathname.match(/@([^/]+)/) || [])[1] || '';\n\n  function safeJsonParse(s) {\n    try { return JSON.parse(s); } catch (_) { return null; }\n  }\n\n  function findItemById(obj, id, seen = new Set(), depth = 0) {\n    if (!obj || typeof obj !== 'object' || seen.has(obj) || depth > 18) return null;\n    seen.add(obj);\n    if (id && obj[id] && typeof obj[id] === 'object') {\n      const candidate = obj[id];\n      if (candidate.desc || candidate.video || candidate.stats || candidate.author || candidate.createTime) return candidate;\n    }\n    if ((obj.id === id || obj.aweme_id === id) && (obj.desc || obj.video || obj.stats || obj.author || obj.createTime)) return obj;\n    for (const k of Object.keys(obj)) {\n      const found = findItemById(obj[k], id, seen, depth + 1);\n      if (found) return found;\n    }\n    return null;\n  }\n\n  let item = null;\n  for (const s of Array.from(document.scripts)) {\n    const raw = s.textContent || '';\n    if (!raw || (videoId && !raw.includes(videoId))) continue;\n    const parsed = safeJsonParse(raw);\n    if (!parsed) continue;\n    item = findItemById(parsed, videoId);\n    if (item) break;\n  }\n\n  const ogDesc = meta('og:description');\n  const nameDesc = meta('description');\n  const pageDesc = ogDesc || nameDesc;\n  const title = meta('og:title') || document.title || '';\n  const authorLink = Array.from(document.querySelectorAll('a[href*=\"/@\"]')).find(a => !a.href.includes('/video/'));\n  const authorHref = authorLink?.href || '';\n  const authorFromDom = authorHref.match(/@([^/?]+)/)?.[1] || usernameFromUrl;\n  const domNickname = title.replace(/\\s+on TikTok\\s*$/i, '').trim();\n\n  function compactCount(v) {\n    if (v == null || v === '') return '';\n    if (typeof v === 'number') return String(v);\n    const str = String(v).trim();\n    const m = str.match(/^([\\d,.]+)\\s*([KMB])?$/i);\n    if (!m) return str;\n    const n = parseFloat(m[1].replace(/,/g, ''));\n    const mult = !m[2] ? 1 : m[2].toUpperCase() === 'K' ? 1000 : m[2].toUpperCase() === 'M' ? 1000000 : 1000000000;\n    return String(Math.round(n * mult));\n  }\n\n  function countByE2E(e2e, ariaWord) {\n    const direct = text(document.querySelector(`[data-e2e=\"${e2e}\"]`));\n    if (direct) return compactCount(direct);\n    const btn = Array.from(document.querySelectorAll('button[aria-label]')).find(b => (b.getAttribute('aria-label') || '').toLowerCase().includes(ariaWord));\n    const aria = btn?.getAttribute('aria-label') || '';\n    const m = aria.match(/([\\d,.]+\\s*[KMB]?)/i);\n    return m ? compactCount(m[1]) : '';\n  }\n\n  function formatDate(createTime) {\n    if (!createTime) return '';\n    const n = Number(createTime);\n    if (!Number.isFinite(n)) return '';\n    const d = new Date(n * 1000);\n    const pad = x => String(x).padStart(2, '0');\n    return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;\n  }\n\n  function secondsFromDurationText(s) {\n    const m = String(s || '').match(/\\/\\s*(\\d{1,2}):(\\d{2})(?::(\\d{2}))?/);\n    if (!m) return '';\n    if (m[3]) return String(Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]));\n    return String(Number(m[1]) * 60 + Number(m[2]));\n  }\n\n  const itemAuthor = item?.author || item?.authorInfo || {};\n  const itemStats = item?.stats || item?.statistics || item?.statsV2 || {};\n  const itemVideo = item?.video || {};\n  const itemMusic = item?.music || item?.musicInfo || {};\n\n  let content = item?.desc || text(document.querySelector('[data-e2e=\"video-desc\"]')) || '';\n  if (!content && nameDesc) {\n    const qm = nameDesc.match(/[“\"](.+?)[”\"]/);\n    content = qm ? qm[1] : nameDesc;\n  }\n\n  const hashtags = Array.from(new Set((content.match(/#[\\p{L}\\p{N}_]+/gu) || []).concat(Array.from(document.querySelectorAll('a[href*=\"/tag/\"]')).map(a => text(a)).filter(Boolean)))).join(' ');\n  const musicLink = Array.from(document.querySelectorAll('a[href*=\"/music/\"]')).find(a => a.href.includes('/music/'));\n  const progressText = Array.from(document.querySelectorAll('p,span,div')).map(text).find(t => /\\d{1,2}:\\d{2}\\s*\\/\\s*\\d{1,2}:\\d{2}/.test(t)) || '';\n\n  window.__USCRAPER_TIKTOK_VIDEO_DETAILS__ = {\n    url,\n    poster: JSON.stringify([authorFromDom || itemAuthor.uniqueId || itemAuthor.id || '', 'video']),\n    tiktoker_nikname: itemAuthor.nickname || itemAuthor.uniqueId || domNickname || authorFromDom || '',\n    post_date: formatDate(item?.createTime || item?.create_time),\n    content,\n    hashtag: hashtags,\n    like_num: compactCount(itemStats.diggCount || itemStats.digg_count || countByE2E('like-count', 'like')),\n    comment_num: compactCount(itemStats.commentCount || itemStats.comment_count || countByE2E('comment-count', 'comment')),\n    views_num: compactCount(itemStats.playCount || itemStats.play_count || itemStats.viewCount || itemStats.view_count || ''),\n    forward_num: compactCount(itemStats.shareCount || itemStats.share_count || countByE2E('share-count', 'share')),\n    bookmark_num: compactCount(itemStats.collectCount || itemStats.collect_count || itemStats.bookmarkCount || itemStats.favoriteCount || countByE2E('undefined-count', 'favorite')),\n    Region: item?.locationCreated || item?.region || item?.regionCode || '',\n    Post_type: 'Video',\n    covers_url: itemVideo.cover || itemVideo.originCover || itemVideo.dynamicCover || attr('meta[property=\"og:image\"]', 'content') || document.querySelector('article img')?.src || '',\n    video_duration: String(itemVideo.duration || item?.duration || secondsFromDurationText(progressText) || ''),\n    video_download: itemVideo.downloadAddr || itemVideo.downloadUrl || itemVideo.playAddr || itemVideo.playUrl || '',\n    video_id: videoId || item?.id || item?.aweme_id || '',\n    music_name: itemMusic.title || text(musicLink) || '',\n    music_author: itemMusic.authorName || itemMusic.author || itemAuthor.nickname || domNickname || '',\n    music_URL: musicLink?.href || (itemMusic.id ? `https://www.tiktok.com/music/${encodeURIComponent(itemMusic.title || 'music')}-${itemMusic.id}` : '')\n  };\n})();"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "rowSelector": "body",
        "fileName": "tiktok-video-details-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "url",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.url || location.href",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "poster",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.poster || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "tiktoker_nikname",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.tiktoker_nikname || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "post_date",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.post_date || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "content",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.content || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "hashtag",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.hashtag || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "like_num",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.like_num || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "comment_num",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.comment_num || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "views_num",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.views_num || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "forward_num",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.forward_num || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "bookmark_num",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.bookmark_num || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Region",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.Region || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Post_type",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.Post_type || 'Video'",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "covers_url",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.covers_url || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "video_duration",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.video_duration || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "video_download",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.video_download || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "video_id",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.video_id || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "music_name",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.music_name || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "music_author",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.music_author || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "music_URL",
            "selector": "window.__USCRAPER_TIKTOK_VIDEO_DETAILS__?.music_URL || ''",
            "attribute": "text",
            "isJs": true
          }
        ],
        "color": "bg-[#42be65]",
        "tags": [
          "csv",
          "structured-data"
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "color": "bg-[#ff832b]",
        "tags": [
          "multi-url-loop"
        ]
      }
    }
  ],
  "connections": [
    {
      "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": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "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": "loop-continue-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 116,
      "width": 1040,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1128,
      "position_y": 116,
      "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": 1488,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1848,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes TikTok video detail pages by URL, exporting poster, nickname, post date, content, hashtags, engagement counts, region, post type, cover URL, duration, download URL, video ID, and music details. Uses a multi-URL navigation loop so all URLs listed in navigate.urls are processed. Best-effort: TikTok may block scraping with login, CAPTCHA, geo restrictions, rate limits, or changing page data structures.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-navigate-1",
      "element_type": "note",
      "title": "Note: Navigate",
      "content": "Multi-URL loop over 1 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 320,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => {\n  const text = (el) => (el && (el.innerText || el.textContent) || '').trim();\n  const attr ...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1400,
      "position_y": 200,
      "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 (url, poster, tiktoker_nikname, post_date, content). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 200,
      "width": 340,
      "height": 130,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 2120,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}