{
  "version": "1.0.0",
  "exported_at": "2026-06-01T10:55:00.000Z",
  "project": {
    "name": "Google Play APP Avis Scraper",
    "description": "Scrapes Google Play app review data from multiple app detail page URLs. Uses a multi-URL navigation loop, skips unavailable/404 app pages, opens the reviews dialog when available, scrolls it to load reviews, and appends extracted review rows to google_play_app_avis_scraper.csv. Best-effort selectors are used because Google Play has dynamic DOM classes and the first provided sample URL returned 404 in analysis.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 240,
      "config": {
        "urls": [
          "https://play.google.com/store/apps/details?id=com.qoolandgames.swordchronicles.gp.us&hl=fr&gl=US",
          "https://play.google.com/store/apps/details?id=com.YoStarEN.Arknights&hl=fr&gl=US",
          "https://play.google.com/store/apps/details?id=com.proximabeta.nikke&hl=fr&gl=US"
        ],
        "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": 240,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 840,
      "position_y": 240,
      "config": {
        "selector": "h1 span, h1"
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 840,
      "position_y": 600,
      "config": {}
    },
    {
      "block_id": "element-exists-2",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 1200,
      "position_y": 240,
      "config": {
        "selector": "(//button[contains(@aria-label, 'Afficher tous les avis') or contains(@aria-label, 'See all reviews') or .//*[contains(normalize-space(.), 'Afficher tous les avis') or contains(normalize-space(.), 'See all reviews')]])[1]"
      }
    },
    {
      "block_id": "click-1",
      "block_type": "process",
      "title": "Click",
      "description": "Click on element",
      "position_x": 1560,
      "position_y": 240,
      "config": {
        "selector": "(//button[contains(@aria-label, 'Afficher tous les avis') or contains(@aria-label, 'See all reviews') or .//*[contains(normalize-space(.), 'Afficher tous les avis') or contains(normalize-space(.), 'See all reviews')]])[1]",
        "timeout": 15
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1920,
      "position_y": 240,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 2280,
      "position_y": 240,
      "config": {
        "jsCode": "(async () => { const sleep = ms => new Promise(r => setTimeout(r, ms)); const dialog = document.querySelector('div[role=\"dialog\"]'); const roots = dialog ? [dialog, ...dialog.querySelectorAll('*')] : [document.scrollingElement, document.documentElement, document.body]; const scrollables = roots.filter(el => el && el.scrollHeight > el.clientHeight + 100); const scroller = scrollables.sort((a, b) => b.scrollHeight - a.scrollHeight)[0] || document.scrollingElement || document.documentElement; let stable = 0; let lastCount = 0; for (let i = 0; i < 80 && stable < 8; i++) { Array.from(document.querySelectorAll('button')).filter(b => /Afficher.*(avis|plus)|Lire la suite|See.*(review|more)|Show.*(review|more)/i.test((b.innerText || b.getAttribute('aria-label') || '').trim())).forEach(b => { try { b.click(); } catch (e) {} }); try { scroller.scrollTop = scroller.scrollHeight; } catch (e) {} window.scrollTo(0, document.body.scrollHeight); await sleep(1200); const count = document.querySelectorAll('div.RHo1pe').length; if (count === lastCount) { stable++; } else { stable = 0; lastCount = count; } } return { reviewsLoaded: document.querySelectorAll('div.RHo1pe').length }; })();",
        "waitForCompletion": true,
        "timeout": 120
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 2640,
      "position_y": 240,
      "config": {
        "selector": "div.RHo1pe",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 3000,
      "position_y": 240,
      "config": {
        "rowSelector": "div.RHo1pe",
        "fileName": "google_play_app_avis_scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "nom_du_App",
            "selector": "(() => { const h = document.querySelector('h1 span') || document.querySelector('h1'); return h ? h.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "star_du_App",
            "selector": "(() => { const el = document.querySelector('.TT9eCd'); const t = el ? el.textContent.trim() : ''; const m = t.match(/\\d+[,.]\\d/); if (m) return m[0] + ' star'; const candidates = Array.from(document.querySelectorAll('[aria-label]')).map(e => (e.getAttribute('aria-label') || '').trim()).filter(Boolean); const rating = candidates.find(t => /\\d+[,.]\\d/.test(t) && /star|étoile|note/i.test(t)) || ''; const m2 = rating.match(/\\d+[,.]\\d/); return m2 ? m2[0] + ' star' : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "nombre_avis",
            "selector": "(() => { const nodes = Array.from(document.querySelectorAll('.g1rdde, [aria-label], body *')); const texts = nodes.map(e => ((e.textContent || e.getAttribute('aria-label') || '') + '').replace(/\\s+/g, ' ').trim()).filter(t => t && t.length < 220); for (const t of texts) { const m = t.match(/([0-9]+(?:[,.][0-9]+)?(?:\\s*[kKmM])?)\\s*(?:avis|reviews)\\b/i); if (m) return m[1].replace(/\\s+/g, ' ').trim(); } return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "nom_du_client",
            "selector": "(() => { const el = ROW.querySelector('.X5PpBb'); return el ? el.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "date_commentaire",
            "selector": "(() => { const el = ROW.querySelector('.bp9Aid'); return el ? el.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "star_rating",
            "selector": "(() => { const el = Array.from(ROW.querySelectorAll('[aria-label]')).find(e => /étoile|star|note|rated/i.test(e.getAttribute('aria-label') || '')); const label = el ? el.getAttribute('aria-label') || '' : ''; const m = label.match(/(\\d+[,.]?\\d*)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "nombre_avis_utiles",
            "selector": "(() => { const el = ROW.querySelector('.AJTPZc'); const t = el ? el.textContent.replace(/\\s+/g, ' ').trim() : ''; const m = t.match(/([0-9][0-9\\s.,]*)/); return m ? m[1].replace(/\\s+/g, ' ').trim() : '0'; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "commentaire",
            "selector": "(() => { const el = ROW.querySelector('.h3YV2d') || ROW.querySelector('[jsname=\"bN97Pc\"]'); return el ? el.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-2",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 3360,
      "position_y": 240,
      "config": {}
    },
    {
      "block_id": "structured-export-2",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 1560,
      "position_y": 600,
      "config": {
        "rowSelector": "div.RHo1pe",
        "fileName": "google_play_app_avis_scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "nom_du_App",
            "selector": "(() => { const h = document.querySelector('h1 span') || document.querySelector('h1'); return h ? h.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "star_du_App",
            "selector": "(() => { const el = document.querySelector('.TT9eCd'); const t = el ? el.textContent.trim() : ''; const m = t.match(/\\d+[,.]\\d/); if (m) return m[0] + ' star'; const candidates = Array.from(document.querySelectorAll('[aria-label]')).map(e => (e.getAttribute('aria-label') || '').trim()).filter(Boolean); const rating = candidates.find(t => /\\d+[,.]\\d/.test(t) && /star|étoile|note/i.test(t)) || ''; const m2 = rating.match(/\\d+[,.]\\d/); return m2 ? m2[0] + ' star' : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "nombre_avis",
            "selector": "(() => { const nodes = Array.from(document.querySelectorAll('.g1rdde, [aria-label], body *')); const texts = nodes.map(e => ((e.textContent || e.getAttribute('aria-label') || '') + '').replace(/\\s+/g, ' ').trim()).filter(t => t && t.length < 220); for (const t of texts) { const m = t.match(/([0-9]+(?:[,.][0-9]+)?(?:\\s*[kKmM])?)\\s*(?:avis|reviews)\\b/i); if (m) return m[1].replace(/\\s+/g, ' ').trim(); } return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "nom_du_client",
            "selector": "(() => { const el = ROW.querySelector('.X5PpBb'); return el ? el.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "date_commentaire",
            "selector": "(() => { const el = ROW.querySelector('.bp9Aid'); return el ? el.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "star_rating",
            "selector": "(() => { const el = Array.from(ROW.querySelectorAll('[aria-label]')).find(e => /étoile|star|note|rated/i.test(e.getAttribute('aria-label') || '')); const label = el ? el.getAttribute('aria-label') || '' : ''; const m = label.match(/(\\d+[,.]?\\d*)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "nombre_avis_utiles",
            "selector": "(() => { const el = ROW.querySelector('.AJTPZc'); const t = el ? el.textContent.replace(/\\s+/g, ' ').trim() : ''; const m = t.match(/([0-9][0-9\\s.,]*)/); return m ? m[1].replace(/\\s+/g, ' ').trim() : '0'; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "commentaire",
            "selector": "(() => { const el = ROW.querySelector('.h3YV2d') || ROW.querySelector('[jsname=\"bN97Pc\"]'); return el ? el.textContent.trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-3",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 1920,
      "position_y": 600,
      "config": {}
    }
  ],
  "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": "element-exists-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "true",
      "to_block_id": "element-exists-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "false",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-2",
      "from_connector_id": "true",
      "to_block_id": "click-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-2",
      "from_connector_id": "false",
      "to_block_id": "structured-export-2",
      "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-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-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-2",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-3",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 136,
      "width": 2840,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 768,
      "position_y": 136,
      "width": 2840,
      "height": 656,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "loop-continue-1",
          "element-exists-2",
          "click-1",
          "loop-continue-2",
          "loop-continue-3"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 2208,
      "position_y": 136,
      "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": 136,
      "width": 1760,
      "height": 656,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1",
          "structured-export-2"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes Google Play app review data from multiple app detail page URLs. Uses a multi-URL navigation loop, skips unavailable/404 app pages, opens the reviews dialog when available, scrolls it to load reviews, and appends extracted review rows to google_play_app_avis_scraper.csv. Best-effort selectors are used because Google Play has dynamic DOM classes and the first provided sample URL returned 404 in analysis.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `h1 span, h1`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 1040,
      "position_y": 220,
      "width": 340,
      "height": 134,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-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": 1040,
      "position_y": 580,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    },
    {
      "id": "note-block-element-exists-2",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `(//button[contains(@aria-label, 'Afficher tous les avis') or contains(@aria-label, 'See all reviews') or .//*[contains(n`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 1400,
      "position_y": 220,
      "width": 340,
      "height": 170,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-2"
      }
    },
    {
      "id": "note-block-click-1",
      "element_type": "note",
      "title": "Note: Click",
      "content": "Uses XPath `(//button[contains(@aria-label, 'Afficher tous les avis') or contains(@aria-label, 'See all reviews'`. XPath breaks easily if DOM structure changes.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 220,
      "width": 340,
      "height": 133,
      "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 () => { const sleep = ms => new Promise(r => setTimeout(r, ms)); const dialog = document.quer...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 220,
      "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 (nom_du_App, star_du_App, nombre_avis, nom_du_client, date_commentaire). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 3200,
      "position_y": 220,
      "width": 340,
      "height": 137,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-2",
      "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": 3560,
      "position_y": 220,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-2"
      }
    },
    {
      "id": "note-block-structured-export-2",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (nom_du_App, star_du_App, nombre_avis, nom_du_client, date_commentaire). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 580,
      "width": 340,
      "height": 137,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-2"
      }
    }
  ]
}