152 lines
4.9 KiB
JavaScript
152 lines
4.9 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const path = require('path');
|
|
const { ApifyClient } = require('apify-client');
|
|
const store = require('./store');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 8086;
|
|
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', path.join(__dirname, 'views'));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// Increase timeout for long Apify runs
|
|
app.use((req, res, next) => {
|
|
res.setTimeout(300000); // 5 min
|
|
next();
|
|
});
|
|
|
|
// --- Pages ---
|
|
app.get('/', (req, res) => {
|
|
const searches = store.getSearches();
|
|
const searchList = Object.entries(searches)
|
|
.map(([key, val]) => ({ key, ...val }))
|
|
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
res.render('index', { searches: searchList, results: null, query: null, error: null });
|
|
});
|
|
|
|
app.get('/leads', (req, res) => {
|
|
const leads = store.getLeads();
|
|
const leadList = Object.entries(leads)
|
|
.map(([key, val]) => ({ key, ...val }))
|
|
.sort((a, b) => (b.savedAt || 0) - (a.savedAt || 0));
|
|
res.render('leads', { leads: leadList });
|
|
});
|
|
|
|
app.get('/history', (req, res) => {
|
|
const searches = store.getSearches();
|
|
const searchList = Object.entries(searches)
|
|
.map(([key, val]) => ({ key, ...val }))
|
|
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
res.render('history', { searches: searchList });
|
|
});
|
|
|
|
// --- API ---
|
|
app.post('/api/search', async (req, res) => {
|
|
const { keyword, city, state, maxResults } = req.body;
|
|
if (!keyword || !city) return res.status(400).json({ error: 'Keyword and city are required' });
|
|
|
|
const token = process.env.APIFY_API_TOKEN;
|
|
if (!token) return res.status(500).json({ error: 'APIFY_API_TOKEN not configured. Add it to .env file.' });
|
|
|
|
const searchQuery = `${keyword} in ${city}${state ? ', ' + state : ''}`;
|
|
const max = parseInt(maxResults) || 20;
|
|
|
|
try {
|
|
const client = new ApifyClient({ token });
|
|
const run = await client.actor('nwua9Gu5YrADL7ZDj').call({
|
|
searchStringsArray: [searchQuery],
|
|
maxCrawledPlacesPerSearch: max,
|
|
language: 'en',
|
|
deeperCityScrape: false,
|
|
});
|
|
|
|
const { items } = await client.dataset(run.defaultDatasetId).listItems();
|
|
|
|
const results = items.map(item => ({
|
|
placeId: item.placeId || item.cid || `place_${Date.now()}_${Math.random().toString(36).slice(2,8)}`,
|
|
title: item.title || '',
|
|
phone: item.phone || '',
|
|
website: item.website || '',
|
|
url: item.url || '',
|
|
address: item.address || item.street || '',
|
|
city: item.city || city,
|
|
state: item.state || state || '',
|
|
totalScore: item.totalScore || 0,
|
|
reviewsCount: item.reviewsCount || 0,
|
|
categoryName: item.categoryName || '',
|
|
email: extractEmail(item),
|
|
imageUrl: item.imageUrl || '',
|
|
openingHours: item.openingHours || [],
|
|
additionalInfo: item.additionalInfo || {},
|
|
isBookmarked: store.isLead(item.placeId || item.cid || ''),
|
|
}));
|
|
|
|
const key = `search:${Date.now()}`;
|
|
store.saveSearch(key, {
|
|
query: searchQuery,
|
|
keyword, city, state,
|
|
maxResults: max,
|
|
resultsCount: results.length,
|
|
timestamp: Date.now(),
|
|
results,
|
|
});
|
|
|
|
res.json({ success: true, key, results, query: searchQuery });
|
|
} catch (err) {
|
|
console.error('Apify error:', err);
|
|
res.status(500).json({ error: `Scraping failed: ${err.message}` });
|
|
}
|
|
});
|
|
|
|
app.get('/api/search/:key', (req, res) => {
|
|
const search = store.getSearch(req.params.key);
|
|
if (!search) return res.status(404).json({ error: 'Search not found' });
|
|
// Refresh bookmark status
|
|
if (search.results) {
|
|
search.results = search.results.map(r => ({
|
|
...r,
|
|
isBookmarked: store.isLead(r.placeId),
|
|
}));
|
|
}
|
|
res.json(search);
|
|
});
|
|
|
|
app.delete('/api/search/:key', (req, res) => {
|
|
store.deleteSearch(req.params.key);
|
|
res.json({ success: true });
|
|
});
|
|
|
|
app.post('/api/leads', (req, res) => {
|
|
const { place } = req.body;
|
|
if (!place || !place.placeId) return res.status(400).json({ error: 'Invalid place data' });
|
|
const key = `lead:${Date.now()}`;
|
|
store.saveLead(key, { ...place, savedAt: Date.now() });
|
|
res.json({ success: true, key });
|
|
});
|
|
|
|
app.delete('/api/leads/:placeId', (req, res) => {
|
|
const leads = store.getLeads();
|
|
const entry = Object.entries(leads).find(([, v]) => v.placeId === req.params.placeId);
|
|
if (entry) store.deleteLead(entry[0]);
|
|
res.json({ success: true });
|
|
});
|
|
|
|
app.get('/api/leads/check/:placeId', (req, res) => {
|
|
res.json({ isLead: store.isLead(req.params.placeId) });
|
|
});
|
|
|
|
function extractEmail(item) {
|
|
// Try to find email in various Apify output fields
|
|
if (item.email) return item.email;
|
|
if (item.emails && item.emails.length) return item.emails[0];
|
|
const text = JSON.stringify(item.additionalInfo || {});
|
|
const match = text.match(/[\w.-]+@[\w.-]+\.\w+/);
|
|
return match ? match[0] : '';
|
|
}
|
|
|
|
app.listen(PORT, () => console.log(`Lead Scraper running on http://localhost:${PORT}`));
|