universal-parcel-scraper 0.769.9-main.4113 → 0.770.0-main.4115
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 +2 -2
- package/data/catalog.json +2 -1
- package/data/coverage-summary.json +1 -1
- package/dist/browser/scraper.js +2 -1
- package/dist/carriers/horoz/adapter.d.ts +17 -0
- package/dist/carriers/horoz/adapter.js +52 -0
- package/dist/carriers/horoz/carrier.json +53 -7
- package/dist/carriers/horoz/parser.d.ts +21 -0
- package/dist/carriers/horoz/parser.js +144 -0
- package/dist/carriers/horoz/status.d.ts +8 -0
- package/dist/carriers/horoz/status.js +39 -0
- package/dist/carriers/horoz/statuses.json +1 -0
- package/dist/core/catalog/statusMaps.js +2 -1
- package/dist/data/catalog.json +2 -1
- package/dist/generated/catalog.d.ts +2 -1
- package/dist/generated/catalog.js +2 -1
- package/dist/generated/registry.js +3 -1
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
|
|
15
15
|
<!-- GENERATED:summary -->
|
|
16
|
-
**3,500+ carriers** through **
|
|
16
|
+
**3,500+ carriers** through **632 dedicated adapters** and **5 universal fallbacks**
|
|
17
17
|
|
|
18
18
|
<sub>807 catalog entries · 135 countries represented</sub>
|
|
19
19
|
<!-- /GENERATED:summary -->
|
|
@@ -161,7 +161,7 @@ the command line's `track --source ParcelsApp` does the same.
|
|
|
161
161
|
<!-- GENERATED:stages -->
|
|
162
162
|
<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">
|
|
163
163
|
|
|
164
|
-
The carrier folders record 16,
|
|
164
|
+
The carrier folders record 16,929 such statuses, each filed under one stage.
|
|
165
165
|
<!-- /GENERATED:stages -->
|
|
166
166
|
|
|
167
167
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
package/data/catalog.json
CHANGED
|
@@ -16959,7 +16959,8 @@
|
|
|
16959
16959
|
"timezone": "Europe/Istanbul",
|
|
16960
16960
|
"tracking": {
|
|
16961
16961
|
"mode": "automatic",
|
|
16962
|
-
"adapter": "
|
|
16962
|
+
"adapter": "horoz",
|
|
16963
|
+
"localClocks": true
|
|
16963
16964
|
},
|
|
16964
16965
|
"canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
|
|
16965
16966
|
"trackingUrlTemplate": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
|
package/dist/browser/scraper.js
CHANGED
|
@@ -13098,7 +13098,8 @@ var Ze = {
|
|
|
13098
13098
|
timezone: "Europe/Istanbul",
|
|
13099
13099
|
tracking: {
|
|
13100
13100
|
mode: "automatic",
|
|
13101
|
-
adapter: "
|
|
13101
|
+
adapter: "horoz",
|
|
13102
|
+
localClocks: !0
|
|
13102
13103
|
},
|
|
13103
13104
|
canaryUrl: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
|
|
13104
13105
|
trackingUrlTemplate: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
|
|
@@ -0,0 +1,17 @@
|
|
|
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 delivery-note page, which looks up the number in its address. */
|
|
5
|
+
export declare const HOROZ_PAGE = "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx";
|
|
6
|
+
/** The delivery-note page's address for a number, as Horoz's links build it. */
|
|
7
|
+
export declare function horozLookupUrl(number: string): string;
|
|
8
|
+
export declare class HorozTracker {
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(options?: {
|
|
11
|
+
fetcher?: typeof fetch;
|
|
12
|
+
recorder?: StepRecorder;
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
});
|
|
15
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
16
|
+
}
|
|
17
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { ChallengeError, 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 { CARRIER, HOROZ_MAX_BYTES, normalizeHorozNumber, parseHoroz, PROVIDER } from './parser.js';
|
|
8
|
+
/** The delivery-note page, which looks up the number in its address. */
|
|
9
|
+
export const HOROZ_PAGE = 'https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx';
|
|
10
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
11
|
+
// The page answers an unknown number with empty fields: a missing route is not a missing shipment.
|
|
12
|
+
const GONE = [404, 405, 410];
|
|
13
|
+
/** The delivery-note page's address for a number, as Horoz's links build it. */
|
|
14
|
+
export function horozLookupUrl(number) {
|
|
15
|
+
return `${HOROZ_PAGE}?${new URLSearchParams({ Ptn: number })}`;
|
|
16
|
+
}
|
|
17
|
+
export class HorozTracker {
|
|
18
|
+
options;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
async fetch(raw, context = {}) {
|
|
23
|
+
const number = normalizeHorozNumber(raw);
|
|
24
|
+
const read = async (signal, timeoutMs) => {
|
|
25
|
+
const { response, bytes } = await fetchBounded(horozLookupUrl(number), { method: 'GET', signal, headers: {
|
|
26
|
+
Accept: 'text/html', 'User-Agent': userAgentOf(this.options.userAgent),
|
|
27
|
+
} }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: HOROZ_MAX_BYTES, redirect: 'manual',
|
|
28
|
+
fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, ...GONE, 403] });
|
|
29
|
+
const status = response.status;
|
|
30
|
+
if (status === 403)
|
|
31
|
+
throw new ChallengeError(PROVIDER, 'Horoz Lojistik refused the lookup', { status });
|
|
32
|
+
if (REDIRECTS.includes(status) || GONE.includes(status))
|
|
33
|
+
throw new TransportError(PROVIDER, 'Horoz Lojistik tracking page is unavailable', { status });
|
|
34
|
+
return parseHoroz(decodeText(bytes), number);
|
|
35
|
+
};
|
|
36
|
+
// A read that fails to reach the site, or hangs, gets one more try: the
|
|
37
|
+
// first gets half of the budget, so the retry fits in the rest.
|
|
38
|
+
return runSteps({ carrier: CARRIER, budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
39
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
40
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
|
|
41
|
+
networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const adapter = (environment) => {
|
|
46
|
+
const tracker = new HorozTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
47
|
+
return {
|
|
48
|
+
id: CARRIER, recordsSteps: true, steps: ['direct', 'retry'],
|
|
49
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
50
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeHorozNumber(number))),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -17,18 +17,64 @@
|
|
|
17
17
|
"timezone": "Europe/Istanbul",
|
|
18
18
|
"tracking": {
|
|
19
19
|
"mode": "automatic",
|
|
20
|
-
"adapter": "
|
|
21
|
-
"steps": [
|
|
20
|
+
"adapter": "horoz",
|
|
21
|
+
"steps": [
|
|
22
|
+
"direct",
|
|
23
|
+
"retry"
|
|
24
|
+
],
|
|
25
|
+
"localClocks": true
|
|
22
26
|
},
|
|
23
|
-
"capabilities": [
|
|
27
|
+
"capabilities": [
|
|
28
|
+
"history"
|
|
29
|
+
],
|
|
24
30
|
"portal": {
|
|
25
31
|
"url": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
|
|
26
32
|
"searchUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
|
|
27
33
|
"canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
|
|
28
|
-
"shows": [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
"shows": [
|
|
35
|
+
"status",
|
|
36
|
+
"history",
|
|
37
|
+
"location",
|
|
38
|
+
"eta",
|
|
39
|
+
"delivered_at",
|
|
40
|
+
"service_name",
|
|
41
|
+
"waybill",
|
|
42
|
+
"sender_name",
|
|
43
|
+
"recipient_name",
|
|
44
|
+
"origin",
|
|
45
|
+
"destination",
|
|
46
|
+
"recipient_address",
|
|
47
|
+
"parcel_count",
|
|
48
|
+
"volume",
|
|
49
|
+
"order_contents",
|
|
50
|
+
"vehicle"
|
|
51
|
+
],
|
|
52
|
+
"scraped": [
|
|
53
|
+
"status",
|
|
54
|
+
"history",
|
|
55
|
+
"location",
|
|
56
|
+
"eta"
|
|
57
|
+
],
|
|
58
|
+
"discarded": [
|
|
59
|
+
"delivered_at",
|
|
60
|
+
"service_name",
|
|
61
|
+
"waybill",
|
|
62
|
+
"sender_name",
|
|
63
|
+
"recipient_name",
|
|
64
|
+
"origin",
|
|
65
|
+
"destination",
|
|
66
|
+
"recipient_address",
|
|
67
|
+
"parcel_count",
|
|
68
|
+
"volume",
|
|
69
|
+
"order_contents",
|
|
70
|
+
"vehicle"
|
|
71
|
+
],
|
|
72
|
+
"unavailable": [
|
|
73
|
+
"provider_code",
|
|
74
|
+
"offset_timestamps",
|
|
75
|
+
"pickup_point",
|
|
76
|
+
"proof_of_delivery"
|
|
77
|
+
]
|
|
32
78
|
},
|
|
33
79
|
"links": [
|
|
34
80
|
{
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "Horoz Lojistik";
|
|
3
|
+
export declare const CARRIER = "horoz";
|
|
4
|
+
export declare const HOROZ_MAX_BYTES: number;
|
|
5
|
+
/** A number for Horoz Lojistik's tracker; anything else is not one it issues. */
|
|
6
|
+
export declare function normalizeHorozNumber(raw: string): string;
|
|
7
|
+
/** A firewall or bot-check page in place of the tracker's own. */
|
|
8
|
+
export declare function isBlockPage(html: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The delivery note (ATF) Horoz Lojistik's tracker shows at
|
|
11
|
+
* `frmAtfDetay.aspx?Ptn={number}`: a headline status, the note's details,
|
|
12
|
+
* the masked sender and recipient, and a table of movements, oldest first.
|
|
13
|
+
* Each movement is a departure from or an arrival at a centre, with its
|
|
14
|
+
* clock; a delivered parcel's last row is the delivery. A number the tracker
|
|
15
|
+
* does not know gets the same page with every field empty and no table.
|
|
16
|
+
*
|
|
17
|
+
* The page's form must name the whole number asked. The masked names, the
|
|
18
|
+
* provinces and addresses, the waybill, the vehicle plates, the destination
|
|
19
|
+
* centres and the record clocks are not read.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseHoroz(html: string, raw: string): CarrierResult;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
4
|
+
import { ChallengeError, IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
5
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
6
|
+
import { clean } from '../../core/transport/index.js';
|
|
7
|
+
import { horozStage } from './status.js';
|
|
8
|
+
export const PROVIDER = 'Horoz Lojistik';
|
|
9
|
+
export const CARRIER = 'horoz';
|
|
10
|
+
// A delivery note with a long movement list is under thirty kilobytes.
|
|
11
|
+
export const HOROZ_MAX_BYTES = 512 * 1024;
|
|
12
|
+
const NUMBER = /^\d{14}$/;
|
|
13
|
+
const HEADINGS = ['İşlem', 'Giriş/Çıkış Merkezi', 'Tarihi', 'Plaka', 'Varış Merkezi', 'İşlem Tarihi'];
|
|
14
|
+
// "14.03.2026 16:27:05": the movement's clock, with seconds.
|
|
15
|
+
const CLOCK = /^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})$/;
|
|
16
|
+
const DAY = /^(\d{2})\.(\d{2})\.(\d{4})$/;
|
|
17
|
+
// The page posts back to itself, so its form names the number it was opened with.
|
|
18
|
+
const FORM_ACTION = /^\.\/frmAtfDetay\.aspx\?Ptn=([^&#]*)$/;
|
|
19
|
+
// Firewall and bot-check pages that could stand in for the tracker's own.
|
|
20
|
+
const BLOCK = /<title>\s*(?:Just a moment|Attention Required|Access Denied|Request Rejected)|The requested URL was rejected|_cf_chl_opt|cf-turnstile|challenge-platform|captcha/i;
|
|
21
|
+
const MAX_ROWS = 500;
|
|
22
|
+
const FINAL = new Set(['delivered', 'returned']);
|
|
23
|
+
/** A number for Horoz Lojistik's tracker; anything else is not one it issues. */
|
|
24
|
+
export function normalizeHorozNumber(raw) {
|
|
25
|
+
const number = typeof raw === 'string' && raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
|
|
26
|
+
if (!NUMBER.test(number))
|
|
27
|
+
throw new InvalidInputError(PROVIDER, 'Horoz Lojistik numbers are fourteen digits');
|
|
28
|
+
return number;
|
|
29
|
+
}
|
|
30
|
+
/** A firewall or bot-check page in place of the tracker's own. */
|
|
31
|
+
export function isBlockPage(html) {
|
|
32
|
+
return BLOCK.test(html.slice(0, 64_000));
|
|
33
|
+
}
|
|
34
|
+
/** A movement's date and second as one wall time. The page gives no zone. */
|
|
35
|
+
function wallClock(text) {
|
|
36
|
+
const match = CLOCK.exec(text);
|
|
37
|
+
// UTC only checks the calendar fields; no offset is assigned.
|
|
38
|
+
const parsed = match ? DateTime.fromObject({ year: Number(match[3]), month: Number(match[2]), day: Number(match[1]),
|
|
39
|
+
hour: Number(match[4]), minute: Number(match[5]), second: Number(match[6]) }, { zone: 'UTC' }) : null;
|
|
40
|
+
// Luxon rolls 24:00 over to the next day; only a clock that reads back unchanged is kept.
|
|
41
|
+
if (!match || !parsed?.isValid || parsed.year < 2000 || parsed.toFormat('dd.MM.yyyy HH:mm:ss') !== text) {
|
|
42
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an invalid movement time');
|
|
43
|
+
}
|
|
44
|
+
return parsed.toFormat("yyyy-MM-dd'T'HH:mm:ss");
|
|
45
|
+
}
|
|
46
|
+
/** The delivery date the note expects, as a calendar day, when it gives a real one. */
|
|
47
|
+
function expectedDay(text) {
|
|
48
|
+
const match = DAY.exec(text);
|
|
49
|
+
const parsed = match ? DateTime.fromObject({ year: Number(match[3]), month: Number(match[2]), day: Number(match[1]) }, { zone: 'UTC' }) : null;
|
|
50
|
+
return parsed?.isValid && parsed.year >= 2000 && parsed.toFormat('dd.MM.yyyy') === text ? parsed.toISODate() : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The delivery note (ATF) Horoz Lojistik's tracker shows at
|
|
54
|
+
* `frmAtfDetay.aspx?Ptn={number}`: a headline status, the note's details,
|
|
55
|
+
* the masked sender and recipient, and a table of movements, oldest first.
|
|
56
|
+
* Each movement is a departure from or an arrival at a centre, with its
|
|
57
|
+
* clock; a delivered parcel's last row is the delivery. A number the tracker
|
|
58
|
+
* does not know gets the same page with every field empty and no table.
|
|
59
|
+
*
|
|
60
|
+
* The page's form must name the whole number asked. The masked names, the
|
|
61
|
+
* provinces and addresses, the waybill, the vehicle plates, the destination
|
|
62
|
+
* centres and the record clocks are not read.
|
|
63
|
+
*/
|
|
64
|
+
export function parseHoroz(html, raw) {
|
|
65
|
+
const number = normalizeHorozNumber(raw);
|
|
66
|
+
if (isBlockPage(html))
|
|
67
|
+
throw new ChallengeError(PROVIDER, 'Horoz Lojistik answered with a browser check');
|
|
68
|
+
const $ = load(html);
|
|
69
|
+
const forms = $('form#form1');
|
|
70
|
+
const action = forms.length === 1 ? FORM_ACTION.exec(forms.attr('action') ?? '') : null;
|
|
71
|
+
const note = $('span#lblatfno');
|
|
72
|
+
if (!action || note.length !== 1 || $('span#LblBaslik').length !== 1)
|
|
73
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an unexpected page');
|
|
74
|
+
let asked;
|
|
75
|
+
try {
|
|
76
|
+
asked = decodeURIComponent(action[1]);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an unexpected page');
|
|
80
|
+
}
|
|
81
|
+
if (asked !== number)
|
|
82
|
+
throw new IndeterminateError(PROVIDER, 'Horoz Lojistik answered for another shipment');
|
|
83
|
+
const tables = $('table#grdAtfHareket');
|
|
84
|
+
if (!clean(note.text(), 40)) {
|
|
85
|
+
// Empty fields and no movements are the answer for an unknown number.
|
|
86
|
+
if (!tables.length)
|
|
87
|
+
throw new NotFoundError(PROVIDER);
|
|
88
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned movements without a delivery note');
|
|
89
|
+
}
|
|
90
|
+
if (!tables.length)
|
|
91
|
+
throw new NoHistoryError(PROVIDER);
|
|
92
|
+
const rows = tables.length === 1 ? tables.find('tr') : null;
|
|
93
|
+
const headings = rows?.first().children('td').map((_, element) => clean($(element).text(), 40)).get() ?? [];
|
|
94
|
+
if (!rows || headings.join('|') !== HEADINGS.join('|'))
|
|
95
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik history layout changed');
|
|
96
|
+
if (rows.length - 1 > MAX_ROWS)
|
|
97
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an oversized history');
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const movements = [];
|
|
100
|
+
rows.slice(1).each((index, element) => {
|
|
101
|
+
const cells = $(element).children('td');
|
|
102
|
+
if (cells.length !== HEADINGS.length)
|
|
103
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an invalid movement');
|
|
104
|
+
const description = clean(cells.eq(0).text(), 200);
|
|
105
|
+
if (!description)
|
|
106
|
+
throw new SchemaError(PROVIDER, 'Horoz Lojistik returned a movement without wording');
|
|
107
|
+
const centre = clean(cells.eq(1).text(), 200);
|
|
108
|
+
const local = wallClock(clean(cells.eq(2).text(), 60));
|
|
109
|
+
// A row the page lists twice is kept once.
|
|
110
|
+
const identity = JSON.stringify([local, description, centre]);
|
|
111
|
+
if (seen.has(identity))
|
|
112
|
+
return;
|
|
113
|
+
seen.add(identity);
|
|
114
|
+
const staged = horozStage(description);
|
|
115
|
+
movements.push({ local, index, event: {
|
|
116
|
+
local_time: local,
|
|
117
|
+
description,
|
|
118
|
+
...(centre ? { location: centre } : {}),
|
|
119
|
+
...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
|
|
120
|
+
} });
|
|
121
|
+
});
|
|
122
|
+
if (!movements.length)
|
|
123
|
+
throw new NoHistoryError(PROVIDER);
|
|
124
|
+
// Newest first; of equal clocks, the row further down the page is the newer.
|
|
125
|
+
movements.sort((left, right) => right.local.localeCompare(left.local) || right.index - left.index);
|
|
126
|
+
const events = movements.map(({ event }) => event);
|
|
127
|
+
const newest = events[0];
|
|
128
|
+
// The headline sums the note up; without a stage of its own, the newest staged movement stands.
|
|
129
|
+
const headline = clean($('span#LblBaslik').text(), 200);
|
|
130
|
+
const summary = headline ? horozStage(headline) : null;
|
|
131
|
+
const latest = events.find((event) => event.stage);
|
|
132
|
+
const current = summary ?? (latest ? { stage: latest.stage, source: latest.stage_source } : null);
|
|
133
|
+
const stage = current?.stage;
|
|
134
|
+
const expected = stage && FINAL.has(stage) ? null : expectedDay(clean($('span#lblolasiteslimtarihi').text(), 40));
|
|
135
|
+
return {
|
|
136
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
137
|
+
...(stage ? { current_stage: stage, current_stage_source: current.source } : {}),
|
|
138
|
+
last_status_text: headline || newest.description,
|
|
139
|
+
last_update: null,
|
|
140
|
+
last_update_local: newest.local_time,
|
|
141
|
+
expected_delivery: expected,
|
|
142
|
+
events,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -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 Horoz Lojistik's map gives a wording in `normalizeStatusWording` form. */
|
|
4
|
+
export declare function horozMappedStage(wording: string): Stage | undefined;
|
|
5
|
+
/** A wording's stage: the map, else the shared wording rules, else nothing. */
|
|
6
|
+
export declare function horozStage(wording: string): ClassifiedWording | null;
|
|
7
|
+
/** Every wording seen is mapped. */
|
|
8
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { classifyWording } from '../../core/status/index.js';
|
|
2
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
3
|
+
/**
|
|
4
|
+
* The wordings of Horoz Lojistik's delivery note, which carry no code: the
|
|
5
|
+
* movements of its table, its headline, and the states complaints quote from
|
|
6
|
+
* its tracking screen. Another wording falls to the shared wording rules.
|
|
7
|
+
*/
|
|
8
|
+
const WORDINGS = new Map([
|
|
9
|
+
// A departure from, or an arrival at, a centre: the sender's depot or one of Horoz's.
|
|
10
|
+
['Çıkış', 'in_transit'],
|
|
11
|
+
['Giriş', 'in_transit'],
|
|
12
|
+
['dağıtım şubesinde', 'in_transit'],
|
|
13
|
+
['dağıtımda', 'out_for_delivery'],
|
|
14
|
+
['dağıtıma çıktı', 'out_for_delivery'],
|
|
15
|
+
// Not delivered: the recipient was not found, or the address was not enough.
|
|
16
|
+
['adreste bulunamadı', 'failed_attempt'],
|
|
17
|
+
['siparişiniz teslim edilememiştir', 'failed_attempt'],
|
|
18
|
+
['adres yetersiz', 'failed_attempt'],
|
|
19
|
+
['Teslim', 'delivered'],
|
|
20
|
+
['teslim edildi', 'delivered'],
|
|
21
|
+
['GÖNDERİNİZ TESLİM EDİLMİŞTİR', 'delivered'],
|
|
22
|
+
].map(([wording, stage]) => [normalizeStatusWording(wording), stage]));
|
|
23
|
+
/** The stage Horoz Lojistik's map gives a wording in `normalizeStatusWording` form. */
|
|
24
|
+
export function horozMappedStage(wording) {
|
|
25
|
+
return WORDINGS.get(wording);
|
|
26
|
+
}
|
|
27
|
+
/** A wording's stage: the map, else the shared wording rules, else nothing. */
|
|
28
|
+
export function horozStage(wording) {
|
|
29
|
+
const mapped = horozMappedStage(normalizeStatusWording(wording));
|
|
30
|
+
if (mapped)
|
|
31
|
+
return { stage: mapped, source: 'carrier_map' };
|
|
32
|
+
const worded = classifyWording(wording, 'in_transit');
|
|
33
|
+
return worded.source === 'none' ? null : worded;
|
|
34
|
+
}
|
|
35
|
+
/** Every wording seen is mapped. */
|
|
36
|
+
export const statusMap = {
|
|
37
|
+
stage: (_code, wording) => horozMappedStage(wording),
|
|
38
|
+
gaps: [],
|
|
39
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
{"wording": "Çıkış", "stage": "in_transit", "language": "tr", "confirmedBy": "portal", "note": "Departure from a centre, in the delivery note's movement list."},
|
|
5
5
|
{"wording": "Giriş", "stage": "in_transit", "language": "tr", "confirmedBy": "portal", "note": "Arrival at a centre, in the delivery note's movement list."},
|
|
6
6
|
{"wording": "Teslim", "stage": "delivered", "language": "tr", "confirmedBy": "portal", "note": "Last movement of a delivered parcel."},
|
|
7
|
+
{"wording": "GÖNDERİNİZ TESLİM EDİLMİŞTİR", "stage": "delivered", "language": "tr", "confirmedBy": "portal", "note": "The delivery note's headline once the parcel is delivered."},
|
|
7
8
|
{"wording": "dağıtım şubesinde", "stage": "in_transit", "language": "tr", "confirmedBy": "source", "note": "Quoted in Şikayetvar complaints as the tracking state; at the delivery branch."},
|
|
8
9
|
{"wording": "dağıtımda", "stage": "out_for_delivery", "language": "tr", "confirmedBy": "source", "note": "Quoted in Şikayetvar complaints as the tracking state."},
|
|
9
10
|
{"wording": "dağıtıma çıktı", "stage": "out_for_delivery", "language": "tr", "confirmedBy": "source", "note": "Quoted in a Şikayetvar complaint as the tracking state."},
|
|
@@ -285,6 +285,7 @@ import { statusMap as hermesDe } from '../../carriers/hermes-de/status.js';
|
|
|
285
285
|
import { statusMap as hispapost } from '../../carriers/hispapost/status.js';
|
|
286
286
|
import { statusMap as homexpress } from '../../carriers/homexpress/status.js';
|
|
287
287
|
import { statusMap as hongkongPost } from '../../carriers/hongkong-post/status.js';
|
|
288
|
+
import { statusMap as horoz } from '../../carriers/horoz/status.js';
|
|
288
289
|
import { statusMap as houngAhLoun } from '../../carriers/houng-ah-loun/status.js';
|
|
289
290
|
import { statusMap as hovership } from '../../carriers/hovership/status.js';
|
|
290
291
|
import { statusMap as hrParcel } from '../../carriers/hr-parcel/status.js';
|
|
@@ -680,7 +681,7 @@ export const STATUS_MAPS = {
|
|
|
680
681
|
globalpost, 'gms-worldwide': gmsWorldwide, 'go-express': goExpress, go4, gobolt, gofo, 'gofo-ca': gofoCa, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, 'gofo-nl': gofoNl, 'gogo-xpress': gogoXpress,
|
|
681
682
|
'grand-slam-express': grandSlamExpress, 'gs-postbox': gsPostbox, 'gsi-express': gsiExpress, 'gta-airskip': gtaAirskip, 'gtl-goto': gtlGoto, guatex, gzyl,
|
|
682
683
|
hanjin, 'happy-post': happyPost, haypost, 'hct-logistics': hctLogistics, helthjem, heppner, hepsijet, hermes, 'hermes-at': hermesAt, 'hermes-de': hermesDe, hispapost, homexpress, 'hongkong-post': hongkongPost,
|
|
683
|
-
'houng-ah-loun': houngAhLoun, hovership, 'hr-parcel': hrParcel, 'hrvatska-posta': hrvatskaPosta, huahan, 'hunter-express': hunterExpress, hywl,
|
|
684
|
+
horoz, 'houng-ah-loun': houngAhLoun, hovership, 'hr-parcel': hrParcel, 'hrvatska-posta': hrvatskaPosta, huahan, 'hunter-express': hunterExpress, hywl,
|
|
684
685
|
'icc-worldwide': iccWorldwide, 'ics-courier': icsCourier, 'ids-logistik': idsLogistik, 'ilyang-logis': ilyangLogis, imile,
|
|
685
686
|
'indah-logistik': indahLogistik, 'india-post': indiaPost, inpost, instabox, 'integral-pack': integralPack, 'intel-valley': intelValley, intelcom, 'internet-express': internetExpress, interrapidisimo,
|
|
686
687
|
'intex-paketdienst': intexPaketdienst, 'israel-post': israelPost, 'j-and-t': jAndT, 'j-and-t-au': jAndTAu, 'j-and-t-de': jAndTDe, 'j-and-t-cargo': jntCargo,
|
package/dist/data/catalog.json
CHANGED
|
@@ -16959,7 +16959,8 @@
|
|
|
16959
16959
|
"timezone": "Europe/Istanbul",
|
|
16960
16960
|
"tracking": {
|
|
16961
16961
|
"mode": "automatic",
|
|
16962
|
-
"adapter": "
|
|
16962
|
+
"adapter": "horoz",
|
|
16963
|
+
"localClocks": true
|
|
16963
16964
|
},
|
|
16964
16965
|
"canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
|
|
16965
16966
|
"trackingUrlTemplate": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
|
|
@@ -10990,7 +10990,8 @@ export declare const CARRIER_CATALOG: {
|
|
|
10990
10990
|
readonly timezone: "Europe/Istanbul";
|
|
10991
10991
|
readonly tracking: {
|
|
10992
10992
|
readonly mode: "automatic";
|
|
10993
|
-
readonly adapter: "
|
|
10993
|
+
readonly adapter: "horoz";
|
|
10994
|
+
readonly localClocks: true;
|
|
10994
10995
|
};
|
|
10995
10996
|
readonly canaryUrl: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx";
|
|
10996
10997
|
readonly trackingUrlTemplate: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}";
|
|
@@ -16960,7 +16960,8 @@ export const CARRIER_CATALOG = {
|
|
|
16960
16960
|
"timezone": "Europe/Istanbul",
|
|
16961
16961
|
"tracking": {
|
|
16962
16962
|
"mode": "automatic",
|
|
16963
|
-
"adapter": "
|
|
16963
|
+
"adapter": "horoz",
|
|
16964
|
+
"localClocks": true
|
|
16964
16965
|
},
|
|
16965
16966
|
"canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
|
|
16966
16967
|
"trackingUrlTemplate": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
|
|
@@ -274,6 +274,7 @@ import { adapter as hermesDe } from '../carriers/hermes-de/adapter.js';
|
|
|
274
274
|
import { adapter as hispapost } from '../carriers/hispapost/adapter.js';
|
|
275
275
|
import { adapter as homexpress } from '../carriers/homexpress/adapter.js';
|
|
276
276
|
import { adapter as hongkongPost } from '../carriers/hongkong-post/adapter.js';
|
|
277
|
+
import { adapter as horoz } from '../carriers/horoz/adapter.js';
|
|
277
278
|
import { adapter as houngAhLoun } from '../carriers/houng-ah-loun/adapter.js';
|
|
278
279
|
import { adapter as hovership } from '../carriers/hovership/adapter.js';
|
|
279
280
|
import { adapter as hrParcel } from '../carriers/hr-parcel/adapter.js';
|
|
@@ -907,6 +908,7 @@ export const REGISTRY = {
|
|
|
907
908
|
"hispapost": hispapost,
|
|
908
909
|
"homexpress": homexpress,
|
|
909
910
|
"hongkong-post": hongkongPost,
|
|
911
|
+
"horoz": horoz,
|
|
910
912
|
"houng-ah-loun": houngAhLoun,
|
|
911
913
|
"hovership": hovership,
|
|
912
914
|
"hr-parcel": hrParcel,
|
|
@@ -1603,7 +1605,7 @@ export const REGISTRY = {
|
|
|
1603
1605
|
"hispapost": "hispapost",
|
|
1604
1606
|
"homexpress": "homexpress",
|
|
1605
1607
|
"hongkong-post": "hongkong-post",
|
|
1606
|
-
"horoz": "
|
|
1608
|
+
"horoz": "horoz",
|
|
1607
1609
|
"hound-express": "universal",
|
|
1608
1610
|
"houng-ah-loun": "houng-ah-loun",
|
|
1609
1611
|
"hovership": "hovership",
|
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.770.0-main.4115",
|
|
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",
|