@shaivpidadi/trends-js 1.0.0 → 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
@@ -46,8 +46,26 @@ const result = await GoogleTrendsApi.dailyTrends({
46
46
 
47
47
  // Result structure:
48
48
  // {
49
- // allTrendingStories: Array<...>,
50
- // summary: string[]
49
+ // allTrendingStories: Array<{
50
+ // title: string,
51
+ // traffic: string,
52
+ // image?: {
53
+ // newsUrl: string,
54
+ // source: string,
55
+ // imageUrl: string
56
+ // },
57
+ // articles: Array<{
58
+ // title: string,
59
+ // url: string,
60
+ // source: string,
61
+ // time: string,
62
+ // snippet: string
63
+ // }>,
64
+ // shareUrl: string,
65
+ // startTime: number, // Unix timestamp
66
+ // endTime?: number // Unix timestamp (optional)
67
+ // }>,
68
+ // summary: Array<...>
51
69
  // }
52
70
  ```
53
71
 
@@ -63,8 +81,26 @@ const result = await GoogleTrendsApi.realTimeTrends({
63
81
 
64
82
  // Result structure:
65
83
  // {
66
- // allTrendingStories: Array<...>,
67
- // summary: string[]
84
+ // allTrendingStories: Array<{
85
+ // title: string,
86
+ // traffic: string,
87
+ // image?: {
88
+ // newsUrl: string,
89
+ // source: string,
90
+ // imageUrl: string
91
+ // },
92
+ // articles: Array<{
93
+ // title: string,
94
+ // url: string,
95
+ // source: string,
96
+ // time: string,
97
+ // snippet: string
98
+ // }>,
99
+ // shareUrl: string,
100
+ // startTime: number, // Unix timestamp
101
+ // endTime?: number // Unix timestamp (optional)
102
+ // }>,
103
+ // summary: Array<...>
68
104
  // }
69
105
  ```
70
106
 
@@ -111,14 +147,15 @@ Get interest data by region:
111
147
 
112
148
  ```typescript
113
149
  const result = await GoogleTrendsApi.interestByRegion({
114
- keyword: 'Stock Market', // Required - string or string[]
150
+ keyword: 'Stock Market', // Required - string
115
151
  startTime: new Date('2024-01-01'), // Optional - defaults to 2004-01-01
116
152
  endTime: new Date(), // Optional - defaults to current date
117
- geo: 'US', // Optional - string or string[] - defaults to 'US'
153
+ geo: 'US', // Optional - string - defaults to 'US'
118
154
  resolution: 'REGION', // Optional - 'COUNTRY' | 'REGION' | 'CITY' | 'DMA'
119
155
  hl: 'en-US', // Optional - defaults to 'en-US'
120
156
  timezone: -240, // Optional - defaults to local timezone
121
157
  category: 0, // Optional - defaults to 0
158
+ enableBackoff: true // Optional - defaults to false
122
159
  });
123
160
 
124
161
  // Result structure:
@@ -149,6 +186,7 @@ const result = await GoogleTrendsApi.interestByRegion({
149
186
  startTime: new Date('2024-01-01'),
150
187
  endTime: new Date(),
151
188
  resolution: 'CITY',
189
+ enableBackoff: true
152
190
  });
153
191
  ```
154
192
 
@@ -288,6 +326,7 @@ interface InterestByRegionOptions {
288
326
  hl?: string; // Optional - language code
289
327
  timezone?: number; // Optional - timezone offset
290
328
  category?: number; // Optional - category number
329
+ enableBackoff?: boolean // Optional
291
330
  }
292
331
  ```
293
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,13 +1,13 @@
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
- // For future refrence and update: from google trends page rpc call response,
5
+ // For future reference and update: from google trends page rpc call response,
6
6
  // 0 "twitter down" The main trending search term.
7
- // 1 null Unused (reserved for future Google Trends data).
7
+ // 1 null OR [newsUrl, source, imageUrl, [articles...]] Image/article data (often null in current API responses).
8
8
  // 2 "US" Country code (where the trend is happening).
9
- // 3 [1741599600] Unix timestamp (represents when the search started trending).
10
- // 4 null Unused (reserved for future data).
9
+ // 3 [1741599600] Unix timestamp array - first element is when the trend started.
10
+ // 4 null OR [1741602000] Unix timestamp array - trend end time (if available).
11
11
  // 5 null Unused (reserved for future data).
12
12
  // 6 500000 Search volume index (estimated search interest for the term).
13
13
  // 7 null Unused (reserved for future data).
@@ -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 {
@@ -41,6 +56,48 @@ const extractJsonFromResponse = (text) => {
41
56
  }
42
57
  };
43
58
  exports.extractJsonFromResponse = extractJsonFromResponse;
59
+ const extractArticles = (item) => {
60
+ const imageData = item[1];
61
+ if (!imageData || !Array.isArray(imageData))
62
+ return [];
63
+ if (imageData.length <= 3)
64
+ return [];
65
+ const articlesArray = imageData[3];
66
+ if (!Array.isArray(articlesArray))
67
+ return [];
68
+ return articlesArray
69
+ .filter((article) => Array.isArray(article) && article.length >= 5)
70
+ .map((article) => ({
71
+ title: String(article[0] || ''),
72
+ url: String(article[1] || ''),
73
+ source: String(article[2] || ''),
74
+ time: String(article[3] || ''),
75
+ snippet: String(article[4] || ''),
76
+ }));
77
+ };
78
+ const extractTimestamp = (item, index) => {
79
+ const timeArray = item[index];
80
+ if (!Array.isArray(timeArray))
81
+ return undefined;
82
+ if (timeArray.length === 0)
83
+ return undefined;
84
+ const timestamp = timeArray[0];
85
+ if (typeof timestamp !== 'number')
86
+ return undefined;
87
+ return timestamp;
88
+ };
89
+ const extractImage = (item) => {
90
+ const imageData = item[1];
91
+ if (!imageData || !Array.isArray(imageData))
92
+ return undefined;
93
+ if (imageData.length < 3)
94
+ return undefined;
95
+ return {
96
+ newsUrl: String(imageData[0] || ''),
97
+ source: String(imageData[1] || ''),
98
+ imageUrl: String(imageData[2] || ''),
99
+ };
100
+ };
44
101
  const updateResponseObject = (data) => {
45
102
  if (!Array.isArray(data)) {
46
103
  throw new GoogleTrendsError_js_1.ParseError('Invalid data format: expected array');
@@ -49,30 +106,26 @@ const updateResponseObject = (data) => {
49
106
  const summary = [];
50
107
  data.forEach((item) => {
51
108
  if (Array.isArray(item)) {
109
+ const articles = extractArticles(item);
110
+ const startTime = extractTimestamp(item, 3) ?? 0;
111
+ const endTime = extractTimestamp(item, 4);
112
+ const image = extractImage(item);
52
113
  const story = {
53
114
  title: String(item[0] || ''),
54
115
  traffic: String(item[6] || '0'),
55
- articles: Array.isArray(item[9]) ? item[9].map((article) => ({
56
- title: String(article[0] || ''),
57
- url: String(article[1] || ''),
58
- source: String(article[2] || ''),
59
- time: String(article[3] || ''),
60
- snippet: String(article[4] || '')
61
- })) : [],
62
- shareUrl: String(item[12] || '')
116
+ articles: articles,
117
+ shareUrl: String(item[12] || ''),
118
+ startTime,
119
+ ...(endTime && { endTime }),
120
+ ...(image && { image }),
63
121
  };
64
- if (item[1]) {
65
- story.image = {
66
- newsUrl: String(item[1][0] || ''),
67
- source: String(item[1][1] || ''),
68
- imageUrl: String(item[1][2] || '')
69
- };
70
- }
71
122
  allTrendingStories.push(story);
72
123
  summary.push({
73
124
  title: story.title,
74
125
  traffic: story.traffic,
75
- articles: story.articles
126
+ articles: story.articles,
127
+ startTime,
128
+ ...(endTime && { endTime }),
76
129
  });
77
130
  }
78
131
  });
@@ -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,88 +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 }) {
155
- const formatDate = (date) => {
156
- return date.toISOString().split('T')[0];
157
- };
158
- const formatTrendsDate = (date) => {
159
- const pad = (n) => n.toString().padStart(2, '0');
160
- const yyyy = date.getFullYear();
161
- const mm = pad(date.getMonth() + 1);
162
- const dd = pad(date.getDate());
163
- const hh = pad(date.getHours());
164
- const min = pad(date.getMinutes());
165
- const ss = pad(date.getSeconds());
166
- return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
167
- };
168
- const getDateRangeParam = (date) => {
169
- const yesterday = new Date(date);
170
- yesterday.setDate(date.getDate() - 1);
171
- const formattedStart = formatTrendsDate(yesterday);
172
- const formattedEnd = formatTrendsDate(date);
173
- return `${formattedStart} ${formattedEnd}`;
174
- };
175
- const exploreResponse = await this.explore({
176
- keyword: Array.isArray(keyword) ? keyword[0] : keyword,
177
- geo: Array.isArray(geo) ? geo[0] : geo,
178
- time: `${getDateRangeParam(startTime)} ${getDateRangeParam(endTime)}`,
179
- category,
180
- hl
181
- });
182
- if ('error' in exploreResponse) {
183
- return { error: exploreResponse.error };
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 }) {
158
+ const keywordValue = Array.isArray(keyword) ? keyword[0] : keyword;
159
+ const geoValue = Array.isArray(geo) ? geo[0] : geo;
160
+ if (!keywordValue || keywordValue.trim() === '') {
161
+ return { error: new GoogleTrendsError_js_1.InvalidRequestError('Keyword is required') };
184
162
  }
185
- const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
186
- if (!widget) {
187
- 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') };
188
165
  }
189
- const options = {
190
- ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.interestByRegion],
191
- 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,
192
172
  hl,
193
- tz: timezone.toString(),
194
- req: JSON.stringify({
195
- geo: {
196
- country: Array.isArray(geo) ? geo[0] : geo
197
- },
198
- comparisonItem: [{
199
- time: `${formatDate(startTime)} ${formatDate(endTime)}`,
200
- complexKeywordsRestriction: {
201
- keyword: [{
202
- type: 'BROAD', //'ENTITY',
203
- value: Array.isArray(keyword) ? keyword[0] : keyword
204
- }]
205
- }
206
- }],
207
- resolution,
208
- locale: hl,
209
- requestOptions: {
210
- property: '',
211
- backend: 'CM', //'IZG',
212
- category
213
- },
214
- userConfig: {
215
- userType: 'USER_TYPE_LEGIT_USER'
216
- }
217
- }),
218
- token: widget.token
173
+ enableBackoff
174
+ });
175
+ if ('error' in exploreResponse) {
176
+ return { error: exploreResponse.error };
219
177
  }
220
- };
221
- try {
222
- 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);
223
195
  const text = await response.text();
224
- // 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)
225
200
  const data = JSON.parse(text.slice(5));
226
- return data;
201
+ return { data: data.default.geoMapData };
227
202
  }
228
203
  catch (error) {
229
204
  if (error instanceof Error) {
230
- 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}`) };
231
206
  }
