first up load
This commit is contained in:
commit
d421469685
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "multi-search",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Search aggregator for Basalam, Torob, Digikala, SnappFood, Khanoumi, Emalls, and LionComputer",
|
||||||
|
"main": "server.cjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.cjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"node-fetch": "^2.6.12",
|
||||||
|
"puppeteer": "^23.1.1",
|
||||||
|
"puppeteer-extra": "^3.3.4",
|
||||||
|
"puppeteer-extra-plugin-stealth": "^2.12.3"
|
||||||
|
},
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/all at onece.iml" filepath="$PROJECT_DIR$/.idea/all at onece.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
// basalam.js
|
||||||
|
const fetch = require("node-fetch"); // نصب با: npm install node-fetch@2
|
||||||
|
|
||||||
|
// ساخت URL جستجوی Basalam
|
||||||
|
function buildUrl(q, from = 0, size =25) {
|
||||||
|
const base = "https://services.basalam.com/web/v1/search/product/search";
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
from: String(from),
|
||||||
|
q: q,
|
||||||
|
dynamicFacets: "true",
|
||||||
|
size: String(size),
|
||||||
|
enableNavigations: "true",
|
||||||
|
adsImpressionDisable: "false",
|
||||||
|
grouped: "true"
|
||||||
|
});
|
||||||
|
return base + "?" + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// استخراج آیتمها از JSON پاسخ Basalam
|
||||||
|
function extractItems(json) {
|
||||||
|
const candidates = [
|
||||||
|
json?.data?.products,
|
||||||
|
json?.data?.items,
|
||||||
|
json?.products,
|
||||||
|
json?.items,
|
||||||
|
json?.hits,
|
||||||
|
json?.groups?.flatMap(g => g.items || g.products || []),
|
||||||
|
json?.dynamicFacets?.flatMap(f => f.items || [])
|
||||||
|
];
|
||||||
|
for (const c of candidates) {
|
||||||
|
if (Array.isArray(c) && c.length) return c;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// نگاشت هر آیتم به ساختار استاندارد
|
||||||
|
function mapItem(item) {
|
||||||
|
const title =
|
||||||
|
item.title ||
|
||||||
|
item.name ||
|
||||||
|
item.displayName ||
|
||||||
|
item.productName ||
|
||||||
|
item?.payload?.title ||
|
||||||
|
"بدون عنوان";
|
||||||
|
|
||||||
|
const image = [
|
||||||
|
item?.photo?.MEDIUM,
|
||||||
|
item?.photo?.SMALL,
|
||||||
|
item?.image_url,
|
||||||
|
item?.image,
|
||||||
|
item?.media?.[0]?.url,
|
||||||
|
item?.thumbnail,
|
||||||
|
Array.isArray(item?.images) ? item.images[0] : null
|
||||||
|
].find(src => typeof src === "string" && src.trim().length > 0) || "https://via.placeholder.com/150";
|
||||||
|
|
||||||
|
const price =
|
||||||
|
item.price ||
|
||||||
|
item.unitPrice ||
|
||||||
|
item?.payload?.price ||
|
||||||
|
item?.minPrice ||
|
||||||
|
(item.prices && item.prices.price);
|
||||||
|
|
||||||
|
const priceText =
|
||||||
|
price != null
|
||||||
|
? typeof price === "object"
|
||||||
|
? price.value || JSON.stringify(price)
|
||||||
|
: price
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
const category = item?.categoryTitle || item?.category || "—";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
price: priceText,
|
||||||
|
image,
|
||||||
|
description:
|
||||||
|
item.description ||
|
||||||
|
item.summary ||
|
||||||
|
item.details ||
|
||||||
|
item.overview ||
|
||||||
|
item?.payload?.description ||
|
||||||
|
""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// تابع اصلی برای جستجوی Basalam
|
||||||
|
async function searchBasalam(query, size = 12) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
const url = buildUrl(query, 0, size);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
"accept-language": "en-US,en;q=0.9,fa;q=0.8",
|
||||||
|
"x-client-info": JSON.stringify({
|
||||||
|
version: "3.38.7",
|
||||||
|
project: "charsou",
|
||||||
|
name: "web.public",
|
||||||
|
platform: "web",
|
||||||
|
deviceId: "0e238114-80e2-47b4-89d6-2173f7db9b54",
|
||||||
|
sessionId: "da951b95-26f2-4b54-bc17-6c82ec110d80"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { headers });
|
||||||
|
const json = await resp.json();
|
||||||
|
const items = extractItems(json).map(mapItem);
|
||||||
|
return items;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Basalam search error:", err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export تابع
|
||||||
|
module.exports = { searchBasalam };
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// digikala.js
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search products on Digikala by query
|
||||||
|
*/
|
||||||
|
async function searchDigikala(query, limit = 12) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encodedQuery = encodeURIComponent(query);
|
||||||
|
const url = `https://api.digikala.com/v3/search/?q=${encodedQuery}`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9,fa;q=0.8"
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Digikala API error: ${response.status} ${response.statusText}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.data || !data.data.products || !Array.isArray(data.data.products)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data.products.slice(0, limit).map(item => {
|
||||||
|
const title = decodeURIComponent(escape(item.title_fa || item.title || "بدون عنوان"));
|
||||||
|
const price = item.default_variant?.price?.selling_price ||
|
||||||
|
item.default_variant?.price?.rrp ||
|
||||||
|
"—";
|
||||||
|
const formattedPrice = price !== "—" ?
|
||||||
|
new Intl.NumberFormat('fa-IR').format(price) + " تومان" :
|
||||||
|
"—";
|
||||||
|
const image = item.images?.main?.url ||
|
||||||
|
item.images?.gallery?.[0]?.url ||
|
||||||
|
"https://via.placeholder.com/150";
|
||||||
|
const link = `https://www.digikala.com${item.url?.uri || ''}`;
|
||||||
|
const category = item.category?.title_fa || item.category?.title || "—";
|
||||||
|
const description = item.description || item.short_description || "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
price: formattedPrice,
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
link
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Digikala API error:", err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single product by its numeric ID
|
||||||
|
*/
|
||||||
|
async function getProductById(productId) {
|
||||||
|
if (!productId) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `https://api.digikala.com/v2/product/${productId}/`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9,fa;q=0.8"
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Digikala product API error: ${response.status} ${response.statusText}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.data?.product) {
|
||||||
|
console.warn("Product data not found in response");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = data.data.product;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const title = product.title_fa || product.title || "بدون عنوان";
|
||||||
|
|
||||||
|
// Image
|
||||||
|
const image = product.images?.main?.url || product.images?.gallery?.[0]?.url || "https://via.placeholder.com/150";
|
||||||
|
|
||||||
|
// Description
|
||||||
|
const description = product.description || product.short_description || "توضیحاتی موجود نیست.";
|
||||||
|
|
||||||
|
// Price
|
||||||
|
let price = "—";
|
||||||
|
if (product?.default_variant?.price?.selling_price) {
|
||||||
|
price = product.default_variant.price.selling_price;
|
||||||
|
} else if (product?.variants?.length > 0 && product.variants[0]?.price?.selling_price) {
|
||||||
|
price = product.variants[0].price.selling_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedPrice = price !== "—"
|
||||||
|
? new Intl.NumberFormat('fa-IR').format(price) + " تومان"
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
price: formattedPrice
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching product by ID:", err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { searchDigikala, getProductById };
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
// emalls.js
|
||||||
|
const puppeteer = require("puppeteer");
|
||||||
|
|
||||||
|
async function searchEmalls(query, limit = 20) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto(`https://emalls.ir/search?q=${encodeURIComponent(query)}`, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for any content to load
|
||||||
|
await page.waitForSelector('body', { timeout: 10000 });
|
||||||
|
|
||||||
|
const products = await page.evaluate((limit) => {
|
||||||
|
// Try to find products by looking for links containing "/product/"
|
||||||
|
const links = Array.from(document.querySelectorAll('a[href*="/product/"]'));
|
||||||
|
const products = [];
|
||||||
|
|
||||||
|
for (let link of links) {
|
||||||
|
if (products.length >= limit) break;
|
||||||
|
|
||||||
|
const title = link.querySelector('h1, h2, h3, h4, .title, [class*="title"]')?.textContent ||
|
||||||
|
link.title || link.getAttribute('title') || "بدون عنوان";
|
||||||
|
|
||||||
|
const price = link.querySelector('.price, [class*="price"]')?.textContent || "—";
|
||||||
|
|
||||||
|
const img = link.querySelector('img');
|
||||||
|
const image = img?.src || img?.getAttribute('data-src') || "https://via.placeholder.com/150";
|
||||||
|
|
||||||
|
products.push({
|
||||||
|
title: title.trim(),
|
||||||
|
price: price.trim(),
|
||||||
|
image,
|
||||||
|
link: link.href
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}, limit);
|
||||||
|
|
||||||
|
return products;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Emalls scraping error:", err.message);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
if (browser) await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { searchEmalls };
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// khanoumi.js
|
||||||
|
const puppeteer = require("puppeteer");
|
||||||
|
|
||||||
|
async function searchKhanoumi(query, limit = 12) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto(`https://khanoumi.com/search?q=${encodeURIComponent(query)}`, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = await page.evaluate((limit) => {
|
||||||
|
const items = Array.from(document.querySelectorAll('[data-product-id]'));
|
||||||
|
return items.slice(0, limit).map(item => {
|
||||||
|
const title = item.querySelector('[data-product-title]')?.textContent || "بدون عنوان";
|
||||||
|
const price = item.querySelector('[data-product-price]')?.textContent || "—";
|
||||||
|
const image = item.querySelector('img')?.src || "https://via.placeholder.com/150";
|
||||||
|
const link = item.querySelector('a')?.href || "#";
|
||||||
|
|
||||||
|
return { title, price, image, link };
|
||||||
|
});
|
||||||
|
}, limit);
|
||||||
|
|
||||||
|
return products;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Khanoumi scraping error:", err.message);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
if (browser) await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { searchKhanoumi };
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// lioncomputer.js
|
||||||
|
const puppeteer = require("puppeteer");
|
||||||
|
|
||||||
|
async function searchLionComputer(query, limit = 12) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto(`https://lioncomputer.com/search?q=${encodeURIComponent(query)}`, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = await page.evaluate((limit) => {
|
||||||
|
const items = Array.from(document.querySelectorAll('.product-item, .product-card'));
|
||||||
|
return items.slice(0, limit).map(item => {
|
||||||
|
const title = item.querySelector('h3, h4, .title')?.textContent || "بدون عنوان";
|
||||||
|
const price = item.querySelector('.price, .cost')?.textContent || "—";
|
||||||
|
const image = item.querySelector('img')?.src || "https://via.placeholder.com/150";
|
||||||
|
const link = item.querySelector('a')?.href || "#";
|
||||||
|
|
||||||
|
return { title, price, image, link };
|
||||||
|
});
|
||||||
|
}, limit);
|
||||||
|
|
||||||
|
return products;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Lion Computer scraping error:", err.message);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
if (browser) await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { searchLionComputer };
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>جستجوی محصولات</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #f5f6fa;
|
||||||
|
font-family: "Vazir", sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.card img {
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.card-body {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.source {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
#results {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
#searchBtn {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="mb-4 text-center">جستجوی محصولات</h2>
|
||||||
|
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-md-9 col-sm-12">
|
||||||
|
<input id="query" type="text" class="form-control" placeholder="نام محصول را وارد کنید...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-12">
|
||||||
|
<button id="searchBtn" class="btn btn-primary w-100">جستجو</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="results" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-3">
|
||||||
|
<!-- Result cards appear here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
// script.js
|
||||||
|
const searchBtn = document.getElementById("searchBtn");
|
||||||
|
const queryInput = document.getElementById("query");
|
||||||
|
const resultsDiv = document.getElementById("results");
|
||||||
|
|
||||||
|
// تابع دانلود عکس از URL
|
||||||
|
function downloadImage(url, filename = 'product.jpg') {
|
||||||
|
// اطمینان از اینکه URL معتبر است
|
||||||
|
if (!url || url.startsWith('data:') || url.startsWith('blob:')) {
|
||||||
|
// اگر URL دیتای داخلی است، مستقیم دانلود میشود
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// برای URLهای خارجی: با fetch دانلود و به عنوان blob دانلود
|
||||||
|
fetch(url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = blobUrl;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to download image:', err);
|
||||||
|
alert('در حال حاضر امکان دانلود این تصویر وجود ندارد.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBtn.addEventListener("click", async () => {
|
||||||
|
const q = queryInput.value.trim();
|
||||||
|
if (!q) return alert("لطفاً یک نام محصول وارد کنید");
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = "<p class='text-center'>در حال بارگذاری...</p>";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const hasResults = data.basalam?.length > 0 ||
|
||||||
|
data.torob?.length > 0 ||
|
||||||
|
data.digikala?.length > 0;
|
||||||
|
|
||||||
|
if (!hasResults) {
|
||||||
|
resultsDiv.innerHTML = "<p class='text-center'>هیچ نتیجهای یافت نشد</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = "";
|
||||||
|
|
||||||
|
const allItems = [
|
||||||
|
...data.basalam.map(item => ({...item, source: 'باسلام'})),
|
||||||
|
...data.torob.map(item => ({...item, source: 'ترب'})),
|
||||||
|
...data.digikala.map(item => ({...item, source: 'دیجیکالا'}))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allItems.length === 0) {
|
||||||
|
resultsDiv.innerHTML = "<p class='text-center'>هیچ نتیجهای یافت نشد</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allItems.forEach(item => {
|
||||||
|
const col = document.createElement("div");
|
||||||
|
col.className = "col";
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "card h-100";
|
||||||
|
|
||||||
|
const title = item.title || item.name || "بدون عنوان";
|
||||||
|
const price = item.price !== undefined && item.price !== null ?
|
||||||
|
(typeof item.price === 'object' ?
|
||||||
|
(item.price.value || JSON.stringify(item.price)) :
|
||||||
|
item.price) :
|
||||||
|
"—";
|
||||||
|
const image = (item.image || "https://via.placeholder.com/150").trim();
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<img src="${image}" class="card-img-top product-image" alt="${title}" data-url="${image}">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<h5 class="card-title name">${title}</h5>
|
||||||
|
<p class="card-text price">${price}</p>
|
||||||
|
<small class="source">منبع: ${item.source}</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// روی بقیه کارت (نه عکس) کلیک = باز کردن لینک
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
// اگر کلیک روی عکس بود، جلوی انتشار event را بگیر
|
||||||
|
if (e.target.closest('.product-image')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.link) {
|
||||||
|
window.open(item.link, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// روی عکس کلیک = دانلود
|
||||||
|
const imgElement = card.querySelector('.product-image');
|
||||||
|
imgElement.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation(); // جلوگیری از باز شدن لینک
|
||||||
|
const cleanUrl = imgElement.src;
|
||||||
|
const filename = `product_${item.source}_${Date.now()}.jpg`;
|
||||||
|
downloadImage(cleanUrl, filename);
|
||||||
|
});
|
||||||
|
|
||||||
|
col.appendChild(card);
|
||||||
|
resultsDiv.appendChild(col);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
resultsDiv.innerHTML = "<p class='text-center text-danger'>خطا در دریافت دادهها</p>";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow Enter key
|
||||||
|
queryInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
searchBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// server.cjs
|
||||||
|
const express = require("express");
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
const app = express();
|
||||||
|
const PORT = 9093;
|
||||||
|
|
||||||
|
app.use(express.static("public"));
|
||||||
|
|
||||||
|
// Import all search functions
|
||||||
|
const { searchDigikala, getProductById } = require("./digikala");
|
||||||
|
const { searchBasalam } = require("./basalam");
|
||||||
|
const { searchTorob } = require("./torob");
|
||||||
|
const { searchSnappFood } = require("./snappfood");
|
||||||
|
const { searchKhanoumi } = require("./khanoumi");
|
||||||
|
const { searchEmalls } = require("./emalls");
|
||||||
|
const { searchLionComputer } = require("./lioncomputer");
|
||||||
|
|
||||||
|
// Main search API route
|
||||||
|
app.get("/api/search", async (req, res) => {
|
||||||
|
const q = req.query.q || "";
|
||||||
|
if (!q) return res.status(400).json({ error: "Query is required" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute all searches in parallel
|
||||||
|
const [basalam, torob, digikala, snappfood, khanoumi, emalls, lioncomputer] = await Promise.allSettled([
|
||||||
|
searchBasalam(q),
|
||||||
|
searchTorob(q),
|
||||||
|
searchDigikala(q),
|
||||||
|
searchSnappFood(q),
|
||||||
|
searchKhanoumi(q),
|
||||||
|
searchEmalls(q),
|
||||||
|
searchLionComputer(q)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Handle results - return successful results or empty arrays
|
||||||
|
const basalamResults = basalam.status === 'fulfilled' ? basalam.value : [];
|
||||||
|
const torobResults = torob.status === 'fulfilled' ? torob.value : [];
|
||||||
|
const digikalaResults = digikala.status === 'fulfilled' ? digikala.value : [];
|
||||||
|
const snappfoodResults = snappfood.status === 'fulfilled' ? snappfood.value : [];
|
||||||
|
const khanoumiResults = khanoumi.status === 'fulfilled' ? khanoumi.value : [];
|
||||||
|
const emallsResults = emalls.status === 'fulfilled' ? emalls.value : [];
|
||||||
|
const lioncomputerResults = lioncomputer.status === 'fulfilled' ? lioncomputer.value : [];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
basalam: basalamResults,
|
||||||
|
torob: torobResults,
|
||||||
|
digikala: digikalaResults,
|
||||||
|
snappfood: snappfoodResults,
|
||||||
|
khanoumi: khanoumiResults,
|
||||||
|
emalls: emallsResults,
|
||||||
|
lioncomputer: lioncomputerResults
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Search API error:", err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test endpoint for each service
|
||||||
|
app.get("/api/test/:service", async (req, res) => {
|
||||||
|
const service = req.params.service;
|
||||||
|
const query = "تست";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let results = [];
|
||||||
|
switch(service) {
|
||||||
|
case 'basalam':
|
||||||
|
results = await searchBasalam(query);
|
||||||
|
break;
|
||||||
|
case 'torob':
|
||||||
|
results = await searchTorob(query);
|
||||||
|
break;
|
||||||
|
case 'digikala':
|
||||||
|
results = await searchDigikala(query);
|
||||||
|
break;
|
||||||
|
case 'snappfood':
|
||||||
|
results = await searchSnappFood(query);
|
||||||
|
break;
|
||||||
|
case 'khanoumi':
|
||||||
|
results = await searchKhanoumi(query);
|
||||||
|
break;
|
||||||
|
case 'emalls':
|
||||||
|
results = await searchEmalls(query);
|
||||||
|
break;
|
||||||
|
case 'lioncomputer':
|
||||||
|
results = await searchLionComputer(query);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return res.status(400).json({ error: "Invalid service" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ service, results });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ NEW: Get Digikala product by ID (correctly implemented, nothing missing)
|
||||||
|
app.get("/api/digikala/product/:id", async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Validate that ID is a positive integer string
|
||||||
|
if (!/^\d+$/.test(id)) {
|
||||||
|
return res.status(400).json({ error: "Product ID must be a positive integer." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const product = await getProductById(Number(id));
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return res.status(404).json({ error: "Product not found or unavailable." });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(product);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in /api/digikala/product/:id:", err.message);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`✓ Server running at http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// snappfood.js
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
async function searchSnappFood(query, limit = 12) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use Tehran coordinates as default
|
||||||
|
const url = `https://api.snappfood.ir/search/v1/quick?lat=35.6892&lng=51.3890&search=${encodeURIComponent(query)}`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9,fa;q=0.8"
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
if (response.status !== 200) return [];
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const restaurants = data.data?.restaurants || [];
|
||||||
|
|
||||||
|
return restaurants.slice(0, limit).map(restaurant => ({
|
||||||
|
title: restaurant.title || "بدون عنوان",
|
||||||
|
price: restaurant.delivery_time || "—",
|
||||||
|
image: restaurant.logo || "https://via.placeholder.com/150",
|
||||||
|
link: `https://snappfood.ir/restaurant/${restaurant.id}`
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("SnappFood error:", err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { searchSnappFood };
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// torob.js
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
async function searchTorob(query, size = 24) {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encodedQuery = encodeURIComponent(query);
|
||||||
|
// Use the exact working endpoint from your HTML
|
||||||
|
const url = `https://api.torob.com/v4/base-product/search/?page=0&sort=popularity&size=${size}&q=${encodedQuery}`;
|
||||||
|
|
||||||
|
// Minimal headers that actually work
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Referer": "https://www.torob.com/"
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
|
||||||
|
// Don't throw on non-200, just return empty array
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log(`Torob returned status: ${response.status}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const results = data.results || [];
|
||||||
|
|
||||||
|
return results.map(item => ({
|
||||||
|
title: item.name1 || "بدون عنوان",
|
||||||
|
price: item.price_text || "—",
|
||||||
|
image: item.image_url || "https://via.placeholder.com/150",
|
||||||
|
link: `https://torob.com${item.web_client_absolute_url || ''}`
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Torob error:", err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { searchTorob };
|
||||||
Loading…
Reference in New Issue