encarapi 1.0.0 → 1.1.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 +18 -7
- package/package.json +2 -2
- package/src/china.js +36 -1
- package/src/index.d.ts +32 -3
- package/src/index.js +3 -3
- package/src/korea.js +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# EnCarAPI: Node.js client for Korean and Chinese used car data
|
|
2
2
|
|
|
3
|
-
Official **Node.js / TypeScript client** for [EnCarAPI](https://encarapi.com), the REST
|
|
3
|
+
Official **Node.js / TypeScript client** for [EnCarAPI](https://encarapi.com/?utm_source=readme&utm_medium=encarapi-node), the REST
|
|
4
4
|
**Encar API** and **Korean Car API**. One package for:
|
|
5
5
|
|
|
6
6
|
- **Korea:** Encar.com, KB Chachacha (`source: "kbc"`) and K Car (`source: "kcar"`), with
|
|
7
7
|
photos, specs, options, inspection reports, accident and ownership records, condition
|
|
8
8
|
reports, price history, a change feed and full catalog export.
|
|
9
|
-
- **China:** Dongchedi and Che168 in English via [ChinaCarAPI](https://chinacarapi.com)
|
|
9
|
+
- **China:** Dongchedi and Che168 in English via [ChinaCarAPI](https://chinacarapi.com/?utm_source=readme&utm_medium=encarapi-node)
|
|
10
10
|
(separate key or the China add-on for EnCarAPI keys).
|
|
11
11
|
|
|
12
12
|
Built for car exporters, dealers and marketplaces that need reliable used car data without
|
|
13
13
|
scraping, proxies or geo-blocks.
|
|
14
14
|
|
|
15
15
|
> **An API key is required.** The data is a paid service. Get a key (5-day trial) at
|
|
16
|
-
> **[encarapi.com](https://encarapi.com)**.
|
|
16
|
+
> **[encarapi.com](https://encarapi.com/?utm_source=readme&utm_medium=encarapi-node)**.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -66,6 +66,16 @@ for await (const change of client.korea.iterateChanges({ cursor: savedCursor ??
|
|
|
66
66
|
saveCursor(client.korea.lastCursor);
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
The China API works the same way (`client.china`); its events are
|
|
70
|
+
`{ id, source, vehicleId, type: "new" | "price" | "removed" | "relisted", oldPrice, newPrice, at }`:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
for await (const change of client.china.iterateChanges({ cursor: savedCursor ?? 0 })) {
|
|
74
|
+
// change.type: "new" | "price" | "removed" | "relisted"
|
|
75
|
+
}
|
|
76
|
+
saveCursor(client.china.lastCursor);
|
|
77
|
+
```
|
|
78
|
+
|
|
69
79
|
## Korea API (`client.korea`)
|
|
70
80
|
|
|
71
81
|
| Method | Endpoint | Notes |
|
|
@@ -92,10 +102,11 @@ saveCursor(client.korea.lastCursor);
|
|
|
92
102
|
| Method | Endpoint | Notes |
|
|
93
103
|
|---|---|---|
|
|
94
104
|
| `catalog(params)` | `GET /api/catalog` | `make`, `price_min/max` (CNY), `export_ready`, `sort`, ... |
|
|
105
|
+
| `iterateCatalog(params)` | `GET /api/catalog` | Async iterator over all pages (up to the 10,000-result depth limit) |
|
|
95
106
|
| `vehicle(id)` | `GET /api/vehicle/:id` | Price history, photos, seller, export status |
|
|
96
107
|
| `inspection(id)` | `GET /api/inspection/:id` | Accident, flood and fire checks, EV battery data |
|
|
97
108
|
| `bulkVehicles(ids)` | `POST /api/vehicle/bulk` | Up to 500 ids |
|
|
98
|
-
| `changes(params)` | `GET /api/catalog/changes` | Change feed |
|
|
109
|
+
| `changes(params)` / `iterateChanges(params)` | `GET /api/catalog/changes` | Change feed (Business/Scale) |
|
|
99
110
|
| `exportCsv()` | `GET /api/catalog/export` | Full catalog CSV |
|
|
100
111
|
| `enums()` / `models(make)` | `GET /api/enums`, `/api/models` | Filter values, models per make |
|
|
101
112
|
|
|
@@ -117,10 +128,10 @@ try {
|
|
|
117
128
|
|
|
118
129
|
## Links
|
|
119
130
|
|
|
120
|
-
- Website and pricing: https://encarapi.com
|
|
121
|
-
- Documentation: https://encarapi.com/documentation
|
|
131
|
+
- Website and pricing: [encarapi.com](https://encarapi.com/?utm_source=readme&utm_medium=encarapi-node)
|
|
132
|
+
- Documentation: [encarapi.com/documentation](https://encarapi.com/documentation?utm_source=readme&utm_medium=encarapi-node)
|
|
122
133
|
- OpenAPI reference: https://api.encarapi.com/reference
|
|
123
|
-
- China data: https://chinacarapi.com
|
|
134
|
+
- China data: [chinacarapi.com](https://chinacarapi.com/?utm_source=readme&utm_medium=encarapi-node)
|
|
124
135
|
- Python client: https://github.com/ThatMojo/encarapi-python
|
|
125
136
|
|
|
126
137
|
EnCarAPI is an independent service and not affiliated with Encar, KB Chachacha, K Car,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "encarapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Official Node.js client for EnCarAPI: Korean used car data (Encar, KB Chachacha, K Car) and Chinese used car data (Dongchedi, Che168) in one REST API.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"car export",
|
|
37
37
|
"vehicle data api"
|
|
38
38
|
],
|
|
39
|
-
"homepage": "https://encarapi.com",
|
|
39
|
+
"homepage": "https://encarapi.com/?utm_source=npm&utm_medium=encarapi-node",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
42
|
"url": "git+https://github.com/ThatMojo/encarapi-node.git"
|
package/src/china.js
CHANGED
|
@@ -4,6 +4,9 @@ const { HttpClient } = require("./http");
|
|
|
4
4
|
|
|
5
5
|
const enc = encodeURIComponent;
|
|
6
6
|
|
|
7
|
+
// /api/catalog serves at most this many results per query (page * limit).
|
|
8
|
+
const CATALOG_DEPTH = 10000;
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Chinese used-car data (Dongchedi and Che168) in English. Works with a
|
|
9
12
|
* ChinaCarAPI key or an EnCarAPI key that has the China add-on.
|
|
@@ -13,7 +16,7 @@ class ChinaClient {
|
|
|
13
16
|
this.http = new HttpClient({
|
|
14
17
|
apiKey,
|
|
15
18
|
baseUrl: options.baseUrl || "https://api.chinacarapi.com",
|
|
16
|
-
signupUrl: "https://chinacarapi.com",
|
|
19
|
+
signupUrl: "https://chinacarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error#pricing",
|
|
17
20
|
product: "ChinaCarAPI",
|
|
18
21
|
fetchImpl: options.fetch,
|
|
19
22
|
});
|
|
@@ -22,6 +25,21 @@ class ChinaClient {
|
|
|
22
25
|
/** Search listings. Filters: source, make, model, year_min/max, price_min/max (CNY), mileage_max, city, fuel, has_report, export_ready, sort, page, limit, lang. */
|
|
23
26
|
catalog(params) { return this.http.request("GET", "/api/catalog", { params }); }
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Async iterator over all catalog pages: for await (const car of china.iterateCatalog({...})) {}
|
|
30
|
+
* Stops at the API's 10,000-result depth limit; for the whole catalog use
|
|
31
|
+
* exportCsv() plus iterateChanges().
|
|
32
|
+
*/
|
|
33
|
+
async *iterateCatalog(params = {}) {
|
|
34
|
+
const limit = Math.min(params.limit || 100, 100);
|
|
35
|
+
for (let page = params.page || 1; ; page++) {
|
|
36
|
+
const res = await this.catalog({ ...params, limit, page });
|
|
37
|
+
const items = res.results || [];
|
|
38
|
+
for (const item of items) yield item;
|
|
39
|
+
if (items.length < limit || (page + 1) * limit > CATALOG_DEPTH) return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
/** Full record for one car: price history, photos, seller, export status, alsoListedOn. */
|
|
26
44
|
vehicle(id, params) { return this.http.request("GET", `/api/vehicle/${enc(id)}`, { params }); }
|
|
27
45
|
|
|
@@ -37,6 +55,23 @@ class ChinaClient {
|
|
|
37
55
|
/** Change feed: { since } once, then { cursor: nextCursor } (Business/Scale). */
|
|
38
56
|
changes(params) { return this.http.request("GET", "/api/catalog/changes", { params }); }
|
|
39
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Follows the change feed until it is drained and yields every event once:
|
|
60
|
+
* { id, source, vehicleId, type: "new" | "price" | "removed" | "relisted", oldPrice, newPrice, at }.
|
|
61
|
+
* Pass { since } (right after an export) or { cursor } to start; after the loop,
|
|
62
|
+
* `china.lastCursor` holds the cursor to resume from next time.
|
|
63
|
+
*/
|
|
64
|
+
async *iterateChanges(params = {}) {
|
|
65
|
+
let query = { ...params };
|
|
66
|
+
for (;;) {
|
|
67
|
+
const res = await this.changes(query);
|
|
68
|
+
for (const item of res.changes || []) yield item;
|
|
69
|
+
if (res.nextCursor !== undefined) this.lastCursor = res.nextCursor;
|
|
70
|
+
if (!res.hasMore || res.nextCursor === undefined) return;
|
|
71
|
+
query = { ...params, since: undefined, cursor: res.nextCursor };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
40
75
|
/** Full catalog as CSV text (Business/Scale). */
|
|
41
76
|
exportCsv(params) { return this.http.request("GET", "/api/catalog/export", { params, text: true }); }
|
|
42
77
|
|
package/src/index.d.ts
CHANGED
|
@@ -169,16 +169,45 @@ export interface ChinaCatalogResponse {
|
|
|
169
169
|
results: Json[];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
export interface ChinaChangesParams {
|
|
173
|
+
/** ISO timestamp for the first call. */
|
|
174
|
+
since?: string;
|
|
175
|
+
/** nextCursor from the previous call. */
|
|
176
|
+
cursor?: number;
|
|
177
|
+
source?: "dongchedi" | "che168" | (string & {});
|
|
178
|
+
limit?: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface ChinaChange extends Json {
|
|
182
|
+
id: number;
|
|
183
|
+
source: string;
|
|
184
|
+
vehicleId: string;
|
|
185
|
+
type: "new" | "price" | "removed" | "relisted";
|
|
186
|
+
at: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ChinaChangesResponse {
|
|
190
|
+
cursor: number;
|
|
191
|
+
nextCursor: number;
|
|
192
|
+
hasMore: boolean;
|
|
193
|
+
changes: ChinaChange[];
|
|
194
|
+
}
|
|
195
|
+
|
|
172
196
|
/** Chinese used-car data: Dongchedi and Che168. */
|
|
173
197
|
export class ChinaClient {
|
|
174
198
|
constructor(apiKey: string, options?: Pick<ClientOptions, "fetch"> & { baseUrl?: string });
|
|
199
|
+
/** Cursor to resume from after iterateChanges() finished. */
|
|
200
|
+
lastCursor?: number;
|
|
175
201
|
catalog(params?: ChinaCatalogParams): Promise<ChinaCatalogResponse>;
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
/** All pages up to the API's 10,000-result depth limit. */
|
|
203
|
+
iterateCatalog(params?: ChinaCatalogParams): AsyncGenerator<Json, void, unknown>;
|
|
204
|
+
vehicle(id: string, params?: { source?: "dongchedi" | "che168"; lang?: "en" | "zh" }): Promise<Json>;
|
|
205
|
+
inspection(id: string, params?: { source?: "dongchedi" | "che168"; lang?: "en" | "zh" }): Promise<Json>;
|
|
178
206
|
bulkVehicles(ids: string[], params?: { lang?: "en" | "zh" }): Promise<Json>;
|
|
179
207
|
/** Alias of bulkVehicles. */
|
|
180
208
|
bulk(ids: string[], params?: { lang?: "en" | "zh" }): Promise<Json>;
|
|
181
|
-
changes(params:
|
|
209
|
+
changes(params: ChinaChangesParams): Promise<ChinaChangesResponse>;
|
|
210
|
+
iterateChanges(params: ChinaChangesParams): AsyncGenerator<ChinaChange, void, unknown>;
|
|
182
211
|
exportCsv(params?: Json): Promise<string>;
|
|
183
212
|
enums(params?: Json): Promise<Json>;
|
|
184
213
|
models(make: string | number): Promise<Json>;
|
package/src/index.js
CHANGED
|
@@ -23,7 +23,7 @@ class EnCarAPI {
|
|
|
23
23
|
if (!apiKey && !chinaKey) {
|
|
24
24
|
throw new EnCarAPIError(
|
|
25
25
|
"An API key is required. Pass new EnCarAPI('YOUR_KEY') or set ENCARAPI_KEY " +
|
|
26
|
-
"(Korea) / CHINACARAPI_KEY (China). Get a key at https://encarapi.com"
|
|
26
|
+
"(Korea) / CHINACARAPI_KEY (China). Get a key at https://encarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error"
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
this._apiKey = apiKey;
|
|
@@ -35,7 +35,7 @@ class EnCarAPI {
|
|
|
35
35
|
get korea() {
|
|
36
36
|
if (!this._korea) {
|
|
37
37
|
if (!this._apiKey) {
|
|
38
|
-
throw new EnCarAPIError("An EnCarAPI key is required for Korean data. Get one at https://encarapi.com");
|
|
38
|
+
throw new EnCarAPIError("An EnCarAPI key is required for Korean data. Get one at https://encarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error");
|
|
39
39
|
}
|
|
40
40
|
this._korea = new KoreaClient(this._apiKey, { baseUrl: this._options.baseUrl, fetch: this._options.fetch });
|
|
41
41
|
}
|
|
@@ -68,7 +68,7 @@ class ChinaCarAPI extends ChinaClient {
|
|
|
68
68
|
if (!apiKey) {
|
|
69
69
|
throw new EnCarAPIError(
|
|
70
70
|
"A ChinaCarAPI key is required. Pass new ChinaCarAPI('YOUR_KEY') or set CHINACARAPI_KEY. " +
|
|
71
|
-
"Get a key at https://chinacarapi.com"
|
|
71
|
+
"Get a key at https://chinacarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error"
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
super(apiKey, options);
|
package/src/korea.js
CHANGED
|
@@ -15,7 +15,7 @@ class KoreaClient {
|
|
|
15
15
|
this.http = new HttpClient({
|
|
16
16
|
apiKey,
|
|
17
17
|
baseUrl: options.baseUrl || "https://api.encarapi.com",
|
|
18
|
-
signupUrl: "https://encarapi.com",
|
|
18
|
+
signupUrl: "https://encarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error#pricing",
|
|
19
19
|
product: "EnCarAPI",
|
|
20
20
|
fetchImpl: options.fetch,
|
|
21
21
|
});
|