I was experimenting with the NHL API the other day. It was interesting and I didn’t want to make things really complicated, but something that I did want was to be able to update the sceeen in place without it scrolling for each update.
Curses is the obvious solution for this, but it seemed a little complicated for a simple little program. I did some digging and found Blessed which is a nice Python library that wraps up curses functionality in a much easier to use package.
It took me just a few minutes to add the in place updates to the code. I still want to figure out what to do with the games that are finalized or postponed, but it’s good enough for now.
Here is what I came up with:
from blessed import Terminal
from datetime import datetime
import jmespath
import json
import requests
from requests.exceptions import HTTPError
import sys
import time
BASE_URL = 'https://statsapi.web.nhl.com'
SLEEP_SEC = 5
def main():
term = Terminal()
print(term.clear)
# get today's date in yyyy-mm-dd format
today = datetime.today().strftime('%Y-%m-%d')
active_games = get_active_games(today)
if len(active_games) == 0:
print("No active games")
sys.exit()
# loop through and get the live information for each game
while True:
for i, game_url in enumerate(active_games):
res = get_url(game_url)
# get the data
away = jmespath.search("gameData.teams.away.name", res)
home = jmespath.search("gameData.teams.home.name", res)
current_play = jmespath.search("liveData.plays.allPlays[-1].result.event", res)
desc = jmespath.search("liveData.plays.allPlays[-1].result.description", res)
time = jmespath.search("liveData.plays.allPlays[-1].about.periodTimeRemaining", res)
per = jmespath.search("liveData.plays.allPlays[-1].about.ordinalNum", res)
goals_away = jmespath.search("liveData.plays.allPlays[-1].about.goals.away", res)
goals_home = jmespath.search("liveData.plays.allPlays[-1].about.goals.home", res)
status = jmespath.search("gameData.status.detailedState", res)
# remove the games that are postponed or final
if status == "Postponed" or status == "Final":
active_games.remove(game_url)
else:
# get the last play
print(term.move_xy(0, i*3) + term.clear_eol
+ f"{away} @ {home}: {goals_away}-{goals_home}")
print(term.move_xy(0, i*3+1) + term.clear_eol
+ f"\t{desc} at {time} in the {per}")
# break if a keypress is detected and sleep before next loop
with term.cbreak(), term.hidden_cursor():
inp = term.inkey(timeout=SLEEP_SEC)
if inp:
break
# stop the loop if there are no more games left
if len(active_games) == 0:
print("All active games complete")
sys.exit()
def get_active_games(date):
""" get_active_games returns array of games for date in yyyy-mm-dd format """
calendar_url = BASE_URL + '/api/v1/schedule?date=' + date
res = get_url(calendar_url)
current_games = jmespath.search("dates[0].games[].link", res)
# check the live feed for the status of the games
active_games = []
for link in current_games:
game_url = BASE_URL + link
res = get_url(game_url)
# get the game status
status = jmespath.search("gameData.status.detailedState", res)
away = jmespath.search("gameData.teams.away.name", res)
home = jmespath.search("gameData.teams.home.name", res)
# remove the games that are postponed or final
if status == "Postponed":
#print(f"{away} at {home} postponed")
pass
elif status == "Final":
away_goals = jmespath.search("liveData.linescore.teams.away.goals", res)
home_goals = jmespath.search("liveData.linescore.teams.home.goals", res)
#print(f"Final {away}:{away_goals} {home}:{home_goals}")
else:
active_games.append(game_url)
return active_games
def get_url(url):
""" get_url returns json string for a given url or prints an error """
try:
response = requests.get(url)
response.raise_for_status()
except HTTPError as http_err:
print(f'HTTP error: {http_error}')
except Exception as err:
print('Error: {err}')
else:
return response.json()
if __name__ == "__main__":
main()