universal-parcel-scraper 0.993.0-main.5345 → 0.993.2-main.5350
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/dist/carriers/dhlink/clock.d.ts +2 -0
- package/dist/carriers/dhlink/clock.js +5 -1
- package/dist/carriers/dhlink/parser.js +29 -2
- package/dist/providers/parcelsapp/adapter.js +5 -2
- package/dist/providers/seventeentrack/adapter.js +9 -4
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
|
@@ -5,3 +5,5 @@ export declare const DHLINK_RECORD_ZONE = "Asia/Shanghai";
|
|
|
5
5
|
* gives that instant.
|
|
6
6
|
*/
|
|
7
7
|
export declare function usClockCopy(wall: string, timestamp: number): boolean;
|
|
8
|
+
/** The US zone, the state's first, on whose clock `wall` strikes at `timestamp`; null when none does. */
|
|
9
|
+
export declare function usClockZone(wall: string, timestamp: number, state?: string | null): string | null;
|
|
@@ -10,5 +10,9 @@ const US_ZONES = ['America/New_York', 'America/Chicago', 'America/Denver', 'Amer
|
|
|
10
10
|
* gives that instant.
|
|
11
11
|
*/
|
|
12
12
|
export function usClockCopy(wall, timestamp) {
|
|
13
|
-
return
|
|
13
|
+
return usClockZone(wall, timestamp) !== null;
|
|
14
|
+
}
|
|
15
|
+
/** The US zone, the state's first, on whose clock `wall` strikes at `timestamp`; null when none does. */
|
|
16
|
+
export function usClockZone(wall, timestamp, state) {
|
|
17
|
+
return [...(state ? [state] : []), ...US_ZONES].find((zone) => isoTime(wall, zone)?.timestamp === timestamp) ?? null;
|
|
14
18
|
}
|
|
@@ -7,7 +7,7 @@ import { countryCode, firstOccurrence, isoTime, usStateTimeZone } from '../../co
|
|
|
7
7
|
import { cleanScalar } from '../../core/transport/text.js';
|
|
8
8
|
import { isRecord } from '../../core/types.js';
|
|
9
9
|
import { countryZones } from '../../places/zones.js';
|
|
10
|
-
import { DHLINK_RECORD_ZONE as RECORD_ZONE, usClockCopy } from './clock.js';
|
|
10
|
+
import { DHLINK_RECORD_ZONE as RECORD_ZONE, usClockCopy, usClockZone } from './clock.js';
|
|
11
11
|
import { dhlinkEnglish, dhlinkStage, FLIGHT_BOOKED } from './status.js';
|
|
12
12
|
export const PROVIDER = 'DHLink';
|
|
13
13
|
// A long history is a few dozen kilobytes.
|
|
@@ -179,6 +179,10 @@ function projectRow(row, readAt) {
|
|
|
179
179
|
return { stamp: local, event, rank, china, state, hub: usStateTimeZone(hub) !== null,
|
|
180
180
|
recorded: Math.min(recordedAt(row) ?? readAt, readAt) };
|
|
181
181
|
}
|
|
182
|
+
/** A relayed wording without the town and state a US partner can write before it ("Sampletown IL, "). */
|
|
183
|
+
function unplaced(description) {
|
|
184
|
+
return (description ?? '').replace(/^[A-Z][A-Za-z .'-]{0,40} [A-Z]{2}, /, '');
|
|
185
|
+
}
|
|
182
186
|
function outranks(left, right) {
|
|
183
187
|
const index = left.findIndex((value, position) => value !== right[position]);
|
|
184
188
|
return index >= 0 && left[index] > right[index];
|
|
@@ -259,6 +263,29 @@ export function parseDhlink(payload, raw, readAt = Date.now()) {
|
|
|
259
263
|
const zone = scanZone(scan, destination, last?.leg.beijingClock === true && !(scan.hub && last.leg.hubClockPickup));
|
|
260
264
|
return { scan, zone, time: zone ? isoTime(scan.stamp, zone) : null };
|
|
261
265
|
});
|
|
266
|
+
// A US partner's scan can come twice, as UniUni's do: on its hub's wall clock, often after the
|
|
267
|
+
// hub's town, and on UTC. The UTC copy's clock is the other's on a US reading, to the second,
|
|
268
|
+
// before DHLink recorded either: one scan, on that US clock.
|
|
269
|
+
const utcCopies = new Set();
|
|
270
|
+
if (destination === 'US') {
|
|
271
|
+
for (const local of dated) {
|
|
272
|
+
if (utcCopies.has(local))
|
|
273
|
+
continue;
|
|
274
|
+
const wording = unplaced(local.scan.event.description);
|
|
275
|
+
const copy = dated.find((other) => {
|
|
276
|
+
if (other === local || utcCopies.has(other) || unplaced(other.scan.event.description) !== wording)
|
|
277
|
+
return false;
|
|
278
|
+
const instant = isoTime(other.scan.stamp, 'UTC')?.timestamp;
|
|
279
|
+
return instant !== undefined && instant <= Math.min(local.scan.recorded, other.scan.recorded) + CLOCK_SKEW_MS
|
|
280
|
+
&& usClockZone(local.scan.stamp, instant, local.scan.state) !== null;
|
|
281
|
+
});
|
|
282
|
+
if (!copy)
|
|
283
|
+
continue;
|
|
284
|
+
const zone = usClockZone(local.scan.stamp, isoTime(copy.scan.stamp, 'UTC').timestamp, local.scan.state);
|
|
285
|
+
Object.assign(local, { zone, time: isoTime(local.scan.stamp, zone) });
|
|
286
|
+
utcCopies.add(copy);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
262
289
|
// Beside DHLink's own record of a partner's scan on Beijing time, the partner's copy can come
|
|
263
290
|
// on its US wall clock: the same wording, at the instant of DHLink's row on a US reading.
|
|
264
291
|
// Without a partner known to be relayed on Beijing time, DHLink's row must be read on it.
|
|
@@ -266,7 +293,7 @@ export function parseDhlink(payload, raw, readAt = Date.now()) {
|
|
|
266
293
|
&& other.scan.event.description === scan.event.description && usClockCopy(scan.stamp, other.time.timestamp))) : [];
|
|
267
294
|
// A place naming only the destination country is the parcel's, not the scan's: a Canada line's
|
|
268
295
|
// airline hand-over in China reads "Canada".
|
|
269
|
-
const kept = dated.filter((entry) => !copies.includes(entry));
|
|
296
|
+
const kept = dated.filter((entry) => !copies.includes(entry) && !utcCopies.has(entry));
|
|
270
297
|
const events = kept
|
|
271
298
|
.map(({ scan, time }) => {
|
|
272
299
|
const { location, ...event } = scan.event;
|
|
@@ -207,9 +207,12 @@ const WALL_CLOCKS = new Set(['usps', 'canadapost', 'gofoexpress', 'speedpak', 'e
|
|
|
207
207
|
* USPS's own rows read "Shipping Label Created, USPS Awaiting Item". Its date
|
|
208
208
|
* is the instant: read on its place's clock, a label came eight hours after
|
|
209
209
|
* ParcelsApp first returned it, and Juxi Express's rows of this shape fall
|
|
210
|
-
* between 17TRACK's instants of the same warehouse's steps as labeled.
|
|
210
|
+
* between 17TRACK's instants of the same warehouse's steps as labeled. Only a
|
|
211
|
+
* label or a shipping partner's step is such a row: USPS's own scans can come
|
|
212
|
+
* with their place after them too ("USPS picked up item, EXAMPLE CITY, KY
|
|
213
|
+
* 12345"), and USPS's own tracker dates them on their place's clock.
|
|
211
214
|
*/
|
|
212
|
-
const USPS_PARTNER_ROW =
|
|
215
|
+
const USPS_PARTNER_ROW = /^(?:shipping label created|[^,]*\bshipping partner\b[^,]*),[^,]+,\s*[A-Z]{2}\s+\d{5}(?:-\d{4})?$/i;
|
|
213
216
|
const uspsPartnerRow = (name, status) => nameKey(name) === 'usps' && USPS_PARTNER_ROW.test(status.trim());
|
|
214
217
|
// Known clocks that are the destination or origin country's (`knownClock`).
|
|
215
218
|
const DESTINATION = 'destination';
|
|
@@ -16,7 +16,7 @@ import { runSteps } from '../../core/runner/index.js';
|
|
|
16
16
|
import { countryTimeZone, firstOccurrence } from '../../core/time/index.js';
|
|
17
17
|
import { isRecord } from '../../core/types.js';
|
|
18
18
|
import { trackingPlace } from '../../places/trackingLocation.js';
|
|
19
|
-
import { chinaPostEnglish, chinaPostLabelPlace } from '../../carriers/china-post/status.js';
|
|
19
|
+
import { chinaPostDestinationScan, chinaPostEnglish, chinaPostLabelPlace } from '../../carriers/china-post/status.js';
|
|
20
20
|
import { agencyIntake, cttSiteZone, isCttExpressAgency } from '../../carriers/ctt/clock.js';
|
|
21
21
|
import { correosOriginZone } from '../../carriers/correos-spain/clock.js';
|
|
22
22
|
import { daiPostRelayZone } from '../../carriers/dai-post/clock.js';
|
|
@@ -553,6 +553,10 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
|
|
|
553
553
|
? nzPostAbroad(provider.events, number, nzPostDestination(shipment, tracking.providers)) : new Map();
|
|
554
554
|
const origin = reported.key === CHINA_POST ? originZone(number) : reported.key === JAPAN_POST ? japanPostOriginZone(number) : null;
|
|
555
555
|
const outward = reported.key === JAPAN_POST ? OUTWARD_SCAN : ORIGIN_EXCHANGE;
|
|
556
|
+
// China Post's copies of the destination post's own scans keep that post's wall clock, as China Post's adapter
|
|
557
|
+
// reads them, and 17TRACK labels them with China's offset: Correos's arrival at its exchange office at 09:25
|
|
558
|
+
// Madrid time showed in China Post's leg at 09:25 +08:00, seven hours before Correos's own row of it.
|
|
559
|
+
const inbound = reported.key === CHINA_POST ? countryTimeZone(recipientCountry(shipment)) : null;
|
|
556
560
|
const salvador = reported.key === CORREO_EL_SALVADOR;
|
|
557
561
|
const sender = salvador && isValidS10TrackingNumber(number) ? countryTimeZone(number.slice(-2)) : null;
|
|
558
562
|
const bound = reported.key === SWISS_POST ? recipientCountry(shipment) : null;
|
|
@@ -581,6 +585,7 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
|
|
|
581
585
|
if (undatedScan(raw))
|
|
582
586
|
undated++;
|
|
583
587
|
const exchange = origin !== null && outward.test(text(raw.description).trim());
|
|
588
|
+
const arrived = inbound !== null && chinaPostDestinationScan(text(raw.description));
|
|
584
589
|
const own = !salvador ? null : OUTBOUND.test(text(raw.description)) ? sender : 'UTC';
|
|
585
590
|
// The destination post's scan Swiss Post relays, its wall clock under Swiss Post's offset, or Huiyi Logistics' row in China under "+00:00".
|
|
586
591
|
const relayed = bound ? swissPostDestinationZone(text(raw.description), text(raw.location), bound)
|
|
@@ -597,15 +602,15 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
|
|
|
597
602
|
const site = !ctt ? null : cttSiteZone(text(raw.location), countries)
|
|
598
603
|
?? (!text(raw.location) && agencyIntake(wallMillis(raw), agencyScans) ? 'Europe/Madrid' : null);
|
|
599
604
|
// A facility's clock, CTT's, A1's UTC, NZ Post's, Poland Post's, FIC's partner's, Correos de El Salvador's, Correos de Chile's, Shiprocket's, DAI Post's, Quick Rabbit's or an origin post's, on which 17TRACK puts one offset for the whole parcel.
|
|
600
|
-
const read = facility || site || relaysFacilityClock(name) || utc || nz || polish || handed || exchange || own || chile || shiprocket || daiPost
|
|
601
|
-
|| sent !== undefined;
|
|
605
|
+
const read = facility || site || relaysFacilityClock(name) || utc || nz || polish || handed || exchange || arrived || own || chile || shiprocket || daiPost
|
|
606
|
+
|| quickRabbit || sent !== undefined;
|
|
602
607
|
// Any other row 17TRACK labelled with its zone's offset on the day it looks, not on the scan's.
|
|
603
608
|
const season = relayed || hywl || read || poland ? null : seasonZone(raw, labelled, now);
|
|
604
609
|
const wall = relayed || hywl ? rawClock(raw) : read || season ? facilityWall(raw) : null;
|
|
605
610
|
// A label its mailer wrote on UTC, as USPS's adapter reads it.
|
|
606
611
|
const scanned = facility && wall ? uspsRowZone(wall, facility, text(raw.description), now.toMillis()) : facility;
|
|
607
612
|
const zone = !wall ? null : season ?? (hywl ? relayed : relayed ?? scanned ?? site ?? own ?? (sent !== undefined ? sent : abroad.has(index) ? abroad.get(index) ?? null
|
|
608
|
-
: exchange ? origin : utc ? 'UTC' : polish ? polish : handed ? handed
|
|
613
|
+
: exchange ? origin : arrived ? inbound : utc ? 'UTC' : polish ? polish : handed ? handed
|
|
609
614
|
: nz ? 'Pacific/Auckland' : quickRabbit ? 'Europe/Madrid' : chile ? facilityZone(text(raw.location)) ?? 'America/Santiago'
|
|
610
615
|
: shiprocket ? (placedInIndia(text(raw.location)) ? 'Asia/Kolkata' : null)
|
|
611
616
|
: daiPost ? daiPostRelayZone(text(raw.description), text(raw.location)) : facilityZone(text(raw.location))));
|
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.993.
|
|
3
|
+
"version": "0.993.2-main.5350",
|
|
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",
|