HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Services/SerpApiService.php
<?php

namespace App\Services;

use App\Models\Setting;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SerpApiService
{
    private const BASE_URL = 'https://serpapi.com/search.json';

    private const LANGUAGE_NAMES = [
        'af' => 'Afrikaans', 'sq' => 'Albanian', 'ar' => 'Arabic', 'hy' => 'Armenian',
        'az' => 'Azerbaijani', 'eu' => 'Basque', 'be' => 'Belarusian', 'bn' => 'Bengali',
        'bs' => 'Bosnian', 'bg' => 'Bulgarian', 'ca' => 'Catalan', 'zh' => 'Chinese',
        'zh-CN' => 'Chinese', 'zh-TW' => 'Chinese', 'hr' => 'Croatian', 'cs' => 'Czech',
        'da' => 'Danish', 'nl' => 'Dutch', 'et' => 'Estonian', 'fi' => 'Finnish',
        'fr' => 'French', 'gl' => 'Galician', 'ka' => 'Georgian', 'de' => 'German',
        'el' => 'Greek', 'gu' => 'Gujarati', 'ht' => 'Haitian Creole', 'he' => 'Hebrew',
        'hi' => 'Hindi', 'hu' => 'Hungarian', 'id' => 'Indonesian', 'ga' => 'Irish',
        'it' => 'Italian', 'ja' => 'Japanese', 'kn' => 'Kannada', 'kk' => 'Kazakh',
        'ko' => 'Korean', 'lv' => 'Latvian', 'lt' => 'Lithuanian', 'mk' => 'Macedonian',
        'ms' => 'Malay', 'ml' => 'Malayalam', 'mt' => 'Maltese', 'mr' => 'Marathi',
        'mn' => 'Mongolian', 'ne' => 'Nepali', 'nb' => 'Norwegian', 'no' => 'Norwegian',
        'fa' => 'Persian', 'pl' => 'Polish', 'pt' => 'Portuguese', 'pt-BR' => 'Portuguese',
        'pa' => 'Punjabi', 'ro' => 'Romanian', 'ru' => 'Russian', 'sr' => 'Serbian',
        'sk' => 'Slovak', 'sl' => 'Slovenian', 'es' => 'Spanish', 'sw' => 'Swahili',
        'sv' => 'Swedish', 'tl' => 'Filipino', 'ta' => 'Tamil', 'te' => 'Telugu',
        'th' => 'Thai', 'tr' => 'Turkish', 'uk' => 'Ukrainian', 'ur' => 'Urdu',
        'uz' => 'Uzbek', 'vi' => 'Vietnamese', 'cy' => 'Welsh', 'yi' => 'Yiddish',
    ];

    private const US_STATE_MAP = [
        'AL' => 'Alabama',
        'AK' => 'Alaska',
        'AZ' => 'Arizona',
        'AR' => 'Arkansas',
        'CA' => 'California',
        'CO' => 'Colorado',
        'CT' => 'Connecticut',
        'DE' => 'Delaware',
        'FL' => 'Florida',
        'GA' => 'Georgia',
        'HI' => 'Hawaii',
        'ID' => 'Idaho',
        'IL' => 'Illinois',
        'IN' => 'Indiana',
        'IA' => 'Iowa',
        'KS' => 'Kansas',
        'KY' => 'Kentucky',
        'LA' => 'Louisiana',
        'ME' => 'Maine',
        'MD' => 'Maryland',
        'MA' => 'Massachusetts',
        'MI' => 'Michigan',
        'MN' => 'Minnesota',
        'MS' => 'Mississippi',
        'MO' => 'Missouri',
        'MT' => 'Montana',
        'NE' => 'Nebraska',
        'NV' => 'Nevada',
        'NH' => 'New Hampshire',
        'NJ' => 'New Jersey',
        'NM' => 'New Mexico',
        'NY' => 'New York',
        'NC' => 'North Carolina',
        'ND' => 'North Dakota',
        'OH' => 'Ohio',
        'OK' => 'Oklahoma',
        'OR' => 'Oregon',
        'PA' => 'Pennsylvania',
        'RI' => 'Rhode Island',
        'SC' => 'South Carolina',
        'SD' => 'South Dakota',
        'TN' => 'Tennessee',
        'TX' => 'Texas',
        'UT' => 'Utah',
        'VT' => 'Vermont',
        'VA' => 'Virginia',
        'WA' => 'Washington',
        'WV' => 'West Virginia',
        'WI' => 'Wisconsin',
        'WY' => 'Wyoming',
        'DC' => 'District of Columbia',
    ];

    private function apiKey(): string
    {
        try {
            $key = Setting::get('superscrape_serpapi_key', '');
        } catch (\Throwable $e) {
            $key = '';
        }

        return is_string($key) ? trim($key) : '';
    }

    public function isConfigured(): bool
    {
        return $this->apiKey() !== '';
    }

    private function buildMapsQuery(string $query, string $location, string $language): string
    {
        $q = trim($query);

        $normalizedLocation = $location !== '' ? $this->normalizeLocation($location) : '';
        if ($normalizedLocation !== '') {
            $q .= ' in ' . $normalizedLocation;
        }

        $langName = self::LANGUAGE_NAMES[$language] ?? null;
        if ($langName !== null && strtolower($language) !== 'en') {
            $q .= ' with ' . $langName . ' language';
        }

        return $q;
    }

    public function searchMaps(string $query, string $location = '', string $language = 'en', int $limit = 20): array
    {
        $fullQuery = $this->buildMapsQuery($query, $location, $language);

        $params = [
            'engine'        => 'google_maps',
            'type'          => 'search',
            'q'             => $fullQuery,
            'google_domain' => 'google.com',
            'gl'            => 'us',
            'hl'            => $language,
            'api_key'       => $this->apiKey(),
        ];

        return $this->fetchPaginated($params, $limit, 'local_results', $query);
    }

    public function searchPlaces(string $query, string $location = '', string $language = 'en', int $limit = 20): array
    {
        $fullQuery = $this->buildMapsQuery($query, $location, $language);

        $params = [
            'engine'        => 'google_maps',
            'type'          => 'search',
            'q'             => $fullQuery,
            'google_domain' => 'google.com',
            'gl'            => 'us',
            'hl'            => $language,
            'api_key'       => $this->apiKey(),
        ];

        return $this->fetchPaginated($params, $limit, 'local_results', $query);
    }

    public function searchReviews(string $placeId, string $language = 'en', int $limit = 20): array
    {
        $params = [
            'engine'   => 'google_maps_reviews',
            'place_id' => $placeId,
            'hl'       => $language,
            'api_key'  => $this->apiKey(),
        ];

        return $this->fetchPaginated($params, $limit, 'reviews');
    }

    public function searchImages(string $query, string $language = 'en', int $limit = 20): array
    {
        $params = [
            'engine'  => 'google_images',
            'q'       => $query,
            'hl'      => $language,
            'num'     => min($limit, 100),
            'api_key' => $this->apiKey(),
        ];

        try {
            $response = Http::timeout(30)->get(self::BASE_URL, $params);

            if (!$response->successful()) {
                Log::warning('SerpAPI images request failed', ['status' => $response->status(), 'body' => $response->body()]);
                return [];
            }

            $data = $response->json();
            return (array) ($data['images_results'] ?? []);
        } catch (\Throwable $e) {
            Log::error('SerpAPI images error', ['error' => $e->getMessage()]);
            return [];
        }
    }

    private function fetchPaginated(array $params, int $limit, string $resultsKey, ?string $fallbackQuery = null): array
    {
        $allResults = [];
        $start      = 0;
        $pageSize   = 20;
        $retriedFallback = false;

        while (count($allResults) < $limit) {
            $params['start'] = $start;

            try {
                $response = Http::timeout(30)->get(self::BASE_URL, $params);

                if (!$response->successful()) {
                    $body = (string) $response->body();
                    Log::warning('SerpAPI request failed', [
                        'status' => $response->status(),
                        'body'   => substr($body, 0, 500),
                        'params' => array_diff_key($params, ['api_key' => '']),
                    ]);
                    break;
                }

                $data = $response->json();

                if (is_array($data) && !empty($data['error'])) {
                    throw new \RuntimeException((string) $data['error']);
                }

                $results = (array) ($data[$resultsKey] ?? []);

                if (empty($results)) {
                    if (!$retriedFallback && $fallbackQuery !== null && $params['q'] !== $fallbackQuery && $start === 0) {
                        Log::warning('SerpAPI returned empty results with location in query, retrying with query only', [
                            'original_query' => $params['q'],
                            'fallback_query'  => $fallbackQuery,
                        ]);
                        $params['q'] = $fallbackQuery;
                        $retriedFallback = true;
                        $start = 0;
                        $allResults = [];
                        continue;
                    }
                    break;
                }

                $allResults = array_merge($allResults, $results);
                $start += $pageSize;

                if (!isset($data['serpapi_pagination']['next'])) {
                    break;
                }

                usleep(300000);
            } catch (\Throwable $e) {
                Log::error('SerpAPI fetch error', ['error' => $e->getMessage()]);
                break;
            }
        }

        return array_slice($allResults, 0, $limit);
    }

    private function normalizeLocation(string $location): string
    {
        $location = trim($location);

        if ($location === '') {
            return '';
        }

        $parts = array_map(static fn ($part) => trim($part), explode(',', $location));
        $parts = array_values(array_filter($parts, static fn ($part) => $part !== ''));

        foreach ($parts as $index => $part) {
            $upper = strtoupper($part);

            if (isset(self::US_STATE_MAP[$upper])) {
                $parts[$index] = self::US_STATE_MAP[$upper];
                continue;
            }

            if (in_array($upper, ['USA', 'US', 'U.S.A.', 'U.S.'], true)) {
                $parts[$index] = 'United States';
            }
        }

        return implode(', ', $parts);
    }

    public function normalizeMapsResult(array $item): array
    {
        return [
            'name'          => $item['title'] ?? '',
            'email'         => '',
            'phone'         => $item['phone'] ?? '',
            'website'       => $item['website'] ?? '',
            'address'       => $item['address'] ?? '',
            'rating'        => isset($item['rating']) ? (float) $item['rating'] : null,
            'reviews_count' => isset($item['reviews']) ? (int) $item['reviews'] : null,
            'category'      => $item['type'] ?? '',
            'raw_data'      => $item,
        ];
    }

    public function normalizePlacesResult(array $item): array
    {
        return $this->normalizeMapsResult($item);
    }

    public function normalizeReviewsResult(array $item): array
    {
        return [
            'name'          => $item['user']['name'] ?? '',
            'email'         => '',
            'phone'         => '',
            'website'       => $item['user']['link'] ?? '',
            'address'       => '',
            'rating'        => isset($item['rating']) ? (float) $item['rating'] : null,
            'reviews_count' => null,
            'category'      => 'review',
            'raw_data'      => $item,
        ];
    }

    public function normalizeImagesResult(array $item): array
    {
        return [
            'name'          => $item['title'] ?? '',
            'email'         => '',
            'phone'         => '',
            'website'       => $item['source'] ?? ($item['link'] ?? ''),
            'address'       => '',
            'rating'        => null,
            'reviews_count' => null,
            'category'      => 'image',
            'raw_data'      => $item,
        ];
    }
}