Skip to content

Commit

Permalink
Adds Train API and resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert27 committed Apr 9, 2024
1 parent 742db9f commit d118730
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 4 deletions.
4 changes: 2 additions & 2 deletions src/resolvers/charging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const URL =
'https://app.chargecloud.de/emobility:ocpi/7d25c525838f55d21766c0dfee5ad21f/app/2.0/locations?swlat=48.7555&swlng=11.4146&nelat=48.7767&nelng=11.4439'
export const charging = async (): Promise<ChargingData[]> => {
try {
const data = cache.get<ChargingData[]>('charging-stations')
const data = cache.get<ChargingData[]>('chargingStations')
if (data == null) {
const resp = await fetch(URL)
const data = await resp.json()
Expand Down Expand Up @@ -46,7 +46,7 @@ export const charging = async (): Promise<ChargingData[]> => {
?.freeParking ?? null,
})
)
cache.set('charging-stations', result, 60)
cache.set('chargingStations', result, 60)
return result
}
return data
Expand Down
4 changes: 2 additions & 2 deletions src/resolvers/cl-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { cache } from '../..'
const CACHE_TTL = 60 * 60 * 24 // 24 hours

export async function clEvents(): Promise<ClEvent[]> {
let clEvents: ClEvent[] | undefined = await cache.get('mensa')
let clEvents: ClEvent[] | undefined = await cache.get('clEvents')

if (clEvents === undefined || clEvents === null) {
clEvents = await getClEvents()
cache.set(`mensa`, clEvents, CACHE_TTL)
cache.set(`clEvents`, clEvents, CACHE_TTL)
}

return clEvents
Expand Down
2 changes: 2 additions & 0 deletions src/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { charging } from './charging'
import { clEvents } from './cl-events'
import { food } from './food'
import { parking } from './parking'
import { train } from './train'

export const resolvers = {
Query: {
Expand All @@ -11,5 +12,6 @@ export const resolvers = {
food,
clEvents,
bus,
train,
},
}
22 changes: 22 additions & 0 deletions src/resolvers/train.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import getTrain from '@/scraping/train'

import { cache } from '../..'

const CACHE_TTL = 60 // 1 minute

export async function train(
_: any,
args: { station: string }
): Promise<Train[]> {
let trainData: Train[] | undefined = await cache.get(
`train__${args.station}`
)

if (trainData === undefined || trainData === null) {
trainData = await getTrain(args.station)

cache.set(`train__${args.station}`, trainData, CACHE_TTL)
}

return trainData
}
3 changes: 3 additions & 0 deletions src/schema/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ import { chargingType } from './charging'
import { clEventsType } from './cl-events'
import { foodType } from './food'
import { parkingType } from './parking'
import { trainType } from './train'

export const queryType = gql`
${chargingType}
${parkingType}
${foodType}
${clEventsType}
${busType}
${trainType}
"Root query"
type Query {
parking: ParkingData
charging: [ChargingStation!]!
food(locations: [String!]): [Food!]!
clEvents: [ClEvent!]!
bus(station: String!): [Bus!]!
train(station: String!): [Train!]!
}
`
14 changes: 14 additions & 0 deletions src/schema/query/train.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import gql from 'graphql-tag'

export const trainType = gql`
"Train data"
type Train {
name: String!
destination: String!
plannedTime: String!
actualTime: String!
canceled: Boolean!
track: String
url: String
}
`
108 changes: 108 additions & 0 deletions src/scraping/train.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as cheerio from 'cheerio'
import { GraphQLError } from 'graphql'

const url =
'https://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?ld=43120&protocol=https:&rt=1&use_realtime_filter=1&'
const stations: Record<string, string> = {
nord: 'Ingolstadt Nord#008003076',
hbf: 'Ingolstadt Hbf',
audi: 'Ingolstadt Audi#008003074',
}

function dateFromTimestring(str: string): Date {
const match = str.match(/(\d\d):(\d\d)/) ?? []
const [, hourStr, minuteStr] = match
const hour = parseInt(String(hourStr))
const minute = parseInt(String(minuteStr))
const now = new Date()

if (
now.getHours() > hour ||
(now.getHours() === hour && now.getMinutes() > minute)
) {
now.setDate(now.getDate() + 1)
}

now.setHours(hour)
now.setMinutes(minute)
return now
}

export default async function getTrain(station: string): Promise<Train[]> {
if (!Object.prototype.hasOwnProperty.call(stations, station)) {
throw new GraphQLError(
'Invalid station ID. Valid IDs are: ' +
Object.keys(stations).join(', ')
)
}

try {
const getTrainDepatures = async (): Promise<Train[]> => {
const now = new Date()
const pad2 = (x: number): string => x.toString().padStart(2, '0')

const paramObj = {
input: stations[station],
inputRef: '#',
date: `+${pad2(now.getDate())}.${pad2(
now.getMonth() + 1
)}.${now.getFullYear()}`,
time: `${pad2(now.getHours())}:${pad2(now.getMinutes())}`, //
productsFilter: '1111101000000000', // "Nur Bahn"
REQTrain_name: '',
maxJourneys: 10,
start: 'Suchen',
boardType: 'Abfahrt',
ao: 'yes',
}

const params = new URLSearchParams()
for (const key in paramObj) {
params.append(
key,
(paramObj as unknown as Record<any, string>)[key]
)
}

const resp = await fetch(url, {
method: 'POST',
body: params,
})
const body = await resp.text()
if (resp.status !== 200) {
throw new Error('Train data not available')
}

const $ = cheerio.load(body)
const departures = $('.sqdetailsDep').map((i, el) => {
const name = $(el)
.find('.bold')
.eq(0)
.text()
.trim()
.replace(/\s+/g, ' ')
const planned = $(el).find('.bold').eq(1).text().trim()
const actual =
$(el).find('.delayOnTime').text().trim() ?? planned
const text = $(el).text().trim()
const canceled = $(el).find('.red').length > 0
const match = text.match(/>>\n(.*)/) ?? ['']
const destination = match[1]
return {
name,
destination,
plannedTime: dateFromTimestring(planned),
actualTime: dateFromTimestring(actual),
canceled,
track: text.substr(text.length - 2).trim(),
url: $(el).find('a').attr('href') ?? null,
}
})

return departures.get()
}
return await getTrainDepatures()
} catch (e: any) {
throw new GraphQLError('Failed to fetch data: ' + e.message)
}
}
9 changes: 9 additions & 0 deletions src/types/train.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
interface Train {
name: string
destination: string
plannedTime: Date
actualTime: Date
canceled: boolean
track: string
url: string | null
}

0 comments on commit d118730

Please sign in to comment.