@shaivpidadi/trends-js
Version:
Google Trends API for Node.js
336 lines (335 loc) • 13.3 kB
JavaScript
import { GoogleTrendsEndpoints } from '../types/enums';
import { request } from './request';
import { extractJsonFromResponse } from './format';
import { GOOGLE_TRENDS_MAPPER } from '../constants';
import { NetworkError, ParseError, UnknownError, } from '../errors/GoogleTrendsError';
export class GoogleTrendsApi {
/**
* Get autocomplete suggestions for a keyword
* @param keyword - The keyword to get suggestions for
* @param hl - Language code (default: 'en-US')
* @returns Promise with array of suggestion strings
*/
async autocomplete(keyword, hl = 'en-US') {
if (!keyword) {
return { data: [] };
}
const options = {
...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.autocomplete],
qs: {
hl,
tz: '240',
},
};
try {
const response = await request(`${options.url}/${encodeURIComponent(keyword)}`, options);
const text = await response.text();
// Remove the first 5 characters (JSONP wrapper) and parse
const data = JSON.parse(text.slice(5));
return { data: data.default.topics.map((topic) => topic.title) };
}
catch (error) {
if (error instanceof Error) {
return { error: new NetworkError(error.message) };
}
return { error: new UnknownError() };
}
}
/**
* Get daily trending topics
* @param options - Options for daily trends request
* @returns Promise with trending topics data
*/
async dailyTrends({ geo = 'US', lang = 'en' }) {
const defaultOptions = GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.dailyTrends];
const options = {
...defaultOptions,
body: new URLSearchParams({
'f.req': `[[["i0OFE","[null,null,\\"${geo}\\",0,\\"${lang}\\",24,1]",null,"generic"]]]`,
}).toString(),
contentType: 'form'
};
try {
const response = await request(options.url, options);
const text = await response.text();
const trendingTopics = extractJsonFromResponse(text);
if (!trendingTopics) {
return { error: new ParseError() };
}
return { data: trendingTopics };
}
catch (error) {
if (error instanceof Error) {
return { error: new NetworkError(error.message) };
}
return { error: new UnknownError() };
}
}
/**
* Get real-time trending topics
* @param options - Options for real-time trends request
* @returns Promise with trending topics data
*/
async realTimeTrends({ geo = 'US', trendingHours = 4 }) {
const defaultOptions = GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.dailyTrends];
const options = {
...defaultOptions,
body: new URLSearchParams({
'f.req': `[[["i0OFE","[null,null,\\"${geo}\\",0,\\"en\\",${trendingHours},1]",null,"generic"]]]`,
}).toString(),
contentType: 'form'
};
try {
const response = await request(options.url, options);
const text = await response.text();
const trendingTopics = extractJsonFromResponse(text);
if (!trendingTopics) {
return { error: new ParseError() };
}
return { data: trendingTopics };
}
catch (error) {
if (error instanceof Error) {
return { error: new NetworkError(error.message) };
}
return { error: new UnknownError() };
}
}
async explore({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
const options = {
...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.explore],
qs: {
hl,
tz: '240',
req: JSON.stringify({
comparisonItem: [
{
keyword,
geo,
time,
},
],
category,
property,
}),
},
contentType: 'form'
};
try {
const response = await request(options.url, options);
const text = await response.text();
// Check if response is HTML (error page)
if (text.includes('<html') || text.includes('<!DOCTYPE')) {
console.error('Explore request returned HTML instead of JSON');
return { widgets: [] };
}
// Try to parse as JSON
try {
// Remove the first 5 characters (JSONP wrapper) and parse
const data = JSON.parse(text.slice(5));
return data;
}
catch (parseError) {
console.error('Failed to parse explore response as JSON:', parseError instanceof Error ? parseError.message : 'Unknown parse error');
console.error('Response preview:', text.substring(0, 200));
return { widgets: [] };
}
}
catch (error) {
console.error('Explore request failed:', error);
return { widgets: [] };
}
}
//
async interestByRegion({ keyword, startTime = new Date('2004-01-01'), endTime = new Date(), geo = 'US', resolution = 'REGION', hl = 'en-US', timezone = new Date().getTimezoneOffset(), category = 0 }) {
const formatDate = (date) => {
return date.toISOString().split('T')[0];
};
const formatTrendsDate = (date) => {
const pad = (n) => n.toString().padStart(2, '0');
const yyyy = date.getFullYear();
const mm = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
const hh = pad(date.getHours());
const min = pad(date.getMinutes());
const ss = pad(date.getSeconds());
return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
};
const getDateRangeParam = (date) => {
const yesterday = new Date(date);
yesterday.setDate(date.getDate() - 1);
const formattedStart = formatTrendsDate(yesterday);
const formattedEnd = formatTrendsDate(date);
return `${formattedStart} ${formattedEnd}`;
};
const exploreResponse = await this.explore({
keyword: Array.isArray(keyword) ? keyword[0] : keyword,
geo: Array.isArray(geo) ? geo[0] : geo,
time: `${getDateRangeParam(startTime)} ${getDateRangeParam(endTime)}`,
category,
hl
});
const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
if (!widget) {
return { default: { geoMapData: [] } };
}
const options = {
...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.interestByRegion],
qs: {
hl,
tz: timezone.toString(),
req: JSON.stringify({
geo: {
country: Array.isArray(geo) ? geo[0] : geo
},
comparisonItem: [{
time: `${formatDate(startTime)} ${formatDate(endTime)}`,
complexKeywordsRestriction: {
keyword: [{
type: 'BROAD', //'ENTITY',
value: Array.isArray(keyword) ? keyword[0] : keyword
}]
}
}],
resolution,
locale: hl,
requestOptions: {
property: '',
backend: 'CM', //'IZG',
category
},
userConfig: {
userType: 'USER_TYPE_LEGIT_USER'
}
}),
token: widget.token
}
};
try {
const response = await request(options.url, options);
const text = await response.text();
// Remove the first 5 characters (JSONP wrapper) and parse
const data = JSON.parse(text.slice(5));
return data;
}
catch (error) {
return { default: { geoMapData: [] } };
}
}
async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
try {
// Validate keyword
if (!keyword || keyword.trim() === '') {
return { error: new ParseError() };
}
const autocompleteResult = await this.autocomplete(keyword, hl);
if (autocompleteResult.error) {
return { error: autocompleteResult.error };
}
const relatedTopics = autocompleteResult.data?.slice(0, 10).map((suggestion, index) => ({
topic: {
mid: `/m/${index}`,
title: suggestion,
type: 'Topic'
},
value: 100 - index * 10,
formattedValue: (100 - index * 10).toString(),
hasData: true,
link: `/trends/explore?q=${encodeURIComponent(suggestion)}&date=${time}&geo=${geo}`
})) || [];
return {
data: {
default: {
rankedList: [{
rankedKeyword: relatedTopics
}]
}
}
};
}
catch (error) {
if (error instanceof Error) {
return { error: new NetworkError(error.message) };
}
return { error: new UnknownError() };
}
}
async relatedQueries({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
try {
// Validate keyword
if (!keyword || keyword.trim() === '') {
return { error: new ParseError() };
}
const autocompleteResult = await this.autocomplete(keyword, hl);
if (autocompleteResult.error) {
return { error: autocompleteResult.error };
}
const relatedQueries = autocompleteResult.data?.slice(0, 10).map((suggestion, index) => ({
query: suggestion,
value: 100 - index * 10,
formattedValue: (100 - index * 10).toString(),
hasData: true,
link: `/trends/explore?q=${encodeURIComponent(suggestion)}&date=${time}&geo=${geo}`
})) || [];
return {
data: {
default: {
rankedList: [{
rankedKeyword: relatedQueries
}]
}
}
};
}
catch (error) {
if (error instanceof Error) {
return { error: new NetworkError(error.message) };
}
return { error: new UnknownError() };
}
}
async relatedData({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
try {
// Validate keyword
if (!keyword || keyword.trim() === '') {
return { error: new ParseError() };
}
const autocompleteResult = await this.autocomplete(keyword, hl);
if (autocompleteResult.error) {
return { error: autocompleteResult.error };
}
const suggestions = autocompleteResult.data?.slice(0, 10) || [];
const topics = suggestions.map((suggestion, index) => ({
topic: {
mid: `/m/${index}`,
title: suggestion,
type: 'Topic'
},
value: 100 - index * 10,
formattedValue: (100 - index * 10).toString(),
hasData: true,
link: `/trends/explore?q=${encodeURIComponent(suggestion)}&date=${time}&geo=${geo}`
}));
const queries = suggestions.map((suggestion, index) => ({
query: suggestion,
value: 100 - index * 10,
formattedValue: (100 - index * 10).toString(),
hasData: true,
link: `/trends/explore?q=${encodeURIComponent(suggestion)}&date=${time}&geo=${geo}`
}));
return {
data: {
topics,
queries
}
};
}
catch (error) {
if (error instanceof Error) {
return { error: new NetworkError(error.message) };
}
return { error: new UnknownError() };
}
}
}
export default new GoogleTrendsApi();