From 2fe91bd8264254cc04b6ed4c2b9738edcf4fb8c4 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Sun, 18 Feb 2024 01:54:33 -0500 Subject: [PATCH] first commit --- .gitignore | 2 + Dockerfile | 25 ++++ app.py | 248 +++++++++++++++++++++++++++++++++++++ nhl_standings.db | Bin 0 -> 8192 bytes requirements.txt | 6 + static/styles.css | 243 ++++++++++++++++++++++++++++++++++++ templates/index.html | 139 +++++++++++++++++++++ update_nhl_standings_db.py | 64 ++++++++++ 8 files changed, 727 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 nhl_standings.db create mode 100644 requirements.txt create mode 100644 static/styles.css create mode 100644 templates/index.html create mode 100644 update_nhl_standings_db.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b191cc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/nhle_scoreboard_response.txt +/nhle_standings_response.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..07fc1c7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Use an official Python runtime as a parent image +FROM python:3.9-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set the working directory in the container +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY . /app + +# Install any needed dependencies specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Expose the Flask port +EXPOSE 5000 + +# Copy static files and templates +COPY ./templates /app/templates +COPY ./static /app/static + +# Run the Flask application +CMD ["python", "app.py"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..3b05f9e --- /dev/null +++ b/app.py @@ -0,0 +1,248 @@ +from flask import Flask, render_template +import requests +from datetime import datetime, timedelta +from waitress import serve +import sqlite3 +import threading +import time +import schedule +import json + + +app = Flask(__name__) + +# Configuration +scoreboard_data = None + +# Data Retrieval +def get_nhle_scoreboard(): + now = datetime.now() + start_time_evening = now.replace(hour=23, minute=0, second=0, microsecond=0) # 7:00 PM EST + end_time_evening = now.replace(hour=8, minute=0, second=0, microsecond=0) # 3:00 AM EST + + if now >= start_time_evening or now < end_time_evening: + # Use now URL + nhle_api_url = "https://api-web.nhle.com/v1/score/now" + + else: + # Use current data URL + nhle_api_url = f"https://api-web.nhle.com/v1/score/{now.strftime('%Y-%m-%d')}" + + response = requests.get(nhle_api_url) + if response.status_code == 200: + return response.json() + else: + print("Error:", response.status_code) + +# Store scoreboard data locally +def store_scoreboard_data(): + global scoreboard_data + scoreboard_data = get_nhle_scoreboard() + +# Schedule the task to run every 10 seconds +def schedule_task(): + schedule.every(10).seconds.do(store_scoreboard_data) + while True: + schedule.run_pending() + time.sleep(1) + +# Data Processing +def extract_game_info(): + global scoreboard_data + if scoreboard_data: + extracted_info = [] + for game in scoreboard_data.get("games", []): + home_team = game["homeTeam"]["name"]["default"] + away_team = game["awayTeam"]["name"]["default"] + home_logo = game["homeTeam"]["logo"] + away_logo = game["awayTeam"]["logo"] + game_state = convert_game_state(game["gameState"]) + period, time_remaining, time_running, is_intermission = process_time_and_period(game_state, game) + home_score, away_score, home_shots, away_shots = process_scores_and_shots(game_state, game) + start_time_str, home_record, away_record = process_start_time_and_records(game_state, game) + game_priority = calculate_game_priority(game) + + # Get power play information + home_power_play = get_power_play_info(game, home_team) + away_power_play = get_power_play_info(game, away_team) + + # Get game outcome + last_period_type = get_game_outcome(game_state, game) + + + extracted_info.append({ + "Home Team": home_team, + "Home Score": home_score, + "Away Team": away_team, + "Away Score": away_score, + "Home Logo": home_logo, + "Away Logo": away_logo, + "Game State": game_state, + "Period": period, + "Time Remaining": time_remaining, + "Time Running": time_running, + "Intermission": is_intermission, + "Priority": game_priority, + "Start Time": start_time_str, + "Home Record": home_record, + "Away Record": away_record, + "Home Shots": home_shots, + "Away Shots": away_shots, + "Home Power Play": home_power_play, + "Away Power Play": away_power_play, + "Last Period Type": last_period_type + }) + + # Sort games based on priority + sorted_info = sorted(extracted_info, key=lambda x: x["Priority"], reverse=True) + return sorted_info + +def convert_game_state(game_state): + if game_state == "OFF": + return "FINAL" + elif game_state == "CRIT": + return "LIVE" + elif game_state == "FUT": + return "PRE" + else: + return game_state + +def process_time_and_period(game_state, game): + if game_state in ["PRE", "FUT"]: + return 0, "20:00", False, False + elif game_state in ["FINAL", "OFF"]: + return "N/A", "00:00", False, False + else: + period = game["periodDescriptor"]["number"] + time_remaining = game["clock"]["timeRemaining"] + if time_remaining == "00:00": + time_remaining = "END" + time_running = game["clock"]["running"] + is_intermission = game["clock"]["inIntermission"] + return period, time_remaining, time_running, is_intermission + +def process_scores_and_shots(game_state, game): + if game_state in ["PRE", "FUT"]: + return 0, 0, 0, 0 + else: + return game["homeTeam"]["score"], game["awayTeam"]["score"], game["homeTeam"]["sog"], game["awayTeam"]["sog"] + +def process_start_time_and_records(game_state, game): + if game_state in ["PRE", "FUT"]: + start_time_utc = game["startTimeUTC"] + start_time_str = utc_to_est_time(start_time_utc) + home_record = game["homeTeam"]["record"] + away_record = game["awayTeam"]["record"] + game_state = "PRE" + else: + start_time_str = "N/A" + home_record = "N/A" + away_record = "N/A" + return start_time_str, home_record, away_record + +def get_power_play_info(game, team_name): + if "situation" in game and "situationDescriptions" in game["situation"]: + for situation in game["situation"]["situationDescriptions"]: + if situation == "PP" and game["awayTeam"]["name"]["default"] == team_name: + return f"PP {game['situation']['timeRemaining']}" + elif situation == "PP" and game["homeTeam"]["name"]["default"] == team_name: + return f"PP {game['situation']['timeRemaining']}" + return "" # Return empty string if team is not on power play + +def get_game_outcome(game_state, game): + if game_state == "FINAL": + last_period_type = game["gameOutcome"]["lastPeriodType"] + else: + last_period_type = "N/A" + + return last_period_type + + + + +def calculate_game_priority(game): + if game["gameState"] in ["FINAL", "OFF", "PRE", "FUT"] or game["clock"]["inIntermission"]: + return 0 + else: + # Get standings information from the database for home and away teams + home_team_standings = get_team_standings(game["homeTeam"]["name"]["default"]) + away_team_standings = get_team_standings(game["awayTeam"]["name"]["default"]) + + # Calculate total values of leagueSequence + leagueL10Sequence for each team + home_team_total = home_team_standings["league_sequence"] + home_team_standings["league_l10_sequence"] + away_team_total = away_team_standings["league_sequence"] + away_team_standings["league_l10_sequence"] + + # Calculate the priority adjustment factor by subtracting away team's total from home team's total + matchup_adjustment = home_team_total + away_team_total + + period = game.get("periodDescriptor", {}).get("number", 0) + time_remaining = game.get("clock", {}).get("secondsRemaining", 0) + home_score = game["homeTeam"]["score"] + away_score = game["awayTeam"]["score"] + score_difference = abs(home_score - away_score) + score_total = (home_score + away_score) * 15 + + if period == 4: + base_priority = 400 + elif period == 3: + base_priority = 300 + elif period == 2: + base_priority = 200 + else: + base_priority = 100 + + if score_difference > 3: + base_priority -= 500 + elif score_difference > 2: + base_priority -= 350 + elif score_difference > 1: + base_priority -= 100 + + if score_difference == 0 and period == 3 and time_remaining <= 600: + base_priority += 100 + + time_priority = (1200 - time_remaining) / 20 + + # Add the priority adjustment factor to the base priority + return int(base_priority + time_priority - matchup_adjustment + score_total) + +def get_team_standings(team_name): + conn = sqlite3.connect("nhl_standings.db") + cursor = conn.cursor() + cursor.execute(""" + SELECT league_sequence, league_l10_sequence + FROM standings + WHERE team_common_name = ? + """, (team_name,)) + result = cursor.fetchone() + conn.close() + if result: + return {"league_sequence": result[0], "league_l10_sequence": result[1]} + else: + return {"league_sequence": 0, "league_l10_sequence": 0} + +def utc_to_est_time(utc_time): + utc_datetime = datetime.strptime(utc_time, "%Y-%m-%dT%H:%M:%SZ") + est_offset = timedelta(hours=-5) + est_datetime = utc_datetime + est_offset + est_time_str = est_datetime.strftime("%I:%M %p") + return est_time_str + +# Routes +@app.route('/') +def index(): + game_info = extract_game_info() + if game_info: + live_games_exist = any(game["Game State"] == "LIVE" for game in game_info) + pre_games_exist = any(game["Game State"] == "PRE" for game in game_info) + final_games_exist = any(game["Game State"] == "FINAL" for game in game_info) + return render_template('index.html', games=game_info, live_games_exist=live_games_exist, + pre_games_exist=pre_games_exist, final_games_exist=final_games_exist) + else: + print("Failed to retrieve scoreboard data") + return "Failed to retrieve scoreboard data" + +if __name__ == '__main__': + store_scoreboard_data() + threading.Thread(target=schedule_task).start() + serve(app, host="0.0.0.0", port=2897) \ No newline at end of file diff --git a/nhl_standings.db b/nhl_standings.db new file mode 100644 index 0000000000000000000000000000000000000000..36f4ed15a0be2e6b171d24aa83f50a2ce483ad41 GIT binary patch literal 8192 zcmeI#KW`H;7zXfj>NdU0#ZJ=vX_BTf0tA&1BESw!8%oCL^^O)VTG8RJC zUB)cSvh|hF*J(rg2A_8Hzv=(4+17P=oYpBs+m`-80|5{K0T2KI5C8!X009sH0T2Lz zb1(3sfs&jT?Uj&x$RXvpFL1X

