1 """
2 The live module provides a mechanism of periodically checking which games are
3 being actively played.
4
5 It requires the third party library pytz to be
6 installed, which makes sure game times are compared properly with respect
7 to time zones. pytz can be downloaded from PyPI:
8 http://pypi.python.org/pypi/pytz/
9
10 It works by periodically downloading data from NFL.com for games that started
11 before the current time. Once a game completes, the live module stops asking
12 NFL.com for data for that game.
13
14 If there are no games being actively played (i.e., it's been more than N hours
15 since the last game started), then the live module sleeps for longer periods
16 of time.
17
18 Thus, the live module can switch between two different modes: active and
19 inactive.
20
21 In the active mode, the live module downloads data from NFL.com in
22 short intervals. A transition to an inactive mode occurs when no more games
23 are being played.
24
25 In the inactive mode, the live module only checks if a game is playing (or
26 about to play) every 15 minutes. If a game is playing or about to play, the
27 live module switches to the active mode. Otherwise, it stays in the inactive
28 mode.
29
30 With this strategy, if the live module is working properly, you could
31 theoretically keep it running for the entire season.
32
33 (N.B. Half-time is ignored. Games are either being actively played or not.)
34 """
35 import datetime
36 import time
37 import urllib2
38 import xml.dom.minidom as xml
39
40 import pytz
41
42 import nflgame.game
43 import nflgame.schedule
44
45 _MAX_GAME_TIME = 60 * 60 * 6
46 """
47 The assumed maximum time allowed for a game to complete. This is used to
48 determine whether a particular game that isn't over is currently active.
49 """
50
51 _WEEK_INTERVAL = 60 * 60 * 12
52 """
53 How often to check what the current week is. By default, it is twice a day.
54 """
55
56 _CUR_SCHEDULE_URL = "http://www.nfl.com/liveupdate/scorestrip/ss.xml"
57 """
58 Pinged infrequently to discover the current week number, year and week type.
59 The actual schedule of games is taken from the schedule module.
60 """
61
62 _EASTERN_TZ = pytz.timezone('US/Eastern')
63 """Used to convert game times in EST to UTC."""
64
65 _cur_week = None
66 """The current week. It is updated infrequently automatically."""
67
68 _cur_year = None
69 """The current year. It is updated infrequently automatically."""
70
71 _preseason = False
72 """True when it's the preseason."""
73
74 _regular = False
75 """True when it's the regular season."""
76
77 _completed = []
78 """
79 A list of game eids that have been completed since the live module started
80 checking for updated game stats.
81 """
82
83
84 -def run(callback, active_interval=15, inactive_interval=900, stop=None):
85 """
86 Starts checking for games that are currently playing.
87
88 Every time there is an update, callback will be called with two lists:
89 active and completed. The active list is a list of game.Game that are
90 currently being played. The completed list is a list of game.Game that
91 have just finished. A game will appear in the completed list only once,
92 after which that game will not be in either the active or completed lists.
93 No game can ever be in both lists at the same time.
94
95 It is possible that a game in the active list is not yet playing because
96 it hasn't started yet. It ends up in the active list because the "pregame"
97 has started on NFL.com's GameCenter web site, and sometimes game data is
98 partially filled. When this is the case, the 'playing' method on
99 a nflgame.game.Game will return False.
100
101 When in the active mode (see live module description), active_interval
102 specifies the number of seconds to wait between checking for updated game
103 data. Please do not make this number too low to avoid angering NFL.com.
104 If you anger them too much, it is possible that they could ban your IP
105 address.
106
107 Note that NFL.com's GameCenter page is updated every 15 seconds, so
108 setting the active_interval much smaller than that is wasteful.
109
110 When in the inactive mode (see live module description), inactive_interval
111 specifies the number of seconds to wait between checking whether any games
112 have started or are about to start.
113
114 With the default parameters, run will never stop. However, you may set
115 stop to a Python datetime.datetime value. After time passes the stopping
116 point, run will quit. (Technically, it's possible that it won't quit until
117 at most inactive_interval seconds after the stopping point is reached.)
118 The stop value is compared against datetime.datetime.now().
119 """
120 active = False
121 last_week_check = _update_week_number()
122
123
124
125
126 for info in _active_games(inactive_interval):
127 game = nflgame.game.Game(info['eid'])
128
129
130
131 if game is None:
132 continue
133
134
135
136 if game.game_over():
137 _completed.append(info['eid'])
138
139 while True:
140 if stop is not None and datetime.datetime.now() > stop:
141 return
142
143 if time.time() - last_week_check > _WEEK_INTERVAL:
144 last_week_check = _update_week_number()
145
146 games = _active_games(inactive_interval)
147 if active:
148 active = _run_active(callback, games)
149 if not active:
150 continue
151 time.sleep(active_interval)
152 else:
153 active = not _run_inactive(games)
154 if active:
155 continue
156 time.sleep(inactive_interval)
157
158
160 """
161 The active mode traverses each of the active games and fetches info for
162 each from NFL.com.
163
164 Then each game (that has info available on NFL.com---that is, the game
165 has started) is added to one of two lists: active and completed, which
166 are passed as the first and second parameters to callback. A game is
167 put in the active list if it's still being played, and into the completed
168 list if it has finished. In the latter case, it is added to a global store
169 of completed games and will never be passed to callback again.
170 """
171
172
173 if len(games) == 0:
174 return False
175
176 active, completed = [], []
177 for info in games:
178 game = nflgame.game.Game(info['eid'])
179
180
181
182 if game is None:
183 continue
184
185
186 if game.game_over():
187 completed.append(game)
188 _completed.append(info['eid'])
189 else:
190 active.append(game)
191
192 callback(active, completed)
193 return True
194
195
197 """
198 The inactive mode simply checks if there are any active games. If there
199 are, inactive mode needs to stop and transition to active mode---thus
200 we return False. If there aren't any active games, then the inactive
201 mode should continue, where we return True.
202
203 That is, so long as there are no active games, we go back to sleep.
204 """
205 return len(games) == 0
206
207
209 """
210 Returns a list of all active games. In this case, an active game is a game
211 that will start within inactive_interval seconds, or has started within
212 _MAX_GAME_TIME seconds in the past.
213 """
214 games = []
215 for (year, t, week, _, _), info in nflgame.schedule.games:
216 if year != _cur_year:
217 continue
218 if week != _cur_week:
219 continue
220 if t == 'PRE' and not _preseason:
221 continue
222 if t == 'REG' and not _regular:
223 continue
224 if not _game_is_active(info, inactive_interval):
225 continue
226 games.append(info)
227 return games
228
229
231 """
232 Returns true if the game is active. A game is considered active if the
233 game start time is in the past and not in the completed list (which is
234 a private module level variable that is populated automatically) or if the
235 game start time is within inactive_interval seconds from starting.
236 """
237 gametime = _game_datetime(gameinfo)
238 now = _now()
239 if gametime >= now:
240 return (gametime - now).seconds <= inactive_interval
241 return gameinfo['eid'] not in _completed
242
243
245 now = _now()
246 assert now <= gametime
247 return (gametime - now).seconds
248
249
251 now = _now()
252 assert now >= gametime
253 return (now - gametime).seconds
254
255
257 hour, minute = gameinfo['time'].strip().split(':')
258 d = datetime.datetime(gameinfo['year'], gameinfo['month'], gameinfo['day'],
259 (int(hour) + 12) % 24, int(minute))
260 return _EASTERN_TZ.localize(d).astimezone(pytz.utc)
261
262
264 return datetime.datetime.now(pytz.utc)
265
266
277