exorcist123 commited on
Commit
d033e7d
Β·
1 Parent(s): cc95037
Files changed (1) hide show
  1. visa_availability_scraper_playwright.py +420 -107
visa_availability_scraper_playwright.py CHANGED
@@ -2,6 +2,8 @@ import asyncio
2
  import json
3
  from typing import Dict, Optional, List
4
  from playwright.async_api import async_playwright
 
 
5
 
6
  class PassportIndexVisaScraper:
7
  def __init__(self, debug: bool = True):
@@ -17,57 +19,102 @@ class PassportIndexVisaScraper:
17
  self.browser = None
18
  self.context = None
19
  self.page = None
 
 
20
 
21
  async def __aenter__(self):
22
  """Initialize browser with stealth mode"""
23
- self.playwright = await async_playwright().start()
 
 
 
 
 
 
24
 
25
- # Launch browser with stealth settings
26
  self.browser = await self.playwright.chromium.launch(
27
- headless=True, # Using headless mode
28
  args=[
29
  '--disable-blink-features=AutomationControlled',
30
  '--disable-dev-shm-usage',
31
  '--no-sandbox',
32
  '--disable-setuid-sandbox',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  '--disable-web-security',
34
- '--disable-features=IsolateOrigins,site-per-process'
 
 
 
35
  ]
36
  )
 
 
 
 
 
 
 
 
37
 
38
  # Create context with realistic settings
39
  self.context = await self.browser.new_context(
40
- viewport={'width': 1920, 'height': 1080},
41
- user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
42
  locale='en-US',
43
- timezone_id='America/New_York'
 
 
 
 
 
 
 
44
  )
45
 
46
  self.page = await self.context.new_page()
47
 
48
- # Add stealth JavaScript to avoid detection
49
  await self.page.add_init_script("""
50
- // Override the navigator.webdriver property
51
- Object.defineProperty(navigator, 'webdriver', {
52
- get: () => undefined
53
- });
54
-
55
- // Override chrome property
56
- window.chrome = {
57
- runtime: {}
58
- };
59
-
60
- // Override permissions
61
- const originalQuery = window.navigator.permissions.query;
62
- window.navigator.permissions.query = (parameters) => (
63
- parameters.name === 'notifications' ?
64
- Promise.resolve({ state: Notification.permission }) :
65
- originalQuery(parameters)
66
- );
67
  """)
68
 
69
  if self.debug:
70
- print("πŸš€ Browser initialized with stealth mode")
71
 
72
  return self
73
 
@@ -79,8 +126,8 @@ class PassportIndexVisaScraper:
79
  await self.context.close()
80
  if self.browser:
81
  await self.browser.close()
82
- if self.playwright:
83
- await self.playwright.stop()
84
 
85
  if self.debug:
86
  print("πŸ”’ Browser closed")
@@ -89,44 +136,147 @@ class PassportIndexVisaScraper:
89
  """
90
  Navigate to the website and wait for it to load properly
91
  """
92
- try:
93
- if self.debug:
94
- print("πŸ“± Initializing session...")
95
-
96
- # Navigate to the page
97
  try:
 
 
 
 
 
 
 
 
98
  response = await self.page.goto(
99
  self.base_url,
100
- wait_until='domcontentloaded',
101
  timeout=30000
102
  )
