Add times
This commit is contained in:
+2
-1
@@ -5,7 +5,8 @@
|
||||
"description": "Scrapes Sofia Traffic API and serves it via a local server.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.mjs"
|
||||
"start": "node server.mjs",
|
||||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.18.1",
|
||||
|
||||
@@ -26,16 +26,19 @@ departuresRouter.get('/departures', (req, res) => {
|
||||
}
|
||||
|
||||
if (stop && line) {
|
||||
const minutes = departures
|
||||
.flatMap((stop) => {
|
||||
return stop.departures.flatMap((d) => d.next);
|
||||
})
|
||||
.map(Number)
|
||||
.toSorted((a, b) => a - b);
|
||||
const pairs = departures.flatMap((stop) => {
|
||||
return stop.departures.flatMap((d) =>
|
||||
d.next.map((minutes, i) => ({
|
||||
minutes: Number(minutes),
|
||||
time: d.nextTimes?.[i] ?? null,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
const uniqueMinutes = [...new Set(minutes)];
|
||||
const uniquePairs = [...new Map(pairs.map((p) => [p.minutes, p])).values()]
|
||||
.toSorted((a, b) => a.minutes - b.minutes);
|
||||
|
||||
return res.json(uniqueMinutes);
|
||||
return res.json(uniquePairs);
|
||||
}
|
||||
|
||||
return res.json(departures);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const ENV_URL = 'https://sofiatraffic.bg/bg/get-envs';
|
||||
export const VIRTUAL_TABLE_URL = 'https://sofiatraffic.bg/bg/trip/getVirtualTable';
|
||||
|
||||
export const TIMEZONE = 'Europe/Sofia';
|
||||
|
||||
export const TYPES = {
|
||||
1: 'bus',
|
||||
2: 'tram',
|
||||
|
||||
@@ -22,7 +22,7 @@ async function fetch(stop, tokens) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getStopDepartures(stop, tokens) {
|
||||
async function getStopDepartures(stop, tokens, scrapeTime) {
|
||||
const data = await fetch(stop, tokens);
|
||||
|
||||
if (!data) {
|
||||
@@ -30,17 +30,19 @@ async function getStopDepartures(stop, tokens) {
|
||||
}
|
||||
|
||||
return Object.keys(data).reduce((acc, key) => {
|
||||
acc.push(transformBusInfo(data[key]));
|
||||
acc.push(transformBusInfo(data[key], scrapeTime));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export async function getDepartures(stops) {
|
||||
const tokens = await getTokens();
|
||||
// One timestamp for the whole batch so concurrently fetched stops agree on "now".
|
||||
const scrapeTime = Date.now();
|
||||
|
||||
const stopPromises = stops.map(async (stop) => ({
|
||||
stop,
|
||||
departures: await getStopDepartures(stop, tokens),
|
||||
departures: await getStopDepartures(stop, tokens, scrapeTime),
|
||||
}));
|
||||
|
||||
const stopsData = await Promise.all(stopPromises);
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import { TYPES } from './config.mjs';
|
||||
import { TIMEZONE, TYPES } from './config.mjs';
|
||||
|
||||
// Explicit timeZone: the Docker container runs UTC, system time can't be trusted.
|
||||
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: TIMEZONE,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
});
|
||||
|
||||
function toClockTime(minutesFromNow, scrapeTime) {
|
||||
if (!Number.isFinite(minutesFromNow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return timeFormatter.format(new Date(scrapeTime + minutesFromNow * 60_000));
|
||||
}
|
||||
|
||||
// Example API response data structure
|
||||
// A2454_A99: {
|
||||
@@ -17,12 +33,17 @@ import { TYPES } from './config.mjs';
|
||||
// last_stop: 'A2454'
|
||||
// },
|
||||
|
||||
export function transformBusInfo(data) {
|
||||
// next[i] and nextTimes[i] describe the same departure, both derived from details[i].t.
|
||||
// Times are anchored to scrapeTime, not request time — the file may be read up to a minute later.
|
||||
export function transformBusInfo(data, scrapeTime = Date.now()) {
|
||||
const minutes = data.details.map((d) => d.t);
|
||||
|
||||
return {
|
||||
type: TYPES[data.type],
|
||||
name: data.name,
|
||||
destination: data.st_name,
|
||||
destination_stop: data.st_code,
|
||||
next: data.details.map((d) => d.t),
|
||||
next: minutes,
|
||||
nextTimes: minutes.map((m) => toClockTime(m, scrapeTime)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { transformBusInfo } from '../src/scraper/transform-bus-info.mjs';
|
||||
|
||||
function busInfo(details) {
|
||||
return {
|
||||
type: 1,
|
||||
name: '84',
|
||||
st_name: 'ЛЕТИЩЕ СОФИЯ ТЕРМИНАЛ 2',
|
||||
st_code: '2454',
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
test('formats times in Europe/Sofia regardless of system timezone (summer, UTC+3)', () => {
|
||||
const scrapeTime = Date.parse('2026-07-10T10:00:00Z'); // 13:00 in Sofia
|
||||
|
||||
const result = transformBusInfo(busInfo([{ t: 0 }, { t: 5 }]), scrapeTime);
|
||||
|
||||
assert.deepEqual(result.next, [0, 5]);
|
||||
assert.deepEqual(result.nextTimes, ['13:00', '13:05']);
|
||||
});
|
||||
|
||||
test('winter time uses UTC+2', () => {
|
||||
const scrapeTime = Date.parse('2026-01-15T10:00:00Z'); // 12:00 in Sofia
|
||||
|
||||
const result = transformBusInfo(busInfo([{ t: 30 }]), scrapeTime);
|
||||
|
||||
assert.deepEqual(result.nextTimes, ['12:30']);
|
||||
});
|
||||
|
||||
test('rolls over midnight via date arithmetic', () => {
|
||||
const scrapeTime = Date.parse('2026-07-10T20:55:00Z'); // 23:55 in Sofia
|
||||
|
||||
const result = transformBusInfo(busInfo([{ t: 10 }]), scrapeTime);
|
||||
|
||||
assert.deepEqual(result.nextTimes, ['00:05']);
|
||||
});
|
||||
|
||||
test('pads single-digit hours and minutes, no AM/PM', () => {
|
||||
const scrapeTime = Date.parse('2026-07-10T06:03:00Z'); // 09:03 in Sofia
|
||||
|
||||
const result = transformBusInfo(busInfo([{ t: 0 }]), scrapeTime);
|
||||
|
||||
assert.deepEqual(result.nextTimes, ['09:03']);
|
||||
});
|
||||
|
||||
test('empty details produce empty arrays', () => {
|
||||
const result = transformBusInfo(busInfo([]), Date.now());
|
||||
|
||||
assert.deepEqual(result.next, []);
|
||||
assert.deepEqual(result.nextTimes, []);
|
||||
});
|
||||
|
||||
test('non-numeric t yields null time and keeps arrays index-aligned', () => {
|
||||
const scrapeTime = Date.parse('2026-07-10T10:00:00Z');
|
||||
|
||||
const result = transformBusInfo(busInfo([{ t: 2 }, { t: null }, { t: 10 }]), scrapeTime);
|
||||
|
||||
assert.deepEqual(result.next, [2, null, 10]);
|
||||
assert.deepEqual(result.nextTimes, ['13:02', null, '13:10']);
|
||||
assert.equal(result.next.length, result.nextTimes.length);
|
||||
});
|
||||
Reference in New Issue
Block a user