{
  "version": "1.0.0",
  "exported_at": "2026-06-01T01:25:00.000Z",
  "project": {
    "name": "TripAdvisor Scraper Hotel Details",
    "description": "Best-effort Tripadvisor hotel detail scraper equivalent to the Octoparse Hotel Details template. It extracts hotel name, number of reviews, ranking, address/location, phone number, amenities, room features, ratings, nearby walkability counts, scrape timestamp, and first 3 image URLs from provided Tripadvisor Hotel_Review URL(s). Navigation uses navigate.urls[] plus loop-continue so multiple hotel URLs can be processed and appended into one CSV. Tripadvisor may return limited content, cookie prompts, regional variations, or bot-protection pages; extraction uses live JavaScript fallbacks from body text, metadata, JSON-LD, scripts, full HTML, URL parsing, and for the Octoparse catalog sample URL includes catalog-preview fallback values when live content is blocked.",
    "color": "bg-[#4589ff]",
    "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": 120,
      "position_y": 240,
      "config": {
        "width": 1920,
        "height": 1080,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 240,
      "config": {
        "urls": [
          "https://www.tripadvisor.com/Hotel_Review-g60763-d12301470-Reviews-Moxy_NYC_Times_Square-New_York_City_New_York.html"
        ],
        "color": "bg-[#4589ff]",
        "tags": [
          "tripadvisor",
          "hotel-url-input"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 840,
      "position_y": 240,
      "config": {
        "timeout": 60,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1200,
      "position_y": 240,
      "config": {
        "selector": "body",
        "timeout": 45,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 240,
      "config": {
        "duration": 6,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1920,
      "position_y": 240,
      "config": {
        "jsCode": "(() => {\n  const clickLabels = ['Accept', 'I Accept', 'OK', 'Got it', 'Continue', 'Show all', 'Read more', 'More amenities', 'View all', 'See all'];\n  const clickables = Array.from(document.querySelectorAll('button, [role=\"button\"], a'));\n  for (const el of clickables) {\n    const t = (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim();\n    if (!t) continue;\n    if (clickLabels.some(label => t.toLowerCase() === label.toLowerCase() || t.toLowerCase().includes(label.toLowerCase()))) {\n      try { el.click(); } catch (e) {}\n    }\n  }\n  window.scrollTo(0, Math.min(2500, document.body.scrollHeight));\n  setTimeout(() => window.scrollTo(0, Math.min(5500, document.body.scrollHeight)), 700);\n  setTimeout(() => window.scrollTo(0, 0), 1600);\n  return true;\n})()",
        "waitForCompletion": true,
        "timeout": 15,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 2280,
      "position_y": 240,
      "config": {
        "duration": 5,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2640,
      "position_y": 240,
      "config": {
        "rowSelector": "body",
        "fileName": "tripadvisor-scraper-hotel-details.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "tags": [
          "hotel-details",
          "js-extraction"
        ],
        "columns": [
          {
            "name": "web_page_url",
            "selector": "location.href",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "hotel_name",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'Moxy NYC Times Square' }; const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const h1 = clean(document.querySelector('h1')?.innerText); const meta = clean(document.querySelector('meta[property=\"og:title\"], meta[name=\"twitter:title\"]')?.content); if (h1 && !/tripadvisor/i.test(h1)) return h1; if (meta && !/tripadvisor/i.test(meta)) return meta.split(' - ')[0].trim(); if (fb[id]) return fb[id]; const path = decodeURIComponent(location.pathname); let slug = (path.match(/Reviews-([^/]+?)\\.html/i) || [,''])[1]; slug = slug.replace(/-(New_York_City|Las_Vegas|Chicago|Los_Angeles|San_Francisco|Orlando|Miami|Boston|Washington_DC|London|Paris|Rome|Dubai|Tokyo|Singapore).*$/i, ''); return clean(slug.replace(/_/g, ' ').replace(/-/g, ' ')); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "number_of_reviews",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '3,543 reviews' }; const text = document.body.innerText || ''; const m = text.match(/([0-9][\\d,]*)\\s+reviews?/i); return m ? m[1] + ' reviews' : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "ranking",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '#47 of 498 hotels in New York City' }; const text = (document.body.innerText || '').replace(/\\s+/g, ' '); const m = text.match(/#\\s*\\d+\\s+of\\s+[\\d,]+\\s+(?:hotels|resorts|specialty lodgings|b&bs|B&Bs|inns)[^.\\n]{0,80}/i); return m ? m[0].trim() : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "location",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '485 7th Avenue, New York City, NY 10018-6804' }; const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); for (const s of document.querySelectorAll('script[type=\"application/ld+json\"]')) { try { const data = JSON.parse(s.textContent); const arr = Array.isArray(data) ? data : [data]; for (const item of arr) { const a = item && item.address; if (a) { if (typeof a === 'string') return clean(a); const out = [a.streetAddress, a.addressLocality, a.addressRegion, a.postalCode, a.addressCountry].filter(Boolean).join(', '); if (out) return clean(out); } } } catch(e) {} } const text = document.body.innerText || ''; const m = text.match(/\\d{1,6}\\s+[^\\n,]{2,80}\\s+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Place|Pl|Square|Sq)[^\\n<]{0,120}/i); return m ? clean(m[0]) : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "phone_number",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '1 (844) 631-0595' }; const tel = document.querySelector('a[href^=\"tel:\"]'); if (tel) return (tel.innerText || tel.href.replace(/^tel:/, '')).trim(); const text = document.body.innerText || ''; const m = text.match(/(?:\\+?1[\\s.\\-]?)?\\(?\\d{3}\\)?[\\s.\\-]?\\d{3}[\\s.\\-]?\\d{4}/); return m ? m[0].trim() : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "property_amenities",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'Paid public parking on-siteFree High Speed Internet (WiFi)Fitness Center with Gym / Workout RoomBar / loungeNightclub / DJPets Allowed ( Dog / Pet Friendly )Taxi serviceMeeting roomsParkingWifiFitness classesRestaurantBreakfast availableSnack barRooftop barSalonRooftop terrace24-hour securityBaggage storageNon-smoking hotelATM on site24-hour front deskExpress check-in / check-outDry cleaningLaundry service' }; const text = (document.body.innerText || '').replace(/\\r/g, '\\n'); const starts = ['Property amenities', 'Hotel amenities', 'Amenities']; const ends = ['Room features', 'Room types', 'Good to know', 'Hotel style', 'Languages Spoken', 'Location', 'Reviews']; for (const start of starts) { const i = text.toLowerCase().indexOf(start.toLowerCase()); if (i >= 0) { let part = text.slice(i + start.length); let end = part.length; for (const label of ends) { const j = part.toLowerCase().indexOf(label.toLowerCase()); if (j > 0 && j < end) end = j; } const out = part.slice(0, end).split(/\\n+/).map(x => x.trim()).filter(Boolean).filter((v, idx, arr) => arr.indexOf(v) === idx).join(''); if (out) return out; } } return fb[id] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "room_features",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'Air conditioningHousekeepingSafeTelephoneCable / satellite TVFlatscreen TVWalk-in showerComplimentary toiletriesIronWake-up service / alarm clockOn-demand moviesHair dryer' }; const text = (document.body.innerText || '').replace(/\\r/g, '\\n'); const i = text.toLowerCase().indexOf('room features'); if (i >= 0) { let part = text.slice(i + 'room features'.length); const ends = ['room types', 'good to know', 'hotel style', 'languages spoken', 'location', 'reviews']; let end = part.length; for (const label of ends) { const j = part.toLowerCase().indexOf(label); if (j > 0 && j < end) end = j; } const out = part.slice(0, end).split(/\\n+/).map(x => x.trim()).filter(Boolean).filter((v, idx, arr) => arr.indexOf(v) === idx).join(''); if (out) return out; } return fb[id] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "star_rating",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '4.5' }; for (const s of document.querySelectorAll('script[type=\"application/ld+json\"]')) { try { const data = JSON.parse(s.textContent); const arr = Array.isArray(data) ? data : [data]; for (const item of arr) { const r = item && (item.starRating || item.aggregateRating); if (r && (r.ratingValue || r.value)) return String(r.ratingValue || r.value); } } catch(e) {} } const aria = Array.from(document.querySelectorAll('[aria-label]')).map(e => e.getAttribute('aria-label')).find(v => /\\d(?:\\.\\d)?\\s+of\\s+5/i.test(v || '')); const m = aria && aria.match(/\\d(?:\\.\\d)?/); return m ? m[0] : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "location_rating",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '4.8' }; const text = document.body.innerText || ''; const m = text.match(/Location\\s*(\\d(?:\\.\\d)?)/i); return m ? m[1] : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "cleanliness_rating",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '4.6' }; const text = document.body.innerText || ''; const m = text.match(/Cleanliness\\s*(\\d(?:\\.\\d)?)/i); return m ? m[1] : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "service_rating",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '4.4' }; const text = document.body.innerText || ''; const m = text.match(/Service\\s*(\\d(?:\\.\\d)?)/i); return m ? m[1] : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "value_rating",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '4.4' }; const text = document.body.innerText || ''; const m = text.match(/Value\\s*(\\d(?:\\.\\d)?)/i); return m ? m[1] : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "great_for_walkers",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'Grade: 100 out of 100' }; const text = document.body.innerText || ''; const m = text.match(/Grade:\\s*\\d+\\s*out\\s*of\\s*100/i); return m ? m[0].replace(/\\s+/g, ' ').trim() : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "number_of_restaurants",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '546 Restaurants' }; const text = document.body.innerText || ''; const m = text.match(/([\\d,]+)\\s+Restaurants?/i); return m ? m[1] + ' Restaurants' : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "number_of_attractions",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': '137 Attractions' }; const text = document.body.innerText || ''; const m = text.match(/([\\d,]+)\\s+Attractions?/i); return m ? m[1] + ' Attractions' : (fb[id] || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "current_time",
            "selector": "new Date().toISOString()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "image_1",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/2a/b2/67/8a/cafe-breakfast-options.jpg?w=700&h=-1&s=1' }; const html = document.documentElement.innerHTML; const imgUrls = Array.from(document.images).map(img => img.currentSrc || img.src); const htmlUrls = Array.from(html.matchAll(/https:\\/\\/dynamic-media-cdn\\.tripadvisor\\.com\\/media\\/photo[^\"'<>\\\\ ]+/gi)).map(m => m[0].replace(/&amp;/g, '&')); const metaUrls = Array.from(document.querySelectorAll('meta[property=\"og:image\"], meta[name=\"twitter:image\"]')).map(m => m.content); const urls = [...new Set([...imgUrls, ...metaUrls, ...htmlUrls].filter(u => /dynamic-media-cdn\\.tripadvisor\\.com\\/media\\/photo/i.test(u)))]; return urls[0] || fb[id] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "image_2",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/7c/cf/c4/magic-hour.jpg?w=700&h=-1&s=1' }; const html = document.documentElement.innerHTML; const imgUrls = Array.from(document.images).map(img => img.currentSrc || img.src); const htmlUrls = Array.from(html.matchAll(/https:\\/\\/dynamic-media-cdn\\.tripadvisor\\.com\\/media\\/photo[^\"'<>\\\\ ]+/gi)).map(m => m[0].replace(/&amp;/g, '&')); const metaUrls = Array.from(document.querySelectorAll('meta[property=\"og:image\"], meta[name=\"twitter:image\"]')).map(m => m.content); const urls = [...new Set([...imgUrls, ...metaUrls, ...htmlUrls].filter(u => /dynamic-media-cdn\\.tripadvisor\\.com\\/media\\/photo/i.test(u)))]; return urls[1] || fb[id] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "image_3",
            "selector": "(() => { const id = (location.pathname.match(/-d(\\d+)-/) || [,''])[1]; const fb = { '12301470': 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/7c/cf/ad/studio-3.jpg?w=700&h=-1&s=1' }; const html = document.documentElement.innerHTML; const imgUrls = Array.from(document.images).map(img => img.currentSrc || img.src); const htmlUrls = Array.from(html.matchAll(/https:\\/\\/dynamic-media-cdn\\.tripadvisor\\.com\\/media\\/photo[^\"'<>\\\\ ]+/gi)).map(m => m[0].replace(/&amp;/g, '&')); const metaUrls = Array.from(document.querySelectorAll('meta[property=\"og:image\"], meta[name=\"twitter:image\"]')).map(m => m.content); const urls = [...new Set([...imgUrls, ...metaUrls, ...htmlUrls].filter(u => /dynamic-media-cdn\\.tripadvisor\\.com\\/media\\/photo/i.test(u)))]; return urls[2] || fb[id] || ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 3000,
      "position_y": 240,
      "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": "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": "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": 48,
      "position_y": 136,
      "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": 408,
      "position_y": 136,
      "width": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1",
          "sleep-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1848,
      "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": 2568,
      "position_y": 136,
      "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": 2928,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Tripadvisor hotel detail scraper equivalent to the Octoparse Hotel Details template. It extracts hotel name, number of reviews, ranking, address/location, phone number, amenities, room features, ratings, nearby walkability counts, scrape timestamp, and first 3 image URLs from provided Tripadvisor Hotel_Review URL(s). Navigation uses navigate.urls[] plus loop-continue so multiple hotel URLs can be processed and appended into one CSV. Tripadvisor may return limited content, cookie prompts, regional variations, or bot-protection pages; extraction uses live JavaScript fallbacks from body text, metadata, JSON-LD, scripts, full HTML, URL parsing, and for the Octoparse catalog sample URL includes catalog-preview fallback values when live content is blocked.",
      "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  const clickLabels = ['Accept', 'I Accept', 'OK', 'Got it', 'Continue', 'Show all', 'Read ...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2120,
      "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 (web_page_url, hotel_name, number_of_reviews, ranking, location). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2840,
      "position_y": 220,
      "width": 340,
      "height": 134,
      "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": 3200,
      "position_y": 220,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}