mirror of
https://github.com/kaaninchen/Gleiswechsel.git
synced 2026-09-17 16:52:47 +00:00
Compare commits
7
Commits
db43ee4279
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f65aa3cfa0 | ||
|
|
e9708d435e | ||
|
|
d50cf0277b | ||
|
|
dde0c443de | ||
|
|
93b9712394 | ||
|
|
21a279839d | ||
|
|
1c006731a4 |
@@ -4,7 +4,7 @@ Gleiswechsel [ˈɡlaɪsvɛksəl] (german for "platform change") is a selfhostabl
|
||||

|
||||
|
||||
## Features
|
||||
- [International support](https://transitous.org/sources/) for countries as well as 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
|
||||
- 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
|
||||
@@ -121,6 +121,30 @@ 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.
|
||||
|
||||
##### 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
|
||||
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
|
||||
@@ -155,6 +179,12 @@ To reduce spam, the bot will only send announcements if someone is in the voice
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
You don't have to restart the bot after placing new audio files in the folder.
|
||||
|
||||
#### `map:`
|
||||
The bot can generate a map of your route in the `/info` embed. Set `map` to `true` to enable.
|
||||
|
||||
#### 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. 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
|
||||
|
||||
+6
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"discord": {
|
||||
"token": "",
|
||||
"server": ,
|
||||
"vc": ,
|
||||
"server": "",
|
||||
"vc": "",
|
||||
"lang": "de",
|
||||
"formatting": "┇",
|
||||
"emojis": true
|
||||
@@ -14,6 +14,7 @@
|
||||
"Helsinki"
|
||||
],
|
||||
"IDs": [],
|
||||
"priority": {},
|
||||
"blacklist": [
|
||||
"OTHER",
|
||||
"RIDE_SHARING"
|
||||
@@ -25,9 +26,10 @@
|
||||
},
|
||||
"announcements": {
|
||||
"text_announcements": true,
|
||||
"voice_announcements": false
|
||||
"voice_announcements": false,
|
||||
"map": true
|
||||
},
|
||||
"http": {
|
||||
"user_agent": "Gleiswechsel-Discord-Bot/1.0 (https://github.com/kaaninchen/Gleiswechsel)"
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-47
@@ -142,10 +142,12 @@ def get_random_stop_id() -> str | None:
|
||||
return None
|
||||
|
||||
stop_ids_list = list(stop_ids.keys())
|
||||
chosen_stop_id = random.choice(stop_ids_list)
|
||||
if len(stop_ids_list) > 1:
|
||||
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")
|
||||
chosen_stop_id, chosen_station = random.choice(list(stop_ids.items()))
|
||||
logger(f"Selected station: {stop_ids[chosen_stop_id]}")
|
||||
|
||||
return chosen_stop_id
|
||||
|
||||
def get_random_connection(stop_id: str) -> str | None:
|
||||
@@ -153,60 +155,46 @@ def get_random_connection(stop_id: str) -> str | 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 = []
|
||||
params = {"stopId": stop_id, "n": 50, "time": now}
|
||||
|
||||
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")
|
||||
return None
|
||||
|
||||
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
|
||||
candidates = []
|
||||
for entry in data.get("stopTimes", []):
|
||||
if entry["mode"] in blacklist:
|
||||
continue
|
||||
candidates.append(entry)
|
||||
|
||||
stop_times = data.get("stopTimes", [])
|
||||
all_stop_times.extend(stop_times)
|
||||
if not candidates:
|
||||
logger("Couldn't find any connections", "error")
|
||||
return None
|
||||
|
||||
for entry in stop_times:
|
||||
trip_id = entry["tripId"]
|
||||
if entry["mode"] in blacklist:
|
||||
continue
|
||||
else:
|
||||
trip_ids.append(entry["tripId"])
|
||||
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
|
||||
|
||||
if len(trip_ids) >= min_results:
|
||||
break
|
||||
best_candidates = []
|
||||
for entry in candidates:
|
||||
priority = config.connections.priority.get(entry["mode"], 99)
|
||||
if priority == best_priority:
|
||||
best_candidates.append(entry)
|
||||
|
||||
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
|
||||
chosen = random.choice(best_candidates)
|
||||
else:
|
||||
chosen = random.choice(candidates)
|
||||
|
||||
return {
|
||||
"trip_id": trip_id,
|
||||
"from_station": from_station
|
||||
"trip_id": chosen["tripId"],
|
||||
"from_station": chosen.get("place", {}).get("name"),
|
||||
}
|
||||
|
||||
def get_trip_details(random_connection: dict | None) -> dict | None:
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
@@ -15,6 +15,7 @@ class DiscordConfig:
|
||||
class ConnectionsConfig:
|
||||
stations: list[str]
|
||||
IDs: list[str]
|
||||
priority: dict
|
||||
blacklist: list[str]
|
||||
min_duration: int
|
||||
timezone: str
|
||||
@@ -25,6 +26,7 @@ class ConnectionsConfig:
|
||||
class AnnouncementConfig:
|
||||
text_announcements: bool
|
||||
voice_announcements: bool
|
||||
map: bool
|
||||
|
||||
@dataclass
|
||||
class HttpConfig:
|
||||
|
||||
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
|
||||
@@ -74,7 +74,7 @@ OPERATORS = {
|
||||
"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"]
|
||||
"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",
|
||||
@@ -165,6 +165,7 @@ OPERATOR_ALIASES = {
|
||||
"Bayerische Regiobahn": OPERATORS["db_bayern"],
|
||||
"DB Regio AG Bayern": OPERATORS["db_bayern"],
|
||||
"DB Fernverkehr AG": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG NRW": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Südost": OPERATORS["db_allgemein"],
|
||||
"DB Regio AG Nordost": OPERATORS["db_allgemein"],
|
||||
@@ -177,7 +178,8 @@ OPERATOR_ALIASES = {
|
||||
"VR": OPERATORS["Vr"],
|
||||
"FlixBus-gb": OPERATORS["Flixbus"],
|
||||
"FlixBus-eu": OPERATORS["Flixbus"],
|
||||
"S-Bahn Berlin GmbH": OPERATORS["S-Bahn"]
|
||||
"S-Bahn Berlin GmbH": OPERATORS["S-Bahn"],
|
||||
"DB Regio AG S-Bahn München": OPERATORS["S-Bahn"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
+5
-3
@@ -4,12 +4,13 @@ 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
|
||||
from src.lang.locales import lang
|
||||
from src.config import config
|
||||
|
||||
lang_embed = lang.embeds
|
||||
|
||||
def build_embed_footer(slogans):
|
||||
footer_notice = f"https://transitous.org/sources/ • Maps (C) CARTO (C) OpenStreetMap.org contributors"
|
||||
icon = "https://raw.githubusercontent.com/kaaninchen/Gleiswechsel/refs/heads/main/src/data/assets/transitous-logo.png"
|
||||
footer_notice = f"https://transitous.org/sources/"
|
||||
icon = "https://api.transitous.org/favicon.png"
|
||||
|
||||
if slogans is not None:
|
||||
footer_text = f"{random.choice(slogans)} • {footer_notice}"
|
||||
@@ -61,7 +62,8 @@ def build_info_embed() -> discord.Embed:
|
||||
embed.set_author(name=agency)
|
||||
embed.set_thumbnail(url=metadata["logo"])
|
||||
|
||||
embed.set_image(url="attachment://ride.png")
|
||||
if config.announcements.map:
|
||||
embed.set_image(url="attachment://ride.png")
|
||||
return embed
|
||||
|
||||
def build_announcement_embed(msg):
|
||||
|
||||
+4
-3
@@ -18,7 +18,7 @@ async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = Fals
|
||||
_scheduled_task.cancel()
|
||||
|
||||
attempt = 0
|
||||
max_attempt = 15
|
||||
max_attempt = 30
|
||||
trip = choose_connection()
|
||||
while trip is None and attempt < max_attempt:
|
||||
attempt += 1
|
||||
@@ -28,7 +28,8 @@ async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = Fals
|
||||
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
|
||||
return False
|
||||
|
||||
generate_static_map(trip["stops"], trip["mode"], trip["agency"], trip["route_color"])
|
||||
if config.announcements.map:
|
||||
generate_static_map(trip["stops"], trip["mode"], trip["agency"], trip["route_color"])
|
||||
|
||||
arrival = trip["arrival"]
|
||||
channel_name = trip["channel_name"]
|
||||
@@ -70,7 +71,7 @@ async def announcer(announcement: str, voice_channel: discord.VoiceChannel, dest
|
||||
embed = None
|
||||
|
||||
if embed:
|
||||
if image:
|
||||
if image and config.announcements.map:
|
||||
await voice_channel.send(file=image, embed=embed)
|
||||
else:
|
||||
await voice_channel.send(embed=embed)
|
||||
|
||||
+2
-49
@@ -217,22 +217,7 @@ def generate_static_map(stops: dict, mode: str, operator: str, route_color: str)
|
||||
|
||||
context = staticmaps.Context()
|
||||
|
||||
context.set_tile_provider(
|
||||
staticmaps.TileProvider(
|
||||
"carto-voyager",
|
||||
url_pattern=(
|
||||
"https://$s.basemaps.cartocdn.com/"
|
||||
"rastertiles/voyager/$z/$x/$y.png"
|
||||
),
|
||||
shards=["a", "b", "c", "d"],
|
||||
attribution="",
|
||||
)
|
||||
# It is against the law (and against your morals...) to not give
|
||||
# OSM and carto credits for their great work
|
||||
# I only removed the attribution text because I really dislike the
|
||||
# ugly white box that py-staticmaps adds. Credits are still visible in the
|
||||
# embeds footer
|
||||
)
|
||||
context.set_tile_provider(staticmaps.tile_provider_OSM)
|
||||
|
||||
stop_coords = [
|
||||
staticmaps.create_latlng(
|
||||
@@ -246,7 +231,7 @@ def generate_static_map(stops: dict, mode: str, operator: str, route_color: str)
|
||||
staticmaps.Line(
|
||||
stop_coords,
|
||||
color=white,
|
||||
width=20,
|
||||
width=15,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -279,38 +264,6 @@ def generate_static_map(stops: dict, mode: str, operator: str, route_color: str)
|
||||
size=12
|
||||
)
|
||||
)
|
||||
|
||||
lats = [c.lat().degrees for c in stop_coords]
|
||||
lons = [c.lng().degrees for c in stop_coords]
|
||||
lat_span = max(lats) - min(lats)
|
||||
lon_span = max(lons) - min(lons)
|
||||
|
||||
MIN_SPAN = 0.01
|
||||
|
||||
if lat_span < MIN_SPAN or lon_span < MIN_SPAN:
|
||||
center_lat = (max(lats) + min(lats)) / 2
|
||||
center_lon = (max(lons) + min(lons)) / 2
|
||||
pad = MIN_SPAN / 2
|
||||
|
||||
context.add_object(
|
||||
staticmaps.Circle(
|
||||
staticmaps.create_latlng(center_lat + pad, center_lon + pad),
|
||||
radius_km=0.01,
|
||||
fill_color=staticmaps.TRANSPARENT,
|
||||
color=staticmaps.TRANSPARENT,
|
||||
width=0,
|
||||
)
|
||||
)
|
||||
context.add_object(
|
||||
staticmaps.Circle(
|
||||
staticmaps.create_latlng(center_lat - pad, center_lon - pad),
|
||||
radius_km=0.01,
|
||||
fill_color=staticmaps.TRANSPARENT,
|
||||
color=staticmaps.TRANSPARENT,
|
||||
width=0,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
image = context.render_cairo(400, 300)
|
||||
image.write_to_png("src/data/assets/current_map.png")
|
||||
|
||||
Reference in New Issue
Block a user