universal-parcel-scraper 0.428.0-main.2178 → 0.430.0-main.2182
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 +2 -2
- package/data/catalog.json +5 -2
- package/data/coverage-summary.json +1 -1
- package/data/detection-golden.json +8 -0
- package/dist/browser/scraper.js +6 -2
- package/dist/carriers/gsi-express/adapter.d.ts +16 -0
- package/dist/carriers/gsi-express/adapter.js +47 -0
- package/dist/carriers/gsi-express/carrier.json +41 -7
- package/dist/carriers/gsi-express/numbers.json +36 -16
- package/dist/carriers/gsi-express/parser.d.ts +14 -0
- package/dist/carriers/gsi-express/parser.js +165 -0
- package/dist/carriers/gsi-express/status.d.ts +7 -0
- package/dist/carriers/gsi-express/status.js +27 -0
- package/dist/carriers/gsi-express/statuses.json +2 -2
- package/dist/carriers/vrl-logistics/adapter.d.ts +15 -0
- package/dist/carriers/vrl-logistics/adapter.js +47 -0
- package/dist/carriers/vrl-logistics/carrier.json +49 -7
- package/dist/carriers/vrl-logistics/numbers.json +2 -2
- package/dist/carriers/vrl-logistics/parser.d.ts +15 -0
- package/dist/carriers/vrl-logistics/parser.js +160 -0
- package/dist/carriers/vrl-logistics/status.d.ts +7 -0
- package/dist/carriers/vrl-logistics/status.js +32 -0
- package/dist/carriers/vrl-logistics/statuses.json +6 -0
- package/dist/core/catalog/statusMaps.js +4 -2
- package/dist/data/catalog.json +5 -2
- package/dist/generated/catalog.d.ts +5 -2
- package/dist/generated/catalog.js +5 -2
- package/dist/generated/recognition.js +1 -0
- package/dist/generated/registry.js +6 -2
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
|
|
15
15
|
<!-- GENERATED:summary -->
|
|
16
|
-
**3,500+ carriers** through **
|
|
16
|
+
**3,500+ carriers** through **465 dedicated adapters** and **5 universal fallbacks**
|
|
17
17
|
|
|
18
18
|
<sub>767 catalog entries · 134 countries represented</sub>
|
|
19
19
|
<!-- /GENERATED:summary -->
|
|
@@ -155,7 +155,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
|
|
|
155
155
|
<!-- GENERATED:stages -->
|
|
156
156
|
<img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
|
|
157
157
|
|
|
158
|
-
The carrier folders record 14,
|
|
158
|
+
The carrier folders record 14,530 such statuses, each filed under one stage.
|
|
159
159
|
<!-- /GENERATED:stages -->
|
|
160
160
|
|
|
161
161
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
package/data/catalog.json
CHANGED
|
@@ -15237,7 +15237,8 @@
|
|
|
15237
15237
|
"timezone": "America/New_York",
|
|
15238
15238
|
"tracking": {
|
|
15239
15239
|
"mode": "automatic",
|
|
15240
|
-
"adapter": "
|
|
15240
|
+
"adapter": "gsi-express",
|
|
15241
|
+
"localClocks": true
|
|
15241
15242
|
},
|
|
15242
15243
|
"canaryUrl": "https://www.gsiexpress.com/tracking_query.php",
|
|
15243
15244
|
"trackingUrlTemplate": "https://www.gsiexpress.com/track_pop.php?textfield={trackingNumber}&track_type=invoice_no",
|
|
@@ -29631,7 +29632,9 @@
|
|
|
29631
29632
|
"timezone": "Asia/Kolkata",
|
|
29632
29633
|
"tracking": {
|
|
29633
29634
|
"mode": "automatic",
|
|
29634
|
-
"adapter": "
|
|
29635
|
+
"adapter": "vrl-logistics",
|
|
29636
|
+
"recognitionRank": 935,
|
|
29637
|
+
"localClocks": true
|
|
29635
29638
|
},
|
|
29636
29639
|
"canaryUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
29637
29640
|
"trackingSearchUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
@@ -47323,6 +47323,14 @@
|
|
|
47323
47323
|
"dhl-ecommerce-pl"
|
|
47324
47324
|
]
|
|
47325
47325
|
},
|
|
47326
|
+
{
|
|
47327
|
+
"input": "6258970",
|
|
47328
|
+
"carrier": "unknown",
|
|
47329
|
+
"confidence": "low",
|
|
47330
|
+
"candidates": [
|
|
47331
|
+
"seur"
|
|
47332
|
+
]
|
|
47333
|
+
},
|
|
47326
47334
|
{
|
|
47327
47335
|
"input": "626020539",
|
|
47328
47336
|
"carrier": "unknown",
|
package/dist/browser/scraper.js
CHANGED
|
@@ -11694,7 +11694,8 @@ var Re = {
|
|
|
11694
11694
|
timezone: "America/New_York",
|
|
11695
11695
|
tracking: {
|
|
11696
11696
|
mode: "automatic",
|
|
11697
|
-
adapter: "
|
|
11697
|
+
adapter: "gsi-express",
|
|
11698
|
+
localClocks: !0
|
|
11698
11699
|
},
|
|
11699
11700
|
canaryUrl: "https://www.gsiexpress.com/tracking_query.php",
|
|
11700
11701
|
trackingUrlTemplate: "https://www.gsiexpress.com/track_pop.php?textfield={trackingNumber}&track_type=invoice_no",
|
|
@@ -22265,7 +22266,9 @@ var Re = {
|
|
|
22265
22266
|
timezone: "Asia/Kolkata",
|
|
22266
22267
|
tracking: {
|
|
22267
22268
|
mode: "automatic",
|
|
22268
|
-
adapter: "
|
|
22269
|
+
adapter: "vrl-logistics",
|
|
22270
|
+
recognitionRank: 935,
|
|
22271
|
+
localClocks: !0
|
|
22269
22272
|
},
|
|
22270
22273
|
canaryUrl: "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
22271
22274
|
trackingSearchUrl: "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
@@ -29494,6 +29497,7 @@ var _l = {
|
|
|
29494
29497
|
venipak: 240,
|
|
29495
29498
|
"via-cargo": 936,
|
|
29496
29499
|
"vinted-go": 410,
|
|
29500
|
+
"vrl-logistics": 935,
|
|
29497
29501
|
wanbexpress: 33,
|
|
29498
29502
|
whistl: 320,
|
|
29499
29503
|
winit: 855,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AdapterFactory, TrackingContext } from '../../core/adapter/index.js';
|
|
2
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
|
+
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
4
|
+
/** The page the tracker's invoice-number search opens for the number typed in. */
|
|
5
|
+
export declare const GSI_EXPRESS_TRACKING = "https://www.gsiexpress.com/track_pop.php";
|
|
6
|
+
export declare class GsiExpressTracker {
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(options?: {
|
|
9
|
+
fetcher?: typeof fetch;
|
|
10
|
+
recorder?: StepRecorder;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
});
|
|
13
|
+
private read;
|
|
14
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
15
|
+
}
|
|
16
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TransportError } from '../../core/errors/index.js';
|
|
2
|
+
import { runSteps } from '../../core/runner/index.js';
|
|
3
|
+
import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
|
|
4
|
+
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
5
|
+
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
6
|
+
import { GSI_EXPRESS_MAX_BYTES, normalizeGsiExpressNumber, parseGsiExpress, PROVIDER } from './parser.js';
|
|
7
|
+
/** The page the tracker's invoice-number search opens for the number typed in. */
|
|
8
|
+
export const GSI_EXPRESS_TRACKING = 'https://www.gsiexpress.com/track_pop.php';
|
|
9
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
10
|
+
const GONE = [404, 410];
|
|
11
|
+
const DEFAULT_BUDGET_MS = 15_000;
|
|
12
|
+
export class GsiExpressTracker {
|
|
13
|
+
options;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
async read(number, signal, timeoutMs) {
|
|
18
|
+
const url = `${GSI_EXPRESS_TRACKING}?${new URLSearchParams({ textfield: number, track_type: 'invoice_no' })}`;
|
|
19
|
+
const { response, bytes } = await fetchBounded(url, { method: 'GET', signal, headers: {
|
|
20
|
+
Accept: 'text/html', 'User-Agent': userAgentOf(this.options.userAgent),
|
|
21
|
+
} }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: GSI_EXPRESS_MAX_BYTES, redirect: 'manual',
|
|
22
|
+
fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, ...GONE] });
|
|
23
|
+
// The tracker answers a known and an unknown number alike with HTTP 200.
|
|
24
|
+
if (REDIRECTS.includes(response.status))
|
|
25
|
+
throw new TransportError(PROVIDER, 'GSI Express redirected its tracker', { status: response.status });
|
|
26
|
+
if (GONE.includes(response.status))
|
|
27
|
+
throw new TransportError(PROVIDER, 'GSI Express tracker is unavailable', { status: response.status });
|
|
28
|
+
return parseGsiExpress(decodeText(bytes), number);
|
|
29
|
+
}
|
|
30
|
+
async fetch(raw, context = {}) {
|
|
31
|
+
const number = normalizeGsiExpressNumber(raw);
|
|
32
|
+
// A read that fails to reach GSI Express, or hangs, is sent once more: the
|
|
33
|
+
// first try gets half of the budget.
|
|
34
|
+
return runSteps({ carrier: 'gsi-express', budgetMs: context.budgetMs ?? DEFAULT_BUDGET_MS, signal: context.signal,
|
|
35
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
36
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => this.read(number, signal, firstAttemptMs(remainingMs)) },
|
|
37
|
+
networkRetryStep(({ signal, remainingMs }) => this.read(number, signal, remainingMs)),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const adapter = (environment) => {
|
|
42
|
+
const tracker = new GsiExpressTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
43
|
+
return {
|
|
44
|
+
id: 'gsi-express', recordsSteps: true, steps: ['direct', 'retry'],
|
|
45
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -18,18 +18,52 @@
|
|
|
18
18
|
"timezone": "America/New_York",
|
|
19
19
|
"tracking": {
|
|
20
20
|
"mode": "automatic",
|
|
21
|
-
"adapter": "
|
|
22
|
-
"steps": [
|
|
21
|
+
"adapter": "gsi-express",
|
|
22
|
+
"steps": [
|
|
23
|
+
"direct",
|
|
24
|
+
"retry"
|
|
25
|
+
],
|
|
26
|
+
"localClocks": true
|
|
23
27
|
},
|
|
24
|
-
"capabilities": [
|
|
28
|
+
"capabilities": [
|
|
29
|
+
"history",
|
|
30
|
+
"delivery_partner",
|
|
31
|
+
"delivery_tracking_number"
|
|
32
|
+
],
|
|
25
33
|
"portal": {
|
|
26
34
|
"url": "https://www.gsiexpress.com/track_pop.php?textfield={trackingNumber}&track_type=invoice_no",
|
|
27
35
|
"searchUrl": "https://www.gsiexpress.com/tracking_query.php",
|
|
28
36
|
"canaryUrl": "https://www.gsiexpress.com/tracking_query.php",
|
|
29
|
-
"shows": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
"shows": [
|
|
38
|
+
"status",
|
|
39
|
+
"history",
|
|
40
|
+
"delivery_tracking_number",
|
|
41
|
+
"related_awb_numbers",
|
|
42
|
+
"order_reference",
|
|
43
|
+
"recipient_name",
|
|
44
|
+
"recipient_address"
|
|
45
|
+
],
|
|
46
|
+
"scraped": [
|
|
47
|
+
"status",
|
|
48
|
+
"history",
|
|
49
|
+
"delivery_partner",
|
|
50
|
+
"delivery_tracking_number"
|
|
51
|
+
],
|
|
52
|
+
"discarded": [
|
|
53
|
+
"related_awb_numbers",
|
|
54
|
+
"order_reference",
|
|
55
|
+
"recipient_name",
|
|
56
|
+
"recipient_address"
|
|
57
|
+
],
|
|
58
|
+
"unavailable": [
|
|
59
|
+
"location",
|
|
60
|
+
"eta",
|
|
61
|
+
"weight",
|
|
62
|
+
"delivered_at",
|
|
63
|
+
"offset_timestamps",
|
|
64
|
+
"service_name",
|
|
65
|
+
"sender_name"
|
|
66
|
+
]
|
|
33
67
|
},
|
|
34
68
|
"links": [
|
|
35
69
|
{
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"access": "indexed_excerpt"
|
|
57
57
|
},
|
|
58
58
|
"context": {
|
|
59
|
-
"assessment": "
|
|
59
|
+
"assessment": "confirmed"
|
|
60
60
|
},
|
|
61
61
|
"expect": {
|
|
62
62
|
"carrier": "unknown",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"seur"
|
|
66
66
|
]
|
|
67
67
|
},
|
|
68
|
-
"note": "An archived lookup of a receipt number on GSI Express's own tracker
|
|
68
|
+
"note": "An archived lookup of a receipt number on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"number": "3241306",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"access": "indexed_excerpt"
|
|
79
79
|
},
|
|
80
80
|
"context": {
|
|
81
|
-
"assessment": "
|
|
81
|
+
"assessment": "confirmed"
|
|
82
82
|
},
|
|
83
83
|
"expect": {
|
|
84
84
|
"carrier": "unknown",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"seur"
|
|
88
88
|
]
|
|
89
89
|
},
|
|
90
|
-
"note": "An archived lookup of a receipt number on GSI Express's own tracker
|
|
90
|
+
"note": "An archived lookup of a receipt number on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"number": "3468869",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"access": "indexed_excerpt"
|
|
101
101
|
},
|
|
102
102
|
"context": {
|
|
103
|
-
"assessment": "
|
|
103
|
+
"assessment": "confirmed"
|
|
104
104
|
},
|
|
105
105
|
"expect": {
|
|
106
106
|
"carrier": "unknown",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"seur"
|
|
110
110
|
]
|
|
111
111
|
},
|
|
112
|
-
"note": "An archived lookup of a receipt number on GSI Express's own tracker
|
|
112
|
+
"note": "An archived lookup of a receipt number on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"number": "3650251",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"access": "indexed_excerpt"
|
|
123
123
|
},
|
|
124
124
|
"context": {
|
|
125
|
-
"assessment": "
|
|
125
|
+
"assessment": "confirmed"
|
|
126
126
|
},
|
|
127
127
|
"expect": {
|
|
128
128
|
"carrier": "unknown",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"seur"
|
|
132
132
|
]
|
|
133
133
|
},
|
|
134
|
-
"note": "An archived lookup of a receipt number on GSI Express's own tracker
|
|
134
|
+
"note": "An archived lookup of a receipt number on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"number": "4262236",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"access": "indexed_excerpt"
|
|
145
145
|
},
|
|
146
146
|
"context": {
|
|
147
|
-
"assessment": "
|
|
147
|
+
"assessment": "confirmed"
|
|
148
148
|
},
|
|
149
149
|
"expect": {
|
|
150
150
|
"carrier": "unknown",
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"seur"
|
|
154
154
|
]
|
|
155
155
|
},
|
|
156
|
-
"note": "An archived lookup of a receipt number on GSI Express's own tracker
|
|
156
|
+
"note": "An archived lookup of a receipt number on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"number": "4274023",
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"access": "indexed_excerpt"
|
|
167
167
|
},
|
|
168
168
|
"context": {
|
|
169
|
-
"assessment": "
|
|
169
|
+
"assessment": "confirmed"
|
|
170
170
|
},
|
|
171
171
|
"expect": {
|
|
172
172
|
"carrier": "unknown",
|
|
@@ -175,7 +175,27 @@
|
|
|
175
175
|
"seur"
|
|
176
176
|
]
|
|
177
177
|
},
|
|
178
|
-
"note": "An archived lookup of a receipt number on GSI Express's own tracker
|
|
178
|
+
"note": "An archived lookup of a receipt number on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"number": "6258970",
|
|
182
|
+
"role": "shipment",
|
|
183
|
+
"evidence": "public_shipment_report",
|
|
184
|
+
"source": {
|
|
185
|
+
"url": "https://www.gsiexpress.com/track_pop.php?textfield=6258970&sd=2022-07-29",
|
|
186
|
+
"access": "indexed_excerpt"
|
|
187
|
+
},
|
|
188
|
+
"context": {
|
|
189
|
+
"assessment": "confirmed"
|
|
190
|
+
},
|
|
191
|
+
"expect": {
|
|
192
|
+
"carrier": "unknown",
|
|
193
|
+
"confidence": "low",
|
|
194
|
+
"candidates": [
|
|
195
|
+
"seur"
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
"note": "A search engine indexes this lookup on GSI Express's own tracker, which returns the parcel with its departure, customs clearance and Korea Post number."
|
|
179
199
|
},
|
|
180
200
|
{
|
|
181
201
|
"number": "6099726383792",
|
|
@@ -240,7 +260,7 @@
|
|
|
240
260
|
"access": "indexed_excerpt"
|
|
241
261
|
},
|
|
242
262
|
"context": {
|
|
243
|
-
"assessment": "
|
|
263
|
+
"assessment": "confirmed"
|
|
244
264
|
},
|
|
245
265
|
"expect": {
|
|
246
266
|
"carrier": "unknown",
|
|
@@ -253,7 +273,7 @@
|
|
|
253
273
|
"korea-post"
|
|
254
274
|
]
|
|
255
275
|
},
|
|
256
|
-
"note": "An archived lookup of a Korea Post registered number on GSI Express's own tracker
|
|
276
|
+
"note": "An archived lookup of a Korea Post registered number on GSI Express's own tracker, which returns it as the waybill of a GSI parcel."
|
|
257
277
|
},
|
|
258
278
|
{
|
|
259
279
|
"number": "6063009754271",
|
|
@@ -266,7 +286,7 @@
|
|
|
266
286
|
"access": "indexed_excerpt"
|
|
267
287
|
},
|
|
268
288
|
"context": {
|
|
269
|
-
"assessment": "
|
|
289
|
+
"assessment": "confirmed"
|
|
270
290
|
},
|
|
271
291
|
"expect": {
|
|
272
292
|
"carrier": "unknown",
|
|
@@ -279,7 +299,7 @@
|
|
|
279
299
|
"korea-post"
|
|
280
300
|
]
|
|
281
301
|
},
|
|
282
|
-
"note": "An archived lookup of a Korea Post registered number on GSI Express's own tracker
|
|
302
|
+
"note": "An archived lookup of a Korea Post registered number on GSI Express's own tracker, which returns it as the waybill of a GSI parcel."
|
|
283
303
|
}
|
|
284
304
|
]
|
|
285
305
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "GSI Express";
|
|
3
|
+
export declare const GSI_EXPRESS_MAX_BYTES: number;
|
|
4
|
+
/** A GSI Express receipt number: seven digits; anything else is not one its tracker reads. */
|
|
5
|
+
export declare function normalizeGsiExpressNumber(raw: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* The tracker's page for one receipt number, `track_pop.php`. Each field has a
|
|
8
|
+
* row of its own: the receipt number, the master air waybill, the waybill of
|
|
9
|
+
* the domestic leg, the merchant's order number, the recipient, the time the
|
|
10
|
+
* parcel left the US warehouse, the flight table, the customs clearance date
|
|
11
|
+
* and the domestic delivery section. A receipt number the tracker has no
|
|
12
|
+
* record of gets its notice that there is no data.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseGsiExpress(html: string, rawNumber: string): CarrierResult;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
4
|
+
import { ChallengeError, IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
5
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
6
|
+
import { clean } from '../../core/transport/index.js';
|
|
7
|
+
import { gsiExpressStage } from './status.js';
|
|
8
|
+
export const PROVIDER = 'GSI Express';
|
|
9
|
+
// A page is about sixteen kilobytes, most of it script.
|
|
10
|
+
export const GSI_EXPRESS_MAX_BYTES = 512 * 1024;
|
|
11
|
+
// The receipt number GSI Express issues for each parcel, which its tracker calls the invoice number.
|
|
12
|
+
const NUMBER = /^\d{7}$/;
|
|
13
|
+
// The tracker's whole answer to a receipt number it has no record of.
|
|
14
|
+
const NOT_FOUND = '해당 데이타가 존재하지 않습니다';
|
|
15
|
+
// A firewall or bot check's page, which can come back with HTTP 200.
|
|
16
|
+
const BLOCKED = /<title>\s*(?:Just a moment|Attention Required|Access Denied|Request Rejected)|_cf_chl_opt|cf-turnstile|g-recaptcha|h-captcha|captcha/i;
|
|
17
|
+
// The page's field labels, each heading one row.
|
|
18
|
+
const RECEIPT = 'GSI 접수번호';
|
|
19
|
+
const WAYBILL = '운송장번호';
|
|
20
|
+
const DEPARTED = '출고시간';
|
|
21
|
+
const FLIGHTS = 'Flight Plan Information';
|
|
22
|
+
const CLEARED = '통관완료일';
|
|
23
|
+
const DOMESTIC = '현지 국내배송상황';
|
|
24
|
+
// The departure is shown twice, in Korean time and thirteen hours earlier as US Eastern time.
|
|
25
|
+
const KOREAN_CLOCK = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s*\(한국시간\)/;
|
|
26
|
+
const CLEARED_DAY = /^(\d{4})년\s*(\d{2})월\s*(\d{2})일\s*\(한국시간\)$/;
|
|
27
|
+
// The domestic section's own lines: the numbered delivery waybill and its table's headings.
|
|
28
|
+
const DOMESTIC_LINE = /^\d+\.\s*주송장번호\s*:\s*[A-Z\d-]*$|^(?:처리일시|배송상태|처리지점)$/;
|
|
29
|
+
// Korea Post's domestic parcel numbers, the ones its own rule suggests.
|
|
30
|
+
const KOREA_POST_PARCEL = /^6\d{12}$/;
|
|
31
|
+
/** A GSI Express receipt number: seven digits; anything else is not one its tracker reads. */
|
|
32
|
+
export function normalizeGsiExpressNumber(raw) {
|
|
33
|
+
const number = typeof raw === 'string' && raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
|
|
34
|
+
if (!NUMBER.test(number))
|
|
35
|
+
throw new InvalidInputError(PROVIDER, 'GSI Express receipt numbers are seven digits');
|
|
36
|
+
return number;
|
|
37
|
+
}
|
|
38
|
+
function unexpected(html, message) {
|
|
39
|
+
if (BLOCKED.test(html.slice(0, 64_000)))
|
|
40
|
+
throw new ChallengeError(PROVIDER, 'GSI Express answered with a browser check');
|
|
41
|
+
throw new SchemaError(PROVIDER, message);
|
|
42
|
+
}
|
|
43
|
+
/** The row a field label heads: the label sits in a small table of its own in the row's first cell. */
|
|
44
|
+
function fieldRow($, label) {
|
|
45
|
+
const labels = $('td[bgcolor] > b').filter((_, element) => clean($(element).text(), 80) === label);
|
|
46
|
+
if (labels.length > 1)
|
|
47
|
+
throw new SchemaError(PROVIDER, `GSI Express returned a page with two ${label} fields`);
|
|
48
|
+
const row = labels.closest('table').parent().closest('tr');
|
|
49
|
+
return row.length === 1 ? row : null;
|
|
50
|
+
}
|
|
51
|
+
/** The text of the cells after a field's label. */
|
|
52
|
+
function fieldValue($, label) {
|
|
53
|
+
const row = fieldRow($, label);
|
|
54
|
+
return row ? clean(row.children('td').slice(1).text(), 300) : null;
|
|
55
|
+
}
|
|
56
|
+
/** A wall clock checked digit for digit: parsing in UTC reads the calendar alone, and Luxon rolls 24:00 over. */
|
|
57
|
+
function wallClock(text, format) {
|
|
58
|
+
const parsed = DateTime.fromFormat(text, format, { zone: 'UTC' });
|
|
59
|
+
if (!parsed.isValid || parsed.toFormat(format) !== text)
|
|
60
|
+
throw new SchemaError(PROVIDER, 'GSI Express returned an invalid date');
|
|
61
|
+
return parsed.toFormat(format.includes('H') ? "yyyy-MM-dd'T'HH:mm:ss" : 'yyyy-MM-dd');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The time the parcel left the warehouse in the United States, in Korean
|
|
65
|
+
* time. The page derives the US Eastern clock beside it by taking thirteen
|
|
66
|
+
* hours off all year, so one of the two is an hour out in winter: the clock
|
|
67
|
+
* stays a wall clock.
|
|
68
|
+
*/
|
|
69
|
+
function departure(text) {
|
|
70
|
+
if (!/\d/.test(text))
|
|
71
|
+
return null;
|
|
72
|
+
const clock = KOREAN_CLOCK.exec(text)?.[1];
|
|
73
|
+
if (!clock)
|
|
74
|
+
throw new SchemaError(PROVIDER, 'GSI Express returned a departure without its Korean time');
|
|
75
|
+
return wallClock(clock, 'yyyy-MM-dd HH:mm:ss');
|
|
76
|
+
}
|
|
77
|
+
/** The day Korean customs cleared the parcel. */
|
|
78
|
+
function clearance(text) {
|
|
79
|
+
// The cell goes on with a button to the Korea Customs Service's lookup, which has no text.
|
|
80
|
+
if (!/\d/.test(text))
|
|
81
|
+
return null;
|
|
82
|
+
const day = CLEARED_DAY.exec(text);
|
|
83
|
+
if (!day)
|
|
84
|
+
throw new SchemaError(PROVIDER, 'GSI Express returned an invalid customs clearance date');
|
|
85
|
+
return wallClock(`${day[1]}-${day[2]}-${day[3]}`, 'yyyy-MM-dd');
|
|
86
|
+
}
|
|
87
|
+
function scan(local, description) {
|
|
88
|
+
const staged = gsiExpressStage(description);
|
|
89
|
+
return { local_time: local, description, ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}) };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether the page lists flights or domestic scans the adapter does not read:
|
|
93
|
+
* rows under the flight table's headings, or lines in the domestic section
|
|
94
|
+
* besides its waybill and headings.
|
|
95
|
+
*/
|
|
96
|
+
function unreadRows($) {
|
|
97
|
+
const flights = fieldRow($, FLIGHTS)?.next('tr').find('tr').slice(1).toArray() ?? [];
|
|
98
|
+
const domestic = fieldRow($, DOMESTIC)?.closest('table').closest('tr').next('tr').find('td')
|
|
99
|
+
.filter((_, cell) => $(cell).find('td').length === 0).toArray() ?? [];
|
|
100
|
+
return flights.some((row) => clean($(row).text(), 40) !== '')
|
|
101
|
+
|| domestic.some((cell) => {
|
|
102
|
+
const text = clean($(cell).text(), 200);
|
|
103
|
+
return text !== '' && !DOMESTIC_LINE.test(text);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The waybill of the domestic leg. Korea Post's domestic parcel numbers are
|
|
108
|
+
* thirteen digits from 6; GSI Express's other partner in Korea, CJ Logistics,
|
|
109
|
+
* and Sagawa in Japan use shorter ones. A parcel the page clears through
|
|
110
|
+
* Korean customs under such a number is handed to Korea Post. Any other single
|
|
111
|
+
* waybill is kept without a carrier.
|
|
112
|
+
*/
|
|
113
|
+
function handoff($, korean) {
|
|
114
|
+
const shown = fieldValue($, WAYBILL) ?? '';
|
|
115
|
+
if (!/^[A-Z\d-]+$/i.test(shown))
|
|
116
|
+
return {};
|
|
117
|
+
const number = normalizeTrackingNumber(shown);
|
|
118
|
+
if (!/^[A-Z0-9]{4,40}$/.test(number))
|
|
119
|
+
return {};
|
|
120
|
+
return { ...(korean && KOREA_POST_PARCEL.test(number) ? { delivery_carrier: 'korea-post' } : {}), delivery_tracking_number: number };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The tracker's page for one receipt number, `track_pop.php`. Each field has a
|
|
124
|
+
* row of its own: the receipt number, the master air waybill, the waybill of
|
|
125
|
+
* the domestic leg, the merchant's order number, the recipient, the time the
|
|
126
|
+
* parcel left the US warehouse, the flight table, the customs clearance date
|
|
127
|
+
* and the domestic delivery section. A receipt number the tracker has no
|
|
128
|
+
* record of gets its notice that there is no data.
|
|
129
|
+
*/
|
|
130
|
+
export function parseGsiExpress(html, rawNumber) {
|
|
131
|
+
const number = normalizeGsiExpressNumber(rawNumber);
|
|
132
|
+
const $ = load(html);
|
|
133
|
+
const notices = $('font').filter((_, element) => clean($(element).text(), 80) === NOT_FOUND);
|
|
134
|
+
if (notices.length && !fieldRow($, RECEIPT))
|
|
135
|
+
throw new NotFoundError(PROVIDER);
|
|
136
|
+
const receipt = fieldValue($, RECEIPT);
|
|
137
|
+
if (receipt === null || !/^\d+$/.test(receipt))
|
|
138
|
+
unexpected(html, 'GSI Express returned a page without the receipt number');
|
|
139
|
+
if (receipt !== number)
|
|
140
|
+
throw new IndeterminateError(PROVIDER, 'GSI Express answered for another shipment');
|
|
141
|
+
const departed = fieldValue($, DEPARTED);
|
|
142
|
+
const cleared = fieldValue($, CLEARED);
|
|
143
|
+
if (departed === null && cleared === null)
|
|
144
|
+
throw new SchemaError(PROVIDER, 'GSI Express returned a page without its milestones');
|
|
145
|
+
const left = departed === null ? null : departure(departed);
|
|
146
|
+
const day = cleared === null ? null : clearance(cleared);
|
|
147
|
+
// Newest first, in the order the parcel passes them; a clearance date has no hour to compare.
|
|
148
|
+
const events = [...(day ? [scan(day, '통관완료')] : []), ...(left ? [scan(left, '출고')] : [])];
|
|
149
|
+
if (!events.length)
|
|
150
|
+
throw new NoHistoryError(PROVIDER);
|
|
151
|
+
const newest = events[0];
|
|
152
|
+
const stage = newest.stage;
|
|
153
|
+
return {
|
|
154
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
155
|
+
...(stage ? { current_stage: stage, current_stage_source: newest.stage_source } : {}),
|
|
156
|
+
last_status_text: newest.description,
|
|
157
|
+
last_update: null,
|
|
158
|
+
last_update_local: newest.local_time,
|
|
159
|
+
expected_delivery: null,
|
|
160
|
+
// The clearance row is Korean customs'.
|
|
161
|
+
...handoff($, cleared !== null),
|
|
162
|
+
events,
|
|
163
|
+
...(unreadRows($) ? { history_truncated: true } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
|
|
2
|
+
import type { CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
3
|
+
/** The stage the map gives a milestone. */
|
|
4
|
+
export declare function gsiExpressMappedStage(wording: string): Stage | undefined;
|
|
5
|
+
/** A milestone's stage: the map's, else the shared wording rules', else nothing. */
|
|
6
|
+
export declare function gsiExpressStage(wording: string): ClassifiedWording | null;
|
|
7
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { classifyWording } from '../../core/status/index.js';
|
|
2
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
3
|
+
/**
|
|
4
|
+
* The milestones the tracker shows as fields, named by the field's label
|
|
5
|
+
* without its 시간 ("time") or 일 ("date"). Its flights and the domestic
|
|
6
|
+
* carrier's scans are not read.
|
|
7
|
+
*/
|
|
8
|
+
const WORDINGS = new Map([
|
|
9
|
+
['출고', 'in_transit'], // The parcel left GSI Express's warehouse in the United States for its flight.
|
|
10
|
+
['통관완료', 'in_transit'], // Korean customs cleared the parcel, which goes on to the delivery carrier.
|
|
11
|
+
].map(([wording, stage]) => [normalizeStatusWording(wording), stage]));
|
|
12
|
+
/** The stage the map gives a milestone. */
|
|
13
|
+
export function gsiExpressMappedStage(wording) {
|
|
14
|
+
return WORDINGS.get(normalizeStatusWording(wording));
|
|
15
|
+
}
|
|
16
|
+
/** A milestone's stage: the map's, else the shared wording rules', else nothing. */
|
|
17
|
+
export function gsiExpressStage(wording) {
|
|
18
|
+
const mapped = gsiExpressMappedStage(wording);
|
|
19
|
+
if (mapped)
|
|
20
|
+
return { stage: mapped, source: 'carrier_map' };
|
|
21
|
+
const worded = classifyWording(wording, 'in_transit');
|
|
22
|
+
return worded.source === 'none' ? null : worded;
|
|
23
|
+
}
|
|
24
|
+
export const statusMap = {
|
|
25
|
+
stage: (_code, wording) => gsiExpressMappedStage(wording),
|
|
26
|
+
gaps: [],
|
|
27
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "gsi-express",
|
|
3
3
|
"entries": [
|
|
4
|
-
{"wording": "
|
|
5
|
-
{"wording": "
|
|
4
|
+
{"wording": "출고", "stage": "in_transit", "language": "ko", "confirmedBy": "portal", "note": "The tracker's 출고시간 field: the time the parcel left GSI Express's warehouse in the United States for its flight, in Korean and US Eastern time."},
|
|
5
|
+
{"wording": "통관완료", "stage": "in_transit", "language": "ko", "confirmedBy": "portal", "note": "The tracker's 통관완료일 field: the day Korean customs cleared the parcel, which goes on to the delivery carrier."}
|
|
6
6
|
]
|
|
7
7
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
|
+
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
4
|
+
/** The tracking page on www.vrlgroup.in, whose form posts the LR number back to it. */
|
|
5
|
+
export declare const VRL_TRACKER = "https://www.vrlgroup.in/Tracking/TrackShipment";
|
|
6
|
+
export declare class VrlTracker {
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(options?: {
|
|
9
|
+
fetcher?: typeof fetch;
|
|
10
|
+
recorder?: StepRecorder;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
});
|
|
13
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
14
|
+
}
|
|
15
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { TransportError } from '../../core/errors/index.js';
|
|
3
|
+
import { runSteps } from '../../core/runner/index.js';
|
|
4
|
+
import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
|
|
5
|
+
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
6
|
+
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
7
|
+
import { normalizeVrlNumber, parseVrl, PROVIDER, VRL_MAX_BYTES } from './parser.js';
|
|
8
|
+
/** The tracking page on www.vrlgroup.in, whose form posts the LR number back to it. */
|
|
9
|
+
export const VRL_TRACKER = 'https://www.vrlgroup.in/Tracking/TrackShipment';
|
|
10
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
11
|
+
export class VrlTracker {
|
|
12
|
+
options;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async fetch(raw, context = {}) {
|
|
17
|
+
const number = normalizeVrlNumber(raw);
|
|
18
|
+
const read = async (signal, timeoutMs) => {
|
|
19
|
+
// The form also sends its anti-forgery token and cookie, which the page does not check.
|
|
20
|
+
const { response, bytes } = await fetchBounded(VRL_TRACKER, { method: 'POST', signal, headers: {
|
|
21
|
+
Accept: 'text/html', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': userAgentOf(this.options.userAgent),
|
|
22
|
+
}, body: new URLSearchParams({ lrno: number }).toString() }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)),
|
|
23
|
+
maxBytes: VRL_MAX_BYTES, redirect: 'manual', fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, 404, 410] });
|
|
24
|
+
if (REDIRECTS.includes(response.status))
|
|
25
|
+
throw new TransportError(PROVIDER, 'VRL redirected its tracking page');
|
|
26
|
+
// The page answers an unknown LR number with HTTP 200; a 404 is a moved route.
|
|
27
|
+
if (response.status === 404 || response.status === 410)
|
|
28
|
+
throw new TransportError(PROVIDER, 'VRL tracking page is unavailable');
|
|
29
|
+
return parseVrl(decodeText(bytes), number);
|
|
30
|
+
};
|
|
31
|
+
// A request that fails to reach the site, or hangs, gets one more try: the
|
|
32
|
+
// first gets half of the budget, so the retry fits in the rest.
|
|
33
|
+
return runSteps({ carrier: 'vrl-logistics', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
34
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
35
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
|
|
36
|
+
networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const adapter = (environment) => {
|
|
41
|
+
const tracker = new VrlTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
42
|
+
return {
|
|
43
|
+
id: 'vrl-logistics', recordsSteps: true, steps: ['direct', 'retry'],
|
|
44
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
45
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeVrlNumber(number))),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -19,17 +19,59 @@
|
|
|
19
19
|
"timezone": "Asia/Kolkata",
|
|
20
20
|
"tracking": {
|
|
21
21
|
"mode": "automatic",
|
|
22
|
-
"adapter": "
|
|
23
|
-
"steps": [
|
|
22
|
+
"adapter": "vrl-logistics",
|
|
23
|
+
"steps": [
|
|
24
|
+
"direct",
|
|
25
|
+
"retry"
|
|
26
|
+
],
|
|
27
|
+
"recognition": {
|
|
28
|
+
"rank": 935
|
|
29
|
+
},
|
|
30
|
+
"localClocks": true
|
|
24
31
|
},
|
|
25
|
-
"capabilities": [
|
|
32
|
+
"capabilities": [
|
|
33
|
+
"history",
|
|
34
|
+
"location"
|
|
35
|
+
],
|
|
26
36
|
"portal": {
|
|
27
37
|
"searchUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
28
38
|
"canaryUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
29
|
-
"shows": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
"shows": [
|
|
40
|
+
"status",
|
|
41
|
+
"history",
|
|
42
|
+
"location",
|
|
43
|
+
"scan_times",
|
|
44
|
+
"origin",
|
|
45
|
+
"destination",
|
|
46
|
+
"parcel_count",
|
|
47
|
+
"delivery_method",
|
|
48
|
+
"depot_code",
|
|
49
|
+
"progress_rail"
|
|
50
|
+
],
|
|
51
|
+
"scraped": [
|
|
52
|
+
"status",
|
|
53
|
+
"history",
|
|
54
|
+
"location"
|
|
55
|
+
],
|
|
56
|
+
"discarded": [
|
|
57
|
+
"scan_times",
|
|
58
|
+
"origin",
|
|
59
|
+
"destination",
|
|
60
|
+
"parcel_count",
|
|
61
|
+
"delivery_method",
|
|
62
|
+
"depot_code",
|
|
63
|
+
"progress_rail"
|
|
64
|
+
],
|
|
65
|
+
"unavailable": [
|
|
66
|
+
"eta",
|
|
67
|
+
"weight",
|
|
68
|
+
"service_name",
|
|
69
|
+
"delivered_at",
|
|
70
|
+
"offset_timestamps",
|
|
71
|
+
"sender_name",
|
|
72
|
+
"recipient_name",
|
|
73
|
+
"proof_of_delivery"
|
|
74
|
+
]
|
|
33
75
|
},
|
|
34
76
|
"links": [
|
|
35
77
|
{
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"access": "indexed_excerpt"
|
|
146
146
|
},
|
|
147
147
|
"context": {
|
|
148
|
-
"assessment": "
|
|
148
|
+
"assessment": "confirmed"
|
|
149
149
|
},
|
|
150
150
|
"expect": {
|
|
151
151
|
"carrier": "unknown",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"gls-es"
|
|
162
162
|
]
|
|
163
163
|
},
|
|
164
|
-
"note": "An archived lookup on VRL's consignment tracking page; the opened capture loads the result separately and shows no details,
|
|
164
|
+
"note": "An archived lookup on VRL's consignment tracking page; the opened capture loads the result separately and shows no details. VRL's current tracker knows the LR, with its booking, transit rows and delivery."
|
|
165
165
|
}
|
|
166
166
|
]
|
|
167
167
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "VRL Logistics";
|
|
3
|
+
export declare const VRL_MAX_BYTES: number;
|
|
4
|
+
/** An LR (lorry receipt) number: nine or ten digits. Anything else is not a number VRL issues. */
|
|
5
|
+
export declare function normalizeVrlNumber(raw: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* The page `POST /Tracking/TrackShipment` answers for one LR number. The
|
|
8
|
+
* server renders the consignment's details card (its number, booking branch,
|
|
9
|
+
* destination branch, booking day and delivery type), then a transit card with
|
|
10
|
+
* its current status and a rail of its scans between the booking and the
|
|
11
|
+
* destination, oldest first, in two layouts: wide screens and phones. The
|
|
12
|
+
* phone layout is read. A hidden sample card ships with every page. An LR
|
|
13
|
+
* number the tracker does not know comes back as the form with an error toast.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseVrl(html: string, raw: string): CarrierResult;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
4
|
+
import { ChallengeError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
5
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
6
|
+
import { clean } from '../../core/transport/index.js';
|
|
7
|
+
import { vrlMappedStage, vrlStage } from './status.js';
|
|
8
|
+
export const PROVIDER = 'VRL Logistics';
|
|
9
|
+
// The page is under a hundred kilobytes, most of it menus and styles.
|
|
10
|
+
export const VRL_MAX_BYTES = 1024 * 1024;
|
|
11
|
+
const NUMBER = /^\d{9,10}$/;
|
|
12
|
+
const DAY = /^\d{2}-[A-Z][a-z]{2}-\d{4}$/;
|
|
13
|
+
const DAY_FORMAT = 'dd-LLL-yyyy';
|
|
14
|
+
// A scan's clock as the page writes it, "19:03, 06-Mar-2026": the hour, then the month where the minutes belong.
|
|
15
|
+
const SCAN_CLOCK = /^([01]\d|2[0-3]):[0-5]\d, (\S+)$/;
|
|
16
|
+
// The page's answer for an LR number it does not know, in an error toast above the form.
|
|
17
|
+
const NO_DATA = 'No data found';
|
|
18
|
+
// The toast for an LR number the tracker refuses outright, also its answer to a request without one.
|
|
19
|
+
const INVALID = /^Invalid LR number\b/i;
|
|
20
|
+
// Bot checks a CDN or firewall could put in place of the page.
|
|
21
|
+
const CHALLENGE = /_cf_chl_opt|cf-turnstile|<title>\s*(?:Just a moment|Attention Required|Access Denied|Request Rejected)|requested URL was rejected|g-recaptcha|h-captcha|AwsWafIntegration|awswaf\.com|_Incapsula_Resource/i;
|
|
22
|
+
// The ends of the progress rail: the booking day at the origin, the delivery day at the destination.
|
|
23
|
+
const MARKERS = new Set(['ORIGIN', 'DESTINATION']);
|
|
24
|
+
const MAX_ROWS = 500;
|
|
25
|
+
/** An LR (lorry receipt) number: nine or ten digits. Anything else is not a number VRL issues. */
|
|
26
|
+
export function normalizeVrlNumber(raw) {
|
|
27
|
+
const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
|
|
28
|
+
if (!NUMBER.test(number))
|
|
29
|
+
throw new InvalidInputError(PROVIDER, 'VRL LR numbers are nine or ten digits');
|
|
30
|
+
return number;
|
|
31
|
+
}
|
|
32
|
+
/** A day as the page writes it, "06-Mar-2026", as an ISO calendar day. */
|
|
33
|
+
function calendarDay(text) {
|
|
34
|
+
// Parsing in UTC checks the calendar alone; a day that reads back changed is not one.
|
|
35
|
+
const parsed = DateTime.fromFormat(text, DAY_FORMAT, { zone: 'UTC', locale: 'en-US' });
|
|
36
|
+
if (!DAY.test(text) || !parsed.isValid || parsed.toFormat(DAY_FORMAT, { locale: 'en-US' }) !== text) {
|
|
37
|
+
throw new SchemaError(PROVIDER, 'VRL returned an invalid date');
|
|
38
|
+
}
|
|
39
|
+
return parsed.toISODate();
|
|
40
|
+
}
|
|
41
|
+
/** The labelled values of a card section, by label. */
|
|
42
|
+
function labelled($, section) {
|
|
43
|
+
const values = new Map();
|
|
44
|
+
section.find('p.track-label').each((_, node) => {
|
|
45
|
+
const value = $(node).next('p.track-value');
|
|
46
|
+
if (value.length)
|
|
47
|
+
values.set(clean($(node).text(), 80), clean(value.text(), 120));
|
|
48
|
+
});
|
|
49
|
+
return values;
|
|
50
|
+
}
|
|
51
|
+
/** The rail's scans in the page's order, oldest first, without its two ends and the rows that have no date. */
|
|
52
|
+
function scansOf($, timeline) {
|
|
53
|
+
const rails = timeline.find('div.track-container.sm-scr-progress');
|
|
54
|
+
if (rails.length !== 1)
|
|
55
|
+
throw new SchemaError(PROVIDER, 'VRL returned a page without its transit rail');
|
|
56
|
+
const steps = rails.children('div.track-stepp');
|
|
57
|
+
if (steps.length > MAX_ROWS)
|
|
58
|
+
throw new SchemaError(PROVIDER, 'VRL returned an oversized history');
|
|
59
|
+
const rows = [];
|
|
60
|
+
steps.each((index, node) => {
|
|
61
|
+
const content = $(node).children('div.step-content');
|
|
62
|
+
const code = clean(content.find('div.step-labell div.status').text(), 40);
|
|
63
|
+
const clock = content.children('div.step-date');
|
|
64
|
+
if (!clock.length) {
|
|
65
|
+
if (!MARKERS.has(code))
|
|
66
|
+
throw new SchemaError(PROVIDER, 'VRL returned a transit row without its date');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const written = clean(clock.text(), 40);
|
|
70
|
+
// The page can list a row with neither a time nor a day, as ", ".
|
|
71
|
+
if (/^,?$/.test(written))
|
|
72
|
+
return;
|
|
73
|
+
const match = SCAN_CLOCK.exec(written);
|
|
74
|
+
if (!match)
|
|
75
|
+
throw new SchemaError(PROVIDER, 'VRL returned an invalid scan time');
|
|
76
|
+
const description = clean(content.children('label').first().text(), 120);
|
|
77
|
+
const location = clean(content.find('div.step-labell div.city').text(), 120);
|
|
78
|
+
if (!description || !location)
|
|
79
|
+
throw new SchemaError(PROVIDER, 'VRL returned a transit row without its status or branch');
|
|
80
|
+
const day = calendarDay(match[2]);
|
|
81
|
+
rows.push({ event: { local_time: day, location, description }, day, index });
|
|
82
|
+
});
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The page `POST /Tracking/TrackShipment` answers for one LR number. The
|
|
87
|
+
* server renders the consignment's details card (its number, booking branch,
|
|
88
|
+
* destination branch, booking day and delivery type), then a transit card with
|
|
89
|
+
* its current status and a rail of its scans between the booking and the
|
|
90
|
+
* destination, oldest first, in two layouts: wide screens and phones. The
|
|
91
|
+
* phone layout is read. A hidden sample card ships with every page. An LR
|
|
92
|
+
* number the tracker does not know comes back as the form with an error toast.
|
|
93
|
+
*/
|
|
94
|
+
export function parseVrl(html, raw) {
|
|
95
|
+
const number = normalizeVrlNumber(raw);
|
|
96
|
+
const $ = load(html);
|
|
97
|
+
const details = $('section.track-card--details').not('.d-none');
|
|
98
|
+
const timeline = $('section.track-card--timeline').not('.d-none');
|
|
99
|
+
if (!details.length && !timeline.length) {
|
|
100
|
+
const toast = clean($('#errorToast .toast-body').text(), 200);
|
|
101
|
+
if (toast === NO_DATA)
|
|
102
|
+
throw new NotFoundError(PROVIDER);
|
|
103
|
+
// A nine- or ten-digit number is one the tracker takes: a refusal means the request changed.
|
|
104
|
+
if (INVALID.test(toast))
|
|
105
|
+
throw new SchemaError(PROVIDER, 'VRL refused the LR number as invalid');
|
|
106
|
+
if (!$('#trackingForm').length && CHALLENGE.test(html.slice(0, 64_000)))
|
|
107
|
+
throw new ChallengeError(PROVIDER, 'VRL answered with a browser check');
|
|
108
|
+
throw new SchemaError(PROVIDER, 'VRL returned a page without an answer');
|
|
109
|
+
}
|
|
110
|
+
if (details.length !== 1 || timeline.length !== 1)
|
|
111
|
+
throw new SchemaError(PROVIDER, 'VRL returned an unexpected number of consignments');
|
|
112
|
+
const heading = details.find('#lrnum');
|
|
113
|
+
if (heading.length !== 1)
|
|
114
|
+
throw new SchemaError(PROVIDER, 'VRL returned a consignment without its number');
|
|
115
|
+
const echoed = clean(heading.text(), 40).replace(/^#\s*/, '');
|
|
116
|
+
if (echoed !== number)
|
|
117
|
+
throw new SchemaError(PROVIDER, 'VRL answered for a different LR number');
|
|
118
|
+
const booking = labelled($, details);
|
|
119
|
+
const transit = labelled($, timeline);
|
|
120
|
+
if (!booking.has('Booking Date') || !booking.has('Booking From'))
|
|
121
|
+
throw new SchemaError(PROVIDER, 'VRL returned a consignment without its booking');
|
|
122
|
+
const rows = scansOf($, timeline);
|
|
123
|
+
const bookedOn = booking.get('Booking Date');
|
|
124
|
+
const bookedAt = booking.get('Booking From');
|
|
125
|
+
// The rail starts with the booking day at the first word of the booking branch's name; the details card names it whole.
|
|
126
|
+
if (bookedOn) {
|
|
127
|
+
const day = calendarDay(bookedOn);
|
|
128
|
+
rows.unshift({ event: { local_time: day, ...(bookedAt ? { location: bookedAt } : {}), description: 'Booked' }, day, index: -1 });
|
|
129
|
+
}
|
|
130
|
+
if (!rows.length)
|
|
131
|
+
throw new NoHistoryError(PROVIDER);
|
|
132
|
+
// Days in order; within a day, the page's order. Without the hour, a row the
|
|
133
|
+
// same day at the same branch with the same status says nothing new.
|
|
134
|
+
const ordered = rows.sort((a, b) => a.day.localeCompare(b.day) || a.index - b.index).reverse();
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const events = [];
|
|
137
|
+
for (const { event } of ordered) {
|
|
138
|
+
const key = JSON.stringify([event.local_time, event.description, event.location]);
|
|
139
|
+
if (seen.has(key))
|
|
140
|
+
continue;
|
|
141
|
+
seen.add(key);
|
|
142
|
+
const staged = vrlStage(event.description);
|
|
143
|
+
events.push({ ...event, ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}) });
|
|
144
|
+
}
|
|
145
|
+
// The transit card's current status decides when the map knows it; else the newest scan with a stage.
|
|
146
|
+
const summary = transit.get('Current Status') ?? '';
|
|
147
|
+
const summarized = summary ? vrlMappedStage(summary) : undefined;
|
|
148
|
+
const decider = summarized ? undefined : events.find((event) => event.stage);
|
|
149
|
+
const stage = (summarized ?? decider?.stage);
|
|
150
|
+
const newest = events[0];
|
|
151
|
+
return {
|
|
152
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
153
|
+
...(stage ? { current_stage: stage, current_stage_source: summarized ? 'carrier_map' : decider.stage_source } : {}),
|
|
154
|
+
last_status_text: summarized ? summary : newest.description,
|
|
155
|
+
last_update: null,
|
|
156
|
+
last_update_local: newest.local_time,
|
|
157
|
+
expected_delivery: null,
|
|
158
|
+
events,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
|
|
2
|
+
import { type CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
3
|
+
/** The stage the map gives a wording, if it knows it. */
|
|
4
|
+
export declare function vrlMappedStage(wording: string): Stage | undefined;
|
|
5
|
+
/** A wording's stage from the map, else from the shared wording rules; wording neither knows stays without one. */
|
|
6
|
+
export declare function vrlStage(wording: string): ClassifiedWording | null;
|
|
7
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { classifyWording } from '../../core/status/index.js';
|
|
2
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
3
|
+
/**
|
|
4
|
+
* The tracker's scans and its current status carry wording only, in capitals:
|
|
5
|
+
* a move between branches, the arrival at the destination branch and the
|
|
6
|
+
* delivery. The adapter words the booking `Booked`. Only wording seen on the
|
|
7
|
+
* current tracker is mapped; other wording, such as the former tracker's,
|
|
8
|
+
* falls to the shared rules.
|
|
9
|
+
*/
|
|
10
|
+
const WORDINGS = new Map([
|
|
11
|
+
['booked', 'accepted'],
|
|
12
|
+
['in-transit', 'in_transit'],
|
|
13
|
+
// The page does not say whether the parcel waits there for collection or goes out to the door.
|
|
14
|
+
['arrived at destination', 'in_transit'],
|
|
15
|
+
['delivered', 'delivered'],
|
|
16
|
+
]);
|
|
17
|
+
/** The stage the map gives a wording, if it knows it. */
|
|
18
|
+
export function vrlMappedStage(wording) {
|
|
19
|
+
return WORDINGS.get(normalizeStatusWording(wording));
|
|
20
|
+
}
|
|
21
|
+
/** A wording's stage from the map, else from the shared wording rules; wording neither knows stays without one. */
|
|
22
|
+
export function vrlStage(wording) {
|
|
23
|
+
const mapped = vrlMappedStage(wording);
|
|
24
|
+
if (mapped)
|
|
25
|
+
return { stage: mapped, source: 'carrier_map' };
|
|
26
|
+
const worded = classifyWording(wording, 'in_transit');
|
|
27
|
+
return worded.source === 'none' ? null : worded;
|
|
28
|
+
}
|
|
29
|
+
export const statusMap = {
|
|
30
|
+
stage: (code, wording) => (code ? undefined : vrlMappedStage(wording)),
|
|
31
|
+
gaps: [],
|
|
32
|
+
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "vrl-logistics",
|
|
3
3
|
"entries": [
|
|
4
|
+
{"wording": "Booked", "stage": "accepted", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-19", "note": "Written by the adapter for the details card's booking day, at the booking branch."},
|
|
5
|
+
{"wording": "IN-TRANSIT", "stage": "in_transit", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-19", "note": "A transit row at a branch or transhipment point on the way, several a day."},
|
|
6
|
+
{"wording": "ARRIVED AT DESTINATION", "stage": "in_transit", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-19", "note": "A transit row at the destination branch, and the current status of a consignment that stays there. The page does not say whether it waits for collection or goes out to the door."},
|
|
7
|
+
{"wording": "DELIVERED", "stage": "delivered", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-19", "note": "A transit row at the destination branch, and the current status once delivered, for an office delivery too. An undated row with this wording can sit under a consignment still at its destination; the adapter skips undated rows."},
|
|
8
|
+
{"wording": "ORIGIN", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-19", "note": "The start of the progress rail, with the booking day and the first word of the booking branch's name; not a scan."},
|
|
9
|
+
{"wording": "DESTINATION", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-19", "note": "The end of the progress rail, with the destination town and, once delivered, the delivery day; not a scan."},
|
|
4
10
|
{"wording": "MATERIAL DELIVERED", "stage": "delivered", "language": "en", "confirmedBy": "portal", "note": "Seen in an archived capture of the LR tracking page, on a delivery run."},
|
|
5
11
|
{"wording": "MATERIAL OUT FOR DELIVERY", "stage": "out_for_delivery", "language": "en", "confirmedBy": "portal", "note": "Seen in an archived capture of the LR tracking page, on a delivery run."},
|
|
6
12
|
{"wording": "NOT DELIVERED", "stage": "failed_attempt", "language": "en", "confirmedBy": "portal", "note": "Seen in an archived capture of the LR tracking page, on a delivery run cancelled with a reason."},
|
|
@@ -164,6 +164,7 @@ import { statusMap as gofoFr } from '../../carriers/gofo-fr/status.js';
|
|
|
164
164
|
import { statusMap as gofoIt } from '../../carriers/gofo-it/status.js';
|
|
165
165
|
import { statusMap as gofoNl } from '../../carriers/gofo-nl/status.js';
|
|
166
166
|
import { statusMap as gogoXpress } from '../../carriers/gogo-xpress/status.js';
|
|
167
|
+
import { statusMap as gsiExpress } from '../../carriers/gsi-express/status.js';
|
|
167
168
|
import { statusMap as gzyl } from '../../carriers/gzyl/status.js';
|
|
168
169
|
import { statusMap as hanjin } from '../../carriers/hanjin/status.js';
|
|
169
170
|
import { statusMap as happyPost } from '../../carriers/happy-post/status.js';
|
|
@@ -370,6 +371,7 @@ import { statusMap as venipak } from '../../carriers/venipak/status.js';
|
|
|
370
371
|
import { statusMap as viaCargo } from '../../carriers/via-cargo/status.js';
|
|
371
372
|
import { statusMap as viettelPost } from '../../carriers/viettel-post/status.js';
|
|
372
373
|
import { statusMap as vintedGo } from '../../carriers/vinted-go/status.js';
|
|
374
|
+
import { statusMap as vrlLogistics } from '../../carriers/vrl-logistics/status.js';
|
|
373
375
|
import { statusMap as wanbexpress } from '../../carriers/wanbexpress/status.js';
|
|
374
376
|
import { statusMap as whistl } from '../../carriers/whistl/status.js';
|
|
375
377
|
import { statusMap as winit } from '../../carriers/winit/status.js';
|
|
@@ -412,7 +414,7 @@ export const STATUS_MAPS = {
|
|
|
412
414
|
'flash-express-ph': flashExpressPh,
|
|
413
415
|
'four-px': fourPx, foxpost, fukuyama, gati, gdex,
|
|
414
416
|
'gebrueder-weiss': gebruederWeiss, geis, 'geniki-taxydromiki': geniki, 'geodis-pl': geodisPl, 'gls-es': glsEs, 'gls-it': glsIt,
|
|
415
|
-
'go-express': goExpress, globalpost, go4, gofo, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, 'gofo-nl': gofoNl, 'gogo-xpress': gogoXpress, gzyl, hanjin, 'happy-post': happyPost, haypost, 'hct-logistics': hctLogistics,
|
|
417
|
+
'go-express': goExpress, globalpost, go4, gofo, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, 'gofo-nl': gofoNl, 'gogo-xpress': gogoXpress, 'gsi-express': gsiExpress, gzyl, hanjin, 'happy-post': happyPost, haypost, 'hct-logistics': hctLogistics,
|
|
416
418
|
helthjem, hepsijet, 'hermes-at': hermesAt, hispapost, 'hr-parcel': hrParcel,
|
|
417
419
|
'hrvatska-posta': hrvatskaPosta, 'hunter-express': hunterExpress, hywl, 'ids-logistik': idsLogistik, imile, 'indah-logistik': indahLogistik,
|
|
418
420
|
instabox, 'internet-express': internetExpress, interrapidisimo,
|
|
@@ -446,7 +448,7 @@ export const STATUS_MAPS = {
|
|
|
446
448
|
trackon, 'tracx-logis': tracxLogis, transaher, transcorp, transmission, tsjy, 'tws-express': twsExpress, 'ubi-smart-parcel': ubiSmartParcel,
|
|
447
449
|
'united-delivery-service': unitedDeliveryService, ups, urbano, urvaam,
|
|
448
450
|
'uzbekistan-post': uzbekistanPost, valmo, 'vasp-expresso': vaspExpresso, veloces, venipak, 'via-cargo': viaCargo, 'viettel-post': viettelPost,
|
|
449
|
-
'vinted-go': vintedGo, wanbexpress, whistl, winit, 'xde-logistics': xdeLogistics, xdp, xlcourier, 'xpo-ltl': xpoLtl,
|
|
451
|
+
'vinted-go': vintedGo, 'vrl-logistics': vrlLogistics, wanbexpress, whistl, winit, 'xde-logistics': xdeLogistics, xdp, xlcourier, 'xpo-ltl': xpoLtl,
|
|
450
452
|
'yilong-logistics': yilongLogistics, yunexpress, zto,
|
|
451
453
|
'yurtici-kargo': yurticiKargo, zajil, zeleris, 'zr-express': zrExpress,
|
|
452
454
|
};
|
package/dist/data/catalog.json
CHANGED
|
@@ -15237,7 +15237,8 @@
|
|
|
15237
15237
|
"timezone": "America/New_York",
|
|
15238
15238
|
"tracking": {
|
|
15239
15239
|
"mode": "automatic",
|
|
15240
|
-
"adapter": "
|
|
15240
|
+
"adapter": "gsi-express",
|
|
15241
|
+
"localClocks": true
|
|
15241
15242
|
},
|
|
15242
15243
|
"canaryUrl": "https://www.gsiexpress.com/tracking_query.php",
|
|
15243
15244
|
"trackingUrlTemplate": "https://www.gsiexpress.com/track_pop.php?textfield={trackingNumber}&track_type=invoice_no",
|
|
@@ -29631,7 +29632,9 @@
|
|
|
29631
29632
|
"timezone": "Asia/Kolkata",
|
|
29632
29633
|
"tracking": {
|
|
29633
29634
|
"mode": "automatic",
|
|
29634
|
-
"adapter": "
|
|
29635
|
+
"adapter": "vrl-logistics",
|
|
29636
|
+
"recognitionRank": 935,
|
|
29637
|
+
"localClocks": true
|
|
29635
29638
|
},
|
|
29636
29639
|
"canaryUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
29637
29640
|
"trackingSearchUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
@@ -9845,7 +9845,8 @@ export declare const CARRIER_CATALOG: {
|
|
|
9845
9845
|
readonly timezone: "America/New_York";
|
|
9846
9846
|
readonly tracking: {
|
|
9847
9847
|
readonly mode: "automatic";
|
|
9848
|
-
readonly adapter: "
|
|
9848
|
+
readonly adapter: "gsi-express";
|
|
9849
|
+
readonly localClocks: true;
|
|
9849
9850
|
};
|
|
9850
9851
|
readonly canaryUrl: "https://www.gsiexpress.com/tracking_query.php";
|
|
9851
9852
|
readonly trackingUrlTemplate: "https://www.gsiexpress.com/track_pop.php?textfield={trackingNumber}&track_type=invoice_no";
|
|
@@ -18868,7 +18869,9 @@ export declare const CARRIER_CATALOG: {
|
|
|
18868
18869
|
readonly timezone: "Asia/Kolkata";
|
|
18869
18870
|
readonly tracking: {
|
|
18870
18871
|
readonly mode: "automatic";
|
|
18871
|
-
readonly adapter: "
|
|
18872
|
+
readonly adapter: "vrl-logistics";
|
|
18873
|
+
readonly recognitionRank: 935;
|
|
18874
|
+
readonly localClocks: true;
|
|
18872
18875
|
};
|
|
18873
18876
|
readonly canaryUrl: "https://www.vrlgroup.in/Tracking/TrackShipment";
|
|
18874
18877
|
readonly trackingSearchUrl: "https://www.vrlgroup.in/Tracking/TrackShipment";
|
|
@@ -15238,7 +15238,8 @@ export const CARRIER_CATALOG = {
|
|
|
15238
15238
|
"timezone": "America/New_York",
|
|
15239
15239
|
"tracking": {
|
|
15240
15240
|
"mode": "automatic",
|
|
15241
|
-
"adapter": "
|
|
15241
|
+
"adapter": "gsi-express",
|
|
15242
|
+
"localClocks": true
|
|
15242
15243
|
},
|
|
15243
15244
|
"canaryUrl": "https://www.gsiexpress.com/tracking_query.php",
|
|
15244
15245
|
"trackingUrlTemplate": "https://www.gsiexpress.com/track_pop.php?textfield={trackingNumber}&track_type=invoice_no",
|
|
@@ -29632,7 +29633,9 @@ export const CARRIER_CATALOG = {
|
|
|
29632
29633
|
"timezone": "Asia/Kolkata",
|
|
29633
29634
|
"tracking": {
|
|
29634
29635
|
"mode": "automatic",
|
|
29635
|
-
"adapter": "
|
|
29636
|
+
"adapter": "vrl-logistics",
|
|
29637
|
+
"recognitionRank": 935,
|
|
29638
|
+
"localClocks": true
|
|
29636
29639
|
},
|
|
29637
29640
|
"canaryUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
29638
29641
|
"trackingSearchUrl": "https://www.vrlgroup.in/Tracking/TrackShipment",
|
|
@@ -193,6 +193,7 @@ import { adapter as gofoFr } from '../carriers/gofo-fr/adapter.js';
|
|
|
193
193
|
import { adapter as gofoIt } from '../carriers/gofo-it/adapter.js';
|
|
194
194
|
import { adapter as gofoNl } from '../carriers/gofo-nl/adapter.js';
|
|
195
195
|
import { adapter as gogoXpress } from '../carriers/gogo-xpress/adapter.js';
|
|
196
|
+
import { adapter as gsiExpress } from '../carriers/gsi-express/adapter.js';
|
|
196
197
|
import { adapter as gzyl } from '../carriers/gzyl/adapter.js';
|
|
197
198
|
import { adapter as hanjin } from '../carriers/hanjin/adapter.js';
|
|
198
199
|
import { adapter as happyPost } from '../carriers/happy-post/adapter.js';
|
|
@@ -443,6 +444,7 @@ import { adapter as venipak } from '../carriers/venipak/adapter.js';
|
|
|
443
444
|
import { adapter as viaCargo } from '../carriers/via-cargo/adapter.js';
|
|
444
445
|
import { adapter as viettelPost } from '../carriers/viettel-post/adapter.js';
|
|
445
446
|
import { adapter as vintedGo } from '../carriers/vinted-go/adapter.js';
|
|
447
|
+
import { adapter as vrlLogistics } from '../carriers/vrl-logistics/adapter.js';
|
|
446
448
|
import { adapter as wanbexpress } from '../carriers/wanbexpress/adapter.js';
|
|
447
449
|
import { adapter as whistl } from '../carriers/whistl/adapter.js';
|
|
448
450
|
import { adapter as winit } from '../carriers/winit/adapter.js';
|
|
@@ -658,6 +660,7 @@ export const REGISTRY = {
|
|
|
658
660
|
"gofo-it": gofoIt,
|
|
659
661
|
"gofo-nl": gofoNl,
|
|
660
662
|
"gogo-xpress": gogoXpress,
|
|
663
|
+
"gsi-express": gsiExpress,
|
|
661
664
|
"gzyl": gzyl,
|
|
662
665
|
"hanjin": hanjin,
|
|
663
666
|
"happy-post": happyPost,
|
|
@@ -908,6 +911,7 @@ export const REGISTRY = {
|
|
|
908
911
|
"via-cargo": viaCargo,
|
|
909
912
|
"viettel-post": viettelPost,
|
|
910
913
|
"vinted-go": vintedGo,
|
|
914
|
+
"vrl-logistics": vrlLogistics,
|
|
911
915
|
"wanbexpress": wanbexpress,
|
|
912
916
|
"whistl": whistl,
|
|
913
917
|
"winit": winit,
|
|
@@ -1234,7 +1238,7 @@ export const REGISTRY = {
|
|
|
1234
1238
|
"grand-slam-express": "universal",
|
|
1235
1239
|
"groupe-guilbault": "universal",
|
|
1236
1240
|
"gs-postbox": "universal",
|
|
1237
|
-
"gsi-express": "
|
|
1241
|
+
"gsi-express": "gsi-express",
|
|
1238
1242
|
"gta-airskip": "universal",
|
|
1239
1243
|
"gtl-goto": "universal",
|
|
1240
1244
|
"guatex": "universal",
|
|
@@ -1666,7 +1670,7 @@ export const REGISTRY = {
|
|
|
1666
1670
|
"vitran-express": "universal",
|
|
1667
1671
|
"vnpost": "universal",
|
|
1668
1672
|
"vozovoz": "universal",
|
|
1669
|
-
"vrl-logistics": "
|
|
1673
|
+
"vrl-logistics": "vrl-logistics",
|
|
1670
1674
|
"wahana": "universal",
|
|
1671
1675
|
"wanbexpress": "wanbexpress",
|
|
1672
1676
|
"ward-trucking": "universal",
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.430.0-main.2182",
|
|
4
4
|
"description": "Self-hosted parcel tracking: carrier detection, dedicated scrapers and optional universal providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|