mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2026-02-27 11:35:30 +02:00
Compare commits
28 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e3a36e20 | ||
|
|
6fa0abbde5 | ||
|
|
0c39991e0c | ||
|
|
3cfff6eec7 | ||
|
|
a60cf63b2e | ||
|
|
9358c51363 | ||
|
|
785f5fcc37 | ||
|
|
ca6ceabbed | ||
|
|
d0120439e6 | ||
|
|
c2216120c9 | ||
|
|
bdbf4f3761 | ||
|
|
a1ab95c249 | ||
|
|
691f07b331 | ||
|
|
13351e3977 | ||
|
|
2f65f3d05d | ||
|
|
f31f56c00d | ||
|
|
31ef3ad56a | ||
|
|
5ac43bcfba | ||
|
|
5a2e4f5d13 | ||
|
|
2b1e816c7f | ||
|
|
b2d9a4e53e | ||
|
|
8e9d6ea67a | ||
|
|
d15369406d | ||
|
|
86f2302ad4 | ||
|
|
b59d7b3084 | ||
|
|
db4c03054a | ||
|
|
eac21d188b | ||
|
|
ad09f8b1be |
47 changed files with 9159 additions and 1533 deletions
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
|
|
@ -10,6 +10,9 @@ env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write # for OIDC-based publishing to npm
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-docker:
|
build-and-push-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -53,7 +56,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
# for OIDC-based publishing to npm
|
||||||
|
- name: setup npm v11
|
||||||
|
run: npm install -g npm@11
|
||||||
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm publish --provenance --access public
|
- run: npm publish --provenance --access public
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
@ -95,6 +95,8 @@
|
||||||
"bitmasks",
|
"bitmasks",
|
||||||
"Blaschkoallee",
|
"Blaschkoallee",
|
||||||
"Blissestr",
|
"Blissestr",
|
||||||
|
"bmis",
|
||||||
|
"BMIS",
|
||||||
"BNWNZF",
|
"BNWNZF",
|
||||||
"Böhme",
|
"Böhme",
|
||||||
"BONVOYO",
|
"BONVOYO",
|
||||||
|
|
@ -195,6 +197,7 @@
|
||||||
"Fehrbelliner",
|
"Fehrbelliner",
|
||||||
"Fernbf",
|
"Fernbf",
|
||||||
"Fernverkehr",
|
"Fernverkehr",
|
||||||
|
"firmen",
|
||||||
"Flexpreis",
|
"Flexpreis",
|
||||||
"Flix",
|
"Flix",
|
||||||
"Fltr",
|
"Fltr",
|
||||||
|
|
@ -263,6 +266,7 @@
|
||||||
"Hohenzollerndamm",
|
"Hohenzollerndamm",
|
||||||
"Hüngheim",
|
"Hüngheim",
|
||||||
"IBNR",
|
"IBNR",
|
||||||
|
"identifikationsart",
|
||||||
"Ihren",
|
"Ihren",
|
||||||
"Ihrer",
|
"Ihrer",
|
||||||
"Informationen",
|
"Informationen",
|
||||||
|
|
@ -610,6 +614,7 @@
|
||||||
"Zoologischer",
|
"Zoologischer",
|
||||||
"Zuege",
|
"Zuege",
|
||||||
"zugart",
|
"zugart",
|
||||||
|
"Zugehoerigkeit",
|
||||||
"zugattrib",
|
"zugattrib",
|
||||||
"zugattribute",
|
"zugattribute",
|
||||||
"Zuges",
|
"Zuges",
|
||||||
|
|
@ -627,7 +632,13 @@
|
||||||
"Intervalle",
|
"Intervalle",
|
||||||
"tagesbest",
|
"tagesbest",
|
||||||
"dbbahnhof",
|
"dbbahnhof",
|
||||||
"cancelation"
|
"Deutschlandticket",
|
||||||
|
"fahrverguenstigungen",
|
||||||
|
"cancelation",
|
||||||
|
"MOTIS",
|
||||||
|
"motis",
|
||||||
|
"Berechnung",
|
||||||
|
"fällt"
|
||||||
],
|
],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"docs/dumps/**",
|
"docs/dumps/**",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ Notes:
|
||||||
* routing-search returns polylines (!)
|
* routing-search returns polylines (!)
|
||||||
|
|
||||||
## Vendo/Movas Navigator API
|
## Vendo/Movas Navigator API
|
||||||
https://app.vendo.noncd.db.de/mob/
|
https://app.vendo.noncd.db.de/mob/ and/or https://app.services-bahn.de/
|
||||||
|
|
||||||
EPs:
|
EPs:
|
||||||
* bahnhofstafel/abfahrt
|
* bahnhofstafel/abfahrt
|
||||||
|
|
|
||||||
17
docs/dumps/PCAPdroid_21_Oct_17_55_08_routesearch.txt
Normal file
17
docs/dumps/PCAPdroid_21_Oct_17_55_08_routesearch.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
POST /mob/angebote/fahrplan HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
X-Correlation-ID: 599238b3-d1e0-43a0-9534-ebc0dbf30b72_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: 48cce61a-1b80-4dc4-8818-50bbb38ef54d
|
||||||
|
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
Content-Length: 822
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"fahrverguenstigungen":{"deutschlandTicketVorhanden":true,"nurDeutschlandTicketVerbindungen":false},"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@B\u003d1@p\u003d1760568530@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@B\u003d1@p\u003d1760568530@i\u003dU×008065969@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_17_55_26_routesearch.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_17_55_26_routesearch.txt
Normal file
Binary file not shown.
17
docs/dumps/PCAPdroid_21_Oct_17_58_35_deticket.txt
Normal file
17
docs/dumps/PCAPdroid_21_Oct_17_58_35_deticket.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
POST /mob/angebote/fahrplan HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
X-Correlation-ID: 599238b3-d1e0-43a0-9534-ebc0dbf30b72_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: 1f809128-7b90-411d-8fc4-99a329bc3bf6
|
||||||
|
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
Content-Length: 821
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"fahrverguenstigungen":{"deutschlandTicketVorhanden":true,"nurDeutschlandTicketVerbindungen":true},"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@B\u003d1@p\u003d1760568530@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@B\u003d1@p\u003d1760568530@i\u003dU×008065969@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_17_58_44_deticket.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_17_58_44_deticket.txt
Normal file
Binary file not shown.
17
docs/dumps/PCAPdroid_21_Oct_18_18_41_locsearch.txt
Normal file
17
docs/dumps/PCAPdroid_21_Oct_18_18_41_locsearch.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
POST /mob/location/search HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.location.v3+json
|
||||||
|
X-Correlation-ID: 599238b3-d1e0-43a0-9534-ebc0dbf30b72_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: a0f760ec-0443-4209-b564-e714ea548a16
|
||||||
|
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||||
|
Content-Length: 45
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
{"locationTypes":["ALL"],"searchTerm":"test"}
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_18_18_54_locsearch.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_18_18_54_locsearch.txt
Normal file
Binary file not shown.
17
docs/dumps/PCAPdroid_21_Oct_18_19_55_board.txt
Normal file
17
docs/dumps/PCAPdroid_21_Oct_18_19_55_board.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
POST /mob/bahnhofstafel/abfahrt HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||||
|
X-Correlation-ID: 9c9405f1-4792-4ec0-ba1f-ca231a15ac36_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: a8b79643-ab22-472d-ab6e-40d5b888b078
|
||||||
|
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||||
|
Content-Length: 401
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
{"anfragezeit":"17:47","datum":"2025-10-21","ursprungsBahnhofId":"A\u003d1@O\u003dTessin@X\u003d12462618@Y\u003d54032020@U\u003d81@L\u003d8013106@B\u003d1@p\u003d1760557009@i\u003dU×008027109@","verkehrsmittel":["HOCHGESCHWINDIGKEITSZUEGE","INTERCITYUNDEUROCITYZUEGE","INTERREGIOUNDSCHNELLZUEGE","NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"]}
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_18_20_05_board.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_18_20_05_board.txt
Normal file
Binary file not shown.
15
docs/dumps/PCAPdroid_21_Oct_18_20_53_zuglauf.txt
Normal file
15
docs/dumps/PCAPdroid_21_Oct_18_20_53_zuglauf.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
GET /mob/zuglauf/2%7C%23VN%231%23ST%231760568530%23PI%230%23ZI%23237216%23TA%230%23DA%23211025%231S%238013106%231T%231814%23LS%238010381%23LT%232015%23PU%2380%23RT%231%23CA%23RB%23ZE%2313134%23ZB%23RB%20%20%20%20%20%20%20%20%20%2013134%23PC%233%23FR%238013106%23FT%231814%23TO%238010381%23TT%232015%23 HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.zuglauf.v2+json
|
||||||
|
Content-Type: application/x.db.vendo.mob.zuglauf.v2+json
|
||||||
|
X-Correlation-ID: 9c9405f1-4792-4ec0-ba1f-ca231a15ac36_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: 1a70c54c-b0a8-4d8a-a23b-d6da23a50423
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_18_21_01_zuglauf.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_18_21_01_zuglauf.txt
Normal file
Binary file not shown.
17
docs/dumps/PCAPdroid_21_Oct_18_23_03_reservierung.txt
Normal file
17
docs/dumps/PCAPdroid_21_Oct_18_23_03_reservierung.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
POST /mob/angebote/recon/autonomereservierung HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
X-Correlation-ID: 232a47fb-0827-4e11-aa2e-d141c718f63f_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: 0b0fdc2d-be90-4e00-b5ed-33bf80fe2e23
|
||||||
|
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
Content-Length: 1862
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
{"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false,"suchParameter":{"reisewunschHin":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@i\u003dU×008065969@"}},"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8011160@a\u003d128@$202510211802$202510212203$ICE 1032$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzEjSElOIzAjRUNLIzQ1MDM2Mnw0NTAzNjJ8NDUwNjAzfDQ1MDYwM3wwfDB8NDg1fDQ1MDM0NHwxfDB8MTA1MHwwfDB8LTIxNDc0ODM2NDgjR0FNIzIxMTAyNTE4MDIjClojVk4jMSNTVCMxNzYwNTY4NTMwI1BJIzAjWkkjMjExNTc4I1RBIzAjREEjMjExMDI1IzFTIzgwMDAxMDUjMVQjMTgwMiNMUyM4MDEwMjU1I0xUIzIyMTUjUFUjODAjUlQjMSNDQSNJQ0UjWkUjMTAzMiNaQiNJQ0UgICAgICAgICAgMTAzMiNQQyMwI0ZSIzgwMDAxMDUjRlQjMTgwMiNUTyM4MDExMTYwI1RUIzIyMDMj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P4UrDMBSFX0XuL4U6knTJ0kKhdqWoTDfGJor4I67prHbtTFOxlD6HD+SLmbaKgiKBkHM493w3DbxIBS7gER+DBfJVGxEGo6twhIkxlHwGt4G82kXgUqt7BOAiC4pKh0JLkyaIUIwIht5cpbvOxJPxBCFjJX3DMbbgMa+jTKsZuLcN6HrfxRbLeWhCuyLu1Nnl1IgXkVV9BSI2tHf9UtOH7VBsyLHcz4rNUJOlsUmeeNife5ES+VNSKX14IdL86PQ+8a89zhjhtu3feBRhxBgn/trjyJ+ZCxmH+oEZ3nt4whBlnNrIT731+xtCHGEzwH2zUamHn0b9OkKpP/mBVFmaHwxcbNvMoWOnAxNzKHe+wRhjhv4BM+ow5zd4K3W4XIGrVSV7tSiy2iDlD+u8qFQu66Co8rgENxFZ+ZkVZZmlpf7Kyk2xEErsTKhp2/YDhSDWlQgCAAA\u003d"}}
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_18_23_16_reservierung.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_18_23_16_reservierung.txt
Normal file
Binary file not shown.
17
docs/dumps/PCAPdroid_21_Oct_18_23_42_recon.txt
Normal file
17
docs/dumps/PCAPdroid_21_Oct_18_23_42_recon.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
POST /mob/angebote/recon HTTP/1.1
|
||||||
|
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
X-Correlation-ID: 232a47fb-0827-4e11-aa2e-d141c718f63f_339465f1-2b99-4c6e-b739-c8ad0efed49a
|
||||||
|
X-Device-Os-Name: Android
|
||||||
|
X-Device-Os-Version: 32
|
||||||
|
X-Device-Model: Google Pixel 3a
|
||||||
|
X-App-Version: 25.18.2
|
||||||
|
User-Agent: DBNavigator/Android/25.18.2
|
||||||
|
Accept-Language: en,de
|
||||||
|
X-INSTANA-ANDROID: 746aa2dd-bc0c-450c-a444-f6036963caf9
|
||||||
|
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
|
||||||
|
Content-Length: 1962
|
||||||
|
Host: app.services-bahn.de
|
||||||
|
Connection: Keep-Alive
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
{"einstiegsTypList":["STANDARD"],"fahrverguenstigungen":{"deutschlandTicketVorhanden":true,"nurDeutschlandTicketVerbindungen":false},"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false,"suchParameter":{"reisewunschHin":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@i\u003dU×008065969@"}},"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8011160@a\u003d128@$202510211802$202510212203$ICE 1032$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzEjSElOIzAjRUNLIzQ1MDM2Mnw0NTAzNjJ8NDUwNjAzfDQ1MDYwM3wwfDB8NDg1fDQ1MDM0NHwxfDB8MTA1MHwwfDB8LTIxNDc0ODM2NDgjR0FNIzIxMTAyNTE4MDIjClojVk4jMSNTVCMxNzYwNTY4NTMwI1BJIzAjWkkjMjExNTc4I1RBIzAjREEjMjExMDI1IzFTIzgwMDAxMDUjMVQjMTgwMiNMUyM4MDEwMjU1I0xUIzIyMTUjUFUjODAjUlQjMSNDQSNJQ0UjWkUjMTAzMiNaQiNJQ0UgICAgICAgICAgMTAzMiNQQyMwI0ZSIzgwMDAxMDUjRlQjMTgwMiNUTyM4MDExMTYwI1RUIzIyMDMj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P4UrDMBSFX0XuL4U6knTJ0kKhdqWoTDfGJor4I67prHbtTFOxlD6HD+SLmbaKgiKBkHM493w3DbxIBS7gER+DBfJVGxEGo6twhIkxlHwGt4G82kXgUqt7BOAiC4pKh0JLkyaIUIwIht5cpbvOxJPxBCFjJX3DMbbgMa+jTKsZuLcN6HrfxRbLeWhCuyLu1Nnl1IgXkVV9BSI2tHf9UtOH7VBsyLHcz4rNUJOlsUmeeNife5ES+VNSKX14IdL86PQ+8a89zhjhtu3feBRhxBgn/trjyJ+ZCxmH+oEZ3nt4whBlnNrIT731+xtCHGEzwH2zUamHn0b9OkKpP/mBVFmaHwxcbNvMoWOnAxNzKHe+wRhjhv4BM+ow5zd4K3W4XIGrVSV7tSiy2iDlD+u8qFQu66Co8rgENxFZ+ZkVZZmlpf7Kyk2xEErsTKhp2/YDhSDWlQgCAAA\u003d"}}
|
||||||
BIN
docs/dumps/PCAPdroid_21_Oct_18_23_50_recon.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_18_23_50_recon.txt
Normal file
Binary file not shown.
BIN
docs/dumps/PCAPdroid_21_Oct_18_24_19_routesearch_delay.txt
Normal file
BIN
docs/dumps/PCAPdroid_21_Oct_18_24_19_routesearch_delay.txt
Normal file
Binary file not shown.
|
|
@ -81,6 +81,7 @@ With `opt`, you can override the default options, which look like this:
|
||||||
firstClass: false, // first or second class for tickets
|
firstClass: false, // first or second class for tickets
|
||||||
loyaltyCard: null, // BahnCards etc., see below
|
loyaltyCard: null, // BahnCards etc., see below
|
||||||
language: 'en', // language to get results in
|
language: 'en', // language to get results in
|
||||||
|
bmisNumber: null, // 7-digit BMIS number for business customer rates
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -316,6 +317,27 @@ hafas.journeys(from, to, {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Using the `bmisNumber` option
|
||||||
|
|
||||||
|
bahn.business customers with a BMIS number can get their corporate rates and corporate tariffs by providing their 7-digit BMIS number:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Option 1: Using the bmisNumber parameter directly
|
||||||
|
await client.journeys(from, to, {
|
||||||
|
bmisNumber: '1234567' // Your 7-digit BMIS number
|
||||||
|
})
|
||||||
|
|
||||||
|
// Option 2: Using the createBusinessClient helper function
|
||||||
|
import {createBusinessClient} from 'db-vendo-client'
|
||||||
|
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||||
|
|
||||||
|
const businessClient = createBusinessClient(dbProfile, userAgent, '1234567')
|
||||||
|
// Now all journey searches will automatically include the BMIS number
|
||||||
|
await businessClient.journeys(from, to)
|
||||||
|
```
|
||||||
|
|
||||||
|
When a BMIS number is provided, the request will include a `firmenZugehoerigkeit` object with the BMIS number and identification type, allowing business customers to access their special rates and conditions.
|
||||||
|
|
||||||
## The `routingMode` option
|
## The `routingMode` option
|
||||||
|
|
||||||
The `routingMode` option is not supported by db-vendo-client. The behavior will be the same as the [`HYBRID` mode of hafas-client](https://github.com/public-transport/hafas-client/blob/main/p/db/readme.md#using-the-routingmode-option), i.e. cancelled trains/infeasible journeys will also be contained for informational purpose.
|
The `routingMode` option is not supported by db-vendo-client. The behavior will be the same as the [`HYBRID` mode of hafas-client](https://github.com/public-transport/hafas-client/blob/main/p/db/readme.md#using-the-routingmode-option), i.e. cancelled trains/infeasible journeys will also be contained for informational purpose.
|
||||||
|
|
@ -10,7 +10,7 @@ With `opt`, you can override the default options, which look like this:
|
||||||
{
|
{
|
||||||
results: 8, // maximum number of results
|
results: 8, // maximum number of results
|
||||||
distance: null, // maximum walking distance in meters
|
distance: null, // maximum walking distance in meters
|
||||||
poi: false, // return points of interest?
|
poi: false, // not supported
|
||||||
stops: true, // return stops/stations?
|
stops: true, // return stops/stations?
|
||||||
subStops: true, // not supported
|
subStops: true, // not supported
|
||||||
entrances: true, // not supported
|
entrances: true, // not supported
|
||||||
|
|
|
||||||
|
|
@ -613,7 +613,7 @@ paths:
|
||||||
default: true
|
default: true
|
||||||
- name: linesOfStops
|
- name: linesOfStops
|
||||||
in: query
|
in: query
|
||||||
description: Parse & return lines of each stop/station?
|
description: not supported
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
@ -1887,11 +1887,11 @@ components:
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
remarks:
|
remarks:
|
||||||
|
|
@ -1995,11 +1995,11 @@ components:
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
remarks:
|
remarks:
|
||||||
|
|
@ -2034,15 +2034,15 @@ components:
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
linesOfStops:
|
linesOfStops:
|
||||||
description: parse & expose lines at each stop/station?
|
description: not supported
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
language:
|
language:
|
||||||
|
|
@ -2061,11 +2061,11 @@ components:
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
remarks:
|
remarks:
|
||||||
|
|
@ -2088,11 +2088,11 @@ components:
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
remarks:
|
remarks:
|
||||||
|
|
@ -2128,11 +2128,11 @@ components:
|
||||||
default: 10
|
default: 10
|
||||||
type: number
|
type: number
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
linesOfStops:
|
linesOfStops:
|
||||||
|
|
@ -2206,7 +2206,7 @@ components:
|
||||||
default: undefined
|
default: undefined
|
||||||
type: number
|
type: number
|
||||||
poi:
|
poi:
|
||||||
description: return points of interest?
|
description: not supported
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
stops:
|
stops:
|
||||||
|
|
@ -2218,15 +2218,15 @@ components:
|
||||||
description: products
|
description: products
|
||||||
default: undefined
|
default: undefined
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
linesOfStops:
|
linesOfStops:
|
||||||
description: parse & expose lines at each stop/station?
|
description: not supported
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
language:
|
language:
|
||||||
|
|
@ -2254,11 +2254,11 @@ components:
|
||||||
description: products
|
description: products
|
||||||
default: undefined
|
default: undefined
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
polylines:
|
polylines:
|
||||||
|
|
@ -2296,11 +2296,11 @@ components:
|
||||||
default: 20
|
default: 20
|
||||||
type: number
|
type: number
|
||||||
subStops:
|
subStops:
|
||||||
description: parse & expose sub-stops of stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
entrances:
|
entrances:
|
||||||
description: parse & expose entrances of stops/stations?
|
description: not supported
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
polylines:
|
polylines:
|
||||||
|
|
|
||||||
34
index.js
34
index.js
|
|
@ -111,7 +111,7 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||||
|
|
||||||
const ctx = {profile, opt, common, res};
|
const ctx = {profile, opt, common, res};
|
||||||
let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries.flat())
|
let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries?.flat() || [])
|
||||||
.map(res => parse(ctx, res)); // TODO sort?, slice
|
.map(res => parse(ctx, res)); // TODO sort?, slice
|
||||||
|
|
||||||
if (!opt.includeRelatedStations) {
|
if (!opt.includeRelatedStations) {
|
||||||
|
|
@ -181,6 +181,9 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
scheduledDays: false, // parse & expose dates each journey is valid on?
|
scheduledDays: false, // parse & expose dates each journey is valid on?
|
||||||
notOnlyFastRoutes: false, // if true, also show routes that are mathematically non-optimal
|
notOnlyFastRoutes: false, // if true, also show routes that are mathematically non-optimal
|
||||||
bestprice: false, // search for lowest prices across the entire day
|
bestprice: false, // search for lowest prices across the entire day
|
||||||
|
deutschlandTicketDiscount: false,
|
||||||
|
deutschlandTicketConnectionsOnly: false,
|
||||||
|
bmisNumber: null, // 7-digit BMIS number for business customer rates
|
||||||
}, opt);
|
}, opt);
|
||||||
|
|
||||||
if (opt.when !== undefined) {
|
if (opt.when !== undefined) {
|
||||||
|
|
@ -239,6 +242,9 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
entrances: true, // parse & expose entrances of stops/stations?
|
entrances: true, // parse & expose entrances of stops/stations?
|
||||||
remarks: true, // parse & expose hints & warnings?
|
remarks: true, // parse & expose hints & warnings?
|
||||||
scheduledDays: false, // parse & expose dates the journey is valid on?
|
scheduledDays: false, // parse & expose dates the journey is valid on?
|
||||||
|
deutschlandTicketDiscount: false,
|
||||||
|
deutschlandTicketConnectionsOnly: false,
|
||||||
|
bmisNumber: null, // 7-digit BMIS number for business customer rates
|
||||||
}, opt);
|
}, opt);
|
||||||
|
|
||||||
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
|
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
|
||||||
|
|
@ -391,7 +397,33 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createBusinessClient = (profile, userAgent, bmisNumber, opt = {}) => {
|
||||||
|
if (!bmisNumber || typeof bmisNumber !== 'string') {
|
||||||
|
throw new TypeError('bmisNumber must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client with BMIS number included in all journey requests
|
||||||
|
const client = createClient(profile, userAgent, opt);
|
||||||
|
|
||||||
|
// Wrap journeys method to always include BMIS number
|
||||||
|
const originalJourneys = client.journeys;
|
||||||
|
client.journeys = async (from, to, opt = {}) => {
|
||||||
|
return originalJourneys(from, to, {...opt, bmisNumber});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wrap refreshJourney method to always include BMIS number
|
||||||
|
if (client.refreshJourney) {
|
||||||
|
const originalRefreshJourney = client.refreshJourney;
|
||||||
|
client.refreshJourney = async (refreshToken, opt = {}) => {
|
||||||
|
return originalRefreshJourney(refreshToken, {...opt, bmisNumber});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createClient,
|
createClient,
|
||||||
|
createBusinessClient,
|
||||||
loadEnrichedStationData,
|
loadEnrichedStationData,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,18 @@ const mapRouteParsers = (route, parsers) => {
|
||||||
default: false,
|
default: false,
|
||||||
parse: parseBoolean,
|
parse: parseBoolean,
|
||||||
},
|
},
|
||||||
|
deutschlandTicketDiscount: {
|
||||||
|
description: 'Calculate ticket prices assuming Deutschlandticket is present',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
parse: parseBoolean,
|
||||||
|
},
|
||||||
|
deutschlandTicketConnectionsOnly: {
|
||||||
|
description: 'Only return journeys that can be used with the Deutschlandticket',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
parse: parseBoolean,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (route.includes('departures') || route.includes('arrivals')) {
|
if (route.includes('departures') || route.includes('arrivals')) {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ const request = async (ctx, userAgent, reqData) => {
|
||||||
if (reqOptions.query) {
|
if (reqOptions.query) {
|
||||||
url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
|
url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
|
||||||
}
|
}
|
||||||
console.log(url);
|
|
||||||
const reqId = randomBytesHexString(6);
|
const reqId = randomBytesHexString(6);
|
||||||
const fetchReq = new Request(url, reqOptions);
|
const fetchReq = new Request(url, reqOptions);
|
||||||
profile.logRequest(ctx, fetchReq, reqId);
|
profile.logRequest(ctx, fetchReq, reqId);
|
||||||
|
|
@ -122,6 +121,7 @@ const request = async (ctx, userAgent, reqData) => {
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// todo [breaking]: make this a FetchError or a HafasClientError?
|
// todo [breaking]: make this a FetchError or a HafasClientError?
|
||||||
|
console.log(JSON.stringify(res), await res.text());
|
||||||
const err = new Error(res.statusText);
|
const err = new Error(res.statusText);
|
||||||
Object.assign(err, errProps);
|
Object.assign(err, errProps);
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
||||||
12
p/db/base.json
Normal file
12
p/db/base.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
|
||||||
|
"bestpriceEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/tagesbestpreis",
|
||||||
|
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon",
|
||||||
|
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
|
||||||
|
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
|
||||||
|
"stopEndpoint": "https://app.vendo.noncd.db.de/mob/location/details/",
|
||||||
|
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
|
||||||
|
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf/",
|
||||||
|
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/",
|
||||||
|
"defaultLanguage": "en"
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,36 @@
|
||||||
import dbnavBase from '../dbnav/base.json' with { type: 'json' };
|
import base from './base.json' with { type: 'json' };
|
||||||
import dbregioguideBase from '../dbregioguide/base.json' with { type: 'json' };
|
|
||||||
import {products} from '../../lib/products.js';
|
import {products} from '../../lib/products.js';
|
||||||
|
|
||||||
// journeys()
|
// journeys()
|
||||||
import {formatJourneysReq} from '../dbnav/journeys-req.js';
|
import {formatJourneysReq} from '../dbnav/journeys-req.js';
|
||||||
const {journeysEndpoint} = dbnavBase;
|
const {journeysEndpoint} = base;
|
||||||
|
const {bestpriceEndpoint} = base;
|
||||||
|
|
||||||
// refreshJourneys()
|
// refreshJourneys()
|
||||||
import {formatRefreshJourneyReq} from '../dbnav/journeys-req.js';
|
import {formatRefreshJourneyReq} from '../dbnav/journeys-req.js';
|
||||||
const {refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline} = dbnavBase;
|
const {refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline} = base;
|
||||||
|
|
||||||
// locations()
|
// locations()
|
||||||
import {formatLocationsReq} from '../dbnav/locations-req.js';
|
import {formatLocationsReq} from '../dbnav/locations-req.js';
|
||||||
import {formatLocationFilter} from '../dbnav/location-filter.js';
|
import {formatLocationFilter} from '../dbnav/location-filter.js';
|
||||||
const {locationsEndpoint} = dbnavBase;
|
const {locationsEndpoint} = base;
|
||||||
|
|
||||||
// stop()
|
// stop()
|
||||||
import {formatStopReq} from '../dbnav/stop-req.js';
|
import {formatStopReq} from '../dbnav/stop-req.js';
|
||||||
import {parseStop} from '../dbnav/parse-stop.js';
|
import {parseStop} from '../dbnav/parse-stop.js';
|
||||||
const {stopEndpoint} = dbnavBase;
|
const {stopEndpoint} = base;
|
||||||
|
|
||||||
// nearby()
|
// nearby()
|
||||||
import {formatNearbyReq} from '../dbnav/nearby-req.js';
|
import {formatNearbyReq} from '../dbnav/nearby-req.js';
|
||||||
const {nearbyEndpoint} = dbnavBase;
|
const {nearbyEndpoint} = base;
|
||||||
|
|
||||||
// trip()
|
// trip()
|
||||||
import {formatTripReq} from './trip-req.js';
|
import {formatTripReq} from './trip-req.js';
|
||||||
const tripEndpoint_dbnav = dbnavBase.tripEndpoint;
|
const {tripEndpoint} = base;
|
||||||
const tripEndpoint_dbregioguide = dbregioguideBase.tripEndpoint;
|
|
||||||
|
|
||||||
// arrivals(), departures()
|
// arrivals(), departures()
|
||||||
import {formatStationBoardReq} from '../dbregioguide/station-board-req.js';
|
import {formatStationBoardReq} from '../dbnav/station-board-req.js';
|
||||||
const {boardEndpoint} = dbregioguideBase;
|
const {boardEndpoint} = base;
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
locale: 'de-DE',
|
locale: 'de-DE',
|
||||||
|
|
@ -41,6 +40,7 @@ const profile = {
|
||||||
|
|
||||||
formatJourneysReq,
|
formatJourneysReq,
|
||||||
journeysEndpoint,
|
journeysEndpoint,
|
||||||
|
bestpriceEndpoint,
|
||||||
|
|
||||||
formatRefreshJourneyReq,
|
formatRefreshJourneyReq,
|
||||||
refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline,
|
refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline,
|
||||||
|
|
@ -55,7 +55,7 @@ const profile = {
|
||||||
nearbyEndpoint,
|
nearbyEndpoint,
|
||||||
|
|
||||||
formatTripReq,
|
formatTripReq,
|
||||||
tripEndpoint_dbnav, tripEndpoint_dbregioguide,
|
tripEndpoint,
|
||||||
|
|
||||||
formatStationBoardReq,
|
formatStationBoardReq,
|
||||||
boardEndpoint,
|
boardEndpoint,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js';
|
import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js';
|
||||||
import {formatTripReq as risTripReq} from '../dbregioguide/trip-req.js';
|
|
||||||
|
|
||||||
|
|
||||||
const formatTripReq = ({profile, opt}, id) => {
|
const formatTripReq = ({profile, opt}, id) => {
|
||||||
const _profile = {...profile};
|
const _profile = {...profile};
|
||||||
if (id.includes('#')) {
|
_profile['tripEndpoint'] = profile.tripEndpoint;
|
||||||
_profile['tripEndpoint'] = profile.tripEndpoint_dbnav;
|
return hafasFormatTripReq({profile: _profile, opt}, id);
|
||||||
return hafasFormatTripReq({profile: _profile, opt}, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_profile['tripEndpoint'] = profile.tripEndpoint_dbregioguide;
|
|
||||||
return risTripReq({profile: _profile, opt}, id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
|
"journeysEndpoint": "https://app.services-bahn.de/mob/angebote/fahrplan",
|
||||||
"bestpriceEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/tagesbestpreis",
|
"bestpriceEndpoint": "https://app.services-bahn.de/mob/angebote/tagesbestpreis",
|
||||||
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon",
|
"refreshJourneysEndpointTickets": "https://app.services-bahn.de/mob/angebote/recon",
|
||||||
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
|
"refreshJourneysEndpointPolyline": "https://app.services-bahn.de/mob/trip/recon",
|
||||||
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
|
"locationsEndpoint": "https://app.services-bahn.de/mob/location/search",
|
||||||
"stopEndpoint": "https://app.vendo.noncd.db.de/mob/location/details/",
|
"stopEndpoint": "https://app.services-bahn.de/mob/location/details/",
|
||||||
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
|
"nearbyEndpoint": "https://app.services-bahn.de/mob/location/nearby",
|
||||||
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf/",
|
"tripEndpoint": "https://app.services-bahn.de/mob/zuglauf/",
|
||||||
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/",
|
"boardEndpoint": "https://app.services-bahn.de/mob/bahnhofstafel/",
|
||||||
"defaultLanguage": "en"
|
"defaultLanguage": "en"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,15 @@ const formatBaseJourneysReq = (ctx) => {
|
||||||
// TODO opt.accessibility
|
// TODO opt.accessibility
|
||||||
// TODO routingMode
|
// TODO routingMode
|
||||||
const travellers = ctx.profile.formatTravellers(ctx);
|
const travellers = ctx.profile.formatTravellers(ctx);
|
||||||
return {
|
const baseReq = {
|
||||||
autonomeReservierung: false,
|
autonomeReservierung: false,
|
||||||
einstiegsTypList: [
|
einstiegsTypList: [
|
||||||
'STANDARD',
|
'STANDARD',
|
||||||
],
|
],
|
||||||
|
fahrverguenstigungen: {
|
||||||
|
deutschlandTicketVorhanden: ctx.opt.deutschlandTicketDiscount,
|
||||||
|
nurDeutschlandTicketVerbindungen: ctx.opt.deutschlandTicketConnectionsOnly,
|
||||||
|
},
|
||||||
klasse: travellers.klasse,
|
klasse: travellers.klasse,
|
||||||
reisendenProfil: {
|
reisendenProfil: {
|
||||||
reisende: travellers.reisende.map(t => {
|
reisende: travellers.reisende.map(t => {
|
||||||
|
|
@ -23,6 +27,16 @@ const formatBaseJourneysReq = (ctx) => {
|
||||||
},
|
},
|
||||||
reservierungsKontingenteVorhanden: false,
|
reservierungsKontingenteVorhanden: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add business customer affiliation if BMIS number is provided
|
||||||
|
if (ctx.opt.bmisNumber) {
|
||||||
|
baseReq.firmenZugehoerigkeit = {
|
||||||
|
bmisNr: ctx.opt.bmisNumber,
|
||||||
|
identifikationsart: 'BMIS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseReq;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
|
const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
|
||||||
|
|
@ -39,6 +53,7 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
|
||||||
wunsch: {
|
wunsch: {
|
||||||
abgangsLocationId: from.lid,
|
abgangsLocationId: from.lid,
|
||||||
verkehrsmittel: filters,
|
verkehrsmittel: filters,
|
||||||
|
alternativeHalteBerechnung: true, // what is this?
|
||||||
zeitWunsch: {
|
zeitWunsch: {
|
||||||
reiseDatum: profile.formatTime(profile, when, true),
|
reiseDatum: profile.formatTime(profile, when, true),
|
||||||
zeitPunktArt: outFrwd ? 'ABFAHRT' : 'ANKUNFT',
|
zeitPunktArt: outFrwd ? 'ABFAHRT' : 'ANKUNFT',
|
||||||
|
|
@ -61,7 +76,7 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
|
||||||
return {
|
return {
|
||||||
endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint,
|
endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint,
|
||||||
body: query,
|
body: query,
|
||||||
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'),
|
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v9+json'),
|
||||||
method: 'post',
|
method: 'post',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -78,7 +93,7 @@ const formatRefreshJourneyReq = (ctx, refreshToken) => {
|
||||||
return {
|
return {
|
||||||
endpoint: opt.tickets ? profile.refreshJourneysEndpointTickets : profile.refreshJourneysEndpointPolyline,
|
endpoint: opt.tickets ? profile.refreshJourneysEndpointTickets : profile.refreshJourneysEndpointPolyline,
|
||||||
body: query,
|
body: query,
|
||||||
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'),
|
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v9+json'),
|
||||||
method: 'post',
|
method: 'post',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const profile = {
|
||||||
journeysOutFrwd: false,
|
journeysOutFrwd: false,
|
||||||
departuresGetPasslist: false,
|
departuresGetPasslist: false,
|
||||||
departuresStbFltrEquiv: true,
|
departuresStbFltrEquiv: true,
|
||||||
trip: false,
|
trip: true,
|
||||||
radar: false,
|
radar: false,
|
||||||
refreshJourney: false,
|
refreshJourney: false,
|
||||||
journeysFromTrip: false,
|
journeysFromTrip: false,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
|
||||||
let query = {
|
let query = {
|
||||||
maxUmstiege: transfers,
|
maxUmstiege: transfers,
|
||||||
minUmstiegszeit: opt.transferTime,
|
minUmstiegszeit: opt.transferTime,
|
||||||
deutschlandTicketVorhanden: false,
|
deutschlandTicketVorhanden: opt.deutschlandTicketDiscount,
|
||||||
nurDeutschlandTicketVerbindungen: false,
|
nurDeutschlandTicketVerbindungen: opt.deutschlandTicketConnectionsOnly,
|
||||||
reservierungsKontingenteVorhanden: false,
|
reservierungsKontingenteVorhanden: false,
|
||||||
schnelleVerbindungen: !opt.notOnlyFastRoutes,
|
schnelleVerbindungen: !opt.notOnlyFastRoutes,
|
||||||
sitzplatzOnly: false,
|
sitzplatzOnly: false,
|
||||||
|
|
|
||||||
2900
package-lock.json
generated
2900
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "db-vendo-client",
|
"name": "db-vendo-client",
|
||||||
"description": "Client for bahn.de public transport APIs.",
|
"description": "Client for bahn.de public transport APIs.",
|
||||||
"version": "6.8.0",
|
"version": "6.10.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const parseLine = (ctx, p) => {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: slugg(p.verkehrsmittel?.langText || p.verkehrmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName || p.lineName), // TODO terrible
|
id: slugg(p.verkehrsmittel?.langText || p.verkehrmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName || p.lineName), // TODO terrible
|
||||||
fahrtNr: String(fahrtNr),
|
fahrtNr: String(fahrtNr),
|
||||||
name: p.verkehrsmittel?.name || p.verkehrsmittel?.langText || p.verkehrmittel?.name || p.verkehrmittel?.langText || p.zugName || p.transport && p.transport.category + ' ' + p.transport.line || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext || p.lineName,
|
name: p.verkehrsmittel?.name || p.verkehrsmittel?.langText || p.verkehrmittel?.name || p.verkehrmittel?.langText || p.zugName || p.transport && p.transport.category + ' ' + (p.transport.line || p.transport.number) || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext || p.lineName,
|
||||||
public: true,
|
public: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,28 +60,28 @@ const parseRemarks = (ctx, ref) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
meldungenAsObject
|
meldungenAsObject
|
||||||
{
|
{
|
||||||
"code": "MDA-AK-MSG-1000",
|
"code": "MDA-AK-MSG-1000",
|
||||||
"nachrichtKurz": "Connection is in the past.",
|
"nachrichtKurz": "Connection is in the past.",
|
||||||
"nachrichtLang": "Selected connection is in the past.",
|
"nachrichtLang": "Selected connection is in the past.",
|
||||||
"fahrtRichtungKennzeichen": "HINFAHRT"
|
"fahrtRichtungKennzeichen": "HINFAHRT"
|
||||||
}
|
}
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": "MDA-AK-MSG-3000",
|
"code": "MDA-AK-MSG-3000",
|
||||||
"nachrichtKurz": "Booking not possible.",
|
"nachrichtKurz": "Booking not possible.",
|
||||||
"nachrichtLang": "Booking is no longer possible for the connection you selected",
|
"nachrichtLang": "Booking is no longer possible for the connection you selected",
|
||||||
"fahrtRichtungKennzeichen": "HINFAHRT"
|
"fahrtRichtungKennzeichen": "HINFAHRT"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
priorisierteMeldungen
|
priorisierteMeldungen
|
||||||
{
|
{
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"text": "ICE 597 departs differently from Mainz Hbf from Platform 1b"
|
"text": "ICE 597 departs differently from Mainz Hbf from Platform 1b"
|
||||||
}
|
}
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"prioritaet": "NIEDRIG",
|
"prioritaet": "NIEDRIG",
|
||||||
"text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations."
|
"text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations."
|
||||||
|
|
@ -90,14 +90,14 @@ const parseRemarks = (ctx, ref) => {
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"text": "The route between Mainz Hbf and Mainz Nord is currently closed. The reason is a repair on the track. At the moment, no train journeys are possible in the affected section of the route. As a result, there are now delays and partial failures. The trains terminates and starts unscheduled in Mainz Hbf. Please check your travel connections shortly before the train departs. This message will be updated as soon as we have more information."
|
"text": "The route between Mainz Hbf and Mainz Nord is currently closed. The reason is a repair on the track. At the moment, no train journeys are possible in the affected section of the route. As a result, there are now delays and partial failures. The trains terminates and starts unscheduled in Mainz Hbf. Please check your travel connections shortly before the train departs. This message will be updated as soon as we have more information."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"text": "Trip is not possible"
|
"text": "Trip is not possible"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"text": "Intervention by authorities"
|
"text": "Intervention by authorities"
|
||||||
|
|
@ -106,44 +106,44 @@ const parseRemarks = (ctx, ref) => {
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure."
|
"text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"text": "Stop cancelled",
|
"text": "Stop cancelled",
|
||||||
"type": "HALT_AUSFALL"
|
"type": "HALT_AUSFALL"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
risNotizen
|
risNotizen
|
||||||
{
|
{
|
||||||
"key": "text.realtime.connection.platform.change",
|
"key": "text.realtime.connection.platform.change",
|
||||||
"value": "ICE 597 departs differently from Mainz Hbf from Platform 1b"
|
"value": "ICE 597 departs differently from Mainz Hbf from Platform 1b"
|
||||||
}
|
}
|
||||||
{key: "FT", value: "Staff delayed due to earlier journey", routeIdxFrom: 0, routeIdxTo: 12}
|
{key: "FT", value: "Staff delayed due to earlier journey", routeIdxFrom: 0, routeIdxTo: 12}
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "text.realtime.connection.cancelled",
|
"key": "text.realtime.connection.cancelled",
|
||||||
"value": "Trip is not possible"
|
"value": "Trip is not possible"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "FT",
|
"key": "FT",
|
||||||
"value": "Intervention by authorities",
|
"value": "Intervention by authorities",
|
||||||
"routeIdxFrom": 9,
|
"routeIdxFrom": 9,
|
||||||
"routeIdxTo": 21
|
"routeIdxTo": 21
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "text.realtime.stop.cancelled",
|
"key": "text.realtime.stop.cancelled",
|
||||||
"value": "Stop cancelled"
|
"value": "Stop cancelled"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
himMeldungen
|
himMeldungen
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ueberschrift": "Construction work.",
|
"ueberschrift": "Construction work.",
|
||||||
"text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations.",
|
"text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations.",
|
||||||
|
|
@ -156,14 +156,14 @@ const parseRemarks = (ctx, ref) => {
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"modDateTime": "2024-12-06T06:24:35"
|
"modDateTime": "2024-12-06T06:24:35"
|
||||||
}
|
}
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ueberschrift": "Disruption.",
|
"ueberschrift": "Disruption.",
|
||||||
"text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure.",
|
"text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure.",
|
||||||
"prioritaet": "HOCH",
|
"prioritaet": "HOCH",
|
||||||
"modDateTime": "2024-12-05T19:01:48"
|
"modDateTime": "2024-12-05T19:01:48"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
zugattribute
|
zugattribute
|
||||||
[
|
[
|
||||||
|
|
@ -213,7 +213,7 @@ const parseCancelled = (ref) => {
|
||||||
(ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled'
|
(ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled'
|
||||||
|| r.type == 'HALT_AUSFALL'
|
|| r.type == 'HALT_AUSFALL'
|
||||||
|| r.text == 'Halt entfällt'
|
|| r.text == 'Halt entfällt'
|
||||||
|| r.text == 'Stop cancelled',
|
|| r.text?.includes('fällt aus') || r.text?.includes('cancelled'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ const parseTickets = (ctx, j) => {
|
||||||
.flatMap(p => [
|
.flatMap(p => [
|
||||||
p.einfacheFahrt?.standard?.reisePosition,
|
p.einfacheFahrt?.standard?.reisePosition,
|
||||||
p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition,
|
p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition,
|
||||||
].filter(p => p)
|
p.einfacheFahrt?.upsellAngebote?.map(a => a.upsellEntgelt?.einfacheFahrt?.reisePosition),
|
||||||
|
].flatMap(p => p)
|
||||||
|
.filter(p => p)
|
||||||
.map(p => {
|
.map(p => {
|
||||||
p.reisePosition.teilpreis = Boolean(p.teilpreisInformationen?.length);
|
p.reisePosition.teilpreis = Boolean(p.teilpreisInformationen?.length);
|
||||||
return p.reisePosition;
|
return p.reisePosition;
|
||||||
|
|
@ -44,7 +46,7 @@ const parseTickets = (ctx, j) => {
|
||||||
amount: Math.round(s.preis?.betrag * 100),
|
amount: Math.round(s.preis?.betrag * 100),
|
||||||
currency: s.preis?.waehrung,
|
currency: s.preis?.waehrung,
|
||||||
},
|
},
|
||||||
firstClass: s.klasse == 'KLASSE_1' || s.premium || Boolean(s.nutzungsInformationen?.find(i => i.klasse == 'KLASSE_1')),
|
firstClass: s.klasse == 'KLASSE_1' || Boolean(s.nutzungsInformationen?.find(i => i.klasse == 'KLASSE_1')),
|
||||||
partialFare: s.teilpreis,
|
partialFare: s.teilpreis,
|
||||||
};
|
};
|
||||||
if (s.teilpreis) {
|
if (s.teilpreis) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const parseTrip = (ctx, t, id) => { // t = raw trip
|
||||||
trip.id = trip.tripId || id;
|
trip.id = trip.tripId || id;
|
||||||
delete trip.tripId;
|
delete trip.tripId;
|
||||||
delete trip.reachable;
|
delete trip.reachable;
|
||||||
trip.cancelled = Boolean(profile.parseCancelled(t));
|
trip.cancelled = Boolean(profile.parseCancelled(t) || trip.stopovers?.length && trip.stopovers?.every(s => s.cancelled));
|
||||||
|
|
||||||
// TODO opt.scheduledDays
|
// TODO opt.scheduledDays
|
||||||
return trip;
|
return trip;
|
||||||
|
|
|
||||||
33
readme.md
33
readme.md
|
|
@ -23,25 +23,25 @@ What doesn't work:
|
||||||
|
|
||||||
Depending on the configured profile, db-vendo-client will use multiple different DB APIs that offer varying functionality, so choose wisely:
|
Depending on the configured profile, db-vendo-client will use multiple different DB APIs that offer varying functionality, so choose wisely:
|
||||||
|
|
||||||
| Profile | `db` | `dbnav` | `dbweb` | `dbbahnhof` | `dbris` |
|
| Profile | `db` | `dbnav` | `dbweb` | `dbris` |
|
||||||
| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
|
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||||
| no API key required | ✅ | ✅ | ✅ | ✅ | ❌ |
|
| no API key required | ✅ | ✅ | ✅ | ❌ |
|
||||||
| all above endpoints supported | ✅ | ✅ | except `stop()` | only boards | only boards |
|
| all above endpoints supported | ✅ | ✅ | except `stop()` | only boards |
|
||||||
| duration for boards | up to 12h | always 1h | always 1h | up to 6h, only from current time | up to 12h |
|
| duration for boards | always 1h | always 1h | always 1h | up to 12h |
|
||||||
| remarks | not for boards | for boards only most important remarks | all remarks on boards and journeys | most remarks | all remarks |
|
| remarks | for boards only most important remarks | for boards only most important remarks | all remarks on boards and journeys | all remarks |
|
||||||
| cancelled trips | contained with cancelled flag in journeys, not contained in boards | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag |
|
| cancelled trips | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag |
|
||||||
| tickets | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | ❌ | ❌ |
|
| tickets | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | ❌ |
|
||||||
| polylines | only for `refreshJourney()` (mutually exclusive with tickets) and for `trip()` (only for HAFAS trip ids) | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | ❌ | ❌ |
|
| polylines | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | ❌ |
|
||||||
| trip ids used | HAFAS trip ids for journeys, RIS trip ids for boards (static on train splits?) | HAFAS trip ids | HAFAS trip ids | RIS trip ids | RIS trip ids |
|
| trip ids used | HAFAS trip ids | HAFAS trip ids | HAFAS trip ids | RIS trip ids |
|
||||||
| line.id/fahrtNr used | actual fahrtNr | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | unreliable/route id | unreliable/route id | ✅ |
|
| line.id/fahrtNr used | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | unreliable/route id | ✅ |
|
||||||
| adminCode/operator | ✅ | only for journeys | only operator | only adminCode | ✅ |
|
| adminCode/operator | only for journeys | only for journeys | only operator | ✅ |
|
||||||
| stopovers | not in boards | not in boards | ✅ | some | ✅ |
|
| stopovers | not in boards | not in boards | ✅ | ✅ |
|
||||||
| assumed backend API stability | less stable | more stable | less stable | less stable | more stable |
|
| assumed backend API stability | less stable | more stable | less stable | more stable |
|
||||||
| quotas | 60 requests per minute for journeys, unknown for boards (IPv4) | 60 requests per minute (IPv4) | aggressive blocking (IPv4/IPv6) | ? | depends on API key |
|
| quotas | 60 requests per minute (IPv4), current backend possibly shut off soon | 60 requests per minute (IPv4), possibly aggressive blocking | aggressive blocking (IPv4/IPv6) | depends on API key |
|
||||||
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> If you think that for your project, quotas may become an issue, [consider alternative ways to obtain the data you need.](https://github.com/derhuerst/db-rest/blob/6/docs/readme.md#why-not-to-use-this-api).
|
> If you think that for your project, quotas may become an issue, [consider alternative ways to obtain the data you need.](https://github.com/derhuerst/db-rest/blob/6/docs/readme.md#why-not-to-use-this-api), e.g. [motis-fptf-client](https://github.com/motis-project/motis-fptf-client) (a drop-in replacement for db-vendo-client/hafas-client) in conjunction with https://transitous.org (please consider the [usage policy](https://transitous.org/api/) there) or a self-hosted [MOTIS](https://github.com/motis-project/motis) instance.
|
||||||
|
|
||||||
Feel free to report anything that you stumble upon via Issues or create a PR :)
|
Feel free to report anything that you stumble upon via Issues or create a PR :)
|
||||||
|
|
||||||
|
|
@ -92,6 +92,7 @@ There are [community-maintained TypeScript typings available as `@types/hafas-cl
|
||||||
- [hafas-client](https://github.com/public-transport/hafas-client/) – including further related projects
|
- [hafas-client](https://github.com/public-transport/hafas-client/) – including further related projects
|
||||||
- [hafas-rest-api](https://github.com/public-transport/hafas-rest-api/) – expose a hafas-client or db-vendo-client instance as a REST API
|
- [hafas-rest-api](https://github.com/public-transport/hafas-rest-api/) – expose a hafas-client or db-vendo-client instance as a REST API
|
||||||
- [db-rest](https://github.com/derhuerst/db-rest/) – for the legacy DB HAFAS endpoint
|
- [db-rest](https://github.com/derhuerst/db-rest/) – for the legacy DB HAFAS endpoint
|
||||||
|
- [motis-fptf-client](https://github.com/motis-project/motis-fptf-client) – a drop-in replacement for db-vendo-client/hafas-client wrapping a [MOTIS](https://github.com/motis-project/motis) instance
|
||||||
- [`*.transport.rest`](https://transport.rest/) – Public APIs wrapping some HAFAS endpoints.
|
- [`*.transport.rest`](https://transport.rest/) – Public APIs wrapping some HAFAS endpoints.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
||||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
||||||
const minute = 60 * 1000;
|
const minute = 60 * 1000;
|
||||||
|
|
||||||
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
const T_MOCK = 1764831628 * 1000; // Thu Dec 04 2025 07:00:28 GMT+0000
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|
@ -127,7 +127,7 @@ if (!process.env.VCR_OFF) {
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('refreshJourney – valid tickets', async (t) => {
|
tap.test('refreshJourney – valid tickets', async (t) => {
|
||||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
const T_MOCK = 1758279600 * 1000;
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
||||||
|
|
@ -261,7 +261,7 @@ if (!process.env.VCR_MODE) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tap.test('refreshJourney', async (t) => {
|
tap.test('refreshJourney', async (t) => {
|
||||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
const T_MOCK = 1763542800 * 1000;
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
const validate = createValidate({...cfg, when});
|
const validate = createValidate({...cfg, when});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import isRoughlyEqual from 'is-roughly-equal';
|
||||||
|
|
||||||
import {createWhen} from './lib/util.js';
|
import {createWhen} from './lib/util.js';
|
||||||
import {createClient} from '../../index.js';
|
import {createClient} from '../../index.js';
|
||||||
import {profile as dbProfile} from '../../p/dbregioguide/index.js';
|
import {profile as dbProfile} from '../../p/dbbahnhof/index.js';
|
||||||
import {
|
import {
|
||||||
createValidateStation,
|
createValidateStation,
|
||||||
createValidateTrip,
|
createValidateTrip,
|
||||||
|
|
@ -23,7 +23,7 @@ import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
||||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
||||||
const minute = 60 * 1000;
|
const minute = 60 * 1000;
|
||||||
|
|
||||||
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
const T_MOCK = 1764831628 * 1000; // Thu Dec 04 2025 07:00:28 GMT+0000
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|
@ -100,7 +100,7 @@ const berlinSüdkreuz = '8011113';
|
||||||
const kölnHbf = '8000207';
|
const kölnHbf = '8000207';
|
||||||
|
|
||||||
|
|
||||||
tap.test('departures at Berlin Schwedter Str.', async (t) => {
|
tap.skip('departures at Berlin Schwedter Str.', async (t) => {
|
||||||
const res = await client.departures(blnSchwedterStr, {
|
const res = await client.departures(blnSchwedterStr, {
|
||||||
duration: 5, when,
|
duration: 5, when,
|
||||||
});
|
});
|
||||||
|
|
@ -114,7 +114,7 @@ tap.test('departures at Berlin Schwedter Str.', async (t) => {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('departures with station object', async (t) => {
|
tap.skip('departures with station object', async (t) => {
|
||||||
const res = await client.departures({
|
const res = await client.departures({
|
||||||
type: 'station',
|
type: 'station',
|
||||||
id: jungfernheide,
|
id: jungfernheide,
|
||||||
|
|
@ -130,7 +130,7 @@ tap.test('departures with station object', async (t) => {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
|
tap.skip('arrivals at Berlin Schwedter Str.', async (t) => {
|
||||||
const res = await client.arrivals(blnSchwedterStr, {
|
const res = await client.arrivals(blnSchwedterStr, {
|
||||||
duration: 5, when,
|
duration: 5, when,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
||||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
||||||
const minute = 60 * 1000;
|
const minute = 60 * 1000;
|
||||||
|
|
||||||
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
const T_MOCK = 1764831628 * 1000; // Thu Dec 04 2025 07:00:28 GMT+0000
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|
@ -126,7 +126,7 @@ tap.test('journeys – Berlin Schwedter Str. to München Hbf', async (t) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('refreshJourney – valid tickets', async (t) => {
|
tap.test('refreshJourney – valid tickets', async (t) => {
|
||||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
const T_MOCK = 1758279600 * 1000;
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
||||||
|
|
@ -261,7 +261,7 @@ if (!process.env.VCR_OFF) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tap.test('refreshJourney', async (t) => {
|
tap.test('refreshJourney', async (t) => {
|
||||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
const T_MOCK = 1763542800 * 1000;
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
const validate = createValidate({...cfg, when});
|
const validate = createValidate({...cfg, when});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,499 +0,0 @@
|
||||||
import tap from 'tap';
|
|
||||||
import isRoughlyEqual from 'is-roughly-equal';
|
|
||||||
|
|
||||||
import {createWhen} from './lib/util.js';
|
|
||||||
import {createClient} from '../../index.js';
|
|
||||||
import {profile as dbProfile} from '../../p/dbregioguide/index.js';
|
|
||||||
import {
|
|
||||||
createValidateStation,
|
|
||||||
createValidateTrip,
|
|
||||||
} from './lib/validators.js';
|
|
||||||
import {createValidateFptfWith as createValidate} from './lib/validate-fptf-with.js';
|
|
||||||
import {testJourneysStationToStation} from './lib/journeys-station-to-station.js';
|
|
||||||
import {testJourneysStationToAddress} from './lib/journeys-station-to-address.js';
|
|
||||||
import {testJourneysStationToPoi} from './lib/journeys-station-to-poi.js';
|
|
||||||
import {testEarlierLaterJourneys} from './lib/earlier-later-journeys.js';
|
|
||||||
import {testLegCycleAlternatives} from './lib/leg-cycle-alternatives.js';
|
|
||||||
import {testRefreshJourney} from './lib/refresh-journey.js';
|
|
||||||
import {journeysFailsWithNoProduct} from './lib/journeys-fails-with-no-product.js';
|
|
||||||
import {testDepartures} from './lib/departures.js';
|
|
||||||
import {testArrivals} from './lib/arrivals.js';
|
|
||||||
import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
|
||||||
|
|
||||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
|
||||||
const minute = 60 * 1000;
|
|
||||||
|
|
||||||
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
|
||||||
|
|
||||||
const cfg = {
|
|
||||||
when,
|
|
||||||
stationCoordsOptional: true, // TODO
|
|
||||||
products: dbProfile.products,
|
|
||||||
minLatitude: 46.673100,
|
|
||||||
maxLatitude: 55.030671,
|
|
||||||
minLongitude: 6.896517,
|
|
||||||
maxLongitude: 16.180237,
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = createValidate(cfg);
|
|
||||||
|
|
||||||
const assertValidPrice = (t, p) => {
|
|
||||||
t.ok(p);
|
|
||||||
if (p.amount !== null) {
|
|
||||||
t.equal(typeof p.amount, 'number');
|
|
||||||
t.ok(p.amount > 0);
|
|
||||||
}
|
|
||||||
if (p.hint !== null) {
|
|
||||||
t.equal(typeof p.hint, 'string');
|
|
||||||
t.ok(p.hint);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const assertValidTickets = (test, tickets) => {
|
|
||||||
test.ok(Array.isArray(tickets));
|
|
||||||
for (let fare of tickets) {
|
|
||||||
test.equal(typeof fare.name, 'string', 'Mandatory field "name" is missing or not a string');
|
|
||||||
test.ok(fare.name);
|
|
||||||
|
|
||||||
test.ok(isObj(fare.priceObj), 'Mandatory field "priceObj" is missing or not an object');
|
|
||||||
test.equal(typeof fare.priceObj.amount, 'number', 'Mandatory field "amount" in "priceObj" is missing or not a number');
|
|
||||||
test.ok(fare.priceObj.amount > 0);
|
|
||||||
if ('currency' in fare.priceObj) {
|
|
||||||
test.equal(typeof fare.priceObj.currency, 'string');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check optional fields
|
|
||||||
if ('addData' in fare) {
|
|
||||||
test.equal(typeof fare.addData, 'string');
|
|
||||||
}
|
|
||||||
if ('addDataTicketInfo' in fare) {
|
|
||||||
test.equal(typeof fare.addDataTicketInfo, 'string');
|
|
||||||
}
|
|
||||||
if ('addDataTicketDetails' in fare) {
|
|
||||||
test.equal(typeof fare.addDataTicketDetails, 'string');
|
|
||||||
}
|
|
||||||
if ('addDataTravelInfo' in fare) {
|
|
||||||
test.equal(typeof fare.addDataTravelInfo, 'string');
|
|
||||||
}
|
|
||||||
if ('addDataTravelDetails' in fare) {
|
|
||||||
test.equal(typeof fare.firstClass, 'boolean');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const client = createClient(dbProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
|
||||||
|
|
||||||
const berlinHbf = '8011160';
|
|
||||||
const münchenHbf = '8000261';
|
|
||||||
const jungfernheide = '8011167';
|
|
||||||
const blnSchwedterStr = '732652';
|
|
||||||
const westhafen = '8089116';
|
|
||||||
const wedding = '8089131';
|
|
||||||
const württembergallee = '731084';
|
|
||||||
const regensburgHbf = '8000309';
|
|
||||||
const blnOstbahnhof = '8010255';
|
|
||||||
const blnTiergarten = '8089091';
|
|
||||||
const blnJannowitzbrücke = '8089019';
|
|
||||||
const potsdamHbf = '8012666';
|
|
||||||
const berlinSüdkreuz = '8011113';
|
|
||||||
const kölnHbf = '8000207';
|
|
||||||
|
|
||||||
/*
|
|
||||||
tap.test('journeys – Berlin Schwedter Str. to München Hbf', async (t) => {
|
|
||||||
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
|
|
||||||
results: 4,
|
|
||||||
departure: when,
|
|
||||||
stopovers: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testJourneysStationToStation({
|
|
||||||
test: t,
|
|
||||||
res,
|
|
||||||
validate,
|
|
||||||
fromId: blnSchwedterStr,
|
|
||||||
toId: münchenHbf,
|
|
||||||
});
|
|
||||||
// todo: find a journey where there pricing info is always available
|
|
||||||
for (let journey of res.journeys) {
|
|
||||||
if (journey.price) {
|
|
||||||
assertValidPrice(t, journey.price);
|
|
||||||
}
|
|
||||||
if (journey.tickets) {
|
|
||||||
assertValidTickets(t, journey.tickets);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('refreshJourney – valid tickets', async (t) => {
|
|
||||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
|
||||||
|
|
||||||
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
|
||||||
results: 4,
|
|
||||||
departure: when,
|
|
||||||
stopovers: true,
|
|
||||||
});
|
|
||||||
const refreshWithoutTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
|
||||||
tickets: false,
|
|
||||||
});
|
|
||||||
const refreshWithTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
|
||||||
tickets: true,
|
|
||||||
});
|
|
||||||
for (let res of [refreshWithoutTicketsRes, refreshWithTicketsRes]) {
|
|
||||||
if (res.journey.tickets !== undefined) {
|
|
||||||
assertValidTickets(t, res.journey.tickets);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo: journeys, only one product
|
|
||||||
|
|
||||||
tap.test('journeys – fails with no product', async (t) => {
|
|
||||||
await journeysFailsWithNoProduct({
|
|
||||||
test: t,
|
|
||||||
fetchJourneys: client.journeys,
|
|
||||||
fromId: blnSchwedterStr,
|
|
||||||
toId: münchenHbf,
|
|
||||||
when,
|
|
||||||
products: dbProfile.products,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
|
|
||||||
const torfstr = {
|
|
||||||
type: 'location',
|
|
||||||
address: 'Torfstraße 17',
|
|
||||||
latitude: 52.5416823,
|
|
||||||
longitude: 13.3491223,
|
|
||||||
};
|
|
||||||
const res = await client.journeys(blnSchwedterStr, torfstr, {
|
|
||||||
results: 3,
|
|
||||||
departure: when,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testJourneysStationToAddress({
|
|
||||||
test: t,
|
|
||||||
res,
|
|
||||||
validate,
|
|
||||||
fromId: blnSchwedterStr,
|
|
||||||
to: torfstr,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
|
|
||||||
const atze = {
|
|
||||||
type: 'location',
|
|
||||||
id: '991598902',
|
|
||||||
poi: true,
|
|
||||||
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
|
|
||||||
latitude: 52.542417,
|
|
||||||
longitude: 13.350437,
|
|
||||||
};
|
|
||||||
const res = await client.journeys(blnSchwedterStr, atze, {
|
|
||||||
results: 3,
|
|
||||||
departure: when,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testJourneysStationToPoi({
|
|
||||||
test: t,
|
|
||||||
res,
|
|
||||||
validate,
|
|
||||||
fromId: blnSchwedterStr,
|
|
||||||
to: atze,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('journeys: via works – with detour', async (t) => {
|
|
||||||
// Going from Westhafen to Wedding via Württembergallee without detour
|
|
||||||
// is currently impossible. We check if the routing engine computes a detour.
|
|
||||||
const res = await client.journeys(westhafen, wedding, {
|
|
||||||
via: württembergallee,
|
|
||||||
results: 1,
|
|
||||||
departure: when,
|
|
||||||
stopovers: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testJourneysWithDetour({
|
|
||||||
test: t,
|
|
||||||
res,
|
|
||||||
validate,
|
|
||||||
detourIds: [württembergallee],
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
|
|
||||||
// todo: without detour
|
|
||||||
|
|
||||||
|
|
||||||
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
|
|
||||||
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
|
|
||||||
await testEarlierLaterJourneys({
|
|
||||||
test: t,
|
|
||||||
fetchJourneys: client.journeys,
|
|
||||||
validate,
|
|
||||||
fromId: jungfernheide,
|
|
||||||
toId: münchenHbf,
|
|
||||||
when,
|
|
||||||
});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!process.env.VCR_MODE) {
|
|
||||||
tap.test('journeys – leg cycle & alternatives', async (t) => {
|
|
||||||
await testLegCycleAlternatives({
|
|
||||||
test: t,
|
|
||||||
fetchJourneys: client.journeys,
|
|
||||||
fromId: blnTiergarten,
|
|
||||||
toId: blnJannowitzbrücke,
|
|
||||||
when,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tap.test('refreshJourney', async (t) => {
|
|
||||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
|
||||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
|
||||||
const validate = createValidate({...cfg, when});
|
|
||||||
|
|
||||||
await testRefreshJourney({
|
|
||||||
test: t,
|
|
||||||
fetchJourneys: client.journeys,
|
|
||||||
refreshJourney: client.refreshJourney,
|
|
||||||
validate,
|
|
||||||
fromId: jungfernheide,
|
|
||||||
toId: münchenHbf,
|
|
||||||
when,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.skip('journeysFromTrip – U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {
|
|
||||||
const blnMehringdamm = '730939';
|
|
||||||
const blnStadtmitte = '732541';
|
|
||||||
const blnNaturkundemuseum = '732539';
|
|
||||||
const blnSpittelmarkt = '732543';
|
|
||||||
|
|
||||||
const isU6Leg = leg => leg.line && leg.line.name
|
|
||||||
&& leg.line.name.toUpperCase()
|
|
||||||
.replace(/\s+/g, '') === 'U6';
|
|
||||||
|
|
||||||
const sameStopOrStation = (stopA) => (stopB) => {
|
|
||||||
if (stopA.id && stopB.id && stopA.id === stopB.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const statA = stopA.stat && stopA.stat.id || NaN;
|
|
||||||
const statB = stopB.stat && stopB.stat.id || NaN;
|
|
||||||
return statA === statB || stopA.id === statB || stopB.id === statA;
|
|
||||||
};
|
|
||||||
const departureOf = st => Number(new Date(st.departure || st.scheduledDeparture));
|
|
||||||
const arrivalOf = st => Number(new Date(st.arrival || st.scheduledArrival));
|
|
||||||
|
|
||||||
// `journeysFromTrip` only supports queries *right now*, so we can't use `when` as in all
|
|
||||||
// other tests. To make the test less brittle, we pick a connection that is served all night. 🙄
|
|
||||||
const when = new Date();
|
|
||||||
const validate = createValidate({...cfg, when});
|
|
||||||
|
|
||||||
const findTripBetween = async (stopAId, stopBId, products = {}) => {
|
|
||||||
const {journeys} = await client.journeys(stopAId, stopBId, {
|
|
||||||
departure: new Date(when - 10 * minute),
|
|
||||||
transfers: 0, products,
|
|
||||||
results: 8, stopovers: false, remarks: false,
|
|
||||||
});
|
|
||||||
for (const j of journeys) {
|
|
||||||
const l = j.legs.find(isU6Leg);
|
|
||||||
if (!l) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const t = await client.trip(l.tripId, {
|
|
||||||
stopovers: true, remarks: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pastStopovers = t.stopovers
|
|
||||||
.filter(st => departureOf(st) < Date.now()); // todo: <= ?
|
|
||||||
const hasStoppedAtA = pastStopovers
|
|
||||||
.find(sameStopOrStation({id: stopAId}));
|
|
||||||
const willStopAtB = t.stopovers
|
|
||||||
.filter(st => arrivalOf(st) > Date.now()) // todo: >= ?
|
|
||||||
.find(sameStopOrStation({id: stopBId}));
|
|
||||||
|
|
||||||
if (hasStoppedAtA && willStopAtB) {
|
|
||||||
const prevStopover = maxBy(pastStopovers, departureOf);
|
|
||||||
return {trip: t, prevStopover};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {trip: null, prevStopover: null};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find a vehicle from U Mehringdamm to U Stadtmitte (to the north) that is currently
|
|
||||||
// between these two stations.
|
|
||||||
const {trip, prevStopover} = await findTripBetween(blnMehringdamm, blnStadtmitte, {
|
|
||||||
regionalExpress: false, regional: false, suburban: false,
|
|
||||||
});
|
|
||||||
t.ok(trip, 'precondition failed: trip not found');
|
|
||||||
t.ok(prevStopover, 'precondition failed: previous stopover missing');
|
|
||||||
|
|
||||||
// todo: "Error: Suche aus dem Zug: Vor Abfahrt des Zuges"
|
|
||||||
const newJourneys = await client.journeysFromTrip(trip.id, prevStopover, blnSpittelmarkt, {
|
|
||||||
results: 3, stopovers: true, remarks: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate with fake prices.
|
|
||||||
const withFakePrice = (j) => {
|
|
||||||
const clone = Object.assign({}, j);
|
|
||||||
clone.price = {amount: 123, currency: 'EUR'};
|
|
||||||
return clone;
|
|
||||||
};
|
|
||||||
// todo: there is no such validator!
|
|
||||||
validate(t, newJourneys.map(withFakePrice), 'journeysFromTrip', 'newJourneys');
|
|
||||||
|
|
||||||
for (let i = 0; i < newJourneys.length; i++) {
|
|
||||||
const j = newJourneys[i];
|
|
||||||
const n = `newJourneys[${i}]`;
|
|
||||||
|
|
||||||
const legOnTrip = j.legs.find(l => l.tripId === trip.id);
|
|
||||||
t.ok(legOnTrip, n + ': leg with trip ID not found');
|
|
||||||
t.equal(last(legOnTrip.stopovers).stop.id, blnStadtmitte);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('trip details', async (t) => {
|
|
||||||
const res = await client.journeys(berlinHbf, münchenHbf, {
|
|
||||||
results: 1, departure: when,
|
|
||||||
});
|
|
||||||
|
|
||||||
const p = res.journeys[0].legs.find(l => !l.walking);
|
|
||||||
t.ok(p.tripId, 'precondition failed');
|
|
||||||
t.ok(p.line.name, 'precondition failed');
|
|
||||||
|
|
||||||
const tripRes = await client.trip(p.tripId, {when});
|
|
||||||
|
|
||||||
const validate = createValidate(cfg, {
|
|
||||||
trip: (cfg) => {
|
|
||||||
const validateTrip = createValidateTrip(cfg);
|
|
||||||
const validateTripWithFakeDirection = (val, trip, name) => {
|
|
||||||
validateTrip(val, {
|
|
||||||
...trip,
|
|
||||||
direction: trip.direction || 'foo', // todo, see #49
|
|
||||||
}, name);
|
|
||||||
};
|
|
||||||
return validateTripWithFakeDirection;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
validate(t, tripRes, 'tripResult', 'tripRes');
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
tap.test('departures at Berlin Schwedter Str.', async (t) => {
|
|
||||||
const res = await client.departures(blnSchwedterStr, {
|
|
||||||
duration: 5, when,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testDepartures({
|
|
||||||
test: t,
|
|
||||||
res,
|
|
||||||
validate,
|
|
||||||
id: blnSchwedterStr,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('departures with station object', async (t) => {
|
|
||||||
const res = await client.departures({
|
|
||||||
type: 'station',
|
|
||||||
id: jungfernheide,
|
|
||||||
name: 'Berlin Jungfernheide',
|
|
||||||
location: {
|
|
||||||
type: 'location',
|
|
||||||
latitude: 1.23,
|
|
||||||
longitude: 2.34,
|
|
||||||
},
|
|
||||||
}, {when});
|
|
||||||
|
|
||||||
validate(t, res, 'departuresResponse', 'res');
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
|
|
||||||
const res = await client.arrivals(blnSchwedterStr, {
|
|
||||||
duration: 5, when,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testArrivals({
|
|
||||||
test: t,
|
|
||||||
res,
|
|
||||||
validate,
|
|
||||||
id: blnSchwedterStr,
|
|
||||||
});
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
tap.test('nearby Berlin Jungfernheide', async (t) => {
|
|
||||||
const nearby = await client.nearby({
|
|
||||||
type: 'location',
|
|
||||||
latitude: 52.530273,
|
|
||||||
longitude: 13.299433,
|
|
||||||
}, {
|
|
||||||
results: 2, distance: 400,
|
|
||||||
});
|
|
||||||
|
|
||||||
validate(t, nearby, 'locations', 'nearby');
|
|
||||||
|
|
||||||
t.equal(nearby.length, 2);
|
|
||||||
|
|
||||||
const s0 = nearby[0];
|
|
||||||
t.equal(s0.id, jungfernheide);
|
|
||||||
t.equal(s0.name, 'Berlin Jungfernheide');
|
|
||||||
t.ok(isRoughlyEqual(0.0005, s0.location.latitude, 52.530408));
|
|
||||||
t.ok(isRoughlyEqual(0.0005, s0.location.longitude, 13.299424));
|
|
||||||
t.ok(s0.distance >= 0);
|
|
||||||
t.ok(s0.distance <= 100);
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('locations named Jungfernheide', async (t) => {
|
|
||||||
const locations = await client.locations('Jungfernheide', {
|
|
||||||
results: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
validate(t, locations, 'locations', 'locations');
|
|
||||||
t.ok(locations.length <= 10);
|
|
||||||
t.ok(locations.some((l) => {
|
|
||||||
return l.station && l.station.id === jungfernheide || l.id === jungfernheide;
|
|
||||||
}), 'Jungfernheide not found');
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('stop', async (t) => {
|
|
||||||
const s = await client.stop(regensburgHbf);
|
|
||||||
|
|
||||||
validate(t, s, ['stop', 'station'], 'stop');
|
|
||||||
t.equal(s.id, regensburgHbf);
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('line with additionalName', async (t) => {
|
|
||||||
const {departures} = await client.departures(potsdamHbf, {
|
|
||||||
when,
|
|
||||||
duration: 12 * 60, // 12 minutes
|
|
||||||
products: {bus: false, suburban: false, tram: false},
|
|
||||||
});
|
|
||||||
t.ok(departures.some(d => d.line && d.line.additionalName));
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -3,7 +3,7 @@ const simplify = j => j.legs.map(l => {
|
||||||
origin: l.origin,
|
origin: l.origin,
|
||||||
destination: l.destination,
|
destination: l.destination,
|
||||||
departure: l.plannedDeparture || l.departure,
|
departure: l.plannedDeparture || l.departure,
|
||||||
arrival: l.plannedArrival || l.arrival,
|
// arrival: l.plannedArrival || l.arrival, // sometimes differs on older journeys
|
||||||
line: l.line,
|
line: l.line,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ tap.test('db trip(): dynamic request formatting', (t) => {
|
||||||
|
|
||||||
const reqDbNav = profile.formatTripReq(ctx, tripIdHafas);
|
const reqDbNav = profile.formatTripReq(ctx, tripIdHafas);
|
||||||
delete reqDbNav.headers['X-Correlation-ID'];
|
delete reqDbNav.headers['X-Correlation-ID'];
|
||||||
const reqDbRegioGuide = profile.formatTripReq(ctx, tripIdRis);
|
// const reqDbRegioGuide = profile.formatTripReq(ctx, tripIdRis);
|
||||||
|
|
||||||
t.same(reqDbNav, reqDbNavExpected);
|
t.same(reqDbNav, reqDbNavExpected);
|
||||||
t.same(reqDbRegioGuide, reqDbRegioGuideExpected);
|
// t.same(reqDbRegioGuide, reqDbRegioGuideExpected);
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ const berlinWienQuery0 = Object.freeze(
|
||||||
einstiegsTypList: [
|
einstiegsTypList: [
|
||||||
'STANDARD',
|
'STANDARD',
|
||||||
],
|
],
|
||||||
|
fahrverguenstigungen: {
|
||||||
|
deutschlandTicketVorhanden: undefined,
|
||||||
|
nurDeutschlandTicketVerbindungen: undefined,
|
||||||
|
},
|
||||||
klasse: 'KLASSE_2',
|
klasse: 'KLASSE_2',
|
||||||
reiseHin: {
|
reiseHin: {
|
||||||
wunsch: {
|
wunsch: {
|
||||||
|
|
@ -48,6 +52,7 @@ const berlinWienQuery0 = Object.freeze(
|
||||||
verkehrsmittel: [
|
verkehrsmittel: [
|
||||||
'ALL',
|
'ALL',
|
||||||
],
|
],
|
||||||
|
alternativeHalteBerechnung: true,
|
||||||
zeitWunsch: {
|
zeitWunsch: {
|
||||||
reiseDatum: '2024-12-07T23:50:12+01:00',
|
reiseDatum: '2024-12-07T23:50:12+01:00',
|
||||||
zeitPunktArt: 'ABFAHRT',
|
zeitPunktArt: 'ABFAHRT',
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,8 @@ const berlinWienQuery0 = Object.freeze(
|
||||||
sitzplatzOnly: false,
|
sitzplatzOnly: false,
|
||||||
bikeCarriage: false,
|
bikeCarriage: false,
|
||||||
reservierungsKontingenteVorhanden: false,
|
reservierungsKontingenteVorhanden: false,
|
||||||
nurDeutschlandTicketVerbindungen: false,
|
nurDeutschlandTicketVerbindungen: undefined,
|
||||||
deutschlandTicketVorhanden: false,
|
deutschlandTicketVorhanden: undefined,
|
||||||
maxUmstiege: null,
|
maxUmstiege: null,
|
||||||
zwischenhalte: null,
|
zwischenhalte: null,
|
||||||
minUmstiegszeit: 0,
|
minUmstiegszeit: 0,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue