{
  "version": "1.0.0",
  "exported_at": "2026-06-02T14:00:00.000Z",
  "project": {
    "name": "Google Hotel Scraper by URLs",
    "description": "Best-effort Google Travel hotel scraper by listing URLs. Extracts check-in/check-out dates when visible, hotel name, price, rating, label, offer/property type, services/amenities, two image URLs, and website/link. Uses navigate.urls[] for one or more Google Travel input URLs, auto-scrolls lazy/infinite results, then normalizes compact hotel cards into synthetic rows before CSV export. Appends rows to google_hotel_scraper_by_urls.csv. Google may show consent, CAPTCHA, or anti-bot pages; solve manually if needed. Rating parsing is normalized after removing the detected price to avoid polluted rating text.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "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": {
        "urls": [
          "https://www.google.com/travel/search?q=new%20york&qs=CAEgACgAOA1IAA&hl=en&ved=0CAAQ5JsGahgKEwjIhLSS3PiQAxUAAAAAHQAAAAAQmgE&ts=CAESCgoCCAMKAggDEAAaXgpAEjwKCS9tLzAyXzI4NjIlMHg4OWMyNGZhNWQzM2YwODNiOjB4YzgwYjhmMDZlMTVZzWjV5ppx1q3iFaz9WdvULDhUzs118SPpDxALGBQSBwjpDxALGBUYATICEAAqEQoJEgIEBToDVVNEGgAiAhgB&ap=KigKEgnQapG2pBA_QBGSpuLreIhbQBISCZcKv88KhkNAEZKm4uss_11AMAE"
        ],
        "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": 820,
      "position_y": 260,
      "config": {
        "timeout": 45,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1180,
      "position_y": 260,
      "config": {
        "jsCode": "return (() => { const labels = ['accept all','i agree','accept','aceptar','agree','ok']; const candidates = Array.from(document.querySelectorAll('button, [role=\"button\"], input[type=\"submit\"]')); const btn = candidates.find(el => { const t = ((el.innerText || el.value || el.getAttribute('aria-label') || '') + '').trim().toLowerCase(); return labels.some(label => t.includes(label)); }); if (btn) { btn.click(); return 'clicked consent button'; } return 'no consent button found'; })();",
        "waitForCompletion": true,
        "timeout": 10,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1540,
      "position_y": 260,
      "config": {
        "duration": 5,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-2",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1900,
      "position_y": 260,
      "config": {
        "jsCode": "return (async () => { let lastHeight = -1; let stableRounds = 0; for (let i = 0; i < 30 && stableRounds < 4; i++) { window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve => setTimeout(resolve, 1500)); const h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); if (h === lastHeight) { stableRounds++; } else { stableRounds = 0; lastHeight = h; } } window.scrollTo(0, 0); await new Promise(resolve => setTimeout(resolve, 1000)); return 'auto-scroll complete; stableRounds=' + stableRounds + '; height=' + lastHeight; })();",
        "waitForCompletion": true,
        "timeout": 90,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "inject-javascript-3",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 2260,
      "position_y": 260,
      "config": {
        "jsCode": "return (() => { const old = document.querySelector('#uscraper-google-hotel-results'); if (old) old.remove(); const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const root = document.createElement('div'); root.id = 'uscraper-google-hotel-results'; root.style.cssText = 'display:block; position:relative; z-index:2147483647; background:#fff; color:#000; padding:8px; font-size:12px;'; const pageText = document.body.innerText || ''; const isoDates = pageText.match(/\\b20\\d{2}-\\d{2}-\\d{2}\\b/g) || []; const entrada = isoDates[0] || ''; const salida = isoDates[1] || ''; const currencyRe = /[$€£¥]\\s?\\d[\\d,.]*/g; const amenityRe = /\\b(Free cancellation|Pet-friendly|Kid-friendly|Pool|Free parking|Paid parking|Parking|Breakfast|Wi-?Fi|Restaurant|Bar|Gym|Fitness center|Spa|Jacuzzi|Beach access|Air conditioning|Kitchen|Balcony|Heating|Washer|Dryer|Desayuno|Piscina|Aparcamiento|Aire acondicionado|Gimnasio|Acceso a la playa|Adecuado para niños|Calefacción|Balcón)\\b/gi; const findRating = txt => { const t = clean(txt); const patterns = [/\\b\\d(?:[.,]\\d)?\\/5\\s*\\([^)]*\\)/i, /\\b\\d(?:[.,]\\d)?\\s*\\([0-9.,Kk]+\\)/i, /\\b\\d(?:[.,]\\d)?\\s+de\\s+5(?:\\s+estrellas?)?(?:\\s+de\\s+[0-9.,Kk]+\\s+reseñas?)?/i]; for (const p of patterns) { const m = t.match(p); if (m) return clean(m[0]); } return ''; }; const imgEls = Array.from(document.querySelectorAll('img')).filter(img => { const src = img.currentSrc || img.src || img.getAttribute('data-src') || ''; return /googleusercontent\\.com/i.test(src) && !/branding|gstatic/i.test(src); }); const rows = []; for (const img of imgEls) { let best = null; let n = img; for (let depth = 0; depth < 15 && n; depth++, n = n.parentElement) { const txt = clean(n.innerText || n.textContent || ''); const prices = txt.match(currencyRe) || []; if (txt.length > 20 && prices.length >= 1 && prices.length <= 2 && txt.length < 1000) { best = n; break; } if (!best && txt.length > 20 && prices.length >= 1 && prices.length <= 4 && txt.length < 1800) best = n; } if (!best) continue; const txt = clean(best.innerText || best.textContent || ''); const priceMatch = txt.match(/[$€£¥]\\s?\\d[\\d,.]*/); const precio = priceMatch ? priceMatch[0] : ''; if (!precio) continue; const txtNoPrice = clean(txt.replace(precio, ' ')); const calificacion = findRating(txtNoPrice); let hotel = ''; const priceIndex = txt.indexOf(precio); if (priceIndex > 0) { hotel = clean(txt.slice(0, priceIndex).replace(/^(Sponsored|Ad|Learn more|View deal)\\s*/i, '')); if (hotel.length > 120) hotel = ''; } if (!hotel) { let after = clean(txt.slice(priceIndex + precio.length)); if (calificacion) after = clean(after.replace(calificacion, ' ')); hotel = clean(after.split(/\\b(?:Amenities for|Free cancellation|Pet-friendly|Kid-friendly|Pool|Parking|Breakfast|Wi-?Fi|VACATION RENTAL|\\d\\s*-?star hotel)\\b/i)[0] || ''); } hotel = hotel.replace(/\\b(GREAT DEAL|Sponsored|Ad|View deal|Learn more)\\b/gi, '').trim(); if (!hotel || hotel.length < 2 || hotel.length > 140) continue; let etiqueta = ''; const labelMatch = txtNoPrice.match(/\\b\\d(?:[.,]\\d)?\\/5\\s*\\([^)]*\\)\\s*[·-]\\s*([^·|]+?)(?=Free|Pet|Kid|Pool|Parking|Breakfast|Wi|$)/i) || txtNoPrice.match(/\\b(\\d\\s*-?star hotel|hotel de \\d\\s*estrellas|VACATION RENTAL|vacation rental|alquiler vacacional)\\b/i); if (labelMatch) etiqueta = clean(labelMatch[1] || labelMatch[0]); const amenities = Array.from(new Set((txtNoPrice.match(amenityRe) || []).map(clean))).join(' | '); const oferta = amenities.split(' | ')[0] || etiqueta; const imageUrls = Array.from(best.querySelectorAll('img')).map(i => i.currentSrc || i.src || i.getAttribute('data-src') || '').filter(u => u && /googleusercontent\\.com/i.test(u) && !/branding|gstatic/i.test(u)); const imagen = imageUrls[0] || (img.currentSrc || img.src || img.getAttribute('data-src') || ''); const imagen2 = imageUrls.find(u => u !== imagen) || ''; const links = Array.from(best.querySelectorAll('a[href]')).map(a => a.href).filter(Boolean); const decoded = links.map(h => { try { const u = new URL(h, location.href); return u.searchParams.get('adurl') || u.searchParams.get('q') || u.href; } catch (e) { return h; } }); const sitio_web = decoded.find(h => /^https?:\\/\\//i.test(h) && !/googleusercontent\\.|gstatic\\./i.test(h)) || decoded.find(h => /^https?:\\/\\//i.test(h)) || ''; rows.push({ entrada, salida, hotel, precio, calificacion, etiqueta, oferta, servicios: amenities, imagen, imagen2, sitio_web }); } const seen = new Set(); let count = 0; for (const r of rows) { const key = (r.hotel + '|' + r.precio + '|' + r.imagen).toLowerCase(); if (seen.has(key)) continue; seen.add(key); const row = document.createElement('div'); row.className = 'uscraper-hotel-row'; row.style.cssText = 'display:block; border:1px solid #ddd; margin:2px; padding:2px;'; for (const k of ['entrada','salida','hotel','precio','calificacion','etiqueta','oferta','servicios','imagen','imagen2','sitio_web']) { const s = document.createElement('span'); s.className = k; s.textContent = r[k] || ''; row.appendChild(s); } root.appendChild(row); count++; } document.body.appendChild(root); return 'normalized hotel rows: ' + count; })();",
        "waitForCompletion": true,
        "timeout": 20,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 2620,
      "position_y": 260,
      "config": {
        "selector": "#uscraper-google-hotel-results .uscraper-hotel-row",
        "timeout": 30,
        "visible": false,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2980,
      "position_y": 260,
      "config": {
        "rowSelector": "#uscraper-google-hotel-results .uscraper-hotel-row",
        "fileName": "google_hotel_scraper_by_urls.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "entrada",
            "selector": ".entrada",
            "attribute": "text"
          },
          {
            "name": "salida",
            "selector": ".salida",
            "attribute": "text"
          },
          {
            "name": "hotel",
            "selector": ".hotel",
            "attribute": "text"
          },
          {
            "name": "precio",
            "selector": ".precio",
            "attribute": "text"
          },
          {
            "name": "calificacion",
            "selector": ".calificacion",
            "attribute": "text"
          },
          {
            "name": "etiqueta",
            "selector": ".etiqueta",
            "attribute": "text"
          },
          {
            "name": "oferta",
            "selector": ".oferta",
            "attribute": "text"
          },
          {
            "name": "servicios",
            "selector": ".servicios",
            "attribute": "text"
          },
          {
            "name": "imagen",
            "selector": ".imagen",
            "attribute": "text"
          },
          {
            "name": "imagen2",
            "selector": ".imagen2",
            "attribute": "text"
          },
          {
            "name": "sitio_web",
            "selector": ".sitio_web",
            "attribute": "text"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 3340,
      "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": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-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-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-2",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-3",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-3",
      "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-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": 2480,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1108,
      "position_y": 156,
      "width": 1400,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1",
          "inject-javascript-2",
          "inject-javascript-3"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2908,
      "position_y": 156,
      "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": 3268,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Google Travel hotel scraper by listing URLs. Extracts check-in/check-out dates when visible, hotel name, price, rating, label, offer/property type, services/amenities, two image URLs, and website/link. Uses navigate.urls[] for one or more Google Travel input URLs, auto-scrolls lazy/infinite results, then normalizes compact hotel cards into synthetic rows before CSV export. Appends rows to google_hotel_scraper_by_urls.csv. Google may show consent, CAPTCHA, or anti-bot pages; solve manually if needed. Rating parsing is normalized after removing the detected price to avoid polluted rating text.",
      "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: `return (() => { const labels = ['accept all','i agree','accept','aceptar','agree','ok']; const candi...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1380,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-inject-javascript-2",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `return (async () => { let lastHeight = -1; let stableRounds = 0; for (let i = 0; i < 30 && stableRou...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2100,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-2"
      }
    },
    {
      "id": "note-block-inject-javascript-3",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `return (() => { const old = document.querySelector('#uscraper-google-hotel-results'); if (old) old.r...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2460,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-3"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Extracts rows matching `#uscraper-google-hotel-results .uscraper-hotel-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 3180,
      "position_y": 240,
      "width": 340,
      "height": 121,
      "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": 3540,
      "position_y": 240,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}