@shaivpidadi/trends-js 0.0.0-beta.8 → 1.0.1

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
@@ -1,8 +1,11 @@
1
- 🚧 WIP: Temporary workaround for [#175](https://github.com/pat310/google-trends-api/issues/175)
1
+ A TypeScript library for interacting with the Google Trends API. This package provides a simple and type-safe way to access Google Trends data programmatically.
2
2
 
3
- # @shaivpidadi/trends-js
3
+ ## Showcase
4
+
5
+ ### EliteTimesNews.com — Built with `@shaivpidadi/trends-js`
6
+ **URL:** https://elitetimesnews.com
7
+ **What it uses:** `dailyTrends()` (US, en) to power the home page “Daily Trending” rail, refreshed on a schedule.
4
8
 
5
- A TypeScript library for interacting with the Google Trends API. This package provides a simple and type-safe way to access Google Trends data programmatically.
6
9
 
7
10
  ## Installation
8
11
 
@@ -43,8 +46,26 @@ const result = await GoogleTrendsApi.dailyTrends({
43
46
 
44
47
  // Result structure:
45
48
  // {
46
- // allTrendingStories: Array<...>,
47
- // 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<...>
48
69
  // }
49
70
  ```
50
71
 
@@ -60,8 +81,26 @@ const result = await GoogleTrendsApi.realTimeTrends({
60
81
 
61
82
  // Result structure:
62
83
  // {
63
- // allTrendingStories: Array<...>,
64
- // 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<...>
65
104
  // }
66
105
  ```
67
106
 
@@ -1,3 +1,3 @@
1
- import { GoogleTrendsEndpoints } from './types/enums';
2
- import { GoogleTrendsMapper } from './types';
1
+ import { GoogleTrendsEndpoints } from './types/enums.js';
2
+ import { GoogleTrendsMapper } from './types/index.js';
3
3
  export declare const GOOGLE_TRENDS_MAPPER: Record<GoogleTrendsEndpoints, GoogleTrendsMapper>;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GOOGLE_TRENDS_MAPPER = void 0;
4
- const enums_1 = require("./types/enums");
4
+ const enums_js_1 = require("./types/enums.js");
5
5
  const GOOGLE_TRENDS_BASE_URL = 'trends.google.com';
6
6
  exports.GOOGLE_TRENDS_MAPPER = {
7
- [enums_1.GoogleTrendsEndpoints.dailyTrends]: {
7
+ [enums_js_1.GoogleTrendsEndpoints.dailyTrends]: {
8
8
  path: '/_/TrendsUi/data/batchexecute',
9
9
  method: 'POST',
10
10
  host: GOOGLE_TRENDS_BASE_URL,
@@ -13,7 +13,7 @@ exports.GOOGLE_TRENDS_MAPPER = {
13
13
  'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
14
14
  },
15
15
  },
16
- [enums_1.GoogleTrendsEndpoints.autocomplete]: {
16
+ [enums_js_1.GoogleTrendsEndpoints.autocomplete]: {
17
17
  path: '/trends/api/autocomplete',
18
18
  method: 'GET',
19
19
  host: GOOGLE_TRENDS_BASE_URL,
@@ -22,28 +22,28 @@ exports.GOOGLE_TRENDS_MAPPER = {
22
22
  accept: 'application/json, text/plain, */*',
23
23
  },
24
24
  },
25
- [enums_1.GoogleTrendsEndpoints.explore]: {
25
+ [enums_js_1.GoogleTrendsEndpoints.explore]: {
26
26
  path: '/trends/api/explore',
27
27
  method: 'POST',
28
28
  host: GOOGLE_TRENDS_BASE_URL,
29
29
  url: `https://${GOOGLE_TRENDS_BASE_URL}/trends/api/explore`,
30
30
  headers: {},
31
31
  },
32
- [enums_1.GoogleTrendsEndpoints.interestByRegion]: {
32
+ [enums_js_1.GoogleTrendsEndpoints.interestByRegion]: {
33
33
  path: '/trends/api/widgetdata/comparedgeo',
34
34
  method: 'GET',
35
35
  host: GOOGLE_TRENDS_BASE_URL,
36
36
  url: `https://${GOOGLE_TRENDS_BASE_URL}/trends/api/widgetdata/comparedgeo`,
37
37
  headers: {},
38
38
  },
39
- [enums_1.GoogleTrendsEndpoints.relatedTopics]: {
39
+ [enums_js_1.GoogleTrendsEndpoints.relatedTopics]: {
40
40
  path: '/trends/api/widgetdata/relatedtopics',
41
41
  method: 'GET',
42
42
  host: GOOGLE_TRENDS_BASE_URL,
43
43
  url: `https://${GOOGLE_TRENDS_BASE_URL}/trends/api/widgetdata/relatedtopics`,
44
44
  headers: {},
45
45
  },
46
- [enums_1.GoogleTrendsEndpoints.relatedQueries]: {
46
+ [enums_js_1.GoogleTrendsEndpoints.relatedQueries]: {
47
47
  path: '/trends/api/widgetdata/relatedqueries',
48
48
  method: 'GET',
49
49
  host: GOOGLE_TRENDS_BASE_URL,
@@ -1,4 +1,4 @@
1
- import { GoogleTrendsError } from '../types';
1
+ import { GoogleTrendsError } from '../types/index.js';
2
2
  export declare class RateLimitError extends Error implements GoogleTrendsError {
3
3
  code: string;
4
4
  statusCode: number;
@@ -1,2 +1,2 @@
1
- import { DailyTrendingTopics } from '../types';
1
+ import { DailyTrendingTopics } from '../types/index.js';
2
2
  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
3
  exports.extractJsonFromResponse = void 0;
4
- const GoogleTrendsError_1 = require("../errors/GoogleTrendsError");
5
- // For future refrence and update: from google trends page rpc call response,
4
+ const GoogleTrendsError_js_1 = require("../errors/GoogleTrendsError.js");
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).
@@ -21,58 +21,96 @@ const extractJsonFromResponse = (text) => {
21
21
  try {
22
22
  const parsedResponse = JSON.parse(cleanedText);
23
23
  if (!Array.isArray(parsedResponse) || parsedResponse.length === 0) {
24
- throw new GoogleTrendsError_1.ParseError('Invalid response format: empty array');
24
+ throw new GoogleTrendsError_js_1.ParseError('Invalid response format: empty array');
25
25
  }
26
26
  const nestedJsonString = parsedResponse[0][2];
27
27
  if (!nestedJsonString) {
28
- throw new GoogleTrendsError_1.ParseError('Invalid response format: missing nested JSON');
28
+ throw new GoogleTrendsError_js_1.ParseError('Invalid response format: missing nested JSON');
29
29
  }
30
30
  const data = JSON.parse(nestedJsonString);
31
31
  if (!data || !Array.isArray(data) || data.length < 2) {
32
- throw new GoogleTrendsError_1.ParseError('Invalid response format: missing data array');
32
+ throw new GoogleTrendsError_js_1.ParseError('Invalid response format: missing data array');
33
33
  }
34
34
  return updateResponseObject(data[1]);
35
35
  }
36
36
  catch (e) {
37
- if (e instanceof GoogleTrendsError_1.ParseError) {
37
+ if (e instanceof GoogleTrendsError_js_1.ParseError) {
38
38
  throw e;
39
39
  }
40
- throw new GoogleTrendsError_1.ParseError('Failed to parse response');
40
+ throw new GoogleTrendsError_js_1.ParseError('Failed to parse response');
41
41
  }
42
42
  };
43
43
  exports.extractJsonFromResponse = extractJsonFromResponse;
44
+ const extractArticles = (item) => {
45
+ const imageData = item[1];
46
+ if (!imageData || !Array.isArray(imageData))
47
+ return [];
48
+ if (imageData.length <= 3)
49
+ return [];
50
+ const articlesArray = imageData[3];
51
+ if (!Array.isArray(articlesArray))
52
+ return [];
53
+ return articlesArray
54
+ .filter((article) => Array.isArray(article) && article.length >= 5)
55
+ .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
+ };
63
+ const extractTimestamp = (item, index) => {
64
+ const timeArray = item[index];
65
+ if (!Array.isArray(timeArray))
66
+ return undefined;
67
+ if (timeArray.length === 0)
68
+ return undefined;
69
+ const timestamp = timeArray[0];
70
+ if (typeof timestamp !== 'number')
71
+ return undefined;
72
+ return timestamp;
73
+ };
74
+ const extractImage = (item) => {
75
+ const imageData = item[1];
76
+ if (!imageData || !Array.isArray(imageData))
77
+ return undefined;
78
+ if (imageData.length < 3)
79
+ return undefined;
80
+ return {
81
+ newsUrl: String(imageData[0] || ''),
82
+ source: String(imageData[1] || ''),
83
+ imageUrl: String(imageData[2] || ''),
84
+ };
85
+ };
44
86
  const updateResponseObject = (data) => {
45
87
  if (!Array.isArray(data)) {
46
- throw new GoogleTrendsError_1.ParseError('Invalid data format: expected array');
88
+ throw new GoogleTrendsError_js_1.ParseError('Invalid data format: expected array');
47
89
  }
48
90
  const allTrendingStories = [];
49
91
  const summary = [];
50
92
  data.forEach((item) => {
51
93
  if (Array.isArray(item)) {
94
+ const articles = extractArticles(item);
95
+ const startTime = extractTimestamp(item, 3) ?? 0;
96
+ const endTime = extractTimestamp(item, 4);
97
+ const image = extractImage(item);
52
98
  const story = {
53
99
  title: String(item[0] || ''),
54
100
  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] || '')
101
+ articles: articles,
102
+ shareUrl: String(item[12] || ''),
103
+ startTime,
104
+ ...(endTime && { endTime }),
105
+ ...(image && { image }),
63
106
  };
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
107
  allTrendingStories.push(story);
72
108
  summary.push({
73
109
  title: story.title,
74
110
  traffic: story.traffic,
75
- articles: story.articles
111
+ articles: story.articles,
112
+ startTime,
113
+ ...(endTime && { endTime }),
76
114
  });
77
115
  }
78
116
  });
@@ -1,4 +1,4 @@
1
- import { DailyTrendingTopics, DailyTrendingTopicsOptions, RealTimeTrendsOptions, ExploreOptions, ExploreResponse, InterestByRegionOptions, InterestByRegionResponse, GoogleTrendsResponse, RelatedTopicsResponse, RelatedQueriesResponse, RelatedData } from '../types/index';
1
+ import { DailyTrendingTopics, DailyTrendingTopicsOptions, RealTimeTrendsOptions, ExploreOptions, ExploreResponse, InterestByRegionOptions, InterestByRegionResponse, GoogleTrendsResponse, GoogleTrendsError, RelatedTopicsResponse, RelatedQueriesResponse, RelatedData } from '../types/index.js';
2
2
  export declare class GoogleTrendsApi {
3
3
  /**
4
4
  * Get autocomplete suggestions for a keyword
@@ -19,8 +19,12 @@ 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>;
23
- interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: InterestByRegionOptions): Promise<InterestByRegionResponse>;
22
+ explore({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<ExploreResponse | {
23
+ error: GoogleTrendsError;
24
+ }>;
25
+ interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: InterestByRegionOptions): Promise<InterestByRegionResponse | {
26
+ error: GoogleTrendsError;
27
+ }>;
24
28
  relatedTopics({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedTopicsResponse>>;
25
29
  relatedQueries({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedQueriesResponse>>;
26
30
  relatedData({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedData>>;
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GoogleTrendsApi = void 0;
4
- const enums_1 = require("../types/enums");
5
- const request_1 = require("./request");
6
- const format_1 = require("./format");
7
- const constants_1 = require("../constants");
8
- const GoogleTrendsError_1 = require("../errors/GoogleTrendsError");
4
+ const enums_js_1 = require("../types/enums.js");
5
+ const request_js_1 = require("./request.js");
6
+ const format_js_1 = require("./format.js");
7
+ const constants_js_1 = require("../constants.js");
8
+ const GoogleTrendsError_js_1 = require("../errors/GoogleTrendsError.js");
9
9
  class GoogleTrendsApi {
10
10
  /**
11
11
  * Get autocomplete suggestions for a keyword
@@ -18,14 +18,14 @@ class GoogleTrendsApi {
18
18
  return { data: [] };
19
19
  }
20
20
  const options = {
21
- ...constants_1.GOOGLE_TRENDS_MAPPER[enums_1.GoogleTrendsEndpoints.autocomplete],
21
+ ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.autocomplete],
22
22
  qs: {
23
23
  hl,
24
24
  tz: '240',
25
25
  },
26
26
  };
27
27
  try {
28
- const response = await (0, request_1.request)(`${options.url}/${encodeURIComponent(keyword)}`, options);
28
+ const response = await (0, request_js_1.request)(`${options.url}/${encodeURIComponent(keyword)}`, options);
29
29
  const text = await response.text();
30
30
  // Remove the first 5 characters (JSONP wrapper) and parse
31
31
  const data = JSON.parse(text.slice(5));
@@ -33,9 +33,9 @@ class GoogleTrendsApi {
33
33
  }
34
34
  catch (error) {
35
35
  if (error instanceof Error) {
36
- return { error: new GoogleTrendsError_1.NetworkError(error.message) };
36
+ return { error: new GoogleTrendsError_js_1.NetworkError(error.message) };
37
37
  }
38
- return { error: new GoogleTrendsError_1.UnknownError() };
38
+ return { error: new GoogleTrendsError_js_1.UnknownError() };
39
39
  }
40
40
  }
41
41
  /**
@@ -44,7 +44,7 @@ class GoogleTrendsApi {
44
44
  * @returns Promise with trending topics data
45
45
  */
46
46
  async dailyTrends({ geo = 'US', lang = 'en' }) {
47
- const defaultOptions = constants_1.GOOGLE_TRENDS_MAPPER[enums_1.GoogleTrendsEndpoints.dailyTrends];
47
+ const defaultOptions = constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.dailyTrends];
48
48
  const options = {
49
49
  ...defaultOptions,
50
50
  body: new URLSearchParams({
@@ -53,19 +53,19 @@ class GoogleTrendsApi {
53
53
  contentType: 'form'
54
54
  };
55
55
  try {
56
- const response = await (0, request_1.request)(options.url, options);
56
+ const response = await (0, request_js_1.request)(options.url, options);
57
57
  const text = await response.text();
58
- const trendingTopics = (0, format_1.extractJsonFromResponse)(text);
58
+ const trendingTopics = (0, format_js_1.extractJsonFromResponse)(text);
59
59
  if (!trendingTopics) {
60
- return { error: new GoogleTrendsError_1.ParseError() };
60
+ return { error: new GoogleTrendsError_js_1.ParseError() };
61
61
  }
62
62
  return { data: trendingTopics };
63
63
  }
64
64
  catch (error) {
65
65
  if (error instanceof Error) {
66
- return { error: new GoogleTrendsError_1.NetworkError(error.message) };
66
+ return { error: new GoogleTrendsError_js_1.NetworkError(error.message) };
67
67
  }
68
- return { error: new GoogleTrendsError_1.UnknownError() };
68
+ return { error: new GoogleTrendsError_js_1.UnknownError() };
69
69
  }
70
70
  }
71
71
  /**
@@ -74,7 +74,7 @@ class GoogleTrendsApi {
74
74
  * @returns Promise with trending topics data
75
75
  */
76
76
  async realTimeTrends({ geo = 'US', trendingHours = 4 }) {
77
- const defaultOptions = constants_1.GOOGLE_TRENDS_MAPPER[enums_1.GoogleTrendsEndpoints.dailyTrends];
77
+ const defaultOptions = constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.dailyTrends];
78
78
  const options = {
79
79
  ...defaultOptions,
80
80
  body: new URLSearchParams({
@@ -83,24 +83,24 @@ class GoogleTrendsApi {
83
83
  contentType: 'form'
84
84
  };
85
85
  try {
86
- const response = await (0, request_1.request)(options.url, options);
86
+ const response = await (0, request_js_1.request)(options.url, options);
87
87
  const text = await response.text();
88
- const trendingTopics = (0, format_1.extractJsonFromResponse)(text);
88
+ const trendingTopics = (0, format_js_1.extractJsonFromResponse)(text);
89
89
  if (!trendingTopics) {
90
- return { error: new GoogleTrendsError_1.ParseError() };
90
+ return { error: new GoogleTrendsError_js_1.ParseError() };
91
91
  }
92
92
  return { data: trendingTopics };
93
93
  }
94
94
  catch (error) {
95
95
  if (error instanceof Error) {
96
- return { error: new GoogleTrendsError_1.NetworkError(error.message) };
96
+ return { error: new GoogleTrendsError_js_1.NetworkError(error.message) };
97
97
  }
98
- return { error: new GoogleTrendsError_1.UnknownError() };
98
+ return { error: new GoogleTrendsError_js_1.UnknownError() };
99
99
  }
100
100
  }
101
101
  async explore({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
102
102
  const options = {
103
- ...constants_1.GOOGLE_TRENDS_MAPPER[enums_1.GoogleTrendsEndpoints.explore],
103
+ ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.explore],
104
104
  qs: {
105
105
  hl,
106
106
  tz: '240',
@@ -119,32 +119,43 @@ class GoogleTrendsApi {
119
119
  contentType: 'form'
120
120
  };
121
121
  try {
122
- const response = await (0, request_1.request)(options.url, options);
122
+ const response = await (0, request_js_1.request)(options.url, options);
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')) {
126
- console.error('Explore request returned HTML instead of JSON');
127
- return { widgets: [] };
126
+ return { error: new GoogleTrendsError_js_1.ParseError('Explore request returned HTML instead of JSON') };
128
127
  }
129
128
  // Try to parse as JSON
130
129
  try {
131
130
  // Remove the first 5 characters (JSONP wrapper) and parse
132
131
  const data = JSON.parse(text.slice(5));
133
- return data;
132
+ // Extract widgets from the response
133
+ if (data && Array.isArray(data) && data.length > 0) {
134
+ const widgets = data[0] || [];
135
+ return { widgets };
136
+ }
137
+ return { widgets: [] };
134
138
  }
135
139
  catch (parseError) {
136
- console.error('Failed to parse explore response as JSON:', parseError instanceof Error ? parseError.message : 'Unknown parse error');
137
- console.error('Response preview:', text.substring(0, 200));
138
- return { widgets: [] };
140
+ if (parseError instanceof Error) {
141
+ return { error: new GoogleTrendsError_js_1.ParseError(`Failed to parse explore response as JSON: ${parseError.message}`) };
142
+ }
143
+ return { error: new GoogleTrendsError_js_1.ParseError('Failed to parse explore response as JSON') };
139
144
  }
140
145
  }
141
146
  catch (error) {
142
- console.error('Explore request failed:', error);
143
- return { widgets: [] };
147
+ if (error instanceof Error) {
148
+ return { error: new GoogleTrendsError_js_1.NetworkError(`Explore request failed: ${error.message}`) };
149
+ }
150
+ return { error: new GoogleTrendsError_js_1.UnknownError('Explore request failed') };
144
151
  }
145
152
  }
146
153
  //
147
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 keywordValue = Array.isArray(keyword) ? keyword[0] : keyword;
156
+ if (!keywordValue || keywordValue.trim() === '') {
157
+ return { error: new GoogleTrendsError_js_1.InvalidRequestError('Keyword is required') };
158
+ }
148
159
  const formatDate = (date) => {
149
160
  return date.toISOString().split('T')[0];
150
161
  };
@@ -172,12 +183,15 @@ class GoogleTrendsApi {
172
183
  category,
173
184
  hl
174
185
  });
186
+ if ('error' in exploreResponse) {
187
+ return { error: exploreResponse.error };
188
+ }
175
189
  const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
176
190
  if (!widget) {
177
- return { default: { geoMapData: [] } };
191
+ return { error: new GoogleTrendsError_js_1.ParseError('No GEO_MAP widget found in explore response') };
178
192
  }
179
193
  const options = {
180
- ...constants_1.GOOGLE_TRENDS_MAPPER[enums_1.GoogleTrendsEndpoints.interestByRegion],
194
+ ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.interestByRegion],
181
195
  qs: {
182
196
  hl,
183
197
  tz: timezone.toString(),
@@ -209,59 +223,116 @@ class GoogleTrendsApi {
209
223
  }
210
224
  };
211
225
  try {
212
- const response = await (0, request_1.request)(options.url, options);
226
+ const response = await (0, request_js_1.request)(options.url, options);
213
227
  const text = await response.text();
214
228
  // Remove the first 5 characters (JSONP wrapper) and parse
215
229
  const data = JSON.parse(text.slice(5));
216
230
  return data;
217
231
  }
218
232
  catch (error) {
219
- return { default: { geoMapData: [] } };
233
+ if (error instanceof Error) {
234
+ return { error: new GoogleTrendsError_js_1.ParseError(`Failed to parse interest by region response: ${error.message}`) };
235
+ }
236
+ return { error: new GoogleTrendsError_js_1.ParseError('Failed to parse interest by region response') };
220
237
  }
221
238
  }
222
239
  async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
223
240
  try {
224
241
  // Validate keyword
225
242
  if (!keyword || keyword.trim() === '') {
226
- return { error: new GoogleTrendsError_1.ParseError() };
243
+ return { error: new GoogleTrendsError_js_1.InvalidRequestError('Keyword is required') };
227
244
  }
228
- const autocompleteResult = await this.autocomplete(keyword, hl);
229
- if (autocompleteResult.error) {
230
- return { error: autocompleteResult.error };
245
+ // Step 1: Call explore to get widget data and token
246
+ const exploreResponse = await this.explore({
247
+ keyword,
248
+ geo,
249
+ time,
250
+ category,
251
+ property,
252
+ hl
253
+ });
254
+ if ('error' in exploreResponse) {
255
+ return { error: exploreResponse.error };
231
256
  }
232
- const relatedTopics = autocompleteResult.data?.slice(0, 10).map((suggestion, index) => ({
233
- topic: {
234
- mid: `/m/${index}`,
235
- title: suggestion,
236
- type: 'Topic'
237
- },
238
- value: 100 - index * 10,
239
- formattedValue: (100 - index * 10).toString(),
240
- hasData: true,
241
- link: `/trends/explore?q=${encodeURIComponent(suggestion)}&date=${time}&geo=${geo}`
242
- })) || [];
243
- return {
244
- data: {
245
- default: {
246
- rankedList: [{
247
- rankedKeyword: relatedTopics
248
- }]
249
- }
257
+ if (!exploreResponse.widgets || exploreResponse.widgets.length === 0) {
258
+ return { error: new GoogleTrendsError_js_1.ParseError('No widgets found in explore response. This might be due to Google blocking the request, invalid parameters, or network issues.') };
259
+ }
260
+ // Step 2: Find the related topics widget or use any available widget
261
+ const relatedTopicsWidget = exploreResponse.widgets.find(widget => widget.id === 'RELATED_TOPICS' ||
262
+ widget.request?.restriction?.complexKeywordsRestriction?.keyword?.[0]?.value === keyword) || exploreResponse.widgets[0]; // Fallback to first widget if no specific one found
263
+ if (!relatedTopicsWidget) {
264
+ return { error: new GoogleTrendsError_js_1.ParseError('No related topics widget found in explore response') };
265
+ }
266
+ // Step 3: Call the related topics API with or without token
267
+ const options = {
268
+ ...constants_js_1.GOOGLE_TRENDS_MAPPER[enums_js_1.GoogleTrendsEndpoints.relatedTopics],
269
+ qs: {
270
+ hl,
271
+ tz: '240',
272
+ req: JSON.stringify({
273
+ restriction: {
274
+ geo: { country: geo },
275
+ time: time,
276
+ originalTimeRangeForExploreUrl: time,
277
+ complexKeywordsRestriction: {
278
+ keyword: [{
279
+ type: 'BROAD',
280
+ value: keyword
281
+ }]
282
+ }
283
+ },
284
+ keywordType: 'ENTITY',
285
+ metric: ['TOP', 'RISING'],
286
+ trendinessSettings: {
287
+ compareTime: time
288
+ },
289
+ requestOptions: {
290
+ property: property,
291
+ backend: 'CM',
292
+ category: category
293
+ },
294
+ language: hl.split('-')[0],
295
+ userCountryCode: geo,
296
+ userConfig: {
297
+ userType: 'USER_TYPE_LEGIT_USER'
298
+ }
299
+ }),
300
+ ...(relatedTopicsWidget.token && { token: relatedTopicsWidget.token })
250
301
  }
251
302
  };
303
+ const response = await (0, request_js_1.request)(options.url, options);
304
+ const text = await response.text();
305
+ // Parse the response
306
+ try {
307
+ const data = JSON.parse(text.slice(5));
308
+ // Return the data in the expected format
309
+ return {
310
+ data: {
311
+ default: {
312
+ rankedList: data.default?.rankedList || []
313
+ }
314
+ }
315
+ };
316
+ }
317
+ catch (parseError) {
318
+ if (parseError instanceof Error) {
319
+ return { error: new GoogleTrendsError_js_1.ParseError(`Failed to parse related topics response: ${parseError.message}`) };
320
+ }
321
+ return { error: new GoogleTrendsError_js_1.ParseError('Failed to parse related topics response') };
322
+ }
252
323
  }
253
324
  catch (error) {
254
325
  if (error instanceof Error) {
255
- return { error: new GoogleTrendsError_1.NetworkError(error.message) };
326
+ return { error: new GoogleTrendsError_js_1.NetworkError(error.message) };
256
327
  }
257
- return { error: new GoogleTrendsError_1.UnknownError() };
328
+ return { error: new GoogleTrendsError_js_1.UnknownError() };
258
329
  }
259
330
  }
260
331
  async relatedQueries({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
261
332
  try {
262
333
  // Validate keyword
263
334
  if (!keyword || keyword.trim() === '') {
264
- return { error: new GoogleTrendsError_1.ParseError() };
335
+ return { error: new GoogleTrendsError_js_1.ParseError() };
265
336
  }
266
337
  const autocompleteResult = await this.autocomplete(keyword, hl);
267
338
  if (autocompleteResult.error) {
@@ -286,16 +357,16 @@ class GoogleTrendsApi {
286
357
  }
287
358
  catch (error) {
288
359
  if (error instanceof Error) {
289
- return { error: new GoogleTrendsError_1.NetworkError(error.message) };
360
+ return { error: new GoogleTrendsError_js_1.NetworkError(error.message) };
290
361
  }
291
- return { error: new GoogleTrendsError_1.UnknownError() };
362
+ return { error: new GoogleTrendsError_js_1.UnknownError() };
292
363
  }
293
364
  }
294
365
  async relatedData({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
295
366
  try {
296
367
  // Validate keyword
297
368
  if (!keyword || keyword.trim() === '') {
298
- return { error: new GoogleTrendsError_1.ParseError() };
369
+ return { error: new GoogleTrendsError_js_1.ParseError() };
299
370
  }
300
371
  const autocompleteResult = await this.autocomplete(keyword, hl);
301
372
  if (autocompleteResult.error) {
@@ -329,9 +400,9 @@ class GoogleTrendsApi {
329
400
  }
330
401
  catch (error) {
331
402
  if (error instanceof Error) {
332
- return { error: new GoogleTrendsError_1.NetworkError(error.message) };
403
+ return { error: new GoogleTrendsError_js_1.NetworkError(error.message) };
333
404
  }
334
- return { error: new GoogleTrendsError_1.UnknownError() };
405
+ return { error: new GoogleTrendsError_js_1.UnknownError() };
335
406
  }
336
407
  }
337
408
  }
@@ -1,19 +1,29 @@
1
- export declare const dailyTrends: ({ geo, lang }: import("./types").DailyTrendingTopicsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
2
- export declare const realTimeTrends: ({ geo, trendingHours }: import("./types").RealTimeTrendsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
3
- export declare const autocomplete: (keyword: string, hl?: string) => Promise<import("./types").GoogleTrendsResponse<string[]>>;
4
- export declare const explore: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").ExploreResponse>;
5
- export declare const interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types").InterestByRegionOptions) => Promise<import("./types").InterestByRegionResponse>;
6
- export declare const relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedTopicsResponse>>;
7
- export declare const relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedQueriesResponse>>;
8
- export declare const relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedData>>;
1
+ import { GoogleTrendsApi } from './helpers/googleTrendsAPI.js';
2
+ export declare const dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
3
+ export declare const realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
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 | {
6
+ error: import("./types/index.js").GoogleTrendsError;
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>>;
12
+ 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
+ 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
+ export { GoogleTrendsApi };
9
15
  declare const _default: {
10
- dailyTrends: ({ geo, lang }: import("./types").DailyTrendingTopicsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
11
- realTimeTrends: ({ geo, trendingHours }: import("./types").RealTimeTrendsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
12
- autocomplete: (keyword: string, hl?: string) => Promise<import("./types").GoogleTrendsResponse<string[]>>;
13
- explore: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").ExploreResponse>;
14
- interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types").InterestByRegionOptions) => Promise<import("./types").InterestByRegionResponse>;
15
- relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedTopicsResponse>>;
16
- relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedQueriesResponse>>;
17
- relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedData>>;
16
+ dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
17
+ realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
18
+ 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 | {
23
+ error: import("./types/index.js").GoogleTrendsError;
24
+ }>;
25
+ relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
26
+ relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedQueriesResponse>>;
27
+ relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedData>>;
18
28
  };
19
29
  export default _default;
package/dist/cjs/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.relatedData = exports.relatedQueries = exports.relatedTopics = exports.interestByRegion = exports.explore = exports.autocomplete = exports.realTimeTrends = exports.dailyTrends = void 0;
4
- const googleTrendsAPI_1 = require("./helpers/googleTrendsAPI");
5
- const api = new googleTrendsAPI_1.GoogleTrendsApi();
3
+ exports.GoogleTrendsApi = exports.relatedData = exports.relatedQueries = exports.relatedTopics = exports.interestByRegion = exports.explore = exports.autocomplete = exports.realTimeTrends = exports.dailyTrends = void 0;
4
+ const googleTrendsAPI_js_1 = require("./helpers/googleTrendsAPI.js");
5
+ Object.defineProperty(exports, "GoogleTrendsApi", { enumerable: true, get: function () { return googleTrendsAPI_js_1.GoogleTrendsApi; } });
6
+ const api = new googleTrendsAPI_js_1.GoogleTrendsApi();
6
7
  exports.dailyTrends = api.dailyTrends.bind(api);
7
8
  exports.realTimeTrends = api.realTimeTrends.bind(api);
8
9
  exports.autocomplete = api.autocomplete.bind(api);
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -1,3 +1,3 @@
1
- import { GoogleTrendsEndpoints } from './types/enums';
2
- import { GoogleTrendsMapper } from './types';
1
+ import { GoogleTrendsEndpoints } from './types/enums.js';
2
+ import { GoogleTrendsMapper } from './types/index.js';
3
3
  export declare const GOOGLE_TRENDS_MAPPER: Record<GoogleTrendsEndpoints, GoogleTrendsMapper>;
@@ -1,4 +1,4 @@
1
- import { GoogleTrendsEndpoints } from './types/enums';
1
+ import { GoogleTrendsEndpoints } from './types/enums.js';
2
2
  const GOOGLE_TRENDS_BASE_URL = 'trends.google.com';
3
3
  export const GOOGLE_TRENDS_MAPPER = {
4
4
  [GoogleTrendsEndpoints.dailyTrends]: {
@@ -1,4 +1,4 @@
1
- import { GoogleTrendsError } from '../types';
1
+ import { GoogleTrendsError } from '../types/index.js';
2
2
  export declare class RateLimitError extends Error implements GoogleTrendsError {
3
3
  code: string;
4
4
  statusCode: number;
@@ -1,2 +1,2 @@
1
- import { DailyTrendingTopics } from '../types';
1
+ import { DailyTrendingTopics } from '../types/index.js';
2
2
  export declare const extractJsonFromResponse: (text: string) => DailyTrendingTopics | null;
@@ -1,10 +1,10 @@
1
- import { ParseError } from '../errors/GoogleTrendsError';
2
- // For future refrence and update: from google trends page rpc call response,
1
+ import { ParseError } from '../errors/GoogleTrendsError.js';
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).
@@ -37,6 +37,48 @@ export const extractJsonFromResponse = (text) => {
37
37
  throw new ParseError('Failed to parse response');
38
38
  }
39
39
  };
40
+ const extractArticles = (item) => {
41
+ const imageData = item[1];
42
+ if (!imageData || !Array.isArray(imageData))
43
+ return [];
44
+ if (imageData.length <= 3)
45
+ return [];
46
+ const articlesArray = imageData[3];
47
+ if (!Array.isArray(articlesArray))
48
+ return [];
49
+ return articlesArray
50
+ .filter((article) => Array.isArray(article) && article.length >= 5)
51
+ .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
+ };
59
+ const extractTimestamp = (item, index) => {
60
+ const timeArray = item[index];
61
+ if (!Array.isArray(timeArray))
62
+ return undefined;
63
+ if (timeArray.length === 0)
64
+ return undefined;
65
+ const timestamp = timeArray[0];
66
+ if (typeof timestamp !== 'number')
67
+ return undefined;
68
+ return timestamp;
69
+ };
70
+ const extractImage = (item) => {
71
+ const imageData = item[1];
72
+ if (!imageData || !Array.isArray(imageData))
73
+ return undefined;
74
+ if (imageData.length < 3)
75
+ return undefined;
76
+ return {
77
+ newsUrl: String(imageData[0] || ''),
78
+ source: String(imageData[1] || ''),
79
+ imageUrl: String(imageData[2] || ''),
80
+ };
81
+ };
40
82
  const updateResponseObject = (data) => {
41
83
  if (!Array.isArray(data)) {
42
84
  throw new ParseError('Invalid data format: expected array');
@@ -45,30 +87,26 @@ const updateResponseObject = (data) => {
45
87
  const summary = [];
46
88
  data.forEach((item) => {
47
89
  if (Array.isArray(item)) {
90
+ const articles = extractArticles(item);
91
+ const startTime = extractTimestamp(item, 3) ?? 0;
92
+ const endTime = extractTimestamp(item, 4);
93
+ const image = extractImage(item);
48
94
  const story = {
49
95
  title: String(item[0] || ''),
50
96
  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] || '')
97
+ articles: articles,
98
+ shareUrl: String(item[12] || ''),
99
+ startTime,
100
+ ...(endTime && { endTime }),
101
+ ...(image && { image }),
59
102
  };
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
103
  allTrendingStories.push(story);
68
104
  summary.push({
69
105
  title: story.title,
70
106
  traffic: story.traffic,
71
- articles: story.articles
107
+ articles: story.articles,
108
+ startTime,
109
+ ...(endTime && { endTime }),
72
110
  });
73
111
  }
74
112
  });
@@ -1,4 +1,4 @@
1
- import { DailyTrendingTopics, DailyTrendingTopicsOptions, RealTimeTrendsOptions, ExploreOptions, ExploreResponse, InterestByRegionOptions, InterestByRegionResponse, GoogleTrendsResponse, RelatedTopicsResponse, RelatedQueriesResponse, RelatedData } from '../types/index';
1
+ import { DailyTrendingTopics, DailyTrendingTopicsOptions, RealTimeTrendsOptions, ExploreOptions, ExploreResponse, InterestByRegionOptions, InterestByRegionResponse, GoogleTrendsResponse, GoogleTrendsError, RelatedTopicsResponse, RelatedQueriesResponse, RelatedData } from '../types/index.js';
2
2
  export declare class GoogleTrendsApi {
3
3
  /**
4
4
  * Get autocomplete suggestions for a keyword
@@ -19,8 +19,12 @@ 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>;
23
- interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: InterestByRegionOptions): Promise<InterestByRegionResponse>;
22
+ explore({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<ExploreResponse | {
23
+ error: GoogleTrendsError;
24
+ }>;
25
+ interestByRegion({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: InterestByRegionOptions): Promise<InterestByRegionResponse | {
26
+ error: GoogleTrendsError;
27
+ }>;
24
28
  relatedTopics({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedTopicsResponse>>;
25
29
  relatedQueries({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedQueriesResponse>>;
26
30
  relatedData({ keyword, geo, time, category, property, hl, }: ExploreOptions): Promise<GoogleTrendsResponse<RelatedData>>;
@@ -1,8 +1,8 @@
1
- import { GoogleTrendsEndpoints } from '../types/enums';
2
- import { request } from './request';
3
- import { extractJsonFromResponse } from './format';
4
- import { GOOGLE_TRENDS_MAPPER } from '../constants';
5
- import { NetworkError, ParseError, UnknownError, } from '../errors/GoogleTrendsError';
1
+ import { GoogleTrendsEndpoints } from '../types/enums.js';
2
+ import { request } from './request.js';
3
+ import { extractJsonFromResponse } from './format.js';
4
+ import { GOOGLE_TRENDS_MAPPER } from '../constants.js';
5
+ import { InvalidRequestError, NetworkError, ParseError, UnknownError, } from '../errors/GoogleTrendsError.js';
6
6
  export class GoogleTrendsApi {
7
7
  /**
8
8
  * Get autocomplete suggestions for a keyword
@@ -120,28 +120,39 @@ export class GoogleTrendsApi {
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')) {
123
- console.error('Explore request returned HTML instead of JSON');
124
- return { widgets: [] };
123
+ return { error: new ParseError('Explore request returned HTML instead of JSON') };
125
124
  }
126
125
  // Try to parse as JSON
127
126
  try {
128
127
  // Remove the first 5 characters (JSONP wrapper) and parse
129
128
  const data = JSON.parse(text.slice(5));
130
- return data;
129
+ // Extract widgets from the response
130
+ if (data && Array.isArray(data) && data.length > 0) {
131
+ const widgets = data[0] || [];
132
+ return { widgets };
133
+ }
134
+ return { widgets: [] };
131
135
  }
132
136
  catch (parseError) {
133
- console.error('Failed to parse explore response as JSON:', parseError instanceof Error ? parseError.message : 'Unknown parse error');
134
- console.error('Response preview:', text.substring(0, 200));
135
- return { widgets: [] };
137
+ if (parseError instanceof Error) {
138
+ return { error: new ParseError(`Failed to parse explore response as JSON: ${parseError.message}`) };
139
+ }
140
+ return { error: new ParseError('Failed to parse explore response as JSON') };
136
141
  }
137
142
  }
138
143
  catch (error) {
139
- console.error('Explore request failed:', error);
140
- return { widgets: [] };
144
+ if (error instanceof Error) {
145
+ return { error: new NetworkError(`Explore request failed: ${error.message}`) };
146
+ }
147
+ return { error: new UnknownError('Explore request failed') };
141
148
  }
142
149
  }
143
150
  //
144
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 keywordValue = Array.isArray(keyword) ? keyword[0] : keyword;
153
+ if (!keywordValue || keywordValue.trim() === '') {
154
+ return { error: new InvalidRequestError('Keyword is required') };
155
+ }
145
156
  const formatDate = (date) => {
146
157
  return date.toISOString().split('T')[0];
147
158
  };
@@ -169,9 +180,12 @@ export class GoogleTrendsApi {
169
180
  category,
170
181
  hl
171
182
  });
183
+ if ('error' in exploreResponse) {
184
+ return { error: exploreResponse.error };
185
+ }
172
186
  const widget = exploreResponse.widgets.find(w => w.id === 'GEO_MAP');
173
187
  if (!widget) {
174
- return { default: { geoMapData: [] } };
188
+ return { error: new ParseError('No GEO_MAP widget found in explore response') };
175
189
  }
176
190
  const options = {
177
191
  ...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.interestByRegion],
@@ -213,39 +227,96 @@ export class GoogleTrendsApi {
213
227
  return data;
214
228
  }
215
229
  catch (error) {
216
- return { default: { geoMapData: [] } };
230
+ if (error instanceof Error) {
231
+ return { error: new ParseError(`Failed to parse interest by region response: ${error.message}`) };
232
+ }
233
+ return { error: new ParseError('Failed to parse interest by region response') };
217
234
  }
218
235
  }
219
236
  async relatedTopics({ keyword, geo = 'US', time = 'now 1-d', category = 0, property = '', hl = 'en-US', }) {
220
237
  try {
221
238
  // Validate keyword
222
239
  if (!keyword || keyword.trim() === '') {
223
- return { error: new ParseError() };
240
+ return { error: new InvalidRequestError('Keyword is required') };
224
241
  }
225
- const autocompleteResult = await this.autocomplete(keyword, hl);
226
- if (autocompleteResult.error) {
227
- return { error: autocompleteResult.error };
242
+ // Step 1: Call explore to get widget data and token
243
+ const exploreResponse = await this.explore({
244
+ keyword,
245
+ geo,
246
+ time,
247
+ category,
248
+ property,
249
+ hl
250
+ });
251
+ if ('error' in exploreResponse) {
252
+ return { error: exploreResponse.error };
228
253
  }
229
- const relatedTopics = autocompleteResult.data?.slice(0, 10).map((suggestion, index) => ({
230
- topic: {
231
- mid: `/m/${index}`,
232
- title: suggestion,
233
- type: 'Topic'
234
- },
235
- value: 100 - index * 10,
236
- formattedValue: (100 - index * 10).toString(),
237
- hasData: true,
238
- link: `/trends/explore?q=${encodeURIComponent(suggestion)}&date=${time}&geo=${geo}`
239
- })) || [];
240
- return {
241
- data: {
242
- default: {
243
- rankedList: [{
244
- rankedKeyword: relatedTopics
245
- }]
246
- }
254
+ if (!exploreResponse.widgets || exploreResponse.widgets.length === 0) {
255
+ return { error: new ParseError('No widgets found in explore response. This might be due to Google blocking the request, invalid parameters, or network issues.') };
256
+ }
257
+ // Step 2: Find the related topics widget or use any available widget
258
+ const relatedTopicsWidget = exploreResponse.widgets.find(widget => widget.id === 'RELATED_TOPICS' ||
259
+ widget.request?.restriction?.complexKeywordsRestriction?.keyword?.[0]?.value === keyword) || exploreResponse.widgets[0]; // Fallback to first widget if no specific one found
260
+ if (!relatedTopicsWidget) {
261
+ return { error: new ParseError('No related topics widget found in explore response') };
262
+ }
263
+ // Step 3: Call the related topics API with or without token
264
+ const options = {
265
+ ...GOOGLE_TRENDS_MAPPER[GoogleTrendsEndpoints.relatedTopics],
266
+ qs: {
267
+ hl,
268
+ tz: '240',
269
+ req: JSON.stringify({
270
+ restriction: {
271
+ geo: { country: geo },
272
+ time: time,
273
+ originalTimeRangeForExploreUrl: time,
274
+ complexKeywordsRestriction: {
275
+ keyword: [{
276
+ type: 'BROAD',
277
+ value: keyword
278
+ }]
279
+ }
280
+ },
281
+ keywordType: 'ENTITY',
282
+ metric: ['TOP', 'RISING'],
283
+ trendinessSettings: {
284
+ compareTime: time
285
+ },
286
+ requestOptions: {
287
+ property: property,
288
+ backend: 'CM',
289
+ category: category
290
+ },
291
+ language: hl.split('-')[0],
292
+ userCountryCode: geo,
293
+ userConfig: {
294
+ userType: 'USER_TYPE_LEGIT_USER'
295
+ }
296
+ }),
297
+ ...(relatedTopicsWidget.token && { token: relatedTopicsWidget.token })
247
298
  }
248
299
  };
300
+ const response = await request(options.url, options);
301
+ const text = await response.text();
302
+ // Parse the response
303
+ try {
304
+ const data = JSON.parse(text.slice(5));
305
+ // Return the data in the expected format
306
+ return {
307
+ data: {
308
+ default: {
309
+ rankedList: data.default?.rankedList || []
310
+ }
311
+ }
312
+ };
313
+ }
314
+ catch (parseError) {
315
+ if (parseError instanceof Error) {
316
+ return { error: new ParseError(`Failed to parse related topics response: ${parseError.message}`) };
317
+ }
318
+ return { error: new ParseError('Failed to parse related topics response') };
319
+ }
249
320
  }
250
321
  catch (error) {
251
322
  if (error instanceof Error) {
@@ -1,19 +1,29 @@
1
- export declare const dailyTrends: ({ geo, lang }: import("./types").DailyTrendingTopicsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
2
- export declare const realTimeTrends: ({ geo, trendingHours }: import("./types").RealTimeTrendsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
3
- export declare const autocomplete: (keyword: string, hl?: string) => Promise<import("./types").GoogleTrendsResponse<string[]>>;
4
- export declare const explore: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").ExploreResponse>;
5
- export declare const interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types").InterestByRegionOptions) => Promise<import("./types").InterestByRegionResponse>;
6
- export declare const relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedTopicsResponse>>;
7
- export declare const relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedQueriesResponse>>;
8
- export declare const relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedData>>;
1
+ import { GoogleTrendsApi } from './helpers/googleTrendsAPI.js';
2
+ export declare const dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
3
+ export declare const realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
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 | {
6
+ error: import("./types/index.js").GoogleTrendsError;
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>>;
12
+ 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
+ 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
+ export { GoogleTrendsApi };
9
15
  declare const _default: {
10
- dailyTrends: ({ geo, lang }: import("./types").DailyTrendingTopicsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
11
- realTimeTrends: ({ geo, trendingHours }: import("./types").RealTimeTrendsOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").DailyTrendingTopics>>;
12
- autocomplete: (keyword: string, hl?: string) => Promise<import("./types").GoogleTrendsResponse<string[]>>;
13
- explore: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").ExploreResponse>;
14
- interestByRegion: ({ keyword, startTime, endTime, geo, resolution, hl, timezone, category }: import("./types").InterestByRegionOptions) => Promise<import("./types").InterestByRegionResponse>;
15
- relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedTopicsResponse>>;
16
- relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedQueriesResponse>>;
17
- relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types").ExploreOptions) => Promise<import("./types").GoogleTrendsResponse<import("./types").RelatedData>>;
16
+ dailyTrends: ({ geo, lang }: import("./types/index.js").DailyTrendingTopicsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
17
+ realTimeTrends: ({ geo, trendingHours }: import("./types/index.js").RealTimeTrendsOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").DailyTrendingTopics>>;
18
+ 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 | {
23
+ error: import("./types/index.js").GoogleTrendsError;
24
+ }>;
25
+ relatedTopics: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedTopicsResponse>>;
26
+ relatedQueries: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedQueriesResponse>>;
27
+ relatedData: ({ keyword, geo, time, category, property, hl, }: import("./types/index.js").ExploreOptions) => Promise<import("./types/index.js").GoogleTrendsResponse<import("./types/index.js").RelatedData>>;
18
28
  };
19
29
  export default _default;
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GoogleTrendsApi } from './helpers/googleTrendsAPI';
1
+ import { GoogleTrendsApi } from './helpers/googleTrendsAPI.js';
2
2
  const api = new GoogleTrendsApi();
3
3
  export const dailyTrends = api.dailyTrends.bind(api);
4
4
  export const realTimeTrends = api.realTimeTrends.bind(api);
@@ -8,6 +8,7 @@ export const interestByRegion = api.interestByRegion.bind(api);
8
8
  export const relatedTopics = api.relatedTopics.bind(api);
9
9
  export const relatedQueries = api.relatedQueries.bind(api);
10
10
  export const relatedData = api.relatedData.bind(api);
11
+ export { GoogleTrendsApi };
11
12
  // Default export for CommonJS compatibility
12
13
  export default {
13
14
  dailyTrends,
@@ -0,0 +1 @@
1
+ {"type":"module"}
package/package.json CHANGED
@@ -1,24 +1,30 @@
1
1
  {
2
2
  "name": "@shaivpidadi/trends-js",
3
- "version": "0.0.0-beta.8",
3
+ "version": "1.0.1",
4
4
  "description": "Google Trends API for Node.js",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
7
- "types": "./dist/types/index.d.ts",
7
+ "types": "./dist/cjs/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/esm/index.js",
11
- "require": "./dist/cjs/index.js",
12
- "types": "./dist/types/index.d.ts"
10
+ "import": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "default": "./dist/esm/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/cjs/index.d.ts",
16
+ "default": "./dist/cjs/index.js"
17
+ }
13
18
  }
14
19
  },
15
20
  "files": [
16
21
  "dist"
17
22
  ],
18
23
  "scripts": {
19
- "build": "npm run build:esm && npm run build:cjs",
24
+ "build": "npm run build:esm && npm run build:cjs && npm run build:pkg",
20
25
  "build:esm": "tsc -p tsconfig.json --outDir dist/esm",
21
- "build:cjs": "tsc -p tsconfig.json --outDir dist/cjs --module commonjs",
26
+ "build:cjs": "tsc -p tsconfig.json --outDir dist/cjs --module commonjs --moduleResolution node",
27
+ "build:pkg": "echo '{\"type\":\"module\"}' > dist/esm/package.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
22
28
  "test": "jest",
23
29
  "prepare": "npm run build"
24
30
  },
@@ -36,7 +42,7 @@
36
42
  "devDependencies": {
37
43
  "@types/jest": "^29.5.0",
38
44
  "@types/node": "^20.0.0",
39
- "jest": "^29.5.0",
45
+ "jest": "^29.7.0",
40
46
  "ts-jest": "^29.1.0",
41
47
  "typescript": "^5.0.0"
42
48
  }