universal-parcel-scraper 0.41.0-main.397 → 0.41.1-main.399
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 +1 -2
- package/dist/browser/scraper.js +2 -6
- package/dist/carriers/dpd/carrier.json +1 -2
- package/dist/carriers/dpd-de/app.js +72 -6
- package/dist/data/catalog.json +1 -2
- package/dist/generated/catalog.d.ts +2 -2
- package/dist/generated/catalog.js +1 -2
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/data/catalog.json
CHANGED
package/dist/browser/scraper.js
CHANGED
|
@@ -752,13 +752,9 @@ var ve = {
|
|
|
752
752
|
]
|
|
753
753
|
},
|
|
754
754
|
dpd: {
|
|
755
|
-
displayName: "DPD",
|
|
755
|
+
displayName: "DPD Switzerland",
|
|
756
756
|
color: "#dc0032",
|
|
757
|
-
aliases: [
|
|
758
|
-
"DPD Switzerland",
|
|
759
|
-
"DPD (Schweiz) AG",
|
|
760
|
-
"myDPD"
|
|
761
|
-
],
|
|
757
|
+
aliases: ["DPD (Schweiz) AG", "myDPD"],
|
|
762
758
|
countries: ["CH"],
|
|
763
759
|
selectable: !0,
|
|
764
760
|
timezone: "Europe/Zurich",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
2
3
|
import { CarrierError, ChallengeError, IndeterminateError, SchemaError, TransportError } from '../../core/errors/index.js';
|
|
3
|
-
import { zonedTime } from '../../core/time/index.js';
|
|
4
|
+
import { calendarDay, isoTime, zonedTime } from '../../core/time/index.js';
|
|
4
5
|
import { clean, decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
5
6
|
import { xmlDocument } from '../../core/transport/xml.js';
|
|
6
7
|
export const DPD_DE_APP_API = 'https://api.paketnavigator.de/services/v1/Navigator3Service.asmx';
|
|
@@ -51,6 +52,63 @@ function scalar(node, name, max = 200) {
|
|
|
51
52
|
invalid();
|
|
52
53
|
return clean(matches[0]?.text ?? '', max);
|
|
53
54
|
}
|
|
55
|
+
function optional(node, name) {
|
|
56
|
+
const matches = children(node, name);
|
|
57
|
+
if (matches.length > 1)
|
|
58
|
+
invalid();
|
|
59
|
+
return matches[0];
|
|
60
|
+
}
|
|
61
|
+
const affirmative = (value) => value === 'true' || value === '1';
|
|
62
|
+
const placeholderDay = (day) => day === '2000-01-01' || day === '0001-01-01';
|
|
63
|
+
/** Validate source digits without assigning a zone to an offset-less estimate. */
|
|
64
|
+
function estimateClock(raw) {
|
|
65
|
+
if (!/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)?$/.test(raw)
|
|
66
|
+
|| placeholderDay(raw.slice(0, 10)))
|
|
67
|
+
return null;
|
|
68
|
+
return isoTime(raw, 'UTC');
|
|
69
|
+
}
|
|
70
|
+
function estimateDay(raw) {
|
|
71
|
+
if (estimateClock(raw))
|
|
72
|
+
return raw.slice(0, 10);
|
|
73
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
74
|
+
const display = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
|
|
75
|
+
const day = iso ? calendarDay(Number(iso[1]), Number(iso[2]), Number(iso[3]))
|
|
76
|
+
: display ? calendarDay(Number(display[3]), Number(display[2]), Number(display[1])) : null;
|
|
77
|
+
return day && !placeholderDay(day) ? day : null;
|
|
78
|
+
}
|
|
79
|
+
function announcedDay(wording) {
|
|
80
|
+
const date = /^Your parcel is estimated to be delivered on:\s*(?:[A-Za-z]+,\s*)?(\d{2}\.\d{2}\.\d{4})$/i.exec(wording)?.[1];
|
|
81
|
+
return date ? estimateDay(date) : null;
|
|
82
|
+
}
|
|
83
|
+
/** Specified dates are forecasts, never scans; delivered display dates are actuals. */
|
|
84
|
+
function deliveryEstimate(tracking, announced) {
|
|
85
|
+
const planned = optional(tracking, 'NewDeliveryInfo');
|
|
86
|
+
const day = planned && affirmative(scalar(planned, 'PlannedDeliveryDateSpecified', 8))
|
|
87
|
+
? estimateDay(scalar(planned, 'PlannedDeliveryDate', 64)) : null;
|
|
88
|
+
// The app displays a changed delivery day ahead of its previous forecast.
|
|
89
|
+
if (day && planned && affirmative(scalar(planned, 'DateChanged', 8)))
|
|
90
|
+
return day;
|
|
91
|
+
const live = optional(tracking, 'LiveTracking');
|
|
92
|
+
if (live && affirmative(scalar(live, 'EstimatedDeliveryDateTimeSpecified', 8))) {
|
|
93
|
+
const from = scalar(live, 'EstimatedDeliveryDateTimeFrom', 64);
|
|
94
|
+
const to = scalar(live, 'EstimatedDeliveryDateTimeTo', 64);
|
|
95
|
+
const start = estimateClock(from);
|
|
96
|
+
const end = estimateClock(to);
|
|
97
|
+
const hasOffset = (value) => /(?:Z|[+-]\d{2}:\d{2})$/.test(value);
|
|
98
|
+
if (start && end && hasOffset(from) === hasOffset(to) && end.timestamp >= start.timestamp) {
|
|
99
|
+
// An explicit instant can be shown on the German delivery clock. Unresolved
|
|
100
|
+
// wall clocks retain their digits, without receiving an invented offset.
|
|
101
|
+
const display = (raw, timestamp) => hasOffset(raw)
|
|
102
|
+
? DateTime.fromMillis(timestamp, { zone: 'Europe/Berlin' }).toFormat('yyyy-MM-dd HH:mm')
|
|
103
|
+
: raw.slice(0, 16).replace('T', ' ');
|
|
104
|
+
const first = display(from, start.timestamp);
|
|
105
|
+
const last = display(to, end.timestamp);
|
|
106
|
+
return `${first}–${first.slice(0, 10) === last.slice(0, 10) ? last.slice(11) : last}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// DeliveryDateTime is display text and can omit the year: never invent it.
|
|
110
|
+
return day ?? announced ?? estimateDay(scalar(tracking, 'DeliveryDateTime', 64));
|
|
111
|
+
}
|
|
54
112
|
const escaped = (value) => value.replace(/[<>&"']/g, character => `&#${character.charCodeAt(0)};`);
|
|
55
113
|
const elements = (fields) => Object.entries(fields)
|
|
56
114
|
.map(([name, value]) => `<${name}>${typeof value === 'string' ? escaped(value) : elements(value)}</${name}>`).join('');
|
|
@@ -81,15 +139,20 @@ export function parseDpdDeApp(data, scans, number) {
|
|
|
81
139
|
const events = [];
|
|
82
140
|
const seen = new Set();
|
|
83
141
|
let placed = '';
|
|
142
|
+
let announced = null;
|
|
84
143
|
for (const row of rows) {
|
|
85
144
|
const wording = scalar(row, 'StatusText', 500);
|
|
145
|
+
if (!wording)
|
|
146
|
+
invalid();
|
|
147
|
+
// These rows may carry no scan clock. The date is in the announcement itself.
|
|
148
|
+
if (/^Your parcel is estimated to be delivered\b/i.test(wording)) {
|
|
149
|
+
announced = announcedDay(wording);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
86
152
|
const day = scalar(row, 'ScanDate', 16);
|
|
87
153
|
const time = scalar(row, 'ScanTime', 16);
|
|
88
|
-
if (
|
|
154
|
+
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(day) || !/^\d{2}:\d{2}$/.test(time))
|
|
89
155
|
invalid();
|
|
90
|
-
// An announced delivery day is not a scan.
|
|
91
|
-
if (/^Your parcel is estimated to be delivered\b/i.test(wording))
|
|
92
|
-
continue;
|
|
93
156
|
const place = /^(.{1,120}) \(([A-Z]{2})\)$/.exec(scalar(row, 'Location'));
|
|
94
157
|
if (place)
|
|
95
158
|
placed = place[2];
|
|
@@ -125,10 +188,13 @@ export function parseDpdDeApp(data, scans, number) {
|
|
|
125
188
|
const current = events[0];
|
|
126
189
|
const rail = DPD_DE_APP_RAIL[scalar(one(tracking, 'LastStatusInfo'), 'StatusID', 64)];
|
|
127
190
|
const stage = current.stage ?? rail?.stage;
|
|
191
|
+
const status = rail?.status ?? statusOf(current.stage);
|
|
128
192
|
const kilograms = Number(scalar(one(tracking, 'OrderInfo'), 'Weight', 16).replace(',', '.'));
|
|
129
193
|
return {
|
|
130
|
-
status
|
|
194
|
+
status, ...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
|
|
131
195
|
last_status_text: current.description ?? null, last_update: current.time ?? null,
|
|
196
|
+
expected_delivery: affirmative(scalar(tracking, 'Delivered', 8)) || status === 'delivered' || status === 'exception'
|
|
197
|
+
|| stage === 'delivered' || stage === 'ready_for_pickup' ? null : deliveryEstimate(tracking, announced),
|
|
132
198
|
...(current.time ? {} : { last_update_local: current.local_time }),
|
|
133
199
|
...(Number.isFinite(kilograms) && kilograms > 0 ? { weight_kg: kilograms } : {}),
|
|
134
200
|
events: events.slice(0, 100),
|
package/dist/data/catalog.json
CHANGED
|
@@ -502,9 +502,9 @@ export declare const CARRIER_CATALOG: {
|
|
|
502
502
|
}];
|
|
503
503
|
};
|
|
504
504
|
readonly dpd: {
|
|
505
|
-
readonly displayName: "DPD";
|
|
505
|
+
readonly displayName: "DPD Switzerland";
|
|
506
506
|
readonly color: "#dc0032";
|
|
507
|
-
readonly aliases: readonly ["DPD
|
|
507
|
+
readonly aliases: readonly ["DPD (Schweiz) AG", "myDPD"];
|
|
508
508
|
readonly countries: readonly ["CH"];
|
|
509
509
|
readonly selectable: true;
|
|
510
510
|
readonly timezone: "Europe/Zurich";
|
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.41.
|
|
3
|
+
"version": "0.41.1-main.399",
|
|
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",
|