e0uvNpI6Dv_ft2gM1@j$S3lFyeDtTYx0~##3NmDhukEWbOC4}00JNY z0w4eaAOHd&00JNY0_RL1!|(-!ngRDlL;h@}7<22mj8J#TqminoQ;WES(CWk+sl{Th zhA*4&M6eA#LBy(7BMZ2u<3jQ{jMRdguHq^}E8$TX3stSVm6K|GFA}Pfp=DgxgIw~y zFQh70$-J4p!VmnI2Wo!NEt&YeKp%uvsp1rI0ihi~=oe>6!6dhZ987$v3VAnglA949 z37Ma#bEfciB>E@Im|LJZ^I%;C`cy6=l`FehlV25wexR}inlZy2(Pum6%$}hsYW8Wi zrs!0Pn9imdAz3$VCT{tGE|X4XU3?Lt2mBxqtS$Jya#L>V*P4%XAT>uFQ@QOAhOyKI z94BKN;ro*)@;xqvvhDmVUP5Rs4EjQ{O=+TPcG^jr&2DJaWQoq0BFl$dFW(!AnOu6> wsCKv<=$A~-I9M;x?#4V)*qt)3Z$^e?ijc$!6PNK&Cld)5;i_%K%@@Q!0R!^5TmS$7 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c35fa70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==3.0.2 +Jinja2==3.1.3 +requests==2.31.0 +Werkzeug==3.0.1 +waitress==3.0.0 +schedule==1.2.1 diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..7c9156b --- /dev/null +++ b/static/styles.css @@ -0,0 +1,243 @@ +body { + background-color: #121212; /* Dark background color */ + font-family: Arial, sans-serif; /* Use a common sans-serif font */ + color: #fff; /* White text color */ +} + +h1 { + text-align: center; + margin-top: 20px; + color: #f2f2f2; /* Lighten the text color */ +} + +.scoreboard { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + margin-top: 20px; +} + +.game-box { + background-color: #333; /* Dark background color for game boxes */ + border-radius: 12px; /* Rounded corners */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Add a subtle shadow */ + padding: 20px; + margin-bottom: 20px; + margin-left: 20px; + margin-right: 20px; + width: 300px; + position: relative; /* Position relative for absolute positioning */ +} + +.team-info { + display: flex; + align-items: center; + margin-bottom: 7px; + margin-top: 25px; /* Added margin-top */ +} + +.team-info-column { + display: flex; + flex-direction: column; +} + +.team-logo { + width: 50px; + height: auto; + margin-right: 10px; +} + +.team-name { + font-size: 18px; + font-weight: bold; +} + +.team-score { + font-size: 25px; + font-weight: bold; + margin-left: auto; +} + +.team-record { + font-size: 12px; + margin-left: auto; + font-weight: bold; +} + +.team-sog { + font-size: 12px; /* Adjust font size as needed */ + color: #ddd; /* Lighter text color */ +} + +.team-power-play { + font-size: 12px; /* Adjust font size as needed */ + color: red; /* Set color to red */ + margin-left: 10px; /* Add some margin for spacing */ +} + +.game-info { + margin-top: 10px; + color: #aaa; /* Lighten the text color */ + text-align: center; +} + +.game-info strong { + margin-right: 5px; +} + +.live-dot { + position: absolute; + top: 5px; + right: 5px; + width: 10px; + height: 10px; + background-color: red; + border-radius: 50%; +} + +.pre-state { + position: absolute; + top: 10px; + left: 10px; /* Adjusted left position */ + background-color: #444; /* Darker background color for pre state */ + padding: 5px; + border-radius: 5px; + font-size: 12px; + color: #fff; /* White text color for live state */ + font-weight: bolder; /* Bold text for live state */ + z-index: 1; /* Ensure the live state box is above other content */ +} + +.final-state { + position: absolute; + top: 10px; + left: 10px; /* Adjusted left position */ + background-color: #444; /* Darker background color for final state */ + padding: 5px; + border-radius: 5px; + font-size: 12px; + color: #ddd; /* Lighter text color for final state */ + z-index: 1; /* Ensure the final state box is above other content */ + font-weight: bold; +} + +.live-state { + position: absolute; + top: 10px; + left: 10px; /* Adjusted left position */ + background-color: #0b6e31; /* Darker green background color for live state */ + padding: 5px; + border-radius: 5px; + font-size: 12px; + color: #fff; /* White text color for live state */ + font-weight: bolder; /* Bold text for live state */ + z-index: 1; /* Ensure the live state box is above other content */ +} + +.live-state-intermission { + position: absolute; + top: 10px; + left: 10px; /* Adjusted left position */ + background-color: #444; /* Darker green background color for live state */ + padding: 5px; + border-radius: 5px; + font-size: 12px; + color: #fff; /* White text color for live state */ + font-weight: bolder; /* Bold text for live state */ + z-index: 1; /* Ensure the live state box is above other content */ +} + +.live-time { + position: absolute; + top: 10px; + left: 45px; /* Adjusted left position */ + background-color: #444; /* Darker background color for time box */ + padding: 5px; + border-radius: 5px; + font-size: 12px; + color: #ddd; /* Lighter text color for time box */ + z-index: 1; /* Ensure the time box is above other content */ +} + +.live-time-intermission { + position: absolute; + top: 10px; + left: 60px; /* Adjusted left position */ + background-color: #444; /* Darker background color for time box */ + padding: 5px; + border-radius: 5px; + font-size: 12px; + color: #ddd; /* Lighter text color for time box */ + z-index: 1; /* Ensure the time box is above other content */ +} + +.live-games-section { + display: flex; + align-items: start; + flex-wrap: wrap; + justify-content: flex-start; + margin-top: 20px; +} + +.pre-games-section { + display: flex; + align-items: start; + flex-wrap: wrap; + justify-content: flex-start; + margin-top: 20px; +} + +.final-games-section { + display: flex; + align-items: start; + flex-wrap: wrap; + justify-content: flex-start; + margin-top: 20px; +} + +/* Existing CSS styles */ + +/* Add media query for smaller screens */ +@media only screen and (max-width: 768px) { + .scoreboard { + flex-direction: column; /* Change direction to column for smaller screens */ + align-items: center; /* Center align items */ + } + + .game-box { + width: 90%; /* Adjust width for better fit on smaller screens */ + margin: 10px; /* Adjust margins */ + } + + .team-info { + align-items: center; /* Center align items */ + margin-top: 26px; /* Adjust top margin */ + margin-bottom: 5px; /* Adjust bottom margin */ + } + + .team-logo { + width: 36px; /* Adjust logo size */ + height: 36px; + } + + .team-name { + font-size: 16px; /* Decrease font size for better readability */ + font-weight: bold; + } + + .team-score { + font-size: 24px; /* Decrease font size for better readability */ + font-weight: bold; + } + + .game-info { + font-size: 12px; /* Decrease font size for better readability */ + } + + .live-state, + .live-time, + .pre-state, + .final-state { + font-size: 12px; /* Decrease font size for better readability */ + } +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0503833 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,139 @@ + + + + NHL Scoreboard + + + + + + {% if live_games_exist %} +