232
- 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') };
233
208
  }
234
209
  }
235
- 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, }) {
236
211
  try {
237
212
  // Validate keyword
238
213
  if (!keyword || keyword.trim() === '') {
@@ -245,7 +220,8 @@ class GoogleTrendsApi {
245
220
  time,
246
221
  category,
247
222
  property,
248
- hl
223
+ hl,
224
+ enableBackoff
249
225
  });
250
226
  if ('error' in exploreResponse) {
251
227
  return { error: exploreResponse.error };
@@ -296,7 +272,7 @@ class GoogleTrendsApi {
296
272
  ...(relatedTopicsWidget.token && { token: relatedTopicsWidget.token })
297
273
  }
298
274
  };
299
- const response = await (0, request_js_1.request)(options.url, options);
275
+ const response = await (0, request_js_1.request)(options.url, options, enableBackoff);
300
276
  const text = await response.text();
301
277
  // Parse the response
302
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;
@@ -1,10 +1,10 @@
1
1
  import { ParseError } from '../errors/GoogleTrendsError.js';
2
- // For future refrence and update: from google trends page rpc call response,
2
+ // For future reference and update: from google trends page rpc call response,
3
3
  // 0 "twitter down" The main trending search term.
4
- // 1 null Unused (reserved for future Google Trends data).
4
+ // 1 null OR [newsUrl, source, imageUrl, [articles...]] Image/article data (often null in current API responses).
5
5
  // 2 "US" Country code (where the trend is happening).
6
- // 3 [1741599600] Unix timestamp (represents when the search started trending).
7
- // 4 null Unused (reserved for future data).
6
+ // 3 [1741599600] Unix timestamp array - first element is when the trend started.
7
+ // 4 null OR [1741602000] Unix timestamp array - trend end time (if available).
8
8
  // 5 null Unused (reserved for future data).
9
9
  // 6 500000 Search volume index (estimated search interest for the term).
10
10
  // 7 null Unused (reserved for future data).
@@ -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 {
@@ -37,6 +50,48 @@ export const extractJsonFromResponse = (text) => {
37
50
  throw new ParseError('Failed to parse response');
38
51
  }
39
52
  };
53
+ const extractArticles = (item) => {
54
+ const imageData = item[1];
55
+ if (!imageData || !Array.isArray(imageData))
56
+ return [];
57
+ if (imageData.length <= 3)
58
+ return [];
59
+ const articlesArray = imageData[3];
60
+ if (!Array.isArray(articlesArray))
61
+ return [];
62
+ return articlesArray
63
+ .filter((article) => Array.isArray(article) && article.length >= 5)
64
+ .map((article) => ({
65
+ title: String(article[0] || ''),
66
+ url: String(article[1] || ''),
67
+ source: String(article[2] || ''),
68
+ time: String(article[3] || ''),
69
+ snippet: String(article[4] || ''),
70
+ }));
71
+ };
72
+ const extractTimestamp = (item, index) => {
73
+ const timeArray = item[index];
74
+ if (!Array.isArray(timeArray))
75
+ return undefined;
76
+ if (timeArray.length === 0)
77
+ return undefined;
78
+ const timestamp = timeArray[0];
79
+ if (typeof timestamp !== 'number')
80
+ return undefined;
81
+ return timestamp;
82
+ };
83
+ const extractImage = (item) => {
84
+ const imageData = item[1];
85
+ if (!imageData || !Array.isArray(imageData))
86
+ return undefined;
87
+ if (imageData.length < 3)
88
+ return undefined;
89
+ return {
90
+ newsUrl: String(imageData[0] || ''),
91
+ source: String(imageData[1] || ''),
92
+ imageUrl: String(imageData[2] || ''),
93
+ };
94
+ };
40
95
  const updateResponseObject = (data) => {
41
96
  if (!Array.isArray(data)) {
42
97
  throw new ParseError('Invalid data format: expected array');
@@ -45,30 +100,26 @@ const updateResponseObject = (data) => {
45
100
  const summary = [];
46
101
  data.forEach((item) => {
47
102
  if (Array.isArray(item)) {
103
+ const articles = extractArticles(item);
104
+ const startTime = extractTimestamp(item, 3) ?? 0;
105
+ const endTime = extractTimestamp(item, 4);
106
+ const image = extractImage(item);
48
107
  const story = {
49
108
  title: String(item[0] || ''),
50
109
  traffic: String(item[6] || '0'),
51
- articles: Array.isArray(item[9]) ? item[9].map((article) => ({
52
- title: String(article[0] || ''),
53
- url: String(article[1] || ''),
54
- source: String(article[2] || ''),
55
- time: String(article[3] || ''),
56
- snippet: String(article[4] || '')
57
- })) : [],
58
- shareUrl: String(item[12] || '')
110
+ articles: articles,
111
+ shareUrl: String(item[12] || ''),
112
+ startTime,
113
+ ...(endTime && { endTime }),
114
+ ...(image && { image }),
59
115
  };
60
- if (item[1]) {
61
- story.image = {
62
- newsUrl: String(item[1][0] || ''),
63
- source: String(item[1][1] || ''),
64
- imageUrl: String(item[1][2] || '')
65
- };
66
- }
67
116
  allTrendingStories.push(story);
68
117
  summary.push({
69
118
  title: story.title,
70
119
  traffic: story.traffic,
71
- articles: story.articles
120
+ articles: story.articles,
121
+ startTime,
122
+ ...(endTime && { endTime }),
72
123
  });
73
124
  }
74
125
  });
@@ -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,88 +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 }) {
152
- const formatDate = (date) => {
153
- return date.toISOString().split('T')[0];
154
- };
155
- const formatTrendsDate = (date) => {
156
- const pad = (n) => n.toString().padStart(2, '0');
157
- const yyyy = date.getFullYear();
158
- const mm = pad(date.getMonth() + 1);
159
- const dd = pad(date.getDate());
160
- const hh = pad(date.getHours());
161
- const min = pad(date.getMinutes());
162
- const ss = pad(date.getSeconds());
163
- return `${yyyy}-${mm}-${dd}T${hh}\\:${min}\\:${ss}`;
164
- };
165
- const getDateRangeParam = (date) => {
166
- const yesterday = new Date(date);
167
- yesterday.setDate(date.getDate() - 1);
168
- const formattedStart = formatTrendsDate(yesterday);
169
- const formattedEnd = formatTrendsDate(date);
170
- return `${formattedStart} ${formattedEnd}`;
171
- };
172
- const exploreResponse = await this.explore({
173
- keyword: Array.isArray(keyword) ? keyword[0] : keyword,
174
- geo: Array.isArray(geo) ? geo[0] : geo,
175
- time: `${getDateRangeParam(startTime)} ${getDateRangeParam(endTime)}`,
176
- category,
177
- hl
178
- });
179
- if ('error' in exploreResponse) {
180
- return { error: exploreResponse.error };
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 }) {
155
+ const keywordValue = Array.isArray(keyword) ? keyword[0] : keyword;
156
+ const geoValue = Array.isArray(geo) ? geo[0] : geo;
157
+ if (!keywordValue || keywordValue.trim() === '') {
158
+ return { error: new InvalidRequestError('Keyword is required') };
181
159
  }
182
- const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
183
- if (!widget) {
184
- 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') };
185
162
  }
186
- const options = {
187
- ...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.interestByRegion],
188
- qs: {
163
+ try {
164
+ const exploreResponse = await this.explore({
165
+ keyword: keywordValue,
166
+ geo: geoValue,
167
+ time: `${formatDate(startTime)} ${formatDate(endTime)}`,
168
+ category,
189
169
  hl,
190
- tz: timezone.toString(),
191
- req: JSON.stringify({
192
- geo: {
193
- country: Array.isArray(geo) ? geo[0] : geo
194
- },
195
- comparisonItem: [{
196
- time: `${formatDate(startTime)} ${formatDate(endTime)}`,
197
- complexKeywordsRestriction: {
198
- keyword: [{
199
- type: 'BROAD', //'ENTITY',
200
- value: Array.isArray(keyword) ? keyword[0] : keyword
201
- }]
202
- }
203
- }],
204
- resolution,
205
- locale: hl,
206
- requestOptions: {
207
- property: '',
208
- backend: 'CM', //'IZG',
209
- category
210
- },
211
- userConfig: {
212
- userType: 'USER_TYPE_LEGIT_USER'
213
- }
214
- }),
215
- token: widget.token
170
+ enableBackoff
171
+ });
172
+ if ('error' in exploreResponse) {
173
+ return { error: exploreResponse.error };
216
174
  }
217
- };
218
- try {
219
- 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);
220
192
  const text = await response.text();
221
- // 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)
222
197
  const data = JSON.parse(text.slice(5));
223
- return data;
198
+ return { data: data.default.geoMapData };
224
199
  }
225
200
  catch (error) {
226
201
  if (error instanceof Error) {
227
- 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}`) };
228
203
  }
229
- return { error: new ParseError('Failed to parse interest by region response') };
204
+ return { error: new UnknownError('Interest by region request failed') };
230
205
  }
231
206
  }
232
- 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, }) {
233
208
  try {
234
209
  // Validate keyword
235
210
  if (!keyword || keyword.trim() === '') {
@@ -242,7 +217,8 @@ export class GoogleTrendsApi {
242
217
  time,
243
218
  category,
244
219
  property,
245
- hl
220
+ hl,
221
+ enableBackoff
246
222
  });
247
223
  if ('error' in exploreResponse) {
248
224
  return { error: exploreResponse.error };
@@ -293,7 +269,7 @@ export class GoogleTrendsApi {
293
269
  ...(relatedTopicsWidget.token && { token: relatedTopicsWidget.token })
294
270
  }
295
271
  };
296
- const response = await request(options.url, options);
272
+ const response = await request(options.url, options, enableBackoff);
297
273
  const text = await response.text();
298
274
  // Parse the response
299
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.0",
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",
@@ -42,7 +42,7 @@
42
42
  "devDependencies": {
43
43
  "@types/jest": "^29.5.0",
44
44
  "@types/node": "^20.0.0",
45
- "jest": "^29.5.0",
45
+ "jest": "^29.7.0",
46
46
  "ts-jest": "^29.1.0",
47
47
  "typescript": "^5.0.0"
48
48
  }