mirror of
https://github.com/kaaninchen/Gleiswechsel.git
synced 2026-09-17 16:52:47 +00:00
Compare commits
37
Commits
768da0ee4d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f65aa3cfa0 | ||
|
|
e9708d435e | ||
|
|
d50cf0277b | ||
|
|
dde0c443de | ||
|
|
93b9712394 | ||
|
|
21a279839d | ||
|
|
1c006731a4 | ||
|
|
db43ee4279 | ||
|
|
dc2f9dade0 | ||
|
|
10b6ee560e | ||
|
|
53ded15209 | ||
|
|
c24749383b | ||
|
|
5609a3c425 | ||
|
|
8dfa092229 | ||
|
|
62d5569bcc | ||
|
|
f716b770dc | ||
|
|
9e7c645724 | ||
|
|
8c4e78c493 | ||
|
|
7e47874f78 | ||
|
|
1069879680 | ||
|
|
a4ae134ca0 | ||
|
|
054ab7fc42 | ||
|
|
999fe141b9 | ||
|
|
595e7160df | ||
|
|
ead53f2f75 | ||
|
|
ed7ae614cc | ||
|
|
714841df22 | ||
|
|
cd2d968545 | ||
|
|
2e62355db9 | ||
|
|
e6013a437f | ||
|
|
d46a04af0c | ||
|
|
6006a561b2 | ||
|
|
881d2e865b | ||
|
|
66ec499c27 | ||
|
|
7a3902b7d3 | ||
|
|
bd506ff9da | ||
|
|
59b2ccd64a |
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
@@ -1,6 +1,7 @@
|
|||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
src/data/announcements/*
|
src/data/announcements/*
|
||||||
|
src/data/assets/current_map.png
|
||||||
!src/data/announcements/.gitkeep
|
!src/data/announcements/.gitkeep
|
||||||
config.json
|
config.json
|
||||||
stations.json
|
stations.json
|
||||||
@@ -4,7 +4,7 @@ Gleiswechsel [ˈɡlaɪsvɛksəl] (german for "platform change") is a selfhostabl
|
|||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [International support](https://transitous.org/sources/) for both countries and cities
|
- [International support](https://transitous.org/sources/) for countries as well as cities
|
||||||
- Support for various types of transportations, including but not limited to Subways, Busses, Funiculars, Trams and Trains
|
- Support for various types of transportations, including but not limited to Subways, Busses, Funiculars, Trams and Trains
|
||||||
- Multi language support (+ option to easily add more languages)
|
- Multi language support (+ option to easily add more languages)
|
||||||
- Announcements, including the option to join the voice chat and play an audio file while arriving at a specific station
|
- Announcements, including the option to join the voice chat and play an audio file while arriving at a specific station
|
||||||
@@ -36,55 +36,9 @@ Next, you should configure the bot to your liking
|
|||||||
## Config
|
## Config
|
||||||
Even if it seems a bit tedious, I highly recommend going through the entire config and checking if there's something that you would like to customize.
|
Even if it seems a bit tedious, I highly recommend going through the entire config and checking if there's something that you would like to customize.
|
||||||
|
|
||||||
I added an [example config](config.json.example):
|
I added an [example config](config.json.example). Rename `config.json.example` to `config.json` and fill it out.
|
||||||
```json
|
|
||||||
{
|
|
||||||
"discord": {
|
|
||||||
"token": "",
|
|
||||||
"server": ,
|
|
||||||
"vc": ,
|
|
||||||
"lang": "de",
|
|
||||||
"formatting": "┇",
|
|
||||||
"emojis": true
|
|
||||||
},
|
|
||||||
"connections": {
|
|
||||||
"stations": [
|
|
||||||
"Berlin Hauptbahnhof",
|
|
||||||
"Amsterdam",
|
|
||||||
"Helsinki"
|
|
||||||
],
|
|
||||||
"IDs": [],
|
|
||||||
"blacklist": [
|
|
||||||
"OTHER",
|
|
||||||
"RIDE_SHARING"
|
|
||||||
],
|
|
||||||
"min_duration": 10,
|
|
||||||
"max_duration": null,
|
|
||||||
"max_wait_time": null,
|
|
||||||
"timezone": "Europe/Berlin"
|
|
||||||
},
|
|
||||||
"announcements": {
|
|
||||||
"enabled": true,
|
|
||||||
"voice": [
|
|
||||||
{
|
|
||||||
"enabled": false,
|
|
||||||
"end_stations": {
|
|
||||||
"general": "general.aac",
|
|
||||||
},
|
|
||||||
"stops": {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"user_agent": "Gleiswechsel-Discord-Bot"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You have to rename `config.json.example` to `config.json` and fill out the essential fields so that the bot is usable.
|
### Config explanations
|
||||||
|
|
||||||
### Explanations
|
|
||||||
|
|
||||||
#### discord
|
#### discord
|
||||||
|
|
||||||
@@ -128,13 +82,11 @@ but you can safely ignore this warning if you're fine with it.
|
|||||||
If not, I've created a tool which would help you to get the exact station name. You can use the tool by running
|
If not, I've created a tool which would help you to get the exact station name. You can use the tool by running
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ python run main.py stations
|
$ python main.py stations
|
||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
```sh
|
```json
|
||||||
22:47:56: INFO: Exact match found! London is an assigned station! Bot would use that station directly
|
|
||||||
22:48:33: INFO: {
|
|
||||||
"stations": {
|
"stations": {
|
||||||
"Amsterdam": [
|
"Amsterdam": [
|
||||||
"Amsterdam Zuid",
|
"Amsterdam Zuid",
|
||||||
@@ -157,8 +109,7 @@ Example output:
|
|||||||
"S Buch (Berlin)",
|
"S Buch (Berlin)",
|
||||||
"U Hönow (Berlin)"
|
"U Hönow (Berlin)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The tool will also ask if it should save a .json file with more informations for every similar station. If you're unsure about what which station is, then it can be really helpful! It would give you data like which types of transports arrive at every similar station, in which country they are and also their coordinates.
|
The tool will also ask if it should save a .json file with more informations for every similar station. If you're unsure about what which station is, then it can be really helpful! It would give you data like which types of transports arrive at every similar station, in which country they are and also their coordinates.
|
||||||
@@ -170,12 +121,35 @@ For every station inside of the `stations.json` you'll find an ID. You can add t
|
|||||||
|
|
||||||
This is especially useful if you want to add a station whose name isn't unique and also used by other stations. The bot would falsely use the first station with the same name and consider it an exact match, even if you wanted a different one. This won't happen with the ID, as every ID is uniquely assigned to only one station.
|
This is especially useful if you want to add a station whose name isn't unique and also used by other stations. The bot would falsely use the first station with the same name and consider it an exact match, even if you wanted a different one. This won't happen with the ID, as every ID is uniquely assigned to only one station.
|
||||||
|
|
||||||
|
##### priority
|
||||||
|
You can define priorities of transport modes while selecting a connection. Leave empty to disable
|
||||||
|
|
||||||
|
The list should be descending, with "1" as the most important transport mode. For example, let's say, the bot should always choose Trains and other longer distance public transport modes. If it can't find any, then it should resort to Metros/Subways/Suburbans. If these are also missing at that station, then it should just use Busses/Trams:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"priority": {
|
||||||
|
"HIGHSPEED_RAIL": 1,
|
||||||
|
"LONG_DISTANCE": 1,
|
||||||
|
"NIGHT_RAIL": 1,
|
||||||
|
"REGIONAL_RAIL": 1,
|
||||||
|
"COACH": 1,
|
||||||
|
"FERRY": 1,
|
||||||
|
"METRO": 2,
|
||||||
|
"SUBURBAN": 2,
|
||||||
|
"SUBWAY": 2,
|
||||||
|
"BUS": 3,
|
||||||
|
"TRAM": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
A transport mode that isn't in the priority list would immediately get assigned the value of "99", making it near impossible to get that connection. Except when there are no other connections available, of course.
|
||||||
|
|
||||||
|
You can find the MODE names either in the console log or the stations.json from [helper tool](#stations)
|
||||||
|
|
||||||
##### blacklist
|
##### blacklist
|
||||||
You can blacklist specific types of transport, the bot would then skip them while selecting a connection. You can get the type either in your console (mode)
|
You can blacklist specific types of transport, the bot would then skip them while selecting a connection. You can get the type in your console (mode)
|
||||||
```sh
|
```sh
|
||||||
22:44:47: INFO: Agency: GVB, mode: TRAM
|
22:44:47: INFO: Agency: GVB, mode: TRAM
|
||||||
```
|
```
|
||||||
or in the `/info` embed.
|
|
||||||
|
|
||||||
I highly recommend keeping `"OTHER"` blacklisted, if the API doesn't know what that is then we probably shouldn't use it. Also, it would probably a good idea to keep `"RIDE_SHARING"` blacklisted as they have weird timetables
|
I highly recommend keeping `"OTHER"` blacklisted, if the API doesn't know what that is then we probably shouldn't use it. Also, it would probably a good idea to keep `"RIDE_SHARING"` blacklisted as they have weird timetables
|
||||||
|
|
||||||
@@ -190,6 +164,8 @@ I highly recommend keeping `"OTHER"` blacklisted, if the API doesn't know what t
|
|||||||
- `timezone` is timezone in the IANA timezone format. You can [look it up here](https://www.addevent.com/c/documentation/tools/time-zone-lookup)
|
- `timezone` is timezone in the IANA timezone format. You can [look it up here](https://www.addevent.com/c/documentation/tools/time-zone-lookup)
|
||||||
|
|
||||||
##### announcements
|
##### announcements
|
||||||
|
|
||||||
|
##### `text_announcements:`
|
||||||
The bot can send a text announcement in the voice chat at the start/end of a trip.
|
The bot can send a text announcement in the voice chat at the start/end of a trip.
|
||||||
At the end of a trip, it would send this embed:
|
At the end of a trip, it would send this embed:
|
||||||
|
|
||||||
@@ -199,46 +175,25 @@ It will also send the `/info` embed at the start of a new connection with inform
|
|||||||
|
|
||||||
To reduce spam, the bot will only send announcements if someone is in the voice chat
|
To reduce spam, the bot will only send announcements if someone is in the voice chat
|
||||||
|
|
||||||
##### voice
|
##### `voice_announcements:`
|
||||||
⚠️ Requires `announcements` to be set to enabled
|
The bot can join the voice chat, play an audio file, and disconnect from the voice chat, at various points of your trip. You have to have [FFmpeg](https://www.ffmpeg.org/) installed for this to work.
|
||||||
|
|
||||||
The bot can join the voice chat, play an audio file, and leave at various points of your trip. You have to have [FFmpeg](https://www.ffmpeg.org/) installed for this to work.
|
Place the audio file of your desired station in [src/data/announcements](src/data/announcements/) with the EXACT name of the station. The bot will automatically check if an audio file with the stations name exists, and if it does, play it.
|
||||||
|
|
||||||
Place the audio file of your desired station in [src/data/announcements](src/data/announcements/). Then, define the stations name with the name of the audio file in either `end_stations` or `stops`. The path will be autocompleted to [src/data/announcements](src/data/announcements/). The station name has to be EXACT, if you're unsure then [get the name through the helper script](#stations)
|
You don't have to restart the bot after placing new audio files in the folder.
|
||||||
|
|
||||||
`end_stations` is for audio files that should play at the end of your trip, and `stops` is for audio files that should play while the train is passing through your desired station. If you set the name of a station to `general`, then the bot will always play that file before the trip ends/a new stop has been reached.
|
#### `map:`
|
||||||
|
The bot can generate a map of your route in the `/info` embed. Set `map` to `true` to enable.
|
||||||
I'm hoping that I didn't explain this too complicated. Here's an example to visualize this:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```json
|
|
||||||
"announcements": {
|
|
||||||
"enabled": true,
|
|
||||||
"voice": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"end_stations": {
|
|
||||||
"general": "general.aac",
|
|
||||||
"Hannover Hbf": "hannover.aac"
|
|
||||||
},
|
|
||||||
"stops": {
|
|
||||||
"Amsterdam, Noorderpark": "noorderpark.aac"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
#### http
|
#### http
|
||||||
|
|
||||||
- `"user_agent"`: The user agent of the bot for the API. If you don't know what that is, then you shouldn't have to change that. Even if you do, you still probably don't have to
|
- `"user_agent"`: The user agent of the bot for the API. If you don't know what that is, then you shouldn't have to change that. Modify the contact URL (in my case, the github repo) if you change a lot of the code, so that transitous can contact you in case of any issues
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
After you've set everything up, you're ready to start the bot!
|
After you've set everything up, you're ready to start the bot!
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ python run main.py
|
$ python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## src/data
|
## src/data
|
||||||
@@ -260,5 +215,5 @@ Covered in [locales](#locales)
|
|||||||
### [announcements/](src/data/announcements/)
|
### [announcements/](src/data/announcements/)
|
||||||
Covered in [voice announcements](#voice)
|
Covered in [voice announcements](#voice)
|
||||||
|
|
||||||
### [announcements](src/data/announcements/)
|
### [assets](src/data/assets/)
|
||||||
Assets (such as images) which the bot uses. You shouldn't have to change anything there
|
Assets (such as images) which the bot uses. You shouldn't have to change anything there
|
||||||
+9
-16
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"discord": {
|
"discord": {
|
||||||
"token": "",
|
"token": "",
|
||||||
"server": ,
|
"server": "",
|
||||||
"vc": ,
|
"vc": "",
|
||||||
"lang": "de",
|
"lang": "de",
|
||||||
"formatting": "┇",
|
"formatting": "┇",
|
||||||
"emojis": true
|
"emojis": true
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"Amsterdam",
|
"Amsterdam",
|
||||||
"Helsinki"
|
"Helsinki"
|
||||||
],
|
],
|
||||||
"IDs: []
|
"IDs": [],
|
||||||
|
"priority": {},
|
||||||
"blacklist": [
|
"blacklist": [
|
||||||
"OTHER",
|
"OTHER",
|
||||||
"RIDE_SHARING"
|
"RIDE_SHARING"
|
||||||
@@ -24,19 +25,11 @@
|
|||||||
"timezone": "Europe/Berlin"
|
"timezone": "Europe/Berlin"
|
||||||
},
|
},
|
||||||
"announcements": {
|
"announcements": {
|
||||||
"enabled": true,
|
"text_announcements": true,
|
||||||
"voice": [
|
"voice_announcements": false,
|
||||||
{
|
"map": true
|
||||||
"enabled": false,
|
|
||||||
"end_stations": {
|
|
||||||
"general": "general.aac",
|
|
||||||
},
|
|
||||||
"stops": {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"user_agent": "Gleiswechsel-Discord-Bot"
|
"user_agent": "Gleiswechsel-Discord-Bot/1.0 (https://github.com/kaaninchen/Gleiswechsel)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,6 @@ async def on_ready():
|
|||||||
logger(f"{bot.user} is online")
|
logger(f"{bot.user} is online")
|
||||||
_bot_initialized = True
|
_bot_initialized = True
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name="tschu tschu! • /info"))
|
|
||||||
|
|
||||||
server_id = config.discord.server
|
server_id = config.discord.server
|
||||||
server_vc_id = config.discord.vc
|
server_vc_id = config.discord.vc
|
||||||
channel = validate_channel(bot=bot, server_id=server_id, channel_id=server_vc_id)
|
channel = validate_channel(bot=bot, server_id=server_id, channel_id=server_vc_id)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ numpy==2.5.1
|
|||||||
pandas==3.0.3
|
pandas==3.0.3
|
||||||
propcache==0.5.2
|
propcache==0.5.2
|
||||||
py-cord==2.8.0
|
py-cord==2.8.0
|
||||||
|
py-staticmaps[cairo]==0.5.0
|
||||||
pyyaml==6.0.3
|
pyyaml==6.0.3
|
||||||
pycparser==3.0
|
pycparser==3.0
|
||||||
pynacl==1.6.2
|
pynacl==1.6.2
|
||||||
|
|||||||
+70
-69
@@ -11,7 +11,6 @@ from src.lang.locales import lang
|
|||||||
station_names = config.connections.stations
|
station_names = config.connections.stations
|
||||||
station_IDs = config.connections.IDs
|
station_IDs = config.connections.IDs
|
||||||
blacklist = config.connections.blacklist
|
blacklist = config.connections.blacklist
|
||||||
long_name_lang = lang.channel.long_name
|
|
||||||
user_agent = config.http.user_agent
|
user_agent = config.http.user_agent
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
@@ -21,10 +20,9 @@ headers = {
|
|||||||
endpoint = "https://api.transitous.org"
|
endpoint = "https://api.transitous.org"
|
||||||
|
|
||||||
def check_stations():
|
def check_stations():
|
||||||
|
logger(f"Testing {len(station_names)} stations...")
|
||||||
all_stations_output = {}
|
all_stations_output = {}
|
||||||
minimal_overview = {
|
minimal_overview = {}
|
||||||
"stations": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for station in station_names:
|
for station in station_names:
|
||||||
req = f"{endpoint}/api/v1/geocode"
|
req = f"{endpoint}/api/v1/geocode"
|
||||||
@@ -54,17 +52,16 @@ def check_stations():
|
|||||||
"tz": entry.get("tz"),
|
"tz": entry.get("tz"),
|
||||||
"country": entry.get("country"),
|
"country": entry.get("country"),
|
||||||
"coords": coords,
|
"coords": coords,
|
||||||
"modes": modes,
|
|
||||||
"id": station_id,
|
"id": station_id,
|
||||||
|
"modes": modes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if stop_name:
|
if stop_name:
|
||||||
stops_dict[stop_name] = stop_details
|
stops_dict[stop_name] = stop_details
|
||||||
aliases_list.append(stop_name)
|
aliases_list.append(stop_name)
|
||||||
|
|
||||||
if stop_name == station:
|
if stop_name == station:
|
||||||
if not exact_match:
|
if not exact_match:
|
||||||
logger(f"Exact match found! {station} is an assigned station! Bot would use that station directly")
|
|
||||||
exact_match = True
|
exact_match = True
|
||||||
|
|
||||||
if not stops_dict:
|
if not stops_dict:
|
||||||
@@ -72,10 +69,14 @@ def check_stations():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
all_stations_output[station] = {
|
all_stations_output[station] = {
|
||||||
|
"exact_match": exact_match,
|
||||||
"associated": stops_dict
|
"associated": stops_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
minimal_overview["stations"][station] = aliases_list
|
minimal_overview[station] = {
|
||||||
|
"exact_match": exact_match,
|
||||||
|
"names": aliases_list
|
||||||
|
}
|
||||||
|
|
||||||
logger(json.dumps(minimal_overview, indent=4, ensure_ascii=False))
|
logger(json.dumps(minimal_overview, indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
@@ -133,7 +134,6 @@ def get_random_stop_id() -> str | None:
|
|||||||
|
|
||||||
if stop_name == assigned_station:
|
if stop_name == assigned_station:
|
||||||
stop_ids.clear()
|
stop_ids.clear()
|
||||||
logger(f"Exact match found! Using {stop_name}")
|
|
||||||
stop_ids[stop_id] = stop_name
|
stop_ids[stop_id] = stop_name
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -142,11 +142,12 @@ def get_random_stop_id() -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
stop_ids_list = list(stop_ids.keys())
|
stop_ids_list = list(stop_ids.keys())
|
||||||
|
chosen_stop_id = random.choice(stop_ids_list)
|
||||||
if len(stop_ids_list) > 1:
|
if len(stop_ids_list) > 1:
|
||||||
logger(f"No station associated as '{assigned_station}', choosing random from similar named stations")
|
logger(f"No station associated as '{assigned_station}', choosing random from similar named stations")
|
||||||
logger(f"Run `python run main.py stations` to get exact station names")
|
logger(f"Run `python run main.py stations` to get exact station names")
|
||||||
chosen_stop_id, chosen_station = random.choice(list(stop_ids.items()))
|
logger(f"Selected station: {stop_ids[chosen_stop_id]}")
|
||||||
logger(f"Assigned Station: {chosen_station}")
|
|
||||||
return chosen_stop_id
|
return chosen_stop_id
|
||||||
|
|
||||||
def get_random_connection(stop_id: str) -> str | None:
|
def get_random_connection(stop_id: str) -> str | None:
|
||||||
@@ -154,60 +155,46 @@ def get_random_connection(stop_id: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
cursor = None
|
params = {"stopId": stop_id, "n": 50, "time": now}
|
||||||
max_pages = 5
|
|
||||||
min_results = 5
|
|
||||||
trip_ids = []
|
|
||||||
all_stop_times = []
|
|
||||||
|
|
||||||
for _ in range(max_pages):
|
try:
|
||||||
params = {
|
response = requests.get(f"{endpoint}/api/v5/stoptimes", params=params, headers=headers)
|
||||||
"stopId": stop_id,
|
response.raise_for_status()
|
||||||
"n": 20,
|
data = response.json()
|
||||||
"time": now,
|
except requests.RequestException as e:
|
||||||
}
|
logger(e, "Error")
|
||||||
if cursor:
|
return None
|
||||||
params["pageCursor"] = cursor
|
|
||||||
|
|
||||||
try:
|
candidates = []
|
||||||
response = requests.get(f"{endpoint}/api/v5/stoptimes", params=params, headers=headers)
|
for entry in data.get("stopTimes", []):
|
||||||
response.raise_for_status()
|
if entry["mode"] in blacklist:
|
||||||
data = response.json()
|
continue
|
||||||
except requests.RequestException as e:
|
candidates.append(entry)
|
||||||
logger(e, "Error")
|
|
||||||
break
|
|
||||||
|
|
||||||
stop_times = data.get("stopTimes", [])
|
if not candidates:
|
||||||
all_stop_times.extend(stop_times)
|
logger("Couldn't find any connections", "error")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if config.connections.priority:
|
||||||
|
best_priority = None
|
||||||
|
for entry in candidates:
|
||||||
|
priority = config.connections.priority.get(entry["mode"], 99)
|
||||||
|
if best_priority is None or priority < best_priority:
|
||||||
|
best_priority = priority
|
||||||
|
|
||||||
for entry in stop_times:
|
best_candidates = []
|
||||||
trip_id = entry["tripId"]
|
for entry in candidates:
|
||||||
if entry["mode"] in blacklist:
|
priority = config.connections.priority.get(entry["mode"], 99)
|
||||||
continue
|
if priority == best_priority:
|
||||||
else:
|
best_candidates.append(entry)
|
||||||
trip_ids.append(entry["tripId"])
|
|
||||||
|
|
||||||
if len(trip_ids) >= min_results:
|
chosen = random.choice(best_candidates)
|
||||||
break
|
else:
|
||||||
|
chosen = random.choice(candidates)
|
||||||
cursor = data.get("nextPageCursor")
|
|
||||||
if not cursor:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not trip_ids:
|
|
||||||
logger("Couldn't find any connections", "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 {
|
return {
|
||||||
"trip_id": trip_id,
|
"trip_id": chosen["tripId"],
|
||||||
"from_station": from_station
|
"from_station": chosen.get("place", {}).get("name"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_trip_details(random_connection: dict | None) -> dict | None:
|
def get_trip_details(random_connection: dict | None) -> dict | None:
|
||||||
@@ -238,18 +225,25 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
logger(f"Bot doesn't know what to do with round trips, I'll have to implement this some day", "error")
|
logger(f"Bot doesn't know what to do with round trips, I'll have to implement this some day", "error")
|
||||||
return None # TODO actually handel this
|
return None # TODO actually handel this
|
||||||
|
|
||||||
|
all_stop_names = [train_from] + [stop["name"] for stop in legs["intermediateStops"]] + [goes_to]
|
||||||
|
if len(all_stop_names) != len(set(all_stop_names)):
|
||||||
|
logger("Route reuses station names! Skipping, to be safe...", "error")
|
||||||
|
return None # TODO this too
|
||||||
|
|
||||||
arrival_dt = parse_iso(end_time)
|
arrival_dt = parse_iso(end_time)
|
||||||
departure_dt = parse_iso(start_time)
|
departure_dt = parse_iso(start_time)
|
||||||
|
|
||||||
train_name = get_train_name(display_name, mode)
|
train_name = get_train_name(display_name, mode) # used by lang
|
||||||
if train_from == from_station:
|
if len(lang.channel.long_name.train_from()) >= 100:
|
||||||
long_name = long_name_lang.train_from()
|
channel_name = lang.channel.short_name
|
||||||
else:
|
else:
|
||||||
long_name = long_name_lang.train_via()
|
if train_from == from_station:
|
||||||
|
channel_name = lang.channel.long_name.train_from()
|
||||||
|
else:
|
||||||
|
channel_name = lang.channel.long_name.train_via()
|
||||||
|
|
||||||
trip_details = {
|
trip_details = {
|
||||||
"long_name": long_name,
|
"channel_name": channel_name,
|
||||||
"short_name": display_name,
|
|
||||||
"station": from_station,
|
"station": from_station,
|
||||||
"from": train_from,
|
"from": train_from,
|
||||||
"to": goes_to,
|
"to": goes_to,
|
||||||
@@ -266,7 +260,9 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
train_from_importance = legs["from"]["importance"]
|
train_from_importance = legs["from"]["importance"]
|
||||||
trip_details["stops"][train_from] = {
|
trip_details["stops"][train_from] = {
|
||||||
"arrival": departure_dt,
|
"arrival": departure_dt,
|
||||||
"importance": train_from_importance
|
"importance": train_from_importance,
|
||||||
|
"lat": legs["from"]["lat"],
|
||||||
|
"lon": legs["from"]["lon"]
|
||||||
}
|
}
|
||||||
|
|
||||||
departure_time_iso = start_time
|
departure_time_iso = start_time
|
||||||
@@ -276,9 +272,12 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
stop_importance = stop["importance"]
|
stop_importance = stop["importance"]
|
||||||
stop_details = {
|
stop_details = {
|
||||||
"arrival": stop_arrival_dt,
|
"arrival": stop_arrival_dt,
|
||||||
"importance": stop_importance
|
"importance": stop_importance,
|
||||||
|
"lat": stop["lat"],
|
||||||
|
"lon": stop["lon"]
|
||||||
}
|
}
|
||||||
trip_details["stops"][stop["name"]] = stop_details
|
trip_details["stops"][stop["name"]] = stop_details
|
||||||
|
|
||||||
if stop.get("name") == from_station:
|
if stop.get("name") == from_station:
|
||||||
departure_time_iso = stop["departure"]
|
departure_time_iso = stop["departure"]
|
||||||
trip_details["departure"] = parse_iso(departure_time_iso).strftime("%H:%M")
|
trip_details["departure"] = parse_iso(departure_time_iso).strftime("%H:%M")
|
||||||
@@ -286,12 +285,14 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
train_to_importance = legs["to"]["importance"]
|
train_to_importance = legs["to"]["importance"]
|
||||||
trip_details["stops"][goes_to] = {
|
trip_details["stops"][goes_to] = {
|
||||||
"arrival": arrival_dt,
|
"arrival": arrival_dt,
|
||||||
"importance": train_to_importance
|
"importance": train_to_importance,
|
||||||
|
"lat": legs["to"]["lat"],
|
||||||
|
"lon": legs["to"]["lon"]
|
||||||
}
|
}
|
||||||
|
|
||||||
valid = validate_connection(start_time, end_time, departure_time_iso)
|
valid = validate_connection(start_time, end_time, departure_time_iso)
|
||||||
if not valid:
|
if not valid:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return trip_details
|
return trip_details
|
||||||
|
|
||||||
|
|||||||
+6
-13
@@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -15,22 +15,18 @@ class DiscordConfig:
|
|||||||
class ConnectionsConfig:
|
class ConnectionsConfig:
|
||||||
stations: list[str]
|
stations: list[str]
|
||||||
IDs: list[str]
|
IDs: list[str]
|
||||||
|
priority: dict
|
||||||
blacklist: list[str]
|
blacklist: list[str]
|
||||||
min_duration: int
|
min_duration: int
|
||||||
timezone: str
|
timezone: str
|
||||||
max_wait_time: Optional[int] = None
|
max_wait_time: Optional[int] = None
|
||||||
max_duration: Optional[int] = None
|
max_duration: Optional[int] = None
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class VoiceAnnouncementConfig:
|
|
||||||
enabled: bool
|
|
||||||
end_stations: dict[str, str]
|
|
||||||
stops: dict[str, str]
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AnnouncementConfig:
|
class AnnouncementConfig:
|
||||||
enabled: bool
|
text_announcements: bool
|
||||||
voice: list[VoiceAnnouncementConfig]
|
voice_announcements: bool
|
||||||
|
map: bool
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HttpConfig:
|
class HttpConfig:
|
||||||
@@ -50,10 +46,7 @@ def _load_config() -> Config:
|
|||||||
return Config(
|
return Config(
|
||||||
discord=DiscordConfig(**raw["discord"]),
|
discord=DiscordConfig(**raw["discord"]),
|
||||||
connections=ConnectionsConfig(**raw["connections"]),
|
connections=ConnectionsConfig(**raw["connections"]),
|
||||||
announcements=AnnouncementConfig(
|
announcements=AnnouncementConfig(**raw["announcements"]),
|
||||||
enabled=raw["announcements"]["enabled"],
|
|
||||||
voice=[VoiceAnnouncementConfig(**v) for v in raw["announcements"]["voice"]],
|
|
||||||
),
|
|
||||||
http=HttpConfig(**raw["http"]),
|
http=HttpConfig(**raw["http"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,2 +0,0 @@
|
|||||||
SPDX-FileCopyrightText: 2024 Mathis Brüchert <[email protected]>
|
|
||||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
||||||
@@ -2,9 +2,8 @@ channel:
|
|||||||
long_name:
|
long_name:
|
||||||
train_from: "{train_name} nach {goes_to} von {train_from}"
|
train_from: "{train_name} nach {goes_to} von {train_from}"
|
||||||
train_via: "{train_name} nach {goes_to} über {from_station}"
|
train_via: "{train_name} nach {goes_to} über {from_station}"
|
||||||
|
short_name: "{train_name} nach {goes_to}"
|
||||||
embeds:
|
embeds:
|
||||||
footer:
|
|
||||||
notice: "Daten großzügig bereitgestellt von https://transitous.org"
|
|
||||||
info:
|
info:
|
||||||
description: "Abfahrt von {station} um {departure}. Ankunft um {arrival}"
|
description: "Abfahrt von {station} um {departure}. Ankunft um {arrival}"
|
||||||
via: "Über"
|
via: "Über"
|
||||||
@@ -14,3 +13,5 @@ embeds:
|
|||||||
title: "Informationen zu ihrer Fahrt"
|
title: "Informationen zu ihrer Fahrt"
|
||||||
"end_of_connection":
|
"end_of_connection":
|
||||||
message: "Sehr geehrte Fahrgäste,\nIn wenigen Minuten erreichen wir {destination}. Unsere Reise endet dort\n\nWir wünschen Ihnen eine angenehme Weiterreise.\n\nVielen Dank für ihr Vertrauen und auf Wiedersehen."
|
message: "Sehr geehrte Fahrgäste,\nIn wenigen Minuten erreichen wir {destination}. Unsere Reise endet dort\n\nWir wünschen Ihnen eine angenehme Weiterreise.\n\nVielen Dank für ihr Vertrauen und auf Wiedersehen."
|
||||||
|
rich_presence:
|
||||||
|
arrival: "Ankunft um {arrival} • /info"
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ channel:
|
|||||||
long_name:
|
long_name:
|
||||||
train_from: "{train_name} to {goes_to} from {train_from}"
|
train_from: "{train_name} to {goes_to} from {train_from}"
|
||||||
train_via: "{train_name} to {goes_to} via {from_station}"
|
train_via: "{train_name} to {goes_to} via {from_station}"
|
||||||
|
short_name: "{train_name} to {goes_to}"
|
||||||
embeds:
|
embeds:
|
||||||
footer:
|
|
||||||
notice: "Data provided by https://transitous.org"
|
|
||||||
info:
|
info:
|
||||||
description: "Departure from {station} at {departure}. Arrival by {arrival}"
|
description: "Departure from {station} at {departure}. Arrival by {arrival}"
|
||||||
via: "via"
|
via: "via"
|
||||||
@@ -14,3 +13,5 @@ embeds:
|
|||||||
title: "Information about your trip"
|
title: "Information about your trip"
|
||||||
"end_of_connection":
|
"end_of_connection":
|
||||||
message: "Dear Passengers,\nwe will be arriving at {destination} in a few minutes. Our journey ends there.\n\nWe wish you a pleasant onward journey.\n\nThank you for your patronage, and goodbye."
|
message: "Dear Passengers,\nwe will be arriving at {destination} in a few minutes. Our journey ends there.\n\nWe wish you a pleasant onward journey.\n\nThank you for your patronage, and goodbye."
|
||||||
|
rich_presence:
|
||||||
|
arrival: "Arrival by {arrival} • /info"
|
||||||
|
|||||||
+148
-97
@@ -1,115 +1,162 @@
|
|||||||
OPERATORS = {
|
OPERATORS = {
|
||||||
"fallback": {
|
"Abellio Rail Mitteldeutschland GmbH": {
|
||||||
"logo": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQCVeC5E0mNNBKyQftQaFMzxIVkbDvEnSzWv07h_c8PdA&s=10",
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Abellio_logo.svg/1920px-Abellio_logo.svg.png",
|
||||||
"color": 0xFFFFFF
|
"color": 0xD7002E,
|
||||||
|
"slogan": ["Freie Bahn für deine Ziele!"]
|
||||||
|
},
|
||||||
|
"Agilis": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a3/Agilis_Logo.svg/960px-Agilis_Logo.svg.png",
|
||||||
|
"color": 0xCAE15B,
|
||||||
|
"slogan": ["Reinsetzen, zurücklehnen", "Fahren Sie mit uns. Nehmen Sie Platz."]
|
||||||
|
},
|
||||||
|
"Aktiv Bus Flensburg GmbH": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Aktiv_Bus_Flensburg_GmbH_logo.svg/960px-Aktiv_Bus_Flensburg_GmbH_logo.svg.png?utm_source=de.wikipedia.org&utm_campaign=index&utm_content=thumbnail",
|
||||||
|
"color": 0xFFC331
|
||||||
|
},
|
||||||
|
"Arverio Bayern": {
|
||||||
|
"logo": "https://cdn.discordapp.com/attachments/1383843132906537023/1528006572805062776/Arverio_Avi_Bayern_blau_RGB.png?ex=6a5cba83&is=6a5b6903&hm=5781ae6c372c92ab4f52409c6fc91e5014ac34ccf24db665ade052e8135bdde0&animated=true",
|
||||||
|
"color": 0x0083BE,
|
||||||
|
"slogan": ["Dein Weg ist unser Ziel."]
|
||||||
|
},
|
||||||
|
"Berliner Verkehrsbetriebe": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/BVG_Logo_07.2021.svg/960px-BVG_Logo_07.2021.svg.png",
|
||||||
|
"color": 0xEFD13C,
|
||||||
|
"slogan": ["Weil wir dich lieben"]
|
||||||
|
},
|
||||||
|
"DSB": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/DSB_company_logo.svg/960px-DSB_company_logo.svg.png",
|
||||||
|
"color": 0xB22B32,
|
||||||
|
"slogan": ["Tak for at vise hensyn"]
|
||||||
|
},
|
||||||
|
"enno": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Enno_logo.svg/960px-Enno_logo.svg.png",
|
||||||
|
"color": 0x88216F,
|
||||||
|
"slogan": ["enno fährt bunt"]
|
||||||
|
},
|
||||||
|
"Erfurter Bahn": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Erfurter_Bahn_logo.svg/960px-Erfurter_Bahn_logo.svg.png",
|
||||||
|
"color": 0x009133,
|
||||||
|
"slogan": ["Kommt gut an."]
|
||||||
|
},
|
||||||
|
"eurobahn": {
|
||||||
|
"logo": "https://cdn.discordapp.com/attachments/1383843132906537023/1528405390260179146/Eurpnajm.png?ex=6a5e2df1&is=6a5cdc71&hm=d73fc3458f59f8942a5a0f51311308b12567b35b55b64aab0726bf6be92b3b0e&animated=true",
|
||||||
|
"color": 0x005a9b,
|
||||||
|
"slogan": ["Echte Menschen. Echt schöne Ziele."]
|
||||||
|
},
|
||||||
|
"European Sleeper": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/European_Sleeper_Logo.svg/960px-European_Sleeper_Logo.svg.png",
|
||||||
|
"color": 0xEB4A27,
|
||||||
|
"slogan": ["The Good Night Train", "Travel while you sleep"]
|
||||||
|
},
|
||||||
|
"Flixbus": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Flixbus_201x_logo.svg/1280px-Flixbus_201x_logo.svg.png",
|
||||||
|
"color": 0x8CD541,
|
||||||
|
"slogan": ["Einfach Busfahren.", "Fahr grün!", "FLIX it", "Mehr Leben im echten Leben"]
|
||||||
|
},
|
||||||
|
"GoVolta": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/GoVolta_logo.svg/1280px-GoVolta_logo.svg.png",
|
||||||
|
"color": 0x0B70F6,
|
||||||
|
"slogan": ["Internationale Züge, endlich erschwinglich!", "International trains, finally affordable!"]
|
||||||
|
},
|
||||||
|
"GVB": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/BSicon_LOGO_GVB.svg/960px-BSicon_LOGO_GVB.svg.png",
|
||||||
|
"color": 0x2B62AF,
|
||||||
|
},
|
||||||
|
"Jenaer Nahverkehr GmbH": {
|
||||||
|
"logo": "https://www.stadtwerke-jena.de/dam/jcr:74dc9d81-0447-4864-973b-7e395f1d6948/JNV_Bildmarke_322px.png",
|
||||||
|
"color": 0x0075BD
|
||||||
|
},
|
||||||
|
"Nordbahn Eisenbahngesellschaft": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/b/b5/Logo_Nordbahn_NAH.SH_Blau_positiv_final.png",
|
||||||
|
"color": 0x1A2848,
|
||||||
|
"slogan": ["nordbahn kommt von Norden", "Bringt den Norden näher", "Wir transportieren das Biest"]
|
||||||
|
},
|
||||||
|
"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 hebben een serieus probleem", "Neuken in de keuken"]
|
||||||
|
},
|
||||||
|
"Ostdeutsche Eisenbahn GmbH": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/ODEG-Logo_Neu.svg/960px-ODEG-Logo_Neu.svg.png",
|
||||||
|
"color": 0x00745C,
|
||||||
|
"slogan": ["Rücksicht kommt gut", "Benimm dich, als wäre Mutti dabei.", "Sicher mit der Sachsenschleuder"]
|
||||||
|
},
|
||||||
|
"Regionalverkehre Start Deutschland GmbH (Start Mitteldeutschland)": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Logo_der_Regionalverkehre_Start_Deutschland.svg/960px-Logo_der_Regionalverkehre_Start_Deutschland.svg.png",
|
||||||
|
"color": 0x61A731,
|
||||||
|
},
|
||||||
|
"S-Bahn": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/S-Bahn-Logo.svg/500px-S-Bahn-Logo.svg.png",
|
||||||
|
"color": 0x3E8B55
|
||||||
|
},
|
||||||
|
"S-Bahn Hannover (Transdev)": {
|
||||||
|
"logo": "https://cdn.discordapp.com/attachments/1526274436523888732/1528470315234234520/sbahn.png?ex=6a5e6a68&is=6a5d18e8&hm=c9f630c6f0c646eafeccef9b4d591ce8fcf8856b2b45f14e72fbf25e32edb017&animated=true",
|
||||||
|
"color": 0x1A4389,
|
||||||
|
"slogan": ["Wir geben alles", "Wir bringen dich", "Wir bringen Respekt"]
|
||||||
|
},
|
||||||
|
"SBB": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/23/SBB_logo_simplified.svg/960px-SBB_logo_simplified.svg.png",
|
||||||
|
"color": 0xEB0000,
|
||||||
|
"slogan": ["Der Kluge reist im Zuge", "SBB schafft Verbindungen", "Unterwegs zuhause"]
|
||||||
|
},
|
||||||
|
"SNCF": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/en/thumb/f/f4/Sncf-logo.svg/960px-Sncf-logo.svg.png",
|
||||||
|
"color": 0x812B6D,
|
||||||
|
"slogan": ["Pour nous tous", "Donner au train des idées d'avance"]
|
||||||
|
},
|
||||||
|
"Stadtverkehr Tübingen GmbH": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Stadtwerke_T%C3%BCbingen_logo.svg/330px-Stadtwerke_T%C3%BCbingen_logo.svg.png",
|
||||||
|
"color": 0x439669
|
||||||
|
},
|
||||||
|
"Südwestdeutsche Verkehrs-AG": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/SWEG_Logo.svg/330px-SWEG_Logo.svg.png?utm_source=de.wikipedia.org&utm_campaign=index&utm_content=thumbnail",
|
||||||
|
"color": 0x1A509A
|
||||||
|
},
|
||||||
|
"Tallink Grupp AS": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Tallink_logo.svg/330px-Tallink_logo.svg.png",
|
||||||
|
"color": 0x225197
|
||||||
|
},
|
||||||
|
"Verkehrsbetrieb Potsdam GmbH": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Verkehrsbetrieb_Potsdam_logo.svg/330px-Verkehrsbetrieb_Potsdam_logo.svg.png",
|
||||||
|
"color": 0x000000
|
||||||
|
},
|
||||||
|
"Vr": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Vr_Logo.png/330px-Vr_Logo.png",
|
||||||
|
"color": 0x00B451,
|
||||||
|
"slogan": ["Aikaa yhteisellä matkalla"]
|
||||||
|
},
|
||||||
|
"Westbahn Management GmbH": {
|
||||||
|
"logo": "https://corporate.westbahn.at/uploads/Logos/westbahn2025-logo-signet-small.png",
|
||||||
|
"color": 0x1D4A83,
|
||||||
|
"slogan": ["Immer ein guter Zug", "Da steig ich ein", "Jetzt aber Westbahn"]
|
||||||
|
},
|
||||||
|
"WestfalenBahn": {
|
||||||
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Westfalenbahn_logo.svg/330px-Westfalenbahn_logo.svg.png",
|
||||||
|
"color": 0x48A1DD,
|
||||||
|
"slogan": ["Weil's meine Region ist", "Hier bin ich daheim!"]
|
||||||
},
|
},
|
||||||
"db_allgemein": {
|
"db_allgemein": {
|
||||||
"logo": "https://marketingportal.extranet.deutschebahn.com/resource/blob/13602522/c53f806b9df966e144010b276af72dd2/Bild_09-data.png",
|
"logo": "https://marketingportal.extranet.deutschebahn.com/resource/blob/13602522/c53f806b9df966e144010b276af72dd2/Bild_09-data.png",
|
||||||
"color": 0xEC0016,
|
"color": 0xEC0016,
|
||||||
"slogan": ["Senk ju vor träwelling wis Deutsche Bahn.", "Bitte beachten Sie die umgekehrte Wagenreihung.", "Zurückbleiben bitte!", "Alle reden vom Wetter. Wir nicht.", "Die Bahn macht mobil.", "Grün abgefahren"]
|
"slogan": ["Senk ju vor träwelling wis Deutsche Bahn.", "Bitte beachten Sie die umgekehrte Wagenreihung.", "Zurückbleiben bitte!", "Alle reden vom Wetter. Wir nicht.", "Die Bahn macht mobil.", "Grün abgefahren"]
|
||||||
},
|
},
|
||||||
"db_bayern": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Bahnland_Bayern_Logo_2021.svg/500px-Bahnland_Bayern_Logo_2021.svg.png",
|
|
||||||
"color": 0x0095DB
|
|
||||||
},
|
|
||||||
"db_bawü": {
|
"db_bawü": {
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Bwegt_Logo.svg/960px-Bwegt_Logo.svg.png",
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Bwegt_Logo.svg/960px-Bwegt_Logo.svg.png",
|
||||||
"color": 0xFFBF34,
|
"color": 0xFFBF34,
|
||||||
"slogan": ["Nett hier", "Aber waren Sie schon mal in Baden-Württemberg?", "Bwegt euch!"]
|
"slogan": ["Nett hier", "Aber waren Sie schon mal in Baden-Württemberg?", "Bwegt euch!"]
|
||||||
},
|
},
|
||||||
"Ostdeutsche Eisenbahn GmbH": {
|
"db_bayern": {
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/ODEG-Logo_Neu.svg/960px-ODEG-Logo_Neu.svg.png",
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Bahnland_Bayern_Logo_2021.svg/500px-Bahnland_Bayern_Logo_2021.svg.png",
|
||||||
"color": 0x00745C
|
"color": 0x0095DB,
|
||||||
|
"slogan": ["Zeit für dich", "Bringt dich vorwärts", "Wir bewegen das Bahnland Bayern", "Ey, i muas da ehrlich sogen i wünimma mit dir zaum sein, weil i a eif agrod gmerkt hob das i nix ernstes grod will und es liegt wirkli ned an dir oba es passt halt ned sound du müsstest holt a nü des abhoin wos du bei mir vergessen host"]
|
||||||
},
|
},
|
||||||
"NS": {
|
"fallback": {
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Logo_NS.svg/960px-Logo_NS.svg.png",
|
"logo": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQCVeC5E0mNNBKyQftQaFMzxIVkbDvEnSzWv07h_c8PdA&s=10",
|
||||||
"color": 0X00337F,
|
"color": 0xFFFFFF
|
||||||
"slogan": ["Goed op weg", "Welkom in de trein van morgen", "Veilig, Vlug, Voordelig", "we haben een serious probleem", "Neuken in de keuken"]
|
|
||||||
},
|
|
||||||
"eurobahn": {
|
|
||||||
"logo": "https://cdn.discordapp.com/attachments/1383843132906537023/1528405390260179146/Eurpnajm.png?ex=6a5e2df1&is=6a5cdc71&hm=d73fc3458f59f8942a5a0f51311308b12567b35b55b64aab0726bf6be92b3b0e&animated=true",
|
|
||||||
"color": 0x005a9b,
|
|
||||||
},
|
|
||||||
"Arverio Bayern": {
|
|
||||||
"logo": "https://cdn.discordapp.com/attachments/1383843132906537023/1528006572805062776/Arverio_Avi_Bayern_blau_RGB.png?ex=6a5cba83&is=6a5b6903&hm=5781ae6c372c92ab4f52409c6fc91e5014ac34ccf24db665ade052e8135bdde0&animated=true", # alternative nötig!
|
|
||||||
"color": 0x0083BE
|
|
||||||
},
|
|
||||||
"Abellio Rail Mitteldeutschland GmbH": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Abellio_logo.svg/1920px-Abellio_logo.svg.png",
|
|
||||||
"color": 0xD7002E
|
|
||||||
},
|
|
||||||
"SBB": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/23/SBB_logo_simplified.svg/960px-SBB_logo_simplified.svg.png",
|
|
||||||
"color": 0xEB0000
|
|
||||||
},
|
|
||||||
"Nordbahn Eisenbahngesellschaft": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/b/b5/Logo_Nordbahn_NAH.SH_Blau_positiv_final.png",
|
|
||||||
"color": 0x1A2848
|
|
||||||
},
|
|
||||||
"Erfurter Bahn GmbH": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Erfurter_Bahn_logo.svg/960px-Erfurter_Bahn_logo.svg.png",
|
|
||||||
"color": 0x009133
|
|
||||||
},
|
|
||||||
"S-Bahn Hannover (Transdev)": {
|
|
||||||
"logo": "https://cdn.discordapp.com/attachments/1526274436523888732/1528470315234234520/sbahn.png?ex=6a5e6a68&is=6a5d18e8&hm=c9f630c6f0c646eafeccef9b4d591ce8fcf8856b2b45f14e72fbf25e32edb017&animated=true",
|
|
||||||
"color": 0x1A4389
|
|
||||||
},
|
|
||||||
"Regionalverkehre Start Deutschland GmbH (Start Mitteldeutschland)": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Logo_der_Regionalverkehre_Start_Deutschland.svg/960px-Logo_der_Regionalverkehre_Start_Deutschland.svg.png",
|
|
||||||
"color": 0x61A731
|
|
||||||
},
|
|
||||||
"Westbahn Management GmbH": {
|
|
||||||
"logo": "https://corporate.westbahn.at/uploads/Logos/westbahn2025-logo-signet-small.png",
|
|
||||||
"color": 0x1D4A83
|
|
||||||
},
|
|
||||||
"European Sleeper": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/European_Sleeper_Logo.svg/960px-European_Sleeper_Logo.svg.png",
|
|
||||||
"color": 0xEB4A27
|
|
||||||
},
|
|
||||||
"GoVolta": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/GoVolta_logo.svg/1280px-GoVolta_logo.svg.png",
|
|
||||||
"color": 0x0B70F6
|
|
||||||
},
|
|
||||||
"SNCF": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/en/thumb/f/f4/Sncf-logo.svg/960px-Sncf-logo.svg.png",
|
|
||||||
"color": 0x812B6D
|
|
||||||
},
|
|
||||||
"Agilis": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a3/Agilis_Logo.svg/960px-Agilis_Logo.svg.png",
|
|
||||||
"color": 0xCAE15B
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"GVB": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/BSicon_LOGO_GVB.svg/960px-BSicon_LOGO_GVB.svg.png",
|
|
||||||
"color": 0x2B62AF
|
|
||||||
},
|
|
||||||
"Berliner Verkehrsbetriebe": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/BVG_Logo_07.2021.svg/960px-BVG_Logo_07.2021.svg.png",
|
|
||||||
"color": 0xEFD13C
|
|
||||||
},
|
|
||||||
"Flixbus": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Flixbus_201x_logo.svg/1280px-Flixbus_201x_logo.svg.png",
|
|
||||||
"color": 0x8CD541
|
|
||||||
},
|
|
||||||
"Tallink Grupp AS": {
|
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Tallink_logo.svg/330px-Tallink_logo.svg.png",
|
|
||||||
"color": 0x225197
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
OPERATOR_ALIASES = {
|
OPERATOR_ALIASES = {
|
||||||
"Arverio Bayern GmbH": OPERATORS["Arverio Bayern"],
|
"Arverio Bayern GmbH": OPERATORS["Arverio Bayern"],
|
||||||
"DB Regio AG Baden-Württemberg": OPERATORS["db_bawü"],
|
"DB Regio AG Baden-Württemberg": OPERATORS["db_bawü"],
|
||||||
@@ -118,6 +165,7 @@ OPERATOR_ALIASES = {
|
|||||||
"Bayerische Regiobahn": OPERATORS["db_bayern"],
|
"Bayerische Regiobahn": OPERATORS["db_bayern"],
|
||||||
"DB Regio AG Bayern": OPERATORS["db_bayern"],
|
"DB Regio AG Bayern": OPERATORS["db_bayern"],
|
||||||
"DB Fernverkehr AG": OPERATORS["db_allgemein"],
|
"DB Fernverkehr AG": OPERATORS["db_allgemein"],
|
||||||
|
"DB Regio AG": OPERATORS["db_allgemein"],
|
||||||
"DB Regio AG NRW": OPERATORS["db_allgemein"],
|
"DB Regio AG NRW": OPERATORS["db_allgemein"],
|
||||||
"DB Regio AG Südost": OPERATORS["db_allgemein"],
|
"DB Regio AG Südost": OPERATORS["db_allgemein"],
|
||||||
"DB Regio AG Nordost": OPERATORS["db_allgemein"],
|
"DB Regio AG Nordost": OPERATORS["db_allgemein"],
|
||||||
@@ -128,7 +176,10 @@ OPERATOR_ALIASES = {
|
|||||||
"Schweizerische Bundesbahnen SBB": OPERATORS["SBB"],
|
"Schweizerische Bundesbahnen SBB": OPERATORS["SBB"],
|
||||||
"Dänische Staatsbahnen": OPERATORS["DSB"],
|
"Dänische Staatsbahnen": OPERATORS["DSB"],
|
||||||
"VR": OPERATORS["Vr"],
|
"VR": OPERATORS["Vr"],
|
||||||
"FlixBus-gb": OPERATORS["Flixbus"]
|
"FlixBus-gb": OPERATORS["Flixbus"],
|
||||||
|
"FlixBus-eu": OPERATORS["Flixbus"],
|
||||||
|
"S-Bahn Berlin GmbH": OPERATORS["S-Bahn"],
|
||||||
|
"DB Regio AG S-Bahn München": OPERATORS["S-Bahn"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -4,4 +4,6 @@ from src.dc.embeds import build_info_embed
|
|||||||
def setup_commands(bot: discord.Bot):
|
def setup_commands(bot: discord.Bot):
|
||||||
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
|
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
|
||||||
async def info(ctx):
|
async def info(ctx):
|
||||||
await ctx.respond(embed=build_info_embed())
|
await ctx.defer()
|
||||||
|
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
|
||||||
|
await ctx.respond(file=image, embed=build_info_embed())
|
||||||
+9
-6
@@ -4,12 +4,13 @@ import random
|
|||||||
from src.utils import get_operator_metadata, get_next_station, format_via_list, format_stop_list
|
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
|
from src.dc.helpers import format_timestamp_to_dc
|
||||||
from src.lang.locales import lang
|
from src.lang.locales import lang
|
||||||
|
from src.config import config
|
||||||
|
|
||||||
lang_embed = lang.embeds
|
lang_embed = lang.embeds
|
||||||
|
|
||||||
def build_embed_footer(mode: str, slogans):
|
def build_embed_footer(slogans):
|
||||||
footer_notice = f"{lang.embeds.footer.notice()} • mode: {mode}"
|
footer_notice = f"https://transitous.org/sources/"
|
||||||
icon = "https://raw.githubusercontent.com/kaaninchen/Gleiswechsel/refs/heads/main/src/data/assets/transitous-logo.png"
|
icon = "https://api.transitous.org/favicon.png"
|
||||||
|
|
||||||
if slogans is not None:
|
if slogans is not None:
|
||||||
footer_text = f"{random.choice(slogans)} • {footer_notice}"
|
footer_text = f"{random.choice(slogans)} • {footer_notice}"
|
||||||
@@ -32,7 +33,7 @@ def build_info_embed() -> discord.Embed:
|
|||||||
arrival = format_timestamp_to_dc(trip["arrival"])
|
arrival = format_timestamp_to_dc(trip["arrival"])
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title = trip["long_name"],
|
title = trip["channel_name"],
|
||||||
description=lang_embed.info.description(),
|
description=lang_embed.info.description(),
|
||||||
color = metadata["color"]
|
color = metadata["color"]
|
||||||
)
|
)
|
||||||
@@ -55,12 +56,14 @@ def build_info_embed() -> discord.Embed:
|
|||||||
for field_name, field_value in route_fields:
|
for field_name, field_value in route_fields:
|
||||||
embed.add_field(name=field_name, value=field_value, inline=False)
|
embed.add_field(name=field_name, value=field_value, inline=False)
|
||||||
|
|
||||||
footer = build_embed_footer(trip["mode"], metadata["slogans"])
|
footer = build_embed_footer(metadata["slogans"])
|
||||||
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
||||||
|
|
||||||
embed.set_author(name=agency)
|
embed.set_author(name=agency)
|
||||||
embed.set_thumbnail(url=metadata["logo"])
|
embed.set_thumbnail(url=metadata["logo"])
|
||||||
|
|
||||||
|
if config.announcements.map:
|
||||||
|
embed.set_image(url="attachment://ride.png")
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
def build_announcement_embed(msg):
|
def build_announcement_embed(msg):
|
||||||
@@ -77,7 +80,7 @@ def build_announcement_embed(msg):
|
|||||||
embed.set_author(name=agency)
|
embed.set_author(name=agency)
|
||||||
embed.set_thumbnail(url=metadata["logo"])
|
embed.set_thumbnail(url=metadata["logo"])
|
||||||
|
|
||||||
footer = build_embed_footer(trip["mode"], metadata.get("slogan"))
|
footer = build_embed_footer(metadata.get("slogan"))
|
||||||
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
|
||||||
|
|
||||||
return embed
|
return embed
|
||||||
|
|||||||
+34
-24
@@ -1,9 +1,10 @@
|
|||||||
import discord
|
import discord
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from src.utils import logger, channel_formatting, choose_connection, get_sound_path, LOCAL_TZ, get_next_station
|
from src.utils import logger, channel_formatting, choose_connection, get_sound_path, LOCAL_TZ, get_next_station, generate_static_map
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.lang.locales import lang
|
from src.lang.locales import lang
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = Fals
|
|||||||
_scheduled_task.cancel()
|
_scheduled_task.cancel()
|
||||||
|
|
||||||
attempt = 0
|
attempt = 0
|
||||||
max_attempt = 15
|
max_attempt = 30
|
||||||
trip = choose_connection()
|
trip = choose_connection()
|
||||||
while trip is None and attempt < max_attempt:
|
while trip is None and attempt < max_attempt:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
@@ -26,54 +27,57 @@ async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = Fals
|
|||||||
if trip is None:
|
if trip is None:
|
||||||
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
|
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if config.announcements.map:
|
||||||
|
generate_static_map(trip["stops"], trip["mode"], trip["agency"], trip["route_color"])
|
||||||
|
|
||||||
arrival = trip["arrival"]
|
arrival = trip["arrival"]
|
||||||
long_name = trip["long_name"]
|
channel_name = trip["channel_name"]
|
||||||
|
|
||||||
mode = trip["mode"]
|
mode = trip["mode"]
|
||||||
|
|
||||||
print("-----------------")
|
print("-----------------")
|
||||||
logger(f"Transfer: {long_name}; Arrival: {arrival}")
|
logger(f"Transfer: {channel_name}; Arrival: {arrival}")
|
||||||
logger(f"Agency: {trip["agency"]}, mode: {mode}")
|
logger(f"Agency: {trip["agency"]}, mode: {mode}")
|
||||||
logger(f"Trying to change the channels name. If nothing happens, then the bot is in cooldown... (automatically resolves after up to 10min)")
|
logger(f"Trying to change the channels name. If nothing happens, then the bot is in cooldown... (automatically resolves after up to 10min)")
|
||||||
|
|
||||||
formatting = channel_formatting(mode)
|
formatting = channel_formatting(mode)
|
||||||
await voice_channel.edit(name=f"{formatting}{long_name}")
|
await voice_channel.edit(name=f"{formatting}{channel_name}")
|
||||||
await voice_channel.set_status(None)
|
await voice_channel.set_status(None)
|
||||||
start_next_stop_updates(bot, voice_channel)
|
start_next_stop_updates(voice_channel)
|
||||||
|
|
||||||
|
|
||||||
logger(f"Updated channel name!")
|
logger(f"Updated channel name!")
|
||||||
|
|
||||||
|
await bot.change_presence(activity=discord.Game(name=lang.rich_presence.arrival()))
|
||||||
await announcer("transfer", voice_channel)
|
await announcer("transfer", voice_channel)
|
||||||
|
|
||||||
|
|
||||||
_scheduled_task = asyncio.create_task(_schedule_next_transfer(bot, trip["arrival_dt"], voice_channel, trip["to"]))
|
_scheduled_task = asyncio.create_task(_schedule_next_transfer(bot, trip["arrival_dt"], voice_channel, trip["to"]))
|
||||||
|
|
||||||
async def announcer(announcement: str, voice_channel: discord.VoiceChannel, destination = None):
|
async def announcer(announcement: str, voice_channel: discord.VoiceChannel, destination = None):
|
||||||
from src.dc.embeds import build_info_embed, build_announcement_embed
|
from src.dc.embeds import build_info_embed, build_announcement_embed
|
||||||
announcements_enabled = config.announcements.enabled
|
announcements_enabled = config.announcements.text_announcements
|
||||||
voice_announcement_enabled = config.announcements.voice[0].enabled
|
|
||||||
|
|
||||||
if announcements_enabled:
|
if announcements_enabled:
|
||||||
|
image = None
|
||||||
if len(voice_channel.members) > 0:
|
if len(voice_channel.members) > 0:
|
||||||
match announcement:
|
match announcement:
|
||||||
case "end_of_connection":
|
case "end_of_connection":
|
||||||
if voice_announcement_enabled:
|
embed = build_announcement_embed(lang.embeds.announcement.end_of_connection.message())
|
||||||
announcement_status = await voice_announcer(destination, voice_channel, "end_stations")
|
|
||||||
if announcement_status:
|
|
||||||
return
|
|
||||||
embed = build_announcement_embed(lang.embeds.announcement.end_of_connection.message())
|
|
||||||
case "transfer":
|
case "transfer":
|
||||||
embed = build_info_embed()
|
embed = build_info_embed()
|
||||||
|
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
|
||||||
case _:
|
case _:
|
||||||
logger(f"Unknown announcement: {announcement}")
|
logger(f"Unknown announcement: {announcement}")
|
||||||
embed = None
|
embed = None
|
||||||
|
|
||||||
if embed:
|
if embed:
|
||||||
await voice_channel.send(embed=embed)
|
if image and config.announcements.map:
|
||||||
|
await voice_channel.send(file=image, embed=embed)
|
||||||
|
else:
|
||||||
|
await voice_channel.send(embed=embed)
|
||||||
|
|
||||||
async def voice_announcer(destination: str, voice_channel: discord.VoiceChannel, type_announcement: str) -> bool:
|
async def voice_announcer(station: str, voice_channel: discord.VoiceChannel) -> bool:
|
||||||
sound_path = get_sound_path(destination=destination, type_announcement=type_announcement)
|
sound_path = get_sound_path(station=station)
|
||||||
if sound_path is None:
|
if sound_path is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -108,7 +112,7 @@ async def _schedule_next_transfer(bot: discord.Bot, arrival_dt: datetime, voice_
|
|||||||
|
|
||||||
if wait_seconds > 0:
|
if wait_seconds > 0:
|
||||||
remaining = str(timedelta(seconds=wait_seconds))
|
remaining = str(timedelta(seconds=wait_seconds))
|
||||||
logger(f"Next transfer in {remaining.split('.')[0]} ({arrival_dt.strftime('%H:%M')} Uhr)")
|
logger(f"Next transfer in {remaining.split('.')[0]} ({arrival_dt.strftime('%H:%M')})")
|
||||||
|
|
||||||
if wait_seconds > announcement_countdown:
|
if wait_seconds > announcement_countdown:
|
||||||
wait_until_end_announcement = wait_seconds - announcement_countdown
|
wait_until_end_announcement = wait_seconds - announcement_countdown
|
||||||
@@ -121,7 +125,7 @@ async def _schedule_next_transfer(bot: discord.Bot, arrival_dt: datetime, voice_
|
|||||||
logger("Train arrived, searching for a new connection....")
|
logger("Train arrived, searching for a new connection....")
|
||||||
await rename_vc(bot, voice_channel, from_scheduler=True)
|
await rename_vc(bot, voice_channel, from_scheduler=True)
|
||||||
|
|
||||||
async def _update_next_loop(bot: discord.Bot, voice_channel: discord.VoiceChannel):
|
async def _update_next_loop(voice_channel: discord.VoiceChannel):
|
||||||
global trip
|
global trip
|
||||||
try:
|
try:
|
||||||
if trip is None:
|
if trip is None:
|
||||||
@@ -143,9 +147,15 @@ async def _update_next_loop(bot: discord.Bot, voice_channel: discord.VoiceChanne
|
|||||||
next_stop_str = next_stop["name"]
|
next_stop_str = next_stop["name"]
|
||||||
|
|
||||||
status_text = f"{lang.embeds.info.next_stop()}: {next_stop_str}"
|
status_text = f"{lang.embeds.info.next_stop()}: {next_stop_str}"
|
||||||
await voice_channel.set_status(status_text, reason="Next stop status")
|
|
||||||
|
try:
|
||||||
|
await voice_channel.set_status(status_text, reason="Next stop status")
|
||||||
|
except aiohttp.ClientConnectorDNSError as e:
|
||||||
|
logger(F"Failed to set the channel status (network error?): {e}", "error")
|
||||||
|
|
||||||
await voice_announcer(next_stop_str, voice_channel, type_announcement="stops")
|
if config.announcements.voice_announcements:
|
||||||
|
if len(voice_channel.members) > 0:
|
||||||
|
await voice_announcer(next_stop_str, voice_channel)
|
||||||
|
|
||||||
wait_seconds = (next_stop["arrival"] - datetime.now(LOCAL_TZ)).total_seconds()
|
wait_seconds = (next_stop["arrival"] - datetime.now(LOCAL_TZ)).total_seconds()
|
||||||
if wait_seconds > 0:
|
if wait_seconds > 0:
|
||||||
@@ -154,10 +164,10 @@ async def _update_next_loop(bot: discord.Bot, voice_channel: discord.VoiceChanne
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def start_next_stop_updates(bot: discord.bot, voice_channel: discord.VoiceChannel):
|
def start_next_stop_updates(voice_channel: discord.VoiceChannel):
|
||||||
global _next_stop_task
|
global _next_stop_task
|
||||||
|
|
||||||
if _next_stop_task is not None and not _next_stop_task.done():
|
if _next_stop_task is not None and not _next_stop_task.done():
|
||||||
_next_stop_task.cancel()
|
_next_stop_task.cancel()
|
||||||
|
|
||||||
_next_stop_task = asyncio.create_task(_update_next_loop(bot, voice_channel))
|
_next_stop_task = asyncio.create_task(_update_next_loop(voice_channel))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# generated by datamodel-codegen:
|
# generated by datamodel-codegen:
|
||||||
# filename: de.yaml
|
# filename: en.yaml
|
||||||
# timestamp: 2026-08-17T18:44:37+00:00
|
# timestamp: 2026-08-23T14:08:25+00:00
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -16,11 +16,7 @@ class LongName:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Channel:
|
class Channel:
|
||||||
long_name: LongName
|
long_name: LongName
|
||||||
|
short_name: str
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Footer:
|
|
||||||
notice: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -44,12 +40,17 @@ class Announcement:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Embeds:
|
class Embeds:
|
||||||
footer: Footer
|
|
||||||
info: Info
|
info: Info
|
||||||
announcement: Announcement
|
announcement: Announcement
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RichPresence:
|
||||||
|
arrival: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Model:
|
class Model:
|
||||||
channel: Channel
|
channel: Channel
|
||||||
embeds: Embeds
|
embeds: Embeds
|
||||||
|
rich_presence: RichPresence
|
||||||
|
|||||||
+84
-31
@@ -1,8 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
import random
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import staticmaps
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -30,16 +30,17 @@ def choose_connection() -> dict | None:
|
|||||||
def validate_connection(start_time: str, end_time: str, departure_time_iso: str) -> bool:
|
def validate_connection(start_time: str, end_time: str, departure_time_iso: str) -> bool:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
||||||
|
start_local = start_dt.astimezone(LOCAL_TZ).strftime('%H:%M')
|
||||||
|
|
||||||
end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
||||||
if end_dt < now:
|
if end_dt < now:
|
||||||
logger(f"Connection is from the past: {start_dt}", "error")
|
logger(f"Connection is from the past: {start_local}", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
max_wait_time = config.connections.max_wait_time
|
max_wait_time = config.connections.max_wait_time
|
||||||
if max_wait_time:
|
if max_wait_time:
|
||||||
if start_dt > now + timedelta(hours=max_wait_time):
|
if start_dt > now + timedelta(hours=max_wait_time):
|
||||||
logger(f"Connection is way too far in the future: {start_dt} (max_wait_time: {max_wait_time}h)", "error")
|
logger(f"Connection is way too far in the future: {start_local} (max_wait_time: {max_wait_time}h), retrying...", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
departure_time_iso_dt = datetime.fromisoformat(departure_time_iso.replace("Z", "+00:00"))
|
departure_time_iso_dt = datetime.fromisoformat(departure_time_iso.replace("Z", "+00:00"))
|
||||||
@@ -49,14 +50,14 @@ def validate_connection(start_time: str, end_time: str, departure_time_iso: str)
|
|||||||
min_duration = config.connections.min_duration
|
min_duration = config.connections.min_duration
|
||||||
min_duration_seconds = min_duration * 60
|
min_duration_seconds = min_duration * 60
|
||||||
if trip_duration < min_duration_seconds:
|
if trip_duration < min_duration_seconds:
|
||||||
logger(f"Connection is with {trip_duration_minutes} minutes too short (configured to {min_duration} minutes or more)", "error")
|
logger(f"Connection is with {trip_duration_minutes} minutes too short (configured to {min_duration} minutes or more), retrying...", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
max_duration = config.connections.max_duration
|
max_duration = config.connections.max_duration
|
||||||
if max_duration:
|
if max_duration:
|
||||||
max_duration_seconds = max_duration * 60
|
max_duration_seconds = max_duration * 60
|
||||||
if max_duration_seconds < trip_duration:
|
if max_duration_seconds < trip_duration:
|
||||||
logger(f"Connection is with {trip_duration_minutes} too long (configured to {max_duration} minutes at most)", "error")
|
logger(f"Connection is with {trip_duration_minutes} too long (configured to {max_duration} minutes at most), retrying...", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -123,31 +124,14 @@ def get_operator_metadata(agency: str, route_color: str, mode: str) -> dict:
|
|||||||
"slogans": slogans
|
"slogans": slogans
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_sound_path(destination, type_announcement: str) -> str | None:
|
def get_sound_path(station: str) -> str | None:
|
||||||
if type_announcement == "end_stations":
|
announcement_dir = Path("src/data/announcements")
|
||||||
voice_stations = config.announcements.voice[0].end_stations
|
|
||||||
elif type_announcement == "stops":
|
|
||||||
voice_stations = config.announcements.voice[0].stops
|
|
||||||
|
|
||||||
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}"
|
for file in announcement_dir.iterdir():
|
||||||
if Path(sound_path).is_file() is False:
|
if file.is_file():
|
||||||
logger(f"Couldn't find {sound_path}", "error")
|
if station.lower() in file.stem.lower():
|
||||||
return None
|
sound_file = file.resolve()
|
||||||
|
return sound_file
|
||||||
return sound_path
|
|
||||||
|
|
||||||
def get_next_station(stops: dict, train_from: str) -> dict | None:
|
def get_next_station(stops: dict, train_from: str) -> dict | None:
|
||||||
now = datetime.now(LOCAL_TZ)
|
now = datetime.now(LOCAL_TZ)
|
||||||
@@ -201,10 +185,10 @@ def format_stop_list(stops: dict, next_stop: str | None) -> list[tuple[str, str]
|
|||||||
for name, info in stops.items():
|
for name, info in stops.items():
|
||||||
if name == next_stop:
|
if name == next_stop:
|
||||||
stop_arrival = info["arrival"]
|
stop_arrival = info["arrival"]
|
||||||
line = f"• __{name}__ ({stop_arrival.strftime("%H:%M")} Uhr)"
|
line = f"• __{name}__ ({stop_arrival.strftime("%H:%M")})"
|
||||||
else:
|
else:
|
||||||
stop_arrival = info["arrival"]
|
stop_arrival = info["arrival"]
|
||||||
line = f"• {name} ({stop_arrival.strftime("%H:%M")} Uhr)"
|
line = f"• {name} ({stop_arrival.strftime("%H:%M")})"
|
||||||
|
|
||||||
if field_length + len(line) + 1 > 1024:
|
if field_length + len(line) + 1 > 1024:
|
||||||
route_page_name = "Route" if part == 1 else "Route (Fortsetzung)"
|
route_page_name = "Route" if part == 1 else "Route (Fortsetzung)"
|
||||||
@@ -219,3 +203,72 @@ def format_stop_list(stops: dict, next_stop: str | None) -> list[tuple[str, str]
|
|||||||
fields.append((route_page_name, "\n".join(field_lines)))
|
fields.append((route_page_name, "\n".join(field_lines)))
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
def generate_static_map(stops: dict, mode: str, operator: str, route_color: str):
|
||||||
|
logger("Generating a new route map..")
|
||||||
|
operator_metadata = get_operator_metadata(operator, route_color, mode)
|
||||||
|
operator_color = operator_metadata["color"]
|
||||||
|
if operator_color == 0xFFFFFF:
|
||||||
|
stop_color = staticmaps.Color(105, 105, 105, 255)
|
||||||
|
else:
|
||||||
|
stop_color = staticmaps.parse_color(f"#{operator_color:06x}")
|
||||||
|
|
||||||
|
white = staticmaps.Color(255, 255, 255, 255)
|
||||||
|
|
||||||
|
context = staticmaps.Context()
|
||||||
|
|
||||||
|
context.set_tile_provider(staticmaps.tile_provider_OSM)
|
||||||
|
|
||||||
|
stop_coords = [
|
||||||
|
staticmaps.create_latlng(
|
||||||
|
stop["lat"],
|
||||||
|
stop["lon"],
|
||||||
|
)
|
||||||
|
for stop in stops.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
context.add_object(
|
||||||
|
staticmaps.Line(
|
||||||
|
stop_coords,
|
||||||
|
color=white,
|
||||||
|
width=15,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add_object(
|
||||||
|
staticmaps.Line(
|
||||||
|
stop_coords,
|
||||||
|
color=stop_color,
|
||||||
|
width=8,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
last_index = len(stop_coords) - 1
|
||||||
|
for i, point in enumerate(stop_coords):
|
||||||
|
|
||||||
|
if i == 0 or i == last_index:
|
||||||
|
context.add_object(
|
||||||
|
staticmaps.Circle(
|
||||||
|
point,
|
||||||
|
radius_km=0.1,
|
||||||
|
fill_color=white,
|
||||||
|
color=stop_color,
|
||||||
|
width=12
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if i == last_index:
|
||||||
|
context.add_object(
|
||||||
|
staticmaps.Marker(
|
||||||
|
point,
|
||||||
|
color=stop_color,
|
||||||
|
size=12
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
image = context.render_cairo(400, 300)
|
||||||
|
image.write_to_png("src/data/assets/current_map.png")
|
||||||
|
except RuntimeError:
|
||||||
|
logger(f"You don't have cairo installed! Because of that I can only give you a low-res png of the route...")
|
||||||
|
image = context.render_pillow(400, 300)
|
||||||
|
image.save("src/data/assets/current_map.png")
|
||||||
|
logger("Map generated!")
|
||||||
|
|||||||
Reference in New Issue
Block a user