Compare commits

..
11 Commits
15 changed files with 415 additions and 469 deletions
+2
View File
@@ -3,3 +3,5 @@ __pycache__/
src/data/announcements/* src/data/announcements/*
!src/data/announcements/.gitkeep !src/data/announcements/.gitkeep
config.json config.json
src/legacy/
main_legacy.py
-4
View File
@@ -1,8 +1,4 @@
# Gleiswechsel # Gleiswechsel
## THIS BRANCH IS NO LONGER MAINTAINED
## This is the code for the legacy Gleiswechsel bot, based on the much simpler dbf API.
Gleiswechel ist ein Discord Bot, welcher einen Sprachkanal zu einer real-existierenden, aktuell befahrenden Zugverbindung umbenennt. Diesen Namen behält der Kanal so lange, wie die Verbindung in echt dauert. Gleiswechel ist ein Discord Bot, welcher einen Sprachkanal zu einer real-existierenden, aktuell befahrenden Zugverbindung umbenennt. Diesen Namen behält der Kanal so lange, wie die Verbindung in echt dauert.
![Beispiel Kanal](.github/preview_kanal.png) ![Beispiel Kanal](.github/preview_kanal.png)
+29 -28
View File
@@ -1,40 +1,41 @@
import discord import discord
import random from src.utils import config, logger
from discord.ext import tasks from src.dc.handlers import rename_vc
from src.config import config from src.dc.helpers import validate_channel
from src.handlers import rename_vc from src.dc.commands import setup_commands
from src.commands import setup_commands
from src.embeds import build_error_embed
from src.utils import logger
from src.data.status import discord_status
bot = discord.Bot(intents=discord.Intents.all()) bot = discord.Bot(intents=discord.Intents.all())
setup_commands(bot) setup_commands(bot=bot)
@tasks.loop(minutes=5)
async def change_status():
status = random.choice(discord_status)
await bot.change_presence(activity=discord.Game(name=f"{status} • /info"))
_bot_initialized = False _bot_initialized = False
@bot.event @bot.event
async def on_ready(): async def on_ready():
global _bot_initialized global _bot_initialized
print(f"{bot.user} ist online")
if not change_status.is_running(): logger(f"{bot.user} ist online")
change_status.start()
if not _bot_initialized: if not _bot_initialized:
_bot_initialized = True _bot_initialized = True
await rename_vc(bot) server_id = config["server"]
server_vc_id = config["vc"]
channel = validate_channel(bot=bot, server_id=server_id, channel_id=server_vc_id)
await rename_vc(bot, voice_channel=channel)
else: else:
logger("Discord Reconnect, laufende Fahrt bleibt unangetastet") logger("Reconnected to discord gateway, this wont disturb your current ride")
@bot.event try:
async def on_application_command_error(ctx, error): bot.run(config["token"])
embed = build_error_embed(f"Ein Fehler ist aufgetreten: {error}")
await ctx.respond(embed=embed)
try:
bot.run(config['token'])
except: except:
logger("Fehler beim parsen des token", "fatal") logger("Feher peim parsen des tokens", "fatal")
'''
TODO
- 1024 embed limit
- automatic reload of operators
- discord status
- text announcements
- voice announcements
- improved error handling (retry connection)
- multi language support
- random = False
'''
+147
View File
@@ -0,0 +1,147 @@
import requests
import random
import json
from datetime import datetime, timezone
from src.utils import logger, config, get_train_name, convert_iso_string
stations = config["stations"]
blacklist = config["blacklist"]
user_agent = config["http"]["user_agent"]
headers = {
"User-Agent": f"{user_agent}"
}
endpoint = "https://api.transitous.org"
def get_random_stop_id() -> str:
assigned_station = random.choice(stations)
req = f"{endpoint}/api/v1/geocode"
try:
response = requests.get(req, params={"text": assigned_station}, headers=headers)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
logger(f"An error occured while searching for a connection: {e}", "fatal")
return None
if response.status_code == 404:
logger(f"Error finding station '{assigned_station}'")
for entry in data:
if entry.get("type") != "STOP":
continue
return entry["id"]
def get_random_connection(stop_id: str) -> str:
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
print(f"Aktuelle Zeit für Query: {now}")
cursor = None
max_pages = 5
min_results = 5
trip_ids = []
all_stop_times = []
for _ in range(max_pages):
params = {
"stopId": stop_id,
"n": 20,
"time": now,
}
if cursor:
params["pageCursor"] = cursor
try:
response = requests.get(f"{endpoint}/api/v5/stoptimes", params=params, headers=headers)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
print(e)
break
stop_times = data.get("stopTimes", [])
all_stop_times.extend(stop_times)
for entry in stop_times:
dep = entry.get("place", {}).get("departure") or entry.get("place", {}).get("arrival")
print(f"{entry.get('mode')}: {convert_iso_string(dep)}")
trip_id = entry["tripId"]
if entry["mode"] in blacklist:
continue
trip_ids.append(entry["tripId"])
if len(trip_ids) >= min_results:
break
cursor = data.get("nextPageCursor")
if not cursor:
break
if not trip_ids:
logger("Couldn't find any connection", "fatal")
return None
trip_id = random.choice(trip_ids)
from_station = None
for entry in all_stop_times:
if entry.get("tripId") == trip_id:
from_station = entry.get("place", {}).get("name")
break
return {
"trip_id": trip_id,
"from_station": from_station
}
def get_trip_details(trip_id: str, from_station: str) -> dict:
req = f"{endpoint}/api/v2/trip"
try:
response = requests.get(req, params={"tripId": trip_id}, headers=headers)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
logger(f"An error occured while trying to get the route details: {e}", "fatal")
return None
legs = data["legs"][0]
display_name = legs["displayName"]
trip_from = legs["tripFrom"]["name"]
goes_to = legs["tripTo"]["name"]
start_time = legs["startTime"]
end_time = legs["endTime"]
mode = legs["mode"]
departure = convert_iso_string(start_time)
arrival = convert_iso_string(end_time)
train_name = get_train_name(display_name, mode)
trip_details = {
"long_name": f"{train_name} nach {goes_to} von {from_station}",
"short_name": display_name,
"from": from_station,
"to": goes_to,
"agency": legs["agencyName"],
"route_color": legs.get("routeColor"),
"duration": legs["duration"],
"departure": departure,
"arrival": arrival,
"mode": mode,
"stops": {}
}
trip_details["stops"][trip_from] = departure
for stop in legs["intermediateStops"]:
stop_arrival = convert_iso_string(stop["arrival"])
trip_details["stops"][stop["name"]] = stop_arrival
if stop.get("name") == from_station:
departure_time = stop["departure"]
trip_details["departure"] = convert_iso_string(departure_time)
trip_details["stops"][goes_to] = arrival
return trip_details
-12
View File
@@ -1,12 +0,0 @@
import discord
from discord.ext import commands
from src.embeds import build_info_embed, build_error_embed
def setup_commands(bot: discord.Bot):
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
async def info(ctx):
embed = build_info_embed()
if embed is None:
await build_error_embed("Noch keine Verbindung gesetzt.")
return
await ctx.respond(embed=embed)
-4
View File
@@ -1,4 +0,0 @@
import json
with open("config.json", "r") as file:
config = json.load(file)
+8 -16
View File
@@ -1,18 +1,10 @@
emoji_list = { emoji_list = {
"Fallback": "🚉", "Fallback": "💺",
"Nahverkehr": "🚃", "BUS": "🚎",
"Höchstgeschwindigkeit": "🚄", "COACH": "🚎",
"Bus": "🚎" "TRAM": "🚈",
} "REGIONAL_RAIL": "🚊",
"HIGHSPEED_RAIL": "🚅",
train_types = { # Unvollständig! "METRO": "🚇",
"ICE": emoji_list["Höchstgeschwindigkeit"], "SUBWAY": "🚇"
"TGV": emoji_list["Höchstgeschwindigkeit"],
"RE": emoji_list["Nahverkehr"],
"RB": emoji_list["Nahverkehr"],
"ARV": emoji_list["Nahverkehr"],
"BRB": emoji_list["Nahverkehr"],
"Bus": emoji_list["Bus"]
} }
+5 -3
View File
@@ -38,7 +38,7 @@ OPERATORS = {
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/ODEG-Logo_Neu.svg/960px-ODEG-Logo_Neu.svg.png", "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/ODEG-Logo_Neu.svg/960px-ODEG-Logo_Neu.svg.png",
"color": 0x00745C "color": 0x00745C
}, },
"Nederlandse Spoorwegen": { "NS": {
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Logo_NS.svg/960px-Logo_NS.svg.png", "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Logo_NS.svg/960px-Logo_NS.svg.png",
"color": 0X00337F, "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 haben een serious probleem", "Neuken in de keuken"]
@@ -109,10 +109,12 @@ OPERATOR_ALIASES = {
"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 NRW": OPERATORS["db_allgemein"], "DB Regio AG NRW": OPERATORS["db_allgemein"],
"DB Regio AG Nord": OPERATORS["db_allgemein"],
"DB Regio AG Südost": OPERATORS["db_allgemein"], "DB Regio AG Südost": OPERATORS["db_allgemein"],
"DB Regio AG Nordost": OPERATORS["db_allgemein"], "DB Regio AG Nordost": OPERATORS["db_allgemein"],
"DB Regio AG Mitte": OPERATORS["db_allgemein"], "DB Regio AG Mitte": OPERATORS["db_allgemein"],
"SBB GmbH": OPERATORS["SBB"], "SBB GmbH": OPERATORS["SBB"],
"Schweizerische Bundesbahnen SBB": OPERATORS["SBB"], "Schweizerische Bundesbahnen SBB": OPERATORS["SBB"],
} }
+7
View File
@@ -0,0 +1,7 @@
import discord
from src.dc.embeds import build_info_embed
def setup_commands(bot: discord.Bot):
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
async def info(ctx):
await ctx.respond(embed=build_info_embed())
+50
View File
@@ -0,0 +1,50 @@
import discord
import random
from src.utils import convert_iso_string, get_operator_metadata
from src.dc.helpers import format_timestamp_to_dc
def build_embed_footer(mode: str, slogans):
footer_notice = f"Data provided by https://transitous.org • Typ: {mode}"
icon = "https://avatars.githubusercontent.com/u/24960008?s=60&v=4"
if slogans is not None:
footer_text = f"{random.choice(slogans)}{footer_notice}"
else:
footer_text = footer_notice
return {
"text": footer_text,
"icon": icon
}
def build_info_embed() -> discord.Embed:
from src.dc.handlers import trip
agency = trip["agency"]
metadata = get_operator_metadata(agency, trip["route_color"])
departure = format_timestamp_to_dc(trip["departure"])
arrival = format_timestamp_to_dc(trip["arrival"])
embed = discord.Embed(
title = trip["long_name"],
description=f"Abfahrt von {trip["from"]} um {departure}. Ankunft um {arrival}",
color = metadata["color"]
)
route_lines = []
for stop_name, stop_arrival in trip["stops"].items():
if stop_name == trip["from"]:
route_lines.append(f"**• {stop_name} ({stop_arrival} Uhr)**")
else:
route_lines.append(f"{stop_name} ({stop_arrival} Uhr)")
embed.add_field(name="Route", value="\n".join(route_lines))
footer = build_embed_footer(trip["mode"], metadata["slogans"])
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
embed.set_author(name=agency)
embed.set_thumbnail(url=metadata["logo"])
color = trip.get("color", "keine farbe :(")
print(color)
return embed
+48
View File
@@ -0,0 +1,48 @@
import discord
import asyncio
from datetime import datetime, timedelta, date
from src.utils import logger, channel_formatting, choose_connection
_scheduled_task: asyncio.Task | None = None
async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = False):
global trip, _scheduled_task
if not from_scheduler and _scheduled_task and not _scheduled_task.done():
_scheduled_task.cancel()
trip = choose_connection()
arrival = trip["arrival"]
long_name = trip["long_name"]
print("-----------------")
logger(f"Umstieg: {long_name}, Ankunft: {arrival} Uhr")
logger(f"Betreiber: {trip["agency"]}, Typ: {trip["mode"]}")
logger(f"Versuche Namen zu ändern, wenn nichts passiert bin ich im cooldown... (warte bis zu 10min!)")
formatting = channel_formatting(trip["mode"])
await voice_channel.edit(name=f"{formatting}{long_name}")
await voice_channel.set_status(f"Ankunft um {arrival}")
logger(f"Name geändert!")
_scheduled_task = asyncio.create_task(_schedule_next_transfer(bot, arrival))
async def _schedule_next_transfer(bot, arrival):
now = datetime.now()
parsed_time = datetime.strptime(arrival, "%H:%M").time()
arrival_dt = datetime.combine(date.today(), parsed_time)
if arrival_dt < now:
arrival_dt += timedelta(days=1)
wait_seconds = (arrival_dt - now).total_seconds()
if wait_seconds > 0:
remaining = str(timedelta(seconds=wait_seconds))
logger(f"Nächster Umstieg in {remaining.split('.')[0]} ({arrival} Uhr)")
await asyncio.sleep(wait_seconds)
logger("Zug angekommen, wähle neue Verbindung")
await rename_vc(bot, from_scheduler=True)
+31
View File
@@ -0,0 +1,31 @@
import discord
from datetime import datetime, timedelta
from src.utils import logger, convert_iso_string
def validate_channel(bot: discord.bot, server_id: int, channel_id: int):
guild = bot.get_guild(server_id)
channel = guild.get_channel(channel_id)
if guild is None:
logger(f"Es konnte kein Server mit der ID {server_id} gefunden werden", "fatal")
if not isinstance(channel, discord.VoiceChannel):
logger(f"Es konnte kein VC mit der id {channel_id} gefunden werden", "fatal")
return False
return channel
def format_timestamp_to_dc(timestr):
parsed_time = datetime.strptime(timestr, "%H:%M")
now = datetime.now()
final_datetime = datetime.now().replace(
hour=parsed_time.hour,
minute=parsed_time.minute,
second=0,
microsecond=0
)
if final_datetime <= now:
final_datetime += timedelta(days=1)
return discord.utils.format_dt(final_datetime, style="t")
-109
View File
@@ -1,109 +0,0 @@
import random
import discord
from datetime import datetime, timedelta
import src.handlers as handlers
from src.config import config
from src.utils import operator_metadata, format_via_list, resolve_operator, logger
def format_timestamp(timestr):
parsed_time = datetime.strptime(timestr, "%H:%M")
now = datetime.now()
final_datetime = datetime.now().replace(
hour=parsed_time.hour,
minute=parsed_time.minute,
second=0,
microsecond=0
)
if final_datetime <= now:
final_datetime += timedelta(days=1)
return discord.utils.format_dt(final_datetime, style="t")
def format_iso_timestamp(isostr):
parsed_time = discord.utils.parse_time(isostr)
return discord.utils.format_dt(parsed_time, style="t")
def build_embed_footer(operator_slogans):
dbf = config.get("dbf", "https://dbf.finalrewind.org")
footer_notice = f"Daten großzügig bereitgestellt von {dbf} • Typ: {handlers.train_type}"
footer_text = (
f"{random.choice(operator_slogans)}{footer_notice}"
if operator_slogans else
footer_notice
)
icon = f"{dbf}/static/icons/icon-96x96.png"
return {
"text": footer_text,
"icon": icon
}
def build_info_embed() -> discord.Embed | None:
conn = handlers.current
info = handlers.train_info
if handlers.current is None and handlers.train_info is None:
return None
current_operator = resolve_operator(info["operators"])
arrival = format_iso_timestamp(info["arrival"])
departure = format_timestamp(conn['departure'])
operator_infos = operator_metadata(current_operator)
embed = discord.Embed(
title=handlers.train_name,
description=f"Abfahrt von {conn['station']} um {departure}. Ankunft um {arrival}",
color=operator_infos["color"]
)
if len(conn['via']) > 0:
embed.add_field(name="Über", value=format_via_list(conn['via']), inline=False)
if operator_infos.get("unknown"):
logger(f"Metadaten für {current_operator} konten in operators.py nicht gefunden werden")
embed.set_author(name=current_operator)
embed.set_thumbnail(url=operator_infos["logo"])
route_lines = []
for stop in conn['route']:
stop_name = stop["name"]
if stop_name == conn['station']:
route_lines.append(f"• **{stop_name}**")
else:
route_lines.append(f"{stop_name}")
embed.add_field(name="Route", value="\n".join(route_lines))
footer = build_embed_footer(operator_infos.get("slogan"))
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
return embed
def build_announcement_embed(msg):
current_operator = resolve_operator(handlers.train_info["operators"])
operator_infos = operator_metadata(current_operator)
embed = discord.Embed(
title = "Informationen zu Ihrer Fahrt",
description=msg,
color=operator_infos["color"]
)
embed.set_author(name=current_operator)
embed.set_thumbnail(url=operator_infos["logo"])
footer = build_embed_footer(operator_infos.get("slogan"))
embed.set_footer(text=footer["text"], icon_url=footer["icon"])
return embed
def build_error_embed(errormsg) -> discord.Embed:
embed = discord.Embed(
title="Ein Fehler ist aufgetreten!",
description=errormsg,
color=discord.Colour.red()
)
return embed
-154
View File
@@ -1,154 +0,0 @@
import discord
from datetime import datetime, timedelta
import asyncio
import random
from pathlib import Path
from src.config import config
from src.utils import random_connection, get_train_info, get_channel_formatting, logger
from src.embeds import build_announcement_embed, build_info_embed
current = None
_scheduled_task: asyncio.Task | None = None
async def announcer(bot, announcement):
voice_channel = bot.get_channel(config["vc"])
if len(voice_channel.members) > 0:
match announcement:
case "ende":
destination = current['destination']
embed = build_announcement_embed(
f'Sehr geehrte Fahrgäste,\nIn wenigen Minuten erreichen wir {destination}. Dieser Zug endet dort.\n\nWir wünschen Ihnen eine angenehme Weiterreise.\n\nVielen Dank für Ihr Vertrauen und auf Wiedersehen.')
if config["voice_announcements"][0]["enabled"]:
await voice_announcer(bot, destination, voice_channel)
case "umstieg":
embed = build_info_embed()
case _:
embed = None
if embed is None:
logger(f"Unbekanntes Announcement: {announcement}")
else:
await voice_channel.send(embed=embed)
else:
logger(f"Announcement {announcement} wird geskipped, keiner da")
return
async def voice_announcer(bot: discord.Bot, destination, voice_channel):
voice_announcement_config = config["voice_announcements"][0]
voice_stations = voice_announcement_config["stations"]
if destination in voice_stations:
announcement_for = destination
else:
if voice_stations.get("general", "") == "":
return
announcement_for = "general"
if voice_stations.values() == list:
sound_file = random.choice(voice_stations.get(announcement_for))
else:
sound_file = voice_stations.get(announcement_for)
sound_path = f"src/data/announcements/{sound_file}"
if Path(sound_path).is_file() is False:
logger(f"Konnte Datei {sound_path} nicht finden", "error")
return
logger(f"VC wird betreten, spiele {sound_path}")
vc = await voice_channel.connect(timeout=15, reconnect=True)
audio_source = discord.FFmpegPCMAudio(sound_path)
if not vc.is_playing():
def after_playing(error):
if error:
logger(f"Player error: {error}", "error")
bot.loop.create_task(vc.disconnect())
logger("VC wird verlassen")
vc.play(audio_source, after=after_playing)
async def rename_vc(bot: discord.Bot, from_scheduler: bool = False):
global current, train_name, train_info, train_type, _scheduled_task
guild = bot.get_guild(int(config["server"]))
if guild is None:
logger(f"Es konnte kein Server mit der ID {config['server']} gefunden werden! Ist der Bot ein Member?", "fatal")
return False
channel = guild.get_channel(int(config["vc"]))
if not isinstance(channel, discord.VoiceChannel):
logger(f"Es konnte kein VC mit der ID {config['vc']} auf dem Server gefunden werden", "fatal")
return False
if not from_scheduler and _scheduled_task and not _scheduled_task.done():
_scheduled_task.cancel()
attempt = 0
while True:
if attempt == 20:
logger("Zu viele Fehlversuche. Füge einen anderen Bahnhof hinzu.", "fatal")
return "Es konnte kein Zug gefunden werden."
attempt += 1
current = random_connection()
if current == None:
return None
parts = current['train'].split()
train_type = parts[0]
if parts[1].isdigit():
train = current['train']
train_ID = parts[1]
else:
train = parts[1]
train_ID = current['train_number']
station = current['station']
train_info = get_train_info(station=station, train_ID=train_ID, train_type=train_type)
if train_info and train_info.get('operators') and train_info.get('arrival'):
break
logger(f"Versuch {attempt}: Fehler bei {current['train']} von {station}, versuche neue Verbindung...")
train_name = f"{train} nach {current['destination']} von {current['station']}"
logger(f"Vorbereitung auf {train_name} (typ: {train_type})")
arrival = datetime.fromisoformat((train_info["arrival"]))
formatting = get_channel_formatting(train_type)
print("-----------------------------------------")
logger(f"Umstieg: {train_name}")
logger(f"Betreiber: {''.join(train_info['operators'])}")
logger(f"Train-Type: {train_type}")
logger(f"Wenn der Name nicht geändert wird bin ich im Cooldown")
await channel.edit(name=f"{formatting}{train_name}")
await channel.set_status(f"Ankunft um {arrival.strftime('%H:%M')}")
logger(f"Name geändert!")
if config.get("announcements", True):
await announcer(bot, "umstieg")
_scheduled_task = asyncio.create_task(_schedule_next_umstieg(bot, arrival))
return True
async def _schedule_next_umstieg(bot, arrival):
announcement = config.get("announcements", True)
announcement_countdown = random.randrange(180, 300) # letzte station announcement ist meistens 3-5min vor ankunft
wait_seconds = (arrival - datetime.now()).total_seconds()
if wait_seconds > 0:
remaining = str(timedelta(seconds=wait_seconds))
logger(f"Nächster Umstieg in {remaining.split('.')[0]} ({arrival.strftime('%H:%M:%S')} Uhr)")
if announcement and wait_seconds > announcement_countdown:
wait_until_end_announcement = wait_seconds - announcement_countdown
await asyncio.sleep(wait_until_end_announcement)
await announcer(bot, "ende")
await asyncio.sleep(announcement_countdown)
else:
await asyncio.sleep(wait_seconds)
logger("Zug angekommen, wähle neue Verbindung...")
await rename_vc(bot, from_scheduler=True)
+88 -139
View File
@@ -1,149 +1,98 @@
import requests import json
import random
import os import os
import importlib import importlib
from datetime import datetime from datetime import datetime, timedelta
from src.config import config from zoneinfo import ZoneInfo
import src.data.operators as operators_module
import src.data.emojis as emojis
_operators_mtime = None import src.data.operators as operators
dbf = config.get("dbf", "https://dbf.finalrewind.org") from src.data.emojis import emoji_list
def _reload_operators_if_changed(): _operator_mtime = None
global _operators_mtime
path = operators_module.__file__ with open("config.json", "r") as file:
current_mtime = os.path.getmtime(path) config = json.load(file)
if _operators_mtime is None: def logger(msg, log_type="info") -> str:
_operators_mtime = current_mtime
return
if current_mtime != _operators_mtime:
importlib.reload(operators_module)
_operators_mtime = current_mtime
logger("operators.py wurde automatisch neu geladen (Änderungen erkannt)")
def random_connection():
available_stations = config["stations"].copy()
while True:
if not available_stations:
logger("Keine validen Bahnhöfe. Schlag den richtigen Bahnhofsnamen auf {dbf} nach", "fatal")
station = random.choice(available_stations)
url = f"{dbf}/{station}.json"
blacklist = config.get("blacklist", [])
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
logger(f"Fehler beim aussuchen der Verbindung: {e}", "fatal")
return
if response.status_code == 300:
logger(f"Bahnhof '{station}' konnte nicht gefunden werden ({url})", "error")
available_stations.remove(station)
continue
departures = [
d for d in data.get("departures", [])
if d.get("scheduledDeparture") and d.get("destination") != station
and not d.get("train", "").startswith(tuple(blacklist))
]
if not departures:
continue
if not config['random']:
dep = departures[0]
else:
dep = random.choice(departures)
return {
"train": dep['train'],
"destination": dep['destination'],
"route": dep['route'],
"departure": dep['scheduledDeparture'],
"via": dep['via'],
"station": station,
"train_number": dep['trainNumber']
}
def get_train_info(station, train_ID, train_type):
url = f"{dbf}/z/{train_type}%20{train_ID}/{station}.json"
logger(f"Fetche {url}")
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
logger(f"ReqestException Fehler: {e}")
return None
dep = data.get("departure", [])
if not dep:
logger("Kein departure Feld gefunden", "error")
return None
route_post = dep.get("route_post_diff")
if not route_post:
logger("Kein route_post_diff Feld gefunden", "error")
return None
arrival_iso = route_post[-1].get("sched_arr")
if not arrival_iso:
logger("Keine Ankunftszeit gefunden", "error")
return None
return {
"arrival": arrival_iso,
"operators": dep.get("operators"),
}
def format_via_list(via: list[str]):
if not via:
return ""
if len(via) == 1:
return via[0]
return ", ".join(via[:-1]) + " und " + via[-1]
def resolve_operator(operators):
if not operators:
return None
for op in operators:
if op in operators_module.OPERATOR_ALIASES or op in operators_module.OPERATORS:
return op
return operators[0]
def operator_metadata(operator):
_reload_operators_if_changed()
if not operator:
return operators_module.OPERATORS["fallback"]
if operator in operators_module.OPERATOR_ALIASES:
return operators_module.OPERATOR_ALIASES[operator]
return operators_module.OPERATORS.get(operator, operators_module.OPERATORS["fallback"])
def get_channel_formatting(train_type):
formatting = config.get("formatting", "")
train_emoji = ""
if config.get("emojis", True):
train_emoji = emojis.train_types.get(train_type)
if train_emoji is None:
train_emoji = emojis.emoji_list.get("Fallback", "")
return f"{train_emoji}{formatting}"
def logger(msg, log_type="info"):
status = log_type.upper() status = log_type.upper()
current_time = datetime.now().strftime('%X') current_time = datetime.now().strftime('%X')
print(f"{current_time}: {status}: {msg}") print(f"{current_time}: {status}: {msg}")
if status == "FATAL": if status == "FATAL":
os._exit(1) os._exit(1)
def choose_connection() -> dict:
from src.api import transitous
station_id = transitous.get_random_stop_id()
connection = transitous.get_random_connection(station_id)
trip = transitous.get_trip_details(connection["trip_id"], connection["from_station"])
return trip
def convert_iso_string(isostring) -> str:
timezone = config.get("timezone", "Europe/Berlin")
dt = datetime.fromisoformat(isostring.replace('Z', '+00:00'))
dt = dt.astimezone(ZoneInfo(timezone))
if dt.second >= 30:
dt += timedelta(minutes=1)
return dt.strftime('%H:%M')
def channel_formatting(mode: str) -> str:
formatting = config.get("formatting", "")
if config.get("emojis", True):
emoji = emoji_list.get(mode)
if emoji is None:
emoji = emoji_list.get("Fallback")
return f"{emoji}{formatting}"
def get_train_name(train_name: str, mode: str) -> str:
if train_name.isdigit():
train = f"{mode.capitalize()} {train_name}"
elif "(" in train_name:
train = train_name.split(" (")[0]
else:
train = train_name
return train
def _reload_operators_if_changed():
global _operator_mtime
path = operators.__file__
current_mtime = os.path.getmtime(path)
if _operator_mtime is None:
_operator_mtime = current_mtime
return
if current_mtime != _operator_mtime:
importlib.reload(operators)
_operator_mtime = current_mtime
logger("operators.py wurde automatisch neu geladen (Änderungen erkannt)")
def get_operator_metadata(agency: str, route_color: str) -> dict:
_reload_operators_if_changed()
op_data = operators.OPERATOR_ALIASES.get(agency) or operators.OPERATORS.get(agency) or operators.OPERATORS["fallback"]
logo = op_data.get("logo", operators.OPERATORS["fallback"]["logo"])
slogans = op_data.get("slogans")
color = op_data.get("color")
if color is None:
if route_color is not None:
try:
color = int(f"0x{route_color.upper()}")
except ValueError:
color = operators.OPERATORS["fallback"]["color"]
else:
color = operators.OPERATORS["fallback"]["colors"]
return {
"logo": logo,
"color": color,
"slogans": slogans
}