| 
									
										
										
										
											2014-12-06 21:24:20 +06:00
										 |  |  | # coding: utf-8 | 
					
						
							| 
									
										
										
										
											2014-04-05 17:56:36 +02:00
										 |  |  | from __future__ import unicode_literals | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-07-25 11:46:51 +02:00
										 |  |  | import itertools | 
					
						
							| 
									
										
										
										
											2013-06-23 22:07:27 +02:00
										 |  |  | import re | 
					
						
							| 
									
										
										
										
											2015-01-22 23:34:40 +06:00
										 |  |  | import random | 
					
						
							| 
									
										
										
										
											2013-06-23 22:07:27 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | from .common import InfoExtractor | 
					
						
							| 
									
										
										
										
											2014-12-13 12:24:42 +01:00
										 |  |  | from ..compat import ( | 
					
						
							| 
									
										
										
										
											2016-08-21 22:45:50 +07:00
										 |  |  |     compat_HTTPError, | 
					
						
							| 
									
										
										
										
											2015-08-03 01:54:21 +02:00
										 |  |  |     compat_parse_qs, | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |     compat_str, | 
					
						
							| 
									
										
										
										
											2016-03-26 01:46:57 +06:00
										 |  |  |     compat_urllib_parse_urlencode, | 
					
						
							| 
									
										
										
										
											2015-08-03 01:54:21 +02:00
										 |  |  |     compat_urllib_parse_urlparse, | 
					
						
							| 
									
										
										
										
											2014-12-13 12:24:42 +01:00
										 |  |  | ) | 
					
						
							|  |  |  | from ..utils import ( | 
					
						
							| 
									
										
										
										
											2016-08-21 22:45:50 +07:00
										 |  |  |     clean_html, | 
					
						
							| 
									
										
										
										
											2013-06-23 22:07:27 +02:00
										 |  |  |     ExtractorError, | 
					
						
							| 
									
										
										
										
											2015-08-17 20:20:04 +06:00
										 |  |  |     int_or_none, | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  |     js_to_json, | 
					
						
							| 
									
										
										
										
											2016-03-03 22:11:26 +06:00
										 |  |  |     orderedSet, | 
					
						
							| 
									
										
										
										
											2015-08-03 01:54:21 +02:00
										 |  |  |     parse_duration, | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  |     parse_iso8601, | 
					
						
							| 
									
										
										
										
											2016-12-24 22:10:54 +07:00
										 |  |  |     update_url_query, | 
					
						
							| 
									
										
										
										
											2016-03-26 02:19:24 +06:00
										 |  |  |     urlencode_postdata, | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  |     urljoin, | 
					
						
							| 
									
										
										
										
											2013-06-23 22:07:27 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | class TwitchBaseIE(InfoExtractor): | 
					
						
							| 
									
										
										
										
											2017-09-15 22:18:38 +07:00
										 |  |  |     _VALID_URL_BASE = r'https?://(?:(?:www|go)\.)?twitch\.tv' | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-17 22:58:18 +07:00
										 |  |  |     _API_BASE = 'https://api.twitch.tv' | 
					
						
							| 
									
										
										
										
											2016-07-01 23:16:43 +07:00
										 |  |  |     _USHER_BASE = 'https://usher.ttvnw.net' | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  |     _LOGIN_URL = 'https://www.twitch.tv/login' | 
					
						
							| 
									
										
										
										
											2016-09-15 20:58:02 +07:00
										 |  |  |     _CLIENT_ID = 'jzkbprff40iqj646a697cyrvl0zt2m6' | 
					
						
							| 
									
										
										
										
											2015-03-03 12:59:17 +01:00
										 |  |  |     _NETRC_MACHINE = 'twitch' | 
					
						
							| 
									
										
										
										
											2013-06-23 22:07:27 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  |     def _handle_error(self, response): | 
					
						
							|  |  |  |         if not isinstance(response, dict): | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         error = response.get('error') | 
					
						
							|  |  |  |         if error: | 
					
						
							|  |  |  |             raise ExtractorError( | 
					
						
							|  |  |  |                 '%s returned error: %s - %s' % (self.IE_NAME, error, response.get('message')), | 
					
						
							|  |  |  |                 expected=True) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |     def _call_api(self, path, item_id, note): | 
					
						
							|  |  |  |         response = self._download_json( | 
					
						
							| 
									
										
										
										
											2016-09-15 20:58:02 +07:00
										 |  |  |             '%s/%s' % (self._API_BASE, path), item_id, note, | 
					
						
							|  |  |  |             headers={'Client-ID': self._CLIENT_ID}) | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  |         self._handle_error(response) | 
					
						
							|  |  |  |         return response | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     def _real_initialize(self): | 
					
						
							|  |  |  |         self._login() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _login(self): | 
					
						
							|  |  |  |         (username, password) = self._get_login_info() | 
					
						
							|  |  |  |         if username is None: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-21 22:45:50 +07:00
										 |  |  |         def fail(message): | 
					
						
							|  |  |  |             raise ExtractorError( | 
					
						
							|  |  |  |                 'Unable to login. Twitch said: %s' % message, expected=True) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-05 02:06:33 +07:00
										 |  |  |         def login_step(page, urlh, note, data): | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  |             form = self._hidden_inputs(page) | 
					
						
							|  |  |  |             form.update(data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             page_url = urlh.geturl() | 
					
						
							|  |  |  |             post_url = self._search_regex( | 
					
						
							|  |  |  |                 r'<form[^>]+action=(["\'])(?P<url>.+?)\1', page, | 
					
						
							|  |  |  |                 'post url', default=page_url, group='url') | 
					
						
							|  |  |  |             post_url = urljoin(page_url, post_url) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             headers = {'Referer': page_url} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 response = self._download_json( | 
					
						
							|  |  |  |                     post_url, None, note, | 
					
						
							|  |  |  |                     data=urlencode_postdata(form), | 
					
						
							|  |  |  |                     headers=headers) | 
					
						
							|  |  |  |             except ExtractorError as e: | 
					
						
							|  |  |  |                 if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400: | 
					
						
							|  |  |  |                     response = self._parse_json( | 
					
						
							|  |  |  |                         e.cause.read().decode('utf-8'), None) | 
					
						
							| 
									
										
										
										
											2018-01-16 22:34:16 +07:00
										 |  |  |                     fail(response.get('message') or response['errors'][0]) | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-16 22:34:16 +07:00
										 |  |  |             if 'Authenticated successfully' in response.get('message', ''): | 
					
						
							|  |  |  |                 return None, None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             redirect_url = urljoin( | 
					
						
							|  |  |  |                 post_url, | 
					
						
							|  |  |  |                 response.get('redirect') or response['redirect_path']) | 
					
						
							| 
									
										
										
										
											2017-03-05 02:06:33 +07:00
										 |  |  |             return self._download_webpage_handle( | 
					
						
							|  |  |  |                 redirect_url, None, 'Downloading login redirect page', | 
					
						
							|  |  |  |                 headers=headers) | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-10-17 18:28:21 +06:00
										 |  |  |         login_page, handle = self._download_webpage_handle( | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             self._LOGIN_URL, None, 'Downloading login page') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-21 22:45:50 +07:00
										 |  |  |         # Some TOR nodes and public proxies are blocked completely | 
					
						
							|  |  |  |         if 'blacklist_message' in login_page: | 
					
						
							|  |  |  |             fail(clean_html(login_page)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-05 02:06:33 +07:00
										 |  |  |         redirect_page, handle = login_step( | 
					
						
							| 
									
										
										
										
											2017-11-11 20:49:03 +07:00
										 |  |  |             login_page, handle, 'Logging in', { | 
					
						
							| 
									
										
										
										
											2017-03-05 02:06:33 +07:00
										 |  |  |                 'username': username, | 
					
						
							|  |  |  |                 'password': password, | 
					
						
							| 
									
										
										
										
											2017-03-07 15:34:06 +08:00
										 |  |  |             }) | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-16 22:34:16 +07:00
										 |  |  |         # Successful login | 
					
						
							|  |  |  |         if not redirect_page: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  |         if re.search(r'(?i)<form[^>]+id="two-factor-submit"', redirect_page) is not None: | 
					
						
							|  |  |  |             # TODO: Add mechanism to request an SMS or phone call | 
					
						
							|  |  |  |             tfa_token = self._get_tfa_info('two-factor authentication token') | 
					
						
							| 
									
										
										
										
											2017-03-05 02:06:33 +07:00
										 |  |  |             login_step(redirect_page, handle, 'Submitting TFA token', { | 
					
						
							| 
									
										
										
										
											2017-02-16 17:46:54 +11:00
										 |  |  |                 'authy_token': tfa_token, | 
					
						
							|  |  |  |                 'remember_2fa': 'true', | 
					
						
							| 
									
										
										
										
											2017-03-05 02:06:33 +07:00
										 |  |  |             }) | 
					
						
							| 
									
										
										
										
											2015-07-10 21:16:42 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-06 10:53:49 +01:00
										 |  |  |     def _prefer_source(self, formats): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             source = next(f for f in formats if f['format_id'] == 'Source') | 
					
						
							|  |  |  |             source['preference'] = 10 | 
					
						
							|  |  |  |         except StopIteration: | 
					
						
							|  |  |  |             pass  # No Source stream present | 
					
						
							|  |  |  |         self._sort_formats(formats) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  | class TwitchItemBaseIE(TwitchBaseIE): | 
					
						
							|  |  |  |     def _download_info(self, item, item_id): | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |         return self._extract_info(self._call_api( | 
					
						
							|  |  |  |             'kraken/videos/%s%s' % (item, item_id), item_id, | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             'Downloading %s info JSON' % self._ITEM_TYPE)) | 
					
						
							| 
									
										
										
										
											2014-12-18 21:25:42 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     def _extract_media(self, item_id): | 
					
						
							|  |  |  |         info = self._download_info(self._ITEM_SHORTCUT, item_id) | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |         response = self._call_api( | 
					
						
							|  |  |  |             'api/videos/%s%s' % (self._ITEM_SHORTCUT, item_id), item_id, | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             'Downloading %s playlist JSON' % self._ITEM_TYPE) | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  |         entries = [] | 
					
						
							|  |  |  |         chunks = response['chunks'] | 
					
						
							|  |  |  |         qualities = list(chunks.keys()) | 
					
						
							|  |  |  |         for num, fragment in enumerate(zip(*chunks.values()), start=1): | 
					
						
							|  |  |  |             formats = [] | 
					
						
							|  |  |  |             for fmt_num, fragment_fmt in enumerate(fragment): | 
					
						
							|  |  |  |                 format_id = qualities[fmt_num] | 
					
						
							|  |  |  |                 fmt = { | 
					
						
							|  |  |  |                     'url': fragment_fmt['url'], | 
					
						
							|  |  |  |                     'format_id': format_id, | 
					
						
							|  |  |  |                     'quality': 1 if format_id == 'live' else 0, | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 m = re.search(r'^(?P<height>\d+)[Pp]', format_id) | 
					
						
							|  |  |  |                 if m: | 
					
						
							|  |  |  |                     fmt['height'] = int(m.group('height')) | 
					
						
							|  |  |  |                 formats.append(fmt) | 
					
						
							|  |  |  |             self._sort_formats(formats) | 
					
						
							|  |  |  |             entry = dict(info) | 
					
						
							| 
									
										
										
										
											2014-10-18 21:57:24 +07:00
										 |  |  |             entry['id'] = '%s_%d' % (entry['id'], num) | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  |             entry['title'] = '%s part %d' % (entry['title'], num) | 
					
						
							|  |  |  |             entry['formats'] = formats | 
					
						
							|  |  |  |             entries.append(entry) | 
					
						
							| 
									
										
										
										
											2014-10-17 23:46:53 +07:00
										 |  |  |         return self.playlist_result(entries, info['id'], info['title']) | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _extract_info(self, info): | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             'id': info['_id'], | 
					
						
							| 
									
										
										
										
											2015-08-17 20:16:43 +06:00
										 |  |  |             'title': info.get('title') or 'Untitled Broadcast', | 
					
						
							| 
									
										
										
										
											2015-08-17 20:20:04 +06:00
										 |  |  |             'description': info.get('description'), | 
					
						
							|  |  |  |             'duration': int_or_none(info.get('length')), | 
					
						
							|  |  |  |             'thumbnail': info.get('preview'), | 
					
						
							|  |  |  |             'uploader': info.get('channel', {}).get('display_name'), | 
					
						
							|  |  |  |             'uploader_id': info.get('channel', {}).get('name'), | 
					
						
							|  |  |  |             'timestamp': parse_iso8601(info.get('recorded_at')), | 
					
						
							|  |  |  |             'view_count': int_or_none(info.get('views')), | 
					
						
							| 
									
										
										
										
											2014-10-16 22:23:35 +07:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     def _real_extract(self, url): | 
					
						
							|  |  |  |         return self._extract_media(self._match_id(url)) | 
					
						
							| 
									
										
										
										
											2014-12-06 21:24:20 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | class TwitchVideoIE(TwitchItemBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:video' | 
					
						
							| 
									
										
										
										
											2015-03-22 15:39:35 +01:00
										 |  |  |     _VALID_URL = r'%s/[^/]+/b/(?P<id>\d+)' % TwitchBaseIE._VALID_URL_BASE | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     _ITEM_TYPE = 'video' | 
					
						
							|  |  |  |     _ITEM_SHORTCUT = 'a' | 
					
						
							| 
									
										
										
										
											2014-12-06 21:24:20 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     _TEST = { | 
					
						
							|  |  |  |         'url': 'http://www.twitch.tv/riotgames/b/577357806', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'a577357806', | 
					
						
							|  |  |  |             'title': 'Worlds Semifinals - Star Horn Royal Club vs. OMG', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'playlist_mincount': 12, | 
					
						
							| 
									
										
										
										
											2016-05-12 23:57:52 +06:00
										 |  |  |         'skip': 'HTTP Error 404: Not Found', | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2014-12-06 21:24:20 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | class TwitchChapterIE(TwitchItemBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:chapter' | 
					
						
							| 
									
										
										
										
											2015-03-22 15:39:35 +01:00
										 |  |  |     _VALID_URL = r'%s/[^/]+/c/(?P<id>\d+)' % TwitchBaseIE._VALID_URL_BASE | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     _ITEM_TYPE = 'chapter' | 
					
						
							|  |  |  |     _ITEM_SHORTCUT = 'c' | 
					
						
							| 
									
										
										
										
											2014-12-06 21:24:20 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-22 18:18:21 +01:00
										 |  |  |     _TESTS = [{ | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         'url': 'http://www.twitch.tv/acracingleague/c/5285812', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'c5285812', | 
					
						
							|  |  |  |             'title': 'ACRL Off Season - Sports Cars @ Nordschleife', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'playlist_mincount': 3, | 
					
						
							| 
									
										
										
										
											2016-05-12 23:57:52 +06:00
										 |  |  |         'skip': 'HTTP Error 404: Not Found', | 
					
						
							| 
									
										
										
										
											2015-01-22 18:18:21 +01:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         'url': 'http://www.twitch.tv/tsm_theoddone/c/2349361', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							|  |  |  |     }] | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchVodIE(TwitchItemBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:vod' | 
					
						
							| 
									
										
										
										
											2016-12-31 22:40:42 +07:00
										 |  |  |     _VALID_URL = r'''(?x)
 | 
					
						
							|  |  |  |                     https?:// | 
					
						
							|  |  |  |                         (?: | 
					
						
							| 
									
										
										
										
											2017-09-15 22:18:38 +07:00
										 |  |  |                             (?:(?:www|go)\.)?twitch\.tv/(?:[^/]+/v|videos)/| | 
					
						
							| 
									
										
										
										
											2016-12-31 22:40:42 +07:00
										 |  |  |                             player\.twitch\.tv/\?.*?\bvideo=v | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                         (?P<id>\d+) | 
					
						
							|  |  |  |                     '''
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     _ITEM_TYPE = 'vod' | 
					
						
							|  |  |  |     _ITEM_SHORTCUT = 'v' | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-17 20:23:52 +06:00
										 |  |  |     _TESTS = [{ | 
					
						
							| 
									
										
										
										
											2015-08-03 01:54:21 +02:00
										 |  |  |         'url': 'http://www.twitch.tv/riotgames/v/6528877?t=5m10s', | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         'info_dict': { | 
					
						
							| 
									
										
										
										
											2015-06-28 13:31:37 +08:00
										 |  |  |             'id': 'v6528877', | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             'ext': 'mp4', | 
					
						
							| 
									
										
										
										
											2015-06-28 13:31:37 +08:00
										 |  |  |             'title': 'LCK Summer Split - Week 6 Day 1', | 
					
						
							| 
									
										
										
										
											2017-01-02 20:08:07 +08:00
										 |  |  |             'thumbnail': r're:^https?://.*\.jpg$', | 
					
						
							| 
									
										
										
										
											2015-06-28 13:31:37 +08:00
										 |  |  |             'duration': 17208, | 
					
						
							|  |  |  |             'timestamp': 1435131709, | 
					
						
							|  |  |  |             'upload_date': '20150624', | 
					
						
							|  |  |  |             'uploader': 'Riot Games', | 
					
						
							|  |  |  |             'uploader_id': 'riotgames', | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             'view_count': int, | 
					
						
							| 
									
										
										
										
											2015-08-03 01:54:21 +02:00
										 |  |  |             'start_time': 310, | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         }, | 
					
						
							|  |  |  |         'params': { | 
					
						
							|  |  |  |             # m3u8 download | 
					
						
							|  |  |  |             'skip_download': True, | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2015-08-17 20:23:52 +06:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         # Untitled broadcast (title is None) | 
					
						
							|  |  |  |         'url': 'http://www.twitch.tv/belkao_o/v/11230755', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'v11230755', | 
					
						
							|  |  |  |             'ext': 'mp4', | 
					
						
							|  |  |  |             'title': 'Untitled Broadcast', | 
					
						
							| 
									
										
										
										
											2017-01-02 20:08:07 +08:00
										 |  |  |             'thumbnail': r're:^https?://.*\.jpg$', | 
					
						
							| 
									
										
										
										
											2015-08-17 20:23:52 +06:00
										 |  |  |             'duration': 1638, | 
					
						
							|  |  |  |             'timestamp': 1439746708, | 
					
						
							|  |  |  |             'upload_date': '20150816', | 
					
						
							|  |  |  |             'uploader': 'BelkAO_o', | 
					
						
							|  |  |  |             'uploader_id': 'belkao_o', | 
					
						
							|  |  |  |             'view_count': int, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'params': { | 
					
						
							|  |  |  |             # m3u8 download | 
					
						
							|  |  |  |             'skip_download': True, | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2016-10-01 16:37:49 +08:00
										 |  |  |         'skip': 'HTTP Error 404: Not Found', | 
					
						
							| 
									
										
										
										
											2016-12-31 22:40:42 +07:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         'url': 'http://player.twitch.tv/?t=5m10s&video=v6528877', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							| 
									
										
										
										
											2017-01-27 21:31:26 +07:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         'url': 'https://www.twitch.tv/videos/6528877', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							| 
									
										
										
										
											2015-08-17 20:23:52 +06:00
										 |  |  |     }] | 
					
						
							| 
									
										
										
										
											2014-12-06 21:24:20 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-06-23 22:07:27 +02:00
										 |  |  |     def _real_extract(self, url): | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         item_id = self._match_id(url) | 
					
						
							| 
									
										
										
										
											2015-10-17 18:29:54 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         info = self._download_info(self._ITEM_SHORTCUT, item_id) | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |         access_token = self._call_api( | 
					
						
							|  |  |  |             'api/vods/%s/access_token' % item_id, item_id, | 
					
						
							| 
									
										
										
										
											2015-10-17 18:43:12 +06:00
										 |  |  |             'Downloading %s access token' % self._ITEM_TYPE) | 
					
						
							| 
									
										
										
										
											2015-10-17 18:29:54 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         formats = self._extract_m3u8_formats( | 
					
						
							| 
									
										
										
										
											2015-10-17 18:43:12 +06:00
										 |  |  |             '%s/vod/%s?%s' % ( | 
					
						
							|  |  |  |                 self._USHER_BASE, item_id, | 
					
						
							| 
									
										
										
										
											2016-03-26 01:46:57 +06:00
										 |  |  |                 compat_urllib_parse_urlencode({ | 
					
						
							| 
									
										
										
										
											2015-10-17 18:29:54 +06:00
										 |  |  |                     'allow_source': 'true', | 
					
						
							| 
									
										
										
										
											2016-03-07 21:03:24 -06:00
										 |  |  |                     'allow_audio_only': 'true', | 
					
						
							| 
									
										
										
										
											2015-10-17 18:29:54 +06:00
										 |  |  |                     'allow_spectre': 'true', | 
					
						
							|  |  |  |                     'player': 'twitchweb', | 
					
						
							|  |  |  |                     'nauth': access_token['token'], | 
					
						
							|  |  |  |                     'nauthsig': access_token['sig'], | 
					
						
							|  |  |  |                 })), | 
					
						
							| 
									
										
										
										
											2016-06-05 03:19:44 +07:00
										 |  |  |             item_id, 'mp4', entry_protocol='m3u8_native') | 
					
						
							| 
									
										
										
										
											2015-10-17 18:29:54 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-06 10:53:49 +01:00
										 |  |  |         self._prefer_source(formats) | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         info['formats'] = formats | 
					
						
							| 
									
										
										
										
											2015-08-03 01:54:21 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         parsed_url = compat_urllib_parse_urlparse(url) | 
					
						
							|  |  |  |         query = compat_parse_qs(parsed_url.query) | 
					
						
							|  |  |  |         if 't' in query: | 
					
						
							|  |  |  |             info['start_time'] = parse_duration(query['t'][0]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-12-24 22:10:54 +07:00
										 |  |  |         if info.get('timestamp') is not None: | 
					
						
							|  |  |  |             info['subtitles'] = { | 
					
						
							|  |  |  |                 'rechat': [{ | 
					
						
							|  |  |  |                     'url': update_url_query( | 
					
						
							|  |  |  |                         'https://rechat.twitch.tv/rechat-messages', { | 
					
						
							|  |  |  |                             'video_id': 'v%s' % item_id, | 
					
						
							|  |  |  |                             'start': info['timestamp'], | 
					
						
							|  |  |  |                         }), | 
					
						
							|  |  |  |                     'ext': 'json', | 
					
						
							|  |  |  |                 }], | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         return info | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchPlaylistBaseIE(TwitchBaseIE): | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |     _PLAYLIST_PATH = 'kraken/channels/%s/videos/?offset=%d&limit=%d' | 
					
						
							| 
									
										
										
										
											2016-03-03 22:12:55 +06:00
										 |  |  |     _PAGE_LIMIT = 100 | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _extract_playlist(self, channel_id): | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |         info = self._call_api( | 
					
						
							|  |  |  |             'kraken/channels/%s' % channel_id, | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             channel_id, 'Downloading channel info JSON') | 
					
						
							|  |  |  |         channel_name = info.get('display_name') or info.get('name') | 
					
						
							|  |  |  |         entries = [] | 
					
						
							|  |  |  |         offset = 0 | 
					
						
							|  |  |  |         limit = self._PAGE_LIMIT | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |         broken_paging_detected = False | 
					
						
							|  |  |  |         counter_override = None | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         for counter in itertools.count(1): | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |             response = self._call_api( | 
					
						
							|  |  |  |                 self._PLAYLIST_PATH % (channel_id, offset, limit), | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |                 channel_id, | 
					
						
							| 
									
										
										
										
											2016-12-17 20:20:23 +07:00
										 |  |  |                 'Downloading %s JSON page %s' | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |                 % (self._PLAYLIST_TYPE, counter_override or counter)) | 
					
						
							| 
									
										
										
										
											2015-01-23 21:58:40 +06:00
										 |  |  |             page_entries = self._extract_playlist_page(response) | 
					
						
							|  |  |  |             if not page_entries: | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |                 break | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |             total = int_or_none(response.get('_total')) | 
					
						
							|  |  |  |             # Since the beginning of March 2016 twitch's paging mechanism | 
					
						
							|  |  |  |             # is completely broken on the twitch side. It simply ignores | 
					
						
							|  |  |  |             # a limit and returns the whole offset number of videos. | 
					
						
							|  |  |  |             # Working around by just requesting all videos at once. | 
					
						
							| 
									
										
										
										
											2016-03-15 21:45:43 +06:00
										 |  |  |             # Upd: pagination bug was fixed by twitch on 15.03.2016. | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |             if not broken_paging_detected and total and len(page_entries) > limit: | 
					
						
							|  |  |  |                 self.report_warning( | 
					
						
							| 
									
										
										
										
											2016-03-15 21:45:43 +06:00
										 |  |  |                     'Twitch pagination is broken on twitch side, requesting all videos at once', | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |                     channel_id) | 
					
						
							|  |  |  |                 broken_paging_detected = True | 
					
						
							| 
									
										
										
										
											2016-03-03 22:18:32 +06:00
										 |  |  |                 offset = total | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |                 counter_override = '(all at once)' | 
					
						
							|  |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2015-01-23 21:58:40 +06:00
										 |  |  |             entries.extend(page_entries) | 
					
						
							| 
									
										
										
										
											2016-03-03 22:10:36 +06:00
										 |  |  |             if broken_paging_detected or total and len(page_entries) >= total: | 
					
						
							|  |  |  |                 break | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |             offset += limit | 
					
						
							| 
									
										
										
										
											2015-01-23 21:58:40 +06:00
										 |  |  |         return self.playlist_result( | 
					
						
							| 
									
										
										
										
											2018-01-03 04:22:55 -05:00
										 |  |  |             [self._make_url_result(entry) for entry in orderedSet(entries)], | 
					
						
							| 
									
										
										
										
											2015-01-23 21:58:40 +06:00
										 |  |  |             channel_id, channel_name) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-03 04:22:55 -05:00
										 |  |  |     def _make_url_result(self, url): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             video_id = 'v%s' % TwitchVodIE._match_id(url) | 
					
						
							|  |  |  |             return self.url_result(url, TwitchVodIE.ie_key(), video_id=video_id) | 
					
						
							|  |  |  |         except AssertionError: | 
					
						
							|  |  |  |             return self.url_result(url) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-23 21:58:40 +06:00
										 |  |  |     def _extract_playlist_page(self, response): | 
					
						
							|  |  |  |         videos = response.get('videos') | 
					
						
							|  |  |  |         return [video['url'] for video in videos] if videos else [] | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _real_extract(self, url): | 
					
						
							|  |  |  |         return self._extract_playlist(self._match_id(url)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchProfileIE(TwitchPlaylistBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:profile' | 
					
						
							|  |  |  |     _VALID_URL = r'%s/(?P<id>[^/]+)/profile/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE | 
					
						
							|  |  |  |     _PLAYLIST_TYPE = 'profile' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _TEST = { | 
					
						
							|  |  |  |         'url': 'http://www.twitch.tv/vanillatv/profile', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'vanillatv', | 
					
						
							|  |  |  |             'title': 'VanillaTV', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'playlist_mincount': 412, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-12-17 20:20:23 +07:00
										 |  |  | class TwitchVideosBaseIE(TwitchPlaylistBaseIE): | 
					
						
							|  |  |  |     _VALID_URL_VIDEOS_BASE = r'%s/(?P<id>[^/]+)/videos' % TwitchBaseIE._VALID_URL_BASE | 
					
						
							|  |  |  |     _PLAYLIST_PATH = TwitchPlaylistBaseIE._PLAYLIST_PATH + '&broadcast_type=' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchAllVideosIE(TwitchVideosBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:videos:all' | 
					
						
							|  |  |  |     _VALID_URL = r'%s/all' % TwitchVideosBaseIE._VALID_URL_VIDEOS_BASE | 
					
						
							|  |  |  |     _PLAYLIST_PATH = TwitchVideosBaseIE._PLAYLIST_PATH + 'archive,upload,highlight' | 
					
						
							|  |  |  |     _PLAYLIST_TYPE = 'all videos' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _TEST = { | 
					
						
							|  |  |  |         'url': 'https://www.twitch.tv/spamfish/videos/all', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'spamfish', | 
					
						
							|  |  |  |             'title': 'Spamfish', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'playlist_mincount': 869, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchUploadsIE(TwitchVideosBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:videos:uploads' | 
					
						
							|  |  |  |     _VALID_URL = r'%s/uploads' % TwitchVideosBaseIE._VALID_URL_VIDEOS_BASE | 
					
						
							|  |  |  |     _PLAYLIST_PATH = TwitchVideosBaseIE._PLAYLIST_PATH + 'upload' | 
					
						
							|  |  |  |     _PLAYLIST_TYPE = 'uploads' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _TEST = { | 
					
						
							|  |  |  |         'url': 'https://www.twitch.tv/spamfish/videos/uploads', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'spamfish', | 
					
						
							|  |  |  |             'title': 'Spamfish', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'playlist_mincount': 0, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchPastBroadcastsIE(TwitchVideosBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:videos:past-broadcasts' | 
					
						
							|  |  |  |     _VALID_URL = r'%s/past-broadcasts' % TwitchVideosBaseIE._VALID_URL_VIDEOS_BASE | 
					
						
							|  |  |  |     _PLAYLIST_PATH = TwitchVideosBaseIE._PLAYLIST_PATH + 'archive' | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     _PLAYLIST_TYPE = 'past broadcasts' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _TEST = { | 
					
						
							| 
									
										
										
										
											2016-12-17 20:20:23 +07:00
										 |  |  |         'url': 'https://www.twitch.tv/spamfish/videos/past-broadcasts', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'spamfish', | 
					
						
							|  |  |  |             'title': 'Spamfish', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'playlist_mincount': 0, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchHighlightsIE(TwitchVideosBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:videos:highlights' | 
					
						
							|  |  |  |     _VALID_URL = r'%s/highlights' % TwitchVideosBaseIE._VALID_URL_VIDEOS_BASE | 
					
						
							|  |  |  |     _PLAYLIST_PATH = TwitchVideosBaseIE._PLAYLIST_PATH + 'highlight' | 
					
						
							|  |  |  |     _PLAYLIST_TYPE = 'highlights' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _TEST = { | 
					
						
							|  |  |  |         'url': 'https://www.twitch.tv/spamfish/videos/highlights', | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'spamfish', | 
					
						
							|  |  |  |             'title': 'Spamfish', | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2016-12-17 20:20:23 +07:00
										 |  |  |         'playlist_mincount': 805, | 
					
						
							| 
									
										
										
										
											2015-01-21 22:27:21 +06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchStreamIE(TwitchBaseIE): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:stream' | 
					
						
							| 
									
										
										
										
											2017-02-04 23:21:07 +07:00
										 |  |  |     _VALID_URL = r'''(?x)
 | 
					
						
							|  |  |  |                     https?:// | 
					
						
							|  |  |  |                         (?: | 
					
						
							| 
									
										
										
										
											2017-09-15 22:18:38 +07:00
										 |  |  |                             (?:(?:www|go)\.)?twitch\.tv/| | 
					
						
							| 
									
										
										
										
											2017-02-04 23:21:07 +07:00
										 |  |  |                             player\.twitch\.tv/\?.*?\bchannel= | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                         (?P<id>[^/#?]+) | 
					
						
							|  |  |  |                     '''
 | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-20 02:16:07 +06:00
										 |  |  |     _TESTS = [{ | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |         'url': 'http://www.twitch.tv/shroomztv', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': '12772022048', | 
					
						
							|  |  |  |             'display_id': 'shroomztv', | 
					
						
							|  |  |  |             'ext': 'mp4', | 
					
						
							|  |  |  |             'title': 're:^ShroomzTV [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', | 
					
						
							|  |  |  |             'description': 'H1Z1 - lonewolfing with ShroomzTV | A3 Battle Royale later - @ShroomzTV', | 
					
						
							|  |  |  |             'is_live': True, | 
					
						
							|  |  |  |             'timestamp': 1421928037, | 
					
						
							|  |  |  |             'upload_date': '20150122', | 
					
						
							|  |  |  |             'uploader': 'ShroomzTV', | 
					
						
							|  |  |  |             'uploader_id': 'shroomztv', | 
					
						
							|  |  |  |             'view_count': int, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         'params': { | 
					
						
							|  |  |  |             # m3u8 download | 
					
						
							|  |  |  |             'skip_download': True, | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2015-07-20 02:16:07 +06:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         'url': 'http://www.twitch.tv/miracle_doto#profile-0', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							| 
									
										
										
										
											2017-02-04 23:21:07 +07:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         'url': 'https://player.twitch.tv/?channel=lotsofs', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							| 
									
										
										
										
											2017-09-15 22:18:38 +07:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         'url': 'https://go.twitch.tv/food', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							| 
									
										
										
										
											2015-07-20 02:16:07 +06:00
										 |  |  |     }] | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-04 23:21:07 +07:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def suitable(cls, url): | 
					
						
							|  |  |  |         return (False | 
					
						
							|  |  |  |                 if any(ie.suitable(url) for ie in ( | 
					
						
							|  |  |  |                     TwitchVideoIE, | 
					
						
							|  |  |  |                     TwitchChapterIE, | 
					
						
							|  |  |  |                     TwitchVodIE, | 
					
						
							|  |  |  |                     TwitchProfileIE, | 
					
						
							|  |  |  |                     TwitchAllVideosIE, | 
					
						
							|  |  |  |                     TwitchUploadsIE, | 
					
						
							|  |  |  |                     TwitchPastBroadcastsIE, | 
					
						
							|  |  |  |                     TwitchHighlightsIE)) | 
					
						
							|  |  |  |                 else super(TwitchStreamIE, cls).suitable(url)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |     def _real_extract(self, url): | 
					
						
							|  |  |  |         channel_id = self._match_id(url) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |         stream = self._call_api( | 
					
						
							| 
									
										
										
										
											2016-10-23 17:20:45 +07:00
										 |  |  |             'kraken/streams/%s?stream_type=all' % channel_id, channel_id, | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |             'Downloading stream JSON').get('stream') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not stream: | 
					
						
							| 
									
										
										
										
											2016-09-18 19:10:18 +07:00
										 |  |  |             raise ExtractorError('%s is offline' % channel_id, expected=True) | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-18 06:27:45 +06:00
										 |  |  |         # Channel name may be typed if different case than the original channel name | 
					
						
							|  |  |  |         # (e.g. http://www.twitch.tv/TWITCHPLAYSPOKEMON) that will lead to constructing | 
					
						
							|  |  |  |         # an invalid m3u8 URL. Working around by use of original channel name from stream | 
					
						
							|  |  |  |         # JSON and fallback to lowercase if it's not available. | 
					
						
							| 
									
										
										
										
											2015-07-18 06:18:03 +06:00
										 |  |  |         channel_id = stream.get('channel', {}).get('name') or channel_id.lower() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-21 22:09:29 +07:00
										 |  |  |         access_token = self._call_api( | 
					
						
							|  |  |  |             'api/channels/%s/access_token' % channel_id, channel_id, | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |             'Downloading channel access token') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         query = { | 
					
						
							|  |  |  |             'allow_source': 'true', | 
					
						
							| 
									
										
										
										
											2016-03-07 21:03:24 -06:00
										 |  |  |             'allow_audio_only': 'true', | 
					
						
							| 
									
										
										
										
											2016-10-23 17:20:45 +07:00
										 |  |  |             'allow_spectre': 'true', | 
					
						
							| 
									
										
										
										
											2015-01-22 23:34:40 +06:00
										 |  |  |             'p': random.randint(1000000, 10000000), | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |             'player': 'twitchweb', | 
					
						
							|  |  |  |             'segment_preference': '4', | 
					
						
							| 
									
										
										
										
											2015-03-08 16:54:11 +06:00
										 |  |  |             'sig': access_token['sig'].encode('utf-8'), | 
					
						
							|  |  |  |             'token': access_token['token'].encode('utf-8'), | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |         } | 
					
						
							|  |  |  |         formats = self._extract_m3u8_formats( | 
					
						
							|  |  |  |             '%s/api/channel/hls/%s.m3u8?%s' | 
					
						
							| 
									
										
										
										
											2016-03-26 01:46:57 +06:00
										 |  |  |             % (self._USHER_BASE, channel_id, compat_urllib_parse_urlencode(query)), | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  |             channel_id, 'mp4') | 
					
						
							| 
									
										
										
										
											2015-03-06 10:53:49 +01:00
										 |  |  |         self._prefer_source(formats) | 
					
						
							| 
									
										
										
										
											2015-01-22 23:11:22 +06:00
										 |  |  | 
 | 
					
						
							|  |  |  |         view_count = stream.get('viewers') | 
					
						
							|  |  |  |         timestamp = parse_iso8601(stream.get('created_at')) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         channel = stream['channel'] | 
					
						
							|  |  |  |         title = self._live_title(channel.get('display_name') or channel.get('name')) | 
					
						
							|  |  |  |         description = channel.get('status') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         thumbnails = [] | 
					
						
							|  |  |  |         for thumbnail_key, thumbnail_url in stream['preview'].items(): | 
					
						
							|  |  |  |             m = re.search(r'(?P<width>\d+)x(?P<height>\d+)\.jpg$', thumbnail_key) | 
					
						
							|  |  |  |             if not m: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             thumbnails.append({ | 
					
						
							|  |  |  |                 'url': thumbnail_url, | 
					
						
							|  |  |  |                 'width': int(m.group('width')), | 
					
						
							|  |  |  |                 'height': int(m.group('height')), | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             'id': compat_str(stream['_id']), | 
					
						
							|  |  |  |             'display_id': channel_id, | 
					
						
							|  |  |  |             'title': title, | 
					
						
							|  |  |  |             'description': description, | 
					
						
							|  |  |  |             'thumbnails': thumbnails, | 
					
						
							|  |  |  |             'uploader': channel.get('display_name'), | 
					
						
							|  |  |  |             'uploader_id': channel.get('name'), | 
					
						
							|  |  |  |             'timestamp': timestamp, | 
					
						
							|  |  |  |             'view_count': view_count, | 
					
						
							|  |  |  |             'formats': formats, | 
					
						
							|  |  |  |             'is_live': True, | 
					
						
							| 
									
										
										
										
											2015-01-23 21:05:07 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TwitchClipsIE(InfoExtractor): | 
					
						
							|  |  |  |     IE_NAME = 'twitch:clips' | 
					
						
							|  |  |  |     _VALID_URL = r'https?://clips\.twitch\.tv/(?:[^/]+/)*(?P<id>[^/?#&]+)' | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-07-28 22:30:09 +07:00
										 |  |  |     _TESTS = [{ | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  |         'url': 'https://clips.twitch.tv/ea/AggressiveCobraPoooound', | 
					
						
							|  |  |  |         'md5': '761769e1eafce0ffebfb4089cb3847cd', | 
					
						
							|  |  |  |         'info_dict': { | 
					
						
							|  |  |  |             'id': 'AggressiveCobraPoooound', | 
					
						
							|  |  |  |             'ext': 'mp4', | 
					
						
							|  |  |  |             'title': 'EA Play 2016 Live from the Novo Theatre', | 
					
						
							| 
									
										
										
										
											2017-01-02 20:08:07 +08:00
										 |  |  |             'thumbnail': r're:^https?://.*\.jpg', | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  |             'creator': 'EA', | 
					
						
							|  |  |  |             'uploader': 'stereotype_', | 
					
						
							|  |  |  |             'uploader_id': 'stereotype_', | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2016-07-28 22:30:09 +07:00
										 |  |  |     }, { | 
					
						
							|  |  |  |         # multiple formats | 
					
						
							|  |  |  |         'url': 'https://clips.twitch.tv/rflegendary/UninterestedBeeDAESuppy', | 
					
						
							|  |  |  |         'only_matching': True, | 
					
						
							|  |  |  |     }] | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _real_extract(self, url): | 
					
						
							|  |  |  |         video_id = self._match_id(url) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         webpage = self._download_webpage(url, video_id) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         clip = self._parse_json( | 
					
						
							|  |  |  |             self._search_regex( | 
					
						
							|  |  |  |                 r'(?s)clipInfo\s*=\s*({.+?});', webpage, 'clip info'), | 
					
						
							|  |  |  |             video_id, transform_source=js_to_json) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-23 12:12:50 -04:00
										 |  |  |         title = clip.get('title') or clip.get('channel_title') or self._og_search_title(webpage) | 
					
						
							| 
									
										
										
										
											2016-07-28 22:30:09 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         formats = [{ | 
					
						
							|  |  |  |             'url': option['source'], | 
					
						
							|  |  |  |             'format_id': option.get('quality'), | 
					
						
							|  |  |  |             'height': int_or_none(option.get('quality')), | 
					
						
							|  |  |  |         } for option in clip.get('quality_options', []) if option.get('source')] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not formats: | 
					
						
							|  |  |  |             formats = [{ | 
					
						
							|  |  |  |                 'url': clip['clip_video_url'], | 
					
						
							|  |  |  |             }] | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-07-29 09:01:53 +07:00
										 |  |  |         self._sort_formats(formats) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  |         return { | 
					
						
							|  |  |  |             'id': video_id, | 
					
						
							|  |  |  |             'title': title, | 
					
						
							|  |  |  |             'thumbnail': self._og_search_thumbnail(webpage), | 
					
						
							|  |  |  |             'creator': clip.get('broadcaster_display_name') or clip.get('broadcaster_login'), | 
					
						
							|  |  |  |             'uploader': clip.get('curator_login'), | 
					
						
							|  |  |  |             'uploader_id': clip.get('curator_display_name'), | 
					
						
							| 
									
										
										
										
											2016-07-28 22:30:09 +07:00
										 |  |  |             'formats': formats, | 
					
						
							| 
									
										
										
										
											2016-06-14 00:06:31 +07:00
										 |  |  |         } |