@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 +45 -6
- package/dist/cjs/helpers/format.d.ts +2 -0
- package/dist/cjs/helpers/format.js +74 -21
- package/dist/cjs/helpers/googleTrendsAPI.d.ts +3 -5
- package/dist/cjs/helpers/googleTrendsAPI.js +51 -75
- package/dist/cjs/helpers/request.d.ts +1 -1
- package/dist/cjs/helpers/request.js +34 -29
- package/dist/cjs/index.d.ts +6 -10
- package/dist/cjs/utils/formatters.d.ts +4 -0
- package/dist/cjs/utils/formatters.js +26 -0
- package/dist/esm/helpers/format.d.ts +2 -0
- package/dist/esm/helpers/format.js +71 -20
- package/dist/esm/helpers/googleTrendsAPI.d.ts +3 -5
- package/dist/esm/helpers/googleTrendsAPI.js +52 -76
- package/dist/esm/helpers/request.d.ts +1 -1
- package/dist/esm/helpers/request.js +34 -29
- package/dist/esm/index.d.ts +6 -10
- package/dist/esm/utils/formatters.d.ts +4 -0
- package/dist/esm/utils/formatters.js +21 -0
- package/package.json +2 -2
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10
|
-
// 4 null
|
|
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:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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.
|
|
205
|
+
return { error: new GoogleTrendsError_js_1.NetworkError(`Interest by region request failed: ${error.message}`) };
|
|
231
206
|
}
|
|
232
|
-
return { error: new GoogleTrendsError_js_1.
|
|
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 {
|
|
@@ -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
|
-
|
|
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', () =>
|
|
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 =
|
|
53
|
-
|
|
54
|
-
|
|
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
|
};
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,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
|
|
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
|
|
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
|
|
7
|
-
// 4 null
|
|
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:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
|
202
|
+
return { error: new NetworkError(`Interest by region request failed: ${error.message}`) };
|
|
228
203
|
}
|
|
229
|
-
return { error: new
|
|
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 {
|
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
2
|
import querystring from 'querystring';
|
|
3
3
|
let cookieVal;
|
|
4
|
-
|
|
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', () =>
|
|
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 =
|
|
47
|
-
|
|
48
|
-
|
|
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
|
};
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,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.
|
|
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.
|
|
45
|
+
"jest": "^29.7.0",
|
|
46
46
|
"ts-jest": "^29.1.0",
|
|
47
47
|
"typescript": "^5.0.0"
|
|
48
48
|
}
|