Files
NHL-Scoreboard/app/routes.py
T
josh f99738d2e4
CI / Lint (push) Successful in 8s
CI / Test (push) Successful in 11s
CI / Build & Push (push) Successful in 20s
fix: show correct "Game X of 7" for future playoff dates
Enrich raw score-endpoint games with gameNumber from the series cache
before parsing. The score API omits gameNumber and its seriesStatus
reflects current wins, so all future games in a series computed the
same number. Now we cross-reference by game id against the cached
series-detail endpoint which includes the correct gameNumber per game.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 15:39:44 -04:00

174 lines
5.0 KiB
Python

import json
import logging
import requests as http_requests
from flask import (
abort,
make_response,
render_template,
jsonify,
request,
send_from_directory,
)
from app import APP_VERSION, app, static_v
from app.config import SCOREBOARD_DATA_FILE
from app.games import parse_games
from app.playoff import today_meta
from app.bracket_view import build_bracket_view
from app.playoff_cache import (
day_n_for_round,
enrich_game_numbers,
fetch_series,
get_bracket,
parse_series_id,
refresh_bracket,
)
from app.series_view import build_series_view
from datetime import datetime
from zoneinfo import ZoneInfo
_EASTERN = ZoneInfo("America/New_York")
_logger = logging.getLogger(__name__)
def _fetch_date(date_str):
url = f"https://api-web.nhle.com/v1/score/{date_str}"
try:
resp = http_requests.get(url, timeout=10)
resp.raise_for_status()
return resp.json()
except http_requests.RequestException as e:
_logger.error("Failed to fetch scores for %s: %s", date_str, e)
return None
def _max_playoff_round(raw_games):
max_round = 0
for g in raw_games or []:
if g.get("gameType") != 3:
continue
r = (g.get("seriesStatus") or {}).get("round") or 0
if r > max_round:
max_round = r
return max_round or None
@app.route("/manifest.json")
def manifest():
return send_from_directory(app.static_folder, "manifest.json")
@app.route("/sw.js")
def service_worker():
precache = [
"/",
static_v("styles.css"),
static_v("script.js"),
static_v("icon-192x192.png"),
static_v("icon-512x512.png"),
"/manifest.json",
]
body = render_template("sw.js.j2", app_version=APP_VERSION, precache=precache)
response = make_response(body)
response.headers["Content-Type"] = "application/javascript; charset=utf-8"
response.headers["Service-Worker-Allowed"] = "/"
response.headers["Cache-Control"] = "no-cache"
return response
@app.route("/favicon.ico")
def favicon():
return send_from_directory(
app.static_folder, "icon-32x32.png", mimetype="image/png"
)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/scoreboard")
def get_scoreboard():
date_param = request.args.get("date")
today_str = datetime.now(_EASTERN).strftime("%Y-%m-%d")
if date_param and date_param != today_str:
try:
datetime.strptime(date_param, "%Y-%m-%d")
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD."}), 400
scoreboard_data = _fetch_date(date_param)
if not scoreboard_data:
return jsonify(
{"error": "Failed to retrieve scoreboard data for that date."}
)
else:
try:
with open(SCOREBOARD_DATA_FILE, "r") as json_file:
scoreboard_data = json.load(json_file)
except FileNotFoundError:
return jsonify(
{"error": "Failed to retrieve scoreboard data. File not found."}
)
except json.JSONDecodeError:
return jsonify(
{"error": "Failed to retrieve scoreboard data. Invalid JSON format."}
)
if scoreboard_data:
raw_games = scoreboard_data.get("games", [])
enrich_game_numbers(raw_games)
games = parse_games(scoreboard_data)
max_round = _max_playoff_round(raw_games)
n = day_n_for_round(max_round) if max_round else None
meta = today_meta(raw_games, day_n=n)
pinned = [g for g in games if g.get("Pinned")]
remaining = [g for g in games if not g.get("Pinned")]
return jsonify(
{
"meta": meta,
"pinned_games": pinned,
"live_games": [
g
for g in remaining
if g["Game State"] == "LIVE" and not g["Intermission"]
],
"intermission_games": [
g
for g in remaining
if g["Game State"] == "LIVE" and g["Intermission"]
],
"pre_games": [g for g in remaining if g["Game State"] == "PRE"],
"final_games": [g for g in remaining if g["Game State"] == "FINAL"],
}
)
else:
return jsonify({"error": "Failed to retrieve scoreboard data"})
@app.route("/series/<series_id>")
def series_detail(series_id):
if parse_series_id(series_id) is None:
abort(404)
payload = fetch_series(series_id)
if payload is None:
abort(404)
view = build_series_view(series_id, payload)
return render_template("series.html", series=view)
@app.route("/bracket")
def bracket():
year = datetime.now(_EASTERN).year
payload, fetched_at = get_bracket(year)
if payload is None:
payload = refresh_bracket(year)
if payload is None:
abort(404)
view = build_bracket_view(year, payload, fetched_at=fetched_at)
return render_template("bracket.html", bracket=view)