diff --git a/index.js b/index.js index b747f8ae..1a1557a5 100644 --- a/index.js +++ b/index.js @@ -85,10 +85,10 @@ const createClient = (profile, userAgent, opt = {}) => { throw new TypeError('type must be a non-empty string.'); } - if (!profile.departuresGetPasslist && 'stopovers' in opt) { + if (!profile.departuresGetPasslist && opt.stopovers) { throw new Error('opt.stopovers is not supported by this endpoint'); } - if (!profile.departuresStbFltrEquiv && 'includeRelatedStations' in opt) { // TODO artificially filter? + if (!profile.departuresStbFltrEquiv && 'includeRelatedStations' in opt) { throw new Error('opt.includeRelatedStations is not supported by this endpoint'); } @@ -185,6 +185,8 @@ const createClient = (profile, userAgent, opt = {}) => { 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 }, opt); if (opt.when !== undefined) { @@ -210,9 +212,15 @@ const createClient = (profile, userAgent, opt = {}) => { 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}; - const verbindungen = Number.isInteger(opt.results) ? res.verbindungen.slice(0, opt.results) : res.verbindungen; + 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, diff --git a/lib/api-parsers.js b/lib/api-parsers.js index cfa41da0..9aaa466f 100644 --- a/lib/api-parsers.js +++ b/lib/api-parsers.js @@ -50,7 +50,7 @@ const mapRouteParsers = (route, parsers) => { firstClass: { description: 'Search for first-class options?', type: 'boolean', - default: 'false', + default: false, parse: parseBoolean, }, loyaltyCard: { @@ -66,6 +66,18 @@ const mapRouteParsers = (route, parsers) => { 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, + }, }; }; diff --git a/p/dbnav/base.json b/p/dbnav/base.json index 9ad7546f..41525e41 100644 --- a/p/dbnav/base.json +++ b/p/dbnav/base.json @@ -1,5 +1,6 @@ { "journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan", + "bestpriceEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/tagesbestpreis", "refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon", "refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon", "locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search", diff --git a/p/dbnav/journeys-req.js b/p/dbnav/journeys-req.js index 8cc8631f..bf234837 100644 --- a/p/dbnav/journeys-req.js +++ b/p/dbnav/journeys-req.js @@ -55,8 +55,11 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => { if (journeysRef) { query.reiseHin.wunsch.context = journeysRef; } + if (opt.notOnlyFastRoutes) { + query.reiseHin.wunsch.economic = true; + } return { - endpoint: ctx.profile.journeysEndpoint, + endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint, body: query, headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'), method: 'post', diff --git a/p/dbweb/base.json b/p/dbweb/base.json index ae4637fd..91d75180 100644 --- a/p/dbweb/base.json +++ b/p/dbweb/base.json @@ -1,5 +1,6 @@ { "journeysEndpoint": "https://int.bahn.de/web/api/angebote/fahrplan", + "bestpriceEndpoint": "https://int.bahn.de/web/api/angebote/tagesbestpreis", "refreshJourneysEndpointTickets": "https://int.bahn.de/web/api/angebote/recon", "refreshJourneysEndpointPolyline": "https://int.bahn.de/web/api/reiseloesung/verbindung", "locationsEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte", diff --git a/p/dbweb/journeys-req.js b/p/dbweb/journeys-req.js index a896c7dc..b5814406 100644 --- a/p/dbweb/journeys-req.js +++ b/p/dbweb/journeys-req.js @@ -13,7 +13,7 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => { deutschlandTicketVorhanden: false, nurDeutschlandTicketVerbindungen: false, reservierungsKontingenteVorhanden: false, - schnelleVerbindungen: true, + schnelleVerbindungen: !opt.notOnlyFastRoutes, sitzplatzOnly: false, abfahrtsHalt: from.lid, zwischenhalte: opt.via @@ -35,14 +35,14 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => { if (opt.results !== null) { // TODO query.numF = opt.results; } - query = Object.assign(query, ctx.profile.formatTravellers(ctx)); + query = Object.assign(query, profile.formatTravellers(ctx)); return { - endpoint: ctx.profile.journeysEndpoint, + endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint, body: query, method: 'post', }; }; -// TODO poly conditional other endpoint + const formatRefreshJourneyReq = (ctx, refreshToken) => { const {profile, opt} = ctx; if (opt.tickets) { diff --git a/parse/journey.js b/parse/journey.js index 7acd05a1..45c7aaa3 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -69,7 +69,15 @@ const parseJourney = (ctx, jj) => { // j = raw journey // TODO if (opt.scheduledDays && j.serviceDays) { // todo [breaking]: rename to scheduledDates - // res.scheduledDays = profile.parseScheduledDays(ctx, j.serviceDays); + // TODO parse scheduledDays as before + res.serviceDays = j.serviceDays.map(d => ({ + irregular: d.irregular, + lastDateInPeriod: d.lastDateInPeriod || d.letztesDatumInZeitraum, + planningPeriodBegin: d.planningPeriodBegin || d.planungsZeitraumAnfang, + planningPeriodEnd: d.planningPeriodEnd || d.planungsZeitraumEnde, + regular: d.regular, + weekdays: d.weekdays || d.wochentage, + })); } res.price = profile.parsePrice(ctx, jj); diff --git a/parse/load-factor.js b/parse/load-factor.js index 6accdb9d..1fad4037 100644 --- a/parse/load-factor.js +++ b/parse/load-factor.js @@ -9,7 +9,7 @@ const parseLoadFactor = (opt, auslastung) => { if (!auslastung) { return null; } - const cls = opt.firstClass + const cls = opt.firstClass === true ? 'KLASSE_1' : 'KLASSE_2'; const load = auslastung.find(a => a.klasse === cls)?.stufe; diff --git a/parse/tickets.js b/parse/tickets.js index e8dd8a4f..c56a19e5 100644 --- a/parse/tickets.js +++ b/parse/tickets.js @@ -1,11 +1,14 @@ +const PARTIAL_FARE_HINT = 'Teilpreis / partial fare'; const parsePrice = (ctx, raw) => { - const p = raw.angebotsPreis || raw.angebote?.preise?.gesamt?.ab; // TODO teilpreis + const p = raw.angebotsPreis || raw.angebote?.preise?.gesamt?.ab || raw.abPreis; if (p?.betrag) { + const partialFare = raw.hasTeilpreis ?? raw.angebote?.preise?.istTeilpreis ?? raw.teilpreis; return { amount: p.betrag, currency: p.waehrung, - hint: null, + hint: partialFare ? PARTIAL_FARE_HINT : null, + partialFare: partialFare, }; } return undefined; @@ -45,7 +48,7 @@ const parseTickets = (ctx, j) => { partialFare: s.teilpreis, }; if (s.teilpreis) { - p.addData = 'Teilpreis / partial fare'; + p.addData = PARTIAL_FARE_HINT; } const conds = s.konditionsAnzeigen || s.konditionen; if (conds) { diff --git a/test/fixtures/dbnav-refresh-journey.js b/test/fixtures/dbnav-refresh-journey.js index 14061a6c..2efa568b 100644 --- a/test/fixtures/dbnav-refresh-journey.js +++ b/test/fixtures/dbnav-refresh-journey.js @@ -212,6 +212,7 @@ const dbNavJourney = { amount: 43.99, currency: 'EUR', hint: null, + partialFare: false, }, tickets: [ { diff --git a/test/fixtures/dbweb-journey.js b/test/fixtures/dbweb-journey.js index 95cc75e8..62d444e4 100644 --- a/test/fixtures/dbweb-journey.js +++ b/test/fixtures/dbweb-journey.js @@ -292,7 +292,7 @@ const dbwebJourney = { }, ], refreshToken: '¶HKI¶T$A=1@O=Köln Hbf@X=6958730@Y=50943029@L=8000207@a=128@$A=1@O=Köln Messe/Deutz@X=6975000@Y=50940872@L=8003368@a=128@$202504110511$202504110512$S 12$$1$$$$$$§W$A=1@O=Köln Messe/Deutz@X=6975000@Y=50940872@L=8003368@a=128@$A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@L=8073368@a=128@$202504110512$202504110519$$$1$$$$$$§T$A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@L=8073368@a=128@$A=1@O=Nürnberg Hbf@X=11082989@Y=49445615@L=8000284@a=128@$202504110520$202504110858$ICE 523$$1$$$$$$', - price: {amount: 31.49, currency: 'EUR', hint: null}, + price: {amount: 31.49, currency: 'EUR', hint: null, partialFare: false}, tickets: [{ name: 'from', priceObj: { diff --git a/test/fixtures/dbweb-refresh-journey.js b/test/fixtures/dbweb-refresh-journey.js index 63e38d49..5cafc764 100644 --- a/test/fixtures/dbweb-refresh-journey.js +++ b/test/fixtures/dbweb-refresh-journey.js @@ -210,6 +210,7 @@ const dbJourney = { amount: 27.99, currency: 'EUR', hint: null, + partialFare: false, }, tickets: [ {