universal-parcel-scraper 0.134.1-main.1120 → 0.135.0
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/data/catalog.json +2 -1
- package/dist/browser/scraper.js +2 -1
- package/dist/carriers/dhl-ecommerce-pl/adapter.d.ts +4 -1
- package/dist/carriers/dhl-ecommerce-pl/adapter.js +30 -7
- package/dist/carriers/dhl-ecommerce-pl/browser.d.ts +5 -0
- package/dist/carriers/dhl-ecommerce-pl/browser.js +83 -0
- package/dist/carriers/dhl-ecommerce-pl/carrier.json +16 -9
- package/dist/carriers/dhl-ecommerce-pl/parser.js +1 -1
- package/dist/carriers/ecoscooting/parser.js +1 -1
- package/dist/data/catalog.json +2 -1
- package/dist/generated/catalog.d.ts +1 -0
- package/dist/generated/catalog.js +2 -1
- package/dist/server/openapi.json +1 -1
- package/package.json +3 -2
package/data/catalog.json
CHANGED
|
@@ -5597,7 +5597,8 @@
|
|
|
5597
5597
|
"tracking": {
|
|
5598
5598
|
"mode": "automatic",
|
|
5599
5599
|
"adapter": "dhl-ecommerce-pl",
|
|
5600
|
-
"recognitionRank": 620
|
|
5600
|
+
"recognitionRank": 620,
|
|
5601
|
+
"localClocks": true
|
|
5601
5602
|
},
|
|
5602
5603
|
"canaryUrl": "https://mojdhl.pl/tracking",
|
|
5603
5604
|
"trackingUrlTemplate": "https://mojdhl.pl/tracking?paczki={trackingNumber}",
|
package/dist/browser/scraper.js
CHANGED
|
@@ -4546,7 +4546,8 @@ var De = {
|
|
|
4546
4546
|
tracking: {
|
|
4547
4547
|
mode: "automatic",
|
|
4548
4548
|
adapter: "dhl-ecommerce-pl",
|
|
4549
|
-
recognitionRank: 620
|
|
4549
|
+
recognitionRank: 620,
|
|
4550
|
+
localClocks: !0
|
|
4550
4551
|
},
|
|
4551
4552
|
canaryUrl: "https://mojdhl.pl/tracking",
|
|
4552
4553
|
trackingUrlTemplate: "https://mojdhl.pl/tracking?paczki={trackingNumber}",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
2
|
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
3
|
+
import type { TrawlClient } from '../../core/transport/trawl.js';
|
|
4
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
5
|
import { type DhlEcommercePlChallenge } from './parser.js';
|
|
4
6
|
/**
|
|
5
7
|
* The proof of work the page computes for every visitor before a lookup: the
|
|
@@ -13,7 +15,8 @@ export declare class DhlEcommercePlTracker {
|
|
|
13
15
|
fetcher?: typeof fetch;
|
|
14
16
|
recorder?: StepRecorder;
|
|
15
17
|
userAgent?: string;
|
|
18
|
+
trawl?: TrawlClient | null;
|
|
16
19
|
});
|
|
17
|
-
fetch(raw: string, context?: TrackingContext): Promise<
|
|
20
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
18
21
|
}
|
|
19
22
|
export declare const adapter: AdapterFactory;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
3
3
|
import { BudgetExceededError, ChallengeError, IndeterminateError, UpstreamHttpError, UpstreamNetworkError } from '../../core/errors/index.js';
|
|
4
|
-
import { runSteps } from '../../core/runner/index.js';
|
|
4
|
+
import { recoverableByDefault, runSteps } from '../../core/runner/index.js';
|
|
5
5
|
import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
|
|
6
6
|
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
7
7
|
import { decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
8
|
+
import { isRecord } from '../../core/types.js';
|
|
9
|
+
import { dhlPolandBrowser } from './browser.js';
|
|
8
10
|
import { normalizeDhlEcommercePlNumber, parseDhlEcommercePl, parseDhlEcommercePlChallenge, parseDhlEcommercePlRejection } from './parser.js';
|
|
9
11
|
const PROVIDER = 'DHL eCommerce Poland';
|
|
10
12
|
// The guest routes behind mojdhl.pl. They return the current status and none
|
|
@@ -37,7 +39,9 @@ export class DhlEcommercePlTracker {
|
|
|
37
39
|
}
|
|
38
40
|
fetch(raw, context = {}) {
|
|
39
41
|
const number = normalizeDhlEcommercePlNumber(raw);
|
|
40
|
-
const budgetMs = context.budgetMs ?? 15_000;
|
|
42
|
+
const budgetMs = context.budgetMs ?? (this.options.trawl ? 65_000 : 15_000);
|
|
43
|
+
let confirmed;
|
|
44
|
+
let waybill;
|
|
41
45
|
const read = async (signal, remainingMs) => {
|
|
42
46
|
signal.throwIfAborted();
|
|
43
47
|
// One deadline covers the challenge, its solution and the lookup.
|
|
@@ -89,17 +93,36 @@ export class DhlEcommercePlTracker {
|
|
|
89
93
|
// A 400 is the portal refusing the proof: nothing was looked up.
|
|
90
94
|
if (response.status !== 200)
|
|
91
95
|
throw new IndeterminateError(PROVIDER, 'DHL eCommerce Poland returned an inconclusive tracking response');
|
|
92
|
-
|
|
96
|
+
const payload = json(bytes);
|
|
97
|
+
const result = parseDhlEcommercePl(payload, number);
|
|
98
|
+
// Parsing first establishes the whole submitted number and one shipment.
|
|
99
|
+
const entry = Array.isArray(payload) ? payload[0] : undefined;
|
|
100
|
+
const shipment = isRecord(entry) && Array.isArray(entry.shipments) ? entry.shipments[0] : undefined;
|
|
101
|
+
const alias = isRecord(shipment) ? shipment.shipmentNumber : undefined;
|
|
102
|
+
waybill = typeof alias === 'string' && /^\d{11}$/.test(alias) ? alias : /^\d{11}$/.test(number) ? number : undefined;
|
|
103
|
+
confirmed = result;
|
|
104
|
+
return result;
|
|
93
105
|
};
|
|
94
106
|
return runSteps({ carrier: 'dhl-ecommerce-pl', budgetMs, signal: context.signal,
|
|
95
107
|
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
96
|
-
{ id: '
|
|
108
|
+
{ id: 'trawl', enabled: !!this.options.trawl, run: async ({ signal, remainingMs }) => {
|
|
109
|
+
const started = performance.now();
|
|
110
|
+
const summary = await read(signal, Math.min(15_000, firstAttemptMs(remainingMs)));
|
|
111
|
+
if (!waybill)
|
|
112
|
+
throw new IndeterminateError(PROVIDER, 'DHL Poland returned no waybill for its history page');
|
|
113
|
+
const history = await dhlPolandBrowser(this.options.trawl, waybill, signal, remainingMs - (performance.now() - started) - 500);
|
|
114
|
+
return { ...summary, ...history };
|
|
115
|
+
} },
|
|
116
|
+
{ id: 'direct', recovers: error => confirmed !== undefined || recoverableByDefault(error),
|
|
117
|
+
run: async ({ signal, remainingMs }) => confirmed ?? read(signal, firstAttemptMs(remainingMs)) },
|
|
97
118
|
networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
|
|
98
119
|
]);
|
|
99
120
|
}
|
|
100
121
|
}
|
|
101
122
|
export const adapter = (environment) => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
const options = { fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent };
|
|
124
|
+
const tracker = new DhlEcommercePlTracker({ ...options, trawl: environment.trawl });
|
|
125
|
+
const http = new DhlEcommercePlTracker(options);
|
|
126
|
+
return { id: 'dhl-ecommerce-pl', recordsSteps: true, steps: ['trawl', 'direct', 'retry'], track: (input, context) => tracker.fetch(input.number, context),
|
|
127
|
+
recognize: (number, context) => recognizeFromLookup(() => http.fetch(number, context), () => accepted(() => normalizeDhlEcommercePlNumber(number))) };
|
|
105
128
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
import type { TrawlClient } from '../../core/transport/trawl.js';
|
|
3
|
+
/** Facility clocks remain local: international rows do not identify their zone. */
|
|
4
|
+
export declare function parseDhlPolandHistory(payload: unknown, waybill: string): CarrierResult;
|
|
5
|
+
export declare function dhlPolandBrowser(trawl: TrawlClient, waybill: string, signal: AbortSignal, remainingMs: number): Promise<CarrierResult>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BudgetExceededError, ChallengeError, IndeterminateError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
2
|
+
import { calendarDay } from '../../core/time/index.js';
|
|
3
|
+
import { clean } from '../../core/transport/index.js';
|
|
4
|
+
import { isRecord } from '../../core/types.js';
|
|
5
|
+
const PROVIDER = 'DHL eCommerce Poland';
|
|
6
|
+
const ENDPOINT = 'https://sprawdz.dhl.com.pl/';
|
|
7
|
+
const MAX_BODY = 300_000;
|
|
8
|
+
const STAGES = [
|
|
9
|
+
[/^(?:Delivered to customer\.|Przesyłka doręczona do Odbiorcy\.)$/i, 'delivered'],
|
|
10
|
+
[/^(?:With delivery courier|Przesyłka przekazana kurierowi do doręczenia)\.?$/i, 'out_for_delivery'],
|
|
11
|
+
[/^(?:The shipment is in transit to the delivery depot\.|Przesyłka w transporcie do oddziału doręczającego\.)$/i, 'in_transit'],
|
|
12
|
+
[/^(?:The shipment has arrives at sorting center\.|Przesyłka została przyjęta w centrum sortowania\.)$/i, 'in_transit'],
|
|
13
|
+
[/^(?:The shipment is processed at depot of origin country\.|Przesyłka jest obsługiwana w oddziale nadającym\.)$/i, 'in_transit'],
|
|
14
|
+
[/^(?:The shipment has been dropped off at depot by the sender\.|Przesyłka została nadana przez nadawcę w oddziale DHL\.)$/i, 'accepted'],
|
|
15
|
+
[/^(?:The shipment is handled in Sorting Center|Przesyłka jest obsługiwana w Centrum Sortowania)\.?$/i, 'in_transit'],
|
|
16
|
+
[/^(?:The shipment has reached DHL terminal\.|Przesyłka dotarła do oddziału nadawczego DHL Parcel\.)$/i, 'accepted'],
|
|
17
|
+
];
|
|
18
|
+
/** Facility clocks remain local: international rows do not identify their zone. */
|
|
19
|
+
export function parseDhlPolandHistory(payload, waybill) {
|
|
20
|
+
if (isRecord(payload) && payload.challenge === true)
|
|
21
|
+
throw new ChallengeError(PROVIDER);
|
|
22
|
+
if (!/^\d{11}$/.test(waybill) || !isRecord(payload) || payload.number !== waybill
|
|
23
|
+
|| !Array.isArray(payload.events) || payload.events.length > 500) {
|
|
24
|
+
throw new SchemaError(PROVIDER, 'DHL Poland returned a different or invalid history');
|
|
25
|
+
}
|
|
26
|
+
const events = payload.events.map((row) => {
|
|
27
|
+
if (!isRecord(row) || typeof row.date !== 'string' || typeof row.time !== 'string'
|
|
28
|
+
|| typeof row.location !== 'string' || row.location.length > 200
|
|
29
|
+
|| typeof row.description !== 'string' || row.description.length > 500) {
|
|
30
|
+
throw new SchemaError(PROVIDER, 'DHL Poland returned an invalid scan');
|
|
31
|
+
}
|
|
32
|
+
const date = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(row.date);
|
|
33
|
+
const day = date && calendarDay(Number(date[3]), Number(date[2]), Number(date[1]));
|
|
34
|
+
if (!day || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(row.time))
|
|
35
|
+
throw new SchemaError(PROVIDER, 'DHL Poland returned an invalid scan clock');
|
|
36
|
+
const description = clean(row.description, 500).replace(/\s*(?:The (?:shipment|parcel) (?:was )?received by|Przesyłka odebrana przez)\b[\s\S]*$/i, '').trim();
|
|
37
|
+
if (!description)
|
|
38
|
+
throw new SchemaError(PROVIDER, 'DHL Poland returned an empty scan');
|
|
39
|
+
const stage = STAGES.find(([pattern]) => pattern.test(description))?.[1];
|
|
40
|
+
const location = clean(row.location, 200);
|
|
41
|
+
return { local_time: `${day}T${row.time}:00`, description,
|
|
42
|
+
...(location ? { location } : {}), ...(stage ? { stage, stage_source: 'carrier_map' } : {}) };
|
|
43
|
+
});
|
|
44
|
+
if (!events.length)
|
|
45
|
+
throw new IndeterminateError(PROVIDER, 'DHL Poland returned no scan history');
|
|
46
|
+
return { summary_only: false, events: events.slice(0, 100), last_update_local: events[0].local_time,
|
|
47
|
+
...(events.length > 100 ? { history_truncated: true } : {}) };
|
|
48
|
+
}
|
|
49
|
+
export async function dhlPolandBrowser(trawl, waybill, signal, remainingMs) {
|
|
50
|
+
const timeoutMs = Math.floor(remainingMs - 2000);
|
|
51
|
+
if (timeoutMs < 1)
|
|
52
|
+
throw new BudgetExceededError(PROVIDER, remainingMs);
|
|
53
|
+
// Leave room for the carrier's already-confirmed summary if the service stalls.
|
|
54
|
+
const bounded = AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, Math.floor(remainingMs - 500)))]);
|
|
55
|
+
const page = await trawl.scrape({ url: `${ENDPOINT}?sn=${waybill}&lang=en-gb`, skipHttp: true, maxTier: 3,
|
|
56
|
+
captureResponses: [ENDPOINT], maxTimeout: timeoutMs, settleTimeout: timeoutMs }, { provider: PROVIDER, timeoutMs, signal: bounded, requireSolved: false, captureWindow: 'last', maxBytes: 4 * MAX_BODY });
|
|
57
|
+
signal.throwIfAborted();
|
|
58
|
+
const responses = page.capturedResponses.filter(row => row.url === ENDPOINT);
|
|
59
|
+
const response = responses.at(-1);
|
|
60
|
+
const status = response?.status ?? page.statusCode;
|
|
61
|
+
if (status === 429)
|
|
62
|
+
throw new RateLimitedError(PROVIDER);
|
|
63
|
+
if ([401, 403, 428].includes(status))
|
|
64
|
+
throw new ChallengeError(PROVIDER);
|
|
65
|
+
if (status >= 400)
|
|
66
|
+
throw new UpstreamHttpError(PROVIDER, status);
|
|
67
|
+
if (!response)
|
|
68
|
+
throw new ChallengeError(PROVIDER, 'DHL Poland did not complete browser verification');
|
|
69
|
+
if (responses.length > 10 || response.status !== 200 || response.error || response.truncated || response.base64Encoded || !response.body) {
|
|
70
|
+
throw new TransportError(PROVIDER, 'DHL Poland returned an incomplete browser response');
|
|
71
|
+
}
|
|
72
|
+
if (response.trackingNumberSubmitted !== waybill || Buffer.byteLength(response.body) > MAX_BODY) {
|
|
73
|
+
throw new SchemaError(PROVIDER, 'DHL Poland returned unbound browser history');
|
|
74
|
+
}
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
payload = JSON.parse(response.body);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new SchemaError(PROVIDER, 'DHL Poland returned invalid browser history');
|
|
81
|
+
}
|
|
82
|
+
return parseDhlPolandHistory(payload, waybill);
|
|
83
|
+
}
|
|
@@ -20,17 +20,21 @@
|
|
|
20
20
|
"mode": "automatic",
|
|
21
21
|
"adapter": "dhl-ecommerce-pl",
|
|
22
22
|
"steps": [
|
|
23
|
+
"trawl",
|
|
23
24
|
"direct",
|
|
24
25
|
"retry"
|
|
25
26
|
],
|
|
26
27
|
"recognition": {
|
|
27
28
|
"rank": 620
|
|
28
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"localClocks": true
|
|
29
31
|
},
|
|
30
32
|
"capabilities": [
|
|
31
33
|
"eta",
|
|
32
34
|
"delivered_at",
|
|
33
|
-
"sender_name"
|
|
35
|
+
"sender_name",
|
|
36
|
+
"history",
|
|
37
|
+
"location"
|
|
34
38
|
],
|
|
35
39
|
"portal": {
|
|
36
40
|
"url": "https://mojdhl.pl/tracking?paczki={trackingNumber}",
|
|
@@ -41,21 +45,24 @@
|
|
|
41
45
|
"eta",
|
|
42
46
|
"delivered_at",
|
|
43
47
|
"sender_name",
|
|
44
|
-
"tracking_alias"
|
|
48
|
+
"tracking_alias",
|
|
49
|
+
"history",
|
|
50
|
+
"location",
|
|
51
|
+
"recipient_name"
|
|
45
52
|
],
|
|
46
53
|
"scraped": [
|
|
47
54
|
"status",
|
|
48
55
|
"eta",
|
|
49
56
|
"delivered_at",
|
|
50
|
-
"sender_name"
|
|
57
|
+
"sender_name",
|
|
58
|
+
"history",
|
|
59
|
+
"location"
|
|
51
60
|
],
|
|
52
61
|
"discarded": [
|
|
53
|
-
"tracking_alias"
|
|
62
|
+
"tracking_alias",
|
|
63
|
+
"recipient_name"
|
|
54
64
|
],
|
|
55
|
-
"unavailable": [
|
|
56
|
-
"history",
|
|
57
|
-
"location"
|
|
58
|
-
]
|
|
65
|
+
"unavailable": []
|
|
59
66
|
},
|
|
60
67
|
"links": [
|
|
61
68
|
{
|
|
@@ -66,7 +66,7 @@ export function parseDhlEcommercePl(payload, rawNumber) {
|
|
|
66
66
|
if (!isRecord(shipment) || typeof shipment.status !== 'string' || !/^[A-Z][A-Z0-9_]{0,39}$/.test(shipment.status)) {
|
|
67
67
|
throw new SchemaError(PROVIDER, 'DHL eCommerce Poland returned an invalid shipment status');
|
|
68
68
|
}
|
|
69
|
-
// The sender is the name the portal shows;
|
|
69
|
+
// The sender is the name the portal shows; self-service links stay out of results.
|
|
70
70
|
const sender = clean(shipment.sender, 200);
|
|
71
71
|
const { description, stage } = dhlEcommercePlStatus(shipment.status, shipment.timelineStep, clean(shipment.title, 200) || clean(shipment.step, 200));
|
|
72
72
|
// Only a receipt is timed, by the recipient or by the sender a return reached. The posting
|
|
@@ -30,7 +30,7 @@ export function parseEcoscooting(payload, rawNumber) {
|
|
|
30
30
|
// The same query error is returned for absent and unavailable orders. It
|
|
31
31
|
// neither identifies a parcel nor proves that Ecoscooting does not know it.
|
|
32
32
|
if (payload.success !== 'true')
|
|
33
|
-
throw new IndeterminateError('Ecoscooting', 'Ecoscooting could not complete the tracking query');
|
|
33
|
+
throw new IndeterminateError('Ecoscooting', 'Ecoscooting could not complete the tracking query', payload.errorMsg === 'query logisticOrder by mailNo error' ? { reason: 'portal_not_found' } : {});
|
|
34
34
|
if (!isRecord(payload.packageParam) || payload.packageParam.trackingNumber !== number)
|
|
35
35
|
throw new SchemaError('Ecoscooting', 'Ecoscooting returned a different parcel');
|
|
36
36
|
if (!Array.isArray(payload.statuses) || payload.statuses.length > 500 || !payload.statuses.every(isRecord))
|
package/dist/data/catalog.json
CHANGED
|
@@ -5597,7 +5597,8 @@
|
|
|
5597
5597
|
"tracking": {
|
|
5598
5598
|
"mode": "automatic",
|
|
5599
5599
|
"adapter": "dhl-ecommerce-pl",
|
|
5600
|
-
"recognitionRank": 620
|
|
5600
|
+
"recognitionRank": 620,
|
|
5601
|
+
"localClocks": true
|
|
5601
5602
|
},
|
|
5602
5603
|
"canaryUrl": "https://mojdhl.pl/tracking",
|
|
5603
5604
|
"trackingUrlTemplate": "https://mojdhl.pl/tracking?paczki={trackingNumber}",
|
|
@@ -3813,6 +3813,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
3813
3813
|
readonly mode: "automatic";
|
|
3814
3814
|
readonly adapter: "dhl-ecommerce-pl";
|
|
3815
3815
|
readonly recognitionRank: 620;
|
|
3816
|
+
readonly localClocks: true;
|
|
3816
3817
|
};
|
|
3817
3818
|
readonly canaryUrl: "https://mojdhl.pl/tracking";
|
|
3818
3819
|
readonly trackingUrlTemplate: "https://mojdhl.pl/tracking?paczki={trackingNumber}";
|
|
@@ -5598,7 +5598,8 @@ export const CARRIER_CATALOG = {
|
|
|
5598
5598
|
"tracking": {
|
|
5599
5599
|
"mode": "automatic",
|
|
5600
5600
|
"adapter": "dhl-ecommerce-pl",
|
|
5601
|
-
"recognitionRank": 620
|
|
5601
|
+
"recognitionRank": 620,
|
|
5602
|
+
"localClocks": true
|
|
5602
5603
|
},
|
|
5603
5604
|
"canaryUrl": "https://mojdhl.pl/tracking",
|
|
5604
5605
|
"trackingUrlTemplate": "https://mojdhl.pl/tracking?paczki={trackingNumber}",
|
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.135.0",
|
|
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",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"eslint": "^10.12.0",
|
|
108
108
|
"@eslint/js": "^10.0.1",
|
|
109
109
|
"typescript-eslint": "^8.71.0",
|
|
110
|
-
"globals": "^17.13.0"
|
|
110
|
+
"globals": "^17.13.0",
|
|
111
|
+
"linkedom": "0.18.13"
|
|
111
112
|
}
|
|
112
113
|
}
|