new feature: profiles

This commit is contained in:
2026-08-07 21:18:42 +02:00
parent 1c3ea8fb87
commit b90b2f9ba1
4 changed files with 67 additions and 30 deletions
+12 -3
View File
@@ -13,12 +13,18 @@ class StationConfig:
exclude_destinations: List[str] = field(default_factory=list) exclude_destinations: List[str] = field(default_factory=list)
@dataclass
class ProfileConfig:
name: str
stations: List[StationConfig] = field(default_factory=list)
@dataclass @dataclass
class AppConfig: class AppConfig:
refresh_seconds: int refresh_seconds: int
cache_seconds: int cache_seconds: int
departures_limit: int departures_limit: int
stations: List[StationConfig] profiles: List[ProfileConfig]
def load_config(path: str = None) -> AppConfig: def load_config(path: str = None) -> AppConfig:
@@ -26,18 +32,21 @@ def load_config(path: str = None) -> AppConfig:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
raw = yaml.safe_load(f) raw = yaml.safe_load(f)
profiles = []
for p in raw.get("profiles", []):
stations = [ stations = [
StationConfig( StationConfig(
name=s["name"], name=s["name"],
type=s["type"].upper(), type=s["type"].upper(),
exclude_destinations=s.get("exclude_destinations", []) or [], exclude_destinations=s.get("exclude_destinations", []) or [],
) )
for s in raw.get("stations", []) for s in p.get("stations", [])
] ]
profiles.append(ProfileConfig(name=p["name"], stations=stations))
return AppConfig( return AppConfig(
refresh_seconds=int(raw.get("refresh_seconds", 60)), refresh_seconds=int(raw.get("refresh_seconds", 60)),
cache_seconds=int(raw.get("cache_seconds", 20)), cache_seconds=int(raw.get("cache_seconds", 20)),
departures_limit=int(raw.get("departures_limit", 10)), departures_limit=int(raw.get("departures_limit", 10)),
stations=stations, profiles=profiles,
) )
+20 -7
View File
@@ -12,7 +12,7 @@ from fastapi.staticfiles import StaticFiles
from mvg import MvgApi, TransportType from mvg import MvgApi, TransportType
from app.config import load_config, StationConfig from app.config import load_config, StationConfig, ProfileConfig
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mvg-departures") logger = logging.getLogger("mvg-departures")
@@ -98,27 +98,40 @@ def fetch_departures_for_station(station_cfg: StationConfig) -> List[Dict[str, A
return result return result
def get_all_departures() -> List[Dict[str, Any]]: def get_profile_by_name(profile_name: str) -> ProfileConfig:
for profile in config.profiles:
if profile.name == profile_name:
return profile
return config.profiles[0] if config.profiles else None
def get_departures_for_profile(profile: ProfileConfig) -> List[Dict[str, Any]]:
all_deps: List[Dict[str, Any]] = [] all_deps: List[Dict[str, Any]] = []
for station_cfg in config.stations: for station_cfg in profile.stations:
all_deps.extend(fetch_departures_for_station(station_cfg)) all_deps.extend(fetch_departures_for_station(station_cfg))
all_deps.sort(key=lambda d: d["time_epoch"] or 0) all_deps.sort(key=lambda d: d["time_epoch"] or 0)
return all_deps return all_deps
@app.get("/api/departures") @app.get("/api/departures")
def api_departures(): def api_departures(profile: str = None):
return JSONResponse(content={"departures": get_all_departures()}) active_profile = get_profile_by_name(profile) if profile else config.profiles[0] if config.profiles else None
if not active_profile:
return JSONResponse(content={"departures": []})
return JSONResponse(content={"departures": get_departures_for_profile(active_profile)})
@app.get("/") @app.get("/")
def index(request: Request): def index(request: Request, profile: str = None):
departures = get_all_departures() active_profile = get_profile_by_name(profile) if profile else config.profiles[0] if config.profiles else None
departures = get_departures_for_profile(active_profile) if active_profile else []
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"index.html", "index.html",
{ {
"departures": departures, "departures": departures,
"profiles": config.profiles,
"active_profile": active_profile,
"refresh_seconds": config.refresh_seconds, "refresh_seconds": config.refresh_seconds,
"generated_at": datetime.now().strftime("%H:%M:%S"), "generated_at": datetime.now().strftime("%H:%M:%S"),
}, },
+13 -1
View File
@@ -5,11 +5,23 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abfahrten</title> <title>Abfahrten</title>
<meta http-equiv="refresh" content="{{ refresh_seconds }}"> <meta http-equiv="refresh" content="{{ refresh_seconds }};url=/?profile={{ active_profile.name }}">
</head> </head>
<body> <body>
<div style="max-width: 375px; margin: 0 auto; width: 100%;"> <div style="max-width: 375px; margin: 0 auto; width: 100%;">
<h1>Abfahrten <span class="updated">{{ generated_at }}</span></h1> <h1>Abfahrten <span class="updated">{{ generated_at }}</span></h1>
{% if profiles|length > 1 %}
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
{% for p in profiles %}
<a href="/?profile={{ p.name }}"
style="flex: 1; padding: 12px; text-align: center; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 18px; color: {{ 'white' if p.name == active_profile.name else '#9aa5b1' }}; background: {{ '#005ca9' if p.name == active_profile.name else '#1e242b' }};">
{{ p.name }}
</a>
{% endfor %}
</div>
{% endif %}
{% if departures %} {% if departures %}
<table> <table>
{% for d in departures %} {% for d in departures %}
+4 -1
View File
@@ -7,15 +7,18 @@ refresh_seconds: 60
cache_seconds: 20 cache_seconds: 20
departures_limit: 10 departures_limit: 10
profiles:
- name: "Hinfahrt"
stations: stations:
- name: "Josephsburg, München" - name: "Josephsburg, München"
type: "UBAHN" type: "UBAHN"
exclude_destinations: exclude_destinations:
- "Messestadt Ost" - "Messestadt Ost"
- "Messestadt West" - "Messestadt West"
- name: "Berg am Laim, München" - name: "Berg am Laim, München"
type: "SBAHN" type: "SBAHN"
exclude_destinations: exclude_destinations:
- "Erding" - "Erding"
- "Markt Schwaben" - "Markt Schwaben"
- name: "Rückfahrt"
stations: []