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/") 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)