Comparing version 0.0.5 to 0.1.0
@@ -10,14 +10,14 @@ 'use strict' | ||
const getNewToken = () => | ||
got.post("https://api.cp.pt/cp-api/oauth/token", { | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': 'Basic Y3AtbW9iaWxlOnBhc3M=', // Base64 of "cp-mobile:pass" | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
}, | ||
body: stringify({ | ||
grant_type: 'client_credentials' | ||
}) | ||
}) | ||
.then((res) => JSON.parse(res.body)) | ||
.then((res) => res.access_token) | ||
got.post('https://api.cp.pt/cp-api/oauth/token', { | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': 'Basic Y3AtbW9iaWxlOnBhc3M=', // Base64 of "cp-mobile:pass" | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
}, | ||
body: stringify({ | ||
grant_type: 'client_credentials' | ||
}) | ||
}) | ||
.then((res) => JSON.parse(res.body)) | ||
.then((res) => res.access_token) | ||
@@ -27,50 +27,46 @@ const savedToken = () => token ? Promise.resolve(token) : renewSavedToken() | ||
const renewSavedToken = () => | ||
getNewToken() | ||
.then((res) => { | ||
token = res | ||
return res | ||
}) | ||
getNewToken() | ||
.then((res) => { | ||
token = res | ||
return res | ||
}) | ||
const getRequest = (url, params = {}) => async (token) => { | ||
const res = await got.get(url, { | ||
json: true, | ||
query: params, | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': `Bearer ${token}` | ||
} | ||
}) | ||
return res.body | ||
const res = await got.get(url, { | ||
json: true, | ||
query: params, | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': `Bearer ${token}` | ||
} | ||
}) | ||
return res.body | ||
} | ||
const get = (url, params) => | ||
retry(() => savedToken() | ||
.then(getRequest(url, params)) | ||
.catch((error) => renewSavedToken), // todo: handling non-token-specific errors | ||
{retries: 3} | ||
) | ||
const postRequest = (url, body = {}) => async (token) => { | ||
const res = await got.post(url, { | ||
body: JSON.stringify(body), | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': `Bearer ${token}`, | ||
'Content-Type': 'application/json' | ||
} | ||
}) | ||
return JSON.parse(res.body) | ||
const res = await got.post(url, { | ||
body: JSON.stringify(body), | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': `Bearer ${token}`, | ||
'Content-Type': 'application/json' | ||
} | ||
}) | ||
return JSON.parse(res.body) | ||
} | ||
const post = (url, body) => | ||
retry(() => savedToken() | ||
.then(postRequest(url, body)) | ||
.catch((error) => renewSavedToken), // todo: handling non-token-specific errors | ||
{retries: 3} | ||
) | ||
const requestWithRetry = request => retry( | ||
() => savedToken().then(request).catch(error => { | ||
renewSavedToken() | ||
throw error | ||
}), | ||
{ retries: 1 } | ||
) | ||
const get = (url, params) => requestWithRetry(getRequest(url, params)) | ||
const post = (url, body) => requestWithRetry(postRequest(url, body)) | ||
module.exports = { | ||
get, | ||
post | ||
get, | ||
post | ||
} |
'use strict' | ||
const post = require('./fetch').post | ||
const { post: postRequest } = require('./fetch') | ||
const isString = require('lodash/isString') | ||
const isDate = require('lodash/isDate') | ||
const sortBy = require('lodash/sortBy') | ||
const get = require('lodash/get') | ||
const pick = require('lodash/pick') | ||
const merge = require('lodash/merge') | ||
const moment = require('moment') | ||
require('moment-duration-format') | ||
const momentTz = require('moment-timezone') // cheap hack for moment-duration-format fail | ||
const retry = require('p-retry') | ||
const clone = (x) => JSON.parse(JSON.stringify(x)) | ||
const { operator, createStation, buildTripId } = require('./helpers') | ||
const createPrice = (j) => (p) => ({ | ||
class: +p.travelClass, | ||
amount: +p.centsValue / 100, | ||
currency: 'EUR', | ||
fareType: p.priceType, | ||
saleURL: (j.saleLink && j.saleLink.code) ? j.saleLink.code : null // todo: saleableOnline | ||
class: +p.travelClass, | ||
amount: +p.centsValue / 100, | ||
currency: 'EUR', | ||
fareType: p.priceType, | ||
url: (j.saleLink && j.saleLink.code) ? j.saleLink.code : null // todo: saleableOnline | ||
}) | ||
const createStation = (s) => ({ | ||
type: 'station', | ||
id: s.code, | ||
name: s.designation | ||
}) | ||
const hashLegs = legs => legs.map(leg => [leg.tripId, leg.origin.id, leg.destination.id].join('@')).join('-') | ||
const hashLeg = (date) => (s) => date+'@'+s.departureStation.code+'@'+s.departureTime+'@'+s.arrivalStation.code+'@'+s.arrivalTime+'@'+s.trainNumber+'@'+s.duration | ||
const hashJourney = (sections, date) => sections.map(hashLeg(date)).join('-') | ||
const createJourney = (date, formattedDate) => (j) => { | ||
// legs | ||
let lastTime = 0 | ||
const legs = [] | ||
const sections = sortBy(j.travelSections, (x) => x.sequenceNumber) | ||
for (let section of sections) { | ||
const leg = { | ||
tripId: buildTripId(section.trainNumber, formattedDate), | ||
origin: createStation(section.departureStation), | ||
destination: createStation(section.arrivalStation), | ||
departurePlatform: section.departurePlatform, | ||
arrivalPlatform: section.arrivalPlatform, | ||
line: { | ||
type: 'line', | ||
id: section.trainNumber + '', | ||
name: section.trainNumber + '', | ||
product: get(section, 'serviceCode.designation'), | ||
productCode: get(section, 'serviceCode.code'), | ||
mode: 'train', // @todo | ||
public: true, | ||
operator | ||
}, | ||
mode: 'train', // todo | ||
public: true, | ||
operator | ||
} | ||
const createJourney = (date) => (j) => { | ||
const journey = { | ||
type: 'journey' | ||
} | ||
// departure time | ||
const departureTime = +moment.duration(section.departureTime) | ||
if (departureTime < lastTime) date.add(1, 'days') | ||
leg.departure = moment.tz(date.format('DD.MM.YYYY') + ' ' + section.departureTime, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toISOString() | ||
lastTime = departureTime | ||
// legs | ||
let lastTime = 0 | ||
const legs = [] | ||
const sections = sortBy(j.travelSections, (x) => x.sequenceNumber) | ||
for(let section of sections){ | ||
const leg = { | ||
origin: createStation(section.departureStation), | ||
destination: createStation(section.arrivalStation), | ||
departurePlatform: section.departurePlatform, | ||
arrivalPlatform: section.arrivalPlatform, | ||
trainNumber: section.trainNumber, | ||
service: section.serviceCode ? { | ||
name: section.serviceCode.designation, | ||
code: section.serviceCode.code | ||
} : null, | ||
mode: 'train', // todo | ||
public: true, | ||
operator: { | ||
type: 'operator', | ||
id: 'cp', | ||
name: 'Comboios de Portugal', | ||
url: 'https://www.cp.pt/' | ||
} | ||
} | ||
const stopovers = [] | ||
for (let s of section.trainStops) { | ||
const stopover = { | ||
type: 'stopover', | ||
stop: { | ||
type: 'station', | ||
id: s.station.code, | ||
name: s.station.designation | ||
}, | ||
arrivalPlatform: s.platform ? s.platform + '' : null, | ||
departurePlatform: s.platform ? s.platform + '' : null | ||
} | ||
// departure time | ||
const departureTime = +moment.duration(section.departureTime) | ||
if(departureTime < lastTime) date.add(1, 'days') | ||
leg.departure = moment.tz(date.format('DD.MM.YYYY')+' '+section.departureTime, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toDate() | ||
lastTime = departureTime | ||
const arrivalTime = +moment.duration(s.arrival) | ||
if (arrivalTime < lastTime) date.add(1, 'days') | ||
stopover.arrival = moment.tz(date.format('DD.MM.YYYY') + ' ' + s.arrival, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toISOString() | ||
lastTime = arrivalTime | ||
const departureTime = +moment.duration(s.departure) | ||
if (departureTime < lastTime) date.add(1, 'days') | ||
stopover.departure = moment.tz(date.format('DD.MM.YYYY') + ' ' + s.departure, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toISOString() | ||
lastTime = departureTime | ||
const stops = [] | ||
for(let s of section.trainStops){ | ||
const station = { | ||
type: 'station', | ||
id: s.station.code, | ||
name: s.station.designation, | ||
platform: s.platform | ||
} | ||
// sort keys | ||
stopovers.push(pick(stopover, ['type', 'stop', 'arrival', 'arrivalPlatform', 'departure', 'departurePlatform'])) | ||
} | ||
const arrivalTime = +moment.duration(s.arrival) | ||
if(arrivalTime < lastTime) date.add(1, 'days') | ||
station.arrival = moment.tz(date.format('DD.MM.YYYY')+' '+s.arrival, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toDate() | ||
lastTime = arrivalTime | ||
const departureTime = +moment.duration(s.departure) | ||
if(departureTime < lastTime) date.add(1, 'days') | ||
station.departure = moment.tz(date.format('DD.MM.YYYY')+' '+s.departure, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toDate() | ||
lastTime = departureTime | ||
// arrival time | ||
const arrivalTime = +moment.duration(section.arrivalTime) | ||
if (arrivalTime < lastTime) date.add(1, 'days') | ||
leg.arrival = moment.tz(date.format('DD.MM.YYYY') + ' ' + section.arrivalTime, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toISOString() | ||
lastTime = arrivalTime | ||
stops.push(station) | ||
} | ||
leg.stopovers = stopovers | ||
// arrival time | ||
const arrivalTime = +moment.duration(section.arrivalTime) | ||
if(arrivalTime < lastTime) date.add(1, 'days') | ||
leg.arrival = moment.tz(date.format('DD.MM.YYYY')+' '+section.arrivalTime, 'DD.MM.YYYY HH:mm', 'Europe/Lisbon').toDate() | ||
lastTime = arrivalTime | ||
// sort keys | ||
legs.push(pick(leg, ['tripId', 'origin', 'destination', 'departure', 'departurePlatform', 'arrival', 'arrivalPlatform', 'line', 'mode', 'public', 'operator', 'stopovers'])) | ||
} | ||
leg.stops = stops | ||
const journey = { | ||
type: 'journey', | ||
id: hashLegs(legs), | ||
legs | ||
} | ||
legs.push(leg) | ||
} | ||
// prices | ||
const prices = j.basePrices.map(createPrice(j)).filter((x) => x.amount > 0) | ||
const sortedPrices = sortBy(prices, 'amount') | ||
if (sortedPrices.length > 0) { | ||
journey.price = { | ||
...prices[0], | ||
fares: prices | ||
} | ||
} | ||
// id | ||
journey.id = hashJourney(sections, date.format("DD.MM.YYYY")) | ||
return journey | ||
} | ||
journey.legs = legs | ||
const defaults = () => ({ | ||
when: new Date() | ||
}) | ||
// prices | ||
let prices = j.basePrices.map(createPrice(j)) | ||
prices = sortBy(prices.filter((x) => x.amount > 0), (x) => x.amount) | ||
if(prices.length > 0){ | ||
const lowestPrice = clone(prices[0]) | ||
journey.price = lowestPrice | ||
const journeys = async (origin, destination, opt = {}) => { | ||
if (isString(origin)) origin = { id: origin, type: 'station' } | ||
if (isString(destination)) destination = { id: destination, type: 'station' } | ||
if (!isString(origin.id)) throw new Error('invalid or missing origin id') | ||
if (origin.type !== 'station') throw new Error('invalid or missing origin type, must be station') | ||
if (!isString(destination.id)) throw new Error('invalid or missing destination id') | ||
if (destination.type !== 'station') throw new Error('invalid or missing destination type, must be station') | ||
if(prices.length > 1){ | ||
journey.price.fares = clone(prices) | ||
} | ||
} | ||
origin = origin.id | ||
destination = destination.id | ||
return journey | ||
} | ||
const options = merge({}, defaults(), opt) | ||
if (!isDate(options.when)) throw new Error('opt.when must be a JS date object') | ||
const date = momentTz.tz(options.when, 'Europe/Lisbon') | ||
const formattedDate = date.format('YYYY-MM-DD') | ||
const journeys = (origin, destination, date = new Date()) => { | ||
if(isString(origin)) origin = {id: origin, type: 'station'} | ||
if(!isString(origin.id)) throw new Error('invalid or missing origin id') | ||
if(origin.type !== 'station') throw new Error('invalid or missing origin type') | ||
origin = origin.id | ||
if(isString(destination)) destination = {id: destination, type: 'station'} | ||
if(!isString(destination.id)) throw new Error('invalid or missing destination id') | ||
if(destination.type !== 'station') throw new Error('invalid or missing destination type') | ||
destination = destination.id | ||
if(!isDate(date)){ | ||
throw new Error('invalid `date` parameter') | ||
} | ||
date = momentTz.tz(date, 'Europe/Lisbon') | ||
const formattedDate = date.format('YYYY-MM-DD') | ||
return post("https://api.cp.pt/cp-api/siv/travel/search", { | ||
departureStationCode: origin, | ||
arrivalStationCode: destination, | ||
classes: [1,2], // todo | ||
searchType: 3, | ||
travelDate: formattedDate, | ||
returnDate: null, // todo | ||
timeLimit: null | ||
}) | ||
.then((res) => res.outwardTrip.map(createJourney(date))) // todo: check if date = res.travelDate | ||
const { outwardTrip } = await postRequest('https://api.cp.pt/cp-api/siv/travel/search', { | ||
departureStationCode: origin, | ||
arrivalStationCode: destination, | ||
classes: [1, 2], // todo | ||
searchType: 3, | ||
travelDate: formattedDate, | ||
returnDate: null, // todo | ||
timeLimit: null | ||
}) | ||
return outwardTrip.map(createJourney(date, formattedDate)) // todo: check if date = res.travelDate | ||
} | ||
module.exports = (origin, destination, date = new Date()) => retry(() => journeys(origin, destination, date), {retries: 3}) | ||
module.exports = journeys |
'use strict' | ||
const get = require('./fetch').get | ||
const retry = require('p-retry') | ||
const { get: getRequest } = require('./fetch') | ||
const createStation = (s) => ({ | ||
type: 'station', | ||
id: s.code, | ||
name: s.designation, | ||
coordinates: !(s.latitude && s.longitude) ? null : { | ||
longitude: +s.longitude, | ||
latitude: +s.latitude | ||
} | ||
type: 'station', | ||
id: s.code, | ||
name: s.designation, | ||
location: !(s.latitude && s.longitude) ? undefined : { | ||
type: 'location', | ||
longitude: +s.longitude, | ||
latitude: +s.latitude | ||
} | ||
}) | ||
const stations = () => | ||
get("https://api.cp.pt/cp-api/siv/stations/") | ||
.then((res) => res.map(createStation)) | ||
getRequest('https://api.cp.pt/cp-api/siv/stations/') | ||
.then((res) => res.map(createStation)) | ||
module.exports = () => retry(() => stations(), {retries: 3}) | ||
module.exports = stations |
{ | ||
"name": "comboios", | ||
"version": "0.1.0", | ||
"description": "Comboios de Portugal (CP, Portugese Railways) API client.", | ||
"version": "0.0.5", | ||
"keywords": [ | ||
"api", | ||
"client", | ||
"comboios", | ||
"cp", | ||
"cp.pt", | ||
"comboios", | ||
"de", | ||
@@ -13,34 +15,43 @@ "portugal", | ||
"railway", | ||
"train", | ||
"api", | ||
"client" | ||
"train" | ||
], | ||
"author": "Julius Tens <mail@juliustens.eu>", | ||
"homepage": "https://github.com/juliuste/comboios", | ||
"bugs": "https://github.com/juliuste/comboios/issues", | ||
"repository": "juliuste/comboios", | ||
"bugs": "https://github.com/juliuste/comboios/issues", | ||
"main": "./index.js", | ||
"license": "ISC", | ||
"author": "Julius Tens <mail@juliustens.eu>", | ||
"files": [ | ||
"index.js", | ||
"lib/*" | ||
], | ||
"main": "lib/index.js", | ||
"scripts": { | ||
"check-deps": "depcheck", | ||
"fix": "eslint --fix lib test.js", | ||
"lint": "eslint lib test.js", | ||
"prepublishOnly": "npm test", | ||
"test": "npm run lint && npm run check-deps && node test" | ||
}, | ||
"dependencies": { | ||
"got": "^9.1.0", | ||
"lodash": "^4.17.10", | ||
"moment": "^2.22.2", | ||
"got": "^9.6.0", | ||
"lodash": "^4.17.11", | ||
"moment": "^2.24.0", | ||
"moment-duration-format": "^2.2.2", | ||
"moment-timezone": "^0.5.21", | ||
"p-retry": "^2.0.0" | ||
"moment-timezone": "^0.5.23", | ||
"p-retry": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"tape": "^4.9.1" | ||
"depcheck": "^0.7.2", | ||
"eslint": "^5.15.3", | ||
"eslint-config-standard": "^12.0.0", | ||
"eslint-plugin-import": "^2.16.0", | ||
"eslint-plugin-node": "^8.0.1", | ||
"eslint-plugin-promise": "^4.0.1", | ||
"eslint-plugin-standard": "^4.0.0", | ||
"tape": "^4.10.1", | ||
"tape-promise": "^4.0.0", | ||
"validate-fptf": "^3.0.0" | ||
}, | ||
"scripts": { | ||
"test": "node test.js", | ||
"prepublishOnly": "npm test" | ||
}, | ||
"engines": { | ||
"node": ">=8" | ||
}, | ||
"license": "ISC" | ||
} | ||
} |
# comboios | ||
Client for the [Comboios de Porgual]() (CP, Portugese Railways) REST API. Inofficial, please ask CP for permission before using this module in production. | ||
JavaScript client for the Portugese 🇵🇹 [Comboios de Portugal (CP)](https://www.cp.pt/) railway API. Complies with the friendly public transport format. Inofficial, using *CP* endpoints. Ask them for permission before using this module in production. | ||
@@ -9,3 +9,3 @@ [![npm version](https://img.shields.io/npm/v/comboios.svg)](https://www.npmjs.com/package/comboios) | ||
[![dependency status](https://img.shields.io/david/juliuste/comboios.svg)](https://david-dm.org/juliuste/comboios) | ||
[![license](https://img.shields.io/github/license/juliuste/comboios.svg?style=flat)](LICENSE) | ||
[![license](https://img.shields.io/github/license/juliuste/comboios.svg?style=flat)](license) | ||
[![chat on gitter](https://badges.gitter.im/juliuste.svg)](https://gitter.im/juliuste) | ||
@@ -16,3 +16,3 @@ | ||
```shell | ||
npm install --save comboios | ||
npm install comboios | ||
``` | ||
@@ -22,15 +22,19 @@ | ||
This package mostly returns data in the [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format): | ||
```javascript | ||
const comboios = require('comboios') | ||
``` | ||
- [`stations()`](docs/stations.md) - List of operated stations | ||
- [`departures(station, date = new Date())`](docs/departures.md) - Departures at a given station | ||
- [`trains(trainNumber, date = new Date())`](docs/trains.md) - Schedule for a given train | ||
- [`journeys(origin, destination, date = new Date())`](docs/journeys.md) - Journeys between stations | ||
This package contains data in the [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format). | ||
- [`stations()`](docs/stations.md) - to get a list of operated stations such as `Lisboa - Oriente` or `Viana do Castelo` | ||
- [`journeys(origin, destination, opt)`](docs/journeys.md) - to get routes between stations | ||
- [`stopovers(station, opt)`](docs/stopovers.md) - to get departures and arrivals at a given station | ||
- [`trip(id)`](docs/trip.md) - to get all stopovers for a given trip (train) | ||
## See also | ||
- [build-cp-gtfs](https://github.com/juliuste/build-cp-gtfs) - Generate CP GTFS using this module | ||
- [european-transport-operators](https://github.com/public-transport/european-transport-operators) - List of european long-distance transport operators, available API endpoints, GTFS feeds and client modules. | ||
## Contributing | ||
If you found a bug, want to propose a feature or feel the urge to complain about your life, feel free to visit [the issues page](https://github.com/juliuste/comboios/issues). | ||
If you found a bug or want to propose a feature, feel free to visit [the issues page](https://github.com/juliuste/comboios/issues). |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17028
10
356
38
10
1
+ Added@types/retry@0.12.0(transitive)
+ Addedp-retry@4.6.2(transitive)
+ Addedretry@0.13.1(transitive)
- Removedp-retry@2.0.0(transitive)
- Removedretry@0.12.0(transitive)
Updatedgot@^9.6.0
Updatedlodash@^4.17.11
Updatedmoment@^2.24.0
Updatedmoment-timezone@^0.5.23
Updatedp-retry@^4.0.0