Live Games

+
+ {% for game in games if game['Game State'] == 'LIVE' %} +
+ {% if game['Time Running'] %} +
+ {% endif %} +
+ +
+ {{ game['Away Team'] }} + SOG: {{ game['Away Shots'] }} + {{ game['Away Power Play'] }} +
+ {{ game['Away Score'] }} +
+
+ +
+ {{ game['Home Team'] }} + SOG: {{ game['Home Shots'] }} + {{ game['Home Power Play'] }} +
+ {{ game['Home Score'] }} +
+
+ {% if game['Intermission'] %} +
+ {% if game['Period'] == 1 %} + 1st Int + {% elif game['Period'] == 2 %} + 2nd Int + {% elif game['Period'] == 3 %} + 3rd Int + {% endif %} +
+
{{ game['Time Remaining'] }}
+ + {% else %} +
+ {% if game['Period'] == 1 %} + 1st + {% elif game['Period'] == 2 %} + 2nd + {% elif game['Period'] == 3 %} + 3rd + {% else %} + OT + {% endif %} +
+
{{ game['Time Remaining'] }}
+ {% endif %} + +
+
+ Game Score: {{ game['Priority'] }} +
+
+ {% endfor %} +
+ {% endif %} + + {% if pre_games_exist %} +

On Later

+
+ {% for game in games if game['Game State'] == 'PRE' %} +
+
{{ game['Start Time'] }}
+
+ + {{ game['Away Team'] }} + {{ game['Away Record'] }} +
+
+ + {{ game['Home Team'] }} + {{ game['Home Record'] }} +
+
+ {% endfor %} +
+ {% endif %} + + {% if final_games_exist %} +

Game Over

+
+ {% for game in games if game['Game State'] == 'FINAL' %} +
+
+ {% if game['Last Period Type'] == 'REG' %} + FINAL + {% elif game['Last Period Type'] == 'OT' %} + FINAL/OT + {% else %} + FINAL/SO + {% endif %} +
+
+ +
+ {{ game['Away Team'] }} + SOG: {{ game['Away Shots'] }} +
+ {{ game['Away Score'] }} +
+
+ +
+ {{ game['Home Team'] }} + SOG: {{ game['Home Shots'] }} +
+ {{ game['Home Score'] }} +
+
+ {% endfor %} +
+ {% endif %} + + diff --git a/update_nhl_standings_db.py b/update_nhl_standings_db.py new file mode 100644 index 0000000..5f5319b --- /dev/null +++ b/update_nhl_standings_db.py @@ -0,0 +1,64 @@ +import sqlite3 +import requests + +def create_standings_table(conn): + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS standings ( + team_common_name TEXT, + league_sequence INTEGER, + league_l10_sequence INTEGER + ) + """) + conn.commit() + +def truncate_standings_table(conn): + cursor = conn.cursor() + cursor.execute("DELETE FROM standings") + conn.commit() + +def insert_standings_info(conn, standings_info): + cursor = conn.cursor() + for team in standings_info: + cursor.execute(""" + INSERT INTO standings (team_common_name, league_sequence, league_l10_sequence) + VALUES (?, ?, ?) + """, (team["team_common_name"], team["league_sequence"], team["league_l10_sequence"])) + conn.commit() + +def extract_standings_info(): + url = "https://api-web.nhle.com/v1/standings/now" + response = requests.get(url) + if response.status_code == 200: + standings_data = response.json() + standings_info = [] + for team in standings_data.get("standings", []): + team_info = { + "team_common_name": team["teamCommonName"]["default"], + "league_sequence": team["leagueSequence"], + "league_l10_sequence": team["leagueL10Sequence"] + } + standings_info.append(team_info) + return standings_info + else: + print("Error:", response.status_code) + return None + +# Connect to SQLite database +conn = sqlite3.connect("nhl_standings.db") + +# Create standings table if it doesn't exist +create_standings_table(conn) + +# Truncate standings table before inserting new data +truncate_standings_table(conn) + +# Extract standings info +standings_info = extract_standings_info() + +# Insert standings info into the database +if standings_info: + insert_standings_info(conn, standings_info) + +# Close database connection +conn.close()