This commit is contained in:
Traines 2025-01-08 23:02:57 +00:00
parent 0e68a375e1
commit 98670d5e08
15 changed files with 493 additions and 32 deletions

View file

@ -1,7 +0,0 @@
const formatStopReq = (ctx, stopRef) => {
// TODO
};
export {
formatStopReq,
};

View file

@ -4,8 +4,6 @@ import readStations from 'db-hafas-stations';
import {defaultProfile} from './lib/default-profile.js';
import {validateProfile} from './lib/validate-profile.js';
import {INVALID_REQUEST} from './lib/errors.js';
import {HafasError} from './lib/errors.js';
// background info: https://github.com/public-transport/hafas-client/issues/286
const FORBIDDEN_USER_AGENTS = [
@ -254,12 +252,10 @@ const createClient = (profile, userAgent, opt = {}) => {
: results;
};
const stop = async (stop, opt = {}) => { // TODO
if ('object' === typeof stop) {
stop = profile.formatStation(stop.id);
} else if ('string' === typeof stop) {
stop = profile.formatStation(stop);
} else {
const stop = async (stop, opt = {}) => {
if (isObj(stop) && stop.id) {
stop = stop.id;
} else if ('string' !== typeof stop) {
throw new TypeError('stop must be an object or a string.');
}
@ -273,15 +269,8 @@ const createClient = (profile, userAgent, opt = {}) => {
const req = profile.formatStopReq({profile, opt}, stop);
const {res} = await profile.request({profile, opt}, userAgent, req);
if (!res || !Array.isArray(res.locL) || !res.locL[0]) {
throw new HafasError('invalid response, expected locL[0]', null, {
// This problem occurs on invalid input. 🙄
code: INVALID_REQUEST,
});
}
const ctx = {profile, opt, res, common};
return profile.parseLocation(ctx, res.locL[0]);
return profile.parseStop(ctx, res, stop);
};
const nearby = async (location, opt = {}) => {

View file

@ -3,7 +3,6 @@ import {products} from '../lib/products.js';
import {ageGroup, ageGroupFromAge, ageGroupLabel} from './age-group.js';
import {formatStationBoardReq} from '../format/station-board-req.js';
import {formatStopReq} from '../format/stop-req.js';
import {formatTripReq} from '../format/trip-req.js';
import {formatNearbyReq} from '../format/nearby-req.js';
@ -63,7 +62,7 @@ const defaultProfile = {
formatStationBoardReq,
formatLocationsReq: notImplemented,
formatStopReq,
formatStopReq: notImplemented,
formatTripReq,
formatNearbyReq,
formatJourneysReq: notImplemented,
@ -92,6 +91,7 @@ const defaultProfile = {
parseHintByCode,
parsePrice,
parseTickets,
parseStop: notImplemented,
formatAddress,
formatCoord,

View file

@ -3,6 +3,7 @@
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon",
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
"stopEndpoint": "https://app.vendo.noncd.db.de/mob/location/details/",
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf/",
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/",

View file

@ -7,8 +7,10 @@ import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatTripReq} from './trip-req.js';
import {formatLocationFilter} from './location-filter.js';
import {formatLocationsReq} from './locations-req.js';
import {formatStopReq} from './stop-req.js';
import {formatNearbyReq} from './nearby-req.js';
import {formatStationBoardReq} from './station-board-req.js';
import {parseStop} from './parse-stop.js';
const profile = {
...baseProfile,
@ -21,8 +23,11 @@ const profile = {
formatTripReq,
formatNearbyReq,
formatLocationsReq,
formatStopReq,
formatStationBoardReq,
formatLocationFilter,
parseStop,
};
export {

47
p/dbnav/parse-stop.js Normal file
View file

@ -0,0 +1,47 @@
const parseStop = (ctx, l, id) => {
const {profile, common, opt} = ctx;
if (!l) {
return null;
}
let stop = {
type: 'station',
id: id,
name: l.haltName,
};
stop.products = profile.parseProducts(ctx, l.produktGattungen?.map(p => p.produktGattung));
if (opt.linesOfStops) {
stop.lines = l.produktGattungen?.flatMap(p => {
const foundProduct = profile.products.find(pp => pp.dbnav == p.produktGattung);
return p.produkte?.map(l => {
return {
type: 'line',
name: l.name,
productName: l.name && l.name.split(' ')[0] || undefined,
mode: foundProduct?.mode,
product: foundProduct?.id,
public: true,
};
});
});
}
if (common && common.locations && common.locations[stop.id]) {
delete stop.type;
stop = {
...common.locations[stop.id],
...stop,
};
}
// TODO isMeta
// TODO entrances
return stop;
};
export {
parseStop,
};

16
p/dbnav/stop-req.js Normal file
View file

@ -0,0 +1,16 @@
import {getHeaders} from './header.js';
const formatStopReq = (ctx, stopRef) => {
const {profile} = ctx;
return {
endpoint: profile.stopEndpoint,
path: stopRef,
headers: getHeaders('application/x.db.vendo.mob.location.v3+json'),
method: 'get',
};
};
export {
formatStopReq,
};

View file

@ -16,9 +16,9 @@ What doesn't work (yet, see TODO's scattered around the code):
* `journeys()` details like scheduledDays, stop/station groups, some line details ...
* loadFactor and other details in boards
* certain stop details like products for `locations()` and geopositions and remarks for boards this can be remedied by turning on `enrichStations` in the options, enriching location info with [db-hafas-stations](https://github.com/derhuerst/db-hafas-stations).
* certain stop details like products for `locations()` and geopositions and remarks for boards this can be remedied by turning on `enrichStations` in the config, enriching location info with [db-hafas-stations](https://github.com/derhuerst/db-hafas-stations).
* some query options/filters (e.g. routingMode for journeys, direction for boards)
* all other endpoints (`tripsByName()`, `radar()`, `journeysFromTrip()`, `reachableFrom()`, `remarks()`, `lines()`, `stop()`, `station()`)
* all other endpoints (`tripsByName()`, `radar()`, `journeysFromTrip()`, `reachableFrom()`, `remarks()`, `lines()`, `station()`)
Depending on the configured profile, db-vendo-client will use multiple different DB APIs that offer varying functionality, so choose wisely:
@ -26,11 +26,12 @@ Depending on the configured profile, db-vendo-client will use multiple different
| ------------- | ------------- | ------------- |
| no API key required | ✅ | ✅ |
| max duration boards | 12h | 1h |
| remarks | not for boards | ✅ |
| remarks | not for boards | ✅ (still no `remarks()` endpoint) |
| cancelled trips | not contained in boards | contained with cancelled flag |
| tickets | only for `refreshJourney()` | only for `refreshJourney()`, mutually exclusive with polylines |
| polylines | only for `trip()` | only for `refreshJourney()/trip()`, mutually exclusive with tickets |
| trip ids used | HAFAS trip ids for journeys, RIS trip ids for boards | HAFAS trip ids |
| `stop()` | ❌ | ✅ |
| assumed backend API stability | less stable | more stable |
Feel free to report anything that you stumble upon via Issues or create a PR :)
@ -54,7 +55,7 @@ Use it as a dependency, e.g. just replacing [hafas-client](https://github.com/pu
npm i db-vendo-client
```
See an example in [api.js](api.js). It shows how you can use `db-vendo-client` together with `hafas-rest-api` in order to run a [FPTF](https://github.com/public-transport/friendly-public-transport-format) API server. The [Dockerfile](Dockerfile) serves this API:
See an example in [api.js](api.js). It shows how you can use `db-vendo-client` together with [hafas-rest-api](https://github.com/public-transport/hafas-rest-api/) in order to run a [FPTF](https://github.com/public-transport/friendly-public-transport-format) API server. The [Dockerfile](Dockerfile) serves this API:
```
docker run -e USER_AGENT=my-awesome-program -p 3000:3000 ghcr.io/public-transport/db-vendo-client

View file

@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/db/index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-departures.json');
import {dbnavDepartures as expected} from './fixtures/dbnav-departures.js';

View file

@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/db/index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-refresh-journey.json');
import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js';

25
test/dbnav-stop.js Normal file
View file

@ -0,0 +1,25 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-stop.json');
import {dbnavDepartures as expected} from './fixtures/dbnav-stop.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
const {profile} = client;
const opt = {
linesOfStops: true,
};
tap.test('parses a dbnav stop correctly', (t) => {
const ctx = {profile, opt, common: null, res};
const stop = profile.parseStop(ctx, res, '8000096');
t.same(stop, expected);
t.end();
});

View file

@ -476,7 +476,6 @@ tap.test('locations named Jungfernheide', async (t) => {
t.end();
});
/*
tap.test('stop', async (t) => {
const s = await client.stop(regensburgHbf);
@ -486,6 +485,7 @@ tap.test('stop', async (t) => {
t.end();
});
/*
tap.test('line with additionalName', async (t) => {
const {departures} = await client.departures(potsdamHbf, {
when,

View file

@ -5109,6 +5109,142 @@
"ssl": -1,
"wait": 445
}
},
{
"_id": "d50b6b1f76add52cd3eb6ffa13dea4d4",
"_order": 0,
"cache": {},
"request": {
"bodySize": 0,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "content-type",
"value": "application/x.db.vendo.mob.location.v3+json"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip, br, deflate"
},
{
"_fromType": "array",
"name": "accept",
"value": "application/x.db.vendo.mob.location.v3+json"
},
{
"_fromType": "array",
"name": "accept-language",
"value": "en"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "public-transport/hafas-client:test"
},
{
"_fromType": "array",
"name": "connection",
"value": "keep-alive"
},
{
"_fromType": "array",
"name": "x-correlation-id",
"value": "null"
},
{
"name": "host",
"value": "app.vendo.noncd.db.de"
}
],
"headersSize": 369,
"httpVersion": "HTTP/1.1",
"method": "GET",
"queryString": [],
"url": "https://app.vendo.noncd.db.de/mob/location/details/8000309"
},
"response": {
"bodySize": 1945,
"content": {
"mimeType": "application/x.db.vendo.mob.location.v3+json",
"size": 1945,
"text": "{\"haltName\":\"Regensburg Hbf\",\"produktGattungen\":[{\"produktGattung\":\"HOCHGESCHWINDIGKEITSZUEGE\",\"produkte\":[{\"name\":\"ICE\"}]},{\"produktGattung\":\"INTERCITYUNDEUROCITYZUEGE\",\"produkte\":[{\"name\":\"IC\"},{\"name\":\"NJ\"}]},{\"produktGattung\":\"INTERREGIOUNDSCHNELLZUEGE\",\"produkte\":[{\"name\":\"BUS\"}]},{\"produktGattung\":\"NAHVERKEHRSONSTIGEZUEGE\",\"produkte\":[{\"name\":\"ALX\"},{\"name\":\"OPB\"},{\"name\":\"RB\"},{\"name\":\"RE\"},{\"name\":\"ag\"},{\"name\":\"Bus EV\"},{\"name\":\"Bus EV\"},{\"name\":\"Bus EV\"},{\"name\":\"Bus EV\"},{\"name\":\"Bus RB23\"},{\"name\":\"Bus RE23\"},{\"name\":\"Bus RE25\"}]},{\"produktGattung\":\"BUSSE\",\"produkte\":[{\"name\":\"Bus 1\"},{\"name\":\"Bus 2\"},{\"name\":\"Bus 3\"},{\"name\":\"Bus 4\"},{\"name\":\"Bus 5\"},{\"name\":\"Bus 5\"},{\"name\":\"Bus 5A\"},{\"name\":\"Bus 6\"},{\"name\":\"Bus 7\"},{\"name\":\"Bus 8\"},{\"name\":\"Bus 8A\"},{\"name\":\"Bus 9\"},{\"name\":\"Bus 10\"},{\"name\":\"Bus 11\"},{\"name\":\"Bus 11A\"},{\"name\":\"Bus 12\"},{\"name\":\"Bus 13\"},{\"name\":\"Bus 14\"},{\"name\":\"Bus 15\"},{\"name\":\"Bus 16\"},{\"name\":\"Bus 17\"},{\"name\":\"Bus 18\"},{\"name\":\"Bus 19\"},{\"name\":\"Bus 20\"},{\"name\":\"Bus 21\"},{\"name\":\"Bus 22\"},{\"name\":\"Bus 23\"},{\"name\":\"Bus 24\"},{\"name\":\"Bus 26\"},{\"name\":\"Bus 27\"},{\"name\":\"Bus 28\"},{\"name\":\"Bus 29\"},{\"name\":\"Bus 30\"},{\"name\":\"Bus 31\"},{\"name\":\"Bus 32\"},{\"name\":\"Bus 33\"},{\"name\":\"Bus 34\"},{\"name\":\"Bus 35\"},{\"name\":\"Bus 38\"},{\"name\":\"Bus 41\"},{\"name\":\"Bus 42\"},{\"name\":\"Bus 43\"},{\"name\":\"Bus 71\"},{\"name\":\"Bus 73\"},{\"name\":\"Bus 73\"},{\"name\":\"Bus 73\"},{\"name\":\"Bus 75\"},{\"name\":\"Bus 77\"},{\"name\":\"Bus 80\"},{\"name\":\"Bus 105\"},{\"name\":\"Bus 117\"},{\"name\":\"Bus 6010\"},{\"name\":\"Bus A\"},{\"name\":\"Bus C1\"},{\"name\":\"Bus C6\"},{\"name\":\"Bus D\"},{\"name\":\"Bus DULT\"},{\"name\":\"Bus N1\"},{\"name\":\"Bus N2\"},{\"name\":\"Bus N3\"},{\"name\":\"Bus N4\"},{\"name\":\"Bus N5\"},{\"name\":\"Bus N6\"},{\"name\":\"Bus N7\"},{\"name\":\"Bus X1\"},{\"name\":\"Bus X4\"},{\"name\":\"Bus X6\"},{\"name\":\"Bus X9\"},{\"name\":\"Bus21/24\"},{\"name\":\"Bus26/27\"},{\"name\":\"Bus36/37\"}]},{\"produktGattung\":\"ANRUFPFLICHTIGEVERKEHRE\",\"produkte\":[{\"name\":\"RUF 32\"}]}]}"
},
"cookies": [
{
"domain": ".app.vendo.noncd.db.de",
"httpOnly": true,
"name": "TS01be2125",
"path": "/",
"secure": true,
"value": "01d513bcd1b9bf9e9663294318145959b6eb5e2a2b5df7da01060b1dc5e7ff5985ac13cb509abfbace0e7798ec534a603b2c68e08c"
}
],
"headers": [
{
"name": "date",
"value": "Wed, 08 Jan 2025 22:50:18 GMT"
},
{
"name": "content-type",
"value": "application/x.db.vendo.mob.location.v3+json"
},
{
"name": "content-length",
"value": "1945"
},
{
"name": "connection",
"value": "keep-alive"
},
{
"name": "server-timing",
"value": "intid;desc=6025e1514766819a, intid;desc=6025e1514766819a, intid;desc=6025e1514766819a, intid;desc=6025e1514766819a"
},
{
"name": "x-correlation-id",
"value": "null"
},
{
"name": "strict-transport-security",
"value": "max-age=16070400; includeSubDomains"
},
{
"name": "x-xss-protection",
"value": "0"
},
{
"name": "content-security-policy",
"value": "frame-ancestors 'none';"
},
{
"name": "x-content-type-options",
"value": "nosniff"
},
{
"_fromType": "array",
"name": "set-cookie",
"value": "TS01be2125=01d513bcd1b9bf9e9663294318145959b6eb5e2a2b5df7da01060b1dc5e7ff5985ac13cb509abfbace0e7798ec534a603b2c68e08c; Path=/; Domain=.app.vendo.noncd.db.de; Secure; HTTPOnly"
}
],
"headersSize": 655,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2025-01-08T22:50:18.535Z",
"time": 170,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 170
}
}
],
"pages": [],

247
test/fixtures/dbnav-stop.js vendored Normal file
View file

@ -0,0 +1,247 @@
const dbnavDepartures = {
type: 'station',
name: 'Stuttgart Hbf',
id: '8000096',
products: {
nationalExpress: true,
national: true,
regionalExpress: true,
regional: true,
suburban: true,
bus: true,
ferry: false,
subway: false,
tram: true,
taxi: false,
},
lines: [
{
type: 'line',
name: 'ICE',
productName: 'ICE',
mode: 'train',
product: 'nationalExpress',
public: true,
},
{
type: 'line',
name: 'RJ',
productName: 'RJ',
mode: 'train',
product: 'nationalExpress',
public: true,
},
{
type: 'line',
name: 'RJX',
productName: 'RJX',
mode: 'train',
product: 'nationalExpress',
public: true,
},
{
type: 'line',
name: 'TGV',
productName: 'TGV',
mode: 'train',
product: 'nationalExpress',
public: true,
},
{
type: 'line',
name: 'EC',
productName: 'EC',
mode: 'train',
product: 'national',
public: true,
},
{
type: 'line',
name: 'IC',
productName: 'IC',
mode: 'train',
product: 'national',
public: true,
},
{
type: 'line',
name: 'NJ',
productName: 'NJ',
mode: 'train',
product: 'national',
public: true,
},
{
type: 'line',
name: 'BUS',
productName: 'BUS',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'Bus',
productName: 'Bus',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'D',
productName: 'D',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'DBK',
productName: 'DBK',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'EN',
productName: 'EN',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'FLX',
productName: 'FLX',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'WB',
productName: 'WB',
mode: 'train',
product: 'regionalExpress',
public: true,
},
{
type: 'line',
name: 'GA',
productName: 'GA',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'IRE',
productName: 'IRE',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'MEX',
productName: 'MEX',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'RB',
productName: 'RB',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'RE',
productName: 'RE',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'Bus EV',
productName: 'Bus',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'BusMEX90',
productName: 'BusMEX90',
mode: 'train',
product: 'regional',
public: true,
},
{
type: 'line',
name: 'S 1',
productName: 'S',
mode: 'train',
product: 'suburban',
public: true,
},
{
type: 'line',
name: 'S 2',
productName: 'S',
mode: 'train',
product: 'suburban',
public: true,
},
{
type: 'line',
name: 'Bus 40',
productName: 'Bus',
mode: 'bus',
product: 'bus',
public: true,
},
{
type: 'line',
name: 'Bus 42',
productName: 'Bus',
mode: 'bus',
product: 'bus',
public: true,
},
{
type: 'line',
name: 'BusMEX90',
productName: 'BusMEX90',
mode: 'bus',
product: 'bus',
public: true,
},
{
type: 'line',
name: 'STB U1',
productName: 'STB',
mode: 'train',
product: 'tram',
public: true,
},
{
type: 'line',
name: 'STB U5',
productName: 'STB',
mode: 'train',
product: 'tram',
public: true,
},
],
};
export {
dbnavDepartures,
};

1
test/fixtures/dbnav-stop.json vendored Normal file
View file

@ -0,0 +1 @@
{"haltName":"Stuttgart Hbf","produktGattungen":[{"produktGattung":"HOCHGESCHWINDIGKEITSZUEGE","produkte":[{"name":"ICE"},{"name":"RJ"},{"name":"RJX"},{"name":"TGV"}]},{"produktGattung":"INTERCITYUNDEUROCITYZUEGE","produkte":[{"name":"EC"},{"name":"IC"},{"name":"NJ"}]},{"produktGattung":"INTERREGIOUNDSCHNELLZUEGE","produkte":[{"name":"BUS"},{"name":"Bus"},{"name":"D"},{"name":"DBK"},{"name":"EN"},{"name":"FLX"},{"name":"WB"}]},{"produktGattung":"NAHVERKEHRSONSTIGEZUEGE","produkte":[{"name":"GA"},{"name":"IRE"},{"name":"MEX"},{"name":"RB"},{"name":"RE"},{"name":"Bus EV"},{"name":"BusMEX90"}]},{"produktGattung":"SBAHNEN","produkte":[{"name":"S 1"},{"name":"S 2"}]},{"produktGattung":"BUSSE","produkte":[{"name":"Bus 40"},{"name":"Bus 42"},{"name":"BusMEX90"}]},{"produktGattung":"STRASSENBAHN","produkte":[{"name":"STB U1"},{"name":"STB U5"}]}]}