mirror of
https://github.com/kaaninchen/Gleiswechsel.git
synced 2026-09-17 16:52:47 +00:00
Compare commits
4
Commits
cd2d968545
...
595e7160df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595e7160df | ||
|
|
ead53f2f75 | ||
|
|
ed7ae614cc | ||
|
|
714841df22 |
@@ -1,6 +1,7 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
src/data/announcements/*
|
||||
src/data/assets/current_map.png
|
||||
!src/data/announcements/.gitkeep
|
||||
config.json
|
||||
stations.json
|
||||
@@ -158,7 +158,7 @@ The bot can join the voice chat, play an audio file, and disconnect from the voi
|
||||
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.
|
||||
#### 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.
|
||||
|
||||
## Running
|
||||
After you've set everything up, you're ready to start the bot!
|
||||
@@ -186,5 +186,5 @@ Covered in [locales](#locales)
|
||||
### [announcements/](src/data/announcements/)
|
||||
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
|
||||
@@ -13,6 +13,7 @@ numpy==2.5.1
|
||||
pandas==3.0.3
|
||||
propcache==0.5.2
|
||||
py-cord==2.8.0
|
||||
py-staticmaps==0.5.0
|
||||
pyyaml==6.0.3
|
||||
pycparser==3.0
|
||||
pynacl==1.6.2
|
||||
|
||||
+15
-8
@@ -243,13 +243,13 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
||||
|
||||
train_name = get_train_name(display_name, mode) # used by lang
|
||||
if train_from == from_station:
|
||||
long_name = long_name_lang.train_from()
|
||||
train_name = long_name_lang.train_from()
|
||||
else:
|
||||
long_name = long_name_lang.train_via()
|
||||
|
||||
trip_details = {
|
||||
"long_name": long_name,
|
||||
"short_name": display_name,
|
||||
"short_name": train_name,
|
||||
"station": from_station,
|
||||
"from": train_from,
|
||||
"to": goes_to,
|
||||
@@ -266,7 +266,9 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
||||
train_from_importance = legs["from"]["importance"]
|
||||
trip_details["stops"][train_from] = {
|
||||
"arrival": departure_dt,
|
||||
"importance": train_from_importance
|
||||
"importance": train_from_importance,
|
||||
"lat": legs["from"]["lat"],
|
||||
"lon": legs["from"]["lon"]
|
||||
}
|
||||
|
||||
departure_time_iso = start_time
|
||||
@@ -276,22 +278,27 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
||||
stop_importance = stop["importance"]
|
||||
stop_details = {
|
||||
"arrival": stop_arrival_dt,
|
||||
"importance": stop_importance
|
||||
"importance": stop_importance,
|
||||
"lat": stop["lat"],
|
||||
"lon": stop["lon"]
|
||||
}
|
||||
trip_details["stops"][stop["name"]] = stop_details
|
||||
|
||||
if stop.get("name") == from_station:
|
||||
departure_time_iso = stop["departure"]
|
||||
trip_details["departure"] = parse_iso(departure_time_iso).strftime("%H:%M")
|
||||
departure_time = parse_iso(stop["departure"])
|
||||
trip_details["departure"] = departure_time.strftime("%H:%M")
|
||||
|
||||
train_to_importance = legs["to"]["importance"]
|
||||
trip_details["stops"][goes_to] = {
|
||||
"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)
|
||||
if not valid:
|
||||
return None
|
||||
|
||||
|
||||
return trip_details
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ channel:
|
||||
train_via: "{train_name} nach {goes_to} über {from_station}"
|
||||
embeds:
|
||||
footer:
|
||||
notice: "Daten großzügig bereitgestellt von https://transitous.org"
|
||||
notice: "Daten bereitgestellt von https://transitous.org • Maps (C) CARTO (C) OpenStreetMap.org contributors"
|
||||
info:
|
||||
description: "Abfahrt von {station} um {departure}. Ankunft um {arrival}"
|
||||
via: "Über"
|
||||
|
||||
@@ -4,7 +4,7 @@ channel:
|
||||
train_via: "{train_name} to {goes_to} via {from_station}"
|
||||
embeds:
|
||||
footer:
|
||||
notice: "Data provided by https://transitous.org"
|
||||
notice: "Data provided by https://transitous.org • Maps (C) CARTO (C) OpenStreetMap.org contributors"
|
||||
info:
|
||||
description: "Departure from {station} at {departure}. Arrival by {arrival}"
|
||||
via: "via"
|
||||
|
||||
@@ -29,7 +29,7 @@ OPERATORS = {
|
||||
"color": 0x88216F,
|
||||
"slogan": ["enno fährt bunt"]
|
||||
},
|
||||
"Erfurter Bahn GmbH": {
|
||||
"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."]
|
||||
@@ -92,10 +92,18 @@ OPERATORS = {
|
||||
"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
|
||||
},
|
||||
"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,
|
||||
|
||||
+3
-1
@@ -4,4 +4,6 @@ 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())
|
||||
await ctx.defer()
|
||||
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
|
||||
await ctx.respond(file=image, embed=build_info_embed())
|
||||
@@ -61,6 +61,7 @@ def build_info_embed() -> discord.Embed:
|
||||
embed.set_author(name=agency)
|
||||
embed.set_thumbnail(url=metadata["logo"])
|
||||
|
||||
embed.set_image(url="attachment://ride.png")
|
||||
return embed
|
||||
|
||||
def build_announcement_embed(msg):
|
||||
|
||||
+17
-6
@@ -3,7 +3,7 @@ import asyncio
|
||||
import random
|
||||
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.lang.locales import lang
|
||||
|
||||
@@ -26,18 +26,25 @@ async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = Fals
|
||||
if trip is None:
|
||||
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"])
|
||||
|
||||
arrival = trip["arrival"]
|
||||
long_name = trip["long_name"]
|
||||
|
||||
if len(trip["long_name"]) >= 100:
|
||||
channel_name = trip["short_name"]
|
||||
else:
|
||||
channel_name = trip["long_name"]
|
||||
|
||||
mode = trip["mode"]
|
||||
|
||||
print("-----------------")
|
||||
logger(f"Transfer: {long_name}; Arrival: {arrival}")
|
||||
logger(f"Transfer: {channel_name}; Arrival: {arrival}")
|
||||
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)")
|
||||
|
||||
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)
|
||||
start_next_stop_updates(voice_channel)
|
||||
|
||||
@@ -59,12 +66,16 @@ async def announcer(announcement: str, voice_channel: discord.VoiceChannel, dest
|
||||
embed = build_announcement_embed(lang.embeds.announcement.end_of_connection.message())
|
||||
case "transfer":
|
||||
embed = build_info_embed()
|
||||
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
|
||||
case _:
|
||||
logger(f"Unknown announcement: {announcement}")
|
||||
embed = None
|
||||
|
||||
if embed:
|
||||
await voice_channel.send(embed=embed)
|
||||
if image:
|
||||
await voice_channel.send(file=image, embed=embed)
|
||||
else:
|
||||
await voice_channel.send(embed=embed)
|
||||
|
||||
async def voice_announcer(station: str, voice_channel: discord.VoiceChannel) -> bool:
|
||||
sound_path = get_sound_path(station=station)
|
||||
@@ -145,7 +156,7 @@ async def _update_next_loop(voice_channel: discord.VoiceChannel):
|
||||
|
||||
wait_seconds = (next_stop["arrival"] - datetime.now(LOCAL_TZ)).total_seconds()
|
||||
if wait_seconds > 0:
|
||||
await asyncio.sleep(wait_seconds)
|
||||
await (wait_seconds)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
+127
-1
@@ -1,8 +1,8 @@
|
||||
import os
|
||||
import importlib
|
||||
import random
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import staticmaps
|
||||
from src.config import config
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -202,3 +202,129 @@ def format_stop_list(stops: dict, next_stop: str | None) -> list[tuple[str, str]
|
||||
fields.append((route_page_name, "\n".join(field_lines)))
|
||||
|
||||
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.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
|
||||
)
|
||||
|
||||
stop_coords = [
|
||||
staticmaps.create_latlng(
|
||||
stop["lat"],
|
||||
stop["lon"],
|
||||
)
|
||||
for stop in stops.values()
|
||||
]
|
||||
|
||||
context.add_object(
|
||||
staticmaps.Line(
|
||||
stop_coords,
|
||||
color=white,
|
||||
width=20,
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
elif mode not in ["TRAM", "BUS", "SUBWAY", "SUBURBAN", "METRO"]: # wayyy too many stops...
|
||||
context.add_object(
|
||||
staticmaps.Circle(
|
||||
center=point,
|
||||
fill_color=white,
|
||||
radius_km=0.1,
|
||||
color=stop_color,
|
||||
width=10,
|
||||
)
|
||||
)
|
||||
|
||||
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")
|
||||
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