346 lines
14 KiB
Python
346 lines
14 KiB
Python
from i3pystatus.core.util import internet, require
|
|
from i3pystatus.scores import ScoresBackend
|
|
|
|
import copy
|
|
import json
|
|
import pytz
|
|
import re
|
|
import time
|
|
from datetime import datetime
|
|
from urllib.request import urlopen
|
|
|
|
LIVE_URL = 'https://www.mlb.com/gameday/{id}'
|
|
SCOREBOARD_URL = 'http://m.mlb.com/scoreboard'
|
|
API_URL = 'https://statsapi.mlb.com/api/v1/schedule?sportId=1,51&date={date:%Y-%m-%d}&gameTypes=E,S,R,A,F,D,L,W&hydrate=team(),linescore(matchup,runners),stats,game(content(media(featured,epg),summary),tickets),seriesStatus(useOverride=true)&useLatestGames=false&language=en&leagueId=103,104,420'
|
|
|
|
|
|
class MLB(ScoresBackend):
|
|
'''
|
|
Backend to retrieve MLB scores. For usage examples, see :py:mod:`here
|
|
<.scores>`.
|
|
|
|
.. rubric:: Available formatters
|
|
|
|
* `{home_team}` — Depending on the value of the ``team_format`` option,
|
|
will contain either the home team's name, abbreviation, or city
|
|
* `{home_score}` — Home team's current score
|
|
* `{home_wins}` — Home team's number of wins
|
|
* `{home_losses}` — Home team's number of losses
|
|
* `{home_favorite}` — Displays the value for the :py:mod:`.scores` module's
|
|
``favorite`` attribute, if the home team is one of the teams being
|
|
followed. Otherwise, this formatter will be blank.
|
|
* `{away_team}` — Depending on the value of the ``team_format`` option,
|
|
will contain either the away team's name, abbreviation, or city
|
|
* `{away_score}` — Away team's current score
|
|
* `{away_wins}` — Away team's number of wins
|
|
* `{away_losses}` — Away team's number of losses
|
|
* `{away_favorite}` — Displays the value for the :py:mod:`.scores` module's
|
|
``favorite`` attribute, if the away team is one of the teams being
|
|
followed. Otherwise, this formatter will be blank.
|
|
* `{top_bottom}` — Displays the value of either ``inning_top`` or
|
|
``inning_bottom`` based on whether the game is in the top or bottom of an
|
|
inning.
|
|
* `{inning}` — Current inning
|
|
* `{outs}` — Number of outs in current inning
|
|
* `{venue}` — Name of ballpark where game is being played
|
|
* `{start_time}` — Start time of game in system's localtime (supports
|
|
strftime formatting, e.g. `{start_time:%I:%M %p}`)
|
|
* `{delay}` — Reason for delay, if game is currently delayed. Otherwise,
|
|
this formatter will be blank.
|
|
* `{postponed}` — Reason for postponement, if game has been postponed.
|
|
Otherwise, this formatter will be blank.
|
|
* `{suspended}` — Reason for suspension, if game has been suspended.
|
|
Otherwise, this formatter will be blank.
|
|
* `{extra_innings}` — When a game lasts longer than 9 innings, this
|
|
formatter will show that number of innings. Otherwise, it will blank.
|
|
|
|
.. rubric:: Team abbreviations
|
|
|
|
* **ARI** — Arizona Diamondbacks
|
|
* **ATL** — Atlanta Braves
|
|
* **BAL** — Baltimore Orioles
|
|
* **BOS** — Boston Red Sox
|
|
* **CHC** — Chicago Cubs
|
|
* **CIN** — Cincinnati Reds
|
|
* **CLE** — Cleveland Guardians
|
|
* **COL** — Colorado Rockies
|
|
* **CWS** — Chicago White Sox
|
|
* **DET** — Detroit Tigers
|
|
* **HOU** — Houston Astros
|
|
* **KC** — Kansas City Royals
|
|
* **LAA** — Los Angeles Angels of Anaheim
|
|
* **LAD** — Los Angeles Dodgers
|
|
* **MIA** — Miami Marlins
|
|
* **MIL** — Milwaukee Brewers
|
|
* **MIN** — Minnesota Twins
|
|
* **NYY** — New York Yankees
|
|
* **NYM** — New York Mets
|
|
* **OAK** — Oakland Athletics
|
|
* **PHI** — Philadelphia Phillies
|
|
* **PIT** — Pittsburgh Pirates
|
|
* **SD** — San Diego Padres
|
|
* **SEA** — Seattle Mariners
|
|
* **SF** — San Francisco Giants
|
|
* **STL** — St. Louis Cardinals
|
|
* **TB** — Tampa Bay Rays
|
|
* **TEX** — Texas Rangers
|
|
* **TOR** — Toronto Blue Jays
|
|
* **WSH** — Washington Nationals
|
|
'''
|
|
interval = 300
|
|
|
|
settings = (
|
|
('favorite_teams', 'List of abbreviations of favorite teams. Games '
|
|
'for these teams will appear first in the scroll '
|
|
'list. A detailed description of how games are '
|
|
'ordered can be found '
|
|
':ref:`here <scores-game-order>`.'),
|
|
('all_games', 'If set to ``True``, all games will be present in '
|
|
'the scroll list. If set to ``False``, then only '
|
|
'games from **favorite_teams** will be present in '
|
|
'the scroll list.'),
|
|
('display_order', 'When **all_games** is set to ``True``, this '
|
|
'option will dictate the order in which games from '
|
|
'teams not in **favorite_teams** are displayed'),
|
|
('format_no_games', 'Format used when no tracked games are scheduled '
|
|
'for the current day (does not support formatter '
|
|
'placeholders)'),
|
|
('format', 'Format used to display game information'),
|
|
('status_pregame', 'Format string used for the ``{game_status}`` '
|
|
'formatter when the game has not started '),
|
|
('status_in_progress', 'Format string used for the ``{game_status}`` '
|
|
'formatter when the game is in progress'),
|
|
('status_final', 'Format string used for the ``{game_status}`` '
|
|
'formatter when the game has finished'),
|
|
('status_postponed', 'Format string used for the ``{game_status}`` '
|
|
'formatter when the game has been postponed'),
|
|
('status_suspended', 'Format string used for the ``{game_status}`` '
|
|
'formatter when the game has been suspended'),
|
|
('inning_top', 'Value for the ``{top_bottom}`` formatter when game '
|
|
'is in the top half of an inning'),
|
|
('inning_bottom', 'Value for the ``{top_bottom}`` formatter when game '
|
|
'is in the bottom half of an inning'),
|
|
('team_colors', 'Dictionary mapping team abbreviations to hex color '
|
|
'codes. If overridden, the passed values will be '
|
|
'merged with the defaults, so it is not necessary to '
|
|
'define all teams if specifying this value.'),
|
|
('team_format', 'One of ``name``, ``abbreviation``, or ``city``. If '
|
|
'not specified, takes the value from the ``scores`` '
|
|
'module.'),
|
|
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
|
|
'format. If unspecified, the current day\'s games will be '
|
|
'displayed starting at 10am Eastern time, with last '
|
|
'evening\'s scores being shown before then. This option '
|
|
'exists primarily for troubleshooting purposes.'),
|
|
('live_url', 'Alternate URL string to launch MLB Gameday. This value '
|
|
'should not need to be changed'),
|
|
('scoreboard_url', 'Link to the MLB.com scoreboard page. Like '
|
|
'**live_url**, this value should not need to be '
|
|
'changed.'),
|
|
('api_url', 'Alternate URL string from which to retrieve score data. '
|
|
'Like **live_url*** this value should not need to be '
|
|
'changed.'),
|
|
)
|
|
|
|
required = ()
|
|
|
|
_default_colors = {
|
|
'AZ': '#A71930',
|
|
'ATL': '#CE1141',
|
|
'BAL': '#DF4601',
|
|
'BOS': '#BD3039',
|
|
'CHC': '#004EC1',
|
|
'CIN': '#C6011F',
|
|
'CLE': '#E31937',
|
|
'COL': '#5E5EB6',
|
|
'CWS': '#DADADA',
|
|
'DET': '#FF6600',
|
|
'HOU': '#EB6E1F',
|
|
'KC': '#0046DD',
|
|
'LAA': '#BA0021',
|
|
'LAD': '#005A9C',
|
|
'MIA': '#00A3E0',
|
|
'MIL': '#0747CC',
|
|
'MIN': '#D31145',
|
|
'NYY': '#0747CC',
|
|
'NYM': '#FF5910',
|
|
'OAK': '#006659',
|
|
'PHI': '#E81828',
|
|
'PIT': '#FFCC01',
|
|
'SD': '#FFC425',
|
|
'SEA': '#2E8B90',
|
|
'SF': '#FD5A1E',
|
|
'STL': '#B53B30',
|
|
'TB': '#8FBCE6',
|
|
'TEX': '#C0111F',
|
|
'TOR': '#0046DD',
|
|
'WSH': '#C70003',
|
|
}
|
|
|
|
_valid_teams = [x for x in _default_colors]
|
|
_valid_display_order = ['in_progress', 'suspended', 'final', 'pregame', 'postponed']
|
|
|
|
display_order = _valid_display_order
|
|
format_no_games = 'MLB: No games'
|
|
format = '[{scroll} ]MLB: [{away_favorite} ]{away_team} [{away_score} ]({away_wins}-{away_losses}) at [{home_favorite} ]{home_team} [{home_score} ]({home_wins}-{home_losses}) {game_status}'
|
|
status_pregame = '{start_time:%H:%M %Z}[ ({delay} Delay)]'
|
|
status_in_progress = '({top_bottom} {inning}, {outs} Out)[ ({delay} Delay)]'
|
|
status_final = '(Final[/{extra_innings}])'
|
|
status_postponed = '(PPD: {postponed})'
|
|
status_suspended = '(Suspended: {suspended})'
|
|
inning_top = 'Top'
|
|
inning_bottom = 'Bot'
|
|
team_colors = _default_colors
|
|
live_url = LIVE_URL
|
|
scoreboard_url = SCOREBOARD_URL
|
|
api_url = API_URL
|
|
|
|
# These will inherit from the Scores class if not overridden
|
|
team_format = None
|
|
|
|
@require(internet)
|
|
def check_scores(self):
|
|
self.get_api_date()
|
|
url = self.api_url.format(date=self.date)
|
|
|
|
game_list = self.get_nested(
|
|
self.api_request(url),
|
|
'dates:0:games',
|
|
default=[])
|
|
if not isinstance(game_list, list):
|
|
# When only one game is taking place during a given day, the game
|
|
# data is just a single dict containing that game's data, rather
|
|
# than a list of dicts. Encapsulate the single game dict in a list
|
|
# to make it process correctly in the loop below.
|
|
game_list = [game_list]
|
|
|
|
# Convert list of games to dictionary for easy reference later on
|
|
data = {}
|
|
team_game_map = {}
|
|
for game in game_list:
|
|
try:
|
|
id_ = game['gamePk']
|
|
except (KeyError, TypeError):
|
|
continue
|
|
|
|
away_abbrev = self.get_nested(
|
|
game,
|
|
'teams:away:team:abbreviation').upper()
|
|
home_abbrev = self.get_nested(
|
|
game,
|
|
'teams:home:team:abbreviation').upper()
|
|
if away_abbrev and home_abbrev:
|
|
try:
|
|
for team in (home_abbrev, away_abbrev):
|
|
if team in self.favorite_teams:
|
|
team_game_map.setdefault(team, []).append(id_)
|
|
except KeyError:
|
|
continue
|
|
|
|
data[id_] = game
|
|
|
|
self.interpret_api_return(data, team_game_map)
|
|
|
|
def process_game(self, game):
|
|
ret = {}
|
|
|
|
self.logger.debug(f'Processing {self.name} game data: {game}')
|
|
|
|
linescore = self.get_nested(game, 'linescore', default={})
|
|
|
|
ret['id'] = game['gamePk']
|
|
ret['inning'] = self.get_nested(linescore, 'currentInning', default=0)
|
|
ret['outs'] = self.get_nested(linescore, 'outs')
|
|
ret['live_url'] = self.live_url.format(id=ret['id'])
|
|
|
|
for team in ('away', 'home'):
|
|
team_data = self.get_nested(game, f'teams:{team}', default={})
|
|
|
|
if team == 'home':
|
|
ret['venue'] = self.get_nested(team_data, 'venue:name')
|
|
|
|
ret[f'{team}_city'] = self.get_nested(
|
|
team_data,
|
|
'team:locationName')
|
|
ret[f'{team}_name'] = self.get_nested(
|
|
team_data,
|
|
'team:teamName')
|
|
ret[f'{team}_abbreviation'] = self.get_nested(
|
|
team_data,
|
|
'team:abbreviation')
|
|
|
|
ret[f'{team}_wins'] = self.get_nested(
|
|
team_data,
|
|
'leagueRecord:wins',
|
|
callback=self.zero_fallback,
|
|
default=0)
|
|
ret[f'{team}_losses'] = self.get_nested(
|
|
team_data,
|
|
'leagueRecord:losses',
|
|
callback=self.zero_fallback,
|
|
default=0)
|
|
|
|
ret[f'{team}_score'] = self.get_nested(
|
|
linescore,
|
|
f'teams:{team}:runs',
|
|
callback=self.zero_fallback,
|
|
default=0)
|
|
|
|
for key in ('delay', 'postponed', 'suspended'):
|
|
ret[key] = ''
|
|
|
|
ret['status'] = self.get_nested(game, 'status:detailedState').replace(' ', '_').lower()
|
|
|
|
if ret['status'] == 'delayed_start':
|
|
ret['status'] = 'pregame'
|
|
ret['delay'] = self.get_nested(game, 'status:reason', default='Unknown')
|
|
elif ret['status'].startswith('delayed'):
|
|
ret['status'] = 'in_progress'
|
|
ret['delay'] = game['status']['detailedState'].split(':', 1)[-1].strip()
|
|
elif ret['status'] == 'postponed':
|
|
ret['postponed'] = self.get_nested(game, 'status:reason', default='Unknown Reason')
|
|
elif ret['status'].startswith('suspended'):
|
|
ret['status'] = 'suspended'
|
|
ret['suspended'] = self.get_nested(
|
|
game,
|
|
'status:detailedState',
|
|
default='Suspended').replace('Suspended: ', '')
|
|
elif ret['status'].startswith('completed_early') or ret['status'] == 'game_over':
|
|
ret['status'] = 'final'
|
|
elif ret['status'] not in ('in_progress', 'final'):
|
|
ret['status'] = 'pregame'
|
|
|
|
try:
|
|
ret['extra_innings'] = ret['inning'] \
|
|
if ret['status'] == 'final' and ret['inning'] != 9 \
|
|
else ''
|
|
except ValueError:
|
|
ret['extra_innings'] = ''
|
|
|
|
top_bottom = self.get_nested(linescore, 'inningHalf').lower()
|
|
ret['top_bottom'] = self.inning_top if top_bottom == 'top' \
|
|
else self.inning_bottom if top_bottom == 'bottom' \
|
|
else ''
|
|
|
|
try:
|
|
game_time = datetime.strptime(
|
|
self.get_nested(game, 'gameDate'),
|
|
'%Y-%m-%dT%H:%M:%SZ')
|
|
except ValueError as exc:
|
|
# Log when the date retrieved from the API return doesn't match the
|
|
# expected format (to help troubleshoot API changes), and set an
|
|
# actual datetime so format strings work as expected. The times
|
|
# will all be wrong, but the logging here will help us make the
|
|
# necessary changes to adapt to any API changes.
|
|
self.logger.exception(
|
|
f'Error encountered determining {self.name} game time for '
|
|
f'game {game["gamePk"]}'
|
|
)
|
|
game_time = datetime(1970, 1, 1)
|
|
|
|
ret['start_time'] = pytz.timezone('UTC').localize(game_time).astimezone()
|
|
|
|
self.logger.debug(f'Returned {self.name} formatter data: {ret}')
|
|
|
|
return ret
|