iqair-api 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 ADDED
@@ -0,0 +1,104 @@
1
+ # iqair-api
2
+
3
+ Fully typed TypeScript SDK for scraping IQAir air quality data.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add iqair-api
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createIQAirClient } from "iqair-api";
15
+
16
+ const client = createIQAirClient();
17
+
18
+ async function main() {
19
+ const dataSource = "central-pollution-control-board";
20
+
21
+ const stations = await client.getStations({ dataSource });
22
+ console.log(`Found ${stations.length} stations`);
23
+
24
+ const nearest = await client.getNearestStation({
25
+ lat: 28.6139,
26
+ lng: 77.209,
27
+ dataSource,
28
+ });
29
+ console.log("Nearest station:", nearest);
30
+
31
+ if (nearest) {
32
+ const aqi = await client.getStationAQI({ stationUrl: nearest.url });
33
+ console.log("Station AQI:", aqi);
34
+ }
35
+
36
+ const locationAQI = await client.getAQIForLocation({
37
+ lat: 28.6139,
38
+ lng: 77.209,
39
+ dataSource,
40
+ });
41
+ console.log("Location AQI:", locationAQI);
42
+ }
43
+
44
+ main().catch(console.error);
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `createIQAirClient(config?)`
50
+
51
+ | Option | Type | Required | Description |
52
+ |--------|------|----------|-------------|
53
+ | `baseUrl` | `string` | No | IQAir base URL (default: `https://www.iqair.com`) |
54
+ | `locale` | `string` | No | Locale path segment (default: `in-en`) |
55
+ | `userAgent` | `string` | No | Custom user agent string |
56
+
57
+ ### Methods
58
+
59
+ | Method | Description |
60
+ |--------|-------------|
61
+ | `getStations(params)` | Get all stations for a data source |
62
+ | `getStationAQI(params)` | Get AQI data for a specific station |
63
+ | `getNearestStation(params)` | Find the nearest station to coordinates |
64
+ | `getNearestStations(params)` | Find nearest stations to coordinates (sorted) |
65
+ | `getAQIForLocation(params)` | Get AQI for the nearest station to coordinates |
66
+
67
+ ### Types
68
+
69
+ ```typescript
70
+ import type {
71
+ Station,
72
+ StationWithDistance,
73
+ StationDetails,
74
+ StationItem,
75
+ AQIResponse,
76
+ CurrentConditions,
77
+ Pollutant,
78
+ HistoricalDataPoint,
79
+ ForecastItem,
80
+ GetStationsParams,
81
+ GetStationAQIParams,
82
+ GetNearestStationParams,
83
+ GetNearestStationsParams,
84
+ GetAQIForLocationParams,
85
+ } from "iqair-api";
86
+ ```
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ bun install
92
+ bun run build
93
+ bun test
94
+ bun test:unit
95
+ bun test:e2e
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
101
+
102
+ ## Disclaimer
103
+
104
+ This is an **unofficial** API client and is not affiliated with, endorsed by, or associated with IQAir or its parent organization. This package is provided for educational and informational purposes under fair use. Accessing publicly available air quality data is lawful and serves the public interest.
@@ -0,0 +1,284 @@
1
+ //#region src/client/iqair-client.config.d.ts
2
+ interface IQAirClientConfig {
3
+ baseUrl?: string;
4
+ locale?: string;
5
+ userAgent?: string;
6
+ }
7
+ //#endregion
8
+ //#region src/types/station/station.interface.d.ts
9
+ interface Station {
10
+ id: string;
11
+ name: string;
12
+ url: string;
13
+ latitude: number;
14
+ longitude: number;
15
+ city?: string;
16
+ country?: string;
17
+ }
18
+ //#endregion
19
+ //#region src/types/station/station-with-distance.interface.d.ts
20
+ interface StationWithDistance extends Station {
21
+ distance: number;
22
+ }
23
+ //#endregion
24
+ //#region src/types/station/station-item.interface.d.ts
25
+ interface StationItem {
26
+ id: string;
27
+ name: string;
28
+ url: string;
29
+ coordinates: {
30
+ latitude: number;
31
+ longitude: number;
32
+ };
33
+ current: {
34
+ aqi: {
35
+ value: number;
36
+ color: string;
37
+ label: string;
38
+ };
39
+ ts: string;
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/types/aqi/pollutant.interface.d.ts
44
+ interface Pollutant {
45
+ unit: string;
46
+ description: string;
47
+ aqi: number;
48
+ concentration: number;
49
+ pollutantName: string;
50
+ }
51
+ //#endregion
52
+ //#region src/types/aqi/current-conditions.interface.d.ts
53
+ interface CurrentConditions {
54
+ ts: string;
55
+ aqi: number;
56
+ mainPollutant: string;
57
+ concentration: number;
58
+ estimated: boolean;
59
+ condition?: string;
60
+ icon?: string;
61
+ humidity?: number;
62
+ pressure?: number;
63
+ temperature?: number;
64
+ wind?: {
65
+ speed: number;
66
+ direction: number;
67
+ };
68
+ pollutants?: Pollutant[];
69
+ aqiDescription?: string;
70
+ }
71
+ //#endregion
72
+ //#region src/types/forecast/forecast-item.interface.d.ts
73
+ interface ForecastItem {
74
+ ts: string;
75
+ aqi?: number;
76
+ pressure?: number;
77
+ humidity?: number;
78
+ wind?: {
79
+ speed: number;
80
+ direction: number;
81
+ };
82
+ icon?: string;
83
+ condition?: string;
84
+ temperature?: number | {
85
+ max: number;
86
+ min: number;
87
+ };
88
+ }
89
+ //#endregion
90
+ //#region src/types/station/station-details.interface.d.ts
91
+ interface StationDetails {
92
+ id: string;
93
+ name: string;
94
+ country: string;
95
+ state: string;
96
+ type: string;
97
+ timezone: string;
98
+ coordinates: {
99
+ latitude: number;
100
+ longitude: number;
101
+ };
102
+ current: CurrentConditions;
103
+ forecasts?: {
104
+ hourly: ForecastItem[];
105
+ daily: ForecastItem[];
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/types/history/historical-data-point.interface.d.ts
110
+ interface HistoricalDataPoint {
111
+ ts: string;
112
+ aqi?: number;
113
+ pm25?: {
114
+ aqi: number;
115
+ concentration: number;
116
+ };
117
+ pm10?: {
118
+ aqi: number;
119
+ concentration: number;
120
+ };
121
+ o3?: {
122
+ aqi: number;
123
+ concentration: number;
124
+ };
125
+ no2?: {
126
+ aqi: number;
127
+ concentration: number;
128
+ };
129
+ so2?: {
130
+ aqi: number;
131
+ concentration: number;
132
+ };
133
+ co?: {
134
+ aqi: number;
135
+ concentration: number;
136
+ };
137
+ pressure?: number;
138
+ humidity?: number;
139
+ temperature?: number | {
140
+ max: number;
141
+ min: number;
142
+ };
143
+ wind?: {
144
+ speed: number;
145
+ direction: number;
146
+ };
147
+ condition?: string;
148
+ icon?: string;
149
+ }
150
+ //#endregion
151
+ //#region src/types/aqi/aqi-response.interface.d.ts
152
+ interface AQIResponse {
153
+ station: {
154
+ id: string;
155
+ name: string;
156
+ url: string;
157
+ distance: number;
158
+ };
159
+ current: {
160
+ ts: string;
161
+ aqi: number;
162
+ mainPollutant?: string;
163
+ concentration?: number;
164
+ condition?: string;
165
+ temperature?: number;
166
+ humidity?: number;
167
+ pressure?: number;
168
+ wind?: {
169
+ speed: number;
170
+ direction: number;
171
+ };
172
+ };
173
+ pollutants?: Array<{
174
+ name: string;
175
+ aqi: number;
176
+ concentration: number;
177
+ unit: string;
178
+ }>;
179
+ historical?: {
180
+ hourly: HistoricalDataPoint[];
181
+ daily: HistoricalDataPoint[];
182
+ };
183
+ }
184
+ //#endregion
185
+ //#region src/types/params/get-stations.params.d.ts
186
+ interface GetStationsParams {
187
+ dataSource: string;
188
+ limit?: number;
189
+ }
190
+ //#endregion
191
+ //#region src/types/params/get-station-aqi.params.d.ts
192
+ interface GetStationAQIParams {
193
+ stationUrl: string;
194
+ }
195
+ //#endregion
196
+ //#region src/types/params/get-nearest-station.params.d.ts
197
+ interface GetNearestStationParams {
198
+ lat: number;
199
+ lng: number;
200
+ dataSource: string;
201
+ }
202
+ //#endregion
203
+ //#region src/types/params/get-nearest-stations.params.d.ts
204
+ interface GetNearestStationsParams {
205
+ lat: number;
206
+ lng: number;
207
+ dataSource: string;
208
+ limit?: number;
209
+ }
210
+ //#endregion
211
+ //#region src/types/params/get-aqi-for-location.params.d.ts
212
+ interface GetAQIForLocationParams {
213
+ lat: number;
214
+ lng: number;
215
+ dataSource: string;
216
+ }
217
+ //#endregion
218
+ //#region src/types/internal/station-list-response.interface.d.ts
219
+ interface StationListData {
220
+ profileStations?: {
221
+ data: StationItem[];
222
+ response: Record<string, unknown>;
223
+ };
224
+ stationsMapData?: {
225
+ data: StationItem[];
226
+ response: Record<string, unknown>;
227
+ };
228
+ }
229
+ interface StationListResponse {
230
+ "routes/$(locale)": {
231
+ data: StationListData;
232
+ };
233
+ "routes/$(locale).profile.$slug": {
234
+ data: StationListData;
235
+ };
236
+ }
237
+ //#endregion
238
+ //#region src/types/internal/individual-station-response.interface.d.ts
239
+ interface IndividualStationData {
240
+ details?: StationDetails;
241
+ measurements?: {
242
+ data?: {
243
+ measurements?: {
244
+ hourly?: HistoricalDataPoint[];
245
+ daily?: HistoricalDataPoint[];
246
+ };
247
+ };
248
+ };
249
+ }
250
+ interface IndividualStationResponse {
251
+ root: {
252
+ data: IndividualStationData;
253
+ };
254
+ "routes/$": {
255
+ data: IndividualStationData;
256
+ };
257
+ }
258
+ //#endregion
259
+ //#region src/client/iqair-client.d.ts
260
+ declare class IQAirClient {
261
+ private readonly baseUrl;
262
+ private readonly locale;
263
+ private readonly userAgent;
264
+ constructor(config?: IQAirClientConfig);
265
+ private request;
266
+ getStations(params: GetStationsParams): Promise<Station[]>;
267
+ getStationAQI(params: GetStationAQIParams): Promise<AQIResponse>;
268
+ getNearestStation(params: GetNearestStationParams): Promise<StationWithDistance | null>;
269
+ getNearestStations(params: GetNearestStationsParams): Promise<StationWithDistance[]>;
270
+ getAQIForLocation(params: GetAQIForLocationParams): Promise<AQIResponse | null>;
271
+ }
272
+ //#endregion
273
+ //#region src/client/create-iqair-client.d.ts
274
+ declare function createIQAirClient(config?: IQAirClientConfig): IQAirClient;
275
+ //#endregion
276
+ //#region src/exceptions/iqair.exception.d.ts
277
+ declare class IQAirException extends Error {
278
+ readonly statusCode: number;
279
+ readonly body?: string;
280
+ constructor(message: string, statusCode: number, body?: string);
281
+ }
282
+ //#endregion
283
+ export { AQIResponse, CurrentConditions, ForecastItem, GetAQIForLocationParams, GetNearestStationParams, GetNearestStationsParams, GetStationAQIParams, GetStationsParams, HistoricalDataPoint, IQAirClient, type IQAirClientConfig, IQAirException, IndividualStationData, IndividualStationResponse, Pollutant, Station, StationDetails, StationItem, StationListData, StationListResponse, StationWithDistance, createIQAirClient };
284
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client/iqair-client.config.ts","../src/types/station/station.interface.ts","../src/types/station/station-with-distance.interface.ts","../src/types/station/station-item.interface.ts","../src/types/aqi/pollutant.interface.ts","../src/types/aqi/current-conditions.interface.ts","../src/types/forecast/forecast-item.interface.ts","../src/types/station/station-details.interface.ts","../src/types/history/historical-data-point.interface.ts","../src/types/aqi/aqi-response.interface.ts","../src/types/params/get-stations.params.ts","../src/types/params/get-station-aqi.params.ts","../src/types/params/get-nearest-station.params.ts","../src/types/params/get-nearest-stations.params.ts","../src/types/params/get-aqi-for-location.params.ts","../src/types/internal/station-list-response.interface.ts","../src/types/internal/individual-station-response.interface.ts","../src/client/iqair-client.ts","../src/client/create-iqair-client.ts","../src/exceptions/iqair.exception.ts"],"sourcesContent":[],"mappings":";UAAiB,iBAAA;EAAA,OAAA,CAAA,EAAA,MAAA;;;;;;UCAA,OAAA;EDAA,EAAA,EAAA,MAAA;;;;ECAA,SAAA,EAAO,MAAA;;;;;;ADAP,UEEA,mBAAA,SAA4B,OFFX,CAAA;;;;;UGAjB,WAAA;EHAA,EAAA,EAAA,MAAA;;;;ICAA,QAAO,EAAA,MAAA;;;;ICEP,GAAA,EAAA;;;;ICFA,CAAA;;;;;;UCAA,SAAA;EJAA,IAAA,EAAA,MAAA;;;;ECAA,aAAO,EAAA,MAAA;;;;ADAP,UKEA,iBAAA,CLFiB;;;;ECAjB,aAAO,EAAA,MAAA;;;;ECEP,QAAA,CAAA,EAAA,MAAA;;;;ICFA,KAAA,EAAA,MAAW;;;eEiBb;EDjBE,cAAS,CAAA,EAAA,MAAA;;;;UEAT,YAAA;ENAA,EAAA,EAAA,MAAA;;;;ECAA,IAAA,CAAA,EAAA;;;;ECEA,IAAA,CAAA,EAAA,MAAA;;;;ICFA,GAAA,EAAA,MAAW;;;;;UIGX,cAAA;;;ENHA,OAAA,EAAA,MAAO;;;;ECEP,WAAA,EAAA;;;;ECFA,OAAA,EIcN,iBJdiB;;YIgBhB;WACD;EHjBM,CAAA;;;;UIAA,mBAAA;ERAA,EAAA,EAAA,MAAA;;;;ICAA,aAAO,EAAA,MAAA;;;;ICEP,aAAA,EAAA,MAAoB;;;;ICFpB,aAAW,EAAA,MAAA;;;;ICAX,aAAS,EAAA,MAAA;;;;ICET,aAAA,EAAA,MAAiB;;;;ICFjB,aAAY,EAAA,MAAA;;;;ECGZ,WAAA,CAAA,EAAA,MAAc,GAAA;IAWpB,GAAA,EAAA,MAAA;IAEC,GAAA,EAAA,MAAA;EACD,CAAA;EAAY,IAAA,CAAA,EAAA;;;;ECjBN,SAAA,CAAA,EAAA,MAAA;;;;;ARAA,USEA,WAAA,CTFiB;;;;ICAjB,GAAA,EAAA,MAAO;;;;ICEP,EAAA,EAAA,MAAA;;;;ICFA,SAAA,CAAW,EAAA,MAAA;;;;ICAX,IAAA,CAAA,EAAA;;;;ECEA,CAAA;eIqBF;;;IHvBE,aAAY,EAAA,MAAA;;;;ICGZ,MAAA,EE2BL,mBF3BmB,EAAA;IAWpB,KAAA,EEiBA,mBFjBA,EAAA;EAEC,CAAA;;;;UGhBK,iBAAA;EVAA,UAAA,EAAA,MAAA;;;;;UWAA,mBAAA;EXAA,UAAA,EAAA,MAAA;;;;UYAA,uBAAA;EZAA,GAAA,EAAA,MAAA;;;;;;UaAA,wBAAA;EbAA,GAAA,EAAA,MAAA;;;;ACAjB;;;UaAiB,uBAAA;EdAA,GAAA,EAAA,MAAA;;;;;;AAAA,UeEA,eAAA,CfFiB;;UeIxB;cACI;EdLG,CAAA;;UcQP;cACI;EbPG,CAAA;;UaWA,mBAAA;;IZbA,IAAA,EYeP,eZfkB;;;UYkBlB;EXlBO,CAAA;;;;UYGA,qBAAA;YACL;;IfJK,IAAA,CAAA,EAAO;;iBeQP;gBACD;MdPC,CAAA;;;;ACFA,UaeA,yBAAA,CbfW;;UaiBlB;;EZjBO,UAAA,EAAS;UYoBhB;;;;;cCKG,WAAA;;;EhBzBI,iBAAO,SAAA;uBgB8BF;;sBAUM,oBAAoB,QAAQ;EftCvC,aAAA,CAAA,MAAA,EeiEa,mBfjEsB,CAAA,EeiEA,OfjEA,CeiEQ,WfjER,CAAA;4BeuHxC,0BACP,QAAQ;6BAMD,2BACP,QAAQ;4BAMD,0BACP,QAAQ;AdxIb;;;iBeGgB,iBAAA,UAA2B,oBAAoB;;;cCHlD,cAAA,SAAuB,KAAA;EnBAnB,SAAA,UAAA,EAAiB,MAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,206 @@
1
+ import { UNSAFE_decodeViaTurboStream } from "react-router";
2
+
3
+ //#region src/constants/base-url.constant.ts
4
+ const DEFAULT_BASE_URL = "https://www.iqair.com";
5
+
6
+ //#endregion
7
+ //#region src/constants/locale.constant.ts
8
+ const DEFAULT_LOCALE = "in-en";
9
+
10
+ //#endregion
11
+ //#region src/constants/routes.constant.ts
12
+ const STATION_LIST_ROUTES = encodeURIComponent("routes/$(locale),routes/$(locale).profile.$slug");
13
+ const INDIVIDUAL_STATION_ROUTES = encodeURIComponent("routes/$");
14
+
15
+ //#endregion
16
+ //#region src/constants/user-agent.constant.ts
17
+ const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36";
18
+
19
+ //#endregion
20
+ //#region src/exceptions/iqair.exception.ts
21
+ var IQAirException = class extends Error {
22
+ statusCode;
23
+ body;
24
+ constructor(message, statusCode, body) {
25
+ super(message);
26
+ this.name = "IQAirException";
27
+ this.statusCode = statusCode;
28
+ this.body = body;
29
+ }
30
+ };
31
+
32
+ //#endregion
33
+ //#region src/utils/decoder.util.ts
34
+ async function resolveAllPromises(obj) {
35
+ if (obj instanceof Promise) return resolveAllPromises(await obj);
36
+ if (Array.isArray(obj)) return await Promise.all(obj.map(resolveAllPromises));
37
+ if (obj !== null && typeof obj === "object") {
38
+ const entries = Object.entries(obj);
39
+ const resolvedEntries = await Promise.all(entries.map(async ([key, value]) => [key, await resolveAllPromises(value)]));
40
+ return Object.fromEntries(resolvedEntries);
41
+ }
42
+ return obj;
43
+ }
44
+ async function decodeTurboStream(stream) {
45
+ const decoded = await UNSAFE_decodeViaTurboStream(stream, globalThis);
46
+ await decoded.done;
47
+ return await resolveAllPromises(decoded.value);
48
+ }
49
+ async function fetchAndDecode(url, userAgent) {
50
+ const response = await fetch(url, { headers: {
51
+ "User-Agent": userAgent,
52
+ Accept: "*/*",
53
+ "Accept-Language": "en-US,en;q=0.9"
54
+ } });
55
+ if (!response.ok) {
56
+ const text = await response.text();
57
+ throw new IQAirException(`Request failed: ${response.status} ${response.statusText}`, response.status, text);
58
+ }
59
+ if (!response.body) throw new IQAirException("Response body is null", 500);
60
+ return decodeTurboStream(response.body);
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/utils/geo.util.ts
65
+ const EARTH_RADIUS_KM = 6371;
66
+ function toRadians(degrees) {
67
+ return degrees * (Math.PI / 180);
68
+ }
69
+ function haversineDistance(lat1, lon1, lat2, lon2) {
70
+ const dLat = toRadians(lat2 - lat1);
71
+ const dLon = toRadians(lon2 - lon1);
72
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
73
+ return EARTH_RADIUS_KM * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
74
+ }
75
+ function findNearestStation(latitude, longitude, stations) {
76
+ if (stations.length === 0) return null;
77
+ let nearest = null;
78
+ let minDistance = Infinity;
79
+ for (const station of stations) {
80
+ const distance = haversineDistance(latitude, longitude, station.latitude, station.longitude);
81
+ if (distance < minDistance) {
82
+ minDistance = distance;
83
+ nearest = {
84
+ ...station,
85
+ distance
86
+ };
87
+ }
88
+ }
89
+ return nearest;
90
+ }
91
+ function findNearestStations(latitude, longitude, stations, limit = 5) {
92
+ const withDistances = stations.map((station) => ({
93
+ ...station,
94
+ distance: haversineDistance(latitude, longitude, station.latitude, station.longitude)
95
+ }));
96
+ withDistances.sort((a, b) => a.distance - b.distance);
97
+ return withDistances.slice(0, limit);
98
+ }
99
+
100
+ //#endregion
101
+ //#region src/utils/build-url.util.ts
102
+ function buildUrl(baseUrl, path, params) {
103
+ const url = new URL(baseUrl + path);
104
+ if (params) Object.entries(params).forEach(([key, value]) => {
105
+ if (value !== void 0) url.searchParams.set(key, String(value));
106
+ });
107
+ return url;
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/client/iqair-client.ts
112
+ var IQAirClient = class {
113
+ baseUrl;
114
+ locale;
115
+ userAgent;
116
+ constructor(config = {}) {
117
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
118
+ this.locale = config.locale ?? DEFAULT_LOCALE;
119
+ this.userAgent = config.userAgent ?? DEFAULT_USER_AGENT;
120
+ }
121
+ async request(url) {
122
+ return fetchAndDecode(url, this.userAgent);
123
+ }
124
+ async getStations(params) {
125
+ const url = buildUrl(this.baseUrl, `/${this.locale}/profile/${params.dataSource}.data`, {
126
+ limit: params.limit ?? 500,
127
+ _routes: STATION_LIST_ROUTES
128
+ });
129
+ const stationsData = ((await this.request(url.toString()))["routes/$(locale).profile.$slug"]?.data)?.stationsMapData?.data;
130
+ if (!stationsData || stationsData.length === 0) return [];
131
+ return stationsData.map((station) => ({
132
+ id: station.id,
133
+ name: station.name,
134
+ url: station.url,
135
+ latitude: station.coordinates.latitude,
136
+ longitude: station.coordinates.longitude
137
+ }));
138
+ }
139
+ async getStationAQI(params) {
140
+ const url = buildUrl(this.baseUrl, `/${this.locale}${params.stationUrl}.data`, { _routes: INDIVIDUAL_STATION_ROUTES });
141
+ const data = await this.request(url.toString());
142
+ const routeData = data.root?.data || data["routes/$"]?.data;
143
+ const details = routeData?.details;
144
+ if (!details || !details.current) throw new IQAirException("No station details found in response", 404);
145
+ const measurements = routeData?.measurements?.data?.measurements;
146
+ const historical = measurements ? {
147
+ hourly: measurements.hourly || [],
148
+ daily: measurements.daily || []
149
+ } : void 0;
150
+ return {
151
+ station: {
152
+ id: details.id,
153
+ name: details.name,
154
+ url: params.stationUrl,
155
+ distance: 0
156
+ },
157
+ current: {
158
+ ts: details.current.ts,
159
+ aqi: details.current.aqi,
160
+ mainPollutant: details.current.mainPollutant,
161
+ concentration: details.current.concentration,
162
+ condition: details.current.condition,
163
+ temperature: details.current.temperature,
164
+ humidity: details.current.humidity,
165
+ pressure: details.current.pressure,
166
+ wind: details.current.wind
167
+ },
168
+ pollutants: details.current.pollutants?.map((p) => ({
169
+ name: p.pollutantName,
170
+ aqi: p.aqi,
171
+ concentration: p.concentration,
172
+ unit: p.unit
173
+ })),
174
+ historical
175
+ };
176
+ }
177
+ async getNearestStation(params) {
178
+ const stations = await this.getStations({ dataSource: params.dataSource });
179
+ return findNearestStation(params.lat, params.lng, stations);
180
+ }
181
+ async getNearestStations(params) {
182
+ const stations = await this.getStations({ dataSource: params.dataSource });
183
+ return findNearestStations(params.lat, params.lng, stations, params.limit);
184
+ }
185
+ async getAQIForLocation(params) {
186
+ const nearestStation = await this.getNearestStation({
187
+ lat: params.lat,
188
+ lng: params.lng,
189
+ dataSource: params.dataSource
190
+ });
191
+ if (!nearestStation) return null;
192
+ const aqi = await this.getStationAQI({ stationUrl: nearestStation.url });
193
+ aqi.station.distance = nearestStation.distance;
194
+ return aqi;
195
+ }
196
+ };
197
+
198
+ //#endregion
199
+ //#region src/client/create-iqair-client.ts
200
+ function createIQAirClient(config) {
201
+ return new IQAirClient(config);
202
+ }
203
+
204
+ //#endregion
205
+ export { IQAirClient, IQAirException, createIQAirClient };
206
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["nearest: StationWithDistance | null","withDistances: StationWithDistance[]"],"sources":["../src/constants/base-url.constant.ts","../src/constants/locale.constant.ts","../src/constants/routes.constant.ts","../src/constants/user-agent.constant.ts","../src/exceptions/iqair.exception.ts","../src/utils/decoder.util.ts","../src/utils/geo.util.ts","../src/utils/build-url.util.ts","../src/client/iqair-client.ts","../src/client/create-iqair-client.ts"],"sourcesContent":["export const DEFAULT_BASE_URL = \"https://www.iqair.com\";\n","export const DEFAULT_LOCALE = \"in-en\";\n","export const STATION_LIST_ROUTES = encodeURIComponent(\n \"routes/$(locale),routes/$(locale).profile.$slug\"\n);\n\nexport const INDIVIDUAL_STATION_ROUTES = encodeURIComponent(\"routes/$\");\n","export const DEFAULT_USER_AGENT =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36\";\n","export class IQAirException extends Error {\n readonly statusCode: number;\n readonly body?: string;\n\n constructor(message: string, statusCode: number, body?: string) {\n super(message);\n this.name = \"IQAirException\";\n this.statusCode = statusCode;\n this.body = body;\n }\n}\n","import { UNSAFE_decodeViaTurboStream } from \"react-router\";\nimport { IQAirException } from \"../exceptions\";\n\nasync function resolveAllPromises<T>(obj: T): Promise<T> {\n if (obj instanceof Promise) {\n const resolved = await obj;\n return resolveAllPromises(resolved);\n }\n\n if (Array.isArray(obj)) {\n const resolved = await Promise.all(obj.map(resolveAllPromises));\n return resolved as T;\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const entries = Object.entries(obj);\n const resolvedEntries = await Promise.all(\n entries.map(async ([key, value]) => [key, await resolveAllPromises(value)])\n );\n return Object.fromEntries(resolvedEntries) as T;\n }\n\n return obj;\n}\n\nexport async function decodeTurboStream<T>(\n stream: ReadableStream<Uint8Array>\n): Promise<T> {\n const decoded = await UNSAFE_decodeViaTurboStream(stream, globalThis);\n await decoded.done;\n const resolved = await resolveAllPromises(decoded.value);\n return resolved as T;\n}\n\nexport async function fetchAndDecode<T>(url: string, userAgent: string): Promise<T> {\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": userAgent,\n Accept: \"*/*\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n },\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new IQAirException(\n `Request failed: ${response.status} ${response.statusText}`,\n response.status,\n text\n );\n }\n\n if (!response.body) {\n throw new IQAirException(\"Response body is null\", 500);\n }\n\n return decodeTurboStream<T>(response.body);\n}\n","import type { Station, StationWithDistance } from \"../types\";\n\nconst EARTH_RADIUS_KM = 6371;\n\nfunction toRadians(degrees: number): number {\n return degrees * (Math.PI / 180);\n}\n\nexport function haversineDistance(\n lat1: number,\n lon1: number,\n lat2: number,\n lon2: number\n): number {\n const dLat = toRadians(lat2 - lat1);\n const dLon = toRadians(lon2 - lon1);\n\n const a =\n Math.sin(dLat / 2) * Math.sin(dLat / 2) +\n Math.cos(toRadians(lat1)) *\n Math.cos(toRadians(lat2)) *\n Math.sin(dLon / 2) *\n Math.sin(dLon / 2);\n\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\n return EARTH_RADIUS_KM * c;\n}\n\nexport function findNearestStation(\n latitude: number,\n longitude: number,\n stations: Station[]\n): StationWithDistance | null {\n if (stations.length === 0) {\n return null;\n }\n\n let nearest: StationWithDistance | null = null;\n let minDistance = Infinity;\n\n for (const station of stations) {\n const distance = haversineDistance(\n latitude,\n longitude,\n station.latitude,\n station.longitude\n );\n\n if (distance < minDistance) {\n minDistance = distance;\n nearest = { ...station, distance };\n }\n }\n\n return nearest;\n}\n\nexport function findNearestStations(\n latitude: number,\n longitude: number,\n stations: Station[],\n limit: number = 5\n): StationWithDistance[] {\n const withDistances: StationWithDistance[] = stations.map((station) => ({\n ...station,\n distance: haversineDistance(\n latitude,\n longitude,\n station.latitude,\n station.longitude\n ),\n }));\n\n withDistances.sort((a, b) => a.distance - b.distance);\n\n return withDistances.slice(0, limit);\n}\n","export function buildUrl(\n baseUrl: string,\n path: string,\n params?: Record<string, string | number | undefined>\n): URL {\n const url = new URL(baseUrl + path);\n\n if (params) {\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n });\n }\n\n return url;\n}\n","import { IQAirClientConfig } from \"./iqair-client.config\";\nimport {\n DEFAULT_BASE_URL,\n DEFAULT_LOCALE,\n DEFAULT_USER_AGENT,\n STATION_LIST_ROUTES,\n INDIVIDUAL_STATION_ROUTES,\n} from \"../constants\";\nimport { IQAirException } from \"../exceptions\";\nimport { fetchAndDecode } from \"../utils/decoder.util\";\nimport { findNearestStation, findNearestStations } from \"../utils/geo.util\";\nimport { buildUrl } from \"../utils/build-url.util\";\nimport type {\n Station,\n StationWithDistance,\n AQIResponse,\n StationListResponse,\n IndividualStationResponse,\n GetStationsParams,\n GetStationAQIParams,\n GetNearestStationParams,\n GetNearestStationsParams,\n GetAQIForLocationParams,\n} from \"../types\";\n\nexport class IQAirClient {\n private readonly baseUrl: string;\n private readonly locale: string;\n private readonly userAgent: string;\n\n constructor(config: IQAirClientConfig = {}) {\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.locale = config.locale ?? DEFAULT_LOCALE;\n this.userAgent = config.userAgent ?? DEFAULT_USER_AGENT;\n }\n\n private async request<T>(url: string): Promise<T> {\n return fetchAndDecode<T>(url, this.userAgent);\n }\n\n async getStations(params: GetStationsParams): Promise<Station[]> {\n const url = buildUrl(\n this.baseUrl,\n `/${this.locale}/profile/${params.dataSource}.data`,\n {\n limit: params.limit ?? 500,\n _routes: STATION_LIST_ROUTES,\n }\n );\n\n const data = await this.request<StationListResponse>(url.toString());\n const routeData = data[\"routes/$(locale).profile.$slug\"]?.data;\n const stationsData = routeData?.stationsMapData?.data;\n\n if (!stationsData || stationsData.length === 0) {\n return [];\n }\n\n return stationsData.map((station) => ({\n id: station.id,\n name: station.name,\n url: station.url,\n latitude: station.coordinates.latitude,\n longitude: station.coordinates.longitude,\n }));\n }\n\n async getStationAQI(params: GetStationAQIParams): Promise<AQIResponse> {\n const url = buildUrl(\n this.baseUrl,\n `/${this.locale}${params.stationUrl}.data`,\n {\n _routes: INDIVIDUAL_STATION_ROUTES,\n }\n );\n\n const data = await this.request<IndividualStationResponse>(url.toString());\n const routeData = data.root?.data || data[\"routes/$\"]?.data;\n const details = routeData?.details;\n\n if (!details || !details.current) {\n throw new IQAirException(\"No station details found in response\", 404);\n }\n\n const measurements = routeData?.measurements?.data?.measurements;\n const historical = measurements\n ? {\n hourly: measurements.hourly || [],\n daily: measurements.daily || [],\n }\n : undefined;\n\n return {\n station: {\n id: details.id,\n name: details.name,\n url: params.stationUrl,\n distance: 0,\n },\n current: {\n ts: details.current.ts,\n aqi: details.current.aqi,\n mainPollutant: details.current.mainPollutant,\n concentration: details.current.concentration,\n condition: details.current.condition,\n temperature: details.current.temperature,\n humidity: details.current.humidity,\n pressure: details.current.pressure,\n wind: details.current.wind,\n },\n pollutants: details.current.pollutants?.map((p) => ({\n name: p.pollutantName,\n aqi: p.aqi,\n concentration: p.concentration,\n unit: p.unit,\n })),\n historical,\n };\n }\n\n async getNearestStation(\n params: GetNearestStationParams\n ): Promise<StationWithDistance | null> {\n const stations = await this.getStations({ dataSource: params.dataSource });\n return findNearestStation(params.lat, params.lng, stations);\n }\n\n async getNearestStations(\n params: GetNearestStationsParams\n ): Promise<StationWithDistance[]> {\n const stations = await this.getStations({ dataSource: params.dataSource });\n return findNearestStations(params.lat, params.lng, stations, params.limit);\n }\n\n async getAQIForLocation(\n params: GetAQIForLocationParams\n ): Promise<AQIResponse | null> {\n const nearestStation = await this.getNearestStation({\n lat: params.lat,\n lng: params.lng,\n dataSource: params.dataSource,\n });\n\n if (!nearestStation) {\n return null;\n }\n\n const aqi = await this.getStationAQI({ stationUrl: nearestStation.url });\n aqi.station.distance = nearestStation.distance;\n return aqi;\n }\n}\n","import { IQAirClient } from \"./iqair-client\";\nimport { IQAirClientConfig } from \"./iqair-client.config\";\n\nexport function createIQAirClient(config?: IQAirClientConfig): IQAirClient {\n return new IQAirClient(config);\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "iqair-api",
3
+ "type": "module",
4
+ "version": "1.0.1",
5
+ "description": "Fully typed TypeScript SDK for scraping IQAir air quality data",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/neo773/iqair-api.git"
9
+ },
10
+ "author": "neo773",
11
+ "exports": {
12
+ ".": "./dist/index.mjs",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "main": "./dist/index.mjs",
16
+ "module": "./dist/index.mjs",
17
+ "types": "./dist/index.d.mts",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsdown src/index.ts",
23
+ "dev": "tsdown src/index.ts --watch",
24
+ "test": "bun test",
25
+ "test:unit": "bun test tests/unit",
26
+ "test:e2e": "bun test tests/e2e",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "keywords": [
30
+ "iqair",
31
+ "air-quality",
32
+ "aqi",
33
+ "pollution",
34
+ "pm25",
35
+ "api",
36
+ "sdk"
37
+ ],
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "react-router": "^7.6.1",
41
+ "turbo-stream": "^2.4.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "^1.3.8",
45
+ "tsdown": "^0.18.1",
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "directories": {
49
+ "example": "examples",
50
+ "test": "tests"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/neo773/iqair-api/issues"
54
+ },
55
+ "homepage": "https://github.com/neo773/iqair-api#readme"
56
+ }