{
  "version": "1.0.0",
  "exported_at": "2026-05-31T12:30:00.000Z",
  "project": {
    "name": "Google Play Review Scraper",
    "description": "Scrapes publicly visible Google Play app reviews from app detail/review URLs. Extracts app name, developer/company, page URL, reviewer name, review date, star rating, helpful vote count, review comments, and app version when available. Google Play uses infinite scrolling for reviews, so this template starts an asynchronous review loader, waits for additional reviews to load, then exports all loaded review cards. Edit navigate.urls to scrape multiple Google Play apps.",
    "color": "bg-[#42be65]",
    "template_id": "ai-generated-google-play-review-scraper"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://play.google.com/store/apps/details?id=com.target.ui&hl=en&showAllReviews=true"
        ],
        "color": "bg-[#4589ff]"
      }
    },
    {
      "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": 30,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "duration": 3,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "jsCode": "(() => {\n  window.__uscraperGooglePlayReviewLoaderStatus = {\n    started: true,\n    done: false,\n    reviewCount: document.querySelectorAll('div.RHo1pe').length,\n    scrolls: 0,\n    stableRounds: 0\n  };\n\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\n  const getReviewCount = () => document.querySelectorAll('div.RHo1pe').length;\n\n  const clickMatching = (pattern) => {\n    const nodes = Array.from(document.querySelectorAll('button, a, div[role=\"button\"]'));\n    const target = nodes.find((el) => pattern.test((el.textContent || el.getAttribute('aria-label') || '').trim()));\n    if (target) {\n      try {\n        target.click();\n        return true;\n      } catch (e) {\n        return false;\n      }\n    }\n    return false;\n  };\n\n  const expandLongReviews = () => {\n    Array.from(document.querySelectorAll('button, div[role=\"button\"]'))\n      .filter((el) => /show full review|full review|more/i.test((el.textContent || el.getAttribute('aria-label') || '').trim()))\n      .forEach((el) => {\n        try { el.click(); } catch (e) {}\n      });\n  };\n\n  const getScroller = () => {\n    const dialog = document.querySelector('div[role=\"dialog\"]');\n    if (dialog) {\n      const scrollables = Array.from(dialog.querySelectorAll('div')).filter((el) => el.scrollHeight > el.clientHeight + 120);\n      if (scrollables.length) {\n        return scrollables.sort((a, b) => b.scrollHeight - a.scrollHeight)[0];\n      }\n      return dialog;\n    }\n    return document.scrollingElement || document.documentElement || document.body;\n  };\n\n  window.__uscraperGooglePlayReviewLoaderPromise = (async () => {\n    clickMatching(/see all reviews|all reviews/i);\n    await sleep(1800);\n\n    let previousCount = getReviewCount();\n    let stableRounds = 0;\n    const maxScrolls = 90;\n\n    for (let i = 0; i < maxScrolls && stableRounds < 10; i++) {\n      expandLongReviews();\n\n      const scroller = getScroller();\n      try {\n        scroller.scrollTop = scroller.scrollHeight;\n      } catch (e) {}\n      try {\n        window.scrollTo(0, document.body.scrollHeight);\n      } catch (e) {}\n\n      await sleep(900);\n\n      const currentCount = getReviewCount();\n      if (currentCount <= previousCount) {\n        stableRounds += 1;\n      } else {\n        stableRounds = 0;\n        previousCount = currentCount;\n      }\n\n      window.__uscraperGooglePlayReviewLoaderStatus = {\n        started: true,\n        done: false,\n        reviewCount: currentCount,\n        scrolls: i + 1,\n        stableRounds\n      };\n    }\n\n    expandLongReviews();\n    window.__uscraperGooglePlayReviewLoaderStatus = {\n      started: true,\n      done: true,\n      reviewCount: getReviewCount(),\n      scrolls: window.__uscraperGooglePlayReviewLoaderStatus.scrolls,\n      stableRounds\n    };\n  })();\n\n  return 'Started Google Play asynchronous review loader';\n})();",
        "waitForCompletion": true,
        "timeout": 10,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "duration": 90,
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "selector": "div.RHo1pe",
        "timeout": 45,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 220,
      "config": {
        "rowSelector": "div.RHo1pe",
        "fileName": "google-play-review-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "app_name",
            "selector": "(() => { const selectors = ['h1 span', 'h1', '[itemprop=\"name\"]']; for (const selector of selectors) { const el = document.querySelector(selector); const text = el && el.textContent ? el.textContent.trim() : ''; if (text) return text; } return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "company_name",
            "selector": "(() => { const selectors = ['a[href*=\"/store/apps/dev\"]', '.Vbfug a', 'div.Vbfug']; for (const selector of selectors) { const el = document.querySelector(selector); const text = el && el.textContent ? el.textContent.trim() : ''; if (text) return text; } return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "page_url",
            "selector": "location.href",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "reviewer_name",
            "selector": ".X5PpBb",
            "attribute": "text"
          },
          {
            "name": "review_date",
            "selector": ".bp9Aid",
            "attribute": "text"
          },
          {
            "name": "star_rating",
            "selector": "(() => { const el = ROW.querySelector('[role=\"img\"][aria-label*=\"Rated\"], [role=\"img\"][aria-label*=\"stars\"], .iXRFPc'); const label = el ? (el.getAttribute('aria-label') || '') : ''; const match = label.match(/(?:Rated\\s*)?([0-5](?:\\.\\d)?)/i); return match ? match[1] : label; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "helpful",
            "selector": "(() => { const candidates = Array.from(ROW.querySelectorAll('.AJTPZc, div, span')).map((el) => (el.textContent || '').trim()).filter(Boolean); const helpfulText = candidates.find((text) => /\\d[\\d,]*\\s+(people|person).*helpful/i.test(text)) || candidates.find((text) => /^\\d[\\d,]*$/.test(text)) || ''; const match = helpfulText.match(/\\d[\\d,]*/); return match ? match[0] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "comments",
            "selector": ".h3YV2d",
            "attribute": "text"
          },
          {
            "name": "app_version",
            "selector": "(() => { const text = ROW.innerText || ''; const match = text.match(/App version\\s*[:\\n]\\s*([^\\n]+)/i) || text.match(/Version\\s+([0-9][^\\s\\n]*)/i); return match ? match[1].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_id",
            "selector": "",
            "attribute": "data-review-id"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 220,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "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": "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": "sleep-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-2",
      "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": "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": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "sleep-2",
          "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": 2208,
      "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": 2568,
      "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 publicly visible Google Play app reviews from app detail/review URLs. Extracts app name, developer/company, page URL, reviewer name, review date, star rating, helpful vote count, review comments, and app version when available. Google Play uses infinite scrolling for reviews, so this template starts an asynchronous review loader, waits for additional reviews to load, then exports all loaded review cards. Edit navigate.urls to scrape multiple Google Play apps.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => {\n  window.__uscraperGooglePlayReviewLoaderStatus = {\n    started: true,\n    done: false,\n   ...` 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 (app_name, company_name, page_url, star_rating, helpful). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 200,
      "width": 340,
      "height": 132,
      "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": 2840,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}