From d9218ed9a8536684a3739e2e5937247673ad5403 Mon Sep 17 00:00:00 2001 From: Joshua McKinney Date: Mon, 5 Dec 2016 02:27:40 -0600 Subject: [PATCH 1/5] Add extractor for NHL TV --- youtube_dl/extractor/extractors.py | 1 + youtube_dl/extractor/nhl.py | 131 +++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/youtube_dl/extractor/extractors.py b/youtube_dl/extractor/extractors.py index 46d007b7d..1f2347ddf 100644 --- a/youtube_dl/extractor/extractors.py +++ b/youtube_dl/extractor/extractors.py @@ -597,6 +597,7 @@ from .nhl import ( NHLNewsIE, NHLVideocenterCategoryIE, NHLIE, + NHLTVIE, ) from .nick import ( NickIE, diff --git a/youtube_dl/extractor/nhl.py b/youtube_dl/extractor/nhl.py index 62ce800c0..6413a0f48 100644 --- a/youtube_dl/extractor/nhl.py +++ b/youtube_dl/extractor/nhl.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals +import base64 import re import json import os +import time from .common import InfoExtractor from ..compat import ( @@ -14,9 +16,11 @@ from ..compat import ( from ..utils import ( unified_strdate, determine_ext, + ExtractorError, int_or_none, parse_iso8601, parse_duration, + sanitized_Request, ) @@ -349,3 +353,130 @@ class NHLIE(InfoExtractor): 'thumbnails': thumbnails, 'formats': formats, } + + +class NHLTVIE(InfoExtractor): + IE_NAME = 'nhl.com:nhltv' + _VALID_URL = r'https?://(?:www\.)?nhl.com/tv/(?P\d+)/(?:[^/]+/)*(?P\d+)' + _OAUTH_URL = 'https://user.svc.nhl.com/oauth/token?grant_type=client_credentials' + _LOGIN_URL = 'https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login' + _NETRC_MACHINE = 'nhltv' + _TEST = { + # This is a free video that can be accessed by anyone with an NHL TV login + 'url': 'https://www.nhl.com/tv/2016020321/221-1003765/46561403', + 'md5': '34d9518c495ebdad947b9723b5a7c9a9', + 'info_dict': { + 'id': '46561403', + 'ext': 'ts', + 'title': '2016-11-26: Anaheim Ducks @ San Jose Sharks (AWAY feed)', + 'timestamp': 1480217427, + }, + 'skip': 'requires a login to NHL TV, and takes a long time to run as the video is 3h+', + 'params': { + 'usenetrc': True, + 'format': '[width=400]', + 'hls_use_mpegts': True, + }, + } + + def _login(self): + # TODO cache login to avoid 'Sign-on restriction: Too many usage attempts' + (username, password) = self._get_login_info() + if username is None: + self.raise_login_required() + + authorization = base64.b64encode( + 'web_nhl-v1.0.0:2d1d846ea3b194a18ef40ac9fbce97e3'.encode('utf-8')).decode('ascii') + oauth_request = sanitized_Request( + self._OAUTH_URL, + data="", + headers={ + 'Referer': 'https://www.nhl.com/login', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Authorization': 'Basic %s' % authorization, + 'Content-Type': 'application/json', + }) + oauth_response = self._download_json( + oauth_request, + None, # video_id + 'Requesting OAuth access token', + 'Unable to get OAuth access token') + access_token = oauth_response['access_token'] + + login_data = { + 'nhlCredentials': { + 'email': username, + 'password': password, + } + } + login_request = sanitized_Request( + self._LOGIN_URL, + data=json.dumps(login_data, sort_keys=True).encode('utf-8'), + headers={ + 'Referer': 'https://www.nhl.com/login', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Authorization': access_token, + 'Content-Type': 'application/json' + }) + # sets up the cookies we need to download + self._download_webpage( + login_request, None, 'Logging in', 'Unable to log in') + + def _real_initialize(self): + self._login() + + def extract_stream_info(self, video_id): + timestamp = int(time.time()) + session_key = "abcdefghijklmnop" + stream_data_url = 'https://mf.svc.nhl.com/ws/media/mf/v2.4/stream?contentId=%s&playbackScenario=HTTP_CLOUD_WIRED_WEB&sessionKey=%s&auth=response&format=json&platform=WEB_MEDIAPLAYER&_=%s000' % (video_id, session_key, timestamp) + stream_data = self._download_json(stream_data_url, video_id, 'Downloading stream data', 'Unable to download stream data') + status_code = stream_data.get('status_code') + if status_code != 1: + # e.g. Media not found, Too many sign ons, etc. + status_message = stream_data.get('status_message') + raise ExtractorError(status_message, expected=True) + media_auth = stream_data.get('session_info').get('sessionAttributes')[0].get('attributeValue') + m3u8_url = stream_data.get('user_verified_event')[0].get('user_verified_content')[0].get('user_verified_media_item')[0].get('url') + return (media_auth, m3u8_url) + + def extract_game_info(self, video_id, game_id): + game_data_url = 'https://statsapi.web.nhl.com/api/v1/schedule?gamePk=%s&expand=schedule.game.content.media.milestones&expand=schedule.game.content.media.epg&expand=schedule.venue' % game_id + game_data = self._download_json(game_data_url, video_id, 'Downloading game data', 'Unable to download game data') + game_date = game_data.get('dates')[0] + date = game_date.get('date') # yyyy-mm-dd + game = game_date.get('games')[0] + teams = game.get('teams') + away = teams.get('away').get('team').get('name') + home = teams.get('home').get('team').get('name') + feed_type = "UNKNOWN" + media_node = game.get("content").get("media") + for epg_item in media_node.get("epg", []): + if epg_item.get("title") != "NHLTV": + continue + for item in epg_item.get('items', []): + if item.get('mediaPlaybackId') != video_id: + continue + feed_type = item.get('mediaFeedType') + timestamp = parse_iso8601(media_node.get('milestones').get('streamStart')) + title = "%s: %s @ %s (%s feed)" % (date, away, home, feed_type) + return (title, timestamp) + + def _real_extract(self, url): + mobj = re.match(self._VALID_URL, url) + video_id, game_id = mobj.group('id'), mobj.group('gameId') + + title, timestamp = self.extract_game_info(video_id, game_id) + media_auth, m3u8_url = self.extract_stream_info(video_id) + + # media auth cookie is required for the downloader + self._set_cookie('nhl.com', 'mediaAuth_v2', media_auth) + formats = self._extract_m3u8_formats(m3u8_url, video_id, 'ts', m3u8_id='hls') + self._check_formats(formats, video_id) + self._sort_formats(formats, ('preference', 'width', 'height', 'tbr', 'format_id')) + + return { + 'id': video_id, + 'title': title, + 'timestamp': timestamp, + 'formats': formats, + } From 7d4af4d53c575f0bee217836c7a695051ea7b125 Mon Sep 17 00:00:00 2001 From: Joshua McKinney Date: Fri, 20 Jan 2017 19:42:31 -0600 Subject: [PATCH 2/5] [nhl] get 60fps videos and default first media item --- youtube_dl/extractor/nhl.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/youtube_dl/extractor/nhl.py b/youtube_dl/extractor/nhl.py index 6413a0f48..111be6344 100644 --- a/youtube_dl/extractor/nhl.py +++ b/youtube_dl/extractor/nhl.py @@ -357,11 +357,11 @@ class NHLIE(InfoExtractor): class NHLTVIE(InfoExtractor): IE_NAME = 'nhl.com:nhltv' - _VALID_URL = r'https?://(?:www\.)?nhl.com/tv/(?P\d+)/(?:[^/]+/)*(?P\d+)' + _VALID_URL = r'https?://(?:www\.)?nhl.com/tv/(?P\d+)(/[^/]+)*(/(?P\d+))?' _OAUTH_URL = 'https://user.svc.nhl.com/oauth/token?grant_type=client_credentials' _LOGIN_URL = 'https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login' _NETRC_MACHINE = 'nhltv' - _TEST = { + _TESTS = [{ # This is a free video that can be accessed by anyone with an NHL TV login 'url': 'https://www.nhl.com/tv/2016020321/221-1003765/46561403', 'md5': '34d9518c495ebdad947b9723b5a7c9a9', @@ -376,8 +376,14 @@ class NHLTVIE(InfoExtractor): 'usenetrc': True, 'format': '[width=400]', 'hls_use_mpegts': True, - }, - } + } + }, { + 'url': 'https://www.nhl.com/tv/2016020362/221-1003808', + 'only_matching': True, + }, { + 'url': 'https://www.nhl.com/tv/2016020373', + 'only_matching': True, + }] def _login(self): # TODO cache login to avoid 'Sign-on restriction: Too many usage attempts' @@ -454,19 +460,25 @@ class NHLTVIE(InfoExtractor): if epg_item.get("title") != "NHLTV": continue for item in epg_item.get('items', []): - if item.get('mediaPlaybackId') != video_id: - continue - feed_type = item.get('mediaFeedType') + if item.get('mediaPlaybackId') == video_id or video_id == None: + feed_type = item.get('mediaFeedType') + video_id = item.get('mediaPlaybackId') timestamp = parse_iso8601(media_node.get('milestones').get('streamStart')) title = "%s: %s @ %s (%s feed)" % (date, away, home, feed_type) - return (title, timestamp) + return (video_id, title, timestamp) + + + def get_60fps_playlist(self, url): + """Returns a modified url that adds a 60 fps broadcast""" + return re.sub('_wired_web', '_wired60', url) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) video_id, game_id = mobj.group('id'), mobj.group('gameId') - title, timestamp = self.extract_game_info(video_id, game_id) + video_id, title, timestamp = self.extract_game_info(video_id, game_id) media_auth, m3u8_url = self.extract_stream_info(video_id) + m3u8_url = self.get_60fps_playlist(m3u8_url) # media auth cookie is required for the downloader self._set_cookie('nhl.com', 'mediaAuth_v2', media_auth) From 379cf9472683da96a65397604df19fdc3eecebed Mon Sep 17 00:00:00 2001 From: Joshua McKinney Date: Fri, 20 Jan 2017 19:44:08 -0600 Subject: [PATCH 3/5] [nhl] add --auth-provider option to support rogers logins Addresses https://github.com/rg3/youtube-dl/pull/11366#issuecomment-271127084 --- README.md | 1 + youtube_dl/YoutubeDL.py | 1 + youtube_dl/__init__.py | 1 + youtube_dl/extractor/common.py | 10 ++++++++ youtube_dl/extractor/nhl.py | 43 ++++++++++++++++++++++++++++------ youtube_dl/options.py | 4 ++++ 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ea9131c3a..b75750858 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,7 @@ which means you can modify it, redistribute it or use it however you like. -2, --twofactor TWOFACTOR Two-factor auth code -n, --netrc Use .netrc authentication data --video-password PASSWORD Video password (vimeo, smotri, youku) + --auth-provider PROVIDER Authentication provider (nhl, rogers) ## Adobe Pass Options: --ap-mso MSO Adobe Pass multiple-system operator (TV diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index 53f20ac2c..d0357ea88 100755 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -131,6 +131,7 @@ class YoutubeDL(object): username: Username for authentication purposes. password: Password for authentication purposes. videopassword: Password for accessing a video. + auth_provider: Authentication provider ap_mso: Adobe Pass multiple-system operator identifier. ap_username: Multiple-system operator account username. ap_password: Multiple-system operator account password. diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 6850d95e1..fe58d1fea 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -300,6 +300,7 @@ def _real_main(argv=None): 'password': opts.password, 'twofactor': opts.twofactor, 'videopassword': opts.videopassword, + 'auth_provider': opts.auth_provider, 'ap_mso': opts.ap_mso, 'ap_username': opts.ap_username, 'ap_password': opts.ap_password, diff --git a/youtube_dl/extractor/common.py b/youtube_dl/extractor/common.py index 05c51fac9..b18f51c20 100644 --- a/youtube_dl/extractor/common.py +++ b/youtube_dl/extractor/common.py @@ -715,6 +715,16 @@ class InfoExtractor(object): return username, password + def _get_auth_provider(self): + """ + Get the authentication provider (e.g. nhl / rogers) + """ + if self._downloader is None: + return None + + downloader_params = self._downloader.params + return downloader_params.get('auth_provider') + def _get_tfa_info(self, note='two-factor verification code'): """ Get the two-factor authentication info diff --git a/youtube_dl/extractor/nhl.py b/youtube_dl/extractor/nhl.py index 111be6344..46c699f07 100644 --- a/youtube_dl/extractor/nhl.py +++ b/youtube_dl/extractor/nhl.py @@ -359,7 +359,8 @@ class NHLTVIE(InfoExtractor): IE_NAME = 'nhl.com:nhltv' _VALID_URL = r'https?://(?:www\.)?nhl.com/tv/(?P\d+)(/[^/]+)*(/(?P\d+))?' _OAUTH_URL = 'https://user.svc.nhl.com/oauth/token?grant_type=client_credentials' - _LOGIN_URL = 'https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login' + _NHL_LOGIN_URL = 'https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login' + _ROGERS_LOGIN_URL = 'https://activation-rogers.svc.nhl.com/ws/subscription/flow/rogers.login-check' _NETRC_MACHINE = 'nhltv' _TESTS = [{ # This is a free video that can be accessed by anyone with an NHL TV login @@ -409,24 +410,52 @@ class NHLTVIE(InfoExtractor): 'Unable to get OAuth access token') access_token = oauth_response['access_token'] + auth_provider = self._get_auth_provider() + if auth_provider == 'rogers': + login_request = self._create_rogers_login_request(username, password, access_token) + elif auth_provider == 'nhl' or auth_provider is None: + login_request = self._create_nhl_login_request(username, password, access_token) + else: + raise ExtractorError('Unknown authentication provider: %s. Valid values are nhl, rogers' % auth_provider) + + # sets up the cookies we need to download + self._download_webpage( + login_request, None, 'Logging in', 'Unable to log in') + + def _create_nhl_login_request(self, username, password, access_token): login_data = { 'nhlCredentials': { 'email': username, 'password': password, } } - login_request = sanitized_Request( - self._LOGIN_URL, + return sanitized_Request( + self._NHL_LOGIN_URL, data=json.dumps(login_data, sort_keys=True).encode('utf-8'), headers={ - 'Referer': 'https://www.nhl.com/login', + 'Referer': 'https://www.nhl.com/login/nhl', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Authorization': access_token, + 'Content-Type': 'application/json' + }) + + + def _create_rogers_login_request(self, username, password, access_token): + login_data = { + 'rogerCredentials': { + 'email': username, + 'password': password, + } + } + return sanitized_Request( + self._ROGERS_LOGIN_URL, + data=json.dumps(login_data, sort_keys=True).encode('utf-8'), + headers={ + 'Referer': 'https://www.nhl.com/login/rogers', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Authorization': access_token, 'Content-Type': 'application/json' }) - # sets up the cookies we need to download - self._download_webpage( - login_request, None, 'Logging in', 'Unable to log in') def _real_initialize(self): self._login() diff --git a/youtube_dl/options.py b/youtube_dl/options.py index 53497fbc6..0c39cff80 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -350,6 +350,10 @@ def parseOpts(overrideArguments=None): '--video-password', dest='videopassword', metavar='PASSWORD', help='Video password (vimeo, smotri, youku)') + authentication.add_option( + '--auth-provider', + dest='auth_provider', metavar='AUTH_PROVIDER', + help='Authentication provider (nhl, rogers, ...)') adobe_pass = optparse.OptionGroup(parser, 'Adobe Pass Options') adobe_pass.add_option( From d044355fe0e27500b294905b62b317b349f1ab29 Mon Sep 17 00:00:00 2001 From: Joshua McKinney Date: Wed, 25 Jan 2017 16:41:21 -0600 Subject: [PATCH 4/5] [nhl] Fix flake8 issues --- youtube_dl/extractor/nhl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/youtube_dl/extractor/nhl.py b/youtube_dl/extractor/nhl.py index 46c699f07..e949c4276 100644 --- a/youtube_dl/extractor/nhl.py +++ b/youtube_dl/extractor/nhl.py @@ -439,7 +439,6 @@ class NHLTVIE(InfoExtractor): 'Content-Type': 'application/json' }) - def _create_rogers_login_request(self, username, password, access_token): login_data = { 'rogerCredentials': { @@ -489,14 +488,13 @@ class NHLTVIE(InfoExtractor): if epg_item.get("title") != "NHLTV": continue for item in epg_item.get('items', []): - if item.get('mediaPlaybackId') == video_id or video_id == None: + if item.get('mediaPlaybackId') == video_id or video_id is None: feed_type = item.get('mediaFeedType') video_id = item.get('mediaPlaybackId') timestamp = parse_iso8601(media_node.get('milestones').get('streamStart')) title = "%s: %s @ %s (%s feed)" % (date, away, home, feed_type) return (video_id, title, timestamp) - def get_60fps_playlist(self, url): """Returns a modified url that adds a 60 fps broadcast""" return re.sub('_wired_web', '_wired60', url) From 34266aa604b6f4ca2bddbe4676d5a78af1efbfbb Mon Sep 17 00:00:00 2001 From: Joshua McKinney Date: Thu, 26 Jan 2017 05:19:47 -0600 Subject: [PATCH 5/5] [nhl] Cache authentication between runs This hopefully solves the sign-on restriction error messages when a user attempts to login too many times. Refactored API calls to a NHLApi class and reduced repetition Made timestamp an optional field Made non-optional parsing use [] instead of get() (e.g. everything that produces the title / url / id) --- youtube_dl/extractor/nhl.py | 263 ++++++++++++++++++++---------------- 1 file changed, 148 insertions(+), 115 deletions(-) diff --git a/youtube_dl/extractor/nhl.py b/youtube_dl/extractor/nhl.py index e949c4276..5d78f6c18 100644 --- a/youtube_dl/extractor/nhl.py +++ b/youtube_dl/extractor/nhl.py @@ -8,6 +8,7 @@ import time from .common import InfoExtractor from ..compat import ( + compat_cookies, compat_urlparse, compat_urllib_parse_urlencode, compat_urllib_parse_urlparse, @@ -355,15 +356,99 @@ class NHLIE(InfoExtractor): } +class NHLApi: + def __init__(self, extractor, auth=None): + self.extractor = extractor + self.auth = auth + if auth: + extractor.to_screen("Using cached credentials. Use the --rm-cache-dir option to remove.") + cookie = compat_cookies.SimpleCookie(auth.encode('utf8', 'replace')) + auth_cookie = cookie['Authorization'] + # TODO handle cookie expiry + extractor._set_cookie('nhl.com', 'Authorization', auth_cookie.value) + + def login(self, username, password, auth_provider='nhl'): + if auth_provider not in ['nhl', 'rogers']: + raise ExtractorError('Unknown authentication provider: %s. Valid values are nhl, rogers' % auth_provider) + + access_token = self._get_oauth_access_token() + + if auth_provider == 'nhl': + url = 'https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login' + credentials = { + 'nhlCredentials': { + 'email': username, + 'password': password, + } + } + referrer = 'https://www.nhl.com/login/nhl' + elif auth_provider == 'rogers': + url = 'https://activation-rogers.svc.nhl.com/ws/subscription/flow/rogers.login-check' + credentials = { + 'rogerCredentials': { + 'email': username, + 'password': password, + } + } + referrer = 'https://www.nhl.com/login/rogers' + + login_request = sanitized_Request( + url, + data=json.dumps(credentials, sort_keys=True).encode('utf-8'), + headers={ + 'Referer': referrer, + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Authorization': access_token, + 'Content-Type': 'application/json' + }) + self.extractor._download_webpage( + login_request, None, 'Logging in', 'Unable to log in') + # TODO this doesn't extract the cookie expiry from the cookie correctly + self.auth = self.extractor._get_cookies('http://nhl.com').get('Authorization').output() + + def _get_oauth_access_token(self): + authorization = base64.b64encode( + 'web_nhl-v1.0.0:2d1d846ea3b194a18ef40ac9fbce97e3'.encode('utf-8') + ).decode('ascii') + oauth_request = sanitized_Request( + 'https://user.svc.nhl.com/oauth/token?grant_type=client_credentials', + data='', + headers={ + 'Referer': 'https://www.nhl.com/login', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Authorization': 'Basic %s' % authorization, + 'Content-Type': 'application/json', + }) + oauth_response = self.extractor._download_json( + oauth_request, + None, # video_id + 'Requesting OAuth access token', + 'Unable to get OAuth access token') + return oauth_response['access_token'] + + def get_game_data(self, video_id, game_id): + game_data_url = 'https://statsapi.web.nhl.com/api/v1/schedule?gamePk=%s&expand=schedule.game.content.media.milestones&expand=schedule.game.content.media.epg&expand=schedule.venue' % game_id + return self.extractor._download_json( + game_data_url, + video_id, + 'Downloading game data', + 'Unable to download game data') + + def get_stream_data(self, video_id): + stream_data_url = 'https://mf.svc.nhl.com/ws/media/mf/v2.4/stream?contentId=%s&playbackScenario=HTTP_CLOUD_WIRED_WEB&sessionKey=%s&auth=response&format=json&platform=WEB_MEDIAPLAYER&_=%s000' + session_key = 'abcdefghijklmnop' + timestamp = int(time.time()) + url = stream_data_url % (video_id, session_key, timestamp) + return self.extractor._download_json(url, video_id, 'Downloading stream data', 'Unable to download stream data') + + class NHLTVIE(InfoExtractor): IE_NAME = 'nhl.com:nhltv' _VALID_URL = r'https?://(?:www\.)?nhl.com/tv/(?P\d+)(/[^/]+)*(/(?P\d+))?' - _OAUTH_URL = 'https://user.svc.nhl.com/oauth/token?grant_type=client_credentials' - _NHL_LOGIN_URL = 'https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login' - _ROGERS_LOGIN_URL = 'https://activation-rogers.svc.nhl.com/ws/subscription/flow/rogers.login-check' _NETRC_MACHINE = 'nhltv' + _NHLTV_CACHE = 'nhltv' _TESTS = [{ - # This is a free video that can be accessed by anyone with an NHL TV login + # This is a free video that anyone with an NHL TV login can access 'url': 'https://www.nhl.com/tv/2016020321/221-1003765/46561403', 'md5': '34d9518c495ebdad947b9723b5a7c9a9', 'info_dict': { @@ -386,128 +471,31 @@ class NHLTVIE(InfoExtractor): 'only_matching': True, }] + def _real_initialize(self): + auth = self._downloader.cache.load(self._NHLTV_CACHE, 'auth') + self.api = NHLApi(self, auth) + if auth is None: + self._login() + def _login(self): - # TODO cache login to avoid 'Sign-on restriction: Too many usage attempts' (username, password) = self._get_login_info() if username is None: self.raise_login_required() - - authorization = base64.b64encode( - 'web_nhl-v1.0.0:2d1d846ea3b194a18ef40ac9fbce97e3'.encode('utf-8')).decode('ascii') - oauth_request = sanitized_Request( - self._OAUTH_URL, - data="", - headers={ - 'Referer': 'https://www.nhl.com/login', - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Authorization': 'Basic %s' % authorization, - 'Content-Type': 'application/json', - }) - oauth_response = self._download_json( - oauth_request, - None, # video_id - 'Requesting OAuth access token', - 'Unable to get OAuth access token') - access_token = oauth_response['access_token'] - - auth_provider = self._get_auth_provider() - if auth_provider == 'rogers': - login_request = self._create_rogers_login_request(username, password, access_token) - elif auth_provider == 'nhl' or auth_provider is None: - login_request = self._create_nhl_login_request(username, password, access_token) - else: - raise ExtractorError('Unknown authentication provider: %s. Valid values are nhl, rogers' % auth_provider) - - # sets up the cookies we need to download - self._download_webpage( - login_request, None, 'Logging in', 'Unable to log in') - - def _create_nhl_login_request(self, username, password, access_token): - login_data = { - 'nhlCredentials': { - 'email': username, - 'password': password, - } - } - return sanitized_Request( - self._NHL_LOGIN_URL, - data=json.dumps(login_data, sort_keys=True).encode('utf-8'), - headers={ - 'Referer': 'https://www.nhl.com/login/nhl', - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Authorization': access_token, - 'Content-Type': 'application/json' - }) - - def _create_rogers_login_request(self, username, password, access_token): - login_data = { - 'rogerCredentials': { - 'email': username, - 'password': password, - } - } - return sanitized_Request( - self._ROGERS_LOGIN_URL, - data=json.dumps(login_data, sort_keys=True).encode('utf-8'), - headers={ - 'Referer': 'https://www.nhl.com/login/rogers', - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Authorization': access_token, - 'Content-Type': 'application/json' - }) - - def _real_initialize(self): - self._login() - - def extract_stream_info(self, video_id): - timestamp = int(time.time()) - session_key = "abcdefghijklmnop" - stream_data_url = 'https://mf.svc.nhl.com/ws/media/mf/v2.4/stream?contentId=%s&playbackScenario=HTTP_CLOUD_WIRED_WEB&sessionKey=%s&auth=response&format=json&platform=WEB_MEDIAPLAYER&_=%s000' % (video_id, session_key, timestamp) - stream_data = self._download_json(stream_data_url, video_id, 'Downloading stream data', 'Unable to download stream data') - status_code = stream_data.get('status_code') - if status_code != 1: - # e.g. Media not found, Too many sign ons, etc. - status_message = stream_data.get('status_message') - raise ExtractorError(status_message, expected=True) - media_auth = stream_data.get('session_info').get('sessionAttributes')[0].get('attributeValue') - m3u8_url = stream_data.get('user_verified_event')[0].get('user_verified_content')[0].get('user_verified_media_item')[0].get('url') - return (media_auth, m3u8_url) - - def extract_game_info(self, video_id, game_id): - game_data_url = 'https://statsapi.web.nhl.com/api/v1/schedule?gamePk=%s&expand=schedule.game.content.media.milestones&expand=schedule.game.content.media.epg&expand=schedule.venue' % game_id - game_data = self._download_json(game_data_url, video_id, 'Downloading game data', 'Unable to download game data') - game_date = game_data.get('dates')[0] - date = game_date.get('date') # yyyy-mm-dd - game = game_date.get('games')[0] - teams = game.get('teams') - away = teams.get('away').get('team').get('name') - home = teams.get('home').get('team').get('name') - feed_type = "UNKNOWN" - media_node = game.get("content").get("media") - for epg_item in media_node.get("epg", []): - if epg_item.get("title") != "NHLTV": - continue - for item in epg_item.get('items', []): - if item.get('mediaPlaybackId') == video_id or video_id is None: - feed_type = item.get('mediaFeedType') - video_id = item.get('mediaPlaybackId') - timestamp = parse_iso8601(media_node.get('milestones').get('streamStart')) - title = "%s: %s @ %s (%s feed)" % (date, away, home, feed_type) - return (video_id, title, timestamp) - - def get_60fps_playlist(self, url): - """Returns a modified url that adds a 60 fps broadcast""" - return re.sub('_wired_web', '_wired60', url) + auth_provider = self._get_auth_provider() or 'nhl' + self.report_login() + self.api.login(username, password, auth_provider) + self._downloader.cache.store(self._NHLTV_CACHE, 'auth', self.api.auth) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) video_id, game_id = mobj.group('id'), mobj.group('gameId') - + # At this point we may have a game_id without a video_id. The next call + # ensures that we get a video_id for the desired game. video_id, title, timestamp = self.extract_game_info(video_id, game_id) - media_auth, m3u8_url = self.extract_stream_info(video_id) + m3u8_url, media_auth = self.extract_playlist_url_and_auth(video_id) m3u8_url = self.get_60fps_playlist(m3u8_url) - # media auth cookie is required for the downloader + # media auth cookie authenticates the specific download url self._set_cookie('nhl.com', 'mediaAuth_v2', media_auth) formats = self._extract_m3u8_formats(m3u8_url, video_id, 'ts', m3u8_id='hls') self._check_formats(formats, video_id) @@ -519,3 +507,48 @@ class NHLTVIE(InfoExtractor): 'timestamp': timestamp, 'formats': formats, } + + def extract_game_info(self, video_id, game_id): + """calls the nhl api to get the video_id, title, and optionally the timestamp of the start of the game""" + game_data = self.api.get_game_data(video_id, game_id) + game_date = game_data['dates'][0] + date = game_date['date'] # yyyy-mm-dd + game = game_date['games'][0] + teams = game['teams'] + away = teams['away']['team']['name'] + home = teams['home']['team']['name'] + feed_type = "UNKNOWN" + media = game['content']['media'] + for epg_item in media['epg']: + # ignore audio feeds and highlights + if epg_item.get('title') != 'NHLTV': + continue + for item in epg_item.get('items', []): + # get the specified video feed based on the video id or the + # first video if there no video id is specified + if item.get('mediaPlaybackId') == video_id or video_id is None: + video_id = item.get('mediaPlaybackId') + feed_type = item.get('mediaFeedType') # HOME / AWAY + title = "%s: %s @ %s (%s feed)" % (date, away, home, feed_type) + + streamStart = media.get('milestones', {}).get('streamStart') + if streamStart: + timestamp = parse_iso8601(streamStart) + + return (video_id, title, timestamp) + + def extract_playlist_url_and_auth(self, video_id): + """Calls the nhl api to get the url of the video and an authorization key""" + stream_data = self.api.get_stream_data(video_id) + status_code = stream_data['status_code'] + if status_code != 1: + # e.g. Media not found, Too many sign ons, etc. + status_message = stream_data['status_message'] + raise ExtractorError(status_message, expected=True) + m3u8_url = stream_data['user_verified_event'][0]['user_verified_content'][0]['user_verified_media_item'][0]['url'] + media_auth = stream_data['session_info']['sessionAttributes'][0]['attributeValue'] + return (m3u8_url, media_auth) + + def get_60fps_playlist(self, url): + """Returns a modified url that adds a 720p 60fps broadcast""" + return re.sub('_wired_web', '_wired60', url)