universal-parcel-scraper 0.414.0-main.2134 → 0.415.0-main.2136

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
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
13
13
  [![License: Apache-2.0](https://img.shields.io/badge/core-Apache--2.0-blue.svg)](LICENSE)
14
14
 
15
15
  <!-- GENERATED:summary -->
16
- **3,500+ carriers** through **449 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **450 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>760 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,288 such statuses, each filed under one stage.
158
+ The carrier folders record 14,300 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
@@ -18174,7 +18174,9 @@
18174
18174
  "timezone": "Asia/Riyadh",
18175
18175
  "tracking": {
18176
18176
  "mode": "automatic",
18177
- "adapter": "universal"
18177
+ "adapter": "labaih",
18178
+ "recognitionRank": 788,
18179
+ "localClocks": true
18178
18180
  },
18179
18181
  "canaryUrl": "https://mylabaih.com/",
18180
18182
  "trackingUrlTemplate": "https://track.mylabaih.com/track/?cn={trackingNumber}",
@@ -18184,7 +18186,7 @@
18184
18186
  "domains": [
18185
18187
  "track.mylabaih.com"
18186
18188
  ],
18187
- "pathPattern": "^/track/?$",
18189
+ "pathPattern": "^/track(?:/en)?/?$",
18188
18190
  "params": [
18189
18191
  "cn"
18190
18192
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 760,
3
- "activeAdapters": 449,
3
+ "activeAdapters": 450,
4
4
  "countries": 134,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -13879,14 +13879,16 @@ var Re = {
13879
13879
  timezone: "Asia/Riyadh",
13880
13880
  tracking: {
13881
13881
  mode: "automatic",
13882
- adapter: "universal"
13882
+ adapter: "labaih",
13883
+ recognitionRank: 788,
13884
+ localClocks: !0
13883
13885
  },
13884
13886
  canaryUrl: "https://mylabaih.com/",
13885
13887
  trackingUrlTemplate: "https://track.mylabaih.com/track/?cn={trackingNumber}",
13886
13888
  trackingSearchUrl: "https://mylabaih.com/",
13887
13889
  linkRules: [{
13888
13890
  domains: ["track.mylabaih.com"],
13889
- pathPattern: "^/track/?$",
13891
+ pathPattern: "^/track(?:/en)?/?$",
13890
13892
  params: ["cn"]
13891
13893
  }],
13892
13894
  detectionRules: []
@@ -29142,6 +29144,7 @@ var _l = {
29142
29144
  "kuehne-nagel": 435,
29143
29145
  "l-post": 808,
29144
29146
  "la-poste": 1e3,
29147
+ labaih: 788,
29145
29148
  "latvijas-pasts": 100,
29146
29149
  "leopards-courier": 965,
29147
29150
  "lietuvos-pastas": 895,
@@ -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 tracker's English page; the Arabic one that links open leaves some scans out. */
5
+ export declare const LABAIH_TRACKING = "https://track.mylabaih.com/track/en/?cn=";
6
+ export declare class LabaihTracker {
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,59 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { NotFoundError, 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 { LABAIH_MAX_BYTES, normalizeLabaihNumber, parseLabaih, PROVIDER } from './parser.js';
8
+ /** The tracker's English page; the Arabic one that links open leaves some scans out. */
9
+ export const LABAIH_TRACKING = 'https://track.mylabaih.com/track/en/?cn=';
10
+ const REDIRECTS = [301, 302, 303, 307, 308];
11
+ /** The tracker's redirect for a number it does not have, to its "Shipment(s) Not Found" page. */
12
+ function missing(response, url) {
13
+ const location = response.headers.get('location');
14
+ if (!location)
15
+ return false;
16
+ try {
17
+ const target = new URL(location, url);
18
+ return target.hostname === 'track.mylabaih.com' && /^\/errors?\/404\/?$/.test(target.pathname);
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ export class LabaihTracker {
25
+ options;
26
+ constructor(options = {}) {
27
+ this.options = options;
28
+ }
29
+ async fetch(raw, context = {}) {
30
+ const number = normalizeLabaihNumber(raw);
31
+ const url = `${LABAIH_TRACKING}${number}`;
32
+ const read = async (signal, timeoutMs) => {
33
+ const { response, bytes } = await fetchBounded(url, { signal, headers: {
34
+ Accept: 'text/html', 'User-Agent': userAgentOf(this.options.userAgent),
35
+ } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: LABAIH_MAX_BYTES, redirect: 'manual',
36
+ fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, 404, 410] });
37
+ if (REDIRECTS.includes(response.status) && missing(response, url))
38
+ throw new NotFoundError(PROVIDER);
39
+ // Any other redirect, or a 404, is a moved page.
40
+ if (response.status !== 200)
41
+ throw new TransportError(PROVIDER, 'LaBaih tracking page moved', { status: response.status });
42
+ return parseLabaih(decodeText(bytes), number);
43
+ };
44
+ // A read that fails to reach the site, or hangs, gets one more: the first gets half of the budget.
45
+ return runSteps({ carrier: 'labaih', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
46
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
47
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
48
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
49
+ ]);
50
+ }
51
+ }
52
+ export const adapter = (environment) => {
53
+ const tracker = new LabaihTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
54
+ return {
55
+ id: 'labaih', recordsSteps: true, steps: ['direct', 'retry'],
56
+ track: (input, context) => tracker.fetch(input.number, context),
57
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeLabaihNumber(number))),
58
+ };
59
+ };
@@ -17,25 +17,65 @@
17
17
  "timezone": "Asia/Riyadh",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "labaih",
21
+ "steps": [
22
+ "direct",
23
+ "retry"
24
+ ],
25
+ "recognition": {
26
+ "rank": 788
27
+ },
28
+ "localClocks": true
22
29
  },
23
- "capabilities": [],
30
+ "capabilities": [
31
+ "history",
32
+ "location"
33
+ ],
24
34
  "portal": {
25
35
  "url": "https://track.mylabaih.com/track/?cn={trackingNumber}",
26
36
  "searchUrl": "https://mylabaih.com/",
27
37
  "canaryUrl": "https://mylabaih.com/",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
38
+ "shows": [
39
+ "status",
40
+ "progress_rail",
41
+ "history",
42
+ "location",
43
+ "sender_name",
44
+ "order_contents",
45
+ "customer_reference",
46
+ "recipient_address"
47
+ ],
48
+ "scraped": [
49
+ "status",
50
+ "history",
51
+ "location"
52
+ ],
53
+ "discarded": [
54
+ "progress_rail",
55
+ "sender_name",
56
+ "order_contents",
57
+ "customer_reference",
58
+ "recipient_address"
59
+ ],
60
+ "unavailable": [
61
+ "eta",
62
+ "weight",
63
+ "service_name",
64
+ "provider_code",
65
+ "recipient_name",
66
+ "proof_of_delivery",
67
+ "delivered_at",
68
+ "offset_timestamps",
69
+ "delivery_partner",
70
+ "delivery_tracking_number"
71
+ ]
32
72
  },
33
73
  "links": [
34
74
  {
35
75
  "domains": [
36
76
  "track.mylabaih.com"
37
77
  ],
38
- "pathPattern": "^/track/?$",
78
+ "pathPattern": "^/track(?:/en)?/?$",
39
79
  "params": [
40
80
  "cn"
41
81
  ]
@@ -0,0 +1,23 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "LaBaih";
3
+ export declare const LABAIH_MAX_BYTES = 1000000;
4
+ /** A LaBaih number: fourteen digits, which begin with the date and time it was created. */
5
+ export declare function normalizeLabaihNumber(raw: string): string;
6
+ /** A challenge or block page in place of the tracker. */
7
+ export declare function assertNotChallenged(html: string): void;
8
+ /**
9
+ * A scan's wording. A failed attempt's reason follows a colon and can go on,
10
+ * after another colon, with the recipient's district or city as given:
11
+ * "Unsuccessful Delivery: Wrong Customer District: …". Only the reason is
12
+ * kept, and only when it is words; phone numbers are dropped.
13
+ */
14
+ export declare function labaihWording(text: string): string;
15
+ /**
16
+ * The tracker's English page for one number, `track.mylabaih.com/track/en/?cn=`,
17
+ * which the site renders on its server: a heading with the number and the shop's
18
+ * reference, the shop, a progress rail, then the history, newest first. Each
19
+ * scan has a wording, a wall clock without a zone and, for an arrival at a
20
+ * distribution centre, its city. An unknown number is a redirect the adapter
21
+ * reads before this. The shop, its reference and the rail are not kept.
22
+ */
23
+ export declare function parseLabaih(html: string, raw: string): CarrierResult;
@@ -0,0 +1,127 @@
1
+ import { load } from 'cheerio';
2
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
3
+ import { ChallengeError, IndeterminateError, InvalidInputError, NoHistoryError, SchemaError } from '../../core/errors/index.js';
4
+ import { languageStageStatus } from '../../core/status/index.js';
5
+ import { calendarDay } from '../../core/time/index.js';
6
+ import { clean } from '../../core/transport/index.js';
7
+ import { labaihStage } from './status.js';
8
+ export const PROVIDER = 'LaBaih';
9
+ // A page is about 110 kB, most of it inline icons.
10
+ export const LABAIH_MAX_BYTES = 1_000_000;
11
+ const NUMBER = /^\d{14}$/;
12
+ // The heading over the shipment: the number, then the shop's own reference.
13
+ const TITLE = /^Shipment Tracking - (\d{14})(?:\s+-.*)?$/;
14
+ // A scan's wall clock, without a zone: "2026-01-02 13:45:06".
15
+ const CLOCK = /^(\d{4})-(\d{2})-(\d{2}) ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
16
+ // Bot checks a CDN or firewall could put in place of the page.
17
+ const CHALLENGE = /_cf_chl_opt|cf-turnstile|\/cdn-cgi\/challenge-platform\/|<title>\s*(?:Just a moment|Attention Required!|Access Denied)/i;
18
+ // A run of nine or more digits, with spaces, brackets, dots or hyphens between them, as phone numbers are written.
19
+ const PHONE = /(?<![A-Z\d])\+?\d(?:[\s().-]{0,2}\d){8,}(?![A-Z\d])/gi;
20
+ // A failed attempt's reason as the page words it, from the courier's list: letters only.
21
+ const REASON = /^[\p{L}][\p{L}\s'’/&().,-]{0,79}$/u;
22
+ const MAX_ROWS = 300;
23
+ /** A LaBaih number: fourteen digits, which begin with the date and time it was created. */
24
+ export function normalizeLabaihNumber(raw) {
25
+ const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
26
+ if (!NUMBER.test(number))
27
+ throw new InvalidInputError(PROVIDER, 'LaBaih numbers are fourteen digits');
28
+ return number;
29
+ }
30
+ /** A challenge or block page in place of the tracker. */
31
+ export function assertNotChallenged(html) {
32
+ if (CHALLENGE.test(html.slice(0, 64_000)))
33
+ throw new ChallengeError(PROVIDER, 'LaBaih answered with a browser check');
34
+ }
35
+ /**
36
+ * A scan's wording. A failed attempt's reason follows a colon and can go on,
37
+ * after another colon, with the recipient's district or city as given:
38
+ * "Unsuccessful Delivery: Wrong Customer District: …". Only the reason is
39
+ * kept, and only when it is words; phone numbers are dropped.
40
+ */
41
+ export function labaihWording(text) {
42
+ const [head = '', reason = ''] = clean(text, 500).split(':').map((part) => clean(part, 500));
43
+ const label = clean(head.replace(PHONE, ''), 160).replace(/[\s,;\-–]+$/, '');
44
+ return REASON.test(reason) ? `${label}: ${reason}` : label;
45
+ }
46
+ /** A scan's wall clock in `local_time` form, checked against the calendar. */
47
+ function wallClock(text) {
48
+ const match = CLOCK.exec(text);
49
+ const day = match ? calendarDay(Number(match[1]), Number(match[2]), Number(match[3])) : null;
50
+ if (!match || !day || Number(match[1]) < 2015)
51
+ throw new SchemaError(PROVIDER, 'LaBaih returned an invalid scan time');
52
+ return `${day}T${match[4]}:${match[5]}:${match[6]}`;
53
+ }
54
+ /**
55
+ * The tracker's English page for one number, `track.mylabaih.com/track/en/?cn=`,
56
+ * which the site renders on its server: a heading with the number and the shop's
57
+ * reference, the shop, a progress rail, then the history, newest first. Each
58
+ * scan has a wording, a wall clock without a zone and, for an arrival at a
59
+ * distribution centre, its city. An unknown number is a redirect the adapter
60
+ * reads before this. The shop, its reference and the rail are not kept.
61
+ */
62
+ export function parseLabaih(html, raw) {
63
+ const number = normalizeLabaihNumber(raw);
64
+ assertNotChallenged(html);
65
+ const $ = load(html);
66
+ const titles = $('.shipper-details h1');
67
+ if (titles.length === 0)
68
+ throw new SchemaError(PROVIDER, 'LaBaih returned a page without a shipment');
69
+ if (titles.length > 1)
70
+ throw new SchemaError(PROVIDER, 'LaBaih returned several shipments');
71
+ const shown = TITLE.exec(clean(titles.text(), 200))?.[1];
72
+ if (!shown)
73
+ throw new SchemaError(PROVIDER, 'LaBaih returned a shipment without its number');
74
+ if (shown !== number)
75
+ throw new IndeterminateError(PROVIDER, 'LaBaih answered for another shipment');
76
+ const lists = $('.shipment-details .collapsible');
77
+ if (lists.length !== 1)
78
+ throw new SchemaError(PROVIDER, 'LaBaih returned a page without its history');
79
+ const rows = lists.find('.list-update').toArray();
80
+ if (rows.length > MAX_ROWS)
81
+ throw new SchemaError(PROVIDER, 'LaBaih returned an oversized history');
82
+ const seen = new Set();
83
+ const scans = [];
84
+ rows.forEach((row, index) => {
85
+ const headings = $(row).find('h3');
86
+ const metas = $(row).find('.up-meta');
87
+ if (headings.length !== 1 || metas.length !== 1)
88
+ throw new SchemaError(PROVIDER, 'LaBaih returned an invalid scan');
89
+ const description = labaihWording(headings.text());
90
+ if (!description)
91
+ throw new SchemaError(PROVIDER, 'LaBaih returned a scan without its wording');
92
+ const location = clean(metas.find('strong').text(), 80);
93
+ const meta = metas.clone();
94
+ meta.find('strong').remove();
95
+ const clock = wallClock(clean(meta.text(), 60));
96
+ const staged = labaihStage(description);
97
+ const event = {
98
+ local_time: clock,
99
+ ...(location ? { location } : {}),
100
+ description,
101
+ ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
102
+ };
103
+ // A scan the page lists twice is kept once.
104
+ const identity = JSON.stringify(event);
105
+ if (seen.has(identity))
106
+ return;
107
+ seen.add(identity);
108
+ scans.push({ event, clock, index });
109
+ });
110
+ if (!scans.length)
111
+ throw new NoHistoryError(PROVIDER);
112
+ // Newest first; scans at the same second keep the page's order.
113
+ const events = scans.sort((left, right) => (left.clock < right.clock ? 1 : left.clock > right.clock ? -1 : left.index - right.index))
114
+ .map(({ event }) => event);
115
+ const newest = events[0];
116
+ const current = events.find((event) => event.stage);
117
+ const stage = current?.stage;
118
+ return {
119
+ status: stage ? languageStageStatus(stage) : 'unknown',
120
+ ...(stage ? { current_stage: stage, current_stage_source: current.stage_source } : {}),
121
+ last_status_text: newest.description,
122
+ last_update: null,
123
+ last_update_local: newest.local_time,
124
+ expected_delivery: null,
125
+ events,
126
+ };
127
+ }
@@ -0,0 +1,8 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import { type CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage LaBaih's wordings give a scan, in `normalizeStatusWording` form or as shown. */
4
+ export declare function labaihMappedStage(wording: string): Stage | undefined;
5
+ /** A scan's stage: LaBaih's wordings, else the shared wording rules, else nothing. */
6
+ export declare function labaihStage(wording: string): ClassifiedWording | null;
7
+ /** What the map says about one scan, by its wording: the tracker shows no codes. */
8
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,54 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ import { normalizeStatusWording } from '../../core/status/statusMap.js';
3
+ const key = normalizeStatusWording;
4
+ /**
5
+ * LaBaih's scan wordings, in English as the adapter reads them and in Arabic
6
+ * as the default page shows them. The tracker shows no codes. A parcel is
7
+ * collected from the online shop that sends it, arrives at a city's
8
+ * distribution centre and goes out to the customer from there.
9
+ */
10
+ const WORDINGS = new Map([
11
+ [key('Shipment created'), 'registered'],
12
+ [key('تم إنشاء الشحنة'), 'registered'],
13
+ [key('Shipment Collected From Partner'), 'accepted'],
14
+ [key('تم استلام الشحنة من الشريك'), 'accepted'],
15
+ // Back at the distribution centre after an attempt; the Arabic page words it as an arrival there.
16
+ [key('Returned To Facility'), 'in_transit'],
17
+ [key('وصلت الشحنة إلى مركز التوزيع'), 'in_transit'],
18
+ [key('In-Transit to Customer'), 'out_for_delivery'],
19
+ [key('الشحنة في طريقها إلى العميل'), 'out_for_delivery'],
20
+ [key('Unsuccessful Delivery'), 'failed_attempt'],
21
+ [key('محاولة توصيل غير ناجحة'), 'failed_attempt'],
22
+ [key('Shipment Delivered'), 'delivered'],
23
+ [key('تم توصيل الشحنة'), 'delivered'],
24
+ ]);
25
+ // An arrival at a city's distribution centre: "Arrived at Riyadh Facility", or in Arabic with "بـ" and the city.
26
+ const ARRIVED = /^(?:arrived at \S.* facility|وصلت الشحنة إلى مركز التوزيع بـ\S.*)$/u;
27
+ // A failed attempt keeps the reason the page gives after a colon: "Unsuccessful Delivery: Waiting time exceed".
28
+ const FAILED = /^(?:unsuccessful delivery|محاولة توصيل غير ناجحة):/u;
29
+ // Messages about the delivery location, which move nothing.
30
+ const NOTICES = [
31
+ 'SMS sent requesting location to customer',
32
+ 'Customer sent location',
33
+ 'العميل قام بإرسال إحداثيات التوصيل',
34
+ ].map(key);
35
+ /** The stage LaBaih's wordings give a scan, in `normalizeStatusWording` form or as shown. */
36
+ export function labaihMappedStage(wording) {
37
+ const normalized = key(wording);
38
+ return WORDINGS.get(normalized) ?? (ARRIVED.test(normalized) ? 'in_transit' : FAILED.test(normalized) ? 'failed_attempt' : undefined);
39
+ }
40
+ /** A scan's stage: LaBaih's wordings, else the shared wording rules, else nothing. */
41
+ export function labaihStage(wording) {
42
+ const mapped = labaihMappedStage(wording);
43
+ if (mapped)
44
+ return { stage: mapped, source: 'carrier_map' };
45
+ if (NOTICES.includes(key(wording)))
46
+ return null;
47
+ const worded = classifyWording(wording, 'in_transit');
48
+ return worded.source === 'none' ? null : worded;
49
+ }
50
+ /** What the map says about one scan, by its wording: the tracker shows no codes. */
51
+ export const statusMap = {
52
+ stage: (code, wording) => (code ? undefined : labaihMappedStage(wording)),
53
+ gaps: NOTICES.map((wording) => ({ wording, note: 'A message asking for, or giving, the delivery location; the parcel does not move.' })),
54
+ };
@@ -4,11 +4,23 @@
4
4
  {"wording": "تم إنشاء الشحنة", "stage": "registered", "language": "ar", "confirmedBy": "portal", "note": "Shipment created; the first event of every captured history."},
5
5
  {"wording": "تم استلام الشحنة من الشريك", "stage": "accepted", "language": "ar", "confirmedBy": "portal", "note": "Shipment collected from the partner, the online shop that sends it."},
6
6
  {"wording": "العميل قام بإرسال إحداثيات التوصيل", "language": "ar", "confirmedBy": "portal", "note": "The recipient sent the delivery location; not a parcel movement."},
7
- {"wording": "وصلت الشحنة إلى مركز التوزيع", "stage": "in_transit", "language": "ar", "confirmedBy": "portal", "note": "Shipment arrived at the distribution centre."},
7
+ {"wording": "وصلت الشحنة إلى مركز التوزيع", "stage": "in_transit", "language": "ar", "confirmedBy": "portal", "note": "Shipment arrived at the distribution centre. The Arabic page also words a return to the facility after an attempt this way."},
8
8
  {"wording": "وصلت الشحنة إلى مركز التوزيع بـالرياض", "stage": "in_transit", "language": "ar", "confirmedBy": "portal", "note": "The same event with the city appended; Jeddah appears the same way."},
9
9
  {"wording": "الشحنة في طريقها إلى العميل", "stage": "out_for_delivery", "language": "ar", "confirmedBy": "portal", "note": "Shipment on its way to the customer."},
10
10
  {"wording": "محاولة توصيل غير ناجحة", "stage": "failed_attempt", "language": "ar", "confirmedBy": "portal", "note": "Unsuccessful delivery attempt."},
11
+ {"wording": "تم توصيل الشحنة", "stage": "delivered", "language": "ar", "confirmedBy": "live", "note": "Shipment delivered."},
11
12
  {"wording": "تم استلام الشحنة", "stage": "accepted", "language": "ar", "confirmedBy": "portal", "note": "Step label in the tracker's progress bar."},
12
- {"wording": "تم التوصيل", "stage": "delivered", "language": "ar", "confirmedBy": "portal", "note": "Step label in the tracker's progress bar."}
13
+ {"wording": "تم التوصيل", "stage": "delivered", "language": "ar", "confirmedBy": "portal", "note": "Step label in the tracker's progress bar."},
14
+ {"wording": "Shipment created", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "The first event of every history."},
15
+ {"wording": "Shipment Collected From Partner", "stage": "accepted", "language": "en", "confirmedBy": "live", "note": "Collected from the online shop that sends the parcel."},
16
+ {"wording": "SMS sent requesting location to customer", "language": "en", "confirmedBy": "live", "note": "A text asking the recipient for the delivery location; only the English page lists it, and the parcel does not move."},
17
+ {"wording": "Customer sent location", "language": "en", "confirmedBy": "live", "note": "The recipient sent the delivery location; the parcel does not move."},
18
+ {"wording": "Arrived at Riyadh Facility", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Arrival at a city's distribution centre, whose city the scan also gives as its place; Jeddah appears the same way."},
19
+ {"wording": "In-Transit to Customer", "stage": "out_for_delivery", "language": "en", "confirmedBy": "live", "note": "On its way to the customer."},
20
+ {"wording": "Unsuccessful Delivery: Waiting time exceed", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "A failed attempt with the courier's reason after the colon."},
21
+ {"wording": "Unsuccessful Delivery: Wrong Customer District", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "The page adds the district as the recipient gave it after another colon; the adapter drops it."},
22
+ {"wording": "Unsuccessful Delivery: Wrong Customer City", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "The page adds the city as the recipient gave it after another colon; the adapter drops it."},
23
+ {"wording": "Returned To Facility", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Back at the distribution centre after a failed attempt, before going out again; the Arabic page words it as an arrival there."},
24
+ {"wording": "Shipment Delivered", "stage": "delivered", "language": "en", "confirmedBy": "live", "note": "Delivered."}
13
25
  ]
14
26
  }
@@ -202,6 +202,7 @@ import { statusMap as kyoungdong } from '../../carriers/kyoungdong/status.js';
202
202
  import { statusMap as lPost } from '../../carriers/l-post/status.js';
203
203
  import { statusMap as laPoste } from '../../carriers/la-poste/status.js';
204
204
  import { statusMap as laPosteSenegal } from '../../carriers/la-poste-senegal/status.js';
205
+ import { statusMap as labaih } from '../../carriers/labaih/status.js';
205
206
  import { statusMap as latvijasPasts } from '../../carriers/latvijas-pasts/status.js';
206
207
  import { statusMap as leopardsCourier } from '../../carriers/leopards-courier/status.js';
207
208
  import { statusMap as liccardi } from '../../carriers/liccardi/status.js';
@@ -404,7 +405,7 @@ export const STATUS_MAPS = {
404
405
  'intex-paketdienst': intexPaketdienst, 'israel-post': israelPost, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo,
405
406
  'j-and-t-middle-east': jAndTMiddleEast, janio, jeebly, 'jersey-post': jerseyPost, 'jet-logistic': jetLogistic, jhgj, jitsu, jne, 'jordan-post': jordanPost,
406
407
  jrt, 'js-express': jsExpress, kazpost, kex, 'kuehne-nagel': kuehneNagel, kyoungdong, 'l-post': lPost, 'la-poste': laPoste,
407
- 'la-poste-senegal': laPosteSenegal, 'latvijas-pasts': latvijasPasts,
408
+ 'la-poste-senegal': laPosteSenegal, labaih, 'latvijas-pasts': latvijasPasts,
408
409
  'leopards-courier': leopardsCourier, liccardi, 'lietuvos-pastas': lietuvosPastas, linhan, 'lion-parcel': lionParcel, logen, loggi, logoix, 'loomis-express': loomisExpress,
409
410
  'lotte-global': lotteGlobal, 'm-one': mOne, 'magyar-posta': magyarPosta, mailamericas, mainfreight, maltapost, matkahuolto, mayiyinfu, meest, 'messenger-cz': messengerCz,
410
411
  'metropolitan-warehouse': metropolitanWarehouse, 'mng-kargo': mngKargo, mtscn, mylerz, naqel, nationex, 'next-level-delivery': nextLevelDelivery, nipost, 'nippon-express': nipponExpress,
@@ -18174,7 +18174,9 @@
18174
18174
  "timezone": "Asia/Riyadh",
18175
18175
  "tracking": {
18176
18176
  "mode": "automatic",
18177
- "adapter": "universal"
18177
+ "adapter": "labaih",
18178
+ "recognitionRank": 788,
18179
+ "localClocks": true
18178
18180
  },
18179
18181
  "canaryUrl": "https://mylabaih.com/",
18180
18182
  "trackingUrlTemplate": "https://track.mylabaih.com/track/?cn={trackingNumber}",
@@ -18184,7 +18186,7 @@
18184
18186
  "domains": [
18185
18187
  "track.mylabaih.com"
18186
18188
  ],
18187
- "pathPattern": "^/track/?$",
18189
+ "pathPattern": "^/track(?:/en)?/?$",
18188
18190
  "params": [
18189
18191
  "cn"
18190
18192
  ]
@@ -11687,14 +11687,16 @@ export declare const CARRIER_CATALOG: {
11687
11687
  readonly timezone: "Asia/Riyadh";
11688
11688
  readonly tracking: {
11689
11689
  readonly mode: "automatic";
11690
- readonly adapter: "universal";
11690
+ readonly adapter: "labaih";
11691
+ readonly recognitionRank: 788;
11692
+ readonly localClocks: true;
11691
11693
  };
11692
11694
  readonly canaryUrl: "https://mylabaih.com/";
11693
11695
  readonly trackingUrlTemplate: "https://track.mylabaih.com/track/?cn={trackingNumber}";
11694
11696
  readonly trackingSearchUrl: "https://mylabaih.com/";
11695
11697
  readonly linkRules: readonly [{
11696
11698
  readonly domains: readonly ["track.mylabaih.com"];
11697
- readonly pathPattern: "^/track/?$";
11699
+ readonly pathPattern: "^/track(?:/en)?/?$";
11698
11700
  readonly params: readonly ["cn"];
11699
11701
  }];
11700
11702
  readonly detectionRules: readonly [];
@@ -18175,7 +18175,9 @@ export const CARRIER_CATALOG = {
18175
18175
  "timezone": "Asia/Riyadh",
18176
18176
  "tracking": {
18177
18177
  "mode": "automatic",
18178
- "adapter": "universal"
18178
+ "adapter": "labaih",
18179
+ "recognitionRank": 788,
18180
+ "localClocks": true
18179
18181
  },
18180
18182
  "canaryUrl": "https://mylabaih.com/",
18181
18183
  "trackingUrlTemplate": "https://track.mylabaih.com/track/?cn={trackingNumber}",
@@ -18185,7 +18187,7 @@ export const CARRIER_CATALOG = {
18185
18187
  "domains": [
18186
18188
  "track.mylabaih.com"
18187
18189
  ],
18188
- "pathPattern": "^/track/?$",
18190
+ "pathPattern": "^/track(?:/en)?/?$",
18189
18191
  "params": [
18190
18192
  "cn"
18191
18193
  ]
@@ -182,6 +182,7 @@ export const CARRIER_RECOGNITION_RANKS = {
182
182
  "kuehne-nagel": 435,
183
183
  "l-post": 808,
184
184
  "la-poste": 1000,
185
+ "labaih": 788,
185
186
  "latvijas-pasts": 100,
186
187
  "leopards-courier": 965,
187
188
  "lietuvos-pastas": 895,
@@ -241,6 +241,7 @@ import { adapter as kyoungdong } from '../carriers/kyoungdong/adapter.js';
241
241
  import { adapter as lPost } from '../carriers/l-post/adapter.js';
242
242
  import { adapter as laPoste } from '../carriers/la-poste/adapter.js';
243
243
  import { adapter as laPosteSenegal } from '../carriers/la-poste-senegal/adapter.js';
244
+ import { adapter as labaih } from '../carriers/labaih/adapter.js';
244
245
  import { adapter as landmarkGlobal } from '../carriers/landmark-global/adapter.js';
245
246
  import { adapter as latvijasPasts } from '../carriers/latvijas-pasts/adapter.js';
246
247
  import { adapter as lbcExpress } from '../carriers/lbc-express/adapter.js';
@@ -692,6 +693,7 @@ export const REGISTRY = {
692
693
  "l-post": lPost,
693
694
  "la-poste": laPoste,
694
695
  "la-poste-senegal": laPosteSenegal,
696
+ "labaih": labaih,
695
697
  "landmark-global": landmarkGlobal,
696
698
  "latvijas-pasts": latvijasPasts,
697
699
  "lbc-express": lbcExpress,
@@ -1298,7 +1300,7 @@ export const REGISTRY = {
1298
1300
  "la-poste-senegal": "la-poste-senegal",
1299
1301
  "la-poste-tunisienne": "universal",
1300
1302
  "laar-courier": "universal",
1301
- "labaih": "universal",
1303
+ "labaih": "labaih",
1302
1304
  "landmark-global": "landmark-global",
1303
1305
  "lao-post": "universal",
1304
1306
  "last-one-mile-coop": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.414.0"
5
+ "version": "0.415.0"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.414.0-main.2134",
3
+ "version": "0.415.0-main.2136",
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",