@shaivpidadi/trends-js
Version:
Google Trends API for Node.js
166 lines (165 loc) • 7.27 kB
JavaScript
import https from 'https';
import { URL } from 'url';
export class SessionManager {
constructor(config = {}) {
this.cookies = {};
this.lastRefresh = 0;
this.userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15'
];
this.config = {
autoRefresh: config.autoRefresh ?? true,
maxRetries: config.maxRetries ?? 3,
baseDelayMs: config.baseDelayMs ?? 750,
cookieRefreshInterval: config.cookieRefreshInterval ?? 30 * 60 * 1000 // 30 min
};
if (config.initialCookies) {
if (typeof config.initialCookies === 'string') {
this.cookies = this.parseCookieString(config.initialCookies);
}
else {
this.cookies = config.initialCookies;
}
}
}
async initialize() {
// If we already have essential cookies (like NID) provided manually,
// we can skip the initial fetch if desired, or verify them.
// For now, we'll only fetch if we don't have them or if autoRefresh implies we should.
if (Object.keys(this.cookies).length === 0) {
await this.refreshSession();
}
}
parseCookieString(cookieStr) {
const cookies = {};
if (!cookieStr)
return cookies;
cookieStr.split(';').forEach(pair => {
const [key, value] = pair.split('=');
if (key && value) {
cookies[key.trim()] = value.trim();
}
});
return cookies;
}
async refreshSession() {
try {
// Step 1: Hit main trends page to get initial cookies
const mainCookies = await this.fetchCookies('https://trends.google.com/trends/');
// Step 2: Hit explore page to get additional auth cookies
const exploreCookies = await this.fetchCookies('https://trends.google.com/trends/explore?geo=US&q=test', mainCookies);
this.cookies = { ...this.cookies, ...mainCookies, ...exploreCookies };
this.lastRefresh = Date.now();
// console.log('[SessionManager] Session refreshed. Cookies:', Object.keys(this.cookies));
}
catch (error) {
console.error('[SessionManager] Failed to refresh session:', error);
// Don't throw if we have fallback cookies, otherwise propagate
if (Object.keys(this.cookies).length === 0) {
throw error;
}
}
}
fetchCookies(url, existingCookies) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
path: parsedUrl.pathname + parsedUrl.search,
method: 'GET',
headers: {
'User-Agent': this.getRandomUserAgent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
...(existingCookies ? { Cookie: this.serializeCookies(existingCookies) } : {})
}
};
const req = https.request(options, (res) => {
const cookies = this.parseCookies(res.headers['set-cookie'] || []);
// Consume response body to free up memory/socket
res.on('data', () => { });
res.on('end', () => resolve(cookies));
});
req.on('error', reject);
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
parseCookies(setCookieHeaders) {
const cookies = {};
for (const header of setCookieHeaders) {
const [cookiePart] = header.split(';');
const [key, value] = cookiePart.split('=');
const normalizedKey = key.replace(/__Secure-/g, '__Secure_');
// We accept more than just specific keys to be robust, but we can filter if needed.
// Keeping the filter for now to avoid junk.
if (['NID', 'AEC', '__Secure_BUCKET', 'OTZ', '_ga', '_gid', '__utma', '__utmz'].includes(normalizedKey)) {
cookies[normalizedKey] = value;
}
}
return cookies;
}
serializeCookies(cookies) {
return Object.entries(cookies)
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => {
// Convert back to __Secure- format for sending
const key = k.replace(/__Secure_/g, '__Secure-');
return `${key}=${v}`;
})
.join('; ');
}
getRandomUserAgent() {
return this.userAgents[Math.floor(Math.random() * this.userAgents.length)];
}
async getCookieHeader() {
// Auto-refresh if needed
if (this.config.autoRefresh) {
const timeSinceRefresh = Date.now() - this.lastRefresh;
if (this.lastRefresh > 0 && timeSinceRefresh > this.config.cookieRefreshInterval) {
// console.log('[SessionManager] Auto-refreshing session...');
await this.refreshSession(); // Background refresh? No, await to ensure valid session.
}
}
return this.serializeCookies(this.cookies);
}
getRequestHeaders() {
return {
'User-Agent': this.getRandomUserAgent(),
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': 'https://trends.google.com/',
'Origin': 'https://trends.google.com',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
};
}
// Manual cookie update from 429 responses
updateFromSetCookie(setCookieHeaders) {
const newCookies = this.parseCookies(setCookieHeaders);
this.cookies = { ...this.cookies, ...newCookies };
// console.log('[SessionManager] Updated cookies from response');
}
// For testing/debugging
getCookies() {
return { ...this.cookies };
}
}