Initial commit: Lead Scraper - Google Maps
This commit is contained in:
151
server.js
Normal file
151
server.js
Normal file
@@ -0,0 +1,151 @@
|
||||
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}`));
|
||||
Reference in New Issue
Block a user