103
- await self.page.wait_for_timeout(3000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- # Get the cl value from the page
 
 
 
106
  cl_value = await self.page.evaluate("""
107
  () => {
108
  const clInput = document.querySelector('#cl');
109
- return clInput ? clInput.value : 'bc2140a2d83928ce1112d01e610bad89';
 
 
 
 
 
 
 
 
 
110
  }
111
  """)
112
 
113
  if self.debug:
114
- print(f"βœ… Page loaded, session ID: {cl_value}")
115
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  return True
117
-
118
  except Exception as e:
119
- if self.debug:
120
- print(f"⚠️ Page load issue: {e}, continuing anyway...")
121
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  except Exception as e:
124
- print(f"❌ Error initializing session: {e}")
125
- return False
126
 
127
- async def check_visa_requirement_browser(self, passport_country: str, destination_country: str) -> Optional[Dict]:
128
  """
129
- Check visa requirements using browser automation
130
 
131
  Args:
132
  passport_country: Two-letter country code for passport
@@ -137,9 +287,12 @@ class PassportIndexVisaScraper:
137
  """
138
  try:
139
  if self.debug:
140
- print(f"🌐 Checking {passport_country.upper()} β†’ {destination_country.upper()}")
 
 
 
141
 
142
- # Get the current session ID from the page
143
  cl_value = await self.page.evaluate("""
144
  () => {
145
  const clInput = document.querySelector('#cl');
@@ -147,13 +300,15 @@ class PassportIndexVisaScraper:
147
  }
148
  """)
149
 
150
- # Make the API request through the browser with proper argument passing
151
  result = await self.page.evaluate("""
152
  async (args) => {
153
  const [passport, destination, sessionId] = args;
 
 
154
  const formData = new URLSearchParams();
155
- formData.append('d', destination);
156
- formData.append('s', passport);
157
  formData.append('cl', sessionId);
158
 
159
  try {
@@ -162,91 +317,254 @@ class PassportIndexVisaScraper:
162
  headers: {
163
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
164
  'X-Requested-With': 'XMLHttpRequest',
165
- 'Accept': 'application/json, text/javascript, */*; q=0.01'
 
 
166
  },
167
  body: formData.toString(),
168
- credentials: 'include'
 
169
  });
170
 
171
- if (!response.ok) {
172
- throw new Error(`HTTP ${response.status}`);
173
- }
174
 
175
- const data = await response.json();
176
- return data;
 
 
 
 
 
 
 
 
 
 
 
177
  } catch (error) {
178
  return { error: error.message };
179
  }
180
  }
181
- """, [passport_country.lower(), destination_country.lower(), cl_value])
182
 
183
- if result and 'error' not in result:
184
  if self.debug:
185
- print(f"βœ… Got result: {result}")
186
  return result
187
  elif result and 'error' in result:
188
- print(f"❌ API Error: {result['error']}")
 
189
  return None
190
  else:
191
  return None
192
 
193
  except Exception as e:
194
- print(f"❌ Error checking visa requirement: {e}")
 
195
  return None
196
 
197
- async def check_visa_interactive(self, passport_country: str, destination_country: str) -> Optional[Dict]:
198
  """
199
- Alternative method: Use the interactive UI to check visa requirements
200
  """
201
  try:
202
  if self.debug:
203
- print(f"πŸ–±οΈ Using interactive method for {passport_country.upper()} β†’ {destination_country.upper()}")
204
 
205
- # Click on the passport selector
206
- await self.page.click('.vch-select-pass')
207
- await self.page.wait_for_timeout(500)
208
 
209
- # Find and click the country in the list
210
- passport_selector = f'.vch-passports .s-div[data-ccode="{passport_country.lower()}"]'
211
- await self.page.wait_for_selector(passport_selector, timeout=5000)
212
- await self.page.click(passport_selector)
213
- await self.page.wait_for_timeout(500)
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- # Click on the destination selector
216
- await self.page.click('.vch-select-des')
217
  await self.page.wait_for_timeout(500)
218
 
219
- # Find and click the destination country
220
- dest_selector = f'.vch-destinations .s-div[data-ccode="{destination_country.lower()}"]'
221
- await self.page.wait_for_selector(dest_selector, timeout=5000)
222
- await self.page.click(dest_selector)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  await self.page.wait_for_timeout(1000)
224
 
225
- # Get the result from the page
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  result = await self.page.evaluate("""
227
  () => {
228
  const resultElement = document.querySelector('.vch-result');
229
  if (resultElement) {
230
  const text = resultElement.querySelector('.text');
231
  const days = resultElement.querySelector('.days');
 
 
 
232
  return {
233
- text: text ? text.textContent : '',
234
- days: days ? days.textContent : '',
235
- pass: '""" + passport_country.lower() + """',
236
- dest: '""" + destination_country.upper() + """'
237
  };
238
  }
239
  return null;
240
  }
241
  """)
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  return result
244
 
245
  except Exception as e:
246
  if self.debug:
247
- print(f"❌ Interactive method failed: {e}")
248
  return None
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  async def check_multiple_destinations(self, passport_country: str, destinations: List[str], delay: float = 2.0) -> Dict:
251
  """
252
  Check visa requirements for multiple destinations
@@ -264,12 +582,7 @@ class PassportIndexVisaScraper:
264
  for i, dest in enumerate(destinations, 1):
265
  print(f"\n[{i}/{len(destinations)}] Checking {passport_country.upper()} β†’ {dest.upper()}...")
266
 
267
- # Try API method first
268
- result = await self.check_visa_requirement_browser(passport_country, dest)
269
-
270
- # If API fails, try interactive method
271
- if not result:
272
- result = await self.check_visa_interactive(passport_country, dest)
273
 
274
  if result:
275
  results[dest] = result
@@ -291,17 +604,20 @@ class PassportIndexVisaScraper:
291
  if not result:
292
  return "No information available"
293
 
294
- text = result.get('text', 'N/A')
295
- dest = result.get('dest', 'N/A')
296
- passport = result.get('pass', 'N/A')
297
-
298
- return f"{passport.upper()} β†’ {dest.upper()}: {text}"
 
 
 
299
 
300
 
301
  async def main():
302
  """Main function to demonstrate usage"""
303
  print("="*60)
304
- print(" Passport Index Visa Checker (Playwright)")
305
  print("="*60)
306
 
307
  async with PassportIndexVisaScraper(debug=True) as scraper:
@@ -317,14 +633,11 @@ async def main():
317
  # Test single visa requirement
318
  print("\nπŸ“ Single visa check: US β†’ GB")
319
  print("-" * 40)
320
- result = await scraper.check_visa_requirement_browser('us', 'gb')
321
  if result:
322
- print(f"Result: {scraper.format_result(result)}")
323
  else:
324
- print("Trying interactive method...")
325
- result = await scraper.check_visa_interactive('us', 'gb')
326
- if result:
327
- print(f"Result: {scraper.format_result(result)}")
328
 
329
  # Test multiple destinations
330
  print("\nπŸ“ Multiple destinations for US passport:")
@@ -335,9 +648,9 @@ async def main():
335
  print("\nπŸ“Š Summary:")
336
  for dest, result in results.items():
337
  if result:
338
- print(f" βœ… {scraper.format_result(result)}")
339
  else:
340
  print(f" ❌ US β†’ {dest.upper()}: Failed")
341
 
342
  if __name__ == "__main__":
343
- asyncio.run(main())
 
2
  import json
3
  from typing import Dict, Optional, List
4
  from playwright.async_api import async_playwright
5
+ from playwright_stealth import Stealth
6
+ import random
7
 
8
  class PassportIndexVisaScraper:
9
  def __init__(self, debug: bool = True):
 
19
  self.browser = None
20
  self.context = None
21
  self.page = None
22
+ self.playwright = None
23
+ self.stealth_manager = None
24
 
25
  async def __aenter__(self):
26
  """Initialize browser with stealth mode"""
27
+ # Initialize Playwright with Stealth using context manager pattern
28
+ self.stealth_manager = Stealth().use_async(async_playwright())
29
+ self.playwright = await self.stealth_manager.__aenter__()
30
+
31
+ # Random viewport for more natural appearance
32
+ viewport_width = random.randint(1366, 1920)
33
+ viewport_height = random.randint(768, 1080)
34
 
35
+ # Launch browser in headless mode with stealth settings
36
  self.browser = await self.playwright.chromium.launch(
37
+ headless=True, # Using headless mode as requested
38
  args=[
39
  '--disable-blink-features=AutomationControlled',
40
  '--disable-dev-shm-usage',
41
  '--no-sandbox',
42
  '--disable-setuid-sandbox',
43
+ f'--window-size={viewport_width},{viewport_height}',
44
+ '--disable-features=IsolateOrigins,site-per-process',
45
+ '--enable-features=NetworkService,NetworkServiceInProcess',
46
+ '--disable-infobars',
47
+ '--hide-scrollbars',
48
+ '--mute-audio',
49
+ '--disable-background-timer-throttling',
50
+ '--disable-renderer-backgrounding',
51
+ '--disable-features=TranslateUI',
52
+ '--disable-ipc-flooding-protection',
53
+ '--force-color-profile=srgb',
54
+ '--metrics-recording-only',
55
+ '--disable-hang-monitor',
56
+ '--disable-popup-blocking',
57
+ '--disable-prompt-on-repost',
58
+ '--disable-background-networking',
59
+ '--disable-breakpad',
60
+ '--disable-client-side-phishing-detection',
61
+ '--disable-component-extensions-with-background-pages',
62
+ '--disable-default-apps',
63
+ '--disable-extensions',
64
+ '--disable-features=TranslateUI,BlinkGenPropertyTrees',
65
+ '--disable-sync',
66
  '--disable-web-security',
67
+ '--disable-site-isolation-trials',
68
+ '--no-first-run',
69
+ '--no-default-browser-check',
70
+ '--no-pings'
71
  ]
72
  )
73
+
74
+ # Rotate user agents
75
+ user_agents = [
76
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
77
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
78
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
79
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
80
+ ]
81
 
82
  # Create context with realistic settings
83
  self.context = await self.browser.new_context(
84
+ viewport={'width': viewport_width, 'height': viewport_height},
85
+ user_agent=random.choice(user_agents),
86
  locale='en-US',
87
+ timezone_id='America/New_York',
88
+ accept_downloads=False,
89
+ has_touch=False,
90
+ is_mobile=False,
91
+ device_scale_factor=1,
92
+ bypass_csp=True,
93
+ ignore_https_errors=True,
94
+ java_script_enabled=True
95
  )
96
 
97
  self.page = await self.context.new_page()
98
 
99
+ # Add additional stealth measures
100
  await self.page.add_init_script("""
101
+ // Additional evasion beyond what stealth plugin provides
102
+ delete Object.getPrototypeOf(navigator).webdriver;
103
+
104
+ // Add some randomness to make fingerprint unique
105
+ const battery = navigator.getBattery;
106
+ if (battery) {
107
+ navigator.getBattery = async () => ({
108
+ charging: Math.random() > 0.5,
109
+ chargingTime: Math.floor(Math.random() * 10000),
110
+ dischargingTime: Math.floor(Math.random() * 10000),
111
+ level: Math.random()
112
+ });
113
+ }
 
 
 
 
114
  """)
115
 
116
  if self.debug:
117
+ print("πŸš€ Browser initialized with stealth mode (headless)")
118
 
119
  return self
120
 
 
126
  await self.context.close()
127
  if self.browser:
128
  await self.browser.close()
129
+ if hasattr(self, 'stealth_manager') and self.stealth_manager:
130
+ await self.stealth_manager.__aexit__(exc_type, exc_val, exc_tb)
131
 
132
  if self.debug:
133
  print("πŸ”’ Browser closed")
 
136
  """
137
  Navigate to the website and wait for it to load properly
138
  """
139
+ max_retries = 3
140
+ for attempt in range(max_retries):
 
 
 
141
  try:
142
+ if self.debug:
143
+ print(f"πŸ“± Initializing session... (Attempt {attempt + 1}/{max_retries})")
144
+
145
+ # Add random delay to appear more human
146
+ if attempt > 0:
147
+ await self.page.wait_for_timeout(random.randint(2000, 5000))
148
+
149
+ # Navigate to the page with proper wait
150
  response = await self.page.goto(
151
  self.base_url,
152
+ wait_until='networkidle',
153
  timeout=30000
154
  )
155
+
156
+ # Check for Cloudflare challenge
157
+ page_content = await self.page.content()
158
+ if "Just a moment" in page_content or "cf-challenge" in page_content:
159
+ if self.debug:
160
+ print("⏳ Cloudflare challenge detected, waiting...")
161
+
162
+ # Wait for Cloudflare to complete
163
+ await self.page.wait_for_timeout(15000)
164
+
165
+ # Try to wait for actual content
166
+ try:
167
+ await self.page.wait_for_selector('.vch-table', state='visible', timeout=20000)
168
+ except:
169
+ if attempt < max_retries - 1:
170
+ continue
171
+ else:
172
+ if self.debug:
173
+ print("⚠️ Cloudflare challenge persists")
174
+ # Try to reload the page
175
+ await self.page.reload(wait_until='networkidle', timeout=30000)
176
+ await self.page.wait_for_timeout(5000)
177
+
178
+
179
+ # Wait for page to be ready - try multiple selectors
180
+ try:
181
+ # Try to wait for any of these elements that indicate page is loaded
182
+ await self.page.wait_for_selector('.vch-table', state='visible', timeout=15000)
183
+ except:
184
+ # If wait fails, just continue after a delay
185
+ if self.debug:
186
+ print("⚠️ Could not find expected elements, continuing anyway...")
187
+ await self.page.wait_for_timeout(3000)
188
 
189
+ # Close/hide the virtual keyboard if it exists
190
+ await self.close_virtual_keyboard()
191
+
192
+ # Get the session value from the page
193
  cl_value = await self.page.evaluate("""
194
  () => {
195
  const clInput = document.querySelector('#cl');
196
+ // Also check if the page has the visa checker elements
197
+ const hasVchTable = document.querySelector('.vch-table') !== null;
198
+ const hasPassSelect = document.querySelector('.vch-select-pass') !== null;
199
+
200
+ return {
201
+ cl: clInput ? clInput.value : null,
202
+ hasTable: hasVchTable,
203
+ hasSelectors: hasPassSelect,
204
+ url: window.location.href
205
+ };
206
  }
207
  """)
208
 
209
  if self.debug:
210
+ if cl_value and cl_value.get('cl'):
211
+ print(f"βœ… Page loaded, session ID: {cl_value['cl']}")
212
+ print(f" Table present: {cl_value['hasTable']}, Selectors present: {cl_value['hasSelectors']}")
213
+ print(f" URL: {cl_value['url']}")
214
+ else:
215
+ print(f"⚠️ Page loaded but missing elements: {cl_value}")
216
+ # If elements are missing, try one more time with a longer wait
217
+ if not cl_value.get('hasTable'):
218
+ await self.page.wait_for_timeout(10000)
219
+ cl_value = await self.page.evaluate("""
220
+ () => {
221
+ const clInput = document.querySelector('#cl');
222
+ const hasVchTable = document.querySelector('.vch-table') !== null;
223
+ const hasPassSelect = document.querySelector('.vch-select-pass') !== null;
224
+ return {
225
+ cl: clInput ? clInput.value : null,
226
+ hasTable: hasVchTable,
227
+ hasSelectors: hasPassSelect,
228
+ url: window.location.href
229
+ };
230
+ }
231
+ """)
232
+ if cl_value.get('hasTable'):
233
+ print(f"βœ… Elements loaded after additional wait")
234
+ else:
235
+ # Continue anyway, might still work
236
+ pass
237
+
238
  return True
239
+
240
  except Exception as e:
241
+ if attempt == max_retries - 1:
242
+ print(f"❌ Error initializing session after {max_retries} attempts: {e}")
243
+ return False
244
+ else:
245
+ if self.debug:
246
+ print(f"⚠️ Attempt {attempt + 1} failed: {e}")
247
+ continue
248
+ return False
249
+
250
+ async def close_virtual_keyboard(self):
251
+ """Close the virtual keyboard if it's open"""
252
+ try:
253
+ # Check if virtual keyboard exists and is visible
254
+ keyboard_visible = await self.page.evaluate("""
255
+ () => {
256
+ const keyboard = document.querySelector('#PI-VirtualKeyboard');
257
+ if (keyboard) {
258
+ // Hide the keyboard
259
+ keyboard.style.display = 'none';
260
+ keyboard.classList.remove('kioskboard-opened');
261
+ // Also hide any overlay elements
262
+ const overlays = document.querySelectorAll('.kioskboard-wrapper, .kioskboard-overlay');
263
+ overlays.forEach(el => el.style.display = 'none');
264
+ return true;
265
+ }
266
+ return false;
267
+ }
268
+ """)
269
+
270
+ if keyboard_visible and self.debug:
271
+ print("⌨️ Virtual keyboard hidden")
272
 
273
  except Exception as e:
274
+ if self.debug:
275
+ print(f"⚠️ Could not hide virtual keyboard: {e}")
276
 
277
+ async def check_visa_requirement_api(self, passport_country: str, destination_country: str) -> Optional[Dict]:
278
  """
279
+ Check visa requirements using direct API call with cookies
280
 
281
  Args:
282
  passport_country: Two-letter country code for passport
 
287
  """
288
  try:
289
  if self.debug:
290
+ print(f"🌐 Checking {passport_country.upper()} β†’ {destination_country.upper()} via API")
291
+
292
+ # Get cookies from the page
293
+ cookies = await self.context.cookies()
294
 
295
+ # Get the session ID
296
  cl_value = await self.page.evaluate("""
297
  () => {
298
  const clInput = document.querySelector('#cl');
 
300
  }
301
  """)
302
 
303
+ # Make the API request with proper cookies and headers
304
  result = await self.page.evaluate("""
305
  async (args) => {
306
  const [passport, destination, sessionId] = args;
307
+
308
+ // Create form data
309
  const formData = new URLSearchParams();
310
+ formData.append('d', destination.toLowerCase());
311
+ formData.append('s', passport.toLowerCase());
312
  formData.append('cl', sessionId);
313
 
314
  try {
 
317
  headers: {
318
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
319
  'X-Requested-With': 'XMLHttpRequest',
320
+ 'Accept': 'application/json, text/javascript, */*; q=0.01',
321
+ 'Origin': 'https://www.passportindex.org',
322
+ 'Referer': 'https://www.passportindex.org/travel-visa-checker/'
323
  },
324
  body: formData.toString(),
325
+ credentials: 'include',
326
+ mode: 'cors'
327
  });
328
 
329
+ const responseText = await response.text();
 
 
330
 
331
+ // Try to parse as JSON
332
+ try {
333
+ return JSON.parse(responseText);
334
+ } catch {
335
+ // Check if it's a Cloudflare challenge
336
+ if (responseText.includes("Just a moment") ||
337
+ responseText.includes("cf-challenge") ||
338
+ response.status === 403) {
339
+ return { error: 'Cloudflare block', status: response.status };
340
+ }
341
+ // Otherwise it might be a different error
342
+ return { error: 'Invalid response', status: response.status };
343
+ }
344
  } catch (error) {
345
  return { error: error.message };
346
  }
347
  }
348
+ """, [passport_country, destination_country, cl_value])
349
 
350
+ if result and 'error' not in result and 'text' in result:
351
  if self.debug:
352
+ print(f"βœ… Got API result: {json.dumps(result)}")
353
  return result
354
  elif result and 'error' in result:
355
+ if self.debug:
356
+ print(f"⚠️ API Error: {result['error']}")
357
  return None
358
  else:
359
  return None
360
 
361
  except Exception as e:
362
+ if self.debug:
363
+ print(f"❌ Error checking visa requirement via API: {e}")
364
  return None
365
 
366
+ async def check_visa_interactive_v2(self, passport_country: str, destination_country: str) -> Optional[Dict]:
367
  """
368
+ Alternative interactive method with better element handling
369
  """
370
  try:
371
  if self.debug:
372
+ print(f"πŸ–±οΈ Using interactive method v2 for {passport_country.upper()} β†’ {destination_country.upper()}")
373
 
374
+ # First, ensure virtual keyboard is hidden
375
+ await self.close_virtual_keyboard()
 
376
 
377
+ # Clear any previous selections via JavaScript
378
+ await self.page.evaluate("""
379
+ () => {
380
+ // Reset the form
381
+ const passSelect = document.querySelector('.vch-select-pass');
382
+ const destSelect = document.querySelector('.vch-select-des');
383
+ if (passSelect) passSelect.textContent = 'Select Passport';
384
+ if (destSelect) destSelect.textContent = 'Select Destination';
385
+
386
+ // Hide any open dropdowns
387
+ const passports = document.querySelector('.vch-passports');
388
+ const destinations = document.querySelector('.vch-destinations');
389
+ if (passports) passports.classList.remove('opener');
390
+ if (destinations) destinations.classList.remove('opener');
391
+ }
392
+ """)
393
 
 
 
394
  await self.page.wait_for_timeout(500)
395
 
396
+ # Select passport using JavaScript
397
+ passport_selected = await self.page.evaluate("""
398
+ (countryCode) => {
399
+ // Click the passport selector
400
+ const passSelect = document.querySelector('.vch-select-pass');
401
+ if (!passSelect) return false;
402
+ passSelect.click();
403
+
404
+ // Wait a bit for dropdown to open
405
+ setTimeout(() => {
406
+ // Find and click the country
407
+ const countryElement = document.querySelector(`.vch-passports .s-div[data-ccode="${countryCode.toLowerCase()}"]`);
408
+ if (countryElement) {
409
+ countryElement.click();
410
+ return true;
411
+ }
412
+ }, 300);
413
+
414
+ return false;
415
+ }
416
+ """, passport_country)
417
+
418
  await self.page.wait_for_timeout(1000)
419
 
420
+ # Select destination using JavaScript
421
+ destination_selected = await self.page.evaluate("""
422
+ (countryCode) => {
423
+ // Click the destination selector
424
+ const destSelect = document.querySelector('.vch-select-des');
425
+ if (!destSelect) return false;
426
+ destSelect.click();
427
+
428
+ // Wait a bit for dropdown to open
429
+ setTimeout(() => {
430
+ // Find and click the country
431
+ const countryElement = document.querySelector(`.vch-destinations .s-div[data-ccode="${countryCode.toLowerCase()}"]`);
432
+ if (countryElement) {
433
+ countryElement.click();
434
+ return true;
435
+ }
436
+ }, 300);
437
+
438
+ return false;
439
+ }
440
+ """, destination_country)
441
+
442
+ await self.page.wait_for_timeout(2000)
443
+
444
+ # Get the result
445
  result = await self.page.evaluate("""
446
  () => {
447
  const resultElement = document.querySelector('.vch-result');
448
  if (resultElement) {
449
  const text = resultElement.querySelector('.text');
450
  const days = resultElement.querySelector('.days');
451
+ const passSelect = document.querySelector('.vch-select-pass');
452
+ const destSelect = document.querySelector('.vch-select-des');
453
+
454
  return {
455
+ text: text ? text.textContent.trim() : '',
456
+ days: days ? days.textContent.trim() : '',
457
+ pass: passSelect ? passSelect.textContent.trim() : '',
458
+ dest: destSelect ? destSelect.textContent.trim() : ''
459
  };
460
  }
461
  return null;
462
  }
463
  """)
464
 
465
+ if result and result['text']:
466
+ return result
467
+
468
+ return None
469
+
470
+ except Exception as e:
471
+ if self.debug:
472
+ print(f"❌ Interactive method v2 failed: {e}")
473
+ return None
474
+
475
+ async def check_visa_direct_manipulation(self, passport_country: str, destination_country: str) -> Optional[Dict]:
476
+ """
477
+ Direct manipulation method - directly trigger the visa check
478
+ """
479
+ try:
480
+ if self.debug:
481
+ print(f"πŸ”§ Using direct manipulation for {passport_country.upper()} β†’ {destination_country.upper()}")
482
+
483
+ # Directly call the visa checker function if it exists
484
+ result = await self.page.evaluate("""
485
+ async (args) => {
486
+ const [passport, destination] = args;
487
+ // Try to find and call the visa checker function directly
488
+ if (typeof visaChecker !== 'undefined' && typeof visaChecker.check === 'function') {
489
+ return await visaChecker.check(passport, destination);
490
+ }
491
+
492
+ // Alternative: manually trigger the check
493
+ const cl = document.querySelector('#cl')?.value || 'bc2140a2d83928ce1112d01e610bad89';
494
+
495
+ // Set the selections visually
496
+ const passSelect = document.querySelector('.vch-select-pass');
497
+ const destSelect = document.querySelector('.vch-select-des');
498
+
499
+ if (passSelect && destSelect) {
500
+ // Find country names
501
+ const passportElement = document.querySelector(`.vch-passports .s-div[data-ccode="${passport}"] .cname`);
502
+ const destElement = document.querySelector(`.vch-destinations .s-div[data-ccode="${destination}"] .cname`);
503
+
504
+ if (passportElement) passSelect.textContent = passportElement.textContent;
505
+ if (destElement) destSelect.textContent = destElement.textContent;
506
+
507
+ // Make the AJAX call
508
+ return new Promise((resolve) => {
509
+ $.ajax({
510
+ url: 'https://www.passportindex.org/core/visachecker.php',
511
+ type: 'POST',
512
+ data: {
513
+ d: destination,
514
+ s: passport,
515
+ cl: cl
516
+ },
517
+ success: function(response) {
518
+ // Update the UI
519
+ if (response.text) {
520
+ const resultElement = document.querySelector('.vch-result');
521
+ if (resultElement) {
522
+ resultElement.className = 'vch-result tv-grey ' + (response.vr ? 'tvvr' : 'tvvf');
523
+ const textElement = resultElement.querySelector('.text');
524
+ if (textElement) textElement.textContent = response.text;
525
+ }
526
+ }
527
+ resolve(response);
528
+ },
529
+ error: function(xhr) {
530
+ resolve({ error: 'Request failed', status: xhr.status });
531
+ }
532
+ });
533
+ });
534
+ }
535
+
536
+ return null;
537
+ }
538
+ """, [passport_country.lower(), destination_country.lower()])
539
+
540
  return result
541
 
542
  except Exception as e:
543
  if self.debug:
544
+ print(f"❌ Direct manipulation failed: {e}")
545
  return None
546
 
547
+ async def check_visa_requirement(self, passport_country: str, destination_country: str) -> Optional[Dict]:
548
+ """
549
+ Main method to check visa requirements - tries multiple approaches
550
+ """
551
+ # Try API method first
552
+ result = await self.check_visa_requirement_api(passport_country, destination_country)
553
+ if result:
554
+ return result
555
+
556
+ # Try direct manipulation
557
+ result = await self.check_visa_direct_manipulation(passport_country, destination_country)
558
+ if result:
559
+ return result
560
+
561
+ # Try interactive method v2
562
+ result = await self.check_visa_interactive_v2(passport_country, destination_country)
563
+ if result:
564
+ return result
565
+
566
+ return None
567
+
568
  async def check_multiple_destinations(self, passport_country: str, destinations: List[str], delay: float = 2.0) -> Dict:
569
  """
570
  Check visa requirements for multiple destinations
 
582
  for i, dest in enumerate(destinations, 1):
583
  print(f"\n[{i}/{len(destinations)}] Checking {passport_country.upper()} β†’ {dest.upper()}...")
584
 
585
+ result = await self.check_visa_requirement(passport_country, dest)
 
 
 
 
 
586
 
587
  if result:
588
  results[dest] = result
 
604
  if not result:
605
  return "No information available"
606
 
607
+ # Return clean JSON format
608
+ return json.dumps({
609
+ 'text': result.get('text', 'N/A'),
610
+ 'col': result.get('col', ''),
611
+ 'link': result.get('link', 0),
612
+ 'dest': result.get('dest', '').upper(),
613
+ 'pass': result.get('pass', '').upper()
614
+ })
615
 
616
 
617
  async def main():
618
  """Main function to demonstrate usage"""
619
  print("="*60)
620
+ print(" Passport Index Visa Checker (Playwright - Fixed)")
621
  print("="*60)
622
 
623
  async with PassportIndexVisaScraper(debug=True) as scraper:
 
633
  # Test single visa requirement
634
  print("\nπŸ“ Single visa check: US β†’ GB")
635
  print("-" * 40)
636
+ result = await scraper.check_visa_requirement('us', 'gb')
637
  if result:
638
+ print(f"Result: {json.dumps(result)}")
639
  else:
640
+ print("❌ Failed to get result")
 
 
 
641
 
642
  # Test multiple destinations
643
  print("\nπŸ“ Multiple destinations for US passport:")
 
648
  print("\nπŸ“Š Summary:")
649
  for dest, result in results.items():
650
  if result:
651
+ print(f" βœ… {dest.upper()}: {json.dumps(result)}")
652
  else:
653
  print(f" ❌ US β†’ {dest.upper()}: Failed")
654
 
655
  if __name__ == "__main__":
656
+ asyncio.run(main())