def run(self, path=None): path = self.kwargs.get('path', path) if not path and self.request.method == 'GET': yield self.login() raise tornado.gen.Return() args = {key: val[0] for key, val in self.args.items()} params = AttrDict(self.kwargs) params['access_key'] = self.get_token('access_key', self.get_from_token) params['access_secret'] = self.get_token('access_secret', self.get_from_token) client = oauth1.Client(client_key=params['key'], client_secret=params['secret'], resource_owner_key=params['access_key'], resource_owner_secret=params['access_secret']) endpoint = params.get('endpoint', 'https://api.twitter.com/1.1/') path = params.get('path', path) uri, headers, body = client.sign(url_concat(endpoint + path, args)) http = self.get_auth_http_client() response = yield http.fetch(uri, headers=headers, raise_error=False) result = yield self.social_response(response) self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write(result)
def setup_auth(cls, auth): # auth: if there's no auth: in handler, default to app.auth if auth is None: auth = conf.app.get('auth') # Treat True as an empty dict, i.e. auth: {} if auth is True: auth = AttrDict() # Set up the auth if isinstance(auth, dict): cls._auth = auth cls._on_init_methods.append(cls.authorize) cls.permissions = [] # Add check for condition if auth.get('condition'): cls.permissions.append( build_transform(auth['condition'], vars=AttrDict(handler=None), filename='url:%s.auth.permission' % cls.name)) # Add check for membership memberships = auth.get('membership', []) if not isinstance(memberships, list): memberships = [memberships] if len(memberships): cls.permissions.append(check_membership(memberships)) elif auth: app_log.error('url:%s.auth is not a dict', cls.name)
def load(): state.PROGRAMS = Tree() # Add configured players for pcls in [Player, Downloader, Postprocessor, ShellCommand]: ptype = camel_to_snake(pcls.__name__) cfgkey = ptype + "s" for name, cfg in config.settings.profile[cfgkey].items(): if not cfg: cfg = AttrDict() path = cfg.pop("path", None) or cfg.get( "command", distutils.spawn.find_executable(name) ) if not path: logger.warning(f"couldn't find command for {name}") continue # First, try to find by "type" config value, if present try: klass = next( c for c in Program.SUBCLASSES[ptype].values() if c.__name__.lower().replace(ptype, "") == cfg.get("type", "").replace("-", "").lower() ) except StopIteration: # Next, try to find by config name matching class name try: klass = next( c for c in Program.SUBCLASSES[ptype].values() if c.cmd == name ) except StopIteration: # Give up and make it a generic program klass = pcls if cfg.get("disabled") == True: continue state.PROGRAMS[ptype][name] = ProgramDef( cls=klass, name=name, path=path, cfg = AttrDict(cfg) ) # Try to find any players not configured for ptype in Program.SUBCLASSES.keys(): cfgkey = ptype + "s" for name, klass in Program.SUBCLASSES[ptype].items(): cfg = config.settings.profile[cfgkey][name] if name in state.PROGRAMS[ptype] or (cfg and cfg.disabled == True): continue path = distutils.spawn.find_executable(name) if path: state.PROGRAMS[ptype][name] = ProgramDef( cls=klass, name=name, path=path, cfg = AttrDict() )
def setup_error(cls, error): ''' Sample configuration:: error: 404: path: template.json # Use a template autoescape: false # with no autoescape whitespace: single # as a single line headers: Content-Type: application/json 500: function: module.fn args: [=status_code, =kwargs, =handler] ''' if not error: return if not isinstance(error, dict): return app_log.error('url:%s.error is not a dict', cls.name) # Compile all errors handlers cls.error = {} for error_code, error_config in error.items(): try: error_code = int(error_code) if error_code < 100 or error_code > 1000: raise ValueError() except ValueError: app_log.error('url.%s.error code %s is not a number (100 - 1000)', cls.name, error_code) continue if not isinstance(error_config, dict): return app_log.error('url:%s.error.%d is not a dict', cls.name, error_code) # Make a copy of the original. When we add headers, etc, it shouldn't affect original error_config = AttrDict(error_config) error_path, error_function = error_config.get('path'), error_config.get('function') if error_function: if error_path: error_config.pop('path') app_log.warning('url.%s.error.%d has function: AND path:. Ignoring path:', cls.name, error_code) cls.error[error_code] = {'function': build_transform( error_config, vars=AttrDict((('status_code', None), ('kwargs', None), ('handler', None))), filename='url:%s.error.%d' % (cls.name, error_code) )} elif error_path: encoding = error_config.get('encoding', 'utf-8') cls.error[error_code] = {'function': cls._error_fn(error_code, error_config)} mime_type, encoding = mimetypes.guess_type(error_path, strict=False) if mime_type: error_config.setdefault('headers', {}).setdefault('Content-Type', mime_type) else: app_log.error('url.%s.error.%d must have a path or function key', cls.name, error_code) # Add the error configuration for reference if error_code in cls.error: cls.error[error_code]['conf'] = error_config cls._write_error, cls.write_error = cls.write_error, cls._write_custom_error
def get_app_config(appname, args): ''' Get the stored configuration for appname, and override it with args. ``.target`` defaults to $GRAMEXDATA/apps/<appname>. ''' apps_config['cmd'] = {appname: args} app_config = AttrDict((+apps_config).get(appname, {})) app_config.setdefault('target', str(app_dir / app_config.get('target', appname))) app_config.target = os.path.abspath(app_config.target) return app_config
def load(cls): global PROGRAMS # Add configured players for ptype in [Player, Helper, Downloader]: cfgkey = ptype.__name__.lower() + "s" for name, cfg in config.settings.profile[cfgkey].items(): if not cfg: cfg = AttrDict() path = cfg.pop("path", None) or cfg.get( "command", distutils.spawn.find_executable(name)) try: # raise Exception(cls.SUBCLASSES[ptype]) klass = next(c for c in cls.SUBCLASSES[ptype].values() if c.cmd == name) except StopIteration: klass = ptype if cfg.get("disabled") == True: logger.info(f"player {name} is disabled") continue PROGRAMS[ptype][name] = ProgramDef(cls=klass, name=name, path=path, cfg=AttrDict(cfg)) # Try to find any players not configured for ptype in cls.SUBCLASSES.keys(): cfgkey = ptype.__name__.lower() + "s" for name, klass in cls.SUBCLASSES[ptype].items(): cfg = config.settings.profile[cfgkey][name] if name in PROGRAMS[ptype] or cfg.disabled == True: continue path = distutils.spawn.find_executable(name) if path: PROGRAMS[ptype][name] = ProgramDef(cls=klass, name=name, path=path, cfg=AttrDict())
def url(conf): '''Set up the tornado web app URL handlers''' handlers = [] # Sort the handlers in descending order of priority specs = sorted(conf.items(), key=_sort_url_patterns, reverse=True) for name, spec in specs: _key = cache_key('url', spec) if _key in _cache: handlers.append(_cache[_key]) continue if 'handler' not in spec: app_log.error('url: %s: no handler specified') continue app_log.debug('url: %s (%s) %s', name, spec.handler, spec.get('priority', '')) urlspec = AttrDict(spec) handler = locate(spec.handler, modules=['gramex.handlers']) if handler is None: app_log.error('url: %s: ignoring missing handler %s', name, spec.handler) continue # Create a subclass of the handler with additional attributes. class_vars = {'name': name, 'conf': spec} # If there's a cache section, get the cache method for use by BaseHandler if 'cache' in urlspec: class_vars['cache'] = _cache_generator(urlspec['cache'], name=name) else: class_vars['cache'] = None # PY27 type() requires the class name to be a string, not unicode urlspec.handler = type(str(spec.handler), (handler, ), class_vars) # If there's a setup method, call it to initialize the class kwargs = urlspec.get('kwargs', {}) if hasattr(handler, 'setup'): try: urlspec.handler.setup_default_kwargs() urlspec.handler.setup(**kwargs) except Exception: app_log.exception('url: %s: setup exception in handler %s', name, spec.handler) # Since we can't set up the handler, all requests must report the error instead class_vars['exc_info'] = sys.exc_info() error_handler = locate('SetupFailedHandler', modules=['gramex.handlers']) urlspec.handler = type(str(spec.handler), (error_handler, ), class_vars) urlspec.handler.setup(**kwargs) try: handler_entry = tornado.web.URLSpec( name=name, pattern=_url_normalize(urlspec.pattern), handler=urlspec.handler, kwargs=kwargs, ) except re.error: app_log.error('url: %s: pattern: %s is invalid', name, urlspec.pattern) continue except Exception: app_log.exception('url: %s: invalid', name) continue _cache[_key] = handler_entry handlers.append(handler_entry) info.app.clear_handlers() info.app.add_handlers('.*$', handlers)
def milestones(self): try: # try to get the precise timestamps for this stream airing = next( a for a in self.provider.session.airings(self.game_id) if len(a["milestones"]) and a["mediaId"] == self.media_id) except StopIteration: # welp, no timestamps -- try to get them from whatever feed has them try: airing = next( a for a in self.provider.session.airings(self.game_id) if len(a["milestones"])) except StopIteration: logger.warning( SGStreamSessionException("No airing for media %s" % (self.media_id))) return AttrDict([("Start", 0)]) start_timestamps = [] try: start_time = next(t["startDatetime"] for t in next( m for m in airing["milestones"] if m["milestoneType"] == "BROADCAST_START")["milestoneTime"] if t["type"] == "absolute") except StopIteration: # Some streams don't have a "BROADCAST_START" milestone. We need # something, so we use the scheduled game start time, which is # probably wrong. start_time = airing["startDate"] # start_timestamps.append( # ("Start", start_time) # ) try: start_offset = next(t["start"] for t in next( m for m in airing["milestones"] if m["milestoneType"] == "BROADCAST_START")["milestoneTime"] if t["type"] == "offset") except StopIteration: # Same as above. Missing BROADCAST_START milestone means we # probably don't get accurate offsets for inning milestones. start_offset = 0 start_timestamps.append(("Start", start_offset)) timestamps = AttrDict(start_timestamps) timestamps.update( AttrDict([("%s%s" % ("T" if next( k for k in m["keywords"] if k["type"] == "top")["value"] == "true" else "B", int( next(k for k in m["keywords"] if k["type"] == "inning")["value"])), next(t["start"] for t in m["milestoneTime"] if t["type"] == "offset")) for m in airing["milestones"] if m["milestoneType"] == "INNING_START"])) # If we didn't get a BROADCAST_START timestamp but did get a timestamp # for the first inning, just use something reasonable (1st inning start # minus 15 minutes.) if timestamps.get("Start") == 0 and "T1" in timestamps: timestamps["Start"] = timestamps["T1"] - 900 timestamps.update([("Live", None)]) return timestamps
class MLBSession(object): HEADERS = {"User-agent": USER_AGENT} def __init__( self, username, password, api_key=None, client_api_key=None, token=None, access_token=None, ): self.session = requests.Session() self.session.cookies = LWPCookieJar() if not os.path.exists(COOKIE_FILE): self.session.cookies.save(COOKIE_FILE) self.session.cookies.load(COOKIE_FILE, ignore_discard=True) self.session.headers = self.HEADERS self._state = AttrDict([ ("username", username), ("password", password), ("api_key", api_key), ("client_api_key", client_api_key), ("token", token), ("access_token", access_token), ]) self.login() def __getattr__(self, attr): if attr in [ "delete", "get", "head", "options", "post", "put", "patch" ]: return getattr(self.session, attr) raise AttributeError(attr) @property def username(self): return self._state.username @property def password(self): return self._state.password @classmethod def new(cls): try: return cls.load() except: return cls(username=config.settings.username, password=config.settings.password) @classmethod def destroy(cls): if os.path.exists(COOKIE_FILE): os.remove(COOKIE_FILE) if os.path.exists(SESSION_FILE): os.remove(SESSION_FILE) @classmethod def load(cls): state = yaml.load(open(SESSION_FILE), Loader=AttrDictYAMLLoader) return cls(**state) def save(self): with open(SESSION_FILE, 'w') as outfile: yaml.dump(self._state, outfile, default_flow_style=False) self.session.cookies.save(COOKIE_FILE) def login(self): logger.debug("logging in") initial_url = ("https://secure.mlb.com/enterworkflow.do" "?flowId=registration.wizard&c_id=mlb") # res = self.session.get(initial_url) # if not res.status_code == 200: # raise MLBSessionException(res.content) data = { "uri": "/account/login_register.jsp", "registrationAction": "identify", "emailAddress": self.username, "password": self.password, "submitButton": "" } if self.logged_in: logger.debug("already logged in") return logger.debug("logging in") login_url = "https://securea.mlb.com/authenticate.do" res = self.session.post(login_url, data=data, headers={"Referer": (initial_url)}) if not (self.ipid and self.fingerprint): raise MLBSessionException("Couldn't get ipid / fingerprint") logger.debug("logged in: %s" % (self.ipid)) self.save() @property def logged_in(self): logged_in_url = ("https://web-secure.mlb.com/enterworkflow.do" "?flowId=registration.newsletter&c_id=mlb") content = self.session.get(logged_in_url).text parser = lxml.etree.HTMLParser() data = lxml.etree.parse(StringIO(content), parser) if "Login/Register" in data.xpath(".//title")[0].text: return False def get_cookie(self, name): return requests.utils.dict_from_cookiejar( self.session.cookies).get(name) @property def ipid(self): return self.get_cookie("ipid") @property def fingerprint(self): return self.get_cookie("fprt") @property def api_key(self): if not self._state.get("api_key"): self.update_api_keys() return self._state.api_key @property def client_api_key(self): if not self._state.get("client_api_key"): self.update_api_keys() return self._state.client_api_key def update_api_keys(self): logger.debug("updating api keys") content = self.session.get("https://www.mlb.com/tv/g490865/").text parser = lxml.etree.HTMLParser() data = lxml.etree.parse(StringIO(content), parser) scripts = data.xpath(".//script") for script in scripts: if script.text and "apiKey" in script.text: self._state.api_key = API_KEY_RE.search( script.text).groups()[0] if script.text and "clientApiKey" in script.text: self._state.client_api_key = CLIENT_API_KEY_RE.search( script.text).groups()[0] self.save() @property def token(self): logger.debug("getting token") if not self._state.token: headers = {"x-api-key": self.api_key} response = self.session.get(TOKEN_URL_TEMPLATE.format( ipid=self.ipid, fingerprint=self.fingerprint, platform=PLATFORM), headers=headers) self._state.token = response.text return self._state.token @property def access_token(self): logger.debug("getting access token") if not self._state.access_token: headers = { "Authorization": "Bearer %s" % (self.client_api_key), "User-agent": USER_AGENT, "Accept": "application/vnd.media-service+json; version=1", "x-bamsdk-version": BAM_SDK_VERSION, "x-bamsdk-platform": PLATFORM, "origin": "https://www.mlb.com" } data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "platform": "browser", "setCookie": "false", "subject_token": self.token, "subject_token_type": "urn:ietf:params:oauth:token-type:jwt" } response = self._access_token = self.session.post(ACCESS_TOKEN_URL, data=data, headers=headers) token_response = response.json() self._state.access_token = token_response["access_token"] self.save() logger.debug("access_token: %s" % (self._state.access_token)) return self._state.access_token def content(self, game_id): return self.session.get( GAME_CONTENT_URL_TEMPLATE.format(game_id=game_id)).json() # def feed(self, game_id): # return self.session.get(GAME_FEED_URL.format(game_id=game_id)).json() @memo(region="short") def schedule( self, sport_id=None, start=None, end=None, game_type=None, team_id=None, game_id=None, ): logger.debug("getting schedule: %s, %s, %s, %s, %s, %s" % (sport_id, start, end, game_type, team_id, game_id)) url = SCHEDULE_TEMPLATE.format( sport_id=sport_id if sport_id else "", start=start.strftime("%Y-%m-%d") if start else "", end=end.strftime("%Y-%m-%d") if end else "", game_type=game_type if game_type else "", team_id=team_id if team_id else "", game_id=game_id if game_id else "") return self.session.get(url).json() @memo(region="short") def get_media(self, game_id, title="MLBTV", preferred_stream=None): logger.debug("geting media for game %d" % (game_id)) schedule = self.schedule(game_id=game_id) # raise Exception(schedule) try: game = schedule["dates"][0]["games"][0] except KeyError: logger.debug("no game data") return for epg in game["content"]["media"]["epg"]: if title in [None, epg["title"]]: for item in epg["items"]: if preferred_stream in [None, item["mediaFeedType"]]: logger.debug("found preferred stream") yield item else: if len(epg["items"]): logger.debug("using non-preferred stream") yield epg["items"][0] # raise StopIteration def get_stream(self, media_id): # try: # media = next(self.get_media(game_id)) # except StopIteration: # logger.debug("no media for stream") # return # media_id = media["mediaId"] headers = { "Authorization": self.access_token, "User-agent": USER_AGENT, "Accept": "application/vnd.media-service+json; version=1", "x-bamsdk-version": "3.0", "x-bamsdk-platform": PLATFORM, "origin": "https://www.mlb.com" } stream = self.session.get( STREAM_URL_TEMPLATE.format(media_id=media_id), headers=headers).json() logger.debug("stream response: %s" % (stream)) if "errors" in stream and len(stream["errors"]): return None return stream
class MLBSession(object): HEADERS = {"User-agent": USER_AGENT} def __init__( self, username, password, api_key=None, client_api_key=None, token=None, access_token=None, access_token_expiry=None, proxies=None, no_cache=False, ): self.session = requests.Session() self.session.cookies = LWPCookieJar() if not os.path.exists(COOKIE_FILE): self.session.cookies.save(COOKIE_FILE) self.session.cookies.load(COOKIE_FILE, ignore_discard=True) self.session.headers = self.HEADERS self._state = AttrDict([("username", username), ("password", password), ("api_key", api_key), ("client_api_key", client_api_key), ("token", token), ("access_token", access_token), ("access_token_expiry", access_token_expiry), ("proxies", proxies)]) self.no_cache = no_cache self._cache_responses = False if not os.path.exists(CACHE_FILE): self.cache_setup(CACHE_FILE) self.conn = sqlite3.connect(CACHE_FILE, detect_types=sqlite3.PARSE_DECLTYPES) self.cursor = self.conn.cursor() self.cache_purge() self.login() def __getattr__(self, attr): if attr in [ "delete", "get", "head", "options", "post", "put", "patch" ]: # return getattr(self.session, attr) session_method = getattr(self.session, attr) return functools.partial(self.request, session_method) # raise AttributeError(attr) def request(self, method, url, *args, **kwargs): response = None use_cache = not self.no_cache and self._cache_responses if use_cache: logger.debug("getting cached response for %s" % (url)) self.cursor.execute( "SELECT response, last_seen " "FROM response_cache " "WHERE url = ?", (url, )) try: (pickled_response, last_seen) = self.cursor.fetchone() td = datetime.now() - last_seen if td.seconds >= self._cache_responses: logger.debug("cache expired for %s" % (url)) else: response = pickle.loads(pickled_response) logger.debug("using cached response for %s" % (url)) except TypeError: logger.debug("no cached response for %s" % (url)) if not response: response = method(url, *args, **kwargs) if use_cache: pickled_response = pickle.dumps(response) sql = """INSERT OR REPLACE INTO response_cache (url, response, last_seen) VALUES (?, ?, ?)""" self.cursor.execute(sql, (url, pickled_response, datetime.now())) self.conn.commit() return response @property def username(self): return self._state.username @property def password(self): return self._state.password @property def proxies(self): return self._state.proxies @proxies.setter def proxies(self, value): # Override proxy environment variables if proxies are defined on session self.session.trust_env = (len(value) == 0) self._state.proxies = value self.session.proxies.update(value) @classmethod def new(cls, **kwargs): try: return cls.load() except: return cls(username=config.settings.profile.username, password=config.settings.profile.password, **kwargs) @classmethod def destroy(cls): if os.path.exists(COOKIE_FILE): os.remove(COOKIE_FILE) if os.path.exists(SESSION_FILE): os.remove(SESSION_FILE) @classmethod def load(cls): state = yaml.load(open(SESSION_FILE), Loader=AttrDictYAMLLoader) return cls(**state) def save(self): with open(SESSION_FILE, 'w') as outfile: yaml.dump(self._state, outfile, default_flow_style=False) self.session.cookies.save(COOKIE_FILE) @contextmanager def cache_responses(self, duration=CACHE_DURATION_DEFAULT): self._cache_responses = duration try: yield finally: self._cache_responses = False def cache_responses_short(self): return self.cache_responses(CACHE_DURATION_SHORT) def cache_responses_medium(self): return self.cache_responses(CACHE_DURATION_MEDIUM) def cache_responses_long(self): return self.cache_responses(CACHE_DURATION_LONG) def cache_setup(self, dbfile): conn = sqlite3.connect(dbfile) c = conn.cursor() c.execute(''' CREATE TABLE response_cache (url TEXT, response TEXT, last_seen TIMESTAMP DEFAULT (datetime('now','localtime')), PRIMARY KEY (url))''') conn.commit() c.close() def cache_purge(self, days=CACHE_DURATION_LONG): self.cursor.execute("DELETE " "FROM response_cache " "WHERE last_seen < datetime('now', '-%d days')" % (days)) def login(self): logger.debug("checking for existing log in") initial_url = ("https://secure.mlb.com/enterworkflow.do" "?flowId=registration.wizard&c_id=mlb") # res = self.get(initial_url) # if not res.status_code == 200: # raise MLBSessionException(res.content) data = { "uri": "/account/login_register.jsp", "registrationAction": "identify", "emailAddress": self.username, "password": self.password, "submitButton": "" } if self.logged_in: logger.debug("already logged in") return logger.debug("attempting new log in") login_url = "https://securea.mlb.com/authenticate.do" res = self.post(login_url, data=data, headers={"Referer": (initial_url)}) if not (self.ipid and self.fingerprint): raise MLBSessionException("Couldn't get ipid / fingerprint") logger.debug("logged in: %s" % (self.ipid)) self.save() @property def logged_in(self): logged_in_url = ("https://web-secure.mlb.com/enterworkflow.do" "?flowId=registration.newsletter&c_id=mlb") content = self.get(logged_in_url).text parser = lxml.etree.HTMLParser() data = lxml.etree.parse(StringIO(content), parser) if "Login/Register" in data.xpath(".//title")[0].text: return False def get_cookie(self, name): return requests.utils.dict_from_cookiejar( self.session.cookies).get(name) @property def ipid(self): return self.get_cookie("ipid") @property def fingerprint(self): return self.get_cookie("fprt") @property def api_key(self): if not self._state.get("api_key"): self.update_api_keys() return self._state.api_key @property def client_api_key(self): if not self._state.get("client_api_key"): self.update_api_keys() return self._state.client_api_key def update_api_keys(self): logger.debug("updating api keys") content = self.get("https://www.mlb.com/tv/g490865/").text parser = lxml.etree.HTMLParser() data = lxml.etree.parse(StringIO(content), parser) scripts = data.xpath(".//script") for script in scripts: if script.text and "apiKey" in script.text: self._state.api_key = API_KEY_RE.search( script.text).groups()[0] if script.text and "clientApiKey" in script.text: self._state.client_api_key = CLIENT_API_KEY_RE.search( script.text).groups()[0] self.save() @property def token(self): logger.debug("getting token") if not self._state.token: headers = {"x-api-key": self.api_key} response = self.get(TOKEN_URL_TEMPLATE.format( ipid=self.ipid, fingerprint=self.fingerprint, platform=PLATFORM), headers=headers) self._state.token = response.text return self._state.token @token.setter def token(self, value): self._state.token = value @property def access_token_expiry(self): if self._state.access_token_expiry: return dateutil.parser.parse(self._state.access_token_expiry) @access_token_expiry.setter def access_token_expiry(self, val): if val: self._state.access_token_expiry = val.isoformat() @property def access_token(self): if not self._state.access_token or not self.access_token_expiry or \ self.access_token_expiry < datetime.now(tz=pytz.UTC): try: self.refresh_access_token() except requests.exceptions.HTTPError: # Clear token and then try to get a new access_token self.refresh_access_token(clear_token=True) logger.debug("access_token: %s" % (self._state.access_token)) return self._state.access_token def refresh_access_token(self, clear_token=False): logger.debug("refreshing access token") if clear_token: self.token = None headers = { "Authorization": "Bearer %s" % (self.client_api_key), "User-agent": USER_AGENT, "Accept": "application/vnd.media-service+json; version=1", "x-bamsdk-version": BAM_SDK_VERSION, "x-bamsdk-platform": PLATFORM, "origin": "https://www.mlb.com" } data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "platform": "browser", "setCookie": "false", "subject_token": self.token, "subject_token_type": "urn:ietf:params:oauth:token-type:jwt" } response = self.post(ACCESS_TOKEN_URL, data=data, headers=headers) response.raise_for_status() token_response = response.json() self.access_token_expiry = datetime.now(tz=pytz.UTC) + \ timedelta(seconds=token_response["expires_in"]) self._state.access_token = token_response["access_token"] self.save() def content(self, game_id): return self.get( GAME_CONTENT_URL_TEMPLATE.format(game_id=game_id)).json() # def feed(self, game_id): # return self.get(GAME_FEED_URL.format(game_id=game_id)).json() @memo(region="short") def schedule( self, sport_id=None, start=None, end=None, game_type=None, team_id=None, game_id=None, ): logger.debug("getting schedule: %s, %s, %s, %s, %s, %s" % (sport_id, start, end, game_type, team_id, game_id)) url = SCHEDULE_TEMPLATE.format( sport_id=sport_id if sport_id else "", start=start.strftime("%Y-%m-%d") if start else "", end=end.strftime("%Y-%m-%d") if end else "", game_type=game_type if game_type else "", team_id=team_id if team_id else "", game_id=game_id if game_id else "") with self.cache_responses_short(): return self.get(url).json() @memo(region="short") def get_epgs(self, game_id, title="MLBTV"): schedule = self.schedule(game_id=game_id) try: # Get last date for games that have been rescheduled to a later date game = schedule["dates"][-1]["games"][0] except KeyError: logger.debug("no game data") return epgs = game["content"]["media"]["epg"] if not isinstance(epgs, list): epgs = [epgs] return [e for e in epgs if (not title) or title == e["title"]] def get_media(self, game_id, media_id=None, title="MLBTV", preferred_stream=None, call_letters=None): logger.debug("geting media for game %d" % (game_id)) epgs = self.get_epgs(game_id, title) for epg in epgs: for item in epg["items"]: if (not preferred_stream or (item.get("mediaFeedType", "").lower() == preferred_stream)) and (not call_letters or (item.get( "callLetters", "").lower() == call_letters)) and ( not media_id or (item.get("mediaId", "").lower() == media_id)): logger.debug("found preferred stream") yield item else: if len(epg["items"]): logger.debug("using non-preferred stream") yield epg["items"][0] # raise StopIteration def airings(self, game_id): airings_url = AIRINGS_URL_TEMPLATE.format(game_id=game_id) airings = self.get(airings_url).json()["data"]["Airings"] return airings def media_timestamps(self, game_id, media_id): try: airing = next(a for a in self.airings(game_id) if a["mediaId"] == media_id) except StopIteration: raise MLBSessionException("No airing for media %s" % (media_id)) start_timestamps = [] try: start_time = next(t["startDatetime"] for t in next( m for m in airing["milestones"] if m["milestoneType"] == "BROADCAST_START")["milestoneTime"] if t["type"] == "absolute") except StopIteration: # Some streams don't have a "BROADCAST_START" milestone. We need # something, so we use the scheduled game start time, which is # probably wrong. start_time = airing["startDate"] start_timestamps.append(("S", start_time)) try: start_offset = next(t["start"] for t in next( m for m in airing["milestones"] if m["milestoneType"] == "BROADCAST_START")["milestoneTime"] if t["type"] == "offset") except StopIteration: # Same as above. Missing BROADCAST_START milestone means we # probably don't get accurate offsets for inning milestones. start_offset = 0 start_timestamps.append(("SO", start_offset)) timestamps = AttrDict(start_timestamps) timestamps.update( AttrDict([("%s%s" % ("T" if next( k for k in m["keywords"] if k["type"] == "top")["value"] == "true" else "B", int( next(k for k in m["keywords"] if k["type"] == "inning")["value"])), next(t["start"] for t in m["milestoneTime"] if t["type"] == "offset")) for m in airing["milestones"] if m["milestoneType"] == "INNING_START"])) return timestamps def get_stream(self, media_id): # try: # media = next(self.get_media(game_id)) # except StopIteration: # logger.debug("no media for stream") # return # media_id = media["mediaId"] headers = { "Authorization": self.access_token, "User-agent": USER_AGENT, "Accept": "application/vnd.media-service+json; version=1", "x-bamsdk-version": "3.0", "x-bamsdk-platform": PLATFORM, "origin": "https://www.mlb.com" } stream_url = STREAM_URL_TEMPLATE.format(media_id=media_id) logger.info("getting stream %s" % (stream_url)) stream = self.get(stream_url, headers=headers).json() logger.debug("stream response: %s" % (stream)) if "errors" in stream and len(stream["errors"]): return None return stream
class Config(MutableMapping): def __init__(self, config_file): self._config = None self._config_file = config_file def init_config(self): from .session import MLBSession, MLBSessionException def mkdir_p(path): try: os.makedirs(path) except OSError as exc: # Python >2.5 if not (exc.errno == errno.EEXIST and os.path.isdir(path)): raise def find_players(): for p in KNOWN_PLAYERS: player = distutils.spawn.find_executable(p) if player: yield player MLBSession.destroy() if os.path.exists(CONFIG_FILE): os.remove(CONFIG_FILE) self._config = AttrDict() time_zone = None player = None mkdir_p(CONFIG_DIR) while True: self.username = prompt("MLB.tv username: "******") continue tz_local = tzlocal.get_localzone().zone # password = prompt("MLB.tv password (will be stored in clear text!): ") found_players = list(find_players()) if not found_players: print("no known media players found") else: print("found the following media players") print("\n".join([ "\t%d: %s" % (n, p) for n, p in enumerate(["My player is not listed"] + found_players) ])) choice = int( prompt( "Select the number corresponding to your preferred player,\n" "or 0 if your player is not listed: ", validator=RangeNumberValidator( maximum=len(found_players)))) if choice: player = found_players[choice - 1] while not player: response = prompt("Please enter the path to your media player: ") player = distutils.spawn.find_executable(response) if not player: print("Couldn't locate player '%s'" % (response)) player_args = prompt( "If you need to pass additional arguments to your media " "player, enter them here: ") if player_args: player = " ".join([player, player_args]) self.player = player print("Your system time zone seems to be %s." % (tz_local)) if not confirm("Is that the time zone you'd like to use? (y/n) "): while not time_zone: response = prompt("Enter your preferred time zone: ") if response in pytz.common_timezones: time_zone = response break elif confirm("Can't find time zone %s: are you sure? (y/n) "): time_zone = response break else: time_zone = tz_local self.time_zone = time_zone self.save() def load(self): if not os.path.exists(self._config_file): raise Exception("config file %s not found" % (CONFIG_FILE)) config = yaml.load(open(self._config_file), Loader=AttrDictYAMLLoader) if config.get("time_zone"): config.tz = pytz.timezone(config.time_zone) self._config = config def save(self): with open(self._config_file, 'w') as outfile: yaml.dump(self._config, outfile, default_flow_style=False) def __getattr__(self, name): return self._config.get(name, None) def __setattr__(self, name, value): if not name.startswith("_"): self._config[name] = value object.__setattr__(self, name, value) def __getitem__(self, key): return self._config[key] def __setitem__(self, key, value): self._config[key] = value def __delitem__(self, key): del self._config[key] def __len__(self): return len(self._config) def __iter__(self): return iter(self._config)