universal-parcel-scraper 0.652.1-main.3103 → 0.652.3-main.3107
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
CHANGED
|
@@ -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 16,
|
|
158
|
+
The carrier folders record 16,475 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.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "dayone",
|
|
3
3
|
"entries": [
|
|
4
|
+
{"wording": "Shipment has begun. In transit to destination city", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "The single scan of a parcel the tracker lists with a YunExpress number, whose tracker carries the rest of its history."},
|
|
4
5
|
{"wording": "Order processed by shipper", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "Yanwen's record of the merchant's order data, the first scan."},
|
|
5
6
|
{"wording": "SHIPPING INFORMATION RECEIVED", "stage": "registered", "language": "en", "confirmedBy": "live"},
|
|
6
7
|
{"wording": "SHIPPING INFORMATION RECEIVED BY AUSTRALIA POST", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "Australia Post's manifest, relayed: it comes after the parcel has moved and is no step back."},
|
|
@@ -14,6 +14,7 @@ const CODES = new Map([
|
|
|
14
14
|
['FPX_C_AAF', { status: 'in_transit', stage: 'in_transit' }],
|
|
15
15
|
['FPX_C_ADFF', { status: 'in_transit', stage: 'in_transit' }],
|
|
16
16
|
['FPX_F_ST', { status: 'in_transit', stage: 'in_transit' }],
|
|
17
|
+
['FPX_M_PT', { status: 'in_transit', stage: 'in_transit' }], // Left the origin facility, awaiting the line haul.
|
|
17
18
|
['FPX_M_HA', { status: 'in_transit', stage: 'in_transit' }],
|
|
18
19
|
['FPX_M_DFOA', { status: 'in_transit', stage: 'in_transit' }],
|
|
19
20
|
['FPX_M_ATA', { status: 'in_transit', stage: 'in_transit' }],
|
|
@@ -23,6 +24,7 @@ const CODES = new Map([
|
|
|
23
24
|
['FPX_D_AOPC', { status: 'in_transit', stage: 'in_transit' }],
|
|
24
25
|
['FPX_D_STPP', { status: 'in_transit', stage: 'in_transit' }],
|
|
25
26
|
['FPX_D_HQ', { status: 'in_transit', stage: 'in_transit' }],
|
|
27
|
+
['FPX_D_DFD', { status: 'in_transit', stage: 'in_transit' }], // Left the destination's national sorting centre.
|
|
26
28
|
['FPX_O_RR', { status: 'in_transit', stage: 'in_transit' }],
|
|
27
29
|
['FPX_M_IT', { status: 'in_transit', stage: 'in_transit' }],
|
|
28
30
|
]);
|
|
@@ -302,6 +302,24 @@
|
|
|
302
302
|
"confirmedBy": "live",
|
|
303
303
|
"firstSeen": "2026-10-19",
|
|
304
304
|
"note": "The pickup shop took the parcel over; relayed from DPD."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"code": "FPX_M_PT",
|
|
308
|
+
"wording": "Pending transportation",
|
|
309
|
+
"language": "en",
|
|
310
|
+
"stage": "in_transit",
|
|
311
|
+
"confirmedBy": "live",
|
|
312
|
+
"firstSeen": "2026-10-22",
|
|
313
|
+
"note": "After the departure from the origin facility, before the hand-over to the airline."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"code": "FPX_D_DFD",
|
|
317
|
+
"wording": "Out from national sorting center",
|
|
318
|
+
"language": "en",
|
|
319
|
+
"stage": "in_transit",
|
|
320
|
+
"confirmedBy": "live",
|
|
321
|
+
"firstSeen": "2026-10-22",
|
|
322
|
+
"note": "After import clearance, the day before the last mile's out-for-delivery scan."
|
|
305
323
|
}
|
|
306
324
|
]
|
|
307
325
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { load } from 'cheerio';
|
|
2
|
+
import { DateTime, FixedOffsetZone } from 'luxon';
|
|
2
3
|
import makeFetchCookie from 'fetch-cookie';
|
|
3
4
|
import { CookieJar } from 'tough-cookie';
|
|
4
5
|
import { carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
@@ -295,30 +296,44 @@ export function parseUPSTrackingHtml(page, trackingNumber) {
|
|
|
295
296
|
function text(value, maxLength) {
|
|
296
297
|
return clean(typeof value === 'string' ? decodeEntities(value) : value, maxLength);
|
|
297
298
|
}
|
|
299
|
+
/** UPS's offset of a scan's place, in minutes. */
|
|
300
|
+
function statedOffset(activity) {
|
|
301
|
+
const offset = /^([+-])(0\d|1[0-4]):([0-5]\d)$/.exec(cleanScalar(activity.gmtOffset));
|
|
302
|
+
return offset ? (offset[1] === '-' ? -1 : 1) * (Number(offset[2]) * 60 + Number(offset[3])) : null;
|
|
303
|
+
}
|
|
298
304
|
/**
|
|
299
305
|
* A scan's clock. UPS sends the same scan twice: a UTC pair (`gmtDate` /
|
|
300
|
-
* `gmtTime`) and a local pair with a `gmtOffset`.
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
306
|
+
* `gmtTime`) and a local pair with a `gmtOffset`. The UTC pair's instant, to
|
|
307
|
+
* the second, is written on that offset when the local pair reads the same
|
|
308
|
+
* minute on it, so the scan keeps its place's clock; otherwise it stays in
|
|
309
|
+
* UTC. A zero offset is written as UTC was, `+00:00`. Without the UTC pair,
|
|
310
|
+
* the local pair and its offset become the time, and no zone is ever guessed.
|
|
311
|
+
* A local pair without an offset stays a `local_time`, a scan UPS dates by
|
|
312
|
+
* its day alone, as it does some drop-offs, keeps that day, other text stays
|
|
313
|
+
* as UPS wrote it, and a scan with no clock at all, like some label scans,
|
|
314
|
+
* gets none.
|
|
305
315
|
*/
|
|
306
316
|
function activityClock(activity) {
|
|
307
|
-
const gmtDate = cleanScalar(activity.gmtDate);
|
|
308
|
-
const gmtTime = cleanScalar(activity.gmtTime);
|
|
309
|
-
if (/^\d{8}$/.test(gmtDate) && /^\d{2}:\d{2}:\d{2}$/.test(gmtTime)) {
|
|
310
|
-
return { time: `${gmtDate.slice(0, 4)}-${gmtDate.slice(4, 6)}-${gmtDate.slice(6, 8)}T${gmtTime}+00:00` };
|
|
311
|
-
}
|
|
312
317
|
const localDate = cleanScalar(activity.date);
|
|
313
318
|
const localTime = cleanScalar(activity.time);
|
|
314
319
|
const dateMatch = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(localDate);
|
|
315
320
|
const timeMatch = /^(\d{1,2}):([0-5]\d)\s*([AP])\.?M\.?$/i.exec(localTime);
|
|
316
321
|
const hour = Number(timeMatch?.[1]);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
+
const local = dateMatch && timeMatch && hour >= 1 && hour <= 12
|
|
323
|
+
? `${dateMatch[3]}-${dateMatch[1]}-${dateMatch[2]}T${String(hour % 12 + (timeMatch[3].toUpperCase() === 'P' ? 12 : 0)).padStart(2, '0')}:${timeMatch[2]}:00`
|
|
324
|
+
: null;
|
|
325
|
+
const offset = statedOffset(activity);
|
|
326
|
+
const gmtDate = cleanScalar(activity.gmtDate);
|
|
327
|
+
const gmtTime = cleanScalar(activity.gmtTime);
|
|
328
|
+
if (/^\d{8}$/.test(gmtDate) && /^\d{2}:\d{2}:\d{2}$/.test(gmtTime)) {
|
|
329
|
+
const utc = `${gmtDate.slice(0, 4)}-${gmtDate.slice(4, 6)}-${gmtDate.slice(6, 8)}T${gmtTime}`;
|
|
330
|
+
const shown = offset ? DateTime.fromISO(utc, { zone: 'utc' }).setZone(FixedOffsetZone.instance(offset)) : null;
|
|
331
|
+
return { time: shown?.isValid && shown.toFormat("yyyy-MM-dd'T'HH:mm':00'") === local
|
|
332
|
+
? shown.toISO({ suppressMilliseconds: true }) : `${utc}+00:00` };
|
|
333
|
+
}
|
|
334
|
+
if (local) {
|
|
335
|
+
const stated = cleanScalar(activity.gmtOffset);
|
|
336
|
+
return /^[+-]\d{2}:\d{2}$/.test(stated) ? { time: `${local}${stated}` } : { local_time: local };
|
|
322
337
|
}
|
|
323
338
|
const day = dateMatch && !localTime ? calendarDay(Number(dateMatch[3]), Number(dateMatch[1]), Number(dateMatch[2])) : null;
|
|
324
339
|
if (day)
|
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.652.
|
|
3
|
+
"version": "0.652.3-main.3107",
|
|
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",
|