mirror of
https://github.com/kaaninchen/Gleiswechsel.git
synced 2026-09-17 16:52:47 +00:00
Compare commits
28
Commits
dbf
...
27f8ef2cbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27f8ef2cbc | ||
|
|
36a1db0647 | ||
|
|
8e9fa7c2f2 | ||
|
|
41e185a211 | ||
|
|
66289e8572 | ||
|
|
ff8c2f55db | ||
|
|
ca3f93f4cd | ||
|
|
cdceeb4e44 | ||
|
|
2ec38c1874 | ||
|
|
c563062b9a | ||
|
|
a75b12ea31 | ||
|
|
1b0546ff4d | ||
|
|
4598dcdc08 | ||
|
|
10e1ea8ade | ||
|
|
a4d9cb36f0 | ||
|
|
5a2d0bf8d6 | ||
|
|
1b0b10dc45 | ||
|
|
1892c734cd | ||
|
|
f3d9a25f95 | ||
|
|
53358c08b1 | ||
|
|
46fe9eb61e | ||
|
|
697a0f6218 | ||
|
|
cf696df5f5 | ||
|
|
f2a686edca | ||
|
|
8770d03bb6 | ||
|
|
826b543347 | ||
|
|
adfa0a0440 | ||
|
|
5f6c3a223b |
@@ -3,3 +3,4 @@ __pycache__/
|
||||
src/data/announcements/*
|
||||
!src/data/announcements/.gitkeep
|
||||
config.json
|
||||
stations.json
|
||||
+21
-8
@@ -1,20 +1,33 @@
|
||||
{
|
||||
"discord": {
|
||||
"token": "",
|
||||
"stations": ["Hamburg Hbf", "München Hbf", "Köln Hbf", "Amsterdam Centraal"],
|
||||
"dbf": "https://dbf.finalrewind.org",
|
||||
"server": ,
|
||||
"vc": ,
|
||||
"random": true,
|
||||
"emojis": true,
|
||||
"formatting": "┇",
|
||||
"announcements": true,
|
||||
"voice_announcements: [
|
||||
"emojis": true
|
||||
},
|
||||
"connections": {
|
||||
"stations": [
|
||||
""
|
||||
],
|
||||
"blacklist": [],
|
||||
"min_duration": 5,
|
||||
"max_duration": null,
|
||||
"max_wait_time": 6,
|
||||
"timezone": "Europe/Berlin"
|
||||
},
|
||||
"announcements": {
|
||||
"enabled": true,
|
||||
"voice": [
|
||||
{
|
||||
"enabled": false,
|
||||
"stations": {
|
||||
"general": ""
|
||||
"general": "general.aac",
|
||||
}
|
||||
}
|
||||
]
|
||||
"blacklist": []
|
||||
},
|
||||
"http": {
|
||||
"user_agent": "Gleiswechsel-Discord-Bot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
{
|
||||
"duration": 1440,
|
||||
"startTime": "2026-08-16T21:25:00Z",
|
||||
"endTime": "2026-08-16T21:49:00Z",
|
||||
"transfers": 0,
|
||||
"id": "CogBCgI1NBIiMjAyNjA4MTZfMjM6MjVfbmwtT3Blbk9WXzM3Njc3ODE5OBoRbmwtT3Blbk9WXzM5ODA3NDch/ilVouwlSkAp147iHHX0E0AyEW5sLU9wZW5PVl8zOTc5OTIwOZRIopdRMEpAQcGxZ89lmhNASKzViNQGUMzgiNQGWgZTVUJXQVlgAQ==",
|
||||
"legs": [
|
||||
{
|
||||
"mode": "SUBWAY",
|
||||
"from": {
|
||||
"name": "Amsterdam, Gein",
|
||||
"stopId": "nl-OpenOV_3980747",
|
||||
"parentId": "nl-OpenOV_NL:S:30009530",
|
||||
"importance": 0.018040798604488373,
|
||||
"lat": 52.296284,
|
||||
"lon": 4.988728000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"departure": "2026-08-16T21:25:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:25:00Z",
|
||||
"scheduledTrack": "1",
|
||||
"track": "1",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NOT_ALLOWED",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS train",
|
||||
"descriptionText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS trains to Zuid or at Spaklerweg for Overamstel. Details: gvb.nl"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
"to": {
|
||||
"name": "Amsterdam, Centraal Station",
|
||||
"stopId": "nl-OpenOV_3979920",
|
||||
"parentId": "nl-OpenOV_NL:S:30009500",
|
||||
"importance": 0.17953550815582275,
|
||||
"lat": 52.377489999999995,
|
||||
"lon": 4.900779000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:49:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:49:00Z",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NOT_ALLOWED",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Neem M52 naar Zuid voor RAI en de richting Isolatorweg. Details: gvb.nl/zomermetro|-- Take M52 to Zuid for RAI and the direction Isolatorweg",
|
||||
"descriptionText": "Neem M52 naar Zuid voor RAI en de richting Isolatorweg. Details: gvb.nl/zomermetro|-- Take M52 to Zuid for RAI and the direction Isolatorweg. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"HIGHSPEED_RAIL",
|
||||
"LONG_DISTANCE",
|
||||
"NIGHT_RAIL",
|
||||
"REGIONAL_RAIL",
|
||||
"SUBWAY",
|
||||
"TRAM",
|
||||
"BUS",
|
||||
"FERRY"
|
||||
]
|
||||
},
|
||||
"duration": 1440,
|
||||
"startTime": "2026-08-16T21:25:00Z",
|
||||
"endTime": "2026-08-16T21:49:00Z",
|
||||
"scheduledStartTime": "2026-08-16T21:25:00Z",
|
||||
"scheduledEndTime": "2026-08-16T21:49:00Z",
|
||||
"realTime": true,
|
||||
"scheduled": true,
|
||||
"interlineWithPreviousLeg": false,
|
||||
"headsign": "Centraal Station",
|
||||
"tripFrom": {
|
||||
"name": "Amsterdam, Gein",
|
||||
"stopId": "nl-OpenOV_3980747",
|
||||
"parentId": "nl-OpenOV_NL:S:30009530",
|
||||
"importance": 0.018040798604488373,
|
||||
"lat": 52.296284,
|
||||
"lon": 4.988728000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"departure": "2026-08-16T21:25:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:25:00Z",
|
||||
"scheduledTrack": "1",
|
||||
"track": "1",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NOT_ALLOWED",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS train",
|
||||
"descriptionText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS trains to Zuid or at Spaklerweg for Overamstel. Details: gvb.nl"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
"tripTo": {
|
||||
"name": "Amsterdam, Centraal Station",
|
||||
"stopId": "nl-OpenOV_3979920",
|
||||
"parentId": "nl-OpenOV_NL:S:30009500",
|
||||
"importance": 0.17953550815582275,
|
||||
"lat": 52.377489999999995,
|
||||
"lon": 4.900779000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:49:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:49:00Z",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NOT_ALLOWED",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Neem M52 naar Zuid voor RAI en de richting Isolatorweg. Details: gvb.nl/zomermetro|-- Take M52 to Zuid for RAI and the direction Isolatorweg",
|
||||
"descriptionText": "Neem M52 naar Zuid voor RAI en de richting Isolatorweg. Details: gvb.nl/zomermetro|-- Take M52 to Zuid for RAI and the direction Isolatorweg. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"HIGHSPEED_RAIL",
|
||||
"LONG_DISTANCE",
|
||||
"NIGHT_RAIL",
|
||||
"REGIONAL_RAIL",
|
||||
"SUBWAY",
|
||||
"TRAM",
|
||||
"BUS",
|
||||
"FERRY"
|
||||
]
|
||||
},
|
||||
"routeId": "nl-OpenOV_152364",
|
||||
"routeUrl": "",
|
||||
"directionId": "0",
|
||||
"routeColor": "fff200",
|
||||
"routeTextColor": "000000",
|
||||
"routeType": 1,
|
||||
"agencyName": "GVB",
|
||||
"agencyUrl": "http://www.gvb.nl/klantenservice",
|
||||
"agencyFareUrl": "",
|
||||
"agencyId": "GVB",
|
||||
"tripId": "20260816_23:25_nl-OpenOV_376778198",
|
||||
"routeShortName": "54",
|
||||
"routeLongName": "Lijn 54",
|
||||
"tripShortName": "",
|
||||
"displayName": "54",
|
||||
"cancelled": false,
|
||||
"source": "nl_OpenOV.gtfs.zip/stop_times.txt:1582071:1582085",
|
||||
"intermediateStops": [
|
||||
{
|
||||
"name": "Amsterdam, Reigersbos",
|
||||
"stopId": "nl-OpenOV_3979741",
|
||||
"parentId": "nl-OpenOV_NL:S:30009528",
|
||||
"importance": 0.018029676750302315,
|
||||
"lat": 52.29557400000001,
|
||||
"lon": 4.973636000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:26:00Z",
|
||||
"departure": "2026-08-16T21:27:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:26:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:27:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS train",
|
||||
"descriptionText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS trains to Zuid or at Spaklerweg for Overamstel. Details: gvb.nl"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Station Holendrecht",
|
||||
"stopId": "nl-OpenOV_3981303",
|
||||
"parentId": "nl-OpenOV_NL:S:30009526",
|
||||
"importance": 0.026576673611998558,
|
||||
"lat": 52.29839300000001,
|
||||
"lon": 4.959859000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:28:00Z",
|
||||
"departure": "2026-08-16T21:29:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:28:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:29:00Z",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS train",
|
||||
"descriptionText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS trains to Zuid or at Spaklerweg for Overamstel. Details: gvb.nl"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"REGIONAL_RAIL",
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Bullewijk",
|
||||
"stopId": "nl-OpenOV_3981144",
|
||||
"parentId": "nl-OpenOV_NL:S:30009524",
|
||||
"importance": 0.017878592014312744,
|
||||
"lat": 52.30693,
|
||||
"lon": 4.952079,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:30:00Z",
|
||||
"departure": "2026-08-16T21:31:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:30:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:31:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS train",
|
||||
"descriptionText": "Stap op Bijlmer ArenA over op NS-treinen naar Zuid of op Spaklerweg voor Overamstel. Details: gvb.nl --|Change at Bijlmer ArenA for NS trains to Zuid or at Spaklerweg for Overamstel. Details: gvb.nl"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Station Bijlmer ArenA",
|
||||
"stopId": "nl-OpenOV_3980962",
|
||||
"parentId": "nl-OpenOV_NL:S:30009522",
|
||||
"importance": 0.04671318084001541,
|
||||
"lat": 52.31261,
|
||||
"lon": 4.946935,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:32:00Z",
|
||||
"departure": "2026-08-16T21:32:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:32:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:32:00Z",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Neem NS-treinen naar Zuid of stap op Spaklerweg over voor Overamstel. Details: gvb.nl/zomermetro --|Take NS trains to Zuid or change at Spak",
|
||||
"descriptionText": "Neem NS-treinen naar Zuid of stap op Spaklerweg over voor Overamstel. Details: gvb.nl/zomermetro --|Take NS trains to Zuid or change at Spaklerweg for Overamstel. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"LONG_DISTANCE",
|
||||
"COACH",
|
||||
"NIGHT_RAIL",
|
||||
"REGIONAL_RAIL",
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Strandvliet",
|
||||
"stopId": "nl-OpenOV_3981329",
|
||||
"parentId": "nl-OpenOV_NL:S:30009520",
|
||||
"importance": 0.017878592014312744,
|
||||
"lat": 52.31868399999999,
|
||||
"lon": 4.941375000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:33:00Z",
|
||||
"departure": "2026-08-16T21:34:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:33:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:34:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Spaklerweg over voor Overamstel of op Amstel voor RAI. Details: gvb.nl/zomermetro –|Change at Spaklerweg for Overamstel or at Amstel",
|
||||
"descriptionText": "Stap op Spaklerweg over voor Overamstel of op Amstel voor RAI. Details: gvb.nl/zomermetro –|Change at Spaklerweg for Overamstel or at Amstel for RAI. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Duivendrecht, Station Duivendrecht",
|
||||
"stopId": "nl-OpenOV_3979812",
|
||||
"parentId": "nl-OpenOV_NL:S:30009551",
|
||||
"importance": 0.018022045493125916,
|
||||
"lat": 52.323895,
|
||||
"lon": 4.936262000000002,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:35:00Z",
|
||||
"departure": "2026-08-16T21:35:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:35:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:35:00Z",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-09T15:42:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-09T15:42:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Neem NS-treinen naar Zuid of stap op Spaklerweg over voor Overamstel. Details: gvb.nl/zomermetro --|Take NS trains to Zuid or change at Spak",
|
||||
"descriptionText": "Neem NS-treinen naar Zuid of stap op Spaklerweg over voor Overamstel. Details: gvb.nl/zomermetro --|Take NS trains to Zuid or change at Spaklerweg for Overamstel. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Van der Madeweg",
|
||||
"stopId": "nl-OpenOV_3981387",
|
||||
"parentId": "nl-OpenOV_NL:S:30009516",
|
||||
"importance": 0.0271453857421875,
|
||||
"lat": 52.329903,
|
||||
"lon": 4.930333000000001,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:36:00Z",
|
||||
"departure": "2026-08-16T21:37:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:36:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:37:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Stap op Spaklerweg over voor Overamstel of op Amstel voor RAI. Details: gvb.nl/zomermetro –|Change at Spaklerweg for Overamstel or at Amstel",
|
||||
"descriptionText": "Stap op Spaklerweg over voor Overamstel of op Amstel voor RAI. Details: gvb.nl/zomermetro –|Change at Spaklerweg for Overamstel or at Amstel for RAI. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Spaklerweg",
|
||||
"stopId": "nl-OpenOV_3980158",
|
||||
"parentId": "nl-OpenOV_NL:S:30009518",
|
||||
"importance": 0.02681240625679493,
|
||||
"lat": 52.340239999999994,
|
||||
"lon": 4.92107,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:39:00Z",
|
||||
"departure": "2026-08-16T21:39:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:39:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:39:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Amstelstation",
|
||||
"stopId": "nl-OpenOV_3980520",
|
||||
"parentId": "nl-OpenOV_NL:S:30009514",
|
||||
"importance": 0.052102621644735336,
|
||||
"lat": 52.346489999999996,
|
||||
"lon": 4.9175699999999996,
|
||||
"level": 0.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:40:00Z",
|
||||
"departure": "2026-08-16T21:41:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:40:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:41:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"alerts": [
|
||||
{
|
||||
"communicationPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"impactPeriod": [
|
||||
{
|
||||
"start": "2026-07-04T04:00:00Z",
|
||||
"end": "2026-08-16T23:00:00Z"
|
||||
}
|
||||
],
|
||||
"cause": "UNKNOWN_CAUSE",
|
||||
"effect": "UNKNOWN_EFFECT",
|
||||
"headerText": "Neem tram 23 voor RAI of bus 78 voor Overamstel. Details: gvb.nl/zomermetro. --|Take tram 23 for RAI or bus 78 for Overamstel. Details: gvb.nl/summermetro",
|
||||
"descriptionText": "Neem tram 23 voor RAI of bus 78 voor Overamstel. Details: gvb.nl/zomermetro. --|Take tram 23 for RAI or bus 78 for Overamstel. Details: gvb.nl/summermetro"
|
||||
}
|
||||
],
|
||||
"modes": [
|
||||
"REGIONAL_RAIL",
|
||||
"SUBWAY",
|
||||
"TRAM",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Wibautstraat",
|
||||
"stopId": "nl-OpenOV_3980265",
|
||||
"parentId": "nl-OpenOV_NL:S:30009512",
|
||||
"importance": 0.027183683589100838,
|
||||
"lat": 52.354332,
|
||||
"lon": 4.9121879999999996,
|
||||
"level": -2.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:42:00Z",
|
||||
"departure": "2026-08-16T21:43:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:42:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:43:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"TRAM",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Weesperplein",
|
||||
"stopId": "nl-OpenOV_3981336",
|
||||
"parentId": "nl-OpenOV_NL:S:30009510",
|
||||
"importance": 0.02798701450228691,
|
||||
"lat": 52.361236999999996,
|
||||
"lon": 4.907945,
|
||||
"level": -2.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:44:00Z",
|
||||
"departure": "2026-08-16T21:45:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:44:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:45:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"TRAM",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Waterlooplein",
|
||||
"stopId": "nl-OpenOV_3980640",
|
||||
"parentId": "nl-OpenOV_NL:S:30009508",
|
||||
"importance": 0.027206193655729294,
|
||||
"lat": 52.366985,
|
||||
"lon": 4.9031400000000005,
|
||||
"level": -2.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:46:00Z",
|
||||
"departure": "2026-08-16T21:46:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:46:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:46:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"modes": [
|
||||
"SUBWAY",
|
||||
"TRAM",
|
||||
"BUS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam, Nieuwmarkt",
|
||||
"stopId": "nl-OpenOV_3981245",
|
||||
"parentId": "nl-OpenOV_NL:S:30009502",
|
||||
"importance": 0.02679653838276863,
|
||||
"lat": 52.371914,
|
||||
"lon": 4.901427000000001,
|
||||
"level": -2.0,
|
||||
"tz": "Europe/Brussels",
|
||||
"arrival": "2026-08-16T21:47:00Z",
|
||||
"departure": "2026-08-16T21:47:00Z",
|
||||
"scheduledArrival": "2026-08-16T21:47:00Z",
|
||||
"scheduledDeparture": "2026-08-16T21:47:00Z",
|
||||
"scheduledTrack": "2",
|
||||
"track": "2",
|
||||
"vertexType": "TRANSIT",
|
||||
"pickupType": "NORMAL",
|
||||
"dropoffType": "NORMAL",
|
||||
"cancelled": false,
|
||||
"modes": [
|
||||
"SUBWAY"
|
||||
]
|
||||
}
|
||||
],
|
||||
"legGeometry": {
|
||||
"points": "wd|vbBornoHN??fGFxa@?hQOzU?jFwBhF}ApDVhFLhg@^bi@LnDM|UBno@?bIKj[?vATxl@NpNFz`@D~j@?z`@?f]Fpd@Vl[?dGLjQVdGL|Jb@hQHlE\\rMnAxVb@|Id@jFzAlZhAtLv@vLjAvL?ZbAvLf@fGfBbSjAtL~Ib~@rClZvAjPd@lEdH|t@tCf\\`C`Tr@zJd@hFdA`IpBhQxBhQjEpX|AvLpCpXjCtWbGpn@zHnz@fBdR|A`TjAbSd@tWVvLWtM[`T_@|JmAhQaBhRcClP_DjQsEbT_DzKoD|JkD~J{E|J}BnE_DfG_DjFkBtBkFfHeFhGiGhGqHjG{OzLee@t\\{PvN_SnQW?eAz@cj@xf@g@\\aAz@a@ZgAz@e@z@u@\\qHfHap@nj@u@z@kAx@O\\e@?qCrCse@bc@_SrP_IdIiXbVeJdI}OxMsAxAaRpPmBxAq@x@q@z@yIdIkGjFeOxMuJbJ}Az@kCrCg@\\sAvA{HjGgGhGO?wXbV{FlFoWfU_d@h`@s`@p]oUjSsOzMoCrCkAx@_DrCeEpDmGfHmh@|e@sAx@kAz@sAvAyAxA{p@lk@u@z@q@z@gEpDsDrCkCrCw`@r\\{JbJqk@vg@qBxAil@pi@yBvBce@db@wAz@mBvA]z@u@\\e@x@u@\\cAz@}OvNwDpDaf@db@sAxAaCtBcTnQS\\}XfUqHjG{BtBaAz@u@x@oCvBkFnEwGfHw`@p]gb@l_@wX`Wu@\\iAvAkBxAgAz@wAvAqRlRgYdVyNvN_DrC?\\K?{FnEgSrPO?cj@|e@oDnDsBxAgm@ri@yAvAuJfI{f@bc@iBtBiAz@u@\\iBtBccA~}@wL`Kcj@th@wVbW_UfT_VdVwAvAgAz@eXxYoCtCyBrCW?k@x@u@z@{FhGkBrCeSjSmGhGuNtO{L~KqLbJoCtBeKbJkK`K_}@lcAW\\cAz@cAvA}@z@sAvAsj@hm@}h@th@wIbJwFjFuWt[}O~KcP`L_DrC{@x@w@z@_DrCcAz@cAz@kAx@gAxAsEpDU\\{AvAgc@l_@iUlSce@ha@_IdImBtByFnEea@r]e@z@wAx@kM|LaCtByBtBsCvBkCtByNzMa@Z}BvBuJdIs`@r]sj@|e@kiC`~Bos@hn@sfAxaA_\\~XoUlR{PzMcVnRyLfIqBz@iCxAqGpDeU|M}bAxj@eA\\{Az@ac@pSwBvAsEvBwDrCcIrDwBxAiA\\_DxAyIrDgOjG_MvCuNpEyUvDgJvCkGxAkNhHcj@fXk_Adf@gJnFiZvOkKnFyBz@ya@pSWZ}^rRyQ|LyIjG_M~Kkl@nk@sb@fb@wVfUaP~L}P~KwMlGuOdJqVhIwSpFcWfJgLpEwLnFgOjHga@nSsFtCs{@lc@eS~LaR~KkdA`i@ia@pSihA~i@c`@nScJpEgGrDcV`Li}@df@uOhIwf@dXsWzNwDxA_MnFmQjHufBxz@oLnFea@fWuJjGiGlFmFlF_DpDmDpDqCnDeEjGeEhG_ItNsY|o@sMvYgZzp@yF|KuE|KkF|KwG|KyDhGyBrC{FfHaHfHmLbKkHnE}FtCoDxAeExAeE|@iBz@mFz@eJ|@_Nb@oG@{JsA_DYeIuAiLgE_TyI_o@aY_NcGkCuAoAy@sEoC_HiEsDqCsIaHoJyJyBoCeEiFcMgPu^kc@qL{IqM_H}S_HkKqAqMsAaNDgSxCoHxAcMtDsR~LkHfHsCpDiBtBaAvBY?sAtBqEfHcCrCuE~Ja@z@oGvM_DdI_IdTkVnt@wBqC",
|
||||
"precision": 6,
|
||||
"length": 430
|
||||
},
|
||||
"bikesAllowed": true,
|
||||
"wheelchairAccessible": "NOT_ACCESSIBLE",
|
||||
"reservation": "NONE",
|
||||
"alternatives": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,40 +1,45 @@
|
||||
import discord
|
||||
import random
|
||||
from discord.ext import tasks
|
||||
from src.config import config
|
||||
from src.handlers import rename_vc
|
||||
from src.commands import setup_commands
|
||||
from src.embeds import build_error_embed
|
||||
import sys
|
||||
|
||||
from src.utils import logger
|
||||
from src.data.status import discord_status
|
||||
from src.config import config
|
||||
from src.dc.handlers import rename_vc
|
||||
from src.dc.helpers import validate_channel
|
||||
from src.dc.commands import setup_commands
|
||||
from src.api.transitous import check_stations
|
||||
|
||||
bot = discord.Bot(intents=discord.Intents.all())
|
||||
setup_commands(bot)
|
||||
|
||||
@tasks.loop(minutes=5)
|
||||
async def change_status():
|
||||
status = random.choice(discord_status)
|
||||
await bot.change_presence(activity=discord.Game(name=f"{status} • /info"))
|
||||
|
||||
setup_commands(bot=bot)
|
||||
_bot_initialized = False
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "stations":
|
||||
check_stations()
|
||||
sys.exit(0)
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
global _bot_initialized
|
||||
print(f"{bot.user} ist online")
|
||||
if not change_status.is_running():
|
||||
change_status.start()
|
||||
|
||||
logger(f"{bot.user} ist online")
|
||||
|
||||
if not _bot_initialized:
|
||||
_bot_initialized = True
|
||||
await rename_vc(bot)
|
||||
server_id = config.discord.server
|
||||
server_vc_id = config.discord.vc
|
||||
channel = validate_channel(bot=bot, server_id=server_id, channel_id=server_vc_id)
|
||||
await rename_vc(bot, voice_channel=channel)
|
||||
else:
|
||||
logger("Discord Reconnect, laufende Fahrt bleibt unangetastet")
|
||||
|
||||
@bot.event
|
||||
async def on_application_command_error(ctx, error):
|
||||
embed = build_error_embed(f"Ein Fehler ist aufgetreten: {error}")
|
||||
await ctx.respond(embed=embed)
|
||||
logger("Reconnected to discord gateway, this wont disturb your current ride")
|
||||
|
||||
try:
|
||||
bot.run(config['token'])
|
||||
bot.run(config.discord.token)
|
||||
except:
|
||||
logger("Fehler beim parsen des token", "fatal")
|
||||
logger("Feher peim parsen des tokens", "fatal")
|
||||
|
||||
'''
|
||||
TODO
|
||||
- discord status
|
||||
- multi language support
|
||||
- README
|
||||
'''
|
||||
@@ -0,0 +1,253 @@
|
||||
import requests
|
||||
import random
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from src.utils import logger, get_train_name, convert_iso_string, validate_connection
|
||||
from src.config import config
|
||||
|
||||
stations = config.connections.stations
|
||||
blacklist = config.connections.blacklist
|
||||
user_agent = config.http.user_agent
|
||||
|
||||
headers = {
|
||||
"User-Agent": f"{user_agent}"
|
||||
}
|
||||
|
||||
endpoint = "https://api.transitous.org"
|
||||
|
||||
def check_stations():
|
||||
all_stations_output = {}
|
||||
minimal_overview = {
|
||||
"stations": {}
|
||||
}
|
||||
|
||||
for station in stations:
|
||||
req = f"{endpoint}/api/v1/geocode"
|
||||
|
||||
try:
|
||||
response = requests.get(req, params={"text": station}, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException as e:
|
||||
logger(f"An error occured while checking for station {station}: {e}")
|
||||
continue
|
||||
|
||||
stops_dict = {}
|
||||
aliases_list = []
|
||||
exact_match = False
|
||||
|
||||
for entry in data:
|
||||
if entry.get("type") != "STOP":
|
||||
continue
|
||||
|
||||
station_id = entry.get("id")
|
||||
stop_name = entry.get("name")
|
||||
coords = f"{entry.get("lat")}, {entry.get("lon")}"
|
||||
modes = entry.get("modes")
|
||||
|
||||
stop_details = {
|
||||
"tz": entry.get("tz"),
|
||||
"country": entry.get("country"),
|
||||
"coords": coords,
|
||||
"modes": modes,
|
||||
"id": station_id,
|
||||
}
|
||||
|
||||
if stop_name:
|
||||
stops_dict[stop_name] = stop_details
|
||||
aliases_list.append(stop_name)
|
||||
|
||||
if stop_name == station:
|
||||
if not exact_match:
|
||||
logger(f"Exact match found! {station} is an assigned station! Bot would use that station directly")
|
||||
exact_match = True
|
||||
|
||||
if not stops_dict:
|
||||
logger("Failed to grab ID from 'search_name'", "error")
|
||||
continue
|
||||
|
||||
all_stations_output[station] = {
|
||||
"associated": stops_dict
|
||||
}
|
||||
|
||||
minimal_overview["stations"][station] = aliases_list
|
||||
|
||||
logger(json.dumps(minimal_overview, indent=4, ensure_ascii=False))
|
||||
|
||||
|
||||
print(f"\nIf you want, I can save a more detailed version directly as a json file to disk.")
|
||||
print("The json would provide informations like coords, country and transport modes that are from every specific associated station.")
|
||||
prompt = input("This would help you to identify the associated stations more accurately (y/n): ")
|
||||
if prompt == "y" or prompt == "yes":
|
||||
with open('stations.json', 'w') as f:
|
||||
json.dump(all_stations_output, f, indent=4, ensure_ascii=False)
|
||||
logger("stations.json generated")
|
||||
else:
|
||||
logger("okay :(")
|
||||
|
||||
|
||||
def get_random_stop_id() -> str | None:
|
||||
assigned_station = random.choice(stations)
|
||||
req = f"{endpoint}/api/v1/geocode"
|
||||
|
||||
try:
|
||||
response = requests.get(req, params={"text": assigned_station}, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException as e:
|
||||
logger(f"An error occured while searching for a connection: {e}", "error")
|
||||
return None
|
||||
|
||||
stop_ids = {}
|
||||
for entry in data:
|
||||
if entry.get("type") != "STOP":
|
||||
continue
|
||||
|
||||
stop_name = entry.get("name")
|
||||
stop_id = entry.get("id", None)
|
||||
stop_ids[stop_id] = stop_name
|
||||
|
||||
if stop_name == assigned_station:
|
||||
stop_ids.clear()
|
||||
stop_ids[stop_id] = stop_name
|
||||
break
|
||||
|
||||
if stop_ids is None:
|
||||
logger(f"Failed to grab ID from '{assigned_station}'", "error")
|
||||
return None
|
||||
|
||||
stop_ids_list = list(stop_ids.keys())
|
||||
stops_string = ", ".join(stop_ids.values())
|
||||
if len(stop_ids_list) > 1:
|
||||
logger(f"Station '{assigned_station}' not found, choosing random from available: {stops_string}")
|
||||
logger(f"No station associated as '{assigned_station}', choosing random from available")
|
||||
chosen_stop_id = random.choice(stop_ids_list)
|
||||
return chosen_stop_id
|
||||
|
||||
def get_random_connection(stop_id: str) -> str | None:
|
||||
if stop_id is None:
|
||||
return None
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
cursor = None
|
||||
max_pages = 5
|
||||
min_results = 5
|
||||
trip_ids = []
|
||||
all_stop_times = []
|
||||
|
||||
for _ in range(max_pages):
|
||||
params = {
|
||||
"stopId": stop_id,
|
||||
"n": 20,
|
||||
"time": now,
|
||||
}
|
||||
if cursor:
|
||||
params["pageCursor"] = cursor
|
||||
|
||||
try:
|
||||
response = requests.get(f"{endpoint}/api/v5/stoptimes", params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException as e:
|
||||
logger(e, "Error")
|
||||
break
|
||||
|
||||
stop_times = data.get("stopTimes", [])
|
||||
all_stop_times.extend(stop_times)
|
||||
|
||||
for entry in stop_times:
|
||||
trip_id = entry["tripId"]
|
||||
if entry["mode"] in blacklist:
|
||||
continue
|
||||
else:
|
||||
trip_ids.append(entry["tripId"])
|
||||
|
||||
if len(trip_ids) >= min_results:
|
||||
break
|
||||
|
||||
cursor = data.get("nextPageCursor")
|
||||
if not cursor:
|
||||
break
|
||||
|
||||
if not trip_ids:
|
||||
logger("Couldn't find any connection", "error")
|
||||
return None
|
||||
trip_id = random.choice(trip_ids)
|
||||
|
||||
from_station = None
|
||||
for entry in all_stop_times:
|
||||
if entry.get("tripId") == trip_id:
|
||||
from_station = entry.get("place", {}).get("name")
|
||||
break
|
||||
|
||||
return {
|
||||
"trip_id": trip_id,
|
||||
"from_station": from_station
|
||||
}
|
||||
|
||||
def get_trip_details(random_connection: dict | None) -> dict | None:
|
||||
if random_connection is None:
|
||||
return None
|
||||
|
||||
req = f"{endpoint}/api/v2/trip"
|
||||
|
||||
try:
|
||||
response = requests.get(req, params={"tripId": random_connection["trip_id"]}, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException as e:
|
||||
logger(f"An error occured while trying to get the route details: {e}", "error")
|
||||
return None
|
||||
|
||||
legs = data["legs"][0]
|
||||
end_time = legs["endTime"]
|
||||
from_station = random_connection["from_station"]
|
||||
display_name = legs["displayName"]
|
||||
train_from = legs["from"]["name"]
|
||||
goes_to = legs["to"]["name"]
|
||||
start_time = legs["startTime"]
|
||||
mode = legs["mode"]
|
||||
|
||||
arrival = convert_iso_string(end_time)
|
||||
departure = convert_iso_string(start_time)
|
||||
|
||||
train_name = get_train_name(display_name, mode)
|
||||
if train_from == from_station:
|
||||
long_name = f"{train_name} nach {goes_to} von {train_from}"
|
||||
else:
|
||||
long_name = f"{train_name} nach {goes_to} über {from_station}"
|
||||
|
||||
trip_details = {
|
||||
"long_name": long_name,
|
||||
"short_name": display_name,
|
||||
"station": from_station,
|
||||
"from": train_from,
|
||||
"to": goes_to,
|
||||
"agency": legs["agencyName"],
|
||||
"route_color": legs.get("routeColor"),
|
||||
"duration": legs["duration"],
|
||||
"departure": departure,
|
||||
"arrival": arrival,
|
||||
"mode": mode,
|
||||
"stops": {}
|
||||
}
|
||||
|
||||
trip_details["stops"][train_from] = departure
|
||||
departure_time = start_time
|
||||
|
||||
for stop in legs["intermediateStops"]:
|
||||
stop_arrival = convert_iso_string(stop["arrival"])
|
||||
trip_details["stops"][stop["name"]] = stop_arrival # not sure if that actually works but im too tired to question it
|
||||
if stop.get("name") == from_station:
|
||||
departure_time = stop["departure"]
|
||||
trip_details["departure"] = convert_iso_string(departure_time)
|
||||
trip_details["stops"][goes_to] = arrival
|
||||
|
||||
valid = validate_connection(start_time, end_time, departure_time)
|
||||
if not valid:
|
||||
return None
|
||||
|
||||
logger(json.dumps(trip_details, indent=4, ensure_ascii=False))
|
||||
return trip_details
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from src.embeds import build_info_embed, build_error_embed
|
||||
|
||||
def setup_commands(bot: discord.Bot):
|
||||
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
|
||||
async def info(ctx):
|
||||
embed = build_info_embed()
|
||||
if embed is None:
|
||||
await build_error_embed("Noch keine Verbindung gesetzt.")
|
||||
return
|
||||
await ctx.respond(embed=embed)
|
||||
+55
-1
@@ -1,4 +1,58 @@
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class DiscordConfig:
|
||||
token: str
|
||||
server: int
|
||||
vc: int
|
||||
formatting: str
|
||||
emojis: bool
|
||||
|
||||
@dataclass
|
||||
class ConnectionsConfig:
|
||||
stations: list[str]
|
||||
blacklist: list[str]
|
||||
min_duration: int
|
||||
max_wait_time: int
|
||||
timezone: str
|
||||
max_duration: Optional[int] = None
|
||||
|
||||
@dataclass
|
||||
class VoiceAnnouncementConfig:
|
||||
enabled: bool
|
||||
stations: dict[str, str]
|
||||
|
||||
@dataclass
|
||||
class AnnouncementConfig:
|
||||
enabled: bool
|
||||
voice: list[VoiceAnnouncementConfig]
|
||||
|
||||
@dataclass
|
||||
class HttpConfig:
|
||||
user_agent: str
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
discord: DiscordConfig
|
||||
connections: ConnectionsConfig
|
||||
announcements: AnnouncementConfig
|
||||
http: HttpConfig
|
||||
|
||||
def _load_config() -> Config:
|
||||
with open("config.json", "r") as file:
|
||||
config = json.load(file)
|
||||
raw = json.load(file)
|
||||
|
||||
return Config(
|
||||
discord=DiscordConfig(**raw["discord"]),
|
||||
connections=ConnectionsConfig(**raw["connections"]),
|
||||
announcements=AnnouncementConfig(
|
||||
enabled=raw["announcements"]["enabled"],
|
||||
voice=[VoiceAnnouncementConfig(**v) for v in raw["announcements"]["voice"]],
|
||||
),
|
||||
http=HttpConfig(**raw["http"]),
|
||||
)
|
||||
|
||||
config = _load_config()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,2 @@
|
||||
SPDX-FileCopyrightText: 2024 Mathis Brüchert <[email protected]>
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
+9
-16
@@ -1,18 +1,11 @@
|
||||
emoji_list = {
|
||||
"Fallback": "🚉",
|
||||
"Nahverkehr": "🚃",
|
||||
"Höchstgeschwindigkeit": "🚄",
|
||||
"Bus": "🚎"
|
||||
}
|
||||
|
||||
train_types = { # Unvollständig!
|
||||
"ICE": emoji_list["Höchstgeschwindigkeit"],
|
||||
"TGV": emoji_list["Höchstgeschwindigkeit"],
|
||||
|
||||
"RE": emoji_list["Nahverkehr"],
|
||||
"RB": emoji_list["Nahverkehr"],
|
||||
"ARV": emoji_list["Nahverkehr"],
|
||||
"BRB": emoji_list["Nahverkehr"],
|
||||
|
||||
"Bus": emoji_list["Bus"]
|
||||
"Fallback": "💺",
|
||||
"BUS": "🚎",
|
||||
"COACH": "🚎",
|
||||
"TRAM": "🚈",
|
||||
"REGIONAL_RAIL": "🚊",
|
||||
"HIGHSPEED_RAIL": "🚅",
|
||||
"LONG_DISTANCE": "🚅",
|
||||
"METRO": "🚇",
|
||||
"SUBWAY": "🚇"
|
||||
}
|
||||
+16
-2
@@ -38,7 +38,7 @@ OPERATORS = {
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/ODEG-Logo_Neu.svg/960px-ODEG-Logo_Neu.svg.png",
|
||||
"color": 0x00745C
|
||||
},
|
||||
"Nederlandse Spoorwegen": {
|
||||
"NS": {
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Logo_NS.svg/960px-Logo_NS.svg.png",
|
||||
"color": 0X00337F,
|
||||
"slogan": ["Goed op weg", "Welkom in de trein van morgen", "Veilig, Vlug, Voordelig", "we haben een serious probleem", "Neuken in de keuken"]
|
||||
@@ -98,6 +98,14 @@ OPERATORS = {
|
||||
"enno": {
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Enno_logo.svg/960px-Enno_logo.svg.png",
|
||||
"color": 0x88216F
|
||||
},
|
||||
"DSB": {
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/DSB_company_logo.svg/960px-DSB_company_logo.svg.png",
|
||||
"color": 0xB22B32
|
||||
},
|
||||
"Vr": {
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Vr_Logo.png/330px-Vr_Logo.png",
|
||||
"color": 0x00B451
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,10 +117,16 @@ OPERATOR_ALIASES = {
|
||||
"DB Regio AG Bayern": OPERATORS["db_bayern"],
|
||||
"DB Fernverkehr AG": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG NRW": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Nord": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Südost": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Nordost": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Nord": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Mitte": OPERATORS["db_allgemein"],
|
||||
"NS International": OPERATORS["NS"],
|
||||
"SBB GmbH": OPERATORS["SBB"],
|
||||
"Schweizerische Bundesbahnen SBB": OPERATORS["SBB"],
|
||||
"Dänische Staatsbahnen": OPERATORS["DSB"],
|
||||
"VR": OPERATORS["Vr"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import discord
|
||||
from src.dc.embeds import build_info_embed
|
||||
|
||||
def setup_commands(bot: discord.Bot):
|
||||
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
|
||||
async def info(ctx):
|
||||
await ctx.respond(embed=build_info_embed())
|
||||
@@ -0,0 +1,79 @@
|
||||
import discord
|
||||
import random
|
||||
|
||||
from src.utils import get_operator_metadata, get_next_station, format_via_list, format_stop_list
|
||||
from src.dc.helpers import format_timestamp_to_dc
|
||||
|
||||
def build_embed_footer(mode: str, slogans):
|
||||
footer_notice = f"Data provided by https://transitous.org • mode: {mode}"
|
||||
icon = "https://raw.githubusercontent.com/kaaninchen/Gleiswechsel/refs/heads/main/src/data/assets/transitous-logo.png"
|
||||
|
||||
if slogans is not None:
|
||||
footer_text = f"{random.choice(slogans)} • {footer_notice}"
|
||||
else:
|
||||
footer_text = footer_notice
|
||||
|
||||
return {
|
||||
"text": footer_text,
|
||||
"icon": icon
|
||||
}
|
||||
|
||||
def build_info_embed() -> discord.Embed:
|
||||
from src.dc.handlers import trip
|
||||
|
||||
agency = trip["agency"]
|
||||
metadata = get_operator_metadata(agency, trip["route_color"])
|
||||
departure = format_timestamp_to_dc(trip["departure"])
|
||||
arrival = format_timestamp_to_dc(trip["arrival"])
|
||||
|
||||
embed = discord.Embed(
|
||||
title = trip["long_name"],
|
||||
description=f"Abfahrt von {trip["station"]} um {departure}. Ankunft um {arrival}",
|
||||
color = metadata["color"]
|
||||
)
|
||||
|
||||
stops = trip["stops"]
|
||||
|
||||
next_stop = get_next_station(trip["stops"])
|
||||
print(f"next_stop: {next_stop}")
|
||||
next_stop_station = None
|
||||
if next_stop:
|
||||
next_stop_station = next_stop.get("name")
|
||||
|
||||
via = format_via_list(stops)
|
||||
|
||||
if via:
|
||||
embed.add_field(name="Über", value=via, inline=False)
|
||||
if next_stop:
|
||||
embed.add_field(name="Nächster Halt", value=f"__{next_stop_station}__", inline=False)
|
||||
|
||||
route_fields = format_stop_list(stops, next_stop_station)
|
||||
for field_name, field_value in route_fields:
|
||||
embed.add_field(name=field_name, value=field_value, inline=False)
|
||||
|
||||
footer = build_embed_footer(trip["mode"], metadata["slogans"])
|
||||
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
||||
|
||||
embed.set_author(name=agency)
|
||||
embed.set_thumbnail(url=metadata["logo"])
|
||||
|
||||
return embed
|
||||
|
||||
def build_announcement_embed(msg):
|
||||
from src.dc.handlers import trip
|
||||
agency = trip["agency"]
|
||||
metadata = get_operator_metadata(agency, trip["route_color"])
|
||||
|
||||
embed = discord.Embed(
|
||||
title = "Informationen zu ihrer Fahrt",
|
||||
description=msg,
|
||||
color=metadata["color"]
|
||||
)
|
||||
|
||||
embed.set_author(name=agency)
|
||||
embed.set_thumbnail(url=metadata["logo"])
|
||||
|
||||
footer = build_embed_footer(trip["mode"], metadata.get("slogan"))
|
||||
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
||||
|
||||
return embed
|
||||
@@ -0,0 +1,117 @@
|
||||
import discord
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
from src.utils import logger, channel_formatting, choose_connection, get_sound_path
|
||||
from src.config import config
|
||||
|
||||
_scheduled_task: asyncio.Task | None = None
|
||||
|
||||
async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = False):
|
||||
global trip, _scheduled_task
|
||||
if not from_scheduler and _scheduled_task and not _scheduled_task.done():
|
||||
_scheduled_task.cancel()
|
||||
|
||||
attempt = 0
|
||||
max_attempt = 15
|
||||
trip = choose_connection()
|
||||
while trip is None and attempt < max_attempt:
|
||||
attempt += 1
|
||||
logger(f"Attempt {attempt}: Failed to select route, retrying...", "error")
|
||||
trip = choose_connection()
|
||||
|
||||
if trip is None:
|
||||
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
|
||||
return False
|
||||
|
||||
arrival = trip["arrival"]
|
||||
long_name = trip["long_name"]
|
||||
mode = trip["mode"]
|
||||
|
||||
print("-----------------")
|
||||
logger(f"Umstieg: {long_name}; Ankunft: {arrival} Uhr")
|
||||
logger(f"Betreiber: {trip["agency"]}, Typ: {mode}")
|
||||
logger(f"Versuche Namen zu ändern, wenn nichts passiert bin ich im cooldown... (warte bis zu 10min!)")
|
||||
|
||||
formatting = channel_formatting(mode)
|
||||
await voice_channel.edit(name=f"{formatting}{long_name}")
|
||||
await voice_channel.set_status(f"Ankunft um {arrival}")
|
||||
|
||||
logger(f"Name geändert!")
|
||||
|
||||
await announcer("umstieg", voice_channel)
|
||||
|
||||
_scheduled_task = asyncio.create_task(_schedule_next_transfer(bot, arrival, voice_channel, trip["to"]))
|
||||
|
||||
|
||||
async def _schedule_next_transfer(bot: discord.Bot, arrival, voice_channel: discord.VoiceChannel, destination: str):
|
||||
now = datetime.now()
|
||||
parsed_time = datetime.strptime(arrival, "%H:%M").time()
|
||||
arrival_dt = datetime.combine(date.today(), parsed_time)
|
||||
|
||||
if arrival_dt < now:
|
||||
arrival_dt += timedelta(days=1)
|
||||
|
||||
wait_seconds = (arrival_dt - now).total_seconds()
|
||||
announcement_countdown = random.randrange(180, 300)
|
||||
announcement_countdown = 3
|
||||
if wait_seconds > 0:
|
||||
remaining = str(timedelta(seconds=wait_seconds))
|
||||
logger(f"Nächster Umstieg in {remaining.split('.')[0]} ({arrival} Uhr)")
|
||||
|
||||
if wait_seconds > announcement_countdown:
|
||||
wait_until_end_announcement = wait_seconds - announcement_countdown
|
||||
await asyncio.sleep(wait_until_end_announcement)
|
||||
await announcer("ende", voice_channel, destination)
|
||||
await asyncio.sleep(announcement_countdown)
|
||||
else:
|
||||
await asyncio.sleep(wait_seconds)
|
||||
|
||||
logger("Zug angekommen, wähle neue Verbindung")
|
||||
await rename_vc(bot, voice_channel, from_scheduler=True)
|
||||
|
||||
async def announcer(announcement: str, voice_channel: discord.VoiceChannel, destination = None):
|
||||
from src.dc.embeds import build_info_embed, build_announcement_embed
|
||||
|
||||
announcements_enabled = config.announcements.enabled
|
||||
voice_announcement_enabled = config.announcements.voice[0].enabled
|
||||
|
||||
if announcements_enabled:
|
||||
if len(voice_channel.members) > 0:
|
||||
match announcement:
|
||||
case "ende":
|
||||
embed = build_announcement_embed(
|
||||
f'Sehr geehrte Fahrgäste,\nIn wenigen Minuten erreichen wir {destination}. Dieser Zug endet dort.\n\nWir wünschen Ihnen eine angenehme Weiterreise.\n\nVielen Dank für ihr Vertrauen und auf Wiedersehen.')
|
||||
if voice_announcement_enabled:
|
||||
await voice_announcer(destination, voice_channel)
|
||||
case "umstieg":
|
||||
embed = build_info_embed()
|
||||
case _:
|
||||
logger(f"Unbekanntes Announcements: {announcement}")
|
||||
embed = None
|
||||
if embed:
|
||||
await voice_channel.send(embed=embed)
|
||||
else:
|
||||
logger(f"Announcement {announcement} wird geskipped, keiner da")
|
||||
return
|
||||
|
||||
async def voice_announcer(destination: str, voice_channel: discord.VoiceChannel):
|
||||
sound_path = get_sound_path(destination=destination)
|
||||
if sound_path is None:
|
||||
return
|
||||
|
||||
logger(f"VC wird betreten, spiele {sound_path}")
|
||||
vc = await voice_channel.connect(timeout=15, reconnect=True)
|
||||
audio_source = discord.FFmpegPCMAudio(sound_path)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
if not vc.is_playing():
|
||||
def after_playing(error):
|
||||
if error:
|
||||
logger(f"Player error: {error}", "error")
|
||||
loop.create_task(vc.disconnect())
|
||||
logger("VC wird verlassen")
|
||||
|
||||
vc.play(audio_source, after=after_playing)
|
||||
@@ -0,0 +1,32 @@
|
||||
import discord
|
||||
from datetime import datetime, timedelta
|
||||
from src.utils import logger
|
||||
|
||||
def validate_channel(bot: discord.bot, server_id: int, channel_id: int):
|
||||
guild = bot.get_guild(server_id)
|
||||
|
||||
if guild is None:
|
||||
logger(f"Es konnte kein Server mit der ID {server_id} gefunden werden", "fatal")
|
||||
return False
|
||||
|
||||
channel = guild.get_channel(channel_id)
|
||||
if not isinstance(channel, discord.VoiceChannel):
|
||||
logger(f"Es konnte kein VC mit der id {channel_id} gefunden werden", "fatal")
|
||||
return False
|
||||
|
||||
return channel
|
||||
|
||||
def format_timestamp_to_dc(timestr):
|
||||
parsed_time = datetime.strptime(timestr, "%H:%M")
|
||||
now = datetime.now()
|
||||
final_datetime = datetime.now().replace(
|
||||
hour=parsed_time.hour,
|
||||
minute=parsed_time.minute,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
|
||||
if final_datetime <= now:
|
||||
final_datetime += timedelta(days=1)
|
||||
|
||||
return discord.utils.format_dt(final_datetime, style="t")
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
import random
|
||||
import discord
|
||||
from datetime import datetime, timedelta
|
||||
import src.handlers as handlers
|
||||
from src.config import config
|
||||
from src.utils import operator_metadata, format_via_list, resolve_operator, logger
|
||||
|
||||
def format_timestamp(timestr):
|
||||
parsed_time = datetime.strptime(timestr, "%H:%M")
|
||||
now = datetime.now()
|
||||
final_datetime = datetime.now().replace(
|
||||
hour=parsed_time.hour,
|
||||
minute=parsed_time.minute,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
|
||||
if final_datetime <= now:
|
||||
final_datetime += timedelta(days=1)
|
||||
|
||||
return discord.utils.format_dt(final_datetime, style="t")
|
||||
|
||||
def format_iso_timestamp(isostr):
|
||||
parsed_time = discord.utils.parse_time(isostr)
|
||||
return discord.utils.format_dt(parsed_time, style="t")
|
||||
|
||||
def build_embed_footer(operator_slogans):
|
||||
dbf = config.get("dbf", "https://dbf.finalrewind.org")
|
||||
footer_notice = f"Daten großzügig bereitgestellt von {dbf} • Typ: {handlers.train_type}"
|
||||
footer_text = (
|
||||
f"{random.choice(operator_slogans)} • {footer_notice}"
|
||||
if operator_slogans else
|
||||
footer_notice
|
||||
)
|
||||
icon = f"{dbf}/static/icons/icon-96x96.png"
|
||||
return {
|
||||
"text": footer_text,
|
||||
"icon": icon
|
||||
}
|
||||
|
||||
def build_info_embed() -> discord.Embed | None:
|
||||
conn = handlers.current
|
||||
info = handlers.train_info
|
||||
|
||||
if handlers.current is None and handlers.train_info is None:
|
||||
return None
|
||||
|
||||
current_operator = resolve_operator(info["operators"])
|
||||
arrival = format_iso_timestamp(info["arrival"])
|
||||
departure = format_timestamp(conn['departure'])
|
||||
|
||||
operator_infos = operator_metadata(current_operator)
|
||||
|
||||
embed = discord.Embed(
|
||||
title=handlers.train_name,
|
||||
description=f"Abfahrt von {conn['station']} um {departure}. Ankunft um {arrival}",
|
||||
color=operator_infos["color"]
|
||||
)
|
||||
|
||||
if len(conn['via']) > 0:
|
||||
embed.add_field(name="Über", value=format_via_list(conn['via']), inline=False)
|
||||
|
||||
if operator_infos.get("unknown"):
|
||||
logger(f"Metadaten für {current_operator} konten in operators.py nicht gefunden werden")
|
||||
|
||||
embed.set_author(name=current_operator)
|
||||
embed.set_thumbnail(url=operator_infos["logo"])
|
||||
|
||||
route_lines = []
|
||||
for stop in conn['route']:
|
||||
stop_name = stop["name"]
|
||||
if stop_name == conn['station']:
|
||||
route_lines.append(f"• **{stop_name}**")
|
||||
else:
|
||||
route_lines.append(f"• {stop_name}")
|
||||
|
||||
embed.add_field(name="Route", value="\n".join(route_lines))
|
||||
|
||||
footer = build_embed_footer(operator_infos.get("slogan"))
|
||||
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
||||
|
||||
return embed
|
||||
|
||||
def build_announcement_embed(msg):
|
||||
current_operator = resolve_operator(handlers.train_info["operators"])
|
||||
operator_infos = operator_metadata(current_operator)
|
||||
|
||||
embed = discord.Embed(
|
||||
title = "Informationen zu Ihrer Fahrt",
|
||||
description=msg,
|
||||
color=operator_infos["color"]
|
||||
)
|
||||
|
||||
embed.set_author(name=current_operator)
|
||||
embed.set_thumbnail(url=operator_infos["logo"])
|
||||
|
||||
footer = build_embed_footer(operator_infos.get("slogan"))
|
||||
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
||||
|
||||
return embed
|
||||
|
||||
def build_error_embed(errormsg) -> discord.Embed:
|
||||
embed = discord.Embed(
|
||||
title="Ein Fehler ist aufgetreten!",
|
||||
description=errormsg,
|
||||
color=discord.Colour.red()
|
||||
)
|
||||
|
||||
return embed
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
import discord
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
from src.config import config
|
||||
from src.utils import random_connection, get_train_info, get_channel_formatting, logger
|
||||
from src.embeds import build_announcement_embed, build_info_embed
|
||||
|
||||
current = None
|
||||
_scheduled_task: asyncio.Task | None = None
|
||||
|
||||
async def announcer(bot, announcement):
|
||||
voice_channel = bot.get_channel(config["vc"])
|
||||
if len(voice_channel.members) > 0:
|
||||
match announcement:
|
||||
case "ende":
|
||||
destination = current['destination']
|
||||
embed = build_announcement_embed(
|
||||
f'Sehr geehrte Fahrgäste,\nIn wenigen Minuten erreichen wir {destination}. Dieser Zug endet dort.\n\nWir wünschen Ihnen eine angenehme Weiterreise.\n\nVielen Dank für Ihr Vertrauen und auf Wiedersehen.')
|
||||
if config["voice_announcements"][0]["enabled"]:
|
||||
await voice_announcer(bot, destination, voice_channel)
|
||||
case "umstieg":
|
||||
embed = build_info_embed()
|
||||
case _:
|
||||
embed = None
|
||||
if embed is None:
|
||||
logger(f"Unbekanntes Announcement: {announcement}")
|
||||
else:
|
||||
await voice_channel.send(embed=embed)
|
||||
else:
|
||||
logger(f"Announcement {announcement} wird geskipped, keiner da")
|
||||
return
|
||||
|
||||
async def voice_announcer(bot: discord.Bot, destination, voice_channel):
|
||||
voice_announcement_config = config["voice_announcements"][0]
|
||||
voice_stations = voice_announcement_config["stations"]
|
||||
|
||||
if destination in voice_stations:
|
||||
announcement_for = destination
|
||||
else:
|
||||
if voice_stations.get("general", "") == "":
|
||||
return
|
||||
announcement_for = "general"
|
||||
|
||||
if voice_stations.values() == list:
|
||||
sound_file = random.choice(voice_stations.get(announcement_for))
|
||||
else:
|
||||
sound_file = voice_stations.get(announcement_for)
|
||||
|
||||
sound_path = f"src/data/announcements/{sound_file}"
|
||||
if Path(sound_path).is_file() is False:
|
||||
logger(f"Konnte Datei {sound_path} nicht finden", "error")
|
||||
return
|
||||
|
||||
logger(f"VC wird betreten, spiele {sound_path}")
|
||||
vc = await voice_channel.connect(timeout=15, reconnect=True)
|
||||
audio_source = discord.FFmpegPCMAudio(sound_path)
|
||||
|
||||
if not vc.is_playing():
|
||||
def after_playing(error):
|
||||
if error:
|
||||
logger(f"Player error: {error}", "error")
|
||||
bot.loop.create_task(vc.disconnect())
|
||||
logger("VC wird verlassen")
|
||||
|
||||
vc.play(audio_source, after=after_playing)
|
||||
|
||||
async def rename_vc(bot: discord.Bot, from_scheduler: bool = False):
|
||||
global current, train_name, train_info, train_type, _scheduled_task
|
||||
|
||||
guild = bot.get_guild(int(config["server"]))
|
||||
if guild is None:
|
||||
logger(f"Es konnte kein Server mit der ID {config['server']} gefunden werden! Ist der Bot ein Member?", "fatal")
|
||||
return False
|
||||
|
||||
channel = guild.get_channel(int(config["vc"]))
|
||||
if not isinstance(channel, discord.VoiceChannel):
|
||||
logger(f"Es konnte kein VC mit der ID {config['vc']} auf dem Server gefunden werden", "fatal")
|
||||
return False
|
||||
|
||||
if not from_scheduler and _scheduled_task and not _scheduled_task.done():
|
||||
_scheduled_task.cancel()
|
||||
|
||||
attempt = 0
|
||||
while True:
|
||||
if attempt == 20:
|
||||
logger("Zu viele Fehlversuche. Füge einen anderen Bahnhof hinzu.", "fatal")
|
||||
return "Es konnte kein Zug gefunden werden."
|
||||
|
||||
attempt += 1
|
||||
current = random_connection()
|
||||
if current == None:
|
||||
return None
|
||||
|
||||
parts = current['train'].split()
|
||||
train_type = parts[0]
|
||||
|
||||
if parts[1].isdigit():
|
||||
train = current['train']
|
||||
train_ID = parts[1]
|
||||
else:
|
||||
train = parts[1]
|
||||
train_ID = current['train_number']
|
||||
|
||||
station = current['station']
|
||||
train_info = get_train_info(station=station, train_ID=train_ID, train_type=train_type)
|
||||
if train_info and train_info.get('operators') and train_info.get('arrival'):
|
||||
break
|
||||
|
||||
logger(f"Versuch {attempt}: Fehler bei {current['train']} von {station}, versuche neue Verbindung...")
|
||||
|
||||
train_name = f"{train} nach {current['destination']} von {current['station']}"
|
||||
logger(f"Vorbereitung auf {train_name} (typ: {train_type})")
|
||||
arrival = datetime.fromisoformat((train_info["arrival"]))
|
||||
formatting = get_channel_formatting(train_type)
|
||||
|
||||
|
||||
print("-----------------------------------------")
|
||||
logger(f"Umstieg: {train_name}")
|
||||
logger(f"Betreiber: {''.join(train_info['operators'])}")
|
||||
logger(f"Train-Type: {train_type}")
|
||||
logger(f"Wenn der Name nicht geändert wird bin ich im Cooldown")
|
||||
await channel.edit(name=f"{formatting}{train_name}")
|
||||
await channel.set_status(f"Ankunft um {arrival.strftime('%H:%M')}")
|
||||
logger(f"Name geändert!")
|
||||
|
||||
if config.get("announcements", True):
|
||||
await announcer(bot, "umstieg")
|
||||
|
||||
_scheduled_task = asyncio.create_task(_schedule_next_umstieg(bot, arrival))
|
||||
|
||||
return True
|
||||
|
||||
async def _schedule_next_umstieg(bot, arrival):
|
||||
announcement = config.get("announcements", True)
|
||||
announcement_countdown = random.randrange(180, 300) # letzte station announcement ist meistens 3-5min vor ankunft
|
||||
wait_seconds = (arrival - datetime.now()).total_seconds()
|
||||
if wait_seconds > 0:
|
||||
remaining = str(timedelta(seconds=wait_seconds))
|
||||
logger(f"Nächster Umstieg in {remaining.split('.')[0]} ({arrival.strftime('%H:%M:%S')} Uhr)")
|
||||
|
||||
if announcement and wait_seconds > announcement_countdown:
|
||||
wait_until_end_announcement = wait_seconds - announcement_countdown
|
||||
await asyncio.sleep(wait_until_end_announcement)
|
||||
await announcer(bot, "ende")
|
||||
await asyncio.sleep(announcement_countdown)
|
||||
|
||||
else:
|
||||
await asyncio.sleep(wait_seconds)
|
||||
|
||||
logger("Zug angekommen, wähle neue Verbindung...")
|
||||
await rename_vc(bot, from_scheduler=True)
|
||||
+194
-137
@@ -1,149 +1,206 @@
|
||||
import requests
|
||||
import random
|
||||
import os
|
||||
import importlib
|
||||
from datetime import datetime
|
||||
import random
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from src.config import config
|
||||
import src.data.operators as operators_module
|
||||
import src.data.emojis as emojis
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
_operators_mtime = None
|
||||
dbf = config.get("dbf", "https://dbf.finalrewind.org")
|
||||
import src.data.operators as operators
|
||||
from src.data.emojis import emoji_list
|
||||
|
||||
def _reload_operators_if_changed():
|
||||
global _operators_mtime
|
||||
_operator_mtime = None
|
||||
|
||||
path = operators_module.__file__
|
||||
current_mtime = os.path.getmtime(path)
|
||||
|
||||
if _operators_mtime is None:
|
||||
_operators_mtime = current_mtime
|
||||
return
|
||||
|
||||
if current_mtime != _operators_mtime:
|
||||
importlib.reload(operators_module)
|
||||
_operators_mtime = current_mtime
|
||||
logger("operators.py wurde automatisch neu geladen (Änderungen erkannt)")
|
||||
|
||||
def random_connection():
|
||||
available_stations = config["stations"].copy()
|
||||
while True:
|
||||
if not available_stations:
|
||||
logger("Keine validen Bahnhöfe. Schlag den richtigen Bahnhofsnamen auf {dbf} nach", "fatal")
|
||||
|
||||
station = random.choice(available_stations)
|
||||
url = f"{dbf}/{station}.json"
|
||||
blacklist = config.get("blacklist", [])
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException as e:
|
||||
logger(f"Fehler beim aussuchen der Verbindung: {e}", "fatal")
|
||||
return
|
||||
|
||||
if response.status_code == 300:
|
||||
logger(f"Bahnhof '{station}' konnte nicht gefunden werden ({url})", "error")
|
||||
available_stations.remove(station)
|
||||
continue
|
||||
|
||||
departures = [
|
||||
d for d in data.get("departures", [])
|
||||
if d.get("scheduledDeparture") and d.get("destination") != station
|
||||
and not d.get("train", "").startswith(tuple(blacklist))
|
||||
]
|
||||
|
||||
if not departures:
|
||||
continue
|
||||
|
||||
if not config['random']:
|
||||
dep = departures[0]
|
||||
else:
|
||||
dep = random.choice(departures)
|
||||
|
||||
return {
|
||||
"train": dep['train'],
|
||||
"destination": dep['destination'],
|
||||
"route": dep['route'],
|
||||
"departure": dep['scheduledDeparture'],
|
||||
"via": dep['via'],
|
||||
"station": station,
|
||||
"train_number": dep['trainNumber']
|
||||
}
|
||||
|
||||
def get_train_info(station, train_ID, train_type):
|
||||
url = f"{dbf}/z/{train_type}%20{train_ID}/{station}.json"
|
||||
logger(f"Fetche {url}")
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException as e:
|
||||
logger(f"ReqestException Fehler: {e}")
|
||||
return None
|
||||
|
||||
dep = data.get("departure", [])
|
||||
if not dep:
|
||||
logger("Kein departure Feld gefunden", "error")
|
||||
return None
|
||||
|
||||
route_post = dep.get("route_post_diff")
|
||||
if not route_post:
|
||||
logger("Kein route_post_diff Feld gefunden", "error")
|
||||
return None
|
||||
|
||||
arrival_iso = route_post[-1].get("sched_arr")
|
||||
if not arrival_iso:
|
||||
logger("Keine Ankunftszeit gefunden", "error")
|
||||
return None
|
||||
|
||||
return {
|
||||
"arrival": arrival_iso,
|
||||
"operators": dep.get("operators"),
|
||||
}
|
||||
|
||||
def format_via_list(via: list[str]):
|
||||
if not via:
|
||||
return ""
|
||||
if len(via) == 1:
|
||||
return via[0]
|
||||
return ", ".join(via[:-1]) + " und " + via[-1]
|
||||
|
||||
def resolve_operator(operators):
|
||||
if not operators:
|
||||
return None
|
||||
for op in operators:
|
||||
if op in operators_module.OPERATOR_ALIASES or op in operators_module.OPERATORS:
|
||||
return op
|
||||
|
||||
return operators[0]
|
||||
|
||||
def operator_metadata(operator):
|
||||
_reload_operators_if_changed()
|
||||
|
||||
if not operator:
|
||||
return operators_module.OPERATORS["fallback"]
|
||||
|
||||
if operator in operators_module.OPERATOR_ALIASES:
|
||||
return operators_module.OPERATOR_ALIASES[operator]
|
||||
|
||||
return operators_module.OPERATORS.get(operator, operators_module.OPERATORS["fallback"])
|
||||
|
||||
def get_channel_formatting(train_type):
|
||||
formatting = config.get("formatting", "")
|
||||
train_emoji = ""
|
||||
|
||||
if config.get("emojis", True):
|
||||
train_emoji = emojis.train_types.get(train_type)
|
||||
if train_emoji is None:
|
||||
train_emoji = emojis.emoji_list.get("Fallback", "")
|
||||
|
||||
return f"{train_emoji}{formatting}"
|
||||
|
||||
def logger(msg, log_type="info"):
|
||||
def logger(msg, log_type="info") -> str:
|
||||
status = log_type.upper()
|
||||
current_time = datetime.now().strftime('%X')
|
||||
print(f"{current_time}: {status}: {msg}")
|
||||
if status == "FATAL":
|
||||
os._exit(1)
|
||||
|
||||
def choose_connection() -> dict | None:
|
||||
from src.api import transitous
|
||||
station_id = transitous.get_random_stop_id()
|
||||
connection = transitous.get_random_connection(station_id)
|
||||
trip = transitous.get_trip_details(connection)
|
||||
|
||||
return trip
|
||||
|
||||
def validate_connection(start_time: str, end_time: str, station_departure: str) -> bool:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
||||
if end_dt < now:
|
||||
logger(f"Verbindung liegt bereits in der Vergangenheit: {start_dt}", "error")
|
||||
return False
|
||||
|
||||
max_wait_time = config.connections.max_wait_time
|
||||
start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
||||
if start_dt > now + timedelta(hours=max_wait_time):
|
||||
logger(f"Verbindung liegt zu weit in der Zukunft: {start_dt}", "error")
|
||||
return False
|
||||
|
||||
station_departure_dt = datetime.fromisoformat(station_departure.replace("Z", "+00:00"))
|
||||
trip_duration = (end_dt - station_departure_dt).total_seconds()
|
||||
trip_duration_minutes = str(timedelta(seconds=trip_duration))
|
||||
|
||||
min_duration = config.connections.min_duration
|
||||
min_duration_seconds = min_duration * 60
|
||||
if trip_duration < min_duration_seconds:
|
||||
logger(f"Verbindung ist mit {trip_duration_minutes} zu kurz (mindestens {min_duration} Minuten gewollt)", "error")
|
||||
return False
|
||||
|
||||
max_duration = config.connections.max_duration
|
||||
if max_duration:
|
||||
max_duration_seconds = max_duration * 60
|
||||
if max_duration_seconds < trip_duration:
|
||||
logger(f"Verbindung ist mit {trip_duration_minutes} zu lang (höchstens {max_duration} Minuten gewollt)", "error")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def convert_iso_string(isostring) -> str:
|
||||
timezone = config.connections.timezone
|
||||
dt = datetime.fromisoformat(isostring.replace('Z', '+00:00'))
|
||||
dt = dt.astimezone(ZoneInfo(timezone))
|
||||
|
||||
if dt.second >= 30:
|
||||
dt += timedelta(minutes=1)
|
||||
return dt.strftime('%H:%M')
|
||||
|
||||
def channel_formatting(mode: str) -> str:
|
||||
formatting = config.discord.formatting
|
||||
|
||||
if config.discord.formatting:
|
||||
emoji = emoji_list.get(mode)
|
||||
if emoji is None:
|
||||
emoji = emoji_list.get("Fallback")
|
||||
|
||||
return f"{emoji}{formatting}"
|
||||
|
||||
def get_train_name(train_name: str, mode: str) -> str:
|
||||
if mode == "BUS" or mode == "TRAM":
|
||||
train = f"{mode.capitalize()} {train_name}"
|
||||
elif "(" in train_name:
|
||||
train = train_name.split(" (")[0]
|
||||
else:
|
||||
train = train_name
|
||||
|
||||
return train
|
||||
|
||||
def _reload_operators_if_changed():
|
||||
global _operator_mtime
|
||||
|
||||
path = operators.__file__
|
||||
current_mtime = os.path.getmtime(path)
|
||||
|
||||
if _operator_mtime is None:
|
||||
_operator_mtime = current_mtime
|
||||
return
|
||||
|
||||
if current_mtime != _operator_mtime:
|
||||
importlib.reload(operators)
|
||||
_operator_mtime = current_mtime
|
||||
logger("operators.py wurde automatisch neu geladen (Änderungen erkannt)")
|
||||
|
||||
|
||||
def get_operator_metadata(agency: str, route_color: str) -> dict:
|
||||
_reload_operators_if_changed()
|
||||
|
||||
op_data = operators.OPERATOR_ALIASES.get(agency) or operators.OPERATORS.get(agency) or operators.OPERATORS["fallback"]
|
||||
|
||||
logo = op_data.get("logo", operators.OPERATORS["fallback"]["logo"])
|
||||
slogans = op_data.get("slogan")
|
||||
|
||||
color = op_data.get("color")
|
||||
if color is None or color == operators.OPERATORS["fallback"]["color"]:
|
||||
if route_color is not None:
|
||||
try:
|
||||
color = int(route_color, 16)
|
||||
except ValueError:
|
||||
color = operators.OPERATORS["fallback"]["color"]
|
||||
else:
|
||||
logger(f"Managed to get color from API, add agency to src/data/operators.py if you don't like it")
|
||||
else:
|
||||
color = operators.OPERATORS["fallback"]["color"]
|
||||
return {
|
||||
"logo": logo,
|
||||
"color": color,
|
||||
"slogans": slogans
|
||||
}
|
||||
|
||||
def get_sound_path(destination) -> str | None:
|
||||
voice_stations = config.announcements.voice[0].stations
|
||||
|
||||
if destination in voice_stations:
|
||||
announcement_for = destination
|
||||
else:
|
||||
general_sound_enabled = voice_stations.get("general", "")
|
||||
if not general_sound_enabled:
|
||||
return None
|
||||
announcement_for = "general"
|
||||
|
||||
if voice_stations.values() == list:
|
||||
sound_file = random.choice(voice_stations.get(announcement_for))
|
||||
else:
|
||||
sound_file = voice_stations.get(announcement_for)
|
||||
|
||||
sound_path = f"src/data/announcements/{sound_file}"
|
||||
if Path(sound_path).is_file() is False:
|
||||
logger(f"Konnte Datei {sound_path} nicht finden", "error")
|
||||
return None
|
||||
|
||||
return sound_path
|
||||
|
||||
def get_next_station(stops: dict) -> dict | None:
|
||||
now = datetime.now()
|
||||
today = datetime.today()
|
||||
day_offset = 0
|
||||
previous_time = None
|
||||
|
||||
for name, arrival_str in stops.items():
|
||||
arrival_time = datetime.strptime(arrival_str, "%H:%M").time()
|
||||
|
||||
if previous_time is not None and arrival_time < previous_time:
|
||||
day_offset += 1
|
||||
|
||||
arrival_dt = datetime.combine(today + timedelta(days=day_offset), arrival_time)
|
||||
previous_time = arrival_time
|
||||
|
||||
if arrival_dt >= now:
|
||||
return {"name": name, "arrival": arrival_str}
|
||||
|
||||
return None
|
||||
|
||||
def format_via_list(stops: dict) -> str:
|
||||
if len(stops) > 2:
|
||||
count = min(3, len(stops))
|
||||
random_stops = random.sample(list(stops), k=count)
|
||||
via = f"{', '.join(random_stops[:-1])} und {random_stops[-1]}"
|
||||
return via
|
||||
return None
|
||||
|
||||
def format_stop_list(stops: dict, next_stop: str | None) -> list[tuple[str, str]]:
|
||||
fields = []
|
||||
field_lines, field_length, part = [], 0, 1
|
||||
|
||||
for name, stop_arrival in stops.items():
|
||||
if name == next_stop:
|
||||
line = f"• __{name} ({stop_arrival} Uhr__)"
|
||||
else:
|
||||
line = f"• {name} ({stop_arrival} Uhr)"
|
||||
|
||||
if field_length + len(line) + 1 > 1024:
|
||||
route_page_name = "Route" if part == 1 else "Route (Fortsetzung)"
|
||||
fields.append((route_page_name, "\n".join(field_lines)))
|
||||
field_lines, field_length, part = [], 0, part + 1
|
||||
|
||||
field_lines.append(line)
|
||||
field_length += len(line) + 1
|
||||
|
||||
if field_lines:
|
||||
route_page_name = "Route" if part == 1 else "Route (Fortsetzung)"
|
||||
fields.append((route_page_name, "\n".join(field_lines)))
|
||||
|
||||
return fields
|
||||
Reference in New Issue
Block a user