from app.utils.browser_utils import safe_browser_operation from app.utils.http_utils import fetch_url import asyncio from bs4 import BeautifulSoup async def visit_url_service(decoded_url): """Service function to visit a URL and get its content""" print(f"Visiting URL: {decoded_url}") # Define the operation to perform with the browser async def visit_operation(page): try: response = await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000) if not response: print(f"Warning: No response object returned for {decoded_url}") # Get page content content = await page.content() return {"status": "success", "content": content} except Exception as e: print(f"Error during page navigation: {e}") # Try to get content anyway try: content = await page.content() return {"status": "partial", "content": content, "error": str(e)} except: raise Exception(f"Failed to get page content: {str(e)}") # Perform the operation return await safe_browser_operation(decoded_url, visit_operation) async def extract_seo_service(decoded_url): """Service function to extract SEO information from a website""" print(f"Extracting SEO from: {decoded_url}") # Define the operation to perform with the browser async def seo_operation(page): try: response = await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000) # Extract SEO information seo_data = await page.evaluate('''() => { const data = { title: document.title || '', description: '', canonical: '', h1: [], h2: [], images: 0, links: 0 }; // Get meta description const metaDescription = document.querySelector('meta[name="description"]'); if (metaDescription) { data.description = metaDescription.getAttribute('content') || ''; } // Get canonical link const canonicalLink = document.querySelector('link[rel="canonical"]'); if (canonicalLink) { data.canonical = canonicalLink.getAttribute('href') || ''; } // Get h1 tags document.querySelectorAll('h1').forEach(h1 => { const text = h1.innerText.trim(); if (text) data.h1.push(text); }); // Get h2 tags document.querySelectorAll('h2').forEach(h2 => { const text = h2.innerText.trim(); if (text) data.h2.push(text); }); // Count images data.images = document.querySelectorAll('img').length; // Count links data.links = document.querySelectorAll('a').length; return data; }''') result = { "status": "success", "url": decoded_url, "seo": seo_data } return result except Exception as e: print(f"Error during SEO extraction: {e}") return {"status": "error", "url": decoded_url, "error": str(e)} # Perform the operation return await safe_browser_operation(decoded_url, seo_operation) async def extract_meta_tags_service(decoded_url): """Service function to extract meta tags from a website""" print(f"Extracting meta tags from: {decoded_url}") try: # Fetch the page content using plain HTTP request content, status = await fetch_url(decoded_url) # Parse the HTML soup = BeautifulSoup(content, 'html.parser') # Extract all meta tags meta_tags = [] for meta in soup.find_all('meta'): attributes = {} for attr in meta.attrs: attributes[attr] = meta[attr] meta_tags.append(attributes) # Extract Open Graph tags og_tags = {} for meta in soup.find_all('meta', property=lambda x: x and x.startswith('og:')): og_tags[meta.get('property')] = meta.get('content') # Extract Twitter card tags twitter_tags = {} for meta in soup.find_all('meta', attrs={'name': lambda x: x and x.startswith('twitter:')}): twitter_tags[meta.get('name')] = meta.get('content') # Get page title title = soup.title.string if soup.title else '' result = { "status": "success", "url": decoded_url, "meta_tags": meta_tags, "open_graph": og_tags, "twitter_card": twitter_tags, "title": title } return result except Exception as e: print(f"Error during meta tag extraction: {e}") return {"status": "error", "url": decoded_url, "error": str(e)} async def detect_pagination_service(decoded_url): """Service function to detect pagination on a website""" print(f"Detecting pagination on: {decoded_url}") # Define the operation to perform with the browser async def pagination_operation(page): try: # Navigate to the URL try: await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000) except Exception as e: print(f"Error navigating to URL: {e}") # Try to get the current URL even if navigation failed try: original_url = page.url except: original_url = decoded_url else: original_url = page.url # Check for common pagination indicators try: pagination_data = await page.evaluate('''() => { const data = { hasPagination: false, paginationType: null, paginationElements: [], detectedParameter: null, lastPageNumber: null }; // Look for numbered pagination links (1, 2, 3...) const numberedLinks = Array.from(document.querySelectorAll('a, button, span')) .filter(el => { const text = el.innerText.trim(); // check for data-page attribute const dataPage = el.getAttribute('data-page'); if (dataPage) { return /^[0-9]+$/.test(dataPage); } return /^[0-9]+$/.test(text) && (el.tagName === 'A' || el.onclick || el.closest('button, [role="button"]')); }); console.log(numberedLinks); // Look for next/prev buttons const nextButtons = Array.from(document.querySelectorAll('a, button, [role="button"]')) .filter(el => { const text = el.innerText.trim().toLowerCase(); const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || ''; const hasNextIcon = el.querySelector('i.fa-chevron-right, i.fa-arrow-right, svg[class*="arrow"], svg[class*="next"]'); return text.includes('next') || text.includes('›') || text.includes('»') || text.includes('→') || ariaLabel.includes('next') || hasNextIcon; }); // Check for pagination containers const paginationContainers = Array.from(document.querySelectorAll( '.pagination, [class*="pagination"], [class*="pager"], nav[aria-label*="pagination"], [role="navigation"]' )); // Collect all potential pagination elements if (numberedLinks.length > 0) { data.hasPagination = true; data.paginationType = 'numbered'; // Get href attributes or other identifiers from numbered links data.paginationElements = numberedLinks.slice(0, 5).map(el => { return { text: el.innerText.trim(), dataPage: el.getAttribute('data-page'), href: el.tagName === 'A' ? el.href : null, classes: el.className, id: el.id }; }); // Try to find the last page number const pageNumbers = numberedLinks .map(el => parseInt(el.innerText.replace(/[^\d]/g, ''))) .filter(num => !isNaN(num)); if (pageNumbers.length > 0) { data.lastPageNumber = Math.max(...pageNumbers); } // Also look for a "last page" element that might have text like "Last" or "»" const lastPageElement = Array.from(document.querySelectorAll('a, button')) .find(el => { const text = el.innerText.trim().toLowerCase(); const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || ''; return text.includes('last') || text === '»' || ariaLabel.includes('last page'); }); if (lastPageElement && lastPageElement.href) { // Try to extract page number from the URL try { const url = new URL(lastPageElement.href); // Check common pagination parameters ['page', 'p', 'pg'].forEach(param => { if (url.searchParams.has(param)) { const value = parseInt(url.searchParams.get(param)); if (!isNaN(value) && (data.lastPageNumber === null || value > data.lastPageNumber)) { data.lastPageNumber = value; } } }); // Check for path-based pagination (like /page/10) const pathMatch = url.pathname.match(/\/(?:page|p)\/(\d+)/i); if (pathMatch && pathMatch[1]) { const value = parseInt(pathMatch[1]); if (!isNaN(value) && (data.lastPageNumber === null || value > data.lastPageNumber)) { data.lastPageNumber = value; } } } catch (e) { console.error("Error parsing last page URL:", e); } } } else if (nextButtons.length > 0) { data.hasPagination = true; data.paginationType = 'next-prev'; // Get information about next buttons data.paginationElements = nextButtons.slice(0, 3).map(el => { return { text: el.innerText.trim(), href: el.tagName === 'A' ? el.href : null, classes: el.className, id: el.id }; }); } // Check for URL parameters that might indicate pagination const currentUrl = window.location.href; const urlParams = new URL(currentUrl).searchParams; // Common pagination parameters const paginationParams = ['page', 'p', 'pg', 'offset', 'o', 'from', 'start', 'limit']; for (const param of paginationParams) { if (urlParams.has(param)) { data.detectedParameter = { name: param, value: urlParams.get(param) }; break; } } return data; }''') except Exception as e: print(f"Error during JavaScript evaluation for pagination detection: {e}") # Return a safe default if JavaScript evaluation fails pagination_data = { 'hasPagination': False, 'paginationType': None, 'paginationElements': [], 'detectedParameter': None, 'lastPageNumber': None } # If pagination is detected, try to navigate to the next page by clicking next_page_url = None pagination_parameter = None url_template = None step_size = None original_parsed = None # Initialize the variable if pagination_data['hasPagination']: print("Pagination detected, attempting to click on a pagination element") # Always try to click on a pagination element, regardless of type clicked = False # First try to click on a numbered link (preferably "2" if we're on page 1) try: clicked = await page.evaluate('''() => { try { // First try to find and click on a "2" link or button const page2Elements = Array.from(document.querySelectorAll('a[href], button, [role="button"]')) .filter(el => { // Check for text content "2" if (el.innerText.trim() === '2') { return true; } // Check for href with page=2 or similar (for anchor elements) if (el.tagName === 'A' && el.href) { try { const url = new URL(el.href, window.location.origin); const paginationParams = ['page', 'p', 'pg', 'offset', 'o', 'from', 'start', 'limit', 'currentpage', 'pagenum', 'pageNumber', 'paged']; for (const param of paginationParams) { if (url.searchParams.has(param) && url.searchParams.get(param) === '2') { return true; } } // Check for path-based pagination like /page/2/ const pathMatch = url.pathname.match(/\/(page|p)\/2\/?$/i); if (pathMatch) { return true; } } catch (e) {} } // Check for data attributes that might indicate pagination if (el.getAttribute('data-page') === '2' || el.getAttribute('data-pagenumber') === '2' || el.getAttribute('data-page-number') === '2') { return true; } return false; }); if (page2Elements.length > 0) { console.log("Clicking on page 2 element"); page2Elements[0].click(); return true; } // If no "2" link found, try any numbered link or button const numberedElements = Array.from(document.querySelectorAll('a[href], button, [role="button"]')) .filter(el => /^\d+$/.test(el.innerText.trim())); if (numberedElements.length > 0) { // Sort by number and get the second one (likely page 2) const sorted = numberedElements.sort((a, b) => { return parseInt(a.innerText.trim()) - parseInt(b.innerText.trim()); }); // Get the second element if available (page 2), otherwise the first one const elementToClick = sorted.length > 1 ? sorted[1] : sorted[0]; console.log("Clicking on numbered element: " + elementToClick.innerText); elementToClick.click(); return true; } // If no numbered links, try next button const nextTexts = ['next', '›', '»', '→']; const nextElements = Array.from(document.querySelectorAll('a, button, [role="button"]')) .filter(el => { const text = el.textContent.trim().toLowerCase(); const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || ''; return nextTexts.some(t => text.includes(t)) || ariaLabel.includes('next') || el.querySelector('i.fa-chevron-right, i.fa-arrow-right, svg[class*="arrow"], svg[class*="next"]'); }); if (nextElements.length > 0) { console.log("Clicking on next button"); nextElements[0].click(); return true; } return false; } catch (error) { console.error("Error during pagination click operation:", error); return false; } }''') if clicked: print("Successfully clicked on pagination element") # Wait for navigation to complete await asyncio.sleep(1) next_page_url = page.url else: print("No clickable pagination element found") except Exception as e: print(f"Error clicking on pagination element: {e}") # Continue with the process even if clicking fails # If we successfully navigated to the next page, analyze the URL difference if next_page_url: print('Searching for pagination parameter') # Parse both URLs try: original_parsed = await page.evaluate(f'''(originalUrl) => {{ try {{ const original = new URL(originalUrl); const current = new URL(window.location.href); // Check for differences in query parameters let paramDiff = null; // Common pagination parameters to check const paginationParams = ['page', 'p', 'pg', 'offset', 'o', 'from', 'start', 'limit', 'currentPage', 'current_page', 'currentpage', 'pagenum', 'pageNumber', 'paged']; for (const param of paginationParams) {{ const originalValue = original.searchParams.get(param); const currentValue = current.searchParams.get(param); if (originalValue !== currentValue && currentValue !== null) {{ paramDiff = {{ name: param, originalValue: originalValue, currentValue: currentValue }}; break; }} }} // Check for path differences (like /page/1 vs /page/2) const originalPath = original.pathname; const currentPath = current.pathname; let pathDiff = null; if (originalPath !== currentPath) {{ const originalSegments = originalPath.split('/').filter(s => s); const currentSegments = currentPath.split('/').filter(s => s); // Find the segment that changed if (originalSegments.length === currentSegments.length) {{ for (let i = 0; i < originalSegments.length; i++) {{ if (originalSegments[i] !== currentSegments[i]) {{ // Check if the difference is numeric if (!isNaN(originalSegments[i]) && !isNaN(currentSegments[i])) {{ pathDiff = {{ index: i, originalValue: originalSegments[i], currentValue: currentSegments[i] }}; }} }} }} }} }} return {{ paramDiff, pathDiff, originalUrl: originalUrl, currentUrl: window.location.href }}; }} catch (error) {{ console.error("Error during URL analysis:", error); return {{ paramDiff: null, pathDiff: null, originalUrl: originalUrl, currentUrl: window.location.href, error: error.message }}; }} }}''', original_url) except Exception as e: print(f"Error during URL analysis: {e}") original_parsed = { 'paramDiff': None, 'pathDiff': None, 'originalUrl': original_url, 'currentUrl': next_page_url, 'error': str(e) } # Determine the pagination parameter and create URL template if original_parsed: print(original_parsed) if original_parsed.get('paramDiff'): param_name = original_parsed['paramDiff']['name'] pagination_parameter = { 'type': 'query', 'name': param_name, 'value': original_parsed['paramDiff']['currentValue'] } # --- STEP SIZE DETECTION FOR QUERY PARAM --- orig_val = original_parsed['paramDiff']['originalValue'] if original_parsed['paramDiff']['originalValue'] is not None else 0 curr_val = original_parsed['paramDiff']['currentValue'] try: if orig_val is not None and curr_val is not None: orig_num = int(orig_val) curr_num = int(curr_val) step_size = abs(curr_num - orig_num) except Exception: step_size = None # Create URL template for query parameter try: url_obj = await page.evaluate(f'''(url, paramName) => {{ try {{ const urlObj = new URL(url); urlObj.searchParams.set(paramName, "{{PAGE_NUMBER}}"); return urlObj.toString(); }} catch (error) {{ console.error("Error creating URL template:", error); return null; }} }}''', original_url, param_name) url_template = url_obj except Exception as e: print(f"Error creating URL template for query parameter: {e}") url_template = None elif original_parsed.get('pathDiff'): path_index = original_parsed['pathDiff']['index'] pagination_parameter = { 'type': 'path', 'index': path_index, 'value': original_parsed['pathDiff']['currentValue'] } # --- STEP SIZE DETECTION FOR PATH PARAM --- orig_val = original_parsed['pathDiff']['originalValue'] curr_val = original_parsed['pathDiff']['currentValue'] try: if orig_val is not None and curr_val is not None: orig_num = int(orig_val) curr_num = int(curr_val) step_size = abs(curr_num - orig_num) except Exception: step_size = None # Create URL template for path parameter try: url_template = await page.evaluate(f'''(url, pathIndex) => {{ try {{ const urlObj = new URL(url); const pathSegments = urlObj.pathname.split('/').filter(s => s); pathSegments[pathIndex] = "{{PAGE_NUMBER}}"; urlObj.pathname = '/' + pathSegments.join('/'); return urlObj.toString(); }} catch (error) {{ console.error("Error creating path URL template:", error); return null; }} }}''', original_url, path_index) except Exception as e: print(f"Error creating URL template for path parameter: {e}") url_template = None if url_template: # Decode URL-encoded characters in the template import urllib.parse url_template = urllib.parse.unquote(url_template) # If we couldn't determine the URL template from navigation, try to infer it if not url_template and pagination_data['detectedParameter']: param_name = pagination_data['detectedParameter']['name'] try: url_template = await page.evaluate(f'''(url, paramName) => {{ try {{ const urlObj = new URL(url); urlObj.searchParams.set(paramName, "{{PAGE_NUMBER}}"); return urlObj.toString(); }} catch (error) {{ console.error("Error creating inferred URL template:", error); return null; }} }}''', original_url, param_name) except Exception as e: print(f"Error creating inferred URL template: {e}") url_template = None # Return the pagination detection results with a simplified structure result = { "status": "success", "hasPagination": pagination_data['hasPagination'], "urlTemplate": url_template, "lastPage": pagination_data['lastPageNumber'], "stepSize": step_size if step_size is not None and step_size >= 5 else 1 } return result except Exception as e: print(f"Error during pagination detection: {e}") return { "status": "error", "hasPagination": False, "urlTemplate": None, "lastPage": None, "stepSize": None, "error": str(e) } # Perform the operation result = await safe_browser_operation(decoded_url, pagination_operation) # Check if the result is an error from safe_browser_operation if isinstance(result, dict) and result.get("status") == "error": return result return result