@shaivpidadi/trends-js 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -147,14 +147,15 @@ Get interest data by region:
147
147
 
148
148
  ```typescript
149
149
  const result = await GoogleTrendsApi.interestByRegion({
150
- keyword: 'Stock Market', // Required - string or string[]
150
+ keyword: 'Stock Market', // Required - string
151
151
  startTime: new Date('2024-01-01'), // Optional - defaults to 2004-01-01
152
152
  endTime: new Date(), // Optional - defaults to current date
153
- geo: 'US', // Optional - string or string[] - defaults to 'US'
153
+ geo: 'US', // Optional - string - defaults to 'US'
154
154
  resolution: 'REGION', // Optional - 'COUNTRY' | 'REGION' | 'CITY' | 'DMA'
155
155
  hl: 'en-US', // Optional - defaults to 'en-US'
156
156
  timezone: -240, // Optional - defaults to local timezone
157
157
  category: 0, // Optional - defaults to 0
158
+ enableBackoff: true // Optional - defaults to false
158
159
  });
159
160
 
160
161
  // Result structure:
@@ -185,6 +186,7 @@ const result = await GoogleTrendsApi.interestByRegion({
185
186
  startTime: new Date('2024-01-01'),
186
187
  endTime: new Date(),
187
188
  resolution: 'CITY',
189
+ enableBackoff: true
188
190
  });
189
191
  ```
190
192
 
@@ -324,6 +326,7 @@ interface InterestByRegionOptions {
324
326
  hl?: string; // Optional - language code
325
327
  timezone?: number; // Optional - timezone offset
326
328
  category?: number; // Optional - category number
329
+ enableBackoff?: boolean // Optional
327
330
  }
328
331
  ```
329
332
 
@@ -1,2 +1,4 @@
1
1
  import { DailyTrendingTopics } from '../types/index.js';
2
+ export declare const formatTrendsDate: (date: Date) => string;
3
+ export declare const formatDate: (date: Date) => string;
2
4
  export declare const extractJsonFromResponse: (text: string) => DailyTrendingTopics | null;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractJsonFromResponse = void 0;
3
+ exports.extractJsonFromResponse = exports.formatDate = exports.formatTrendsDate = void 0;
4
4
  const GoogleTrendsError_js_1 = require("../errors/GoogleTrendsError.js");
5
5
  // For future reference and update: from google trends page rpc call response,
6
6
  // 0 "twitter down" The main trending search term.
@@ -16,6 +16,21 @@ const GoogleTrendsError_js_1 = require("../errors/GoogleTrendsError.js");
16
16
  // 10 [11] Unclear, possibly a category identifier.
17
17
  // 11 [[3606769742, "en", "US"], [3596035008, "en", "US"]] User demographics or trending sources, with numerical IDs, language ("en" for English), and country ("US" for United States).
18
18
  // 12 "twitter down" The original trending keyword (sometimes a duplicate of index 0).
19
+ const formatTrendsDate = (date) => {
20
+ const pad = (n) => n.toString().padStart(2, '0');
21
+ const yyyy = date.getFullYear();
22
+ const mm = pad(date.getMonth() + 1);
23
+ const dd = pad(date.getDate());
24
+ const hh = pad(date.getHours());
25
+ const min = pad(date.getMinutes());
26
+ const ss = pad(date.getSeconds());
27
+ return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
28
+ };
29
+ exports.formatTrendsDate = formatTrendsDate;
30
+ const formatDate = (date) => {
31
+ return date.toISOString().split('T')[0];
32
+ };
33
+ exports.formatDate = formatDate;
19
34
  const extractJsonFromResponse = (text) => {
20
35
  const cleanedText = text.replace(/^\)\]\}'/, '').trim();
21
36
  try {
@@ -19,13 +19,11 @@ export declare class GoogleTrendsApi {
19
19
  * @returns Promise with trending topics data
20
20
  */
21
21
  realTimeTrends({ geo, trendingHours }: RealTimeTrendsOptions): Promise<GoogleTrendsResponse<DailyTrendingTopics>>;
22
- explore({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<ExploreResponse | {
22
+ explore({ keyword, geo, time, category, property, hl, enableBackoff, }: ExploreOptions): Promise<ExploreResponse | {
23
23
  error: GoogleTrendsError;
24
24
  }>;
25
- interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: InterestByRegionOptions): Promise<InterestByRegionResponse | {
26
- error: GoogleTrendsError;
27
- }>;
28
- relatedTopics({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedTopicsResponse>>;
25
+ interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category, enableBackoff }: InterestByRegionOptions): Promise<GoogleTrendsResponse<InterestByRegionResponse>>;
26
+ relatedTopics({ keyword, geo, time, category, property, hl, enableBackoff, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedTopicsResponse>>;
29
27
  relatedQueries({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedQueriesResponse>>;
30
28
  relatedData({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedData>>;
31
29
  }
@@ -98,7 +98,7 @@ class GoogleTrendsApi {
98
98
  return { error: new GoogleTrendsError_js_1.UnknownError() };
99
99
  }
100
100
  }
101
- async explore({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
101
+ async explore({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', enableBackoff = false, }) {
102
102
  const options = {
103
103
  ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.explore],
104
104
  qs: {
@@ -116,10 +116,10 @@ class GoogleTrendsApi {
116
116
  property,
117
117
  }),
118
118
  },
119
- contentType: 'form'
119
+ // contentType: 'form' as const
120
120
  };
121
121
  try {
122
- const response = await (0, request_js_1.request)(options.url, options);
122
+ const response = await (0, request_js_1.request)(options.url, options, enableBackoff);
123
123
  const text = await response.text();
124
124
  // Check if response is HTML (error page)
125
125
  if (text.includes('<html') || text.includes('<!DOCTYPE')) {
@@ -134,6 +134,9 @@ class GoogleTrendsApi {
134
134
  const widgets = data[0] || [];
135
135
  return { widgets };
136
136
  }
137
+ if (data && typeof data === 'object' && Array.isArray(data.widgets)) {
138
+ return { widgets: data.widgets };
139
+ }
137
140
  return { widgets: [] };
138
141
  }
139
142
  catch (parseError) {
@@ -151,92 +154,60 @@ class GoogleTrendsApi {
151
154
  }
152
155
  }
153
156
  //
154
- 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 }) {
157
+ 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, enableBackoff = false }) {
155
158
  const keywordValue = Array.isArray(keyword) ? keyword[0] : keyword;
159
+ const geoValue = Array.isArray(geo) ? geo[0] : geo;
156
160
  if (!keywordValue || keywordValue.trim() === '') {
157
161
  return { error: new GoogleTrendsError_js_1.InvalidRequestError('Keyword is required') };
158
162
  }
159
- const formatDate = (date) => {
160
- return date.toISOString().split('T')[0];
161
- };
162
- const formatTrendsDate = (date) => {
163
- const pad = (n) => n.toString().padStart(2, '0');
164
- const yyyy = date.getFullYear();
165
- const mm = pad(date.getMonth() + 1);
166
- const dd = pad(date.getDate());
167
- const hh = pad(date.getHours());
168
- const min = pad(date.getMinutes());
169
- const ss = pad(date.getSeconds());
170
- return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
171
- };
172
- const getDateRangeParam = (date) => {
173
- const yesterday = new Date(date);
174
- yesterday.setDate(date.getDate() - 1);
175
- const formattedStart = formatTrendsDate(yesterday);
176
- const formattedEnd = formatTrendsDate(date);
177
- return `${formattedStart} ${formattedEnd}`;
178
- };
179
- const exploreResponse = await this.explore({
180
- keyword: Array.isArray(keyword) ? keyword[0] : keyword,
181
- geo: Array.isArray(geo) ? geo[0] : geo,
182
- time: `${getDateRangeParam(startTime)} ${getDateRangeParam(endTime)}`,
183
- category,
184
- hl
185
- });
186
- if ('error' in exploreResponse) {
187
- return { error: exploreResponse.error };
188
- }
189
- const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
190
- if (!widget) {
191
- return { error: new GoogleTrendsError_js_1.ParseError('No GEO_MAP widget found in explore response') };
163
+ if (!geoValue || geoValue.trim() === '') {
164
+ return { error: new GoogleTrendsError_js_1.InvalidRequestError('Geo is required') };
192
165
  }
193
- const options = {
194
- ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.interestByRegion],
195
- qs: {
166
+ try {
167
+ const exploreResponse = await this.explore({
168
+ keyword: keywordValue,
169
+ geo: geoValue,
170
+ time: `${(0, format_js_1.formatDate)(startTime)} ${(0, format_js_1.formatDate)(endTime)}`,
171
+ category,
196
172
  hl,
197
- tz: timezone.toString(),
198
- req: JSON.stringify({
199
- geo: {
200
- country: Array.isArray(geo) ? geo[0] : geo
201
- },
202
- comparisonItem: [{
203
- time: `${formatDate(startTime)} ${formatDate(endTime)}`,
204
- complexKeywordsRestriction: {
205
- keyword: [{
206
- type: 'BROAD', //'ENTITY',
207
- value: Array.isArray(keyword) ? keyword[0] : keyword
208
- }]
209
- }
210
- }],
211
- resolution,
212
- locale: hl,
213
- requestOptions: {
214
- property: '',
215
- backend: 'CM', //'IZG',
216
- category
217
- },
218
- userConfig: {
219
- userType: 'USER_TYPE_LEGIT_USER'
220
- }
221
- }),
222
- token: widget.token
173
+ enableBackoff
174
+ });
175
+ if ('error' in exploreResponse) {
176
+ return { error: exploreResponse.error };
223
177
  }
224
- };
225
- try {
226
- const response = await (0, request_js_1.request)(options.url, options);
178
+ const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
179
+ if (!widget) {
180
+ return { error: new GoogleTrendsError_js_1.ParseError('No GEO_MAP widget found in explore response') };
181
+ }
182
+ const requestFromWidget = {
183
+ ...widget.request, resolution
184
+ };
185
+ const options = {
186
+ ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.interestByRegion],
187
+ qs: {
188
+ hl,
189
+ tz: timezone.toString(),
190
+ req: JSON.stringify(requestFromWidget),
191
+ token: widget.token
192
+ }
193
+ };
194
+ const response = await (0, request_js_1.request)(options.url, options, enableBackoff);
227
195
  const text = await response.text();
228
- // Remove the first 5 characters (JSONP wrapper) and parse
196
+ if (text.includes('<!DOCTYPE') || text.includes('<html')) {
197
+ return { error: new GoogleTrendsError_js_1.ParseError('Interest by region request failed') };
198
+ }
199
+ // Handle JSONP wrapper (usually starts with )]}' or similar)
229
200
  const data = JSON.parse(text.slice(5));
230
- return data;
201
+ return { data: data.default.geoMapData };
231
202
  }
232
203
  catch (error) {
233
204
  if (error instanceof Error) {
234
- return { error: new GoogleTrendsError_js_1.ParseError(`Failed to parse interest by region response: ${error.message}`) };
205
+ return { error: new GoogleTrendsError_js_1.NetworkError(`Interest by region request failed: ${error.message}`) };
235
206
  }
236
- return { error: new GoogleTrendsError_js_1.ParseError('Failed to parse interest by region response') };
207
+ return { error: new GoogleTrendsError_js_1.UnknownError('Interest by region request failed') };
237
208
  }
238
209
  }
239
- async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
210
+ async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', enableBackoff = false, }) {
240
211
  try {
241
212
  // Validate keyword
242
213
  if (!keyword || keyword.trim() === '') {
@@ -249,7 +220,8 @@ class GoogleTrendsApi {
249
220
  time,
250
221
  category,
251
222
  property,
252
- hl
223
+ hl,
224
+ enableBackoff
253
225
  });
254
226
  if ('error' in exploreResponse) {
255
227
  return { error: exploreResponse.error };
@@ -300,7 +272,7 @@ class GoogleTrendsApi {
300
272
  ...(relatedTopicsWidget.token && { token: relatedTopicsWidget.token })
301
273
  }
302
274
  };
303
- const response = await (0, request_js_1.request)(options.url, options);
275
+ const response = await (0, request_js_1.request)(options.url, options, enableBackoff);
304
276
  const text = await response.text();
305
277
  // Parse the response
306
278
  try {
@@ -4,6 +4,6 @@ export declare const request: (url: string, options: {
4
4
  body?: string | Record<string, any>;
5
5
  headers?: Record<string, string>;
6
6
  contentType?: "json" | "form";
7
- }) => Promise<{
7
+ }, enableBackoff?: boolean) => Promise<{
8
8
  text: () => Promise<string>;
9
9
  }>;
@@ -7,12 +7,40 @@ exports.request = void 0;
7
7
  const https_1 = __importDefault(require("https"));
8
8
  const querystring_1 = __importDefault(require("querystring"));
9
9
  let cookieVal;
10
- function rereq(options, body) {
10
+ const MAX_RETRIES = 3;
11
+ const BASE_DELAY_MS = 750;
12
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ async function runRequest(options, body, attempt) {
11
14
  return new Promise((resolve, reject) => {
12
15
  const req = https_1.default.request(options, (res) => {
13
16
  let chunk = '';
14
17
  res.on('data', (data) => { chunk += data; });
15
- res.on('end', () => resolve(chunk));
18
+ res.on('end', async () => {
19
+ const hasSetCookie = !!res.headers['set-cookie']?.length;
20
+ const isRateLimited = res.statusCode === 429 ||
21
+ chunk.includes('Error 429') ||
22
+ chunk.includes('Too Many Requests');
23
+ if (isRateLimited) {
24
+ if (hasSetCookie) {
25
+ cookieVal = res.headers['set-cookie'][0].split(';')[0];
26
+ options.headers['cookie'] = cookieVal;
27
+ }
28
+ if (attempt < MAX_RETRIES) {
29
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.floor(Math.random() * 250);
30
+ await sleep(delay);
31
+ try {
32
+ const retryResponse = await runRequest(options, body, attempt + 1);
33
+ resolve(retryResponse);
34
+ return;
35
+ }
36
+ catch (err) {
37
+ reject(err);
38
+ return;
39
+ }
40
+ }
41
+ }
42
+ resolve(chunk);
43
+ });
16
44
  });
17
45
  req.on('error', reject);
18
46
  if (body)
@@ -20,7 +48,7 @@ function rereq(options, body) {
20
48
  req.end();
21
49
  });
22
50
  }
23
- const request = async (url, options) => {
51
+ const request = async (url, options, enableBackoff = false) => {
24
52
  const parsedUrl = new URL(url);
25
53
  const method = options.method || 'POST';
26
54
  // Prepare body
@@ -49,32 +77,9 @@ const request = async (url, options) => {
49
77
  ...(cookieVal ? { cookie: cookieVal } : {})
50
78
  }
51
79
  };
52
- const response = await new Promise((resolve, reject) => {
53
- const req = https_1.default.request(requestOptions, (res) => {
54
- let chunk = '';
55
- res.on('data', (data) => { chunk += data; });
56
- res.on('end', async () => {
57
- if (res.statusCode === 429 && res.headers['set-cookie']) {
58
- cookieVal = res.headers['set-cookie'][0].split(';')[0];
59
- requestOptions.headers['cookie'] = cookieVal;
60
- try {
61
- const retryResponse = await rereq(requestOptions, bodyString);
62
- resolve(retryResponse);
63
- }
64
- catch (err) {
65
- reject(err);
66
- }
67
- }
68
- else {
69
- resolve(chunk);
70
- }
71
- });
72
- });
73
- req.on('error', reject);
74
- if (bodyString)
75
- req.write(bodyString);
76
- req.end();
77
- });
80
+ const response = enableBackoff
81
+ ? await runRequest(requestOptions, bodyString, 0)
82
+ : await runRequest(requestOptions, bodyString, MAX_RETRIES);
78
83
  return {
79
84
  text: () => Promise.resolve(response)
80
85
  };
@@ -2,13 +2,11 @@ import { GoogleTrendsApi } from './helpers/googleTrendsAPI.js';
2
2
  export declare const dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
3
3
  export declare const realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
4
4
  export declare const autocomplete: (keyword: string, hl?: string) => Promise<import("./types/index.js").GoogleTrendsResponse<string[]>>;
5
- export declare const explore: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
5
+ export declare const explore: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
6
6
  error: import("./types/index.js").GoogleTrendsError;
7
7
  }>;
8
- export declare const interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").InterestByRegionResponse | {
9
- error: import("./types/index.js").GoogleTrendsError;
10
- }>;
11
- export declare const relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
8
+ export declare const interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category, enableBackoff }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").InterestByRegionResponse>>;
9
+ export declare const relatedTopics: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
12
10
  export declare const relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedQueriesResponse>>;
13
11
  export declare const relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedData>>;
14
12
  export { GoogleTrendsApi };
@@ -16,13 +14,11 @@ declare const _default: {
16
14
  dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
17
15
  realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
18
16
  autocomplete: (keyword: string, hl?: string) => Promise<import("./types/index.js").GoogleTrendsResponse<string[]>>;
19
- explore: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
20
- error: import("./types/index.js").GoogleTrendsError;
21
- }>;
22
- interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").InterestByRegionResponse | {
17
+ explore: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
23
18
  error: import("./types/index.js").GoogleTrendsError;
24
19
  }>;
25
- relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
20
+ interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category, enableBackoff }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").InterestByRegionResponse>>;
21
+ relatedTopics: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
26
22
  relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedQueriesResponse>>;
27
23
  relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedData>>;
28
24
  };
@@ -0,0 +1,4 @@
1
+ declare const formatDate: (date: Date) => string;
2
+ declare const formatTrendsDate: (date: Date) => string;
3
+ declare const getDateRangeParam: (date: Date) => string;
4
+ export { formatDate, formatTrendsDate, getDateRangeParam };
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDateRangeParam = exports.formatTrendsDate = exports.formatDate = void 0;
4
+ const formatDate = (date) => {
5
+ return date.toISOString().split('T')[0];
6
+ };
7
+ exports.formatDate = formatDate;
8
+ const formatTrendsDate = (date) => {
9
+ const pad = (n) => n.toString().padStart(2, '0');
10
+ const yyyy = date.getFullYear();
11
+ const mm = pad(date.getMonth() + 1);
12
+ const dd = pad(date.getDate());
13
+ const hh = pad(date.getHours());
14
+ const min = pad(date.getMinutes());
15
+ const ss = pad(date.getSeconds());
16
+ return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
17
+ };
18
+ exports.formatTrendsDate = formatTrendsDate;
19
+ const getDateRangeParam = (date) => {
20
+ const yesterday = new Date(date);
21
+ yesterday.setDate(date.getDate() - 1);
22
+ const formattedStart = formatTrendsDate(yesterday);
23
+ const formattedEnd = formatTrendsDate(date);
24
+ return `${formattedStart} ${formattedEnd}`;
25
+ };
26
+ exports.getDateRangeParam = getDateRangeParam;
@@ -1,2 +1,4 @@
1
1
  import { DailyTrendingTopics } from '../types/index.js';
2
+ export declare const formatTrendsDate: (date: Date) => string;
3
+ export declare const formatDate: (date: Date) => string;
2
4
  export declare const extractJsonFromResponse: (text: string) => DailyTrendingTopics | null;
@@ -13,6 +13,19 @@ import { ParseError } from '../errors/GoogleTrendsError.js';
13
13
  // 10 [11] Unclear, possibly a category identifier.
14
14
  // 11 [[3606769742, "en", "US"], [3596035008, "en", "US"]] User demographics or trending sources, with numerical IDs, language ("en" for English), and country ("US" for United States).
15
15
  // 12 "twitter down" The original trending keyword (sometimes a duplicate of index 0).
16
+ export const formatTrendsDate = (date) => {
17
+ const pad = (n) => n.toString().padStart(2, '0');
18
+ const yyyy = date.getFullYear();
19
+ const mm = pad(date.getMonth() + 1);
20
+ const dd = pad(date.getDate());
21
+ const hh = pad(date.getHours());
22
+ const min = pad(date.getMinutes());
23
+ const ss = pad(date.getSeconds());
24
+ return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
25
+ };
26
+ export const formatDate = (date) => {
27
+ return date.toISOString().split('T')[0];
28
+ };
16
29
  export const extractJsonFromResponse = (text) => {
17
30
  const cleanedText = text.replace(/^\)\]\}'/, '').trim();
18
31
  try {
@@ -19,13 +19,11 @@ export declare class GoogleTrendsApi {
19
19
  * @returns Promise with trending topics data
20
20
  */
21
21
  realTimeTrends({ geo, trendingHours }: RealTimeTrendsOptions): Promise<GoogleTrendsResponse<DailyTrendingTopics>>;
22
- explore({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<ExploreResponse | {
22
+ explore({ keyword, geo, time, category, property, hl, enableBackoff, }: ExploreOptions): Promise<ExploreResponse | {
23
23
  error: GoogleTrendsError;
24
24
  }>;
25
- interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: InterestByRegionOptions): Promise<InterestByRegionResponse | {
26
- error: GoogleTrendsError;
27
- }>;
28
- relatedTopics({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedTopicsResponse>>;
25
+ interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category, enableBackoff }: InterestByRegionOptions): Promise<GoogleTrendsResponse<InterestByRegionResponse>>;
26
+ relatedTopics({ keyword, geo, time, category, property, hl, enableBackoff, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedTopicsResponse>>;
29
27
  relatedQueries({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedQueriesResponse>>;
30
28
  relatedData({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedData>>;
31
29
  }
@@ -1,6 +1,6 @@
1
1
  import { GoogleTrendsEndpoints } from '../types/enums.js';
2
2
  import { request } from './request.js';
3
- import { extractJsonFromResponse } from './format.js';
3
+ import { extractJsonFromResponse, formatDate } from './format.js';
4
4
  import { GOOGLE_TRENDS_MAPPER } from '../constants.js';
5
5
  import { InvalidRequestError, NetworkError, ParseError, UnknownError, } from '../errors/GoogleTrendsError.js';
6
6
  export class GoogleTrendsApi {
@@ -95,7 +95,7 @@ export class GoogleTrendsApi {
95
95
  return { error: new UnknownError() };
96
96
  }
97
97
  }
98
- async explore({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
98
+ async explore({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', enableBackoff = false, }) {
99
99
  const options = {
100
100
  ...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.explore],
101
101
  qs: {
@@ -113,10 +113,10 @@ export class GoogleTrendsApi {
113
113
  property,
114
114
  }),
115
115
  },
116
- contentType: 'form'
116
+ // contentType: 'form' as const
117
117
  };
118
118
  try {
119
- const response = await request(options.url, options);
119
+ const response = await request(options.url, options, enableBackoff);
120
120
  const text = await response.text();
121
121
  // Check if response is HTML (error page)
122
122
  if (text.includes('<html') || text.includes('<!DOCTYPE')) {
@@ -131,6 +131,9 @@ export class GoogleTrendsApi {
131
131
  const widgets = data[0] || [];
132
132
  return { widgets };
133
133
  }
134
+ if (data && typeof data === 'object' && Array.isArray(data.widgets)) {
135
+ return { widgets: data.widgets };
136
+ }
134
137
  return { widgets: [] };
135
138
  }
136
139
  catch (parseError) {
@@ -148,92 +151,60 @@ export class GoogleTrendsApi {
148
151
  }
149
152
  }
150
153
  //
151
- 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 }) {
154
+ 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, enableBackoff = false }) {
152
155
  const keywordValue = Array.isArray(keyword) ? keyword[0] : keyword;
156
+ const geoValue = Array.isArray(geo) ? geo[0] : geo;
153
157
  if (!keywordValue || keywordValue.trim() === '') {
154
158
  return { error: new InvalidRequestError('Keyword is required') };
155
159
  }
156
- const formatDate = (date) => {
157
- return date.toISOString().split('T')[0];
158
- };
159
- const formatTrendsDate = (date) => {
160
- const pad = (n) => n.toString().padStart(2, '0');
161
- const yyyy = date.getFullYear();
162
- const mm = pad(date.getMonth() + 1);
163
- const dd = pad(date.getDate());
164
- const hh = pad(date.getHours());
165
- const min = pad(date.getMinutes());
166
- const ss = pad(date.getSeconds());
167
- return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
168
- };
169
- const getDateRangeParam = (date) => {
170
- const yesterday = new Date(date);
171
- yesterday.setDate(date.getDate() - 1);
172
- const formattedStart = formatTrendsDate(yesterday);
173
- const formattedEnd = formatTrendsDate(date);
174
- return `${formattedStart} ${formattedEnd}`;
175
- };
176
- const exploreResponse = await this.explore({
177
- keyword: Array.isArray(keyword) ? keyword[0] : keyword,
178
- geo: Array.isArray(geo) ? geo[0] : geo,
179
- time: `${getDateRangeParam(startTime)} ${getDateRangeParam(endTime)}`,
180
- category,
181
- hl
182
- });
183
- if ('error' in exploreResponse) {
184
- return { error: exploreResponse.error };
185
- }
186
- const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
187
- if (!widget) {
188
- return { error: new ParseError('No GEO_MAP widget found in explore response') };
160
+ if (!geoValue || geoValue.trim() === '') {
161
+ return { error: new InvalidRequestError('Geo is required') };
189
162
  }
190
- const options = {
191
- ...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.interestByRegion],
192
- qs: {
163
+ try {
164
+ const exploreResponse = await this.explore({
165
+ keyword: keywordValue,
166
+ geo: geoValue,
167
+ time: `${formatDate(startTime)} ${formatDate(endTime)}`,
168
+ category,
193
169
  hl,
194
- tz: timezone.toString(),
195
- req: JSON.stringify({
196
- geo: {
197
- country: Array.isArray(geo) ? geo[0] : geo
198
- },
199
- comparisonItem: [{
200
- time: `${formatDate(startTime)} ${formatDate(endTime)}`,
201
- complexKeywordsRestriction: {
202
- keyword: [{
203
- type: 'BROAD', //'ENTITY',
204
- value: Array.isArray(keyword) ? keyword[0] : keyword
205
- }]
206
- }
207
- }],
208
- resolution,
209
- locale: hl,
210
- requestOptions: {
211
- property: '',
212
- backend: 'CM', //'IZG',
213
- category
214
- },
215
- userConfig: {
216
- userType: 'USER_TYPE_LEGIT_USER'
217
- }
218
- }),
219
- token: widget.token
170
+ enableBackoff
171
+ });
172
+ if ('error' in exploreResponse) {
173
+ return { error: exploreResponse.error };
220
174
  }
221
- };
222
- try {
223
- const response = await request(options.url, options);
175
+ const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
176
+ if (!widget) {
177
+ return { error: new ParseError('No GEO_MAP widget found in explore response') };
178
+ }
179
+ const requestFromWidget = {
180
+ ...widget.request, resolution
181
+ };
182
+ const options = {
183
+ ...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.interestByRegion],
184
+ qs: {
185
+ hl,
186
+ tz: timezone.toString(),
187
+ req: JSON.stringify(requestFromWidget),
188
+ token: widget.token
189
+ }
190
+ };
191
+ const response = await request(options.url, options, enableBackoff);
224
192
  const text = await response.text();
225
- // Remove the first 5 characters (JSONP wrapper) and parse
193
+ if (text.includes('<!DOCTYPE') || text.includes('<html')) {
194
+ return { error: new ParseError('Interest by region request failed') };
195
+ }
196
+ // Handle JSONP wrapper (usually starts with )]}' or similar)
226
197
  const data = JSON.parse(text.slice(5));
227
- return data;
198
+ return { data: data.default.geoMapData };
228
199
  }
229
200
  catch (error) {
230
201
  if (error instanceof Error) {
231
- return { error: new ParseError(`Failed to parse interest by region response: ${error.message}`) };
202
+ return { error: new NetworkError(`Interest by region request failed: ${error.message}`) };
232
203
  }
233
- return { error: new ParseError('Failed to parse interest by region response') };
204
+ return { error: new UnknownError('Interest by region request failed') };
234
205
  }
235
206
  }
236
- async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
207
+ async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', enableBackoff = false, }) {
237
208
  try {
238
209
  // Validate keyword
239
210
  if (!keyword || keyword.trim() === '') {
@@ -246,7 +217,8 @@ export class GoogleTrendsApi {
246
217
  time,
247
218
  category,
248
219
  property,
249
- hl
220
+ hl,
221
+ enableBackoff
250
222
  });
251
223
  if ('error' in exploreResponse) {
252
224
  return { error: exploreResponse.error };
@@ -297,7 +269,7 @@ export class GoogleTrendsApi {
297
269
  ...(relatedTopicsWidget.token && { token: relatedTopicsWidget.token })
298
270
  }
299
271
  };
300
- const response = await request(options.url, options);
272
+ const response = await request(options.url, options, enableBackoff);
301
273
  const text = await response.text();
302
274
  // Parse the response
303
275
  try {
@@ -4,6 +4,6 @@ export declare const request: (url: string, options: {
4
4
  body?: string | Record<string, any>;
5
5
  headers?: Record<string, string>;
6
6
  contentType?: "json" | "form";
7
- }) => Promise<{
7
+ }, enableBackoff?: boolean) => Promise<{
8
8
  text: () => Promise<string>;
9
9
  }>;
@@ -1,12 +1,40 @@
1
1
  import https from 'https';
2
2
  import querystring from 'querystring';
3
3
  let cookieVal;
4
- function rereq(options, body) {
4
+ const MAX_RETRIES = 3;
5
+ const BASE_DELAY_MS = 750;
6
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+ async function runRequest(options, body, attempt) {
5
8
  return new Promise((resolve, reject) => {
6
9
  const req = https.request(options, (res) => {
7
10
  let chunk = '';
8
11
  res.on('data', (data) => { chunk += data; });
9
- res.on('end', () => resolve(chunk));
12
+ res.on('end', async () => {
13
+ const hasSetCookie = !!res.headers['set-cookie']?.length;
14
+ const isRateLimited = res.statusCode === 429 ||
15
+ chunk.includes('Error 429') ||
16
+ chunk.includes('Too Many Requests');
17
+ if (isRateLimited) {
18
+ if (hasSetCookie) {
19
+ cookieVal = res.headers['set-cookie'][0].split(';')[0];
20
+ options.headers['cookie'] = cookieVal;
21
+ }
22
+ if (attempt < MAX_RETRIES) {
23
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.floor(Math.random() * 250);
24
+ await sleep(delay);
25
+ try {
26
+ const retryResponse = await runRequest(options, body, attempt + 1);
27
+ resolve(retryResponse);
28
+ return;
29
+ }
30
+ catch (err) {
31
+ reject(err);
32
+ return;
33
+ }
34
+ }
35
+ }
36
+ resolve(chunk);
37
+ });
10
38
  });
11
39
  req.on('error', reject);
12
40
  if (body)
@@ -14,7 +42,7 @@ function rereq(options, body) {
14
42
  req.end();
15
43
  });
16
44
  }
17
- export const request = async (url, options) => {
45
+ export const request = async (url, options, enableBackoff = false) => {
18
46
  const parsedUrl = new URL(url);
19
47
  const method = options.method || 'POST';
20
48
  // Prepare body
@@ -43,32 +71,9 @@ export const request = async (url, options) => {
43
71
  ...(cookieVal ? { cookie: cookieVal } : {})
44
72
  }
45
73
  };
46
- const response = await new Promise((resolve, reject) => {
47
- const req = https.request(requestOptions, (res) => {
48
- let chunk = '';
49
- res.on('data', (data) => { chunk += data; });
50
- res.on('end', async () => {
51
- if (res.statusCode === 429 && res.headers['set-cookie']) {
52
- cookieVal = res.headers['set-cookie'][0].split(';')[0];
53
- requestOptions.headers['cookie'] = cookieVal;
54
- try {
55
- const retryResponse = await rereq(requestOptions, bodyString);
56
- resolve(retryResponse);
57
- }
58
- catch (err) {
59
- reject(err);
60
- }
61
- }
62
- else {
63
- resolve(chunk);
64
- }
65
- });
66
- });
67
- req.on('error', reject);
68
- if (bodyString)
69
- req.write(bodyString);
70
- req.end();
71
- });
74
+ const response = enableBackoff
75
+ ? await runRequest(requestOptions, bodyString, 0)
76
+ : await runRequest(requestOptions, bodyString, MAX_RETRIES);
72
77
  return {
73
78
  text: () => Promise.resolve(response)
74
79
  };
@@ -2,13 +2,11 @@ import { GoogleTrendsApi } from './helpers/googleTrendsAPI.js';
2
2
  export declare const dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
3
3
  export declare const realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
4
4
  export declare const autocomplete: (keyword: string, hl?: string) => Promise<import("./types/index.js").GoogleTrendsResponse<string[]>>;
5
- export declare const explore: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
5
+ export declare const explore: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
6
6
  error: import("./types/index.js").GoogleTrendsError;
7
7
  }>;
8
- export declare const interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").InterestByRegionResponse | {
9
- error: import("./types/index.js").GoogleTrendsError;
10
- }>;
11
- export declare const relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
8
+ export declare const interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category, enableBackoff }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").InterestByRegionResponse>>;
9
+ export declare const relatedTopics: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
12
10
  export declare const relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedQueriesResponse>>;
13
11
  export declare const relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedData>>;
14
12
  export { GoogleTrendsApi };
@@ -16,13 +14,11 @@ declare const _default: {
16
14
  dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
17
15
  realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
18
16
  autocomplete: (keyword: string, hl?: string) => Promise<import("./types/index.js").GoogleTrendsResponse<string[]>>;
19
- explore: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
20
- error: import("./types/index.js").GoogleTrendsError;
21
- }>;
22
- interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").InterestByRegionResponse | {
17
+ explore: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").ExploreResponse | {
23
18
  error: import("./types/index.js").GoogleTrendsError;
24
19
  }>;
25
- relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
20
+ interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category, enableBackoff }: import("./types/index.js").InterestByRegionOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").InterestByRegionResponse>>;
21
+ relatedTopics: ({ keyword, geo, time, category, property, hl, enableBackoff, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
26
22
  relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedQueriesResponse>>;
27
23
  relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedData>>;
28
24
  };
@@ -0,0 +1,4 @@
1
+ declare const formatDate: (date: Date) => string;
2
+ declare const formatTrendsDate: (date: Date) => string;
3
+ declare const getDateRangeParam: (date: Date) => string;
4
+ export { formatDate, formatTrendsDate, getDateRangeParam };
@@ -0,0 +1,21 @@
1
+ const formatDate = (date) => {
2
+ return date.toISOString().split('T')[0];
3
+ };
4
+ const formatTrendsDate = (date) => {
5
+ const pad = (n) => n.toString().padStart(2, '0');
6
+ const yyyy = date.getFullYear();
7
+ const mm = pad(date.getMonth() + 1);
8
+ const dd = pad(date.getDate());
9
+ const hh = pad(date.getHours());
10
+ const min = pad(date.getMinutes());
11
+ const ss = pad(date.getSeconds());
12
+ return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
13
+ };
14
+ const getDateRangeParam = (date) => {
15
+ const yesterday = new Date(date);
16
+ yesterday.setDate(date.getDate() - 1);
17
+ const formattedStart = formatTrendsDate(yesterday);
18
+ const formattedEnd = formatTrendsDate(date);
19
+ return `${formattedStart} ${formattedEnd}`;
20
+ };
21
+ export { formatDate, formatTrendsDate, getDateRangeParam };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaivpidadi/trends-js",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Google Trends API for Node.js",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",