mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-05-05 22:19:59 +03:00
Compare commits
1158 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b59d7b3084 | ||
|
db4c03054a | ||
|
eac21d188b | ||
|
ad09f8b1be | ||
|
c4d0a55d41 | ||
|
29aab87cdf | ||
|
883eb8c8de | ||
|
b20cf1060a | ||
|
b887c674d4 | ||
|
b3e0e764e2 | ||
|
2ea47f7792 | ||
|
6c2081c14e | ||
|
f741a13670 | ||
|
bcaad526c7 | ||
|
162b946bac | ||
|
14b80dbf33 | ||
|
1927f98906 | ||
|
0ef3935a35 | ||
|
b04a671b50 | ||
|
9975a6c9ac | ||
|
960371e2ec | ||
|
88acdd1620 | ||
|
25cbb288ca | ||
|
a6e84be2df | ||
|
de63bf0a37 | ||
|
040a8f44e4 | ||
|
6b67a77823 | ||
|
debb45a929 | ||
|
53b385a865 | ||
|
185870db3d | ||
|
16829f839c | ||
|
9fe4972d2b | ||
|
1aeb246622 | ||
|
6d1d0c626f | ||
|
afa99b0742 | ||
|
229dbac93e | ||
|
7a1e513fa2 | ||
|
f1302b0a7b | ||
|
177a3cab3f | ||
|
71d1a4f1a9 | ||
|
6ff406ea79 | ||
|
2a23e1ad9b | ||
|
9314e59053 | ||
|
69c098744a | ||
|
c671e995cb | ||
|
1e7977a8bb | ||
|
ff559c83dd | ||
|
76d6121f88 | ||
|
206e709e6a | ||
|
7d10f409ef | ||
|
179ada6f08 | ||
|
4c8c503e48 | ||
|
3c7227635a | ||
|
22c839847f | ||
|
1b0858a253 | ||
|
a59a3d78dc | ||
|
911a6d371e | ||
|
2b55f7148f | ||
|
715541f060 | ||
|
70f4cdb2b0 | ||
|
bb692f4bc9 | ||
|
977da80885 | ||
|
01b95e74f4 | ||
|
911ac17510 | ||
|
32792507ba | ||
|
60656b0119 | ||
|
232893f2dc | ||
|
41feb41b5a | ||
|
63bc542b1c | ||
|
a624e62172 | ||
|
942972d5f0 | ||
|
8d07b24604 | ||
|
126077582b | ||
|
615c36650e | ||
|
bc676fd0b6 | ||
|
98670d5e08 | ||
|
0e68a375e1 | ||
|
632a29d2aa | ||
|
8026689ee8 | ||
|
d992961421 | ||
|
31df18f4be | ||
|
94e130f0d2 | ||
|
9e69594a36 | ||
|
db12ea036d | ||
|
de78b40da4 | ||
|
b431fe65b7 | ||
|
661024cedb | ||
|
db168b7a35 | ||
|
d2a39b33c7 | ||
|
87a705e966 | ||
|
3d998de41c | ||
|
6538f814aa | ||
|
debc1ee150 | ||
|
ad6c356552 | ||
|
771ab128b3 | ||
|
9f5e1fa6bd | ||
|
ec723b3414 | ||
|
bc56d41fbe | ||
|
d24a341def | ||
|
59bd56e824 | ||
|
73b4d40e02 | ||
|
e14a909ebf | ||
|
2a49495959 | ||
|
318644958a | ||
|
4a932f0329 | ||
|
80195404bb | ||
|
073d33e12f | ||
|
5c5c1acd44 | ||
|
e05b31e19c | ||
|
0e2d0e32ab | ||
|
e4a99d4be3 | ||
|
ed8683e8c2 | ||
|
df81b5600d | ||
|
0aedad5192 | ||
|
43ba0fe0be | ||
|
73d9c88ffb | ||
|
6e0f3d66b9 | ||
|
e87acc3e24 | ||
|
175b166864 | ||
|
30d84352ae | ||
|
5bb4e66c9a | ||
|
e18ac3f8d3 | ||
|
760a1bdb54 | ||
|
f379fba930 | ||
|
c6bb1b468a | ||
|
491348bd3b | ||
|
c663a35711 | ||
|
f8a79834b3 | ||
|
65aae69481 | ||
|
a2de274a90 | ||
|
f5008fc747 | ||
|
0e328aa681 | ||
|
80e633dcb7 | ||
|
2f45f66793 | ||
|
2e094c2b78 | ||
|
e9211e8105 | ||
|
31c4f17b10 | ||
|
08db80f165 | ||
|
e411a4b60e | ||
|
b45449a20e | ||
|
1d23bef765 | ||
|
52f0dd19bb | ||
|
96f97f245e | ||
|
d2b490a4ff | ||
|
329c89c4d4 | ||
|
3c8bb905f3 | ||
|
d001fcc1a4 | ||
|
8377202866 | ||
|
6da22a0a12 | ||
|
97268d7a8a | ||
|
1a0b595a71 | ||
|
6e74b9ab60 | ||
|
65096a85b6 | ||
|
7f78dc9458 | ||
|
d3bc9d351d | ||
|
191b9abb6d | ||
|
c4966aeca7 | ||
|
784d273adf | ||
|
9365c00aaf | ||
|
248adb5f72 | ||
|
fd90abdeca | ||
|
c826100bc8 | ||
|
336a9ba115 | ||
|
9eeafd0ae8 | ||
|
613609782d | ||
|
90b1140401 | ||
|
66d9fb5194 | ||
|
228c72531b | ||
|
26c56f8dc6 | ||
|
c85f083db5 | ||
|
8a17401693 | ||
|
eec06ba81a | ||
|
a157d0b15f | ||
|
5660f602a7 | ||
|
8520eb3d1c | ||
|
160039df10 | ||
|
f29ced5b2d | ||
|
b12d235bae | ||
|
5287ced44c | ||
|
9a3a1d3b0c | ||
|
244e88dec0 | ||
|
ecc8fccc54 | ||
|
02c781b180 | ||
|
449d2261bd | ||
|
0bc6ba3650 | ||
|
a8401f36e1 | ||
|
45610fc951 | ||
|
581a47510d | ||
|
19cdde0655 | ||
|
8ff945c075 | ||
|
793cc9eee5 | ||
|
5ce0129c36 | ||
|
92bbc63590 | ||
|
f9c24a4a84 | ||
|
4cb7062302 | ||
|
24ad6117b0 | ||
|
b8f0ab0fd6 | ||
|
4116b53e9b | ||
|
9a1ef7c586 | ||
|
dcc01d1413 | ||
|
1e3cbc09a1 | ||
|
f45842d7a3 | ||
|
0e023136b8 | ||
|
02dc6aef12 | ||
|
d6307aa24b | ||
|
ab3f3636ff | ||
|
14c9805ad8 | ||
|
8faf8ba507 | ||
|
9d35d83c97 | ||
|
e7602e6c84 | ||
|
5910d62535 | ||
|
0f3d6ec858 | ||
|
673eb4d6c7 | ||
|
8ba1adeb39 | ||
|
2639448911 | ||
|
c2a71b08e8 | ||
|
9f85a9af54 | ||
|
557fc66078 | ||
|
547dd4b2a9 | ||
|
fc1afe0625 | ||
|
3493ad1086 | ||
|
d8805d9ea3 | ||
|
3791ec25e2 | ||
|
8278ff9c62 | ||
|
4c8aeeb70c | ||
|
0d965c585d | ||
|
198d50e260 | ||
|
63013d8306 | ||
|
573f4ce6d7 | ||
|
d43d3bafe3 | ||
|
16671b6dc5 | ||
|
7b914ae939 | ||
|
0349ebac20 | ||
|
1e8b5982a2 | ||
|
c2a228a73a | ||
|
dd52411f5a | ||
|
c736ff6427 | ||
|
d80330ba5e | ||
|
3c17678d9d | ||
|
e46514c5f9 | ||
|
339d64e901 | ||
|
28f1316a51 | ||
|
c6085eff26 | ||
|
cef6dcaf0f | ||
|
ef9e3765ee | ||
|
b740539081 | ||
|
30cb1f3d28 | ||
|
c53316668d | ||
|
db442bb578 | ||
|
44c8e37e5c | ||
|
b1c2eb9b93 | ||
|
bb70081ceb | ||
|
2fcaa2304b | ||
|
0cc50a918a | ||
|
a6411707e1 | ||
|
a0a4064bf0 | ||
|
3cbbc3c4da | ||
|
751ae21d18 | ||
|
c4470ca962 | ||
|
1000e48dfd | ||
|
9b263bb379 | ||
|
0275b65c7a | ||
|
7765f9d7a1 | ||
|
b030eec1f5 | ||
|
40957d3515 | ||
|
f5962c4b7f | ||
|
70ae1b48bc | ||
|
7b0374695a | ||
|
673d6f8279 | ||
|
d2bc134645 | ||
|
5ecf03f349 | ||
|
df4124e31d | ||
|
5797105939 | ||
|
1f6115955d | ||
|
1ae1362916 | ||
|
a81e550f2a | ||
|
492fdeb2ef | ||
|
e0cdd55908 | ||
|
90308411fc | ||
|
d5969bc0c4 | ||
|
2baf2f6f04 | ||
|
a60083f8d1 | ||
|
b6900a3ddb | ||
|
61fc2293fb | ||
|
5ff8527b60 | ||
|
c88179777d | ||
|
f530a30fe0 | ||
|
2319d317d0 | ||
|
66d78767ff | ||
|
0f7382e3b8 | ||
|
f20146137e | ||
|
7ccffa5e51 | ||
|
4189ce437b | ||
|
829c9ca461 | ||
|
0dc230837a | ||
|
95af0a0127 | ||
|
7c68f962c1 | ||
|
0a63698100 | ||
|
9f2dcecf19 | ||
|
7929a986d5 | ||
|
2ed2f38195 | ||
|
bdf933f806 | ||
|
38cc9e9af8 | ||
|
dda36c2da0 | ||
|
3c7d8f79b2 | ||
|
08be943906 | ||
|
4aa5f1de85 | ||
|
492cb7dfbc | ||
|
27bf631ae5 | ||
|
27d4479e1a | ||
|
7e1f7ed4ee | ||
|
f8ca2d5d17 | ||
|
119d291d10 | ||
|
1236cf63a0 | ||
|
7a3fd4fca5 | ||
|
f3c2ee6f5a | ||
|
1ab526d900 | ||
|
68ecd7c5e9 | ||
|
947c5d364a | ||
|
2edcd49e99 | ||
|
cbc7f63dcc | ||
|
57084262a2 | ||
|
4b8f7c8198 | ||
|
4cd0e9d98f | ||
|
e3a022972c | ||
|
2ec079adfc | ||
|
fa9a8d9f7d | ||
|
e69d069d43 | ||
|
f6b144f012 | ||
|
e79a371dde | ||
|
674e4a5fe6 | ||
|
9c10a1765f | ||
|
2fd06941b5 | ||
|
8645661cbc | ||
|
95d2c61fbd | ||
|
f6733d937a | ||
|
ed86ad0b56 | ||
|
39950122a6 | ||
|
579fdbde61 | ||
|
ca6f75501c | ||
|
f41b8ac464 | ||
|
601164de21 | ||
|
4492b3a376 | ||
|
c270eed998 | ||
|
6941e7a4ad | ||
|
69ddf5fb8d | ||
|
a8d4d328b8 | ||
|
3453cbe159 | ||
|
97b6a76e75 | ||
|
2f5c8f4619 | ||
|
8d4f8a8357 | ||
|
1f6e6810da | ||
|
3f75e075a3 | ||
|
39d3807c82 | ||
|
a7cb71c870 | ||
|
dd5e436892 | ||
|
84c7582a33 | ||
|
5104fa0fa8 | ||
|
f0d33564f2 | ||
|
39ca7ede57 | ||
|
fd6a349b64 | ||
|
959e894d42 | ||
|
46fb44d0de | ||
|
22a7f16ec9 | ||
|
102c4bf2a5 | ||
|
0148a5bcbc | ||
|
b616330523 | ||
|
c1ee557cf8 | ||
|
6507d5a786 | ||
|
b10c1ce644 | ||
|
0a096a13d7 | ||
|
464650c366 | ||
|
ca75c4407d | ||
|
042668ff56 | ||
|
182a0797e7 | ||
|
421282608f | ||
|
0114f587b2 | ||
|
c3bdcc880f | ||
|
e871c80900 | ||
|
dd52c4ade2 | ||
|
6de2dc7bf3 | ||
|
c10f31811a | ||
|
216102763d | ||
|
62454d5d94 | ||
|
56bd16b562 | ||
|
db2cbfdc45 | ||
|
649a7ec060 | ||
|
0ae13b09af | ||
|
f20466c21c | ||
|
e293223c08 | ||
|
4a7422fe56 | ||
|
0995696c65 | ||
|
aab7babbc2 | ||
|
ce82817631 | ||
|
b717642293 | ||
|
33dab455ce | ||
|
24c2cc6ea4 | ||
|
cec4f4dcaf | ||
|
874ac69b1b | ||
|
f1a19450fe | ||
|
40ec157869 | ||
|
3412a66f6a | ||
|
48af075f6f | ||
|
4aae0cbcb9 | ||
|
83dfc4456c | ||
|
3d50a212b5 | ||
|
62843f79d4 | ||
|
96b4d55f56 | ||
|
251e7925c9 | ||
|
4557e336b2 | ||
|
16e0038fa9 | ||
|
5beec2e15c | ||
|
7cb6210847 | ||
|
99142acf8b | ||
|
e9701648ee | ||
|
44f1206daf | ||
|
3ab6d46b5a | ||
|
dbac565dbe | ||
|
d9de0e004a | ||
|
b6ad9ba0bc | ||
|
15be4a0b58 | ||
|
6f56f15284 | ||
|
9bfd4566ae | ||
|
68d8bf9fca | ||
|
ebe7c59524 | ||
|
ae66568c03 | ||
|
9f82d3305c | ||
|
c6fb966177 | ||
|
7025d3bc01 | ||
|
92f1831c72 | ||
|
78bbf9b66c | ||
|
cb8d92befb | ||
|
2afe4154f8 | ||
|
8ba60dcf92 | ||
|
d86b359cb7 | ||
|
0690724d1e | ||
|
2853fb0408 | ||
|
f47343df8f | ||
|
2ae6a9a4ea | ||
|
d69d2530ef | ||
|
7106d24a70 | ||
|
458d6a77af | ||
|
fe63644827 | ||
|
472646057f | ||
|
c260e34f20 | ||
|
0f284e09b8 | ||
|
6c4785b05b | ||
|
51af991e38 | ||
|
4ee062a19d | ||
|
3df3a7b73d | ||
|
ba27f54952 | ||
|
4efff792ef | ||
|
174ed80749 | ||
|
4950b1e4f9 | ||
|
02af67e26c | ||
|
3b142813e2 | ||
|
d017e62740 | ||
|
3407ad6b5b | ||
|
ad6cfd2288 | ||
|
86bf3b46f0 | ||
|
33ea6242bd | ||
|
ad6f76975b | ||
|
744590ca43 | ||
|
78a0f0fa54 | ||
|
54b7d2845b | ||
|
850ec9484a | ||
|
6815c9eb3c | ||
|
bbf024dd3a | ||
|
2d3c0d6a94 | ||
|
17e08acfbb | ||
|
7444e08819 | ||
|
18f8c81b59 | ||
|
1a0d97dd28 | ||
|
e6bc8c6f86 | ||
|
53e10f73b7 | ||
|
9d8260bf5f | ||
|
7685d5afeb | ||
|
40ca838258 | ||
|
731d9b8b81 | ||
|
ed48971fb1 | ||
|
3e6d6d9917 | ||
|
eed345e3fc | ||
|
99e8bb313c | ||
|
c0152d9605 | ||
|
259fcd7ad5 | ||
|
0e46be7237 | ||
|
66ff661767 | ||
|
48ef7b5a5d | ||
|
6d4f29a3f9 | ||
|
47dfddfeb4 | ||
|
0ce6a41a85 | ||
|
013ab2d3ce | ||
|
92fb29d687 | ||
|
11ca3b171a | ||
|
33d7d30acf | ||
|
8ed218f4d6 | ||
|
de86391dcd | ||
|
fddf25a429 | ||
|
4b72c61c58 | ||
|
c17bd5a541 | ||
|
68aaad1071 | ||
|
e6f25a6471 | ||
|
b2a3ce4d66 | ||
|
c9f8cc680e | ||
|
2d139c8235 | ||
|
25fb25c18d | ||
|
82de7409e8 | ||
|
4d0605737f | ||
|
a621fd6df4 | ||
|
43bd9cf65b | ||
|
9848dfa762 | ||
|
2a5a385515 | ||
|
da01544b42 | ||
|
2612494970 | ||
|
40df65f462 | ||
|
4fc4c3b873 | ||
|
097557c833 | ||
|
3f4c05d821 | ||
|
b2b1b75f04 | ||
|
e18fa7ccae | ||
|
20d00ca3f0 | ||
|
f9bfd6918a | ||
|
a4dd763a46 | ||
|
dd88cea045 | ||
|
2c04e2f728 | ||
|
71db75df93 | ||
|
51f4a66bd8 | ||
|
5c05375650 | ||
|
c8928fb28d | ||
|
6f65d82edf | ||
|
3109a92b19 | ||
|
6b275171b1 | ||
|
5fd2c27a06 | ||
|
d2314e0d40 | ||
|
fea27d3a4a | ||
|
0c3acaba46 | ||
|
51b3ca3c20 | ||
|
3461573f31 | ||
|
de896b1e96 | ||
|
fc2e214bb7 | ||
|
dce42bfa31 | ||
|
7c0be5ed74 | ||
|
a8a9303e6f | ||
|
ee94c651dd | ||
|
57fc610a5f | ||
|
3ca4a0c2b2 | ||
|
542aa8caef | ||
|
156de8e1c5 | ||
|
322004bdcd | ||
|
240df85bf6 | ||
|
0251e314cc | ||
|
9e75f42346 | ||
|
d92eb154c2 | ||
|
1abafb5bd4 | ||
|
76e310218a | ||
|
07c77f8cf9 | ||
|
d98910a651 | ||
|
8fd72ca6f5 | ||
|
b302ba75d2 | ||
|
e02a20b1de | ||
|
3c888a0ea0 | ||
|
87e5649f89 | ||
|
0287899d1d | ||
|
0699d4d22e | ||
|
9c4189a874 | ||
|
fa3146d706 | ||
|
1b0133190f | ||
|
e032ec1acd | ||
|
33d77868a4 | ||
|
ae74bb420d | ||
|
70c02199b2 | ||
|
7d3107e6a7 | ||
|
17031f3e11 | ||
|
01b3693271 | ||
|
ce76c1f983 | ||
|
78487d9163 | ||
|
916ac3067d | ||
|
0dceb414af | ||
|
2cb6a0c32b | ||
|
36a8b388f2 | ||
|
cda96b6698 | ||
|
e0b15f1e1c | ||
|
a93909046e | ||
|
0499163df3 | ||
|
86ddf2c290 | ||
|
522248b908 | ||
|
84637b2e96 | ||
|
0ea2c01abe | ||
|
3a9e548bcf | ||
|
8540f5f610 | ||
|
682f9f948d | ||
|
c2b15fab50 | ||
|
d5116c2399 | ||
|
299b5ac8ae | ||
|
1c790e1cd6 | ||
|
b61c2584b2 | ||
|
225a7c15c1 | ||
|
8c7f164fa3 | ||
|
3ea9380218 | ||
|
2a241375db | ||
|
b9d5c85a54 | ||
|
ff2b677812 | ||
|
1b03b2eb0f | ||
|
b653d4659b | ||
|
9874292a73 | ||
|
df010fc24c | ||
|
c072a70c57 | ||
|
bc30309056 | ||
|
db94a62649 | ||
|
e5abe3d98a | ||
|
fd3bc17d08 | ||
|
8cb7d807f2 | ||
|
940519b15b | ||
|
9522e9296d | ||
|
d2bca32c77 | ||
|
65c79fed5d | ||
|
738354d202 | ||
|
1c67350b48 | ||
|
542a9eea02 | ||
|
c1beb28b85 | ||
|
dfff999406 | ||
|
4837c2309e | ||
|
ea4912aae4 | ||
|
9b0e55c6ad | ||
|
db9287f7fd | ||
|
f771e9fb5d | ||
|
5622f98e62 | ||
|
cf69ff4bc8 | ||
|
8c6a8d858e | ||
|
e9699f98ba | ||
|
e049aa3d04 | ||
|
616da57550 | ||
|
51b1e68ddd | ||
|
42b2a8a7bf | ||
|
e2567efcc2 | ||
|
2f8f82f736 | ||
|
4d11f34e1d | ||
|
39a626784b | ||
|
773035c05d | ||
|
850cd9ce85 | ||
|
6d5c6081ce | ||
|
4652c1694e | ||
|
2cfee22287 | ||
|
9fc6664302 | ||
|
7b7293efea | ||
|
252ce5b515 | ||
|
fb7a5653e3 | ||
|
29d7bd4299 | ||
|
20a1592eaf | ||
|
cc74f6a85b | ||
|
5ea22f7a59 | ||
|
9a6bc2df0d | ||
|
8b2a5a82f2 | ||
|
6c5409fbce | ||
|
9318007455 | ||
|
0165320808 | ||
|
ef7bd42d15 | ||
|
99d1531dbb | ||
|
c2dc8742b6 | ||
|
2d1d482ddf | ||
|
938a6f22b9 | ||
|
2b9280e6c3 | ||
|
29a2cf36e9 | ||
|
4162328fd4 | ||
|
655c425ecf | ||
|
4ba4cefab6 | ||
|
9605ff3bf5 | ||
|
70cf3b21dc | ||
|
d724846e91 | ||
|
f02fe301c7 | ||
|
018fc84bf5 | ||
|
6af8f6d5ec | ||
|
62cc53ffcf | ||
|
758deaf2d5 | ||
|
0cc17ee780 | ||
|
3eae7ab169 | ||
|
4270125bf7 | ||
|
bff7384f06 | ||
|
9a89cd0dc8 | ||
|
0c145d352b | ||
|
9c47a3908c | ||
|
f8210c5515 | ||
|
33c1dc3c7e | ||
|
352fa2e564 | ||
|
35e44d4c92 | ||
|
b8496be1a3 | ||
|
1afe4caf41 | ||
|
a40006f5ca | ||
|
baff692b70 | ||
|
272bf64e5e | ||
|
793457a7a8 | ||
|
93814983da | ||
|
4f9c3435d6 | ||
|
16116358df | ||
|
ed0c606db1 | ||
|
00b184df36 | ||
|
46eadcfde6 | ||
|
b7a6dbc504 | ||
|
56dee669a0 | ||
|
c7a2813039 | ||
|
747f335e32 | ||
|
c883d969e2 | ||
|
9b9cca3823 | ||
|
fceaf86186 | ||
|
2fdff4df5d | ||
|
105c18b30d | ||
|
df943252b6 | ||
|
1cc453b778 | ||
|
9ce72930b1 | ||
|
43b4a6e6d9 | ||
|
2993cc0e87 | ||
|
3c7c1c3d4e | ||
|
19c3ee614c | ||
|
73ca349ee5 | ||
|
ae4e592b00 | ||
|
0bcc9016cd | ||
|
c6de12a707 | ||
|
036d0cdca8 | ||
|
2e88e964bb | ||
|
f783ef0793 | ||
|
77afb9f9a3 | ||
|
3aaa1496f5 | ||
|
f5121f1bf6 | ||
|
b57c212bb5 | ||
|
b144dd5358 | ||
|
ccfeaecef9 | ||
|
e46d6cd588 | ||
|
e5dafa0d48 | ||
|
a2b71e2ab6 | ||
|
0ce5669899 | ||
|
d0f7ca1b6c | ||
|
1e0182f8f6 | ||
|
707fd292d7 | ||
|
3eacc443e1 | ||
|
9078d2d25a | ||
|
f04b5416e4 | ||
|
6da1e80ef2 | ||
|
965e1f9986 | ||
|
875ea18b4c | ||
|
f92e9336c0 | ||
|
4f8bc17015 | ||
|
dca6c15ded | ||
|
57c71865ba | ||
|
3ab099b8e7 | ||
|
b3d75b567d | ||
|
820f2abe86 | ||
|
59de9b8862 | ||
|
d5f9675c2a | ||
|
75432fcf4c | ||
|
3e01303e43 | ||
|
26069806e0 | ||
|
831bcaf4c9 | ||
|
ed97522908 | ||
|
6aa57d4616 | ||
|
ab00a9360f | ||
|
133cee9988 | ||
|
b88090dd30 | ||
|
9503ef1cda | ||
|
5d49fd0a20 | ||
|
8bfc4aee0f | ||
|
29aea9a5ef | ||
|
16f98b943e | ||
|
3b0740d310 | ||
|
7e39a2f333 | ||
|
16461733e4 | ||
|
fbde6a171b | ||
|
748f8ce6b0 | ||
|
c04e041338 | ||
|
eab850e058 | ||
|
eb3ffba4fc | ||
|
4a79b91680 | ||
|
9c449958c4 | ||
|
fcc2a23fc1 | ||
|
8566bcc85f | ||
|
ea17443bd8 | ||
|
567cc98409 | ||
|
c1bb9a6a5c | ||
|
0e1fcb0c99 | ||
|
a972dad7b8 | ||
|
8f9b22e296 | ||
|
d7e439b948 | ||
|
61e7d14531 | ||
|
a1c40ad084 | ||
|
b2b2d11dfe | ||
|
fcc53b5d2a | ||
|
cb535cdabe | ||
|
a1ffad3071 | ||
|
3bc2eff530 | ||
|
88c78c243f | ||
|
0fa9610b30 | ||
|
0daa1c5fef | ||
|
96ff59dc43 | ||
|
2e12206c0b | ||
|
8b87868fba | ||
|
bad0af8e25 | ||
|
bbff1f4c63 | ||
|
59584a3402 | ||
|
bf3c4c58a1 | ||
|
ca1105f139 | ||
|
a9fd9ff814 | ||
|
b99ceb21fb | ||
|
1e13cf15ae | ||
|
73261e99b4 | ||
|
46e772967a | ||
|
dafc96ad11 | ||
|
1eeb0d7bd7 | ||
|
f6a7be0652 | ||
|
1e16a10f3d | ||
|
dd98c6deb4 | ||
|
d797333a70 | ||
|
1e8bdda6df | ||
|
a145feab4a | ||
|
cfda4caf26 | ||
|
9b1bbb92a7 | ||
|
e051884ccc | ||
|
b0f786c42a | ||
|
3f58d84de5 | ||
|
b84db19efb | ||
|
985eadc8d8 | ||
|
ecc26ef313 | ||
|
f0bd8ba61d | ||
|
e50c1694d8 | ||
|
508eaa629b | ||
|
1b941dea16 | ||
|
5d0096c596 | ||
|
b55c2c1579 | ||
|
b9282435a5 | ||
|
f097022b9a | ||
|
aa0f0118db | ||
|
e867dac2ab | ||
|
c70a851249 | ||
|
cb2d2981a3 | ||
|
a68614bfaf | ||
|
48424cf10f | ||
|
5beff47dea | ||
|
215fce004b | ||
|
51bd438681 | ||
|
d11b60bbd1 | ||
|
e1f1d0d258 | ||
|
ae2007c9e2 | ||
|
bcbc366497 | ||
|
b809281d0e | ||
|
e7efcb5405 | ||
|
054b3f3eec | ||
|
b208a3dda4 | ||
|
d9126d531a | ||
|
9f1a0a0f8a | ||
|
f88358e02a | ||
|
8fac5fc0e0 | ||
|
4b56f6608c | ||
|
17b8f1485f | ||
|
1ebb958b4a | ||
|
94673c266d | ||
|
05409c4f10 | ||
|
0ce28fc783 | ||
|
ef40e827ae | ||
|
d8bc5d2bd8 | ||
|
9d96902794 | ||
|
02e0e513ef | ||
|
0e8ed62f23 | ||
|
9936466ce0 | ||
|
4f15cbd049 | ||
|
1a4c09ab4e | ||
|
95839372b5 | ||
|
0b29f805f4 | ||
|
c6b7aa74dd | ||
|
129caa704f | ||
|
2a6b0dc507 | ||
|
ed3ecd7b4d | ||
|
09895ce3fb | ||
|
582c9dee20 | ||
|
af29210c07 | ||
|
e2493b5228 | ||
|
3e3bf1e2bc | ||
|
4b07142d49 | ||
|
b7b5843d46 | ||
|
eddacd0091 | ||
|
035877c368 | ||
|
dfad3d9d84 | ||
|
3ac04a0ebd | ||
|
e65b2d8780 | ||
|
8b572d6184 | ||
|
f6040de6fb | ||
|
b36ccda79d | ||
|
d69c01ba14 | ||
|
5cb2b4f049 | ||
|
ccfee97e4b | ||
|
a24822d161 | ||
|
3c104e2233 | ||
|
e98cec1734 | ||
|
b37bedba26 | ||
|
044a5ee816 | ||
|
ee7ad74fd3 | ||
|
e4ddb49a2e | ||
|
6a73aa5295 | ||
|
ebd1e60010 | ||
|
47a77d7dd2 | ||
|
77994bc5d1 | ||
|
6591d2be17 | ||
|
d0b0b56fbc | ||
|
6acbd8b1b1 | ||
|
980eaa3b6e | ||
|
810aa53e1e | ||
|
50de71b6f4 | ||
|
533f8ece3c | ||
|
313d97c1fc | ||
|
f796337ce1 | ||
|
4d3cbe9ed7 | ||
|
ad24c23701 | ||
|
39cc2f3e1a | ||
|
5ab47e8e8b | ||
|
1f13f68926 | ||
|
9257d3ad7d | ||
|
858fc209bb | ||
|
f5c0e8e58a | ||
|
abf3661edc | ||
|
8f7358b462 | ||
|
40b56f70b7 | ||
|
50bd4409f5 | ||
|
ae31807eb7 | ||
|
ffc392b66b | ||
|
0466e570ad | ||
|
05bd54f021 | ||
|
a3ad876bf5 | ||
|
e20f65823b | ||
|
96c1df5c3a | ||
|
0b81a59c71 | ||
|
d54c26d9be | ||
|
8653e43b2b | ||
|
bec3249b01 | ||
|
067e807db7 | ||
|
da10988b29 | ||
|
8a45d26fda | ||
|
5b754aaa55 | ||
|
3dc492b686 | ||
|
5f39d2875c | ||
|
f20931b6de | ||
|
5d9d738152 | ||
|
b5b2cfb38f | ||
|
d44ec05a1d | ||
|
9c1c2f51e5 | ||
|
36b24e27e4 | ||
|
c4511c949c | ||
|
e6130d7c23 | ||
|
12e61bea0a | ||
|
0c1cec01c2 | ||
|
951c21eecc | ||
|
59fee11216 | ||
|
a475f2048d | ||
|
fe8e68e4a2 | ||
|
c0a04fc74f | ||
|
a2cd5ba187 | ||
|
1551943fdb | ||
|
90519cff68 | ||
|
efc6467c93 | ||
|
4529a93175 | ||
|
0d5a8fab1b | ||
|
0199749a31 | ||
|
c80f355d47 | ||
|
4a454917dd | ||
|
a187f8ee97 | ||
|
ab5ca3db8b | ||
|
86fc27e340 | ||
|
2a6f1f9183 | ||
|
a34999b1c5 | ||
|
65f3603953 | ||
|
4bcae146c1 | ||
|
b6c530915f | ||
|
04d550f80c | ||
|
2f40c20175 | ||
|
d9b7df693a | ||
|
5f03c8e878 | ||
|
8923bcb619 | ||
|
fccd3d0037 | ||
|
c1bdade49f | ||
|
4da8689ace | ||
|
fa0570e3d8 | ||
|
063e2b4254 | ||
|
a952b08b76 | ||
|
bdb9c15c7e | ||
|
440ed6d1fb | ||
|
bb6e42a662 | ||
|
31973431ff | ||
|
17aeacf594 | ||
|
58f183506e | ||
|
471f075dea | ||
|
d3815f80d7 | ||
|
eeb9ec2535 | ||
|
f6c824eecb | ||
|
3ade1af7a2 | ||
|
ac9819b1dd | ||
|
1d5f4ff31c | ||
|
00ea10d9c8 | ||
|
f5eceafdf3 | ||
|
479bac428f | ||
|
b02f012b18 | ||
|
4c86b625c7 | ||
|
9305a129a8 | ||
|
6e60cc8bda | ||
|
a8aa2652df | ||
|
0db84ce644 | ||
|
6d9ffeda94 | ||
|
3c00ed29d0 | ||
|
108eda4450 | ||
|
8c726dce01 | ||
|
ce880c06bd | ||
|
3e672eeabd | ||
|
96f9f93957 | ||
|
edab8fe4a9 | ||
|
f60bebd5ae | ||
|
ebe4fa64d8 | ||
|
6611f262bf | ||
|
3e18f5d415 | ||
|
b6fbaa5825 | ||
|
6ca7924f7b | ||
|
a45d640272 | ||
|
8881d8a1a4 | ||
|
49186ae2d1 | ||
|
1299d7f04f | ||
|
871db25bc9 | ||
|
3556130f6b | ||
|
d3d23140fd | ||
|
47155cdb83 | ||
|
b419477a7d | ||
|
46c05eba05 | ||
|
2d11cfae1e | ||
|
665bed9f79 | ||
|
21c273cec7 | ||
|
fa2cdc5177 | ||
|
d909be3b65 | ||
|
b6e37595a5 | ||
|
d2257c26ff | ||
|
f3609ebd80 | ||
|
0136189aa4 | ||
|
a7c550ab69 | ||
|
92310ebdab | ||
|
769f2e33f6 | ||
|
7ec8540db4 | ||
|
0775a27d1d | ||
|
938195753c | ||
|
856a751b51 | ||
|
0169e83731 | ||
|
a59b340d67 | ||
|
c82ad234e0 | ||
|
f3d8304db8 | ||
|
a17123401d | ||
|
1bb1ce4f8b | ||
|
a356a26e2f | ||
|
634b044bc5 | ||
|
57f3cdb7e0 | ||
|
7541bcae66 | ||
|
187d2ac04e | ||
|
deb88297a0 | ||
|
6063fa6651 | ||
|
e479619387 | ||
|
16c3f01118 | ||
|
908d53189a | ||
|
709b7b4e32 | ||
|
00b1eb387c | ||
|
c5aab72e71 | ||
|
bebb975584 | ||
|
03b9ab9a83 | ||
|
8c896fe8c1 | ||
|
431574b756 | ||
|
deefeb1f64 | ||
|
0ef03015e0 | ||
|
ef42edbfd7 | ||
|
0840d69ee9 | ||
|
c9e77f3050 | ||
|
48f2cefb5b | ||
|
a97e0d31e7 | ||
|
aa480e01a2 | ||
|
7b5f13524d | ||
|
39aac10a17 | ||
|
9a0bfc39a4 | ||
|
06e759966c | ||
|
196681513c | ||
|
4d908e1f72 | ||
|
2ca7e64fd5 | ||
|
c435e25390 | ||
|
0783d9e68a | ||
|
5cea7e47e4 | ||
|
bdc23c64e2 | ||
|
df293e31cc | ||
|
2623fc44c6 | ||
|
47aea604df | ||
|
71d087aa23 | ||
|
37770654e1 | ||
|
7c6e87afff | ||
|
49afc8a75b | ||
|
629385599c | ||
|
5d10d76a15 | ||
|
3c052e9fef | ||
|
d676b8430d | ||
|
7f02bfe4d5 | ||
|
c769638005 | ||
|
fecd2aefc0 | ||
|
2ffcf02946 | ||
|
cb5e01ed5b | ||
|
9501dab9f4 | ||
|
c030724663 | ||
|
c60213a135 | ||
|
3d69ae225d | ||
|
1018369576 | ||
|
6818635ee4 | ||
|
b364269041 | ||
|
63e303b6f8 | ||
|
279dfa4f8f | ||
|
e1f7e074be | ||
|
0ce5e9114f | ||
|
a92afc3c28 | ||
|
e87ccf5a3b | ||
|
46ce4bbbe0 | ||
|
1fb6d64f8a | ||
|
a2abf3b47d | ||
|
08a33497a0 | ||
|
486929acc7 | ||
|
41b574c468 | ||
|
8ce89f0fe6 | ||
|
16e6dd6d14 | ||
|
3680f1fe94 | ||
|
c8a144427e | ||
|
afc0124982 | ||
|
03e5cd350d | ||
|
4fd7108ec6 | ||
|
046b8c4117 | ||
|
d96cbfb228 | ||
|
261cc5ed2c | ||
|
e8f21216a5 | ||
|
27085d1db0 | ||
|
2430a74414 | ||
|
fe8f9c29f9 | ||
|
4cba773eb0 | ||
|
005f3f8e4d | ||
|
87ec80d404 | ||
|
cfd79b1be1 | ||
|
98139f9210 | ||
|
e71908ed1c | ||
|
f037ddf74c | ||
|
f37728c35b | ||
|
db1ad18cd5 | ||
|
ec0c283bf4 | ||
|
7aba19f137 | ||
|
94988d98c0 | ||
|
ccd2dc0783 | ||
|
854b0bd35e | ||
|
ad4d60f3ca | ||
|
a0fb841b8e | ||
|
83cae914a4 | ||
|
89060afc8d | ||
|
40b559f80d | ||
|
bc51d257fd | ||
|
94bbe2361b | ||
|
bbb68d86d5 | ||
|
08357dfa9d | ||
|
83248785e5 | ||
|
ce43f15ad5 | ||
|
b7c1ee3b05 |
270 changed files with 59596 additions and 5938 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
.git
|
||||
node_modules
|
3
.github/funding.yml
vendored
Normal file
3
.github/funding.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
liberapay: derhuerst
|
||||
patreon: derhuerst
|
||||
github: derhuerst
|
59
.github/workflows/build.yml
vendored
Normal file
59
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
name: Build NPM Package & Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-pkg:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
101
.github/workflows/test.yml
vendored
Normal file
101
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,101 @@
|
|||
name: test
|
||||
|
||||
on: [push, pull_request, workflow_call]
|
||||
|
||||
env:
|
||||
npm_config_cache: /tmp/npm-cache
|
||||
|
||||
jobs:
|
||||
lint-and-spellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- run: npm install
|
||||
|
||||
- name: Run lint check
|
||||
run: npm run lint
|
||||
|
||||
- name: Run spell check
|
||||
run: npm run test-spelling
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 22.x
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: setup Node.js v${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- id: cache-npm
|
||||
name: restore npm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-unit-tests
|
||||
path: ${{ env.npm_config_cache }}
|
||||
- run: npm install
|
||||
|
||||
- run: npm run test-unit
|
||||
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 22.x
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: setup Node.js v${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- id: cache-npm
|
||||
name: restore npm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-integration-tests
|
||||
path: ${{ env.npm_config_cache }}
|
||||
- run: npm install
|
||||
|
||||
- run: npm run test-integration
|
||||
|
||||
e2e-tests:
|
||||
needs: [unit-tests, integration-tests]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: setup Node.js v${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- id: cache-npm
|
||||
name: restore npm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-e2e-tests
|
||||
path: ${{ env.npm_config_cache }}
|
||||
- run: npm install
|
||||
|
||||
- run: npm run test-e2e
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,3 +4,6 @@ Thumbs.db
|
|||
.nvm-version
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
/.tap
|
||||
*.ign.*
|
|
@ -1,6 +0,0 @@
|
|||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '8'
|
||||
- '6'
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
|||
FROM node:18-alpine
|
||||
LABEL org.opencontainers.image.title="db-vendo-client"
|
||||
LABEL org.opencontainers.image.description="A clean REST API wrapping around the new Deutsche Bahn API."
|
||||
LABEL org.opencontainers.image.authors="Traines <git@traines.eu>"
|
||||
LABEL org.opencontainers.image.documentation="https://github.com/public-transport/db-vendo-client"
|
||||
LABEL org.opencontainers.image.source="https://github.com/public-transport/db-vendo-client"
|
||||
LABEL org.opencontainers.image.licenses="ISC"
|
||||
WORKDIR /app
|
||||
|
||||
# install dependencies
|
||||
#RUN apk add --update git
|
||||
ADD package.json package-lock.json /app/
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# add source code
|
||||
ADD . /app
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
CMD ["node", "api.js"]
|
|
@ -1,4 +1,7 @@
|
|||
Copyright (c) 2017, Jannis R
|
||||
# ISC License
|
||||
|
||||
- Copyright © 2024 Jannis R
|
||||
- Copyright © 2025 traines-source
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
52
api.js
Normal file
52
api.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {createClient} from './index.js';
|
||||
import {profile as dbProfile} from './p/db/index.js';
|
||||
import {profile as dbnavProfile} from './p/dbnav/index.js';
|
||||
import {profile as dbwebProfile} from './p/dbweb/index.js';
|
||||
import {profile as dbrisProfile} from './p/dbris/index.js';
|
||||
import {profile as dbbahnhofProfile} from './p/dbbahnhof/index.js';
|
||||
import {profile as dbregioguideProfile} from './p/dbregioguide/index.js';
|
||||
import {mapRouteParsers} from './lib/api-parsers.js';
|
||||
import {createHafasRestApi as createApi} from 'hafas-rest-api';
|
||||
|
||||
const config = {
|
||||
hostname: process.env.HOSTNAME || 'localhost',
|
||||
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
|
||||
name: 'db-vendo-client',
|
||||
description: 'db-vendo-client',
|
||||
homepage: 'https://github.com/public-transport/db-vendo-client',
|
||||
version: '6',
|
||||
docsLink: 'https://github.com/public-transport/db-vendo-client',
|
||||
openapiSpec: true,
|
||||
logging: true,
|
||||
aboutPage: true,
|
||||
enrichStations: true,
|
||||
etags: 'strong',
|
||||
csp: 'default-src \'none\'; style-src \'self\' \'unsafe-inline\'; img-src https:',
|
||||
mapRouteParsers,
|
||||
};
|
||||
|
||||
const profiles = {
|
||||
db: dbProfile,
|
||||
dbnav: dbnavProfile,
|
||||
dbweb: dbwebProfile,
|
||||
dbris: dbrisProfile,
|
||||
dbbahnhof: dbbahnhofProfile,
|
||||
dbregioguide: dbregioguideProfile,
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
const vendo = createClient(
|
||||
profiles[process.env.DB_PROFILE] || dbnavProfile,
|
||||
process.env.USER_AGENT || 'link-to-your-project-or-email',
|
||||
config,
|
||||
);
|
||||
const api = await createApi(vendo, config);
|
||||
|
||||
api.listen(config.port, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
start();
|
7
contributing.md
Normal file
7
contributing.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Contributing
|
||||
|
||||
Thanks for helping! 🙏
|
||||
|
||||
## Adding integration/end-to-end tests
|
||||
|
||||
Refer to the [testing docs](docs/tests.md).
|
644
cspell.config.json
Normal file
644
cspell.config.json
Normal file
|
@ -0,0 +1,644 @@
|
|||
{
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"Abfahrt",
|
||||
"abfahrten",
|
||||
"abfahrts",
|
||||
"abfrage",
|
||||
"abgangs",
|
||||
"Abgelaufen",
|
||||
"Abonnement",
|
||||
"Abschnitte",
|
||||
"abschnitts",
|
||||
"Adelsheim",
|
||||
"Adenauerplatz",
|
||||
"agilis",
|
||||
"Ahorn",
|
||||
"Ahrensfelde",
|
||||
"Aktualisierung",
|
||||
"Alexanderplatz",
|
||||
"alterseingabe",
|
||||
"Altstadt",
|
||||
"Alzey",
|
||||
"andere",
|
||||
"Anfang",
|
||||
"anfrage",
|
||||
"Anfrage",
|
||||
"anfragezeit",
|
||||
"Angebot",
|
||||
"angebote",
|
||||
"angebots",
|
||||
"angebotsbeziehung",
|
||||
"angebotseinholung",
|
||||
"ANGEBOTSINFORMATION",
|
||||
"Angebotsoption",
|
||||
"angefragten",
|
||||
"Angeltürn",
|
||||
"ankuenfte",
|
||||
"ankunft",
|
||||
"ankunfts",
|
||||
"Anmeldung",
|
||||
"ANRUFPFLICHTIG",
|
||||
"ANRUFPFLICHTIGEVERKEHRE",
|
||||
"Anteil",
|
||||
"anzahl",
|
||||
"anzeige",
|
||||
"Anzeigen",
|
||||
"APPLEPAY",
|
||||
"ARGUMENTE",
|
||||
"arrs",
|
||||
"Arverio",
|
||||
"Aschaffenburg",
|
||||
"attribut",
|
||||
"Atze",
|
||||
"Auerbach",
|
||||
"AUSFALL",
|
||||
"ausgegeben",
|
||||
"auslastung",
|
||||
"auslastungs",
|
||||
"Auslastungsinformation",
|
||||
"auslastungsmeldungen",
|
||||
"auslastungstexte",
|
||||
"Ausreserviert",
|
||||
"außergewöhnlich",
|
||||
"Ausserhalb",
|
||||
"außerhalb",
|
||||
"Ausstattung",
|
||||
"Auswahl",
|
||||
"autonome",
|
||||
"bahnbonus",
|
||||
"BAHNCARD",
|
||||
"Bahnhof",
|
||||
"bahnhofs",
|
||||
"Bahnhofsinfo",
|
||||
"bahnhofstafel",
|
||||
"bahnhofstafeln",
|
||||
"BARRIEREFREI",
|
||||
"Bayerischer",
|
||||
"BEFÖRDERER",
|
||||
"begrenzt",
|
||||
"behaviour",
|
||||
"Behindertengerechte",
|
||||
"Behindertengerechtes",
|
||||
"Benoit",
|
||||
"Beratzhausen",
|
||||
"bereits",
|
||||
"besonderer",
|
||||
"Besucherpark",
|
||||
"betrag",
|
||||
"Beusselstraße",
|
||||
"bezeichnung",
|
||||
"Bietigheim",
|
||||
"Bismarckstr",
|
||||
"Bissingen",
|
||||
"bitmasks",
|
||||
"Blaschkoallee",
|
||||
"Blissestr",
|
||||
"BNWNZF",
|
||||
"Böhme",
|
||||
"BONVOYO",
|
||||
"Bordbistro",
|
||||
"Bordrestaurant",
|
||||
"Boxberg",
|
||||
"Breckerfeld",
|
||||
"Britz",
|
||||
"brokentrip",
|
||||
"brutto",
|
||||
"Buch",
|
||||
"Buchbar",
|
||||
"buchbarkeit",
|
||||
"BUCHEN",
|
||||
"BUCHUNG",
|
||||
"buchungs",
|
||||
"Bundesbahnen",
|
||||
"Bundesplatz",
|
||||
"BUSSE",
|
||||
"bzgl",
|
||||
"capacitorjs",
|
||||
"Chaussee",
|
||||
"checkin",
|
||||
"childrens",
|
||||
"CITYTICKET",
|
||||
"clie",
|
||||
"cncl",
|
||||
"codeshares",
|
||||
"conds",
|
||||
"consumability",
|
||||
"Consumability",
|
||||
"Creglingen",
|
||||
"crosssell",
|
||||
"Ctrf",
|
||||
"customisation",
|
||||
"customisations",
|
||||
"Damm",
|
||||
"Daten",
|
||||
"Dauer",
|
||||
"dauerhaft",
|
||||
"dbnav",
|
||||
"dbregioguide",
|
||||
"dbris",
|
||||
"dbweb",
|
||||
"Deldicque",
|
||||
"DELFI",
|
||||
"derhuerst",
|
||||
"Deutz",
|
||||
"dhid",
|
||||
"differenzpreis",
|
||||
"Dinkelsbühl",
|
||||
"distanz",
|
||||
"Dombühl",
|
||||
"echtzeit",
|
||||
"Eggmühl",
|
||||
"ehemals",
|
||||
"Eicholzheim",
|
||||
"einchecken",
|
||||
"eine",
|
||||
"einfache",
|
||||
"Einrichtung",
|
||||
"Einstieg",
|
||||
"einstiegs",
|
||||
"Einstiegshilfe",
|
||||
"Einstiegstyp",
|
||||
"Eisenacher",
|
||||
"emis",
|
||||
"empfehlen",
|
||||
"Ende",
|
||||
"entfällt",
|
||||
"Entgelt",
|
||||
"ereignis",
|
||||
"Erforderlich",
|
||||
"erforderlicher",
|
||||
"Ergoldsbach",
|
||||
"Erlenbach",
|
||||
"ERMAESSIGUNG",
|
||||
"ermaessigungen",
|
||||
"erster",
|
||||
"ERWACHSENER",
|
||||
"erwarten",
|
||||
"erwartet",
|
||||
"Eubigheim",
|
||||
"Eurocity",
|
||||
"eventuell",
|
||||
"externe",
|
||||
"Fahrkarte",
|
||||
"fahrplan",
|
||||
"Fahrplanperiode",
|
||||
"Fahrpreis",
|
||||
"Fahrradbeförderung",
|
||||
"FAHRRADMITNAHME",
|
||||
"Fahrt",
|
||||
"FAHRZEUG",
|
||||
"Fahrzeuggebundene",
|
||||
"FAMILIENKIND",
|
||||
"Fehler",
|
||||
"Fehrbelliner",
|
||||
"Fernbf",
|
||||
"Fernverkehr",
|
||||
"Flexpreis",
|
||||
"Flix",
|
||||
"Fltr",
|
||||
"Flughafen",
|
||||
"FPTF",
|
||||
"Freising",
|
||||
"Friedrichshall",
|
||||
"Friedrichstr",
|
||||
"frueher",
|
||||
"Frwd",
|
||||
"FUSSWEG",
|
||||
"Fußweg",
|
||||
"FVFFLPI",
|
||||
"FVFSPPI",
|
||||
"FVFSSPI",
|
||||
"FVKBACI",
|
||||
"GARE",
|
||||
"Garten",
|
||||
"Gastronomie",
|
||||
"Gattung",
|
||||
"gekauft",
|
||||
"Geltungzeitpunkt",
|
||||
"GENERALABONNEMENT",
|
||||
"geolocation",
|
||||
"geopositions",
|
||||
"Geringe",
|
||||
"gesamt",
|
||||
"Gesundbrunnen",
|
||||
"gleis",
|
||||
"Gneisenaustr",
|
||||
"Goerdelersteg",
|
||||
"grafisch",
|
||||
"Greifswalder",
|
||||
"Grenzallee",
|
||||
"Grünanlagen",
|
||||
"gruppen",
|
||||
"Gütersloh",
|
||||
"GUTSCHEIN",
|
||||
"haben",
|
||||
"hafas",
|
||||
"Hagelstadt",
|
||||
"Halbtax",
|
||||
"HALBTAXABO",
|
||||
"Halemweg",
|
||||
"Halensee",
|
||||
"Hallerstraße",
|
||||
"halte",
|
||||
"Haltestellen",
|
||||
"Hansering",
|
||||
"Hansestadt",
|
||||
"Haselhorst",
|
||||
"Hauptbahnhof",
|
||||
"Heidelberger",
|
||||
"Hennef",
|
||||
"Hermannplatz",
|
||||
"Hermannstraße",
|
||||
"hessen",
|
||||
"Heuchelhof",
|
||||
"HINFAHRT",
|
||||
"Hinweis",
|
||||
"Hinweise",
|
||||
"HOCH",
|
||||
"HOCHGESCHWINDIGKEITSZUEGE",
|
||||
"Hohe",
|
||||
"Hohenstadt",
|
||||
"Hohenzollerndamm",
|
||||
"Hüngheim",
|
||||
"IBNR",
|
||||
"Ihren",
|
||||
"Ihrer",
|
||||
"Informationen",
|
||||
"INKLUSIVE",
|
||||
"Innsbrucker",
|
||||
"Instanz",
|
||||
"INTERCITYUNDEUROCITYZUEGE",
|
||||
"INTERREGIOUNDSCHNELLZUEGE",
|
||||
"inventarsystem",
|
||||
"irregulaere",
|
||||
"Jakob",
|
||||
"Jannis",
|
||||
"Jannowitzbrücke",
|
||||
"jetzt",
|
||||
"Johannisthaler",
|
||||
"journeystop",
|
||||
"JUGENDLICHER",
|
||||
"Jungfernheide",
|
||||
"Kanal",
|
||||
"KATALOG",
|
||||
"kategorie",
|
||||
"Kategorien",
|
||||
"kategorisierung",
|
||||
"kein",
|
||||
"KEINE",
|
||||
"Kennung",
|
||||
"Kennzeichen",
|
||||
"Kirche",
|
||||
"klasse",
|
||||
"KLASSENLOS",
|
||||
"KLEINKIND",
|
||||
"Kleistpark",
|
||||
"Klima",
|
||||
"KLIMATICKET",
|
||||
"Köfering",
|
||||
"Köln",
|
||||
"kombinations",
|
||||
"komfort",
|
||||
"konditionen",
|
||||
"konditions",
|
||||
"Konstanzer",
|
||||
"kontext",
|
||||
"Kontingente",
|
||||
"KRCC",
|
||||
"KREDITKARTE",
|
||||
"Kristjan",
|
||||
"Kurz",
|
||||
"kurztext",
|
||||
"Landsberger",
|
||||
"Landshut",
|
||||
"langtext",
|
||||
"LASTSCHRIFT",
|
||||
"Lauda",
|
||||
"letzte",
|
||||
"letztes",
|
||||
"leuchtturm",
|
||||
"Lichtenberg",
|
||||
"liegt",
|
||||
"linien",
|
||||
"Lipschitzallee",
|
||||
"Loesungs",
|
||||
"Ludwigsburg",
|
||||
"luxon",
|
||||
"materialisierungs",
|
||||
"Maxnet",
|
||||
"MBAAA",
|
||||
"Mehringdamm",
|
||||
"Meidling",
|
||||
"Meldungen",
|
||||
"Merchingen",
|
||||
"Messe",
|
||||
"Mierendorffplatz",
|
||||
"Millis",
|
||||
"Minden",
|
||||
"mitteltext",
|
||||
"mittlere",
|
||||
"Mobilitätseingeschränkte",
|
||||
"Mobilitätsservice",
|
||||
"Möckernbrücke",
|
||||
"Möckmühl",
|
||||
"modul",
|
||||
"moeckmuehl",
|
||||
"Moeglich",
|
||||
"möglich",
|
||||
"Montabaur",
|
||||
"Moosburg",
|
||||
"Mosbach",
|
||||
"Movas",
|
||||
"München",
|
||||
"Musiktheater",
|
||||
"mwst",
|
||||
"mxtxm",
|
||||
"Nachgelagert",
|
||||
"Nachricht",
|
||||
"Nahreisezug",
|
||||
"NAHVERKEHRSONSTIGEZUEGE",
|
||||
"Naturkundemuseum",
|
||||
"Neckarsulm",
|
||||
"netto",
|
||||
"Neufahrn",
|
||||
"Neukölln",
|
||||
"Neumarkt",
|
||||
"NICHT",
|
||||
"Niederbay",
|
||||
"NIEDRIG",
|
||||
"noch",
|
||||
"noopener",
|
||||
"Nord",
|
||||
"noreferrer",
|
||||
"Notiz",
|
||||
"Notizen",
|
||||
"NULLPREIS",
|
||||
"nummer",
|
||||
"nutzungs",
|
||||
"Obereubigheim",
|
||||
"Oberpf",
|
||||
"Oberschefflenz",
|
||||
"Obertraubling",
|
||||
"Oberwittstadt",
|
||||
"Ohne",
|
||||
"orte",
|
||||
"Ostbahnhof",
|
||||
"Osterburken",
|
||||
"Österreichische",
|
||||
"Ostkreuz",
|
||||
"Parchimer",
|
||||
"Parsberg",
|
||||
"passend",
|
||||
"Passlist",
|
||||
"Paulsternstr",
|
||||
"Pauschalpreis",
|
||||
"Pergamonkeller",
|
||||
"Pergamonweg",
|
||||
"Perleberg",
|
||||
"PFLICHT",
|
||||
"PLAETZE",
|
||||
"planungs",
|
||||
"platf",
|
||||
"Platz",
|
||||
"platzbedarfe",
|
||||
"platzprofil",
|
||||
"pollyjs",
|
||||
"polyline",
|
||||
"polylines",
|
||||
"Positionen",
|
||||
"Preis",
|
||||
"preise",
|
||||
"preisunterdrueckung",
|
||||
"Prenzlauer",
|
||||
"prio",
|
||||
"priorisierte",
|
||||
"prioritaet",
|
||||
"produkt",
|
||||
"produkte",
|
||||
"produktgattungen",
|
||||
"Profil",
|
||||
"PRUEFEN",
|
||||
"PUBLICTRANSPORT",
|
||||
"Punkte",
|
||||
"rabatt",
|
||||
"Radzio",
|
||||
"RAILPLUS",
|
||||
"randomised",
|
||||
"Ravenstein",
|
||||
"Referenz",
|
||||
"referenzen",
|
||||
"referenziertes",
|
||||
"Regio",
|
||||
"Regionalbf",
|
||||
"regulaere",
|
||||
"regulaerer",
|
||||
"reise",
|
||||
"REISEANGEBOT",
|
||||
"reiseloesung",
|
||||
"reisende",
|
||||
"reisenden",
|
||||
"REISESTELLENKARTE",
|
||||
"reisetag",
|
||||
"Reisezentrum",
|
||||
"REMENTR",
|
||||
"REMRESR",
|
||||
"Reservierbar",
|
||||
"Reservieren",
|
||||
"Reservierung",
|
||||
"reservierungen",
|
||||
"reservierungs",
|
||||
"RESERVIERUNGSANGEBOT",
|
||||
"RESERVIERUNGSENTGELT",
|
||||
"reservierungspflicht",
|
||||
"Rhein",
|
||||
"richtung",
|
||||
"roehrt",
|
||||
"Rohrdamm",
|
||||
"Roigheim",
|
||||
"Rollstuhl",
|
||||
"rollstuhlgerechte",
|
||||
"Römisches",
|
||||
"Rosenthal",
|
||||
"Rothenburg",
|
||||
"Rudow",
|
||||
"Rueck",
|
||||
"Rüsselsheim",
|
||||
"Saale",
|
||||
"Sammelfaehig",
|
||||
"satz",
|
||||
"SBAHN",
|
||||
"SBAHNEN",
|
||||
"Scharnweberstr",
|
||||
"SCHIFF",
|
||||
"SCHIFFE",
|
||||
"Schlachthof",
|
||||
"schnelle",
|
||||
"Schöneberg",
|
||||
"Schönefeld",
|
||||
"Schönhauser",
|
||||
"Schwedter",
|
||||
"Seckach",
|
||||
"Sennfeld",
|
||||
"servicekategorie",
|
||||
"SHCARD",
|
||||
"sich",
|
||||
"SICT",
|
||||
"Sieg",
|
||||
"Siegburg",
|
||||
"Siemensdamm",
|
||||
"Siglingen",
|
||||
"sitzplatz",
|
||||
"Sitzplatzreservierung",
|
||||
"slugg",
|
||||
"Sonnenallee",
|
||||
"Sören",
|
||||
"spaeter",
|
||||
"Sparpreis",
|
||||
"spezifische",
|
||||
"Spichernstr",
|
||||
"Spittelmarkt",
|
||||
"Stadtgebiet",
|
||||
"Stadtmitte",
|
||||
"Stadtpark",
|
||||
"Steinach",
|
||||
"Storkower",
|
||||
"STRASSENBAHN",
|
||||
"stufe",
|
||||
"stufenfrei",
|
||||
"suchbegriff",
|
||||
"Suche",
|
||||
"Südkreuz",
|
||||
"Südstern",
|
||||
"Sutter",
|
||||
"tage",
|
||||
"täglich",
|
||||
"tapjs",
|
||||
"tarif",
|
||||
"Tarifbereiche",
|
||||
"Tarifgebiet",
|
||||
"Tarifzone",
|
||||
"Tauber",
|
||||
"teilpreis",
|
||||
"teilstrecken",
|
||||
"Tempelhof",
|
||||
"Texte",
|
||||
"Tiergarten",
|
||||
"Toel",
|
||||
"Torfstr",
|
||||
"Torfstraße",
|
||||
"Traines",
|
||||
"traveller",
|
||||
"Travellers",
|
||||
"travelling",
|
||||
"Treptower",
|
||||
"Tsua",
|
||||
"tvlr",
|
||||
"UBAHN",
|
||||
"ueber",
|
||||
"uebergreifende",
|
||||
"ueberschrift",
|
||||
"Uiffingen",
|
||||
"Umbuchungen",
|
||||
"Umstiege",
|
||||
"umstiegs",
|
||||
"Umstiegsdauer",
|
||||
"Umstiegszeit",
|
||||
"unsere",
|
||||
"unter",
|
||||
"Unterwittstadt",
|
||||
"upsell",
|
||||
"ursprungs",
|
||||
"ushrp",
|
||||
"uuidv",
|
||||
"Vaihingen",
|
||||
"vendo",
|
||||
"verbindung",
|
||||
"verbindungen",
|
||||
"verbindungs",
|
||||
"verbindungsabschnitt",
|
||||
"verbindungssuche",
|
||||
"Verfuegbar",
|
||||
"verfügbar",
|
||||
"verkehrmittel",
|
||||
"verkehrsmittel",
|
||||
"Verkehrstage",
|
||||
"VERKNUEPFT",
|
||||
"Verlauf",
|
||||
"vertraglicher",
|
||||
"vlexx",
|
||||
"Voordeelurenabo",
|
||||
"Vorhanden",
|
||||
"VORTEILE",
|
||||
"VORTEILSCARD",
|
||||
"Vorverkaufszeitraum",
|
||||
"waehrung",
|
||||
"wagenreihung",
|
||||
"wählen",
|
||||
"Wannsee",
|
||||
"Wegener",
|
||||
"Weichselstr",
|
||||
"wenden",
|
||||
"wenn",
|
||||
"Westbahn",
|
||||
"Westend",
|
||||
"Westf",
|
||||
"westhafen",
|
||||
"Westkreuz",
|
||||
"wichtig",
|
||||
"Wilmersdorfer",
|
||||
"wird",
|
||||
"Witzleben",
|
||||
"wochentage",
|
||||
"Worringen",
|
||||
"wunsch",
|
||||
"Wunschplatz",
|
||||
"Württemberg",
|
||||
"württembergallee",
|
||||
"Würzburg",
|
||||
"Wutzkyallee",
|
||||
"Yorckstr",
|
||||
"Yureka",
|
||||
"Zahlungsarten",
|
||||
"Zeitpunkt",
|
||||
"Zeitraum",
|
||||
"zgck",
|
||||
"ziel",
|
||||
"Zimmern",
|
||||
"Zitadelle",
|
||||
"Zoologischer",
|
||||
"Zuege",
|
||||
"zugart",
|
||||
"zugattrib",
|
||||
"zugattribute",
|
||||
"Zuges",
|
||||
"zugfahrt",
|
||||
"zuglaeufe",
|
||||
"Zuglauf",
|
||||
"zugnummer",
|
||||
"zulaessige",
|
||||
"Zusammenfassung",
|
||||
"Züttlingen",
|
||||
"Zwickauer",
|
||||
"zwischenhalte",
|
||||
"bestprice",
|
||||
"intervalle",
|
||||
"Intervalle",
|
||||
"tagesbest",
|
||||
"dbbahnhof",
|
||||
"Deutschlandticket",
|
||||
"fahrverguenstigungen",
|
||||
"cancelation"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"docs/dumps/**",
|
||||
"test/e2e/fixtures/**",
|
||||
"test/fixtures/**",
|
||||
"test/parse/remarks.js",
|
||||
"test/parse/dbnav-journey.js"
|
||||
],
|
||||
"dictionaries": [
|
||||
"node"
|
||||
]
|
||||
}
|
19
docs/api.md
Normal file
19
docs/api.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# `db-vendo-client` API
|
||||
|
||||
Also see the [root readme](https://github.com/public-transport/db-vendo-client) for a shortlist of differences of db-vendo-client to hafas-client and of differences between the profiles.
|
||||
|
||||
- [`journeys(from, to, [opt])`](journeys.md) – get journeys between locations
|
||||
- [`refreshJourney(refreshToken, [opt])`](refresh-journey.md) – fetch up-to-date/more details of a `journey`
|
||||
- `journeysFromTrip(tripId, previousStopover, to, [opt])` – not supported
|
||||
- [`trip(id, lineName, [opt])`](trip.md) – get details for a trip
|
||||
- `tripsByName(lineNameOrFahrtNr, [opt])` – not supported
|
||||
- [`departures(station, [opt])`](departures.md) – query the next departures at a station
|
||||
- [`arrivals(station, [opt])`](arrivals.md) – query the next arrivals at a station
|
||||
- [`locations(query, [opt])`](locations.md) – find stations, POIs and addresses
|
||||
- [`stop(id, [opt])`](stop.md) – get details about a stop/station
|
||||
- [`nearby(location, [opt])`](nearby.md) – show stations & POIs around
|
||||
- `radar(north, west, south, east, [opt])` – not supported
|
||||
- `reachableFrom(address, [opt])` – not supported
|
||||
- `remarks([opt])` – not supported
|
||||
- `lines(query, [opt])` – not supported
|
||||
- `serverInfo([opt])` – not supported
|
3
docs/arrivals.md
Normal file
3
docs/arrivals.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `arrivals(station, [opt])`
|
||||
|
||||
Just like [`departures(station, [opt])`](departures.md), except that it resolves with arrival times instead of departure times.
|
|
@ -1,6 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
## `2.5.0`
|
||||
|
||||
- new [Schleswig-Holstein (NAH.SH)](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) [profile](../p/nahsh)
|
||||
- new [*writing a profile* guide](./writing-a-profile.md)
|
90
docs/db-apis.md
Normal file
90
docs/db-apis.md
Normal file
|
@ -0,0 +1,90 @@
|
|||
# New DB Board and Route Planning APIs (beyond HAFAS and IRIS)
|
||||
|
||||
(Beware that a DB journey is what you usually call a trip (a vehicle travelling at a certain time) and a DB trip is what you usually call a journey (result of a route search from A to B).)
|
||||
|
||||
## RIS::Boards
|
||||
https://apis.deutschebahn.com/db/apis/ris-boards/v1/public/
|
||||
|
||||
EPs:
|
||||
* departures/<evaNo>
|
||||
* arrivals/<evaNo>
|
||||
|
||||
Notes:
|
||||
* docs (also helpful for other RIS-based APIs below): https://developers.deutschebahn.com/db-api-marketplace/apis/product/ris-boards-transporteure/api/ris-boards-transporteure#/RISBoards_151/overview
|
||||
* needs an API Key
|
||||
* provides remarks
|
||||
* does not provide loadFactor
|
||||
* no route planning
|
||||
* uses RIS trip IDs
|
||||
* boards up to 12 hours
|
||||
|
||||
## bahnhof.de RIS
|
||||
https://www.bahnhof.de/api/boards/departures?evaNumbers=8000105&filterTransports=BUS&duration=60&locale=de
|
||||
|
||||
Notes:
|
||||
* no API Key needed
|
||||
* provides remarks
|
||||
* uses RIS trip IDs
|
||||
* no route planning
|
||||
* boards up to 6 hours, only from current time (or unknown parameter)
|
||||
|
||||
## Regio Guide RIS
|
||||
https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/
|
||||
|
||||
EPs:
|
||||
* departure/8000105?modeOfTransport=HIGH_SPEED_TRAIN,REGIONAL_TRAIN,CITY_TRAIN,INTER_REGIONAL_TRAIN,UNKNOWN,BUS,TRAM,SUBWAY&timeStart=2024-12-11T15:08:25.678Z&timeEnd=2024-12-12T01:53:25.678&expandTimeFrame=TIME_END&&occupancy=true
|
||||
* board/arrival/<evaNo>
|
||||
* routing-search (with POST body, see regio-guide.de)
|
||||
* trip/<tripId-from-routing-search>
|
||||
* journey/<journeyId-from-trip>
|
||||
|
||||
Notes:
|
||||
* no API Key needed
|
||||
* no remarks in boards (or with unknown param), only some in journey
|
||||
* cancelled trips are completely missing from boards (?)
|
||||
* uses RIS trip IDs, does not expose them directly in the routing-search response
|
||||
* loadFactor for some regional services, not for long distance services
|
||||
* boards up to 12 hours
|
||||
* routing-search returns polylines (!)
|
||||
|
||||
## Vendo/Movas Navigator API
|
||||
https://app.vendo.noncd.db.de/mob/
|
||||
|
||||
EPs:
|
||||
* bahnhofstafel/abfahrt
|
||||
* bahnhofstafel/ankunft
|
||||
* location/search
|
||||
* angebote/fahrplan (for route planning)
|
||||
* zuglauf
|
||||
* zuglaeufe/ICE_947/halte/by-abfahrt/8000207_2024 (coach sequence)
|
||||
* angebote/recon (tickets)
|
||||
* trip/recon (polylines)
|
||||
|
||||
Notes:
|
||||
* see [traffic dumps](dumps/)
|
||||
* no API Key needed
|
||||
* used by new DB Navigator
|
||||
* HAFAS trip IDs
|
||||
* boards only 1 hour (or unknown param)
|
||||
* does not contain machine-readable cancelled info in the boards (only "Halt entfällt" string), but contains relevant remarks
|
||||
* loadFactor only on journeys (?)
|
||||
* polylines only for zuglauf and trip/recon
|
||||
* limited remarks on boards
|
||||
|
||||
## Vendo/Movas bahn.de API
|
||||
https://int.bahn.de/web/api/
|
||||
|
||||
EPs:
|
||||
* angebote/fahrplan (for route planning)
|
||||
* reiseloesung/orte
|
||||
* reiseloesung/orte/nearby
|
||||
* reiseloesung/verbindung
|
||||
* reiseloesung/fahrt
|
||||
* reiseloesung/abfahrten?datum=2024-12-30&zeit=11:55:00&ortExtId=8011160&ortId=A%3D1%40O%3DBerlin+Hbf%40X%3D13369549%40Y%3D52525589%40U%3D80%40L%3D8011160%40i%3DU%C3%97008065969%40&mitVias=true&maxVias=8&verkehrsmittel[]=ICE&verkehrsmittel[]=EC_IC&verkehrsmittel[]=IR&verkehrsmittel[]=REGIONAL
|
||||
* reiseloesung/ankuenfte
|
||||
|
||||
Notes:
|
||||
* no API Key needed
|
||||
* uses HAFAS trip IDs
|
||||
* provides loadFactor
|
||||
* polylines only for /verbindung and /fahrt
|
|
@ -24,38 +24,103 @@ With `opt`, you can override the default options, which look like this:
|
|||
```js
|
||||
{
|
||||
when: new Date(),
|
||||
direction: null, // only show departures heading to this station
|
||||
duration: 10 // show departures for the next n minutes
|
||||
direction: null, // only supported in `dbweb` and with `enrichStations=true` (experimental)
|
||||
line: null, // not supported
|
||||
duration: 10, // show departures for the next n minutes
|
||||
results: null, // max. number of results; `null` means "whatever HAFAS wants"
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
linesOfStops: false, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
stopovers: false, // fetch & parse previous/next stopovers?, only supported with `dbweb` profile
|
||||
// departures at related stations
|
||||
// e.g. those that belong together on the metro map.
|
||||
includeRelatedStations: true,
|
||||
moreStops: null // also include departures/arrivals for array of up to nine additional station evaNumbers (not supported with dbnav and dbweb)
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
The maximum supported duration depends on the profile (see main readme), e.g. 720 for `db` and 60 for `dbnav`. In order to use the `dbris` profile, you need to pass the env vars `DB_API_KEY` and `DB_CLIENT_ID`.
|
||||
If you pass an object `opt.products`, its fields will partially override the default products defined in the profile.
|
||||
|
||||
## Response
|
||||
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule.
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule.
|
||||
|
||||
You may pass the `journeyId` field into [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) to get details on the vehicle's journey.
|
||||
You may pass a departure's `tripId` into [`trip(id, lineName, [opt])`](trip.md) to get details on the whole trip. For the `dbnav`/`dbweb` profile HAFAS trip ids will be returned, for the `db` profile, RIS trip ids will be returned, then the `trip()` endpoint supports both id types.
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
For `db` profile, cancelled trips will not be contained in the response! For the `db` and `dbnav` profile, only the most important remarks will be contained in the boards.
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbnavProfile, userAgent)
|
||||
|
||||
// S Charlottenburg
|
||||
client.journeys('900000024101', {duration: 3})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
const {
|
||||
departures,
|
||||
realtimeDataUpdatedAt,
|
||||
} = await client.departures('8089165', {duration: 3})
|
||||
```
|
||||
|
||||
The response may look like this:
|
||||
`realtimeDataUpdatedAt` is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
||||
|
||||
`departures` may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
journeyId: '1|31431|28|86|17122017',
|
||||
tripId: '1|31431|28|86|17122017',
|
||||
trip: 31431,
|
||||
station: {
|
||||
direction: 'S Spandau',
|
||||
// Depending on the HAFAS endpoint, the destination may be present:
|
||||
destination: {
|
||||
type: 'stop',
|
||||
id: '8089165',
|
||||
name: 'S Spandau',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8089165',
|
||||
latitude: 52.534794,
|
||||
longitude: 13.197477
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: true,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true,
|
||||
},
|
||||
},
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '18299',
|
||||
fahrtNr: '12345',
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
public: true,
|
||||
name: 'S9',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
currentTripPosition: {
|
||||
type: 'location',
|
||||
latitude: 52.500851,
|
||||
longitude: 13.283755,
|
||||
},
|
||||
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000024101',
|
||||
name: 'S Charlottenburg',
|
||||
|
@ -74,90 +139,77 @@ The response may look like this:
|
|||
regional: true
|
||||
}
|
||||
},
|
||||
when: '2017-12-17T19:32:00.000+01:00',
|
||||
delay: null
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '18299',
|
||||
name: 'S9',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
productCode: 0,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
direction: 'S Spandau'
|
||||
}, {
|
||||
journeyId: '1|30977|8|86|17122017',
|
||||
trip: 30977,
|
||||
station: { /* … */ },
|
||||
when: null,
|
||||
|
||||
when: '2017-12-17T19:32:00+01:00',
|
||||
plannedWhen: '2017-12-17T19:32:00+01:00',
|
||||
delay: null,
|
||||
platform: '2',
|
||||
plannedPlatform: '2'
|
||||
}, {
|
||||
cancelled: true,
|
||||
tripId: '1|30977|8|86|17122017',
|
||||
trip: 30977,
|
||||
direction: 'S Westkreuz',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '16441',
|
||||
name: 'S5',
|
||||
public: true,
|
||||
fahrtNr: '54321',
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
public: true,
|
||||
name: 'S5',
|
||||
symbol: 'S',
|
||||
nr: 5,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
productCode: 0,
|
||||
operator: { /* … */ }
|
||||
},
|
||||
direction: 'S Westkreuz'
|
||||
}, {
|
||||
journeyId: '1|28671|4|86|17122017',
|
||||
trip: 28671,
|
||||
station: {
|
||||
type: 'station',
|
||||
id: '900000024202',
|
||||
name: 'U Wilmersdorfer Str.',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.506415,
|
||||
longitude: 13.306777
|
||||
},
|
||||
products: {
|
||||
suburban: false,
|
||||
subway: true,
|
||||
tram: false,
|
||||
bus: false,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: false
|
||||
}
|
||||
currentTripPosition: {
|
||||
type: 'location',
|
||||
latitude: 52.505004,
|
||||
longitude: 13.322391,
|
||||
},
|
||||
when: '2017-12-17T19:35:00.000+01:00',
|
||||
delay: 0,
|
||||
|
||||
stop: { /* … */ },
|
||||
|
||||
when: null,
|
||||
plannedWhen: '2017-12-17T19:33:00+01:00'
|
||||
delay: null,
|
||||
platform: null,
|
||||
plannedPlatform: '2',
|
||||
prognosedPlatform: '2'
|
||||
}, {
|
||||
tripId: '1|28671|4|86|17122017',
|
||||
trip: 28671,
|
||||
direction: 'U Rudow',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '19494',
|
||||
name: 'U7',
|
||||
public: true,
|
||||
fahrtNr: '11111',
|
||||
mode: 'train',
|
||||
product: 'subway',
|
||||
public: true,
|
||||
name: 'U7',
|
||||
symbol: 'U',
|
||||
nr: 7,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
productCode: 1,
|
||||
operator: { /* … */ }
|
||||
},
|
||||
direction: 'U Rudow'
|
||||
currentTripPosition: {
|
||||
type: 'location',
|
||||
latitude: 52.49864,
|
||||
longitude: 13.307622,
|
||||
},
|
||||
|
||||
stop: { /* … */ },
|
||||
|
||||
when: '2017-12-17T19:35:00+01:00',
|
||||
plannedWhen: '2017-12-17T19:35:00+01:00',
|
||||
delay: 0,
|
||||
platform: null,
|
||||
plannedPlatform: null
|
||||
} ]
|
||||
```
|
||||
|
|
18
docs/dumps/PCAPdroid_02_Jan_15_23_29_moreloyaltycards.txt
Normal file
18
docs/dumps/PCAPdroid_02_Jan_15_23_29_moreloyaltycards.txt
Normal file
|
@ -0,0 +1,18 @@
|
|||
POST /mob/angebote/fahrplan HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
x-feature-reiseketten-enabled: false
|
||||
X-Correlation-ID: 68f7ceba-70e7-4a88-b9b0-454809655314_3fbcc823-2a69-46f4-9484-6d94b1c0116a
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 4779d837-2aa8-4613-9904-8f950367c3c0
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 1120
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dMünchen Hbf@X\u003d11558339@Y\u003d48140229@U\u003d81@L\u003d8000261@B\u003d1@p\u003d1734722398@i\u003dU×008020347@","verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-01-02T15:21:31.877957+01:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dStuttgart Hbf@X\u003d9181636@Y\u003d48784081@U\u003d81@L\u003d8000096@B\u003d1@p\u003d1734722398@i\u003dU×008029034@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["CH-GENERAL-ABONNEMENT KLASSE_2","CH-GENERAL-ABONNEMENT KLASSE_1","CH-HALBTAXABO_OHNE_RAILPLUS KLASSENLOS","A-VORTEILSCARD KLASSENLOS"],"reisendenTyp":"SENIOR"},{"ermaessigungen":["CH-GENERAL-ABONNEMENT KLASSE_2","CH-GENERAL-ABONNEMENT KLASSE_1","CH-HALBTAXABO_OHNE_RAILPLUS KLASSENLOS","A-VORTEILSCARD KLASSENLOS"],"reisendenTyp":"SENIOR"},{"ermaessigungen":["KLIMATICKET_OE KLASSENLOS","NL-40_OHNE_RAILPLUS KLASSENLOS","BAHNCARD100 KLASSE_1","BAHNCARD100 KLASSE_2"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}
|
17
docs/dumps/PCAPdroid_02_Jan_15_29_10_alter.txt
Normal file
17
docs/dumps/PCAPdroid_02_Jan_15_29_10_alter.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
POST /mob/angebote/recon/autonomereservierung HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
X-Correlation-ID: 68f7ceba-70e7-4a88-b9b0-454809655314_3fbcc823-2a69-46f4-9484-6d94b1c0116a
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 7d70a4fc-751f-4893-b2e2-11c925c0538b
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 1872
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"alter":63,"ermaessigungen":["KLIMATICKET_OE KLASSENLOS","NL-40_OHNE_RAILPLUS KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false,"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dMünchen Hbf@X\u003d11558339@Y\u003d48140229@L\u003d8000261@a\u003d128@$A\u003d1@O\u003dStuttgart Hbf@X\u003d9181636@Y\u003d48784081@L\u003d8000096@a\u003d128@$202501021628$202501021832$ICE 912$$1$$$$$$§T$A\u003d1@O\u003dStuttgart Hbf@X\u003d9181636@Y\u003d48784081@L\u003d8000096@a\u003d128@$A\u003d1@O\u003dParis Est@X\u003d2359120@Y\u003d48876976@L\u003d8700011@a\u003d128@$202501021852$202501022214$TGV 9570$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzIjSElOIzAjRUNLIzU2NDAyOHw1NjQwMjh8NTY0Mzc0fDU2NDM3NHwwfDB8NTY1fDU2Mzk1OHwxfDB8MjZ8MHwwfC0yMTQ3NDgzNjQ4I0dBTSMyMDEyNTE2MjgjClojVk4jMSNTVCMxNzM0NzIyMzk4I1BJIzEjWkkjMTk3Mzc3I1RBIzAjREEjMjAxMjUjMVMjODAwMDI2MSMxVCMxNjI4I0xTIzgwMDAwODAjTFQjMjIwMyNQVSM4MSNSVCMxI0NBI0lDRSNaRSM5MTIjWkIjSUNFICA5MTIjUEMjMCNGUiM4MDAwMjYxI0ZUIzE2MjgjVE8jODAwMDA5NiNUVCMxODMyIwpaI1ZOIzEjU1QjMTczNDcyMjM5OCNQSSMxI1pJIzIzOTQ0OSNUQSMwI0RBIzIwMTI1IzFTIzgwMDAwOTYjMVQjMTg1MiNMUyM4NzAwMDExI0xUIzIyMTQjUFUjODEjUlQjMSNDQSNSSFQjWkUjOTU3MCNaQiNUR1YgOTU3MCNQQyMwI0ZSIzgwMDAwOTYjRlQjMTg1MiNUTyM4NzAwMDExI1RUIzIyMTQj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32PS07DMBiEr1J5XarfTtM8JEsmDRWgAhGiCIRYhMZtg/IotlMRRTkHl2HXi/EnEWxA7Dzj8XzjhhykIj6hE8clYyLfDYowmNyHEw+1km/Eb0hR5Qvi2+PuEBAfxqSsTBgbiWEGzAYKjPTmXZp3JrUZBUBr0zec0DF5LepFZtSS+E8NMfW+i0W3NyGG8jLp1MX1HMUhzqq+AphF2ud+03y3HYqRnMj9slwPNVmaYPKUU3HDr46fxXoni9H5y0Y8cEpt27UsTzzyqUunwJgnVtylYsldAGAzKgJ8t+fUsaYOY5bnipSvjh8ALjBAT+AYbYZPLvolsVJ/oqNYpXp0pg1ymWV7lEGPdZ2Z58x+sA5y6T9Yh1IL4Dd2K01UZnWWFugZVcneuiwrVcg6KKsi0cTfxJkeLqJY6yzV5jsr1yUOjHMMNW3bfgFUarsn8gEAAA\u003d\u003d"}}
|
17
docs/dumps/PCAPdroid_02_Jan_15_29_15_intl.txt
Normal file
17
docs/dumps/PCAPdroid_02_Jan_15_29_15_intl.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_02_Jan_21_59_35_initial_transfer.txt
Normal file
17
docs/dumps/PCAPdroid_02_Jan_21_59_35_initial_transfer.txt
Normal file
File diff suppressed because one or more lines are too long
18
docs/dumps/PCAPdroid_02_Jan_22_47_29_tickets.txt
Normal file
18
docs/dumps/PCAPdroid_02_Jan_22_47_29_tickets.txt
Normal file
|
@ -0,0 +1,18 @@
|
|||
POST /mob/angebote/recon HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
x-feature-reiseketten-enabled: false
|
||||
X-Correlation-ID: e1927e98-0d8c-45f2-a161-965622ccd56a_e8cf6ea4-4103-4707-aa44-2287646a87f9
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: dcf2d458-4e11-4e40-a6e3-8922544387e8
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 2749
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false,"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dMünchen Hbf@X\u003d11558339@Y\u003d48140229@L\u003d8000261@a\u003d128@$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8663785@Y\u003d50107149@L\u003d8000105@a\u003d128@$202501030000$202501030438$ICE 618$$1$$$$$$§T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8663785@Y\u003d50107149@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dKassel-Wilhelmshöhe@X\u003d9447114@Y\u003d51312558@L\u003d8003200@a\u003d128@$202501030512$202501030644$ICE 1088$$1$$$$$$§T$A\u003d1@O\u003dKassel-Wilhelmshöhe@X\u003d9447114@Y\u003d51312558@L\u003d8003200@a\u003d128@$A\u003d1@O\u003dHamm(Westf)Hbf@X\u003d7807824@Y\u003d51678077@L\u003d8000149@a\u003d128@$202501030703$202501030850$RE 26708$$1$$$$$$§T$A\u003d1@O\u003dHamm(Westf)Hbf@X\u003d7807824@Y\u003d51678077@L\u003d8000149@a\u003d128@$A\u003d1@O\u003dMünster(Westf)Hbf@X\u003d7635716@Y\u003d51956563@L\u003d8000263@a\u003d128@$202501030859$202501030922$RE 32916$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzQ1MzE2I0hJTiMwI0VDSyM1NjQ0ODB8NTY0NDgwfDU2NDk1NHw1NjUwNDJ8MHwwfDU2NXw1NjQ0MTN8NHwwfDh8MHwwfC0yMTQ3NDgzNjQ4I0dBTSMzMDEyNTAwMDAjClojVk4jMSNTVCMxNzM0NzIyMzk4I1BJIzEjWkkjMTk1OTUyI1RBIzAjREEjMzAxMjUjMVMjODAwMDI2MSMxVCMwI0xTIzgwMDAxOTkjTFQjMTEyMyNQVSM4MSNSVCMxI0NBI0lDRSNaRSM2MTgjWkIjSUNFICA2MTgjUEMjMCNGUiM4MDAwMjYxI0ZUIzAjVE8jODAwMDEwNSNUVCM0MzgjClojVk4jMSNTVCMxNzM0NzIyMzk4I1BJIzEjWkkjMTkyODc1I1RBIzAjREEjMzAxMjUjMVMjODAwMDEwNSMxVCM1MTIjTFMjODAwMDE5OSNMVCMxMDE4I1BVIzgxI1JUIzEjQ0EjSUNFI1pFIzEwODgjWkIjSUNFIDEwODgjUEMjMCNGUiM4MDAwMTA1I0ZUIzUxMiNUTyM4MDAzMjAwI1RUIzY0NCMKWiNWTiMxI1NUIzE3MzQ3MjIzOTgjUEkjMSNaSSMyMTg2NjIjVEEjMCNEQSMzMDEyNSMxUyM4MDAzMjAwIzFUIzcwMyNMUyM4MDAwMTQ5I0xUIzg1MCNQVSM4MSNSVCMxI0NBI0RQTiNaRSMyNjcwOCNaQiNSRSAyNjcwOCNQQyMzI0ZSIzgwMDMyMDAjRlQjNzAzI1RPIzgwMDAxNDkjVFQjODUwIwpaI1ZOIzEjU1QjMTczNDcyMjM5OCNQSSMxI1pJIzIxNzk5NiNUQSMwI0RBIzMwMTI1IzFTIzgwMDAxNzEjMVQjODQzI0xTIzgwMDAzMTYjTFQjOTUxI1BVIzgxI1JUIzEjQ0EjRFBOI1pFIzMyOTE2I1pCI1JFIDMyOTE2I1BDIzMjRlIjODAwMDE0OSNGVCM4NTkjVE8jODAwMDI2MyNUVCM5MjIj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P3UrDMACFX0VypVBHfpr+QSB2ZahMV8T5g3hR13SrdO1M0mEpfQ5fxru9mGmLFyJ6l3Nycr6TFuyFBAFAE9cDFhDv2ogonNxFE99oKd5A0IKy3s5AQK3+EIIAWqCqdZRoYcIYYgoRxGAwb/PtYGKbQGisbGg4RRZ4LZtZoeUcBE8t0M2uj8U3i8iEtlXaq4vrqRH7pKh7ZSoJ6J6HTdPNeiw25FTs5tVqrCny1CTPGOILdnX4LFcbUR6dv2T8gSFEqUeIzx+Z7SEbYuzzJfMQnzMPQogdxEPzbseQS2wXY+J7PGfLwweEHsTQeNyMUXr85GxYkkj5J1ppIY/vhdLZyTjAdQh1kWP4FPnUoQ75ySf/8RFE6Dd/LXRcFU2Rl8bTshaDdVnVshRNWNVlqkCQJYUaL+JEqSJX+jsrVlWcyGRrQm3XdV8RCJMF+wEAAA\u003d\u003d"}}
|
17
docs/dumps/PCAPdroid_02_Jan_22_47_34_tickets.txt
Normal file
17
docs/dumps/PCAPdroid_02_Jan_22_47_34_tickets.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_16_Dec_19_30_43_locsearch.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_30_43_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: 4f2e274e-6e5c-4711-9125-87229046bba3_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 560187ed-f13a-4654-bc62-b9f69f989563
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 45
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"locationTypes":["ALL"],"searchTerm":"test"}
|
17
docs/dumps/PCAPdroid_16_Dec_19_31_06_locsearch.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_31_06_locsearch.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Mon, 16 Dec 2024 18:29:33 GMT
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 2994
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=a0c42ccdea3bbb18
|
||||
Server-Timing: intid;desc=a0c42ccdea3bbb18
|
||||
Server-Timing: intid;desc=a0c42ccdea3bbb18
|
||||
x-correlation-id: 4f2e274e-6e5c-4711-9125-87229046bba3_64466773-556f-4aa8-b128-0948c4d60887
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=a0c42ccdea3bbb18
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd1e15efa531633ee310f49108472c2b1a482b002a4d22b5cf935d3d6970510c0b134616b579c639af340fe261746662484; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
[{"name":"Tessin West","stationId":"7983","locationId":"A=1@O=Tessin West@X=12442572@Y=54034438@U=81@L=8079604@B=1@p=1734031727@i=U×008030295@","evaNr":"8079604","coordinates":{"latitude":54.0344,"longitude":12.442203},"weight":1489,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Testa Grigia","locationId":"A=1@O=Testa Grigia@X=7707540@Y=45934474@U=81@L=8511303@B=1@p=1734031727@i=U×008511303@","evaNr":"8511303","coordinates":{"latitude":45.934475,"longitude":7.70754},"weight":3825,"products":["NAHVERKEHRSONSTIGEZUEGE"],"locationType":"ST"},{"name":"Teschenhagen","stationId":"6172","locationId":"A=1@O=Teschenhagen@X=13374232@Y=54389368@U=81@L=8013104@B=1@p=1734031727@i=U×008028497@","evaNr":"8013104","coordinates":{"latitude":54.38936,"longitude":13.374196},"weight":3825,"products":["NAHVERKEHRSONSTIGEZUEGE"],"locationType":"ST"},{"name":"Testelt","locationId":"A=1@O=Testelt@X=4946863@Y=51009783@U=81@L=8800244@B=1@p=1734031727@i=U×008833266@","evaNr":"8800244","coordinates":{"latitude":51.009785,"longitude":4.946863},"weight":2627,"products":["INTERCITYUNDEUROCITYZUEGE","NAHVERKEHRSONSTIGEZUEGE"],"locationType":"ST"},{"name":"Tessin","stationId":"6174","locationId":"A=1@O=Tessin@X=12462618@Y=54032020@U=81@L=8013106@B=1@p=1734031727@i=U×008027109@","evaNr":"8013106","coordinates":{"latitude":54.032,"longitude":12.461656},"weight":2402,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE"],"locationType":"ST"},{"name":"Chemnitz Zentralhaltestelle","locationId":"A=1@O=Chemnitz Zentralhaltestelle@X=12922263@Y=50831626@U=81@L=8017419@B=1@p=1734031727@i=U×008042918@","evaNr":"8017419","coordinates":{"latitude":50.831627,"longitude":12.922263},"weight":5813,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Teschow","stationId":"6173","locationId":"A=1@O=Teschow@X=11637956@Y=53994355@U=81@L=8013105@B=1@p=1734031727@i=U×008027118@","evaNr":"8013105","coordinates":{"latitude":53.994175,"longitude":11.637687},"weight":1524,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE"],"locationType":"ST"},{"name":"Tesperhude Hudehof, Geesthacht","locationId":"A=1@O=Tesperhude Hudehof, Geesthacht@X=10431253@Y=53404393@U=81@L=694334@B=1@p=1734031727@","evaNr":"694334","coordinates":{"latitude":53.404392,"longitude":10.431253},"weight":912,"products":["BUSSE"],"locationType":"ST"},{"name":"Tesperhude Strandweg, Geesthacht","locationId":"A=1@O=Tesperhude Strandweg, Geesthacht@X=10427424@Y=53402316@U=81@L=694333@B=1@p=1734031727@","evaNr":"694333","coordinates":{"latitude":53.402317,"longitude":10.427424},"weight":912,"products":["BUSSE"],"locationType":"ST"},{"name":"Testorf Umspannwerk, Wangels","locationId":"A=1@O=Testorf Umspannwerk, Wangels@X=10778516@Y=54250161@U=81@L=700240@B=1@p=1734031727@","evaNr":"700240","coordinates":{"latitude":54.25016,"longitude":10.778516},"weight":220,"products":["BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"}]
|
18
docs/dumps/PCAPdroid_16_Dec_19_33_04_routesearch.txt
Normal file
18
docs/dumps/PCAPdroid_16_Dec_19_33_04_routesearch.txt
Normal file
|
@ -0,0 +1,18 @@
|
|||
POST /mob/angebote/fahrplan HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
x-feature-reiseketten-enabled: false
|
||||
X-Correlation-ID: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 0d0c6c50-3bea-4cb7-b008-09bc92a0d96a
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 679
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8011160@B\u003d1@p\u003d1734031727@i\u003dU×008065969@","verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2024-12-16T19:28:48.659812+01:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@U\u003d81@L\u003d8000207@B\u003d1@p\u003d1734031727@i\u003dU×008015458@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}
|
17
docs/dumps/PCAPdroid_16_Dec_19_33_08_routesearch.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_33_08_routesearch.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_16_Dec_19_35_50_triprecon.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_35_50_triprecon.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
POST /mob/trip/recon HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
X-Correlation-ID: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 757b9806-028b-4b0e-bdde-2441c2e1e1ee
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 1162
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"reconCtx":"¶HKI¶T$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8011160@a\u003d128@$A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@L\u003d8000207@a\u003d128@$202412161946$202412170057$ICE 842$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzEjSElOIzAjRUNLIzUzOTc0Nnw1Mzk3NDZ8NTQwMDU3fDU0MDA1N3wwfDB8NTY1fDUzOTcyNXwxfDB8MTA1MHwwfDB8LTIxNDc0ODM2NDgjR0FNIzE2MTIyNDE5NDYjClojVk4jMSNTVCMxNzM0MDMxNzI3I1BJIzEjWkkjMTc3NjUxI1RBIzAjREEjMTYxMjI0IzFTIzgwMTAyNTUjMVQjMTkzNCNMUyM4MDAwMjA3I0xUIzEwMDU3I1BVIzgxI1JUIzEjQ0EjSUNFI1pFIzg0MiNaQiNJQ0UgIDg0MiNQQyMwI0ZSIzgwMTExNjAjRlQjMTk0NiNUTyM4MDAwMjA3I1RUIzEwMDU3Iw\u003d\u003d¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P7UrDMBiFb0Xyu4436WcKgdiV4cfQIk4U8Udds1lJ25mmw1J6Hd6JN7AbM20ZCIrkT87JyXnet0N7oVCI8MwPkIXEhzYijmb38YwarcQ7CjtUNsUCha41XCIUgoWqRsepFiZMgDiYYA+N5l1eDCamJAAw1mZsOMUWeivbhdRqicKnDul2N8SS25vYhIoqG9TF9dyIfSqbsQKIjfrncab563YqNuRM7JbVeqqReWaSZwzzGxYJJfPy5Pxlwx8Ytm2Pug7lj8wl5rgB5SsWYL5kAWCMPeCR+bVj2LcdsLFPfJ6z1eETIADPpR7lZpRaTysuxjlSpf4EXx2+5JFroIFvw4AF6thAfmABCPj/YLHruMFv7FbopJKt2c14WjVitC6rRpWijaqmzGoUblJZTw9JWtcyr/UxK9ZVkqq0MKGu7/tvvH4lCvABAAA\u003d"}
|
17
docs/dumps/PCAPdroid_16_Dec_19_35_55_triprecon.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_35_55_triprecon.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_16_Dec_19_36_19_triprecon.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_36_19_triprecon.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
POST /mob/trip/recon HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
X-Correlation-ID: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: f30d7ef5-d9a2-428b-acc2-e8bb68c7d705
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 2507
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"reconCtx":"¶HKI¶T$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8098160@a\u003d128@$A\u003d1@O\u003dHannover Hbf@X\u003d9741017@Y\u003d52376764@L\u003d8000152@a\u003d128@$202412162128$202412162322$ICE 840$$1$$$$$$§T$A\u003d1@O\u003dHannover Hbf@X\u003d9741017@Y\u003d52376764@L\u003d8000152@a\u003d128@$A\u003d1@O\u003dHanau Hbf@X\u003d8929003@Y\u003d50120957@L\u003d8000150@a\u003d128@$202412170030$202412170414$IC 60471$$1$$$$$$§T$A\u003d1@O\u003dHanau Hbf@X\u003d8929003@Y\u003d50120957@L\u003d8000150@a\u003d128@$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8663785@Y\u003d50107149@L\u003d8000105@a\u003d128@$202412170455$202412170515$RB 15501$$1$$$$$$§T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8663785@Y\u003d50107149@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@L\u003d8000207@a\u003d128@$202412170526$202412170633$ICE 222$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzI2MCNISU4jNDU1I0VDSyM1Mzk4NDh8NTM5ODQ4fDU0MDM5M3w1NDAzOTN8MHwwfDU2NXw1Mzk4NDh8NHwwfDE4fDB8MHwtMjE0NzQ4MzY0OCNHQU0jMTYxMjI0MjEyOCMKWiNWTiMxI1NUIzE3MzQwMzE3MjcjUEkjMSNaSSMxNzc2MTkjVEEjMCNEQSMxNjEyMjQjMVMjODA5ODE2MCMxVCMyMTI4I0xTIzgwMDAxNTIjTFQjMjMyMiNQVSM4MSNSVCMxI0NBI0lDRSNaRSM4NDAjWkIjSUNFICA4NDAjUEMjMCNGUiM4MDk4MTYwI0ZUIzIxMjgjVE8jODAwMDE1MiNUVCMyMzIyIwpaI1ZOIzEjU1QjMTczNDAzMTcyNyNQSSMxI1pJIzE3MzY1MSNUQSMwI0RBIzE2MTIyNCMxUyM4MDAyNTUzIzFUIzIxNTIjTFMjODUwMzAwMCNMVCMxMTAwNSNQVSM4MSNSVCMxI0NBI0lDI1pFIzYwNDcxI1pCI0lDIDYwNDcxI1BDIzEjRlIjODAwMDE1MiNGVCMxMDAzMCNUTyM4MDAwMTUwI1RUIzEwNDE0IwpaI1ZOIzEjU1QjMTczNDAzMTcyNyNQSSMxI1pJIzE4NzYxOCNUQSMwI0RBIzE3MTIyNCMxUyM4MDA2MTMyIzFUIzQxOCNMUyM4MDAwMTA1I0xUIzUxNSNQVSM4MSNSVCMxI0NBI1JCI1pFIzE1NTAxI1pCI1JCIDE1NTAxI1BDIzMjRlIjODAwMDE1MCNGVCM0NTUjVE8jODAwMDEwNSNUVCM1MTUjClojVk4jMSNTVCMxNzM0MDMxNzI3I1BJIzEjWkkjMjExNDMzI1RBIzAjREEjMTcxMjI0IzFTIzgwMDAxMDUjMVQjNTI2I0xTIzg0MDAwNTgjTFQjOTI5I1BVIzgxI1JUIzEjQ0EjSUNFI1pFIzIyMiNaQiNJQ0UgIDIyMiNQQyMwI0ZSIzgwMDAxMDUjRlQjNTI2I1RPIzgwMDAyMDcjVFQjNjMzIw\u003d\u003d¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P7UrDMBiFb0Xyu4436WcKgdiV4cfQIk4U8Udds1lJ25mmw1J6Hd6JN7AbM20ZCIrkT87JyXnet0N7oVCI8MwPkIXEhzYijmb38YwarcQ7CjtUNsUCha41XCIUgoWqRsepFiZMgDiYYA+N5l1eDCamJAAw1mZsOMUWeivbhdRqicKnDul2N8SS25vYhIoqG9TF9dyIfSqbsQKIjfrncab563YqNuRM7JbVeqqReWaSZwzzGxYJJfPy5Pxlwx8Ytm2Pug7lj8wl5rgB5SsWYL5kAWCMPeCR+bVj2LcdsLFPfJ6z1eETIADPpR7lZpRaTysuxjlSpf4EXx2+5JFroIFvw4AF6thAfmABCPj/YLHruMFv7FbopJKt2c14WjVitC6rRpWijaqmzGoUblJZTw9JWtcyr/UxK9ZVkqq0MKGu7/tvvH4lCvABAAA\u003d"}
|
17
docs/dumps/PCAPdroid_16_Dec_19_36_22_triprecon.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_36_22_triprecon.txt
Normal file
File diff suppressed because one or more lines are too long
15
docs/dumps/PCAPdroid_16_Dec_19_37_19_locationdetails.txt
Normal file
15
docs/dumps/PCAPdroid_16_Dec_19_37_19_locationdetails.txt
Normal file
|
@ -0,0 +1,15 @@
|
|||
GET /mob/location/details/8011160 HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
X-Correlation-ID: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: b9bd5e93-e6d2-4998-bc5a-17b72ca65ca3
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
17
docs/dumps/PCAPdroid_16_Dec_19_37_22_locationdetails.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_37_22_locationdetails.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Mon, 16 Dec 2024 18:36:50 GMT
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 1158
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=5ae5ce547c4a8b38
|
||||
Server-Timing: intid;desc=5ae5ce547c4a8b38
|
||||
Server-Timing: intid;desc=5ae5ce547c4a8b38
|
||||
x-correlation-id: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=5ae5ce547c4a8b38
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd125629ac814505605565f3e87a8081db30d56beae22efb148df982534183fd830148fa0901fa5a36116fab13d52c5d86a; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
{"haltName":"Berlin Hbf","produktGattungen":[{"produktGattung":"HOCHGESCHWINDIGKEITSZUEGE","produkte":[{"name":"ICE"},{"name":"RJ"}]},{"produktGattung":"INTERCITYUNDEUROCITYZUEGE","produkte":[{"name":"EC"},{"name":"IC"},{"name":"NJ"}]},{"produktGattung":"INTERREGIOUNDSCHNELLZUEGE","produkte":[{"name":"BUS"},{"name":"Bus"},{"name":"D"},{"name":"EN"},{"name":"ES"},{"name":"FLX"},{"name":"UEX"}]},{"produktGattung":"NAHVERKEHRSONSTIGEZUEGE","produkte":[{"name":"FEX"},{"name":"HBX"},{"name":"R"},{"name":"RB"},{"name":"RE"},{"name":"RSM"},{"name":"Bus RE3"},{"name":"Bus RE5"},{"name":"Bus RE7"},{"name":"Bus RE8"},{"name":"Bus S7"}]},{"produktGattung":"SBAHNEN","produkte":[{"name":"S 3"},{"name":"S 5"},{"name":"S 7"},{"name":"S 9"},{"name":"S 45"}]},{"produktGattung":"BUSSE","produkte":[{"name":"Bus 120"},{"name":"Bus 123"},{"name":"Bus 142"},{"name":"Bus 147"},{"name":"Bus 245"},{"name":"Bus M41"},{"name":"Bus M85"},{"name":"Bus N5"},{"name":"Bus N20"},{"name":"Bus N40"}]},{"produktGattung":"UBAHN","produkte":[{"name":"U 5"}]},{"produktGattung":"STRASSENBAHN","produkte":[{"name":"STR 12"},{"name":"STR M5"},{"name":"STR M8"},{"name":"STR M10"}]}]}
|
17
docs/dumps/PCAPdroid_16_Dec_19_37_26_departures.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_37_26_departures.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: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: b2d3fe71-a8d6-4675-bbe5-a5081368a6ab
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 197
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"anfragezeit":"21:28","datum":"2024-12-16","ursprungsBahnhofId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8098160@i\u003dU×008031922@","verkehrsmittel":["ALL"]}
|
17
docs/dumps/PCAPdroid_16_Dec_19_37_34_departures.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_37_34_departures.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_16_Dec_19_37_57_departures.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_37_57_departures.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_16_Dec_19_38_00_departures.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_38_00_departures.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: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 8c20d0c0-3c0b-40ef-bc95-849c65e375df
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 378
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"anfragezeit":"21:28","datum":"2024-12-16","ursprungsBahnhofId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8098160@i\u003dU×008031922@","verkehrsmittel":["HOCHGESCHWINDIGKEITSZUEGE","INTERCITYUNDEUROCITYZUEGE","INTERREGIOUNDSCHNELLZUEGE","NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"]}
|
17
docs/dumps/PCAPdroid_16_Dec_19_38_44_arrivals.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_38_44_arrivals.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
POST /mob/bahnhofstafel/ankunft HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
X-Correlation-ID: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: d7e7526d-1d69-41d1-bf48-9c2ed5f4a302
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 378
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"anfragezeit":"21:28","datum":"2024-12-16","ursprungsBahnhofId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8098160@i\u003dU×008031922@","verkehrsmittel":["HOCHGESCHWINDIGKEITSZUEGE","INTERCITYUNDEUROCITYZUEGE","INTERREGIOUNDSCHNELLZUEGE","NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"]}
|
17
docs/dumps/PCAPdroid_16_Dec_19_38_47_arrivals.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_38_47_arrivals.txt
Normal file
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_16_Dec_19_39_50_locsearch_station.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_39_50_locsearch_station.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: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 91946e30-da87-4813-9a13-48dce2cd2cdd
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 44
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"locationTypes":["ST"],"searchTerm":"test"}
|
17
docs/dumps/PCAPdroid_16_Dec_19_39_53_locsearch_station.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_39_53_locsearch_station.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Mon, 16 Dec 2024 18:39:38 GMT
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 2994
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=6e7cf7863b2dbf7b
|
||||
Server-Timing: intid;desc=6e7cf7863b2dbf7b
|
||||
Server-Timing: intid;desc=6e7cf7863b2dbf7b
|
||||
x-correlation-id: 0564dcdf-edbf-4412-a147-7299f6481470_64466773-556f-4aa8-b128-0948c4d60887
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=6e7cf7863b2dbf7b
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd109bdf5d864ee9ff6d510d0c0e6688e510fd271d9dba08fe92a99ec7d68b43cb8eddd472ec4074727d7ed3137ea220214; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
[{"name":"Tessin West","stationId":"7983","locationId":"A=1@O=Tessin West@X=12442572@Y=54034438@U=81@L=8079604@B=1@p=1734031727@i=U×008030295@","evaNr":"8079604","coordinates":{"latitude":54.0344,"longitude":12.442203},"weight":1489,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Testa Grigia","locationId":"A=1@O=Testa Grigia@X=7707540@Y=45934474@U=81@L=8511303@B=1@p=1734031727@i=U×008511303@","evaNr":"8511303","coordinates":{"latitude":45.934475,"longitude":7.70754},"weight":3825,"products":["NAHVERKEHRSONSTIGEZUEGE"],"locationType":"ST"},{"name":"Teschenhagen","stationId":"6172","locationId":"A=1@O=Teschenhagen@X=13374232@Y=54389368@U=81@L=8013104@B=1@p=1734031727@i=U×008028497@","evaNr":"8013104","coordinates":{"latitude":54.38936,"longitude":13.374196},"weight":3825,"products":["NAHVERKEHRSONSTIGEZUEGE"],"locationType":"ST"},{"name":"Testelt","locationId":"A=1@O=Testelt@X=4946863@Y=51009783@U=81@L=8800244@B=1@p=1734031727@i=U×008833266@","evaNr":"8800244","coordinates":{"latitude":51.009785,"longitude":4.946863},"weight":2627,"products":["INTERCITYUNDEUROCITYZUEGE","NAHVERKEHRSONSTIGEZUEGE"],"locationType":"ST"},{"name":"Tessin","stationId":"6174","locationId":"A=1@O=Tessin@X=12462618@Y=54032020@U=81@L=8013106@B=1@p=1734031727@i=U×008027109@","evaNr":"8013106","coordinates":{"latitude":54.032,"longitude":12.461656},"weight":2402,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE"],"locationType":"ST"},{"name":"Chemnitz Zentralhaltestelle","locationId":"A=1@O=Chemnitz Zentralhaltestelle@X=12922263@Y=50831626@U=81@L=8017419@B=1@p=1734031727@i=U×008042918@","evaNr":"8017419","coordinates":{"latitude":50.831627,"longitude":12.922263},"weight":5813,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Teschow","stationId":"6173","locationId":"A=1@O=Teschow@X=11637956@Y=53994355@U=81@L=8013105@B=1@p=1734031727@i=U×008027118@","evaNr":"8013105","coordinates":{"latitude":53.994175,"longitude":11.637687},"weight":1524,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE"],"locationType":"ST"},{"name":"Tesperhude Hudehof, Geesthacht","locationId":"A=1@O=Tesperhude Hudehof, Geesthacht@X=10431253@Y=53404393@U=81@L=694334@B=1@p=1734031727@","evaNr":"694334","coordinates":{"latitude":53.404392,"longitude":10.431253},"weight":912,"products":["BUSSE"],"locationType":"ST"},{"name":"Tesperhude Strandweg, Geesthacht","locationId":"A=1@O=Tesperhude Strandweg, Geesthacht@X=10427424@Y=53402316@U=81@L=694333@B=1@p=1734031727@","evaNr":"694333","coordinates":{"latitude":53.402317,"longitude":10.427424},"weight":912,"products":["BUSSE"],"locationType":"ST"},{"name":"Testorf Umspannwerk, Wangels","locationId":"A=1@O=Testorf Umspannwerk, Wangels@X=10778516@Y=54250161@U=81@L=700240@B=1@p=1734031727@","evaNr":"700240","coordinates":{"latitude":54.25016,"longitude":10.778516},"weight":220,"products":["BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"}]
|
18
docs/dumps/PCAPdroid_16_Dec_19_42_53_specialrouting.txt
Normal file
18
docs/dumps/PCAPdroid_16_Dec_19_42_53_specialrouting.txt
Normal file
|
@ -0,0 +1,18 @@
|
|||
POST /mob/angebote/fahrplan HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
x-feature-reiseketten-enabled: false
|
||||
X-Correlation-ID: 67b8a500-1983-49f5-a4ff-177d58b395ed_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: be862f65-99be-46af-a622-8bc332dc70df
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 1122
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8011160@B\u003d1@p\u003d1734031727@i\u003dU×008065969@","economic":true,"minUmstiegsdauer":20,"verkehrsmittel":["NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"],"viaLocations":[{"locationId":"A\u003d1@O\u003dHannover Hbf@X\u003d9741017@Y\u003d52376764@U\u003d81@L\u003d8000152@B\u003d1@p\u003d1734031727@i\u003dU×008013552@","minUmstiegsdauer":60,"verkehrsmittel":["NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"]}],"zeitWunsch":{"reiseDatum":"2024-12-16T19:28:48.659+01:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@U\u003d81@L\u003d8000207@B\u003d1@p\u003d1734031727@i\u003dU×008015458@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}
|
17
docs/dumps/PCAPdroid_16_Dec_19_43_07_specialrouting.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_43_07_specialrouting.txt
Normal file
File diff suppressed because one or more lines are too long
18
docs/dumps/PCAPdroid_16_Dec_19_46_29_bahncard.txt
Normal file
18
docs/dumps/PCAPdroid_16_Dec_19_46_29_bahncard.txt
Normal file
|
@ -0,0 +1,18 @@
|
|||
POST /mob/angebote/fahrplan HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
x-feature-reiseketten-enabled: false
|
||||
X-Correlation-ID: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: c752e01a-e03e-42a5-bcd3-26b1cc2574b3
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 783
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@U\u003d81@L\u003d8000207@B\u003d1@p\u003d1734031727@i\u003dU×008015458@","minUmstiegsdauer":20,"verkehrsmittel":["NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"],"zeitWunsch":{"reiseDatum":"2024-12-16T19:45:47.459239+01:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8011160@B\u003d1@p\u003d1734031727@i\u003dU×008065969@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["BAHNCARD25 KLASSE_2"],"reisendenTyp":"SENIOR"}]},"reservierungsKontingenteVorhanden":false}
|
17
docs/dumps/PCAPdroid_16_Dec_19_46_32_bahncard.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_46_32_bahncard.txt
Normal file
File diff suppressed because one or more lines are too long
15
docs/dumps/PCAPdroid_16_Dec_19_47_54_zuglauf.txt
Normal file
15
docs/dumps/PCAPdroid_16_Dec_19_47_54_zuglauf.txt
Normal file
|
@ -0,0 +1,15 @@
|
|||
GET /mob/zuglauf/2%7C%23VN%231%23ST%231734031727%23PI%231%23ZI%23178229%23TA%230%23DA%23161224%231S%238000207%231T%231926%23LS%238098160%23LT%2310012%23PU%2381%23RT%231%23CA%23ICE%23ZE%23947%23ZB%23ICE%20%20947%23PC%230%23FR%238000207%23FT%231926%23TO%238098160%23TT%2310012%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: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 57c6969b-e937-4591-accf-0850e7b72278
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
17
docs/dumps/PCAPdroid_16_Dec_19_47_58_zuglauf.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_47_58_zuglauf.txt
Normal file
File diff suppressed because one or more lines are too long
15
docs/dumps/PCAPdroid_16_Dec_19_48_15_coachseq.txt
Normal file
15
docs/dumps/PCAPdroid_16_Dec_19_48_15_coachseq.txt
Normal file
|
@ -0,0 +1,15 @@
|
|||
GET /mob/zuglaeufe/ICE_947/halte/by-abfahrt/8000207_2024-12-16T19:26:00+01:00/wagenreihung HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.wagenreihung.v3+json
|
||||
Content-Type: application/x.db.vendo.mob.wagenreihung.v3+json
|
||||
X-Correlation-ID: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 4ce19a5d-9da2-480d-a278-543fade2f485
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
15
docs/dumps/PCAPdroid_16_Dec_19_48_25_coachseq.txt
Normal file
15
docs/dumps/PCAPdroid_16_Dec_19_48_25_coachseq.txt
Normal file
|
@ -0,0 +1,15 @@
|
|||
GET /mob/zuglaeufe/ICE_947/halte/by-abfahrt/8000207_2024-12-16T19:26:00+01:00/wagenreihung HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.wagenreihung.v3+json
|
||||
Content-Type: application/x.db.vendo.mob.wagenreihung.v3+json
|
||||
X-Correlation-ID: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 4ce19a5d-9da2-480d-a278-543fade2f485
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
17
docs/dumps/PCAPdroid_16_Dec_19_49_08_coachseq.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_19_49_08_coachseq.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Mon, 16 Dec 2024 18:48:06 GMT
|
||||
Content-Type: application/x.db.vendo.mob.wagenreihung.v3+json
|
||||
Content-Length: 4538
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=8f64b8dc41cc78fe
|
||||
Server-Timing: intid;desc=8f64b8dc41cc78fe
|
||||
Server-Timing: intid;desc=8f64b8dc41cc78fe
|
||||
x-correlation-id: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=8f64b8dc41cc78fe
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd1b784b8e477d277c1a5b018395e42eab8d1fee3cb97bf2b9ec80a7dd3763f495c187c41e167ab4fcf87709c96c07cb192; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
{"status":"MATCHES_SCHEDULE","gleis":{"start":{"position":0},"ende":{"position":487},"sektoren":[{"bezeichnung":"G","start":{"position":0},"ende":{"position":96.75},"gleisabschnittswuerfelPosition":58,"ersteKlasse":false},{"bezeichnung":"F","start":{"position":96.75},"ende":{"position":161.75},"gleisabschnittswuerfelPosition":135.5,"ersteKlasse":false},{"bezeichnung":"E","start":{"position":161.75},"ende":{"position":216.4},"gleisabschnittswuerfelPosition":188,"ersteKlasse":false},{"bezeichnung":"D","start":{"position":216.4},"ende":{"position":275.525},"gleisabschnittswuerfelPosition":244.8,"ersteKlasse":false},{"bezeichnung":"C","start":{"position":275.525},"ende":{"position":327.4},"gleisabschnittswuerfelPosition":306.25,"ersteKlasse":false},{"bezeichnung":"B","start":{"position":327.4},"ende":{"position":371.5},"gleisabschnittswuerfelPosition":348.55,"ersteKlasse":false},{"bezeichnung":"A","start":{"position":371.5},"ende":{"position":487},"gleisabschnittswuerfelPosition":394.45,"ersteKlasse":true}],"bezeichnung":"5"},"fahrzeuggruppen":[{"fahrzeuge":[{"fahrzeugtyp":{"fahrzeugkategorie":"POWERCAR","baureihe":"I4020","ersteKlasse":false,"zweiteKlasse":false},"status":"OPEN","orientierung":"FORWARDS","positionAmGleis":{"start":{"position":427.44},"ende":{"position":448},"sektor":"A"},"ausstattungsmerkmale":[]},{"fahrzeugtyp":{"fahrzeugkategorie":"PASSENGERCARRIAGE_FIRST_CLASS","baureihe":"Apmz","ersteKlasse":true,"zweiteKlasse":false},"status":"OPEN","orientierung":"BACKWARDS","positionAmGleis":{"start":{"position":401.04},"ende":{"position":427.44},"sektor":"A"},"ausstattungsmerkmale":[{"art":"SEATS_SEVERELY_DISABLED","status":"UNDEFINED"},{"art":"AIR_CONDITION","status":"UNDEFINED"},{"art":"ZONE_QUIET","status":"UNDEFINED"}],"ordnungsnummer":27},{"fahrzeugtyp":{"fahrzeugkategorie":"PASSENGERCARRIAGE_FIRST_CLASS","baureihe":"Apmz","ersteKlasse":true,"zweiteKlasse":false},"status":"OPEN","orientierung":"FORWARDS","positionAmGleis":{"start":{"position":374.64},"ende":{"position":401.04},"sektor":"A"},"ausstattungsmerkmale":[{"art":"SEATS_BAHN_COMFORT","status":"UNDEFINED"},{"art":"AIR_CONDITION","status":"UNDEFINED"}],"ordnungsnummer":26},{"fahrzeugtyp":{"fahrzeugkategorie":"DININGCAR","baureihe":"WRmbsz","ersteKlasse":false,"zweiteKlasse":false},"status":"OPEN","orientierung":"BACKWARDS","positionAmGleis":{"start":{"position":348.24},"ende":{"position":374.64},"sektor":"B"},"ausstattungsmerkmale":[{"art":"INFO","status":"UNDEFINED"},{"art":"TOILET_WHEELCHAIR","status":"UNDEFINED"},{"art":"WHEELCHAIR_SPACE","status":"AVAILABLE"}],"ordnungsnummer":25},{"fahrzeugtyp":{"fahrzeugkategorie":"PASSENGERCARRIAGE_ECONOMY_CLASS","baureihe":"Bpmbz","ersteKlasse":false,"zweiteKlasse":true},"status":"OPEN","orientierung":"BACKWARDS","positionAmGleis":{"start":{"position":321.84},"ende":{"position":348.24},"sektor":"B"},"ausstattungsmerkmale":[{"art":"SEATS_SEVERELY_DISABLED","status":"UNDEFINED"},{"art":"ZONE_FAMILY","status":"UNDEFINED"},{"art":"CABIN_INFANT","status":"UNDEFINED"},{"art":"AIR_CONDITION","status":"UNDEFINED"},{"art":"TOILET_WHEELCHAIR","status":"UNDEFINED"},{"art":"WHEELCHAIR_SPACE","status":"AVAILABLE"}],"ordnungsnummer":24},{"fahrzeugtyp":{"fahrzeugkategorie":"PASSENGERCARRIAGE_ECONOMY_CLASS","baureihe":"Bpmz","ersteKlasse":false,"zweiteKlasse":true},"status":"OPEN","orientierung":"BACKWARDS","positionAmGleis":{"start":{"position":295.44},"ende":{"position":321.84},"sektor":"C"},"ausstattungsmerkmale":[{"art":"SEATS_BAHN_COMFORT","status":"UNDEFINED"},{"art":"AIR_CONDITION","status":"UNDEFINED"}],"ordnungsnummer":23},{"fahrzeugtyp":{"fahrzeugkategorie":"PASSENGERCARRIAGE_ECONOMY_CLASS","baureihe":"Bpmz","ersteKlasse":false,"zweiteKlasse":true},"status":"OPEN","orientierung":"BACKWARDS","positionAmGleis":{"start":{"position":269.04},"ende":{"position":295.44},"sektor":"C"},"ausstattungsmerkmale":[{"art":"AIR_CONDITION","status":"UNDEFINED"},{"art":"ZONE_QUIET","status":"UNDEFINED"}],"ordnungsnummer":22},{"fahrzeugtyp":{"fahrzeugkategorie":"CONTROLCAR_ECONOMY_CLASS","baureihe":"Bpmzf","ersteKlasse":false,"zweiteKlasse":true},"status":"OPEN","orientierung":"FORWARDS","positionAmGleis":{"start":{"position":242.64},"ende":{"position":269.04},"sektor":"D"},"ausstattungsmerkmale":[{"art":"AIR_CONDITION","status":"UNDEFINED"}],"ordnungsnummer":21}],"fahrtreferenz":{"typ":"HIGH_SPEED_TRAIN","linie":"","ziel":{"bezeichnung":"Berlin Hbf"},"gattung":"ICE","fahrtnummer":947},"bezeichnung":"ICE0225"}],"fahrtrichtung":"RECHTS","gleisSoll":"4","gleisVorschau":"5"}
|
17
docs/dumps/PCAPdroid_16_Dec_20_18_17_reservierung.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_20_18_17_reservierung.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
POST /mob/angebote/recon/autonomereservierung HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
X-Correlation-ID: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: c88c72b9-3515-4acf-b052-3e71578c9461
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 2276
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"ermaessigungen":["BAHNCARD25 KLASSE_2"],"reisendenTyp":"SENIOR"}]},"reservierungsKontingenteVorhanden":false,"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@L\u003d8000207@a\u003d128@$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8663785@Y\u003d50107149@L\u003d8000105@a\u003d128@$202412162342$202412170220$IC 60403$$1$$$$$$§T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8663785@Y\u003d50107149@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dErfurt Hbf@X\u003d11037989@Y\u003d50972352@L\u003d8010101@a\u003d128@$202412170249$202412170518$ICE 698$$1$$$$$$§T$A\u003d1@O\u003dErfurt Hbf@X\u003d11037989@Y\u003d50972352@L\u003d8010101@a\u003d128@$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8098160@a\u003d128@$202412170527$202412170729$ICE 1606$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzQ1MzE1I0hJTiMwI0VDSyM1Mzk5OTV8NTM5OTgyfDU0MDQ0OXw1NDA0NDl8MHwwfDU2NXw1Mzk5NTN8M3wwfDh8MHwwfC0yMTQ3NDgzNjQ4I0dBTSMxNjEyMjQyMzQyIwpaI1ZOIzEjU1QjMTczNDAzMTcyNyNQSSMxI1pJIzE3MzU2MyNUQSMwI0RBIzE2MTIyNCMxUyM4NDAwMDU4IzFUIzIwMTUjTFMjODUwMzAwMCNMVCMxMDgwNSNQVSM4MSNSVCMxI0NBI0lDI1pFIzYwNDAzI1pCI0lDIDYwNDAzI1BDIzEjRlIjODAwMDIwNyNGVCMyMzQyI1RPIzgwMDAxMDUjVFQjMTAyMjAjClojVk4jMSNTVCMxNzM0MDMxNzI3I1BJIzEjWkkjMTc3MTQ3I1RBIzAjREEjMTYxMjI0IzFTIzgwMDAyNjEjMVQjMjE1MSNMUyM4MDk4MTYwI0xUIzEwNzU1I1BVIzgxI1JUIzEjQ0EjSUNFI1pFIzY5OCNaQiNJQ0UgIDY5OCNQQyMwI0ZSIzgwMDAxMDUjRlQjMTAyNDkjVE8jODAxMDEwMSNUVCMxMDUxOCMKWiNWTiMxI1NUIzE3MzQwMzE3MjcjUEkjMSNaSSMxNzQ1MzgjVEEjMCNEQSMxNzEyMjQjMVMjODAxMDEwMSMxVCM1MjcjTFMjODAwMjU1MyNMVCM5MzkjUFUjODEjUlQjMSNDQSNJQ0UjWkUjMTYwNiNaQiNJQ0UgMTYwNiNQQyMwI0ZSIzgwMTAxMDEjRlQjNTI3I1RPIzgwOTgxNjAjVFQjNzI5Iw\u003d\u003d¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P7UrDMBiFb0Xyu443adM2hUDsyvBjaBEnivijrtms9GOm6bCUXod34g3sxkxbBoIi+ZNz3pPzvOnQXioUIDzzfGQh+aGNiMLZfTRjRiv5joIOlU2xQAG1hkuIArBQ1ego0dKECRAHE+yi0bzLisHEzKEAxtqMDafYQm9lu8i1WqLgqUO63Q2x+PYmMqGiSgd1cT03Yp/kzVgBxEb987jT/HU7FRtyKnfLaj3V5Flqkmccixt+dfjKy5Pzl4144C6jvmeDeOQUmGMDYWLFfSyW3AcAAp4IzZsdx57tgI094omMrw6fAD5g6lBfmEVqPX1wMW6RKPUnNpQqz45cbNsG7bABTMyh/g8wxtiFf8AuZS77Dd5KHVd5ayDG06qRo3VZNaqUbVg1ZVqjYJPk9TSIk7rOs1ofs3JdxYlKChPq+r7/Bvf6c1bwAQAA"}}
|
17
docs/dumps/PCAPdroid_16_Dec_20_18_20_reservierung.txt
Normal file
17
docs/dumps/PCAPdroid_16_Dec_20_18_20_reservierung.txt
Normal file
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,17 @@
|
|||
POST /mob/bahnhofstafel/abfahrt HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
X-Correlation-ID: b8d93b08-71ac-4cc2-836e-cd2683e34478_64466773-556f-4aa8-b128-0948c4d60887
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 2b250673-6250-4bcd-8dc3-8cf83c0a7686
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 376
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"anfragezeit":"20:35","datum":"2024-12-16","ursprungsBahnhofId":"A\u003d1@O\u003dKöln Hbf@X\u003d6958730@Y\u003d50943029@U\u003d81@L\u003d8000207@i\u003dU×008015458@","verkehrsmittel":["HOCHGESCHWINDIGKEITSZUEGE","INTERCITYUNDEUROCITYZUEGE","INTERREGIOUNDSCHNELLZUEGE","NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"]}
|
File diff suppressed because one or more lines are too long
17
docs/dumps/PCAPdroid_18_Dec_22_39_50_departures_ondemand.txt
Normal file
17
docs/dumps/PCAPdroid_18_Dec_22_39_50_departures_ondemand.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Wed, 18 Dec 2024 21:38:58 GMT
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 590
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=bf069d97495f8dad
|
||||
Server-Timing: intid;desc=bf069d97495f8dad
|
||||
Server-Timing: intid;desc=bf069d97495f8dad
|
||||
x-correlation-id: faddeefb-53f2-41a9-9879-e1c4edd7845f_5a24d491-8e2e-4cd8-85b5-ddd1d7d322c8
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=bf069d97495f8dad
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd1e76575d2a4e5c74d05675d588d2a35d6e7b8b0e5cad991641ebd4a05f4c1b051774f52d1a60eac0d3365250488a0be85; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
{"bahnhofstafelAbfahrtPositionen":[{"zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#514236#TA#0#DA#181224#1S#541958#1T#1944#LS#548569#LT#2108#PU#81#RT#1#CA#rfb#ZE#8175#ZB#RUF 8175#PC#9#FR#541958#FT#1944#TO#548569#TT#2108#","kurztext":"RUF","mitteltext":"RUF 8175","abfrageOrt":{"name":"Penzing Ortsmitte, Aidenbach","locationId":"A=1@O=Penzing Ortsmitte, Aidenbach@X=13063430@Y=48562182@U=81@L=548572@","evaNr":"548572"},"richtung":"Köching Abzw. Haidenburg Wartehäuschen, Aidenbach","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T21:03:00+01:00","produktGattung":"ANRUFPFLICHTIGEVERKEHRE"}]}
|
17
docs/dumps/PCAPdroid_18_Dec_22_42_29_departures_ferry.txt
Normal file
17
docs/dumps/PCAPdroid_18_Dec_22_42_29_departures_ferry.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Wed, 18 Dec 2024 21:42:04 GMT
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 2498
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=4e6f4ce0b26b14cb
|
||||
Server-Timing: intid;desc=4e6f4ce0b26b14cb
|
||||
Server-Timing: intid;desc=4e6f4ce0b26b14cb
|
||||
x-correlation-id: faddeefb-53f2-41a9-9879-e1c4edd7845f_5a24d491-8e2e-4cd8-85b5-ddd1d7d322c8
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=4e6f4ce0b26b14cb
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd1b5c4411e233c8956b40fa37e9050070c341e1782b80282d570eb1ed0cb63a2fab4db66435d0bad351471a04f949cac0d; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
{"bahnhofstafelAbfahrtPositionen":[{"zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#989203#TA#17#DA#181224#1S#368702#1T#930#LS#936350#LT#935#PU#81#RT#1#CA#FAE#ZE#FÄ1#ZB#Fähre #PC#6#FR#368702#FT#930#TO#936350#TT#935#","kurztext":"Fähre","mitteltext":"Fähre","abfrageOrt":{"name":"Hohe Düne Fähre, Rostock","locationId":"A=1@O=Hohe Düne Fähre, Rostock@X=12097404@Y=54176710@U=81@L=368702@","evaNr":"368702"},"richtung":"Warnemünde","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T09:30:00+01:00","produktGattung":"SCHIFF"},{"zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#417851#TA#1#DA#181224#1S#936250#1T#947#LS#939255#LT#1021#PU#81#RT#1#CA#Bus#ZE#18#ZB#Bus 18#PC#5#FR#936250#FT#947#TO#939255#TT#1021#","kurztext":"Bus","mitteltext":"Bus 18","abfrageOrt":{"name":"Hohe Düne Fähre, Rostock","locationId":"A=1@O=Hohe Düne Fähre, Rostock@X=12098627@Y=54176485@U=81@L=936250@","evaNr":"936250"},"richtung":"Dierkower Kreuz","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T09:47:00+01:00","produktGattung":"BUS"},{"zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#989203#TA#18#DA#181224#1S#368702#1T#950#LS#936350#LT#955#PU#81#RT#1#CA#FAE#ZE#FÄ1#ZB#Fähre #PC#6#FR#368702#FT#950#TO#936350#TT#955#","kurztext":"Fähre","mitteltext":"Fähre","abfrageOrt":{"name":"Hohe Düne Fähre, Rostock","locationId":"A=1@O=Hohe Düne Fähre, Rostock@X=12097404@Y=54176710@U=81@L=368702@","evaNr":"368702"},"richtung":"Warnemünde","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T09:50:00+01:00","produktGattung":"SCHIFF"},{"zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#420342#TA#0#DA#181224#1S#936250#1T#1007#LS#842059#LT#1018#PU#81#RT#1#CA#Bus#ZE#17#ZB#Bus 17#PC#5#FR#936250#FT#1007#TO#842059#TT#1018#","kurztext":"Bus","mitteltext":"Bus 17","abfrageOrt":{"name":"Hohe Düne Fähre, Rostock","locationId":"A=1@O=Hohe Düne Fähre, Rostock@X=12098627@Y=54176485@U=81@L=936250@","evaNr":"936250"},"richtung":"Markgrafenheide Ost","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T10:07:00+01:00","produktGattung":"BUS"},{"zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#989203#TA#19#DA#181224#1S#368702#1T#1010#LS#936350#LT#1015#PU#81#RT#1#CA#FAE#ZE#FÄ1#ZB#Fähre #PC#6#FR#368702#FT#1010#TO#936350#TT#1015#","kurztext":"Fähre","mitteltext":"Fähre","abfrageOrt":{"name":"Hohe Düne Fähre, Rostock","locationId":"A=1@O=Hohe Düne Fähre, Rostock@X=12097404@Y=54176710@U=81@L=368702@","evaNr":"368702"},"richtung":"Warnemünde","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T10:10:00+01:00","produktGattung":"SCHIFF"}]}
|
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Wed, 18 Dec 2024 21:43:02 GMT
|
||||
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
|
||||
Content-Length: 1030
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=3007cc29ce998300
|
||||
Server-Timing: intid;desc=3007cc29ce998300
|
||||
Server-Timing: intid;desc=3007cc29ce998300
|
||||
x-correlation-id: faddeefb-53f2-41a9-9879-e1c4edd7845f_5a24d491-8e2e-4cd8-85b5-ddd1d7d322c8
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=3007cc29ce998300
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd1778d8a6954cb4376d998b52e8b4b9edfa2de0eed859c823593d6a880ffc23b5ab8f107642d7ccfb36b5be2d8c9d6632f; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
{"bahnhofstafelAbfahrtPositionen":[{"gleis":"2","zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#338816#TA#0#DA#181224#1S#8000096#1T#712#LS#8100003#LT#1352#PU#81#RT#1#CA#DPF#ZE#963#ZB#WB 963#PC#2#FR#8000096#FT#712#TO#8100003#TT#1352#","kurztext":"WB","mitteltext":"WB 963","abfrageOrt":{"name":"Stuttgart Hbf","locationId":"A=1@O=Stuttgart Hbf@X=9181636@Y=48784081@U=81@L=8000096@i=U×008029034@","evaNr":"8000096","stationId":"6071"},"echtzeitNotizen":[],"abgangsDatum":"2024-12-18T07:12:00+01:00","produktGattung":"IR"},{"gleis":"8","zuglaufId":"2|#VN#1#ST#1734378327#PI#1#ZI#203510#TA#0#DA#181224#1S#8000096#1T#717#LS#8098160#LT#1315#PU#81#RT#1#CA#DPF#ZE#1240#ZB#FLX 1240#PC#2#FR#8000096#FT#717#TO#8098160#TT#1315#","kurztext":"FLX","mitteltext":"FLX 1240","abfrageOrt":{"name":"Stuttgart Hbf","locationId":"A=1@O=Stuttgart Hbf@X=9181636@Y=48784081@U=81@L=8000096@i=U×008029034@","evaNr":"8000096","stationId":"6071"},"richtung":"Berlin Hbf","echtzeitNotizen":[],"abgangsDatum":"2024-12-18T07:17:00+01:00","produktGattung":"IR"}]}
|
17
docs/dumps/PCAPdroid_18_Dec_23_19_54_locsearch_addr.txt
Normal file
17
docs/dumps/PCAPdroid_18_Dec_23_19_54_locsearch_addr.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: 0079a47f-6208-4507-9d9a-0e02802ef52c_5a24d491-8e2e-4cd8-85b5-ddd1d7d322c8
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: 205f5fd7-f96a-4e07-a47d-bb3edb8c443e
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 52
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"locationTypes":["ALL"],"searchTerm":"schillerstr"}
|
17
docs/dumps/PCAPdroid_18_Dec_23_19_57_locsearch_addr.txt
Normal file
17
docs/dumps/PCAPdroid_18_Dec_23_19_57_locsearch_addr.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
HTTP/1.1 200 OK
|
||||
Date: Wed, 18 Dec 2024 22:19:20 GMT
|
||||
Content-Type: application/x.db.vendo.mob.location.v3+json
|
||||
Content-Length: 2877
|
||||
Connection: keep-alive
|
||||
server-timing: intid;desc=8a52db25dd785bfe
|
||||
Server-Timing: intid;desc=8a52db25dd785bfe
|
||||
Server-Timing: intid;desc=8a52db25dd785bfe
|
||||
x-correlation-id: 0079a47f-6208-4507-9d9a-0e02802ef52c_5a24d491-8e2e-4cd8-85b5-ddd1d7d322c8
|
||||
Strict-Transport-Security: max-age=16070400; includeSubDomains
|
||||
X-XSS-Protection: 0
|
||||
server-timing: intid;desc=8a52db25dd785bfe
|
||||
Content-Security-Policy: frame-ancestors 'none';
|
||||
X-Content-Type-Options: nosniff
|
||||
Set-Cookie: TS01be2125=01d513bcd1de3bcb4b755a3859cad31611f802372756db9780547d536b115fb805b4cb7fe4f2aefad293244318a0660f18cf234872; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly
|
||||
|
||||
[{"name":"Schillerstr., Ofterdingen","locationId":"A=1@O=Schillerstr., Ofterdingen@X=9030635@Y=48420917@U=81@L=750308@B=1@p=1734378327@","evaNr":"750308","coordinates":{"latitude":48.420918,"longitude":9.030635},"weight":995,"products":["BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Schillerstr., Lutherstadt Wittenberg","locationId":"A=1@O=Schillerstr., Lutherstadt Wittenberg@X=12660057@Y=51875984@U=81@L=963037@B=1@p=1734378327@","evaNr":"963037","coordinates":{"latitude":51.875984,"longitude":12.660057},"weight":943,"products":["BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Schillerstr., Geithain","locationId":"A=1@O=Schillerstr., Geithain@X=12691420@Y=51058568@U=81@L=200268@B=1@p=1734378327@","evaNr":"200268","coordinates":{"latitude":51.058567,"longitude":12.69142},"weight":941,"products":["BUSSE"],"locationType":"ST"},{"name":"Schillerstr., Zwickau","locationId":"A=1@O=Schillerstr., Zwickau@X=12493001@Y=50715018@U=81@L=983776@B=1@p=1734378327@","evaNr":"983776","coordinates":{"latitude":50.71502,"longitude":12.493001},"weight":941,"products":["NAHVERKEHRSONSTIGEZUEGE","BUSSE","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Schillerstr., Überherrn","locationId":"A=1@O=Schillerstr., Überherrn@X=6705494@Y=49239322@U=81@L=835123@B=1@p=1734378327@","evaNr":"835123","coordinates":{"latitude":49.239323,"longitude":6.705494},"weight":941,"products":["BUSSE"],"locationType":"ST"},{"name":"Schillerstr./Forstweg, Brieselang","locationId":"A=1@O=Schillerstr./Forstweg, Brieselang@X=12988073@Y=52583830@U=81@L=734788@B=1@p=1734378327@","evaNr":"734788","coordinates":{"latitude":52.58383,"longitude":12.988073},"weight":941,"products":["BUSSE"],"locationType":"ST"},{"name":"Schillerstr., Berlin","locationId":"A=1@O=Schillerstr., Berlin@X=13410584@Y=52591948@U=81@L=732759@B=1@p=1734378327@","evaNr":"732759","coordinates":{"latitude":52.59195,"longitude":13.410584},"weight":412,"products":["STRASSENBAHN"],"locationType":"ST"},{"name":"Schillerstr., Schöneiche b. Berlin","locationId":"A=1@O=Schillerstr., Schöneiche b. Berlin@X=13709386@Y=52476958@U=81@L=738128@B=1@p=1734378327@","evaNr":"738128","coordinates":{"latitude":52.47696,"longitude":13.709386},"weight":412,"products":["STRASSENBAHN"],"locationType":"ST"},{"name":"Schillerstr., Markdorf","locationId":"A=1@O=Schillerstr., Markdorf@X=9386033@Y=47727336@U=81@L=801101@B=1@p=1734378327@","evaNr":"801101","coordinates":{"latitude":47.727337,"longitude":9.386033},"weight":272,"products":["BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"},{"name":"Schillerstr., Salzwedel","locationId":"A=1@O=Schillerstr., Salzwedel@X=11160072@Y=52850918@U=81@L=957938@B=1@p=1734378327@","evaNr":"957938","coordinates":{"latitude":52.850918,"longitude":11.160072},"weight":272,"products":["BUSSE","ANRUFPFLICHTIGEVERKEHRE"],"locationType":"ST"}]
|
|
@ -0,0 +1,18 @@
|
|||
POST /mob/angebote/fahrplan HTTP/1.1
|
||||
Accept: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
x-feature-reiseketten-enabled: false
|
||||
X-Correlation-ID: 84f2ab9b-d4a7-4e98-9a29-113783f0d431_5a24d491-8e2e-4cd8-85b5-ddd1d7d322c8
|
||||
X-Device-Os-Name: Android
|
||||
X-Device-Os-Version: 32
|
||||
X-Device-Model: Google Pixel 3a
|
||||
X-App-Version: 24.32.2
|
||||
Accept-Language: en,de
|
||||
X-INSTANA-ANDROID: a33cf0fb-5fd8-4c2f-befa-d4c1963cf3b7
|
||||
Content-Type: application/x.db.vendo.mob.verbindungssuche.v8+json
|
||||
Content-Length: 686
|
||||
Host: app.vendo.noncd.db.de
|
||||
Connection: Keep-Alive
|
||||
Accept-Encoding: gzip
|
||||
User-Agent: okhttp/4.12.0
|
||||
|
||||
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d2@O\u003dBerlin - Bohnsdorf, Schillerstraße 10@H\u003d10@X\u003d13585128@Y\u003d52398257@U\u003d92@L\u003d980126874@B\u003d1@p\u003d1706613073@","verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2024-12-18T22:38:09.928078+01:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d81@L\u003d8011160@B\u003d1@p\u003d1734031727@i\u003dU×008065969@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["BAHNCARD25 KLASSE_2"],"reisendenTyp":"SENIOR"}]},"reservierungsKontingenteVorhanden":false}
|
File diff suppressed because one or more lines are too long
15
docs/dumps/readme.md
Normal file
15
docs/dumps/readme.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Dumps from DB Navigator API app.vendo.noncd.db.de
|
||||
|
||||
In this directory, some intercepted traffic from DB Navigator. (Repo license does not apply to this directory.)
|
||||
|
||||
You can browse some responses of the bahn.de API and others in the [fixtures](https://github.com/public-transport/db-vendo-client/tree/main/test/fixtures) and [e2e fixtures](https://github.com/public-transport/db-vendo-client/tree/main/test/e2e/fixtures).
|
||||
|
||||
How to intercept DB Navigator traffic:
|
||||
|
||||
1. Download/extract Split APK
|
||||
2. Merge APK (e.g. using [APKEditor](https://github.com/REAndroid/APKEditor))
|
||||
3. decompile using apktool
|
||||
4. edit [res/xml/network_security_config.xml](https://developer.android.com/privacy-and-security/security-config) to allow user CAs not just in debug
|
||||
5. recompile using apktool, sign
|
||||
6. install on an Android
|
||||
7. intercept with a mitm decryption tool of your choice by installing CA cert into Android store (e.g. [PCAPdroid](https://github.com/emanuele-f/PCAPdroid) with [mitm addon](https://github.com/emanuele-f/PCAPdroid-mitm), no root needed)
|
|
@ -1,118 +0,0 @@
|
|||
# `journeyLeg(ref, lineName, [opt])`
|
||||
|
||||
This method can be used to refetch information about a leg of a journey. Note that it is not supported by every profile/endpoint.
|
||||
|
||||
Let's say you used [`journeys`](journeys.md) and now want to get more up-to-date data about the arrival/departure of a leg. You'd pass in a journey leg `id` like `'1|24983|22|86|18062017'`. `lineName` must be the name of the journey leg's `line.name`. You can get them like this:
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
|
||||
// Hauptbahnhof to Heinrich-Heine-Str.
|
||||
client.journeys('900000003201', '900000100008', {results: 1})
|
||||
.then(([journey]) => {
|
||||
const leg = journey.legs[0]
|
||||
return client.journeyLeg(leg.id, leg.line.name)
|
||||
})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
when: new Date(),
|
||||
passedStations: true // return stations on the way?
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
|
||||
client.journeyLeg('1|31431|28|86|17122017', 'S9', {when: 1513534689273})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
```
|
||||
|
||||
The response looked like this:
|
||||
|
||||
```js
|
||||
{
|
||||
id: '1|31431|28|86|17122017',
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000260005',
|
||||
name: 'S Flughafen Berlin-Schönefeld',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.390796,
|
||||
longitude: 13.51352
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
departure: '2017-12-17T18:37:00.000+01:00',
|
||||
departurePlatform: '13',
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000029101',
|
||||
name: 'S Spandau',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.534794,
|
||||
longitude: 13.197477
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
arrival: '2017-12-17T19:49:00.000+01:00',
|
||||
arrivalPlatform: '2',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '18299',
|
||||
name: 'S9',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
productCode: 0,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
direction: 'S Spandau',
|
||||
passed: [ /* … */ ]
|
||||
}
|
||||
```
|
346
docs/journeys.md
346
docs/journeys.md
|
@ -22,12 +22,13 @@
|
|||
{
|
||||
type: 'location',
|
||||
id: '123',
|
||||
poi: true,
|
||||
name: 'foo restaurant',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
|
||||
// an address, which is an FTPF `location` object
|
||||
// an address, which is an FPTF `location` object
|
||||
{
|
||||
type: 'location',
|
||||
address: 'foo street 1',
|
||||
|
@ -40,16 +41,23 @@ With `opt`, you can override the default options, which look like this:
|
|||
|
||||
```js
|
||||
{
|
||||
when: new Date(),
|
||||
// Use either `departure` or `arrival` to specify a date/time.
|
||||
departure: new Date(),
|
||||
arrival: null,
|
||||
|
||||
earlierThan: null, // ref to get journeys earlier than the last query
|
||||
laterThan: null, // ref to get journeys later than the last query
|
||||
results: 5, // how many journeys?
|
||||
|
||||
results: null, // number of journeys – `null` means "whatever HAFAS returns"
|
||||
via: null, // let journeys pass this station
|
||||
passedStations: false, // return stations on the way?
|
||||
transfers: 5, // maximum of 5 transfers
|
||||
stopovers: false, // return stations on the way?
|
||||
transfers: -1, // Maximum nr of transfers. Default: Let HAFAS decide.
|
||||
transferTime: 0, // minimum time for a single transfer in minutes
|
||||
accessibility: 'none', // 'none', 'partial' or 'complete'
|
||||
accessibility: 'none', // not supported
|
||||
bike: false, // only bike-friendly journeys
|
||||
walkingSpeed: 'normal', // not supported
|
||||
// Consider walking to nearby stations at the beginning of a journey?
|
||||
startWithWalking: true, // always true (?)
|
||||
products: {
|
||||
// these entries may vary from profile to profile
|
||||
suburban: true,
|
||||
|
@ -57,41 +65,82 @@ With `opt`, you can override the default options, which look like this:
|
|||
tram: true,
|
||||
bus: true,
|
||||
ferry: true,
|
||||
express: true,
|
||||
nationalExpress: true,
|
||||
national: true,
|
||||
regional: true
|
||||
regionalExpress: true // this is actually FlixTrain and co.
|
||||
},
|
||||
tickets: false // return tickets? only available with some profiles
|
||||
tickets: false, // return tickets? only available for [refreshJourney](refresh-journey.md)
|
||||
polylines: false, // return a shape for each leg? only available for [refreshJourney](refresh-journey.md)
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
scheduledDays: false, // returns a field `serviceDays` (instead of `scheduledDays` in hafas-client!) with a different, human-readable structure
|
||||
notOnlyFastRoutes: false, // if true, also show journeys that are mathematically non-optimal
|
||||
bestprice: false, // search for lowest prices across the entire day, returns list of journeys sorted by price
|
||||
firstClass: false, // first or second class for tickets
|
||||
loyaltyCard: null, // BahnCards etc., see below
|
||||
language: 'en', // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
import {createClient} 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
// Hauptbahnhof to Heinrich-Heine-Str.
|
||||
client.journeys('900000003201', '900000100008', {
|
||||
// Frankfurt to Stuttgart
|
||||
await client.journeys('8000105', '8000096', {
|
||||
results: 1,
|
||||
passedStations: true
|
||||
stopovers: true
|
||||
})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
```
|
||||
|
||||
The response may look like this:
|
||||
`journeys()` will resolve with an object with the following fields:
|
||||
- `journeys`
|
||||
- `earlierRef`/`laterRef` – pass them as `opt.earlierThan`/`opt.laterThan` into another `journeys()` call to retrieve the next "page" of journeys
|
||||
- `realtimeDataUpdatedAt` – is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
||||
|
||||
This object might look like this:
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
{
|
||||
journeys: [ {
|
||||
legs: [ {
|
||||
id: '1|31041|35|86|17122017',
|
||||
tripId: '1|32615|6|86|10072018',
|
||||
direction: 'S Ahrensfelde',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '16845',
|
||||
fahrtNr: '12345',
|
||||
name: 'S7',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
},
|
||||
symbol: 'S',
|
||||
nr: 7,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false
|
||||
},
|
||||
currentLocation: {
|
||||
type: 'location',
|
||||
latitude: 52.51384,
|
||||
longitude: 13.526806,
|
||||
},
|
||||
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000003201',
|
||||
|
@ -111,169 +160,162 @@ The response may look like this:
|
|||
regional: true
|
||||
}
|
||||
},
|
||||
departure: '2017-12-17T19:07:00.000+01:00',
|
||||
departurePlatform: '16',
|
||||
departure: '2018-07-10T23:54:00+02:00',
|
||||
plannedDeparture: '2018-07-10T23:53:00+02:00',
|
||||
departureDelay: 60,
|
||||
departurePlatform: '15',
|
||||
plannedDeparturePlatform: '14',
|
||||
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000024101',
|
||||
name: 'S Charlottenburg',
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.504806,
|
||||
longitude: 13.303846
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: true
|
||||
}
|
||||
products: { /* … */ }
|
||||
},
|
||||
arrival: '2017-12-17T19:47:00.000+01:00',
|
||||
arrivalPlatform: '8',
|
||||
arrivalDelay: 30,
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '16845',
|
||||
name: 'S7',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
symbol: 'S',
|
||||
nr: 7,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
productCode: 0,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
direction: 'S Potsdam Hauptbahnhof',
|
||||
passed: [ {
|
||||
station: {
|
||||
arrival: '2018-07-11T00:02:00+02:00',
|
||||
plannedArrival: '2018-07-11T00:01:00+02:00',
|
||||
arrivalDelay: 60,
|
||||
arrivalPlatform: '3',
|
||||
plannedArrivalPlatform: '3',
|
||||
|
||||
stopovers: [ {
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000003201',
|
||||
name: 'S+U Berlin Hauptbahnhof',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
/* … */
|
||||
},
|
||||
|
||||
arrival: null,
|
||||
plannedArrival: null,
|
||||
arrivalPlatform: null,
|
||||
plannedArrivalPlatform: null,
|
||||
departure: null,
|
||||
cancelled: true
|
||||
plannedDeparture: null,
|
||||
departurePlatform: null,
|
||||
plannedDeparturePlatform: null,
|
||||
|
||||
remarks: [
|
||||
{type: 'hint', code: 'bf', text: 'barrier-free'},
|
||||
{type: 'hint', code: 'FB', text: 'Bicycle conveyance'}
|
||||
]
|
||||
}, {
|
||||
station: {
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000003102',
|
||||
name: 'S Bellevue',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
id: '900000100001',
|
||||
name: 'S+U Friedrichstr.',
|
||||
/* … */
|
||||
},
|
||||
arrival: '2017-12-17T19:09:00.000+01:00',
|
||||
departure: '2017-12-17T19:09:00.000+01:00'
|
||||
}, /* … */ {
|
||||
station: {
|
||||
|
||||
cancelled: true,
|
||||
arrival: null,
|
||||
plannedArrival: '2018-07-10T23:55:00+02:00',
|
||||
prognosedArrival: '2018-07-10T23:56:00+02:00',
|
||||
arrivalDelay: 60,
|
||||
arrivalPlatform: null,
|
||||
plannedArrivalPlatform: null,
|
||||
|
||||
departure: null,
|
||||
plannedDeparture: '2018-07-10T23:56:00+02:00',
|
||||
prognosedDeparture: '2018-07-10T23:57:00+02:00',
|
||||
departureDelay: 60,
|
||||
departurePlatform: null,
|
||||
plannedDeparturePlatform: null,
|
||||
|
||||
remarks: [ /* … */ ]
|
||||
},
|
||||
/* … */
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000024101',
|
||||
name: 'S Charlottenburg',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
/* … */
|
||||
},
|
||||
arrival: '2017-12-17T19:17:00.000+01:00',
|
||||
departure: '2017-12-17T19:17:00.000+01:00'
|
||||
|
||||
arrival: '2018-07-11T00:02:00+02:00',
|
||||
plannedArrival: '2018-07-11T00:01:00+02:00',
|
||||
arrivalDelay: 60,
|
||||
arrivalPlatform: null,
|
||||
plannedArrivalPlatform: null,
|
||||
|
||||
departure: '2018-07-11T00:02:00+02:00',
|
||||
plannedDeparture: '2018-07-11T00:02:00+02:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: null,
|
||||
plannedDeparturePlatform: null,
|
||||
|
||||
remarks: [ /* … */ ]
|
||||
} ]
|
||||
} ],
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000003201',
|
||||
name: 'S+U Berlin Hauptbahnhof',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
departure: '2017-12-17T19:07:00.000+01:00',
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000024101',
|
||||
name: 'S Charlottenburg',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
arrival: '2017-12-17T19:47:00.000+01:00',
|
||||
arrivalDelay: 30
|
||||
},
|
||||
}, {
|
||||
public: true,
|
||||
walking: true,
|
||||
distance: 558,
|
||||
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
departure: '2018-07-11T00:01:00+02:00',
|
||||
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000100008',
|
||||
name: 'U Heinrich-Heine-Str.',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
arrival: '2018-07-11T00:10:00+02:00'
|
||||
} ]
|
||||
} ],
|
||||
earlierRef: '…', // use with the `earlierThan` option
|
||||
laterRef: '…' // use with the `laterThan` option
|
||||
]
|
||||
realtimeDataUpdatedAt: 1531259400, // 2018-07-10T23:50:00+02
|
||||
}
|
||||
```
|
||||
|
||||
Some [profiles](../p) are able to parse the ticket information, if returned by the API. For example, if you pass `tickets: true` with the [VBB profile](../p/vbb), each `journey` will have a tickets array that looks like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis – Regeltarif',
|
||||
price: 2.8,
|
||||
tariff: 'Berlin',
|
||||
coverage: 'AB',
|
||||
variant: 'adult',
|
||||
amount: 1
|
||||
}, {
|
||||
name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis – Ermäßigungstarif',
|
||||
price: 1.7,
|
||||
tariff: 'Berlin',
|
||||
coverage: 'AB',
|
||||
variant: 'reduced',
|
||||
amount: 1,
|
||||
reduced: true
|
||||
}, /* … */ {
|
||||
name: 'Berlin Tarifgebiet A-B: Tageskarte – Ermäßigungstarif',
|
||||
price: 4.7,
|
||||
tariff: 'Berlin',
|
||||
coverage: 'AB',
|
||||
variant: '1 day, reduced',
|
||||
amount: 1,
|
||||
reduced: true,
|
||||
fullDay: true
|
||||
}, /* … */ {
|
||||
name: 'Berlin Tarifgebiet A-B: 4-Fahrten-Karte – Regeltarif',
|
||||
price: 9,
|
||||
tariff: 'Berlin',
|
||||
coverage: 'AB',
|
||||
variant: '4x adult',
|
||||
amount: 4
|
||||
} ]
|
||||
```
|
||||
|
||||
If a journey leg has been cancelled, a `cancelled: true` will be added. Also, `departure`/`departureDelay`/`departurePlatform` and `arrival`/`arrivalDelay`/`arrivalPlatform` will be `null`.
|
||||
|
||||
To get more journeys earlier/later than the current set of results, pass `journeys.earlierRef`/`journeys.laterRef` into `opt.earlierThan`/`opt.laterThan`. For example, query *later* journeys as follows:
|
||||
To get more journeys earlier/later than the current set of results, pass `earlierRef`/`laterRef` into `opt.earlierThan`/`opt.laterThan`. For example, query *later* journeys as follows:
|
||||
|
||||
```js
|
||||
const hbf = '900000003201'
|
||||
const heinrichHeineStr = '900000100008'
|
||||
|
||||
client.journeys(hbf, heinrichHeineStr)
|
||||
.then((journeys) => {
|
||||
const lastJourney = journeys[journeys.length - 1]
|
||||
console.log('departure of last journey', lastJourney.departure)
|
||||
const res1 = await client.journeys(hbf, heinrichHeineStr)
|
||||
const lastJourney = res1.journeys[res1.journeys.length - 1]
|
||||
console.log('departure of last journey', lastJourney.legs[0].departure)
|
||||
|
||||
// get later journeys
|
||||
return client.journeys(hbf, heinrichHeineStr, {
|
||||
laterThan: journeys.laterRef
|
||||
})
|
||||
// get later journeys
|
||||
const res2 = await client.journeys(hbf, heinrichHeineStr, {
|
||||
laterThan: res1.laterRef
|
||||
})
|
||||
.then((laterourneys) => {
|
||||
const firstJourney = laterourneys[laterourneys.length - 1]
|
||||
console.log('departure of first (later) journey', firstJourney.departure)
|
||||
})
|
||||
.catch(console.error)
|
||||
const firstLaterJourney = res2.journeys[res2.journeys.length - 1]
|
||||
console.log('departure of first (later) journey', firstLaterJourney.legs[0].departure)
|
||||
```
|
||||
|
||||
```
|
||||
departure of last journey 2017-12-17T19:07:00.000+01:00
|
||||
departure of first (later) journey 2017-12-17T19:19:00.000+01:00
|
||||
departure of last journey 2017-12-17T19:07:00+01:00
|
||||
departure of first (later) journey 2017-12-17T19:19:00+01:00
|
||||
```
|
||||
|
||||
## Using the `loyaltyCard` option
|
||||
|
||||
```js
|
||||
import {data as loyaltyCards} from 'db-vendo-client/format/loyalty-cards.js' // see there for a list
|
||||
|
||||
hafas.journeys(from, to, {
|
||||
loyaltyCard: {type: data.BAHNCARD, discount: 25}
|
||||
})
|
||||
```
|
||||
|
||||
## 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.
|
|
@ -1,86 +0,0 @@
|
|||
# `location(station)`
|
||||
|
||||
`station` must be in one of these formats:
|
||||
|
||||
```js
|
||||
// a station ID, in a format compatible to the profile you use
|
||||
'900000123456'
|
||||
|
||||
// an FPTF `station` object
|
||||
{
|
||||
type: 'station',
|
||||
id: '900000123456',
|
||||
name: 'foo station',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
|
||||
client.location('900000042101') // U Spichernstr.
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
```
|
||||
|
||||
The response may look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'station',
|
||||
id: '900000042101',
|
||||
name: 'U Spichernstr.',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.496581,
|
||||
longitude: 13.330616
|
||||
},
|
||||
products: {
|
||||
suburban: false,
|
||||
subway: true,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: false
|
||||
},
|
||||
lines: [ {
|
||||
type: 'line',
|
||||
id: 'u1',
|
||||
name: 'U1',
|
||||
public: true,
|
||||
class: 2,
|
||||
product: 'subway',
|
||||
mode: 'train',
|
||||
symbol: 'U',
|
||||
nr: 1,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false },
|
||||
// …
|
||||
{ type: 'line',
|
||||
id: 'n9',
|
||||
name: 'N9',
|
||||
public: true,
|
||||
class: 8,
|
||||
product: 'bus',
|
||||
mode: 'bus',
|
||||
symbol: 'N',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: true
|
||||
} ]
|
||||
}
|
||||
```
|
|
@ -6,34 +6,36 @@ With `opt`, you can override the default options, which look like this:
|
|||
|
||||
```js
|
||||
{
|
||||
fuzzy: true // find only exact matches?
|
||||
, results: 10 // how many search results?
|
||||
, stations: true
|
||||
fuzzy: true // not supported
|
||||
, results: 5 // how many search results?
|
||||
, stops: true // return stops/stations?
|
||||
, addresses: true
|
||||
, poi: true // points of interest
|
||||
, subStops: true // not supported
|
||||
, entrances: true // not supported
|
||||
, linesOfStops: false // not supported
|
||||
, language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbnavProfile, userAgent)
|
||||
|
||||
client.locations('Alexanderplatz', {results: 3})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
await client.locations('Alexanderplatz', {results: 3})
|
||||
```
|
||||
|
||||
The response may look like this:
|
||||
The result may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
type: 'station',
|
||||
type: 'stop',
|
||||
id: '900000100003',
|
||||
name: 'S+U Alexanderplatz',
|
||||
location: {
|
||||
|
@ -52,14 +54,16 @@ The response may look like this:
|
|||
}
|
||||
}, { // point of interest
|
||||
type: 'location',
|
||||
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
|
||||
id: '900980709',
|
||||
poi: true,
|
||||
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
|
||||
latitude: 52.523549,
|
||||
longitude: 13.418441
|
||||
}, { // point of interest
|
||||
type: 'location',
|
||||
name: 'Berlin, Hotel Agon am Alexanderplatz',
|
||||
id: '900980176',
|
||||
poi: true,
|
||||
name: 'Berlin, Hotel Agon am Alexanderplatz',
|
||||
latitude: 52.524556,
|
||||
longitude: 13.420266
|
||||
} ]
|
||||
|
|
|
@ -1,43 +1,45 @@
|
|||
# `nearby(location, [opt])`
|
||||
|
||||
This method can be used to find stations close to a location. Note that it is not supported by every profile/endpoint.
|
||||
This method can be used to find stops/stations & POIs close to a location. Note that it is not supported by every profile/endpoint.
|
||||
|
||||
`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#location-objects).
|
||||
`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#location-objects).
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
results: 8, // maximum number of results
|
||||
distance: null, // maximum walking distance in meters
|
||||
poi: false, // return points of interest?
|
||||
stations: true, // return stations?
|
||||
poi: false, // not supported
|
||||
stops: true, // return stops/stations?
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
linesOfStops: false, // not supported
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
client.nearby({
|
||||
await client.nearby({
|
||||
type: 'location',
|
||||
latitude: 52.5137344,
|
||||
longitude: 13.4744798
|
||||
}, {distance: 400})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
```
|
||||
|
||||
The response may look like this:
|
||||
The result may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
type: 'station',
|
||||
type: 'stop',
|
||||
id: '900000120001',
|
||||
name: 'S+U Frankfurter Allee',
|
||||
location: {
|
||||
|
@ -56,7 +58,7 @@ The response may look like this:
|
|||
},
|
||||
distance: 56
|
||||
}, {
|
||||
type: 'station',
|
||||
type: 'stop',
|
||||
id: '900000120540',
|
||||
name: 'Scharnweberstr./Weichselstr.',
|
||||
location: {
|
||||
|
@ -67,7 +69,7 @@ The response may look like this:
|
|||
products: { /* … */ },
|
||||
distance: 330
|
||||
}, {
|
||||
type: 'station',
|
||||
type: 'stop',
|
||||
id: '900000160544',
|
||||
name: 'Rathaus Lichtenberg',
|
||||
location: {
|
||||
|
|
2492
docs/openapi.yaml
Normal file
2492
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load diff
158
docs/radar.md
158
docs/radar.md
|
@ -1,158 +0,0 @@
|
|||
# `radar(north, west, south, east, [opt])`
|
||||
|
||||
Use this method to find all vehicles currently in an area. Note that it is not supported by every profile/endpoint.
|
||||
|
||||
`north`, `west`, `south` and `eath` must be numbers (e.g. `52.52411`). Together, they form a [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box).
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
results: 256, // maximum number of vehicles
|
||||
duration: 30, // compute frames for the next n seconds
|
||||
frames: 3, // nr of frames to compute
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
|
||||
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||
|
||||
```js
|
||||
const createClient = require('hafas-client')
|
||||
const vbbProfile = require('hafas-client/p/vbb')
|
||||
|
||||
const client = createClient(vbbProfile)
|
||||
|
||||
client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 5})
|
||||
.then(console.log)
|
||||
.catch(console.error)
|
||||
```
|
||||
|
||||
The response may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.521508,
|
||||
longitude: 13.411267
|
||||
},
|
||||
line: {
|
||||
type: 'line',
|
||||
id: 's9',
|
||||
name: 'S9',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
direction: 'S Flughafen Berlin-Schönefeld',
|
||||
trip: 31463,
|
||||
nextStops: [ {
|
||||
station: {
|
||||
type: 'station',
|
||||
id: '900000029101',
|
||||
name: 'S Spandau',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.534794,
|
||||
longitude: 13.197477
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
arrival: null,
|
||||
arrivalDelay: null,
|
||||
departure: '2017-12-17T19:16:00.000+01:00',
|
||||
departureDelay: null
|
||||
} /* … */ ],
|
||||
frames: [ {
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000100003',
|
||||
name: 'S+U Alexanderplatz',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
t: 0
|
||||
}, /* … */ {
|
||||
origin: { /* Alexanderplatz */ },
|
||||
destination: { /* Jannowitzbrücke */ },
|
||||
t: 30000
|
||||
} ]
|
||||
}, {
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.523297,
|
||||
longitude: 13.411151
|
||||
},
|
||||
line: {
|
||||
type: 'line',
|
||||
id: 'm2',
|
||||
name: 'M2',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'tram',
|
||||
symbol: 'M',
|
||||
nr: 2,
|
||||
metro: true,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 'berliner-verkehrsbetriebe',
|
||||
name: 'Berliner Verkehrsbetriebe'
|
||||
}
|
||||
},
|
||||
direction: 'Heinersdorf',
|
||||
trip: 26321,
|
||||
nextStops: [ {
|
||||
station: { /* S+U Alexanderplatz/Dircksenstr. */ },
|
||||
arrival: null,
|
||||
arrivalDelay: null,
|
||||
departure: '2017-12-17T19:52:00.000+01:00',
|
||||
departureDelay: null
|
||||
}, {
|
||||
station: { /* Memhardstr. */ },
|
||||
arrival: '2017-12-17T19:54:00.000+01:00',
|
||||
arrivalDelay: null,
|
||||
departure: '2017-12-17T19:54:00.000+01:00',
|
||||
departureDelay: null
|
||||
}, /* … */ ],
|
||||
frames: [ {
|
||||
origin: { /* S+U Alexanderplatz/Dircksenstr. */ },
|
||||
destination: { /* Memhardstr. */ },
|
||||
t: 0
|
||||
}, /* … */ {
|
||||
origin: { /* Memhardstr. */ },
|
||||
destination: { /* Mollstr./Prenzlauer Allee */ },
|
||||
t: 30000
|
||||
} ]
|
||||
}, /* … */ ]
|
||||
```
|
161
docs/readme.md
161
docs/readme.md
|
@ -1,13 +1,154 @@
|
|||
# API documentation
|
||||
# `db-vendo-client` documentation
|
||||
|
||||
- [`journeys(from, to, [opt])`](journeys.md) – get journeys between locations
|
||||
- [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) – get details for a leg of a journey
|
||||
- [`departures(station, [opt])`](departures.md) – query the next departures at a station
|
||||
- [`locations(query, [opt])`](locations.md) – find stations, POIs and addresses
|
||||
- [`location(id)`](location.md) – get details about a location
|
||||
- [`nearby(location, [opt])`](nearby.md) – show stations & POIs around
|
||||
- [`radar(north, west, south, east, [opt])`](radar.md) – find all vehicles currently in a certain area
|
||||
**[JS API documentation](api.md)**
|
||||
|
||||
## Writing a profile
|
||||
[REST API OpenAPI schema](openapi.yaml) ([open in Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/public-transport/db-vendo-client/refs/heads/main/docs/openapi.yaml))
|
||||
|
||||
Check [the guide](writing-a-profile.md).
|
||||
## Migrating from an old (v5) `hafas-client` version
|
||||
|
||||
`db-vendo-client` tries to be as compatible as possible with `hafas-client` v6. If you were still on v5 or earlier, see the [`5` → `6` migration guide](https://github.com/public-transport/hafas-client/blob/main/docs/migrating-to-6.md) of `hafas-client`.
|
||||
|
||||
## Throttling requests
|
||||
|
||||
There's opt-in support for throttling requests to the endpoint.
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {withThrottling} from 'db-vendo-client/throttle.js'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
|
||||
// create a throttled HAFAS client with Deutsche Bahn profile
|
||||
const client = createClient(withThrottling(dbProfile), userAgent)
|
||||
|
||||
// Berlin Jungfernheide to München Hbf
|
||||
await client.journeys('8011167', '8000261', {results: 1})
|
||||
```
|
||||
|
||||
You can also pass custom values for the nr of requests (`limit`) per interval into `withThrottling`:
|
||||
|
||||
```js
|
||||
// 2 requests per second
|
||||
const throttledDbProfile = withThrottling(dbProfile, 2, 1000)
|
||||
const client = createClient(throttledDbProfile, userAgent)
|
||||
```
|
||||
|
||||
## Retrying failed requests
|
||||
|
||||
There's opt-in support for retrying failed requests to the endpoint.
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {withRetrying} from 'db-vendo-client/retry.js'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
|
||||
// create a client with Deutsche Bahn profile that will retry on HAFAS errors
|
||||
const client = createClient(withRetrying(dbProfile), userAgent)
|
||||
```
|
||||
|
||||
You can pass custom options into `withRetrying`. They will be passed into [`retry`](https://github.com/tim-kos/node-retry#tutorial).
|
||||
|
||||
```js
|
||||
// retry 2 times, after 10 seconds & 30 seconds
|
||||
const retryingDbProfile = withRetrying(dbProfile, {
|
||||
retries: 2,
|
||||
minTimeout: 10 * 1000,
|
||||
factor: 3
|
||||
})
|
||||
const client = createClient(retryingDbProfile, userAgent)
|
||||
```
|
||||
|
||||
## User agent randomization
|
||||
|
||||
By default, `db-vendo-client` does not randomize the client name that you pass into `createClient`, and sends it as [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) as it is. At least DB Navigator always sends the same user agent as well (cf. `dbnav` profile). You can turn on randomization by setting `profile.randomizeUserAgent` to `false`:
|
||||
|
||||
```js
|
||||
const client = createClient({
|
||||
...someProfile,
|
||||
randomizeUserAgent: true,
|
||||
}, userAgent)
|
||||
```
|
||||
|
||||
## Logging requests
|
||||
|
||||
You can use `profile.logRequest` and `profile.logResponse` to process the raw [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), respectively.
|
||||
|
||||
As an example, we can implement a custom logger:
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
|
||||
const logRequest = (ctx, fetchRequest, requestId) => {
|
||||
// ctx looks just like with the other profile.* hooks:
|
||||
const {dbProfile, opt} = ctx
|
||||
|
||||
console.debug(requestId, fetchRequest.headers, fetchRequest.body + '')
|
||||
}
|
||||
|
||||
const logResponse = (ctx, fetchResponse, body, requestId) => {
|
||||
console.debug(requestId, fetchResponse.headers, body + '')
|
||||
}
|
||||
|
||||
// create a client with Deutsche Bahn profile that debug-logs
|
||||
const client = createClient({
|
||||
...dbProfile,
|
||||
logRequest,
|
||||
logResponse,
|
||||
}, userAgent)
|
||||
```
|
||||
|
||||
```js
|
||||
// logRequest output:
|
||||
'29d0e3' {
|
||||
accept: 'application/json',
|
||||
'accept-encoding': 'gzip, br, deflate',
|
||||
'content-type': 'application/json',
|
||||
connection: 'keep-alive',
|
||||
'user-agent': 'hafas842c51-clie842c51nt debug C842c51LI'
|
||||
} {"lang":"de","svcReqL":[{"cfg":{"polyEnc":"GPA"},"meth":"LocMatch",…
|
||||
// logResponse output:
|
||||
'29d0e3' {
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '1010',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
date: 'Thu, 06 Oct 2022 12:31:09 GMT',
|
||||
server: 'Apache',
|
||||
vary: 'User-Agent'
|
||||
} {"ver":"1.45","lang":"deu","id":"sb42zgck4mxtxm4s","err":"OK","graph"…
|
||||
```
|
||||
|
||||
The default `profile.logRequest` [`console.error`](https://nodejs.org/docs/latest-v10.x/api/console.html#console_console_error_data_args)s the request body, if you have set `$DEBUG` to `hafas-client`. Likewise, `profile.logResponse` `console.error`s the response body.
|
||||
|
||||
## Error handling
|
||||
|
||||
Unexpected errors – e.g. due to bugs in `db-vendo-client` itself – aside, its methods may reject with the following errors:
|
||||
|
||||
- `Error` – A generic error, e.g. if the DB backend returned a HTTP error.
|
||||
- `HafasError` – A generic error to signal that something HAFAS-related went wrong, either in the client, or in the HAFAS endpoint.
|
||||
|
||||
Each `HafasError` error has the following properties:
|
||||
|
||||
- `isHafasError` – Always `true`. Allows you to distinguish HAFAS-related errors from e.g. network errors.
|
||||
- `code` – A string representing the error type for all other error classes, e.g. `INVALID_REQUEST` for `HafasInvalidRequestError`. `null` for plain `HafasError`s.
|
||||
- `isCausedByServer` – Boolean, telling you if the HAFAS endpoint says that it couldn't process your request because *it* is unavailable/broken.
|
||||
- `hafasCode` – A HAFAS-specific error code, if the HAFAS endpoint returned one; e.g. `H890` when no journeys could be found. `null` otherwise.
|
||||
- `request` – The [Fetch API `Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) of the request.
|
||||
- `url` – The URL of the request.
|
||||
- `response` – The [Fetch API `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
|
||||
|
||||
|
||||
## Using `db-vendo-client` from another language
|
||||
|
||||
If you want to use `db-vendo-client` to access DB APIs but work with non-Node.js environments, you can use it together with [hafas-rest-api](https://github.com/public-transport/hafas-rest-api) to create a REST API (see the [root readme](https://github.com/public-transport/db-vendo-client/tree/main#usage) and the Docker image).
|
||||
Or use [`hafas-client-rpc`](https://github.com/derhuerst/hafas-client-rpc) to create a [JSON-RPC](https://www.jsonrpc.org) interface that you can send commands to.
|
||||
|
||||
|
||||
## General documentation and notes for DB APIs
|
||||
|
||||
[`db-apis.md`](db-apis.md)
|
||||
|
|
39
docs/refresh-journey.md
Normal file
39
docs/refresh-journey.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# `refreshJourney(refreshToken, [opt])`
|
||||
|
||||
`refreshToken` must be a string, taken from `journey.refreshToken`.
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
stopovers: false, // return stations on the way?
|
||||
polylines: false, // return a shape for each leg? mutually exclusive with tickets
|
||||
tickets: false, // return tickets? mutually exclusive with polylines
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
const {journeys} = await client.journeys('8000105', '8000096', {results: 1})
|
||||
|
||||
// later, fetch up-to-date info on the journey
|
||||
const {
|
||||
journey,
|
||||
realtimeDataUpdatedAt,
|
||||
} = await client.refreshJourney(journeys[0].refreshToken, {stopovers: true, remarks: true})
|
||||
```
|
||||
|
||||
`journey` is a *single* [*Friendly Public Transport Format* v2 draft](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md) `journey`, in the same format as returned by [`journeys()`](journeys.md).
|
||||
|
||||
`realtimeDataUpdatedAt` is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
87
docs/stop.md
Normal file
87
docs/stop.md
Normal file
|
@ -0,0 +1,87 @@
|
|||
# `stop(id, [opt])`
|
||||
|
||||
This endpoint is not available with `dbweb` profile.
|
||||
|
||||
`id` must be in one of these formats:
|
||||
|
||||
```js
|
||||
// a stop/station ID, in a format compatible with the profile you use
|
||||
'900000123456'
|
||||
|
||||
// an FPTF `stop`/`station` object
|
||||
{
|
||||
type: 'station',
|
||||
id: '900000123456',
|
||||
name: 'foo station',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
linesOfStops: false, // parse & expose lines at the stop/station?
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
|
||||
```js
|
||||
import {createClient} from 'hafas-client'
|
||||
import {profile as dbProfile} from 'hafas-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
await client.stop('900000042101') // U Spichernstr.
|
||||
```
|
||||
|
||||
The result may look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'stop',
|
||||
id: '900000042101',
|
||||
name: 'U Spichernstr.',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.496581,
|
||||
longitude: 13.330616
|
||||
},
|
||||
products: {
|
||||
suburban: false,
|
||||
subway: true,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: false
|
||||
},
|
||||
lines: [ {
|
||||
type: 'line',
|
||||
id: 'u1',
|
||||
mode: 'train',
|
||||
product: 'subway',
|
||||
public: true,
|
||||
name: 'U1',
|
||||
},
|
||||
// …
|
||||
{
|
||||
type: 'line',
|
||||
id: 'n9',
|
||||
mode: 'bus',
|
||||
product: 'bus',
|
||||
public: true,
|
||||
name: 'N9',
|
||||
} ]
|
||||
}
|
||||
```
|
26
docs/tests.md
Normal file
26
docs/tests.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# automated tests in `db-vendo-client`
|
||||
|
||||
Because transit data is inherently dynamic (e.g. a different set of departures being returned for a stop now than in 10 minutes), and because it is of paramount importance that `db-vendo-client` actually works with HAFAS endpoints *as they currently work*, its testing setup is a bit unusual.
|
||||
|
||||
`db-vendo-client` has three kinds of automated tests:
|
||||
- unit tests, which test individual aspects of the case base in isolation (e.g. the parsing of HAFAS-formatted dates & times) – run via `npm run test-unit`
|
||||
- end-to-end (E2E) tests, which run actual HTTP requests against their respective profile's HAFAS endpoint – run via `npm run test-e2e`
|
||||
- integration tests, which are the E2E tests running against pre-recorded (and checked-in) HTTP request fixtures – run via `npm run test-integration`
|
||||
|
||||
Because the E2E & integration tests are based on the same code, when changing this code, you should also update the integration test fixtures accordingly.
|
||||
|
||||
*Note:* In order to be as reproducible as possible, the tests query transit data for a certain *fixed* point in time on the future, hard-coded in each profile's test suite (a.k.a. each file `test/e2e/*.js`). In combination with the recording & mocking of HTTP requests, this effectively makes the integration tests deterministic.
|
||||
|
||||
## adding integration test fixtures
|
||||
|
||||
As an example, let's assume that we have added an entirely new test to [the *DB* profile's E2E tests](../test/e2e/db.js).
|
||||
|
||||
The behaviour of the HTTP request recording (into fixtures) and mocking (using the recorded fixtures) is controlled via an environment variable `$VCR_MODE`:
|
||||
- By running the test(s) with `VCR_MODE=record`, we can record the HTTP requests being made. The tests will run just like without `$VCR_MODE`, except that they will query data for date+time specified in `T_MOCK` (e.g. [here](https://github.com/public-transport/db-vendo-client/blob/8ff945c07515155380de0acb33584e474d6d547c/test/e2e/db.js#L33)).
|
||||
- Then, by running the test(s) with `VCR_MODE=playback`, because their HTTP requests match the pre-recorded fixtures, they work on the corresponding mocked HTTP responses.
|
||||
|
||||
Usually, you would not want to update all *already existing* recorded HTTP request fixtures of the test suite you have made changes in, as they are unrelated to the test you have added. To only record your *added* test, temporarily change `tap.test(…)` to read `tap.only(…)`, and run with `TAP_ONLY=1 VCR_MODE=record`; This will skip all unrelated tests entirely.
|
||||
|
||||
Then, check the augmented fixtures (in `test/e2e/fixtures`) into Git, and revert the `tap.only(…)` change. To make sure that everything works, run the entire test suite/file (*without `TAP_ONLY=1`*) with `VCR_MODE=playback`.
|
||||
|
||||
*Note:* It might be that the test suite/file you want to augment hasn't been updated in a while, so that `T_MOCK` is in the past. In this case, recording additional fixtures from actual HTTP requests of your added test is usually not possible because the HAFAS API is unable to serve old transit data. In this case, you will first have to change `T_MOCK` to a future date+time (a weekday as "normal" as possible) and re-record all tests' HTTP requests; Don't hesitate to get in touch with me if you have trouble with this.
|
183
docs/trip.md
Normal file
183
docs/trip.md
Normal file
|
@ -0,0 +1,183 @@
|
|||
# `trip(id, [opt])`
|
||||
|
||||
This method can be used to refetch information about a trip – a vehicle stopping at a set of stops at specific times.
|
||||
|
||||
Let's say you used [`journeys`](journeys.md) and now want to get more up-to-date data about the arrival/departure of a leg. You'd pass in the trip ID from `leg.tripId`, e.g. `'1|24983|22|86|18062017'`, and the name of the line from `leg.line.name` like this:
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbnavProfile, userAgent)
|
||||
|
||||
const {journeys} = client.journeys('8000096', '8000105', {results: 1})
|
||||
const leg = journeys[0].legs[0]
|
||||
|
||||
await client.trip(leg.tripId)
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
stopovers: true, // return stations on the way?
|
||||
polyline: false, // return a shape for the trip? only supported with HAFAS trip id (i.e. not with a trip id from a departure/arrival board of the `db` profile)
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const client = createClient(dbnavProfile)
|
||||
|
||||
const {
|
||||
trip,
|
||||
realtimeDataUpdatedAt,
|
||||
} = await client.trip('1|31431|28|86|17122017', 'S9', {
|
||||
when: 1513534689273,
|
||||
})
|
||||
```
|
||||
|
||||
`realtimeDataUpdatedAt` is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
||||
|
||||
When running the code above, `trip` looked like this:
|
||||
|
||||
```js
|
||||
{
|
||||
id: '1|31431|28|86|17122017',
|
||||
direction: 'S Spandau',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '18299',
|
||||
fahrtNr: '12345',
|
||||
name: 'S9',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
currentLocation: {
|
||||
type: 'location',
|
||||
latitude: 52.447455,
|
||||
longitude: 13.522464,
|
||||
},
|
||||
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000260005',
|
||||
name: 'S Flughafen Berlin-Schönefeld',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.390796,
|
||||
longitude: 13.51352
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
departure: '2017-12-17T18:37:00+01:00',
|
||||
plannedDeparture: '2017-12-17T18:37:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '13',
|
||||
plannedDeparturePlatform: '13',
|
||||
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000029101',
|
||||
name: 'S Spandau',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.534794,
|
||||
longitude: 13.197477
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
arrival: '2017-12-17T19:50:30+01:00',
|
||||
plannedArrival: '2017-12-17T19:49:00+01:00',
|
||||
arrivalDelay: 90,
|
||||
arrivalPlatform: '3a',
|
||||
plannedArrivalPlatform: '2',
|
||||
|
||||
stopovers: [ /* … */ ]
|
||||
}
|
||||
```
|
||||
|
||||
### `polyline` option
|
||||
|
||||
Only supported with HAFAS trip id (i.e. not with a trip id from a departure/arrival board of the `db` profile).
|
||||
|
||||
If you pass `polyline: true`, the trip will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1).
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.3875, 52.43993] // longitude, latitude
|
||||
}
|
||||
},
|
||||
/* … */
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.38892, 52.49448] // longitude, latitude
|
||||
}
|
||||
},
|
||||
/* … */
|
||||
{
|
||||
// intermediate point, without associated station
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.28599, 52.58742] // longitude, latitude
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.28406, 52.58915] // longitude, latitude
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
|
@ -1,166 +0,0 @@
|
|||
# Writing a profile
|
||||
|
||||
**Per endpoint, `hafas-client` has an endpoint-specific customisation called *profile*** which may for example do the following:
|
||||
|
||||
- handle the additional requirements of the endpoint (e.g. authentication),
|
||||
- extract additional information from the data provided by the endpoint,
|
||||
- guard against triggering bugs of certain endpoints (e.g. time limits).
|
||||
|
||||
This guide is about writing such a profile. If you just want to use an already supported endpoint, refer to the [API documentation](readme.md) instead.
|
||||
|
||||
*Note*: **If you get stuck, ask for help by [creating an issue](https://github.com/derhuerst/hafas-client/issues/new)!** We want to help people expand the scope of this library.
|
||||
|
||||
## 0. How do the profiles work?
|
||||
|
||||
A profile contains of three things:
|
||||
|
||||
- **mandatory details about the HAFAS endpoint**
|
||||
- `endpoint`: The protocol, host and path of the endpoint.
|
||||
- `locale`: The [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) [locale](https://en.wikipedia.org/wiki/Locale_(computer_software)) of your endpoint (or the area that your endpoint covers).
|
||||
- `timezone`: An [IANA-time-zone](https://www.iana.org/time-zones)-compatible [timezone](https://en.wikipedia.org/wiki/Time_zone) of your endpoint.
|
||||
- **flags indicating that features are supported by the endpoint** – e.g. `journeyRef`
|
||||
- **methods overriding the [default profile](../lib/default-profile.js)**
|
||||
|
||||
As an example, let's say we have an [Austrian](https://en.wikipedia.org/wiki/Austria) endpoint:
|
||||
|
||||
```js
|
||||
const myProfile = {
|
||||
endpoint: 'https://example.org/bin/mgate.exe',
|
||||
locale: 'de-AT',
|
||||
timezone: 'Europe/Vienna'
|
||||
}
|
||||
```
|
||||
|
||||
Assuming the endpoint returns all lines names prefixed with `foo `, We can strip them like this:
|
||||
|
||||
```js
|
||||
// get the default line parser
|
||||
const createParseLine = require('hafas-client/parse/line')
|
||||
|
||||
const createParseLineWithoutFoo = (profile, operators) => {
|
||||
const parseLine = createParseLine(profile, operators)
|
||||
|
||||
// wrapper function with additional logic
|
||||
const parseLineWithoutFoo = (l) => {
|
||||
const line = parseLine(l)
|
||||
line.name = line.name.replace(/foo /g, '')
|
||||
return line
|
||||
}
|
||||
return parseLineWithoutFoo
|
||||
}
|
||||
|
||||
profile.parseLine = createParseLineWithoutFoo
|
||||
```
|
||||
|
||||
If you pass this profile into `hafas-client`, the `parseLine` method will override [the default one](../parse/line.js).
|
||||
|
||||
## 1. Setup
|
||||
|
||||
*Note*: There are many ways to find the required values. This way is rather easy and has worked for most of the apps that we've looked at so far.
|
||||
|
||||
1. **Get an iOS or Android device and download the "official" app** for the public transport provider that you want to build a profile for.
|
||||
2. **Configure a [man-in-the-middle HTTP proxy](https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/)** like [mitmproxy](https://mitmproxy.org).
|
||||
- Configure your device to trust the self-signed SSL certificate, [as outlined in the mitmproxy docs](https://docs.mitmproxy.org/stable/concepts-certificates/).
|
||||
- *Note*: This method does not work if the app uses [public key pinning](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning). In this case (the app won't be able to query data), please [create an issue](https://github.com/derhuerst/hafas-client/issues/new), so we can discuss other techniques.
|
||||
3. **Record requests of the app.**
|
||||
- [There's a video showing this step](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4).
|
||||
- Make sure to cover all relevant sections of the app, e.g. "journeys", "departures", "live map". Better record more than less; You will regret not having enough information later on.
|
||||
- To help others in the future, post the requests (in their entirety!) on GitHub, e.g. in as format like [this](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886). This will also let us help you if you have any questions.
|
||||
|
||||
## 2. Basic profile
|
||||
|
||||
- **Identify the `endpoint`.** The protocol, host and path of the endpoint, *but not* the query string.
|
||||
- *Note*: **`hafas-client` for now only supports the interface providing JSON** (generated from XML), which is being used by the corresponding iOS/Android apps. It supports neither the JSONP, nor the XML, nor the HTML interface. If the endpoint does not end in `mgate.exe`, it mostly likely won't work.
|
||||
- **Identify the `locale`.** Basically guess work; Use the date & time formats as an indicator.
|
||||
- **Identify the `timezone`.** This may be tricky, a for example [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) returns departures for Moscow as `+01:00` instead of `+03:00`.
|
||||
- **Copy the authentication** and other meta fields, namely `ver`, `ext`, `client` and `lang`.
|
||||
- You can find these fields in the root of each request JSON. Check [a VBB request](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L13-L22) and [the corresponding VBB profile](https://github.com/derhuerst/hafas-client/blob/6e61097687a37b60d53e767f2711466b80c5142c/p/vbb/index.js#L22-L29) for an example.
|
||||
- Add a function `transformReqBody(body)` to your profile, which assigns them to `body`.
|
||||
- Some profiles have a `checksum` parameter (like [here](https://gist.github.com/derhuerst/2a735268bd82a0a6779633f15dceba33#file-journey-details-1-http-L1)) or two `mic` & `mac` parameters (like [here](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L1)). If you see one of them in your requests, jump to [*Appendix A: checksum, mic, mac*](#appendix-a-checksum-mic-mac). Unfortunately, this is necessary to get the profile working.
|
||||
|
||||
## 3. Products
|
||||
|
||||
In `hafas-client`, there's a difference between the `mode` and the `product` field:
|
||||
|
||||
- The `mode` field describes the mode of transport in general. [Standardised by the *Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#modes), it is on purpose limited to a very small number of possible values, e.g. `train` or `bus`.
|
||||
- The value for `product` relates to how a means of transport "works" *in local context*. Example: Even though [*S-Bahn*](https://en.wikipedia.org/wiki/Berlin_S-Bahn) and [*U-Bahn*](https://en.wikipedia.org/wiki/Berlin_U-Bahn) in Berlin are both `train`s, they have different operators, service patterns, stations and look different. Therefore, they are two distinct `product`s `subway` and `suburban`.
|
||||
|
||||
**Specify `product`s that appear in the app** you recorded requests of. For a fictional transit network, this may look like this:
|
||||
|
||||
```js
|
||||
const products = {
|
||||
commuterTrain: {
|
||||
product: 'commuterTrain',
|
||||
mode: 'train',
|
||||
bitmask: 1,
|
||||
name: 'ACME Commuter Rail',
|
||||
short: 'CR'
|
||||
},
|
||||
metro: {
|
||||
product: 'metro',
|
||||
mode: 'train',
|
||||
bitmask: 2,
|
||||
name: 'Foo Bar Metro',
|
||||
short: 'M'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's break this down:
|
||||
|
||||
- `product`: A sensible, [camelCased](https://en.wikipedia.org/wiki/Camel_case#Variations_and_synonyms), alphanumeric identifier. Use it for the key in the `products` object as well.
|
||||
- `mode`: A [valid *Friendly Public Transport Format* `1.0.1` mode](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#modes).
|
||||
- `bitmask`: HAFAS endpoints work with a [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)#Arguments_to_functions) that toggles the individual products. the value should toggle the appropriate bit(s) in the bitmask (see below).
|
||||
- `name`: A short, but distinct name for the means of transport, *just precise enough in local context*, and in the local language. In Berlin, `S-Bahn-Schnellzug` would be too much, because everyone knows what `S-Bahn` means.
|
||||
- `short`: The shortest possible symbol that identifies the product.
|
||||
|
||||
todo: `defaultProducts`, `allProducts`, `bitmasks`, add to profile
|
||||
|
||||
If you want, you can now **verify that the profile works**; We've prepared [a script](https://runkit.com/derhuerst/hafas-client-profile-example) for that. Alternatively, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) and we will help you out with testing and improvements.
|
||||
|
||||
### Finding the right values for the `bitmask` field
|
||||
|
||||
As shown in [the video](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4), search for a journey and toggle off one product at a time, recording the requests. After extracting the products bitmask ([example](https://gist.github.com/derhuerst/193ef489f8aa50c2343f8bf1f2a22069#file-via-http-L34)) you will end up with values looking like these:
|
||||
|
||||
```
|
||||
toggles value binary subtraction bit(s)
|
||||
all products 31 11111 31 - 0
|
||||
all but ACME Commuter Rail 15 01111 31 - 2^4 2^4
|
||||
all but Foo Bar Metro 23 10111 31 - 2^3 2^3
|
||||
all but product E 30 11001 31 - 2^2 - 2^1 2^2, 2^1
|
||||
all but product F 253 11110 31 - 2^1 2^0
|
||||
```
|
||||
|
||||
## 4. Additional info
|
||||
|
||||
We consider these improvements to be *optional*:
|
||||
|
||||
- **Check if the endpoint supports the journey legs call.**
|
||||
- In the app, check if you can query details for the status of a single journey leg. It should load realtime delays and the current progress.
|
||||
- If this feature is supported, add `journeyLeg: true` to the profile.
|
||||
- **Check if the endpoint supports the live map call.** Does the app have a "live map" showing all vehicles within an area? If so, add `radar: true` to the profile.
|
||||
- **Consider transforming station & line names** into the formats that's most suitable for *local users*. Some examples:
|
||||
- `M13 (Tram)` -> `M13`. With Berlin context, it is obvious that `M13` is a tram.
|
||||
- `Berlin Jungfernheide Bhf` -> `Berlin Jungfernheide`. With local context, it's obvious that *Jungfernheide* is a train station.
|
||||
- **Check if the endpoint has non-obvious limitations** and let use know about these. Examples:
|
||||
- Some endpoints have a time limit, after which they won't return more departures, but silently discard them.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: `checksum`, `mic`, `mac`
|
||||
|
||||
As far as we know, there are three different types of authentication used among HAFAS deployments.
|
||||
|
||||
### unprotected endpoints
|
||||
|
||||
You can just query these, as long as you send a formally correct request.
|
||||
|
||||
### endpoints using the `checksum` query parameter
|
||||
|
||||
`checksum` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code): `hafas-client` will compute it by [hashing](https://en.wikipedia.org/wiki/Hash_function) the request body and a secret *salt*. **This secret can be read from the config file inside the app bundle.** There is no guide for this yet, so please [open an issue](https://github.com/derhuerst/hafas-client/issues/new) instead.
|
||||
|
||||
### endpoints using the `mic` & `mac` query parameters
|
||||
|
||||
`mic` is a [message integrity code](https://en.wikipedia.org/wiki/Message_authentication_code), the [hash](https://en.wikipedia.org/wiki/Hash_function) of the request body.
|
||||
|
||||
`mac` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code), the hash of `mic` and a secret *salt*. **This secret can be read from the config file inside the app bundle.** There is no guide for this yet, so please [open an issue](https://github.com/derhuerst/hafas-client/issues/new) instead.
|
62
eslint.config.js
Normal file
62
eslint.config.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import eslintPluginJs from '@eslint/js';
|
||||
import eslintPluginStylistic from '@stylistic/eslint-plugin';
|
||||
import globals from 'globals';
|
||||
|
||||
|
||||
const config = [
|
||||
eslintPluginJs.configs.recommended,
|
||||
eslintPluginStylistic.configs['all-flat'],
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'@stylistic/array-bracket-newline': ['error', 'consistent'],
|
||||
'@stylistic/array-element-newline': ['error', 'consistent'],
|
||||
'@stylistic/arrow-parens': 'off',
|
||||
'@stylistic/comma-dangle': ['error', 'always-multiline'],
|
||||
'@stylistic/dot-location': ['error', 'property'],
|
||||
'@stylistic/function-call-argument-newline': ['error', 'consistent'],
|
||||
'@stylistic/function-paren-newline': 'off',
|
||||
'@stylistic/indent': ['error', 'tab'],
|
||||
'@stylistic/indent-binary-ops': ['error', 'tab'],
|
||||
'@stylistic/max-len': 'off',
|
||||
'@stylistic/multiline-comment-style': 'off',
|
||||
'@stylistic/multiline-ternary': ['error', 'always-multiline'],
|
||||
'@stylistic/newline-per-chained-call': ['error', {ignoreChainWithDepth: 1}],
|
||||
'@stylistic/no-extra-parens': 'off',
|
||||
'@stylistic/no-mixed-operators': 'off',
|
||||
'@stylistic/no-tabs': 'off',
|
||||
'@stylistic/object-property-newline': 'off',
|
||||
'@stylistic/one-var-declaration-per-line': 'off',
|
||||
'@stylistic/operator-linebreak': ['error', 'before'],
|
||||
'@stylistic/padded-blocks': 'off',
|
||||
'@stylistic/quote-props': ['error', 'consistent-as-needed'],
|
||||
'@stylistic/quotes': ['error', 'single'],
|
||||
'curly': 'error',
|
||||
'no-implicit-coercion': 'error',
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'none',
|
||||
ignoreRestSiblings: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['test/**', '**/example.js'],
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'@stylistic/semi': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default config;
|
|
@ -1,24 +1,27 @@
|
|||
'use strict'
|
||||
|
||||
const formatLocationIdentifier = require('./location-identifier')
|
||||
const formatCoord = require('./coord')
|
||||
import {formatLocationIdentifier} from './location-identifier.js';
|
||||
import {formatCoord} from './coord.js';
|
||||
|
||||
const formatAddress = (a) => {
|
||||
if (a.type !== 'location' || !a.latitude || !a.longitude || !a.address) {
|
||||
throw new Error('invalid address')
|
||||
throw new TypeError('invalid address');
|
||||
}
|
||||
|
||||
const data = {
|
||||
A: '2', // address
|
||||
A: '2', // address?
|
||||
O: a.address,
|
||||
X: formatCoord(a.longitude),
|
||||
Y: formatCoord(a.latitude)
|
||||
Y: formatCoord(a.latitude),
|
||||
};
|
||||
if (a.id) {
|
||||
data.L = a.id;
|
||||
}
|
||||
if (a.id) data.L = a.id
|
||||
return {
|
||||
type: 'A', // address
|
||||
name: a.address,
|
||||
lid: formatLocationIdentifier(data)
|
||||
}
|
||||
}
|
||||
lid: formatLocationIdentifier(data),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = formatAddress
|
||||
export {
|
||||
formatAddress,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use strict'
|
||||
const formatCoord = x => Math.round(x * 1000000);
|
||||
|
||||
const formatCoord = x => Math.round(x * 1000000)
|
||||
|
||||
module.exports = formatCoord
|
||||
export {
|
||||
formatCoord,
|
||||
};
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
'use strict'
|
||||
|
||||
const {DateTime} = require('luxon')
|
||||
import {DateTime, IANAZone} from 'luxon';
|
||||
import {luxonIANAZonesByProfile as timezones} from '../lib/luxon-timezones.js';
|
||||
|
||||
// todo: change to `(profile) => (when) => {}`
|
||||
const formatDate = (profile, when) => {
|
||||
return DateTime.fromMillis(+when, {
|
||||
locale: profile.locale,
|
||||
zone: profile.timezone
|
||||
}).toFormat('yyyyMMdd')
|
||||
}
|
||||
let timezone;
|
||||
if (timezones.has(profile)) {
|
||||
timezone = timezones.get(profile);
|
||||
} else {
|
||||
timezone = new IANAZone(profile.timezone);
|
||||
timezones.set(profile, timezone);
|
||||
}
|
||||
|
||||
module.exports = formatDate
|
||||
return DateTime
|
||||
.fromMillis(Number(when), {
|
||||
locale: profile.locale,
|
||||
zone: timezone,
|
||||
})
|
||||
.toFormat('yyyy-MM-dd');
|
||||
};
|
||||
|
||||
export {
|
||||
formatDate,
|
||||
};
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const bike = {type: 'BC', mode: 'INC'}
|
||||
|
||||
const accessibility = {
|
||||
none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'},
|
||||
partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'},
|
||||
complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'}
|
||||
}
|
||||
|
||||
module.exports = {bike, accessibility}
|
|
@ -1,13 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
date: require('./date'),
|
||||
time: require('./time'),
|
||||
filters: require('./filters'),
|
||||
station: require('./station'),
|
||||
address: require('./address'),
|
||||
poi: require('./poi'),
|
||||
location: require('./location'),
|
||||
locationFilter: require('./location-filter'),
|
||||
rectangle: require('./rectangle')
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const formatLocationFilter = (stations, addresses, poi) => {
|
||||
if (stations && addresses && poi) return 'ALL'
|
||||
return (stations ? 'S' : '') + (addresses ? 'A' : '') + (poi ? 'P' : '')
|
||||
}
|
||||
|
||||
module.exports = formatLocationFilter
|
|
@ -1,16 +1,18 @@
|
|||
'use strict'
|
||||
|
||||
const sep = '@'
|
||||
const sep = '@';
|
||||
|
||||
const formatLocationIdentifier = (data) => {
|
||||
let str = ''
|
||||
let str = '';
|
||||
for (let key in data) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) continue
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
str += key + '=' + data[key] + sep // todo: escape, but how?
|
||||
str += key + '=' + data[key] + sep; // todo: escape, but how?
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
module.exports = formatLocationIdentifier
|
||||
export {
|
||||
formatLocationIdentifier,
|
||||
};
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
'use strict'
|
||||
|
||||
const formatLocation = (profile, l) => {
|
||||
if ('string' === typeof l) return profile.formatStation(l)
|
||||
if ('object' === typeof l && !Array.isArray(l)) {
|
||||
if (l.type === 'station') return profile.formatStation(l.id)
|
||||
if ('string' === typeof l.id) return profile.formatPoi(l)
|
||||
if ('string' === typeof l.address) return profile.formatAddress(l)
|
||||
throw new Error('invalid location type: ' + l.type)
|
||||
const formatLocation = (profile, l, name = 'location') => {
|
||||
if ('string' === typeof l) {
|
||||
return profile.formatStation(l);
|
||||
}
|
||||
throw new Error('valid station, address or poi required.')
|
||||
}
|
||||
if ('object' === typeof l && !Array.isArray(l)) {
|
||||
if (l.type === 'station' || l.type === 'stop') {
|
||||
return profile.formatStation(l.id);
|
||||
}
|
||||
if (l.poi) {
|
||||
return profile.formatPoi(l);
|
||||
}
|
||||
if ('string' === typeof l.address) {
|
||||
return profile.formatAddress(l);
|
||||
}
|
||||
if (!l.type) {
|
||||
throw new TypeError(`missing ${name}.type`);
|
||||
}
|
||||
throw new TypeError(`invalid ${name}.type: ${l.type}`);
|
||||
}
|
||||
throw new TypeError(name + ': valid station, address or poi required.');
|
||||
};
|
||||
|
||||
module.exports = formatLocation
|
||||
export {
|
||||
formatLocation,
|
||||
};
|
||||
|
|
65
format/loyalty-cards.js
Normal file
65
format/loyalty-cards.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
const c = {
|
||||
NONE: Symbol('no loyalty card'),
|
||||
BAHNCARD: Symbol('Bahncard'),
|
||||
VORTEILSCARD: Symbol('VorteilsCard'),
|
||||
HALBTAXABO: Symbol('HalbtaxAbo'),
|
||||
VOORDEELURENABO: Symbol('Voordeelurenabo'),
|
||||
SHCARD: Symbol('SH-Card'),
|
||||
GENERALABONNEMENT: Symbol('General-Abonnement'),
|
||||
NL_40: Symbol('NL-40%'),
|
||||
AT_KLIMATICKET: Symbol('AT-KlimaTicket'),
|
||||
};
|
||||
|
||||
const formatLoyaltyCard = (data) => {
|
||||
if (!data) {
|
||||
return {
|
||||
art: 'KEINE_ERMAESSIGUNG',
|
||||
klasse: 'KLASSENLOS',
|
||||
};
|
||||
}
|
||||
const cls = data.class === 1 ? 'KLASSE_1' : 'KLASSE_2';
|
||||
if (data.type.toString() === c.BAHNCARD.toString()) {
|
||||
return {
|
||||
art: 'BAHNCARD' + (data.business ? 'BUSINESS' : '') + data.discount,
|
||||
klasse: cls,
|
||||
};
|
||||
}
|
||||
if (data.type.toString() === c.VORTEILSCARD.toString()) {
|
||||
return {
|
||||
art: 'A-VORTEILSCARD',
|
||||
klasse: 'KLASSENLOS',
|
||||
};
|
||||
}
|
||||
if (data.type.toString() === c.HALBTAXABO.toString()) {
|
||||
return {
|
||||
art: 'CH-HALBTAXABO_OHNE_RAILPLUS',
|
||||
klasse: 'KLASSENLOS',
|
||||
};
|
||||
}
|
||||
if (data.type.toString() === c.GENERALABONNEMENT.toString()) {
|
||||
return {
|
||||
art: 'CH-GENERAL-ABONNEMENT',
|
||||
klasse: cls,
|
||||
};
|
||||
}
|
||||
if (data.type.toString() === c.NL_40.toString()) {
|
||||
return {
|
||||
art: 'NL-40_OHNE_RAILPLUS',
|
||||
klasse: 'KLASSENLOS',
|
||||
};
|
||||
}
|
||||
if (data.type.toString() === c.AT_KLIMATICKET.toString()) {
|
||||
return {
|
||||
art: 'KLIMATICKET_OE',
|
||||
klasse: 'KLASSENLOS',
|
||||
};
|
||||
}
|
||||
return {
|
||||
art: 'KEINE_ERMAESSIGUNG',
|
||||
klasse: 'KLASSENLOS',
|
||||
};
|
||||
};
|
||||
export {
|
||||
c as data,
|
||||
formatLoyaltyCard,
|
||||
};
|
|
@ -1,23 +1,25 @@
|
|||
'use strict'
|
||||
|
||||
const formatLocationIdentifier = require('./location-identifier')
|
||||
const formatCoord = require('./coord')
|
||||
import {formatLocationIdentifier} from './location-identifier.js';
|
||||
import {formatCoord} from './coord.js';
|
||||
|
||||
const formatPoi = (p) => {
|
||||
// todo: use Number.isFinite()!
|
||||
if (p.type !== 'location' || !p.latitude || !p.longitude || !p.id || !p.name) {
|
||||
throw new Error('invalid POI')
|
||||
throw new TypeError('invalid POI');
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'P', // POI
|
||||
name: p.name,
|
||||
lid: formatLocationIdentifier({
|
||||
A: '4', // POI
|
||||
A: '4', // POI?
|
||||
O: p.name,
|
||||
L: p.id,
|
||||
X: formatCoord(p.longitude),
|
||||
Y: formatCoord(p.latitude)
|
||||
})
|
||||
}
|
||||
}
|
||||
Y: formatCoord(p.latitude),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = formatPoi
|
||||
export {
|
||||
formatPoi,
|
||||
};
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const createFormatBitmask = (allProducts) => {
|
||||
const formatBitmask = (products) => {
|
||||
if(Object.keys(products).length === 0) throw new Error('products filter must not be empty')
|
||||
let bitmask = 0
|
||||
for (let product in products) {
|
||||
if (!allProducts[product]) throw new Error('unknown product ' + product)
|
||||
if (products[product] === true) bitmask += allProducts[product].bitmask
|
||||
}
|
||||
return bitmask
|
||||
}
|
||||
return formatBitmask
|
||||
}
|
||||
|
||||
module.exports = createFormatBitmask
|
46
format/products-filter.js
Normal file
46
format/products-filter.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
const isObj = element => element !== null && 'object' === typeof element && !Array.isArray(element);
|
||||
|
||||
const hasProp = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
||||
|
||||
const formatProductsFilter = (ctx, filter, key = 'vendo') => {
|
||||
if (!isObj(filter)) {
|
||||
throw new TypeError('products filter must be an object');
|
||||
}
|
||||
const {profile} = ctx;
|
||||
|
||||
const byProduct = {};
|
||||
const defaultProducts = {};
|
||||
for (let product of profile.products) {
|
||||
byProduct[product.id] = product;
|
||||
defaultProducts[product.id] = product.default;
|
||||
}
|
||||
filter = Object.assign({}, defaultProducts, filter);
|
||||
|
||||
let products = [];
|
||||
let foundDeselected = false;
|
||||
for (let product in filter) {
|
||||
if (!hasProp(filter, product) || filter[product] !== true) {
|
||||
foundDeselected = true;
|
||||
continue;
|
||||
}
|
||||
if (!byProduct[product]) {
|
||||
throw new TypeError('unknown product ' + product);
|
||||
}
|
||||
products.push(byProduct[product][key]);
|
||||
}
|
||||
if (products.length === 0) {
|
||||
throw new Error('no products used');
|
||||
}
|
||||
if (!foundDeselected && key == 'ris') {
|
||||
return undefined;
|
||||
}
|
||||
if (!foundDeselected && key == 'dbnav') {
|
||||
return ['ALL'];
|
||||
}
|
||||
|
||||
return products;
|
||||
};
|
||||
|
||||
export {
|
||||
formatProductsFilter,
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const formatRectangle = (profile, north, west, south, east) => {
|
||||
return {
|
||||
llCrd: {
|
||||
x: profile.formatCoord(west),
|
||||
y: profile.formatCoord(south)
|
||||
},
|
||||
urCrd: {
|
||||
x: profile.formatCoord(east),
|
||||
y: profile.formatCoord(north)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = formatRectangle
|
|
@ -1,15 +1,22 @@
|
|||
'use strict'
|
||||
import {formatLocationIdentifier} from './location-identifier.js';
|
||||
|
||||
const formatLocationIdentifier = require('./location-identifier')
|
||||
const isIBNR = /^\d{6,}$/;
|
||||
|
||||
const formatStation = (id) => {
|
||||
if (!isIBNR.test(id)) {
|
||||
throw new Error('station ID must be an IBNR.');
|
||||
}
|
||||
return {
|
||||
type: 'S', // station
|
||||
// todo: name necessary?
|
||||
lid: formatLocationIdentifier({
|
||||
A: '1', // station
|
||||
L: id
|
||||
})
|
||||
}
|
||||
}
|
||||
A: '1', // station?
|
||||
L: id,
|
||||
// todo: `p` – timestamp of when the ID was obtained
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = formatStation
|
||||
export {
|
||||
formatStation,
|
||||
};
|
||||
|
|
|
@ -1,12 +1,43 @@
|
|||
'use strict'
|
||||
import {DateTime, IANAZone} from 'luxon';
|
||||
import {luxonIANAZonesByProfile as timezones} from '../lib/luxon-timezones.js';
|
||||
|
||||
const {DateTime} = require('luxon')
|
||||
const getTimezone = (profile) => {
|
||||
let timezone;
|
||||
if (timezones.has(profile)) {
|
||||
timezone = timezones.get(profile);
|
||||
} else {
|
||||
timezone = new IANAZone(profile.timezone);
|
||||
timezones.set(profile, timezone);
|
||||
}
|
||||
return timezone;
|
||||
};
|
||||
|
||||
const formatTime = (profile, when, includeOffset = false) => {
|
||||
const timezone = getTimezone(profile);
|
||||
|
||||
return DateTime
|
||||
.fromMillis(Number(when), {
|
||||
locale: profile.locale,
|
||||
zone: timezone,
|
||||
})
|
||||
.startOf('second')
|
||||
.toISO({includeOffset: includeOffset, suppressMilliseconds: true});
|
||||
};
|
||||
|
||||
const formatTimeOfDay = (profile, when) => {
|
||||
const timezone = getTimezone(profile);
|
||||
|
||||
return DateTime
|
||||
.fromMillis(Number(when), {
|
||||
locale: profile.locale,
|
||||
zone: timezone,
|
||||
})
|
||||
.toFormat('HH:mm');
|
||||
};
|
||||
|
||||
export {
|
||||
formatTime,
|
||||
formatTimeOfDay,
|
||||
};
|
||||
|
||||
const formatTime = (profile, when) => {
|
||||
return DateTime.fromMillis(+when, {
|
||||
locale: profile.locale,
|
||||
zone: profile.timezone
|
||||
}).toFormat('HHmmss')
|
||||
}
|
||||
|
||||
module.exports = formatTime
|
||||
|
|
10
format/transfers.js
Normal file
10
format/transfers.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
const formatTransfers = (transfers) => {
|
||||
if (transfers === -1) { // profiles may not accept -1: https://github.com/public-transport/db-vendo-client/issues/5
|
||||
return undefined;
|
||||
}
|
||||
return transfers;
|
||||
};
|
||||
|
||||
export {
|
||||
formatTransfers,
|
||||
};
|
48
format/travellers.js
Normal file
48
format/travellers.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const formatTraveller = ({profile}, ageGroup, age, loyaltyCard) => {
|
||||
const tvlrAgeGroup = age
|
||||
? profile.ageGroupFromAge(age)
|
||||
: ageGroup;
|
||||
let r = {
|
||||
typ: profile.ageGroupLabel[tvlrAgeGroup || profile.ageGroup.ADULT],
|
||||
anzahl: 1,
|
||||
alter: age
|
||||
? [String(age)]
|
||||
: [],
|
||||
ermaessigungen: [profile.formatLoyaltyCard(loyaltyCard)],
|
||||
};
|
||||
return r;
|
||||
};
|
||||
|
||||
const validateArr = (field, length) => {
|
||||
return !field || Array.isArray(field) && field.length == length;
|
||||
};
|
||||
|
||||
const formatTravellers = ({profile, opt}) => {
|
||||
if ('age' in opt && 'ageGroup' in opt) {
|
||||
throw new TypeError(`\
|
||||
opt.age and opt.ageGroup are mutually exclusive.
|
||||
Pass in just opt.age, and the age group will calculated automatically.`);
|
||||
}
|
||||
let travellers = [];
|
||||
if (Array.isArray(opt.loyaltyCard) || Array.isArray(opt.age) || Array.isArray(opt.ageGroup)) {
|
||||
const len = opt.loyaltyCard?.length || opt.age?.length || opt.ageGroup?.length;
|
||||
if (!validateArr(opt.loyaltyCard, len) || !validateArr(opt.age, len) || !validateArr(opt.ageGroup, len)) {
|
||||
throw new TypeError('If any of loyaltyCard, age or ageGroup are an array, all given must be an array of the same length.');
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
travellers.push(formatTraveller({profile}, opt.ageGroup && opt.ageGroup[i], opt.age && opt.age[i], opt.loyaltyCard && opt.loyaltyCard[i]));
|
||||
}
|
||||
} else {
|
||||
travellers.push(formatTraveller({profile}, opt.ageGroup, opt.age, opt.loyaltyCard));
|
||||
}
|
||||
|
||||
const basicCtrfReq = {
|
||||
klasse: opt.firstClass === true ? 'KLASSE_1' : 'KLASSE_2',
|
||||
reisende: travellers,
|
||||
};
|
||||
return basicCtrfReq;
|
||||
};
|
||||
|
||||
export {
|
||||
formatTravellers,
|
||||
};
|
621
index.js
621
index.js
|
@ -1,340 +1,401 @@
|
|||
'use strict'
|
||||
import distance from 'gps-distance';
|
||||
|
||||
const minBy = require('lodash/minBy')
|
||||
const maxBy = require('lodash/maxBy')
|
||||
import {defaultProfile} from './lib/default-profile.js';
|
||||
import {validateProfile} from './lib/validate-profile.js';
|
||||
|
||||
const validateProfile = require('./lib/validate-profile')
|
||||
const defaultProfile = require('./lib/default-profile')
|
||||
const _request = require('./lib/request')
|
||||
const isObj = element => element !== null && 'object' === typeof element && !Array.isArray(element);
|
||||
|
||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o)
|
||||
const isNonEmptyString = str => 'string' === typeof str && str.length > 0
|
||||
// background info: https://github.com/public-transport/hafas-client/issues/286
|
||||
const FORBIDDEN_USER_AGENTS = [
|
||||
'my-awesome-program', // previously used in readme.md, p/*/readme.md & docs/*.md
|
||||
'hafas-client-example', // previously used in p/*/example.js
|
||||
'link-to-your-project-or-email', // now used throughout
|
||||
'db-vendo-client',
|
||||
];
|
||||
|
||||
const createClient = (profile, request = _request) => {
|
||||
profile = Object.assign({}, defaultProfile, profile)
|
||||
validateProfile(profile)
|
||||
const isNonEmptyString = str => 'string' === typeof str && str.length > 0;
|
||||
|
||||
const departures = (station, opt = {}) => {
|
||||
if (isObj(station)) station = profile.formatStation(station.id)
|
||||
else if ('string' === typeof station) station = profile.formatStation(station)
|
||||
else throw new Error('station must be an object or a string.')
|
||||
const validateLocation = (loc, name = 'location') => {
|
||||
if (!isObj(loc)) {
|
||||
throw new TypeError(name + ' must be an object.');
|
||||
} else if (loc.type !== 'location') {
|
||||
throw new TypeError('invalid location object.');
|
||||
} else if ('number' !== typeof loc.latitude) {
|
||||
throw new TypeError(name + '.latitude must be a number.');
|
||||
} else if ('number' !== typeof loc.longitude) {
|
||||
throw new TypeError(name + '.longitude must be a number.');
|
||||
}
|
||||
};
|
||||
|
||||
opt = Object.assign({
|
||||
direction: null, // only show departures heading to this station
|
||||
duration: 10 // show departures for the next n minutes
|
||||
}, opt)
|
||||
opt.when = opt.when || new Date()
|
||||
const products = profile.formatProducts(opt.products || {})
|
||||
const loadEnrichedStationData = async (profile) => {
|
||||
const dbHafasStations = await import('db-hafas-stations');
|
||||
const items = {};
|
||||
for await (const station of dbHafasStations.readFullStations()) {
|
||||
items[station.id] = station;
|
||||
items[station.name] = station;
|
||||
}
|
||||
if (profile.DEBUG) {
|
||||
console.log('Loaded station index.');
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
const dir = opt.direction ? profile.formatStation(opt.direction) : null
|
||||
return request(profile, {
|
||||
meth: 'StationBoard',
|
||||
req: {
|
||||
type: 'DEP',
|
||||
date: profile.formatDate(profile, opt.when),
|
||||
time: profile.formatTime(profile, opt.when),
|
||||
stbLoc: station,
|
||||
dirLoc: dir,
|
||||
jnyFltrL: [products],
|
||||
dur: opt.duration,
|
||||
getPasslist: false
|
||||
}
|
||||
})
|
||||
.then((d) => {
|
||||
if (!Array.isArray(d.jnyL)) return [] // todo: throw err?
|
||||
const parse = profile.parseDeparture(profile, d.locations, d.lines, d.remarks)
|
||||
return d.jnyL.map(parse)
|
||||
.sort((a, b) => new Date(a.when) - new Date(b.when))
|
||||
})
|
||||
const applyEnrichedStationData = async (ctx, shouldLoadEnrichedStationData) => {
|
||||
const {profile, common} = ctx;
|
||||
if (shouldLoadEnrichedStationData && !common.locations) {
|
||||
const locations = await loadEnrichedStationData(profile);
|
||||
common.locations = locations;
|
||||
}
|
||||
};
|
||||
|
||||
const createClient = (profile, userAgent, opt = {}) => {
|
||||
profile = Object.assign({}, defaultProfile, profile);
|
||||
validateProfile(profile);
|
||||
const common = {};
|
||||
let shouldLoadEnrichedStationData = false;
|
||||
if (typeof opt.enrichStations === 'function') {
|
||||
profile.enrichStation = opt.enrichStations;
|
||||
} else if (opt.enrichStations !== false) {
|
||||
shouldLoadEnrichedStationData = true;
|
||||
}
|
||||
|
||||
const journeys = (from, to, opt = {}) => {
|
||||
from = profile.formatLocation(profile, from)
|
||||
to = profile.formatLocation(profile, to)
|
||||
if ('string' !== typeof userAgent) {
|
||||
throw new TypeError('userAgent must be a string');
|
||||
}
|
||||
if (FORBIDDEN_USER_AGENTS.includes(userAgent.toLowerCase())) {
|
||||
throw new TypeError(`userAgent should tell the API operators how to contact you. If you have copied "${userAgent}" value from the documentation, please adapt it.`);
|
||||
}
|
||||
|
||||
if (('earlierThan' in opt) && ('laterThan' in opt)) {
|
||||
throw new Error('opt.laterThan and opt.laterThan are mutually exclusive.')
|
||||
const _stationBoard = async (station, type, resultsField, parse, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
if (isObj(station) && station.id) {
|
||||
station = station.id;
|
||||
} else if ('string' !== typeof station) {
|
||||
throw new TypeError('station must be an object or a string.');
|
||||
}
|
||||
let journeysRef = null
|
||||
|
||||
if ('string' !== typeof type || !type) {
|
||||
throw new TypeError('type must be a non-empty string.');
|
||||
}
|
||||
|
||||
if (!profile.departuresGetPasslist && opt.stopovers) {
|
||||
throw new Error('opt.stopovers is not supported by this endpoint');
|
||||
}
|
||||
if (!profile.departuresStbFltrEquiv && 'includeRelatedStations' in opt) {
|
||||
throw new Error('opt.includeRelatedStations is not supported by this endpoint');
|
||||
}
|
||||
|
||||
opt = Object.assign({
|
||||
// todo: for arrivals(), this is actually a station it *has already* stopped by
|
||||
direction: null, // only show departures stopping by this station
|
||||
line: null, // filter by line ID
|
||||
duration: 10, // show departures for the next n minutes
|
||||
results: null, // max. number of results; `null` means "whatever HAFAS wants"
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
linesOfStops: false, // parse & expose lines at the stop/station?
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
stopovers: false, // fetch & parse previous/next stopovers?
|
||||
// departures at related stations
|
||||
// e.g. those that belong together on the metro map.
|
||||
includeRelatedStations: true,
|
||||
moreStops: null, // also include departures/arrivals for array of up to nine additional station evaNumbers (not supported with dbnav and dbweb)
|
||||
}, opt);
|
||||
opt.when = new Date(opt.when || Date.now());
|
||||
if (Number.isNaN(Number(opt.when))) {
|
||||
throw new Error('opt.when is invalid');
|
||||
}
|
||||
|
||||
const req = profile.formatStationBoardReq({profile, opt}, station, resultsField);
|
||||
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
|
||||
const ctx = {profile, opt, common, res};
|
||||
let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries.flat())
|
||||
.map(res => parse(ctx, res)); // TODO sort?, slice
|
||||
|
||||
if (!opt.includeRelatedStations) {
|
||||
results = results.filter(r => !r.stop?.id || r.stop.id == station);
|
||||
}
|
||||
if (opt.direction) {
|
||||
results = results.filter(r => !r.nextStopovers || r.nextStopovers.find(s => s.stop?.id == opt.direction || s.stop?.name == opt.direction));
|
||||
}
|
||||
return {
|
||||
[resultsField]: results,
|
||||
realtimeDataUpdatedAt: null, // TODO
|
||||
};
|
||||
};
|
||||
|
||||
const departures = async (station, opt = {}) => {
|
||||
return await _stationBoard(station, 'DEP', 'departures', profile.parseDeparture, opt);
|
||||
};
|
||||
const arrivals = async (station, opt = {}) => {
|
||||
return await _stationBoard(station, 'ARR', 'arrivals', profile.parseArrival, opt);
|
||||
};
|
||||
|
||||
const journeys = async (from, to, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
if ('earlierThan' in opt && 'laterThan' in opt) {
|
||||
throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.');
|
||||
}
|
||||
if ('departure' in opt && 'arrival' in opt) {
|
||||
throw new TypeError('opt.departure and opt.arrival are mutually exclusive.');
|
||||
}
|
||||
let journeysRef = null;
|
||||
if ('earlierThan' in opt) {
|
||||
if (!isNonEmptyString(opt.earlierThan)) {
|
||||
throw new Error('opt.earlierThan must be a non-empty string.')
|
||||
throw new TypeError('opt.earlierThan must be a non-empty string.');
|
||||
}
|
||||
if ('when' in opt) {
|
||||
throw new Error('opt.earlierThan and opt.when are mutually exclusive.')
|
||||
if ('departure' in opt || 'arrival' in opt) {
|
||||
throw new TypeError('opt.earlierThan and opt.departure/opt.arrival are mutually exclusive.');
|
||||
}
|
||||
journeysRef = opt.earlierThan
|
||||
journeysRef = opt.earlierThan;
|
||||
}
|
||||
if ('laterThan' in opt) {
|
||||
if (!isNonEmptyString(opt.laterThan)) {
|
||||
throw new Error('opt.laterThan must be a non-empty string.')
|
||||
throw new TypeError('opt.laterThan must be a non-empty string.');
|
||||
}
|
||||
if ('when' in opt) {
|
||||
throw new Error('opt.laterThan and opt.when are mutually exclusive.')
|
||||
if ('departure' in opt || 'arrival' in opt) {
|
||||
throw new TypeError('opt.laterThan and opt.departure/opt.arrival are mutually exclusive.');
|
||||
}
|
||||
journeysRef = opt.laterThan
|
||||
journeysRef = opt.laterThan;
|
||||
}
|
||||
|
||||
opt = Object.assign({
|
||||
results: 5, // how many journeys?
|
||||
results: null, // number of journeys – `null` means "whatever HAFAS returns"
|
||||
via: null, // let journeys pass this station?
|
||||
passedStations: false, // return stations on the way?
|
||||
transfers: 5, // maximum of 5 transfers
|
||||
stopovers: false, // return stations on the way?
|
||||
transfers: null, // maximum nr of transfers
|
||||
transferTime: 0, // minimum time for a single transfer in minutes
|
||||
// todo: does this work with every endpoint?
|
||||
accessibility: 'none', // 'none', 'partial' or 'complete'
|
||||
bike: false, // only bike-friendly journeys
|
||||
walkingSpeed: 'normal', // 'slow', 'normal', 'fast'
|
||||
// Consider walking to nearby stations at the beginning of a journey?
|
||||
startWithWalking: true,
|
||||
tickets: false, // return tickets?
|
||||
}, opt)
|
||||
if (opt.via) opt.via = profile.formatLocation(profile, opt.via)
|
||||
opt.when = opt.when || new Date()
|
||||
polylines: false, // return leg shapes?
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
scheduledDays: false, // parse & expose dates each journey is valid on?
|
||||
notOnlyFastRoutes: false, // if true, also show routes that are mathematically non-optimal
|
||||
bestprice: false, // search for lowest prices across the entire day
|
||||
deutschlandTicketDiscount: false,
|
||||
deutschlandTicketConnectionsOnly: false,
|
||||
}, opt);
|
||||
|
||||
const filters = [
|
||||
profile.formatProducts(opt.products || {})
|
||||
]
|
||||
if (
|
||||
opt.accessibility &&
|
||||
profile.filters &&
|
||||
profile.filters.accessibility &&
|
||||
profile.filters.accessibility[opt.accessibility]
|
||||
) {
|
||||
filters.push(profile.filters.accessibility[opt.accessibility])
|
||||
if (opt.when !== undefined) {
|
||||
throw new Error('opt.when is not supported anymore. Use opt.departure/opt.arrival.');
|
||||
}
|
||||
|
||||
// With protocol version `1.16`, the VBB endpoint fails with
|
||||
// `CGI_READ_FAILED` if you pass `numF`, the parameter for the number
|
||||
// of results. To circumvent this, we loop here, collecting journeys
|
||||
// until we have enough.
|
||||
// see https://github.com/derhuerst/hafas-client/pull/23#issuecomment-370246163
|
||||
// todo: check if `numF` is supported again, revert this change
|
||||
const journeys = []
|
||||
const more = (when, journeysRef) => {
|
||||
const query = {
|
||||
outDate: profile.formatDate(profile, when),
|
||||
outTime: profile.formatTime(profile, when),
|
||||
ctxScr: journeysRef,
|
||||
getPasslist: !!opt.passedStations,
|
||||
maxChg: opt.transfers,
|
||||
minChgTime: opt.transferTime,
|
||||
depLocL: [from],
|
||||
viaLocL: opt.via ? [{loc: opt.via}] : null,
|
||||
arrLocL: [to],
|
||||
jnyFltrL: filters,
|
||||
getTariff: !!opt.tickets,
|
||||
|
||||
// todo: what is req.gisFltrL?
|
||||
getPT: true, // todo: what is this?
|
||||
outFrwd: true, // todo: what is this?
|
||||
getIV: false, // todo: walk & bike as alternatives?
|
||||
getPolyline: false // todo: shape for displaying on a map?
|
||||
let when = new Date(), outFrwd = true;
|
||||
if (opt.departure !== undefined && opt.departure !== null) {
|
||||
when = new Date(opt.departure);
|
||||
if (Number.isNaN(Number(when))) {
|
||||
throw new TypeError('opt.departure is invalid');
|
||||
}
|
||||
if (profile.journeysNumF) query.numF = opt.results
|
||||
|
||||
return request(profile, {
|
||||
cfg: {polyEnc: 'GPA'},
|
||||
meth: 'TripSearch',
|
||||
req: profile.transformJourneysQuery(query, opt)
|
||||
})
|
||||
.then((d) => {
|
||||
if (!Array.isArray(d.outConL)) return []
|
||||
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
|
||||
if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB
|
||||
|
||||
let latestDep = -Infinity
|
||||
for (let j of d.outConL) {
|
||||
j = parse(j)
|
||||
journeys.push(j)
|
||||
|
||||
if (journeys.length === opt.results) { // collected enough
|
||||
journeys.laterRef = d.outCtxScrF
|
||||
return journeys
|
||||
}
|
||||
const dep = +new Date(j.departure)
|
||||
if (dep > latestDep) latestDep = dep
|
||||
}
|
||||
|
||||
const when = new Date(latestDep)
|
||||
return more(when, d.outCtxScrF) // otherwise continue
|
||||
})
|
||||
} else if (opt.arrival !== undefined && opt.arrival !== null) {
|
||||
if (!profile.journeysOutFrwd) {
|
||||
throw new Error('opt.arrival is unsupported');
|
||||
}
|
||||
when = new Date(opt.arrival);
|
||||
if (Number.isNaN(Number(when))) {
|
||||
throw new TypeError('opt.arrival is invalid');
|
||||
}
|
||||
outFrwd = false;
|
||||
}
|
||||
|
||||
return more(opt.when, journeysRef)
|
||||
}
|
||||
const req = profile.formatJourneysReq({profile, opt}, from, to, when, outFrwd, journeysRef);
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
const ctx = {profile, opt, common, res};
|
||||
if (opt.bestprice) {
|
||||
res.verbindungen = (res.intervalle || res.tagesbestPreisIntervalle).flatMap(i => i.verbindungen.map(v => ({...v, ...v.verbindung})));
|
||||
}
|
||||
const verbindungen = Number.isInteger(opt.results) && opt.results != 3 ? res.verbindungen.slice(0, opt.results) : res.verbindungen; // TODO remove default from hafas-rest-api
|
||||
const journeys = verbindungen
|
||||
.map(j => profile.parseJourney(ctx, j));
|
||||
if (opt.bestprice) {
|
||||
journeys.sort((a, b) => a.price?.amount - b.price?.amount);
|
||||
}
|
||||
|
||||
return {
|
||||
earlierRef: res.verbindungReference?.earlier || res.frueherContext || null,
|
||||
laterRef: res.verbindungReference?.later || res.spaeterContext || null,
|
||||
journeys,
|
||||
realtimeDataUpdatedAt: null, // TODO
|
||||
};
|
||||
};
|
||||
|
||||
const refreshJourney = async (refreshToken, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
|
||||
if ('string' !== typeof refreshToken || !refreshToken) {
|
||||
throw new TypeError('refreshToken must be a non-empty string.');
|
||||
}
|
||||
|
||||
opt = Object.assign({
|
||||
stopovers: false, // return stations on the way?
|
||||
tickets: false, // return tickets?
|
||||
polylines: false, // return leg shapes? (not supported by all endpoints)
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
scheduledDays: false, // parse & expose dates the journey is valid on?
|
||||
deutschlandTicketDiscount: false,
|
||||
deutschlandTicketConnectionsOnly: false,
|
||||
}, opt);
|
||||
|
||||
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
|
||||
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
const ctx = {profile, opt, common, res};
|
||||
|
||||
return {
|
||||
journey: profile.parseJourney(ctx, res.verbindungen && res.verbindungen[0] || res),
|
||||
realtimeDataUpdatedAt: null, // TODO
|
||||
};
|
||||
};
|
||||
|
||||
const locations = async (query, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
|
||||
const locations = (query, opt = {}) => {
|
||||
if (!isNonEmptyString(query)) {
|
||||
throw new Error('query must be a non-empty string.')
|
||||
throw new TypeError('query must be a non-empty string.');
|
||||
}
|
||||
opt = Object.assign({
|
||||
fuzzy: true, // find only exact matches?
|
||||
results: 10, // how many search results?
|
||||
stations: true,
|
||||
results: 5, // how many search results?
|
||||
stops: true, // return stops/stations?
|
||||
addresses: true,
|
||||
poi: true // points of interest
|
||||
}, opt)
|
||||
poi: true, // points of interest
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
linesOfStops: false, // parse & expose lines at each stop/station?
|
||||
}, opt);
|
||||
const req = profile.formatLocationsReq({profile, opt}, query);
|
||||
|
||||
const f = profile.formatLocationFilter(opt.stations, opt.addresses, opt.poi)
|
||||
return request(profile, {
|
||||
cfg: {polyEnc: 'GPA'},
|
||||
meth: 'LocMatch',
|
||||
req: {input: {
|
||||
loc: {
|
||||
type: f,
|
||||
name: opt.fuzzy ? query + '?' : query
|
||||
},
|
||||
maxLoc: opt.results,
|
||||
field: 'S' // todo: what is this?
|
||||
}}
|
||||
})
|
||||
.then((d) => {
|
||||
if (!d.match || !Array.isArray(d.match.locL)) return []
|
||||
const parse = profile.parseLocation
|
||||
return d.match.locL.map(loc => parse(profile, loc, d.lines))
|
||||
})
|
||||
}
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
|
||||
const location = (station) => {
|
||||
if ('object' === typeof station) station = profile.formatStation(station.id)
|
||||
else if ('string' === typeof station) station = profile.formatStation(station)
|
||||
else throw new Error('station must be an object or a string.')
|
||||
const ctx = {profile, opt, common, res};
|
||||
const results = res.map(loc => profile.parseLocation(ctx, loc));
|
||||
|
||||
return request(profile, {
|
||||
meth: 'LocDetails',
|
||||
req: {
|
||||
locL: [station]
|
||||
}
|
||||
})
|
||||
.then((d) => {
|
||||
if (!d || !Array.isArray(d.locL) || !d.locL[0]) {
|
||||
// todo: proper stack trace?
|
||||
throw new Error('invalid response')
|
||||
}
|
||||
return profile.parseLocation(profile, d.locL[0], d.lines)
|
||||
})
|
||||
}
|
||||
return Number.isInteger(opt.results)
|
||||
? results.slice(0, opt.results)
|
||||
: results;
|
||||
};
|
||||
|
||||
const nearby = (location, opt = {}) => {
|
||||
if (!isObj(location)) {
|
||||
throw new Error('location must be an object.')
|
||||
} else if (location.type !== 'location') {
|
||||
throw new Error('invalid location object.')
|
||||
} else if ('number' !== typeof location.latitude) {
|
||||
throw new Error('location.latitude must be a number.')
|
||||
} else if ('number' !== typeof location.longitude) {
|
||||
throw new Error('location.longitude must be a number.')
|
||||
const stop = async (stop, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
|
||||
if (isObj(stop) && stop.id) {
|
||||
stop = stop.id;
|
||||
} else if ('string' !== typeof stop) {
|
||||
throw new TypeError('stop must be an object or a string.');
|
||||
}
|
||||
|
||||
opt = Object.assign({
|
||||
linesOfStops: false, // parse & expose lines at the stop/station?
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
}, opt);
|
||||
|
||||
const req = profile.formatStopReq({profile, opt}, stop);
|
||||
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
const ctx = {profile, opt, res, common};
|
||||
return profile.parseStop(ctx, res, stop);
|
||||
};
|
||||
|
||||
const nearby = async (location, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
|
||||
validateLocation(location, 'location');
|
||||
|
||||
opt = Object.assign({
|
||||
results: 8, // maximum number of results
|
||||
distance: null, // maximum walking distance in meters
|
||||
poi: false, // return points of interest?
|
||||
stations: true, // return stations?
|
||||
}, opt)
|
||||
stops: true, // return stops/stations?
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
linesOfStops: false, // parse & expose lines at each stop/station?
|
||||
}, opt);
|
||||
|
||||
return request(profile, {
|
||||
cfg: {polyEnc: 'GPA'},
|
||||
meth: 'LocGeoPos',
|
||||
req: {
|
||||
ring: {
|
||||
cCrd: {
|
||||
x: profile.formatCoord(location.longitude),
|
||||
y: profile.formatCoord(location.latitude)
|
||||
},
|
||||
maxDist: opt.distance || -1,
|
||||
minDist: 0
|
||||
},
|
||||
getPOIs: !!opt.poi,
|
||||
getStops: !!opt.stations,
|
||||
maxLoc: opt.results
|
||||
const req = profile.formatNearbyReq({profile, opt}, location);
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
|
||||
const ctx = {profile, opt, common, res};
|
||||
const results = res.map(loc => {
|
||||
const res = profile.parseLocation(ctx, loc);
|
||||
if (res.latitude || res.location?.latitude) {
|
||||
res.distance = Math.round(distance(location.latitude, location.longitude, res.latitude || res.location?.latitude, res.longitude || res.location?.longitude) * 1000);
|
||||
}
|
||||
})
|
||||
.then((d) => {
|
||||
if (!Array.isArray(d.locL)) return []
|
||||
const parse = profile.parseNearby
|
||||
return d.locL.map(loc => parse(profile, loc))
|
||||
})
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
const journeyLeg = (ref, lineName, opt = {}) => {
|
||||
if (!isNonEmptyString(ref)) {
|
||||
throw new Error('ref must be a non-empty string.')
|
||||
}
|
||||
if (!isNonEmptyString(lineName)) {
|
||||
throw new Error('lineName must be a non-empty string.')
|
||||
return Number.isInteger(opt.results)
|
||||
? results.slice(0, opt.results)
|
||||
: results;
|
||||
};
|
||||
|
||||
const trip = async (id, opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
|
||||
if (!isNonEmptyString(id)) {
|
||||
throw new TypeError('id must be a non-empty string.');
|
||||
}
|
||||
opt = Object.assign({
|
||||
passedStations: true // return stations on the way?
|
||||
}, opt)
|
||||
opt.when = opt.when || new Date()
|
||||
stopovers: true, // return stations on the way?
|
||||
polyline: false, // return a track shape?
|
||||
subStops: true, // parse & expose sub-stops of stations?
|
||||
entrances: true, // parse & expose entrances of stops/stations?
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
scheduledDays: false, // parse & expose dates trip is valid on?
|
||||
}, opt);
|
||||
|
||||
return request(profile, {
|
||||
cfg: {polyEnc: 'GPA'},
|
||||
meth: 'JourneyDetails',
|
||||
req: {
|
||||
jid: ref,
|
||||
name: lineName,
|
||||
date: profile.formatDate(profile, opt.when)
|
||||
}
|
||||
})
|
||||
.then((d) => {
|
||||
const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks)
|
||||
const req = profile.formatTripReq({profile, opt}, id);
|
||||
|
||||
const leg = { // pretend the leg is contained in a journey
|
||||
type: 'JNY',
|
||||
dep: minBy(d.journey.stopL, 'idx'),
|
||||
arr: maxBy(d.journey.stopL, 'idx'),
|
||||
jny: d.journey
|
||||
}
|
||||
return parse(d.journey, leg, !!opt.passedStations)
|
||||
})
|
||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
const ctx = {profile, opt, common, res};
|
||||
|
||||
const trip = profile.parseTrip(ctx, res, id);
|
||||
|
||||
return {
|
||||
trip,
|
||||
realtimeDataUpdatedAt: null, // TODO
|
||||
};
|
||||
};
|
||||
|
||||
// todo [breaking]: rename to trips()?
|
||||
const tripsByName = async (_lineNameOrFahrtNr = '*', _opt = {}) => {
|
||||
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
|
||||
|
||||
throw new Error('not implemented');
|
||||
};
|
||||
|
||||
const client = {
|
||||
departures,
|
||||
arrivals,
|
||||
journeys,
|
||||
locations,
|
||||
stop,
|
||||
nearby,
|
||||
};
|
||||
if (profile.trip) {
|
||||
client.trip = trip;
|
||||
}
|
||||
|
||||
const radar = (north, west, south, east, opt) => {
|
||||
if ('number' !== typeof north) throw new Error('north must be a number.')
|
||||
if ('number' !== typeof west) throw new Error('west must be a number.')
|
||||
if ('number' !== typeof south) throw new Error('south must be a number.')
|
||||
if ('number' !== typeof east) throw new Error('east must be a number.')
|
||||
|
||||
opt = Object.assign({
|
||||
results: 256, // maximum number of vehicles
|
||||
duration: 30, // compute frames for the next n seconds
|
||||
frames: 3, // nr of frames to compute
|
||||
products: null // optionally an object of booleans
|
||||
}, opt || {})
|
||||
opt.when = opt.when || new Date()
|
||||
|
||||
const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000
|
||||
return request(profile, {
|
||||
meth: 'JourneyGeoPos',
|
||||
req: {
|
||||
maxJny: opt.results,
|
||||
onlyRT: false, // todo: does this mean "only realtime"?
|
||||
date: profile.formatDate(profile, opt.when),
|
||||
time: profile.formatTime(profile, opt.when),
|
||||
// todo: would a ring work here as well?
|
||||
rect: profile.formatRectangle(profile, north, west, south, east),
|
||||
perSize: opt.duration * 1000,
|
||||
perStep: Math.round(durationPerStep),
|
||||
ageOfReport: true, // todo: what is this?
|
||||
jnyFltrL: [
|
||||
profile.formatProducts(opt.products || {})
|
||||
],
|
||||
trainPosMode: 'CALC' // todo: what is this? what about realtime?
|
||||
}
|
||||
})
|
||||
.then((d) => {
|
||||
if (!Array.isArray(d.jnyL)) return []
|
||||
|
||||
const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks)
|
||||
return d.jnyL.map(parse)
|
||||
})
|
||||
if (profile.refreshJourney) {
|
||||
client.refreshJourney = refreshJourney;
|
||||
}
|
||||
if (profile.tripsByName) {
|
||||
client.tripsByName = tripsByName;
|
||||
}
|
||||
Object.defineProperty(client, 'profile', {value: profile});
|
||||
return client;
|
||||
};
|
||||
|
||||
const client = {departures, journeys, locations, location, nearby}
|
||||
if (profile.journeyLeg) client.journeyLeg = journeyLeg
|
||||
if (profile.radar) client.radar = radar
|
||||
Object.defineProperty(client, 'profile', {value: profile})
|
||||
return client
|
||||
}
|
||||
|
||||
module.exports = createClient
|
||||
export {
|
||||
createClient,
|
||||
loadEnrichedStationData,
|
||||
};
|
||||
|
|
48
lib/age-group.js
Normal file
48
lib/age-group.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const ageGroup = {
|
||||
BABY: 'B',
|
||||
CHILD: 'K',
|
||||
YOUNG: 'Y',
|
||||
ADULT: 'E',
|
||||
SENIOR: 'S',
|
||||
upperBoundOf: {
|
||||
BABY: 6,
|
||||
CHILD: 15,
|
||||
YOUNG: 27,
|
||||
ADULT: 65,
|
||||
SENIOR: Infinity,
|
||||
},
|
||||
};
|
||||
|
||||
const ageGroupLabel = {
|
||||
B: 'KLEINKIND',
|
||||
K: 'FAMILIENKIND',
|
||||
Y: 'JUGENDLICHER',
|
||||
E: 'ERWACHSENER',
|
||||
S: 'SENIOR',
|
||||
};
|
||||
|
||||
const ageGroupFromAge = (age) => {
|
||||
const {upperBoundOf} = ageGroup;
|
||||
if (age < upperBoundOf.BABY) {
|
||||
return ageGroup.BABY;
|
||||
}
|
||||
if (age < upperBoundOf.CHILD) {
|
||||
return ageGroup.CHILD;
|
||||
}
|
||||
if (age < upperBoundOf.YOUNG) {
|
||||
return ageGroup.YOUNG;
|
||||
}
|
||||
if (age < upperBoundOf.ADULT) {
|
||||
return ageGroup.ADULT;
|
||||
}
|
||||
if (age < upperBoundOf.SENIOR) {
|
||||
return ageGroup.SENIOR;
|
||||
}
|
||||
throw new TypeError(`Invalid age '${age}'`);
|
||||
};
|
||||
|
||||
export {
|
||||
ageGroup,
|
||||
ageGroupLabel,
|
||||
ageGroupFromAge,
|
||||
};
|
109
lib/api-parsers.js
Normal file
109
lib/api-parsers.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {data as cards} from '../format/loyalty-cards.js';
|
||||
import {parseBoolean, parseInteger, parseArrayOfStrings} from 'hafas-rest-api/lib/parse.js';
|
||||
|
||||
const typesByName = new Map([
|
||||
['bahncard-1st-25', {type: cards.BAHNCARD, discount: 25, class: 1}],
|
||||
['bahncard-2nd-25', {type: cards.BAHNCARD, discount: 25, class: 2}],
|
||||
['bahncard-1st-50', {type: cards.BAHNCARD, discount: 50, class: 1}],
|
||||
['bahncard-2nd-50', {type: cards.BAHNCARD, discount: 50, class: 2}],
|
||||
['bahncard-1st-100', {type: cards.BAHNCARD, discount: 100, class: 1}],
|
||||
['bahncard-2nd-100', {type: cards.BAHNCARD, discount: 100, class: 2}],
|
||||
['vorteilscard', {type: cards.VORTEILSCARD}],
|
||||
['halbtaxabo-railplus', {type: cards.HALBTAXABO}],
|
||||
['halbtaxabo', {type: cards.HALBTAXABO}],
|
||||
['voordeelurenabo-railplus', {type: cards.VOORDEELURENABO}],
|
||||
['voordeelurenabo', {type: cards.VOORDEELURENABO}],
|
||||
['shcard', {type: cards.SHCARD}],
|
||||
['generalabonnement-1st', {type: cards.GENERALABONNEMENT, class: 1}],
|
||||
['generalabonnement-2nd', {type: cards.GENERALABONNEMENT, class: 2}],
|
||||
['generalabonnement', {type: cards.GENERALABONNEMENT}],
|
||||
['nl-40', {type: cards.NL_40}],
|
||||
['at-klimaticket', {type: cards.AT_KLIMATICKET}],
|
||||
]);
|
||||
const types = Array.from(typesByName.keys());
|
||||
|
||||
const parseLoyaltyCard = (key, val) => {
|
||||
if (typesByName.has(val)) {
|
||||
return typesByName.get(val);
|
||||
}
|
||||
if (!val) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(key + ' must be one of ' + types.join(', '));
|
||||
};
|
||||
|
||||
const parseArrayOr = (parseEntry) => {
|
||||
return (key, val) => {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(e => parseEntry(key, e));
|
||||
}
|
||||
return parseEntry(key, val);
|
||||
};
|
||||
};
|
||||
|
||||
const mapRouteParsers = (route, parsers) => {
|
||||
if (route.includes('journey')) {
|
||||
return {
|
||||
...parsers,
|
||||
firstClass: {
|
||||
description: 'Search for first-class options?',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
parse: parseBoolean,
|
||||
},
|
||||
loyaltyCard: {
|
||||
description: 'Type of loyalty card in use.',
|
||||
type: 'string',
|
||||
enum: types,
|
||||
defaultStr: '*none*',
|
||||
parse: parseArrayOr(parseLoyaltyCard),
|
||||
},
|
||||
age: {
|
||||
description: 'Age of traveller',
|
||||
type: 'integer',
|
||||
defaultStr: '*adult*',
|
||||
parse: parseArrayOr(parseInteger),
|
||||
},
|
||||
notOnlyFastRoutes: {
|
||||
description: 'If true, also show routes that are mathematically non-optimal',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
parse: parseBoolean,
|
||||
},
|
||||
bestprice: {
|
||||
description: 'Search for lowest prices across the entire day',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
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')) {
|
||||
return {
|
||||
...parsers,
|
||||
moreStops: {
|
||||
description: 'Also include departures/arrivals for up to nine comma-separated station evaNumbers (not supported with dbnav and dbweb)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
parse: parseArrayOfStrings,
|
||||
},
|
||||
};
|
||||
}
|
||||
return parsers;
|
||||
};
|
||||
|
||||
export {
|
||||
mapRouteParsers,
|
||||
};
|
|
@ -1,67 +1,126 @@
|
|||
'use strict'
|
||||
import {request} from '../lib/request.js';
|
||||
import {products} from '../lib/products.js';
|
||||
import {ageGroup, ageGroupFromAge, ageGroupLabel} from './age-group.js';
|
||||
|
||||
const parseDateTime = require('../parse/date-time')
|
||||
const parseDeparture = require('../parse/departure')
|
||||
const parseJourneyLeg = require('../parse/journey-leg')
|
||||
const parseJourney = require('../parse/journey')
|
||||
const parseLine = require('../parse/line')
|
||||
const parseLocation = require('../parse/location')
|
||||
const parseMovement = require('../parse/movement')
|
||||
const parseNearby = require('../parse/nearby')
|
||||
const parseOperator = require('../parse/operator')
|
||||
const parseRemark = require('../parse/remark')
|
||||
const parseStopover = require('../parse/stopover')
|
||||
import {parseDateTime} from '../parse/date-time.js';
|
||||
import {parsePlatform} from '../parse/platform.js';
|
||||
import {parseProducts} from '../parse/products.js';
|
||||
import {parseWhen} from '../parse/when.js';
|
||||
import {parseDeparture} from '../parse/departure.js';
|
||||
import {parseArrival} from '../parse/arrival.js';
|
||||
import {parseTrip} from '../parse/trip.js';
|
||||
import {parseJourneyLeg} from '../parse/journey-leg.js';
|
||||
import {parseJourney} from '../parse/journey.js';
|
||||
import {parseLine} from '../parse/line.js';
|
||||
import {parseLocation, enrichStation} from '../parse/location.js';
|
||||
import {parsePolyline} from '../parse/polyline.js';
|
||||
import {parseOperator} from '../parse/operator.js';
|
||||
import {parseRemarks, parseCancelled} from '../parse/remarks.js';
|
||||
import {parseStopover} from '../parse/stopover.js';
|
||||
import {parseLoadFactor, parseArrOrDepWithLoadFactor} from '../parse/load-factor.js';
|
||||
import {parseHintByCode} from '../parse/hints-by-code.js';
|
||||
import {parseTickets, parsePrice} from '../parse/tickets.js';
|
||||
|
||||
const formatAddress = require('../format/address')
|
||||
const formatCoord = require('../format/coord')
|
||||
const formatDate = require('../format/date')
|
||||
const formatLocationFilter = require('../format/location-filter')
|
||||
const formatPoi = require('../format/poi')
|
||||
const formatStation = require('../format/station')
|
||||
const formatTime = require('../format/time')
|
||||
const formatLocation = require('../format/location')
|
||||
const formatRectangle = require('../format/rectangle')
|
||||
const filters = require('../format/filters')
|
||||
import {formatAddress} from '../format/address.js';
|
||||
import {formatCoord} from '../format/coord.js';
|
||||
import {formatDate} from '../format/date.js';
|
||||
import {formatProductsFilter} from '../format/products-filter.js';
|
||||
import {formatPoi} from '../format/poi.js';
|
||||
import {formatStation} from '../format/station.js';
|
||||
import {formatTime, formatTimeOfDay} from '../format/time.js';
|
||||
import {formatLocation} from '../format/location.js';
|
||||
import {formatTravellers} from '../format/travellers.js';
|
||||
import {formatLoyaltyCard} from '../format/loyalty-cards.js';
|
||||
import {formatTransfers} from '../format/transfers.js';
|
||||
|
||||
const id = x => x
|
||||
const DEBUG = (/(^|,)hafas-client(,|$)/).test(typeof process !== 'undefined' ? process.env.DEBUG || '' : '');
|
||||
const logRequest = DEBUG
|
||||
? (_, req, reqId) => console.error(String(req.body))
|
||||
: () => { };
|
||||
const logResponse = DEBUG
|
||||
? (_, res, body, reqId) => console.error(body)
|
||||
: () => { };
|
||||
|
||||
const id = (_ctx, x) => x;
|
||||
const notImplemented = (_ctx, _x) => {
|
||||
throw new Error('NotImplemented');
|
||||
};
|
||||
|
||||
const defaultProfile = {
|
||||
salt: null,
|
||||
addChecksum: false,
|
||||
addMicMac: false,
|
||||
|
||||
DEBUG,
|
||||
request,
|
||||
products,
|
||||
ageGroup, ageGroupFromAge, ageGroupLabel,
|
||||
transformReqBody: id,
|
||||
transformReq: id,
|
||||
randomizeUserAgent: false,
|
||||
logRequest,
|
||||
logResponse,
|
||||
|
||||
formatJourneysReq: notImplemented,
|
||||
formatRefreshJourneyReq: notImplemented,
|
||||
formatTripReq: notImplemented,
|
||||
formatNearbyReq: notImplemented,
|
||||
formatLocationsReq: notImplemented,
|
||||
formatStopReq: notImplemented,
|
||||
formatStationBoardReq: notImplemented,
|
||||
transformJourneysQuery: id,
|
||||
|
||||
parseDateTime,
|
||||
parsePlatform,
|
||||
parseProducts,
|
||||
parseWhen,
|
||||
parseDeparture,
|
||||
parseArrival,
|
||||
parseTrip,
|
||||
parseJourneyLeg,
|
||||
parseJourney,
|
||||
parseLine,
|
||||
parseStationName: id,
|
||||
parseLocation,
|
||||
parseMovement,
|
||||
parseNearby,
|
||||
enrichStation,
|
||||
parsePolyline,
|
||||
parseOperator,
|
||||
parseRemark,
|
||||
parseRemarks,
|
||||
parseCancelled,
|
||||
parseStopover,
|
||||
parseLoadFactor,
|
||||
parseArrOrDepWithLoadFactor,
|
||||
parseHintByCode,
|
||||
parsePrice,
|
||||
parseTickets,
|
||||
parseStop: notImplemented,
|
||||
|
||||
formatAddress,
|
||||
formatCoord,
|
||||
formatDate,
|
||||
formatLocationFilter,
|
||||
formatLocation,
|
||||
formatLocationFilter: notImplemented,
|
||||
formatLoyaltyCard,
|
||||
formatPoi,
|
||||
formatProductsFilter,
|
||||
formatStation,
|
||||
formatTime,
|
||||
formatLocation,
|
||||
formatRectangle,
|
||||
filters,
|
||||
formatTimeOfDay,
|
||||
formatTransfers,
|
||||
formatTravellers,
|
||||
formatRectangle: id,
|
||||
|
||||
journeysNumF: true, // `journeys()` method: support for `numF` field?
|
||||
journeyLeg: false,
|
||||
radar: false
|
||||
}
|
||||
journeysOutFrwd: true,
|
||||
departuresGetPasslist: false,
|
||||
departuresStbFltrEquiv: true,
|
||||
trip: true,
|
||||
radar: false,
|
||||
refreshJourney: true,
|
||||
journeysFromTrip: false,
|
||||
refreshJourneyUseOutReconL: false,
|
||||
tripsByName: false,
|
||||
remarks: false,
|
||||
remarksGetPolyline: false,
|
||||
reachableFrom: false,
|
||||
lines: false,
|
||||
};
|
||||
|
||||
module.exports = defaultProfile
|
||||
export {
|
||||
defaultProfile,
|
||||
};
|
||||
|
|
302
lib/errors.js
Normal file
302
lib/errors.js
Normal file
|
@ -0,0 +1,302 @@
|
|||
const ACCESS_DENIED = 'ACCESS_DENIED';
|
||||
const INVALID_REQUEST = 'INVALID_REQUEST';
|
||||
const NOT_FOUND = 'NOT_FOUND';
|
||||
const SERVER_ERROR = 'SERVER_ERROR';
|
||||
|
||||
class HafasError extends Error {
|
||||
constructor (cleanMessage, hafasCode, props) {
|
||||
const msg = hafasCode
|
||||
? hafasCode + ': ' + cleanMessage
|
||||
: cleanMessage;
|
||||
super(msg);
|
||||
|
||||
// generic props
|
||||
this.isHafasError = true;
|
||||
|
||||
// error-specific props
|
||||
this.code = null;
|
||||
// By default, we take the blame, unless we know for sure.
|
||||
this.isCausedByServer = false;
|
||||
this.hafasCode = hafasCode;
|
||||
Object.assign(this, props);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class HafasAccessDeniedError extends HafasError {
|
||||
constructor (cleanMessage, hafasCode, props) {
|
||||
super(cleanMessage, hafasCode, props);
|
||||
this.code = ACCESS_DENIED;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class HafasInvalidRequestError extends HafasError {
|
||||
constructor (cleanMessage, hafasCode, props) {
|
||||
super(cleanMessage, hafasCode, props);
|
||||
this.code = INVALID_REQUEST;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class HafasNotFoundError extends HafasError {
|
||||
constructor (cleanMessage, hafasCode, props) {
|
||||
super(cleanMessage, hafasCode, props);
|
||||
this.code = NOT_FOUND;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class HafasServerError extends HafasError {
|
||||
constructor (cleanMessage, hafasCode, props) {
|
||||
super(cleanMessage, hafasCode, props);
|
||||
this.code = SERVER_ERROR;
|
||||
this.isCausedByServer = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const byErrorCode = Object.assign(Object.create(null), {
|
||||
H_UNKNOWN: {
|
||||
Error: HafasError,
|
||||
message: 'unknown internal error',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
AUTH: {
|
||||
Error: HafasAccessDeniedError,
|
||||
message: 'invalid or missing authentication data',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
METHOD_NA: {
|
||||
Error: HafasServerError,
|
||||
message: 'method is not enabled',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
R0001: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'unknown method',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
R0002: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'invalid or missing request parameters',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
R0007: {
|
||||
Error: HafasServerError,
|
||||
message: 'internal communication error',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
R5000: {
|
||||
Error: HafasAccessDeniedError,
|
||||
message: 'access denied',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
S1: {
|
||||
Error: HafasServerError,
|
||||
message: 'journeys search: a connection to the backend server couldn\'t be established',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
PROBLEMS: {
|
||||
Error: HafasServerError,
|
||||
message: 'an unknown problem occurred during search',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
LOCATION: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'location/stop not found',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
NO_MATCH: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'no results found',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
PARAMETER: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'invalid parameter',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H390: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: departure/arrival station replaced',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H410: {
|
||||
// todo: or is it a client error?
|
||||
Error: HafasServerError,
|
||||
message: 'journeys search: incomplete response due to timetable change',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H455: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: prolonged stop',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H460: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: stop(s) passed multiple times',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H500: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: too many trains, connection is not complete',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H890: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'journeys search unsuccessful',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
H891: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'journeys search: no route found, try with an intermediate stations',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H892: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: query too complex, try less intermediate stations',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H895: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: departure & arrival are too near',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H899: {
|
||||
// todo: or is it a client error?
|
||||
Error: HafasServerError,
|
||||
message: 'journeys search unsuccessful or incomplete due to timetable change',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H900: {
|
||||
// todo: or is it a client error?
|
||||
Error: HafasServerError,
|
||||
message: 'journeys search unsuccessful or incomplete due to timetable change',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9220: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'journeys search: no stations found close to the address',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9230: {
|
||||
Error: HafasServerError,
|
||||
message: 'journeys search: an internal error occurred',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
H9240: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'journeys search unsuccessful',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
H9250: {
|
||||
Error: HafasServerError,
|
||||
message: 'journeys search: leg query interrupted',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
H9260: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: unknown departure station',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9280: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: unknown intermediate station',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9300: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: unknown arrival station',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9320: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: the input is incorrect or incomplete',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9360: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: invalid date/time',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
H9380: {
|
||||
Error: HafasInvalidRequestError,
|
||||
message: 'journeys search: departure/arrival/intermediate station defined more than once',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
SQ001: {
|
||||
Error: HafasServerError,
|
||||
message: 'no departures/arrivals data available',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
SQ005: {
|
||||
Error: HafasNotFoundError,
|
||||
message: 'no trips found',
|
||||
props: {
|
||||
},
|
||||
},
|
||||
TI001: {
|
||||
Error: HafasServerError,
|
||||
message: 'no trip info available',
|
||||
props: {
|
||||
shouldRetry: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export {
|
||||
ACCESS_DENIED,
|
||||
INVALID_REQUEST,
|
||||
NOT_FOUND,
|
||||
SERVER_ERROR,
|
||||
HafasError,
|
||||
HafasAccessDeniedError,
|
||||
HafasInvalidRequestError,
|
||||
HafasNotFoundError,
|
||||
HafasServerError,
|
||||
byErrorCode,
|
||||
};
|
6
lib/luxon-timezones.js
Normal file
6
lib/luxon-timezones.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
// hafas-client profile -> luxon.IANAZone
|
||||
const luxonIANAZonesByProfile = new WeakMap();
|
||||
|
||||
export {
|
||||
luxonIANAZonesByProfile,
|
||||
};
|
136
lib/products.js
Normal file
136
lib/products.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
const products = [
|
||||
{
|
||||
id: 'nationalExpress',
|
||||
mode: 'train',
|
||||
bitmasks: [1],
|
||||
name: 'InterCityExpress',
|
||||
short: 'ICE',
|
||||
vendo: 'ICE',
|
||||
ris: 'HIGH_SPEED_TRAIN',
|
||||
ris_alt: 'HIGH_SPEED_TRAIN',
|
||||
dbnav: 'HOCHGESCHWINDIGKEITSZUEGE',
|
||||
dbnav_short: 'ICE',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'national',
|
||||
mode: 'train',
|
||||
bitmasks: [2],
|
||||
name: 'InterCity & EuroCity',
|
||||
short: 'IC/EC',
|
||||
vendo: 'EC_IC',
|
||||
ris: 'INTERCITY_TRAIN',
|
||||
ris_alt: 'INTERCITY_TRAIN',
|
||||
dbnav: 'INTERCITYUNDEUROCITYZUEGE',
|
||||
dbnav_short: 'IC_EC',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'regionalExpress',
|
||||
mode: 'train',
|
||||
bitmasks: [4],
|
||||
name: 'RegionalExpress & InterRegio', // FlixTrain??
|
||||
short: 'RE/IR',
|
||||
vendo: 'IR',
|
||||
ris: 'INTER_REGIONAL_TRAIN',
|
||||
ris_alt: 'INTER_REGIONAL_TRAIN',
|
||||
dbnav: 'INTERREGIOUNDSCHNELLZUEGE',
|
||||
dbnav_short: 'IR',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'regional',
|
||||
mode: 'train',
|
||||
bitmasks: [8],
|
||||
name: 'Regio',
|
||||
short: 'RB',
|
||||
vendo: 'REGIONAL',
|
||||
ris: 'REGIONAL_TRAIN',
|
||||
ris_alt: 'REGIONAL_TRAIN',
|
||||
dbnav: 'NAHVERKEHRSONSTIGEZUEGE',
|
||||
dbnav_short: 'RB',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'suburban',
|
||||
mode: 'train',
|
||||
bitmasks: [16],
|
||||
name: 'S-Bahn',
|
||||
short: 'S',
|
||||
vendo: 'SBAHN',
|
||||
ris: 'CITY_TRAIN',
|
||||
ris_alt: 'CITY_TRAIN',
|
||||
dbnav: 'SBAHNEN',
|
||||
dbnav_short: 'SBAHN',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'bus',
|
||||
mode: 'bus',
|
||||
bitmasks: [32],
|
||||
name: 'Bus',
|
||||
short: 'B',
|
||||
vendo: 'BUS',
|
||||
ris: 'BUS',
|
||||
ris_alt: 'BUS',
|
||||
dbnav: 'BUSSE',
|
||||
dbnav_short: 'BUS',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'ferry',
|
||||
mode: 'watercraft',
|
||||
bitmasks: [64],
|
||||
name: 'Ferry',
|
||||
short: 'F',
|
||||
vendo: 'SCHIFF',
|
||||
ris: 'FERRY',
|
||||
ris_alt: 'FERRY',
|
||||
dbnav: 'SCHIFFE',
|
||||
dbnav_short: 'SCHIFF',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'subway',
|
||||
mode: 'train',
|
||||
bitmasks: [128],
|
||||
name: 'U-Bahn',
|
||||
short: 'U',
|
||||
vendo: 'UBAHN',
|
||||
ris: 'SUBWAY',
|
||||
ris_alt: 'SUBWAY',
|
||||
dbnav: 'UBAHN',
|
||||
dbnav_short: 'UBAHN',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'tram',
|
||||
mode: 'train',
|
||||
bitmasks: [256],
|
||||
name: 'Tram',
|
||||
short: 'T',
|
||||
vendo: 'TRAM',
|
||||
ris: 'TRAM',
|
||||
ris_alt: 'TRAM',
|
||||
dbnav: 'STRASSENBAHN',
|
||||
dbnav_short: 'STR',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'taxi',
|
||||
mode: 'taxi',
|
||||
bitmasks: [512],
|
||||
name: 'Group Taxi',
|
||||
short: 'Taxi',
|
||||
vendo: 'ANRUFPFLICHTIG',
|
||||
ris: 'TAXI',
|
||||
ris_alt: 'SHUTTLE',
|
||||
dbnav: 'ANRUFPFLICHTIGEVERKEHRE',
|
||||
dbnav_short: 'ANRUFPFLICHTIGEVERKEHRE',
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
|
||||
export {
|
||||
products,
|
||||
};
|
228
lib/request.js
228
lib/request.js
|
@ -1,101 +1,155 @@
|
|||
'use strict'
|
||||
import {stringify} from 'qs';
|
||||
import {Request, fetch} from 'cross-fetch';
|
||||
import {parse as parseContentType} from 'content-type';
|
||||
import {HafasError} from './errors.js';
|
||||
|
||||
const crypto = require('crypto')
|
||||
let captureStackTrace = () => {}
|
||||
if (process.env.NODE_ENV === 'dev') {
|
||||
captureStackTrace = require('capture-stack-trace')
|
||||
}
|
||||
const {stringify} = require('query-string')
|
||||
const Promise = require('pinkie-promise')
|
||||
const {fetch} = require('fetch-ponyfill')({Promise})
|
||||
const proxyAddress = typeof process !== 'undefined' && (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) || null;
|
||||
|
||||
const md5 = input => crypto.createHash('md5').update(input).digest()
|
||||
let getAgent = () => undefined;
|
||||
|
||||
const request = (profile, data) => {
|
||||
const body = profile.transformReqBody({lang: 'en', svcReqL: [data]})
|
||||
const req = profile.transformReq({
|
||||
method: 'post',
|
||||
const setupProxy = async () => {
|
||||
if (proxyAddress && !getAgent()) {
|
||||
const a = await import('https-proxy-agent');
|
||||
const agent = new a.default.HttpsProxyAgent(proxyAddress, {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 10 * 1000, // 10s
|
||||
});
|
||||
getAgent = () => agent;
|
||||
}
|
||||
};
|
||||
|
||||
const randomBytesHexString = length => [...Array(length)].map(() => Math.floor(Math.random() * 16)
|
||||
.toString(16))
|
||||
.join('');
|
||||
|
||||
const id = randomBytesHexString(6)
|
||||
.toString('hex');
|
||||
const randomizeUserAgent = (userAgent) => {
|
||||
let ua = userAgent;
|
||||
for (
|
||||
let i = Math.round(5 + Math.random() * 5);
|
||||
i < ua.length;
|
||||
i += Math.round(5 + Math.random() * 5)
|
||||
) {
|
||||
ua = ua.slice(0, i) + id + ua.slice(i);
|
||||
i += id.length;
|
||||
}
|
||||
return ua;
|
||||
};
|
||||
|
||||
const checkIfResponseIsOk = (_) => {
|
||||
const {
|
||||
body,
|
||||
errProps: baseErrProps,
|
||||
} = _;
|
||||
|
||||
const errProps = {
|
||||
...baseErrProps,
|
||||
};
|
||||
if (body.id) {
|
||||
errProps.hafasResponseId = body.id;
|
||||
}
|
||||
|
||||
// Because we want more accurate stack traces, we don't construct the error here,
|
||||
// but only return the constructor & error message.
|
||||
const getError = (_) => {
|
||||
// mutating here is ugly but pragmatic
|
||||
if (_.fehlerNachricht.ueberschrift) {
|
||||
errProps.hafasMessage = _.fehlerNachricht.ueberschrift;
|
||||
}
|
||||
if (_.fehlerNachricht.text) {
|
||||
errProps.hafasDescription = _.fehlerNachricht.text;
|
||||
}
|
||||
return {
|
||||
Error: HafasError,
|
||||
message: errProps.hafasMessage || 'unknown error',
|
||||
props: {code: _.fehlerNachricht.code},
|
||||
};
|
||||
};
|
||||
|
||||
if (body.fehlerNachricht || body.errors) { // TODO better handling
|
||||
const {Error: HafasError, message, props} = getError(body);
|
||||
throw new HafasError(message, body.err || body.errors, {...errProps, ...props});
|
||||
}
|
||||
};
|
||||
|
||||
const request = async (ctx, userAgent, reqData) => {
|
||||
const {profile, opt} = ctx;
|
||||
await setupProxy();
|
||||
|
||||
const endpoint = reqData.endpoint;
|
||||
delete reqData.endpoint;
|
||||
const rawReqBody = profile.transformReqBody(ctx, reqData.body);
|
||||
|
||||
const reqOptions = profile.transformReq(ctx, {
|
||||
agent: getAgent(),
|
||||
keepalive: true,
|
||||
method: reqData.method,
|
||||
// todo: CORS? referrer policy?
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify(rawReqBody),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'user-agent': 'https://github.com/derhuerst/hafas-client'
|
||||
'Accept-Encoding': 'gzip, br, deflate',
|
||||
'Accept': 'application/json',
|
||||
'Accept-Language': opt.language || profile.defaultLanguage || 'en',
|
||||
'user-agent': profile.randomizeUserAgent
|
||||
? randomizeUserAgent(userAgent)
|
||||
: userAgent,
|
||||
...reqData.headers,
|
||||
},
|
||||
query: {}
|
||||
})
|
||||
redirect: 'follow',
|
||||
query: reqData.query,
|
||||
});
|
||||
|
||||
if (profile.addChecksum || profile.addMicMac) {
|
||||
if (!Buffer.isBuffer(profile.salt)) {
|
||||
throw new Error('profile.salt must be a Buffer.')
|
||||
}
|
||||
if (profile.addChecksum) {
|
||||
const checksum = md5(Buffer.concat([
|
||||
Buffer.from(req.body, 'utf8'),
|
||||
profile.salt
|
||||
]))
|
||||
req.query.checksum = checksum.toString('hex')
|
||||
}
|
||||
if (profile.addMicMac) {
|
||||
const mic = md5(Buffer.from(req.body, 'utf8'))
|
||||
req.query.mic = mic.toString('hex')
|
||||
let url = endpoint + (reqData.path || '');
|
||||
if (reqOptions.query) {
|
||||
url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
|
||||
}
|
||||
const reqId = randomBytesHexString(6);
|
||||
const fetchReq = new Request(url, reqOptions);
|
||||
profile.logRequest(ctx, fetchReq, reqId);
|
||||
|
||||
const micAsHex = Buffer.from(mic.toString('hex'), 'utf8')
|
||||
const mac = md5(Buffer.concat([micAsHex, profile.salt]))
|
||||
req.query.mac = mac.toString('hex')
|
||||
const res = await fetch(url, reqOptions);
|
||||
|
||||
const errProps = {
|
||||
// todo [breaking]: assign as non-enumerable property
|
||||
request: fetchReq,
|
||||
// todo [breaking]: assign as non-enumerable property
|
||||
response: res,
|
||||
url,
|
||||
};
|
||||
|
||||
if (!res.ok) {
|
||||
// todo [breaking]: make this a FetchError or a HafasClientError?
|
||||
console.log(JSON.stringify(res), await res.text());
|
||||
const err = new Error(res.statusText);
|
||||
Object.assign(err, errProps);
|
||||
throw err;
|
||||
}
|
||||
|
||||
let cType = res.headers.get('content-type');
|
||||
if (cType) {
|
||||
const {type} = parseContentType(cType);
|
||||
if (type !== reqOptions.headers['Accept']) {
|
||||
throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps);
|
||||
}
|
||||
}
|
||||
|
||||
const url = profile.endpoint + '?' + stringify(req.query)
|
||||
const body = await res.text();
|
||||
profile.logResponse(ctx, res, body, reqId);
|
||||
|
||||
// Async stack traces are not supported everywhere yet, so we create our own.
|
||||
const err = new Error()
|
||||
err.isHafasError = true
|
||||
err.request = body
|
||||
err.url = url
|
||||
captureStackTrace(err)
|
||||
const b = JSON.parse(body);
|
||||
checkIfResponseIsOk({
|
||||
body: b,
|
||||
errProps,
|
||||
});
|
||||
return {
|
||||
res: b,
|
||||
common: {},
|
||||
};
|
||||
};
|
||||
|
||||
return fetch(url, req)
|
||||
.then((res) => {
|
||||
err.statusCode = res.status
|
||||
if (!res.ok) {
|
||||
err.message = res.statusText
|
||||
throw err
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then((b) => {
|
||||
if (b.err) {
|
||||
err.message = b.err
|
||||
throw err
|
||||
}
|
||||
if (!b.svcResL || !b.svcResL[0]) {
|
||||
err.message = 'invalid response'
|
||||
throw err
|
||||
}
|
||||
if (b.svcResL[0].err !== 'OK') {
|
||||
err.message = b.svcResL[0].errTxt || b.svcResL[0].err
|
||||
throw err
|
||||
}
|
||||
const d = b.svcResL[0].res
|
||||
const c = d.common || {}
|
||||
|
||||
if (Array.isArray(c.remL)) {
|
||||
d.remarks = c.remL.map(rem => profile.parseRemark(profile, rem))
|
||||
}
|
||||
if (Array.isArray(c.opL)) {
|
||||
d.operators = c.opL.map(op => profile.parseOperator(profile, op))
|
||||
}
|
||||
if (Array.isArray(c.prodL)) {
|
||||
const parse = profile.parseLine(profile, d.operators)
|
||||
d.lines = c.prodL.map(parse)
|
||||
}
|
||||
if (Array.isArray(c.locL)) {
|
||||
const parse = loc => profile.parseLocation(profile, loc, d.lines)
|
||||
d.locations = c.locL.map(parse)
|
||||
}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = request
|
||||
export {
|
||||
checkIfResponseIsOk,
|
||||
request,
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue