[Letv] Fix test_Letv and test_Letv_1 failures in python 3
[youtube-dl] / youtube_dl / extractor / twitch.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import itertools
5 import re
6 import random
7
8 from .common import InfoExtractor
9 from ..compat import (
10     compat_str,
11     compat_urllib_parse,
12     compat_urllib_request,
13 )
14 from ..utils import (
15     ExtractorError,
16     parse_iso8601,
17 )
18
19
20 class TwitchBaseIE(InfoExtractor):
21     _VALID_URL_BASE = r'https?://(?:www\.)?twitch\.tv'
22
23     _API_BASE = 'https://api.twitch.tv'
24     _USHER_BASE = 'http://usher.twitch.tv'
25     _LOGIN_URL = 'https://secure.twitch.tv/user/login'
26     _NETRC_MACHINE = 'twitch'
27
28     def _handle_error(self, response):
29         if not isinstance(response, dict):
30             return
31         error = response.get('error')
32         if error:
33             raise ExtractorError(
34                 '%s returned error: %s - %s' % (self.IE_NAME, error, response.get('message')),
35                 expected=True)
36
37     def _download_json(self, url, video_id, note='Downloading JSON metadata'):
38         headers = {
39             'Referer': 'http://api.twitch.tv/crossdomain/receiver.html?v=2',
40             'X-Requested-With': 'XMLHttpRequest',
41         }
42         for cookie in self._downloader.cookiejar:
43             if cookie.name == 'api_token':
44                 headers['Twitch-Api-Token'] = cookie.value
45         request = compat_urllib_request.Request(url, headers=headers)
46         response = super(TwitchBaseIE, self)._download_json(request, video_id, note)
47         self._handle_error(response)
48         return response
49
50     def _real_initialize(self):
51         self._login()
52
53     def _login(self):
54         (username, password) = self._get_login_info()
55         if username is None:
56             return
57
58         login_page = self._download_webpage(
59             self._LOGIN_URL, None, 'Downloading login page')
60
61         authenticity_token = self._search_regex(
62             r'<input name="authenticity_token" type="hidden" value="([^"]+)"',
63             login_page, 'authenticity token')
64
65         login_form = {
66             'utf8': '✓'.encode('utf-8'),
67             'authenticity_token': authenticity_token,
68             'redirect_on_login': '',
69             'embed_form': 'false',
70             'mp_source_action': '',
71             'follow': '',
72             'user[login]': username,
73             'user[password]': password,
74         }
75
76         request = compat_urllib_request.Request(
77             self._LOGIN_URL, compat_urllib_parse.urlencode(login_form).encode('utf-8'))
78         request.add_header('Referer', self._LOGIN_URL)
79         response = self._download_webpage(
80             request, None, 'Logging in as %s' % username)
81
82         m = re.search(
83             r"id=([\"'])login_error_message\1[^>]*>(?P<msg>[^<]+)", response)
84         if m:
85             raise ExtractorError(
86                 'Unable to login: %s' % m.group('msg').strip(), expected=True)
87
88
89 class TwitchItemBaseIE(TwitchBaseIE):
90     def _download_info(self, item, item_id):
91         return self._extract_info(self._download_json(
92             '%s/kraken/videos/%s%s' % (self._API_BASE, item, item_id), item_id,
93             'Downloading %s info JSON' % self._ITEM_TYPE))
94
95     def _extract_media(self, item_id):
96         info = self._download_info(self._ITEM_SHORTCUT, item_id)
97         response = self._download_json(
98             '%s/api/videos/%s%s' % (self._API_BASE, self._ITEM_SHORTCUT, item_id), item_id,
99             'Downloading %s playlist JSON' % self._ITEM_TYPE)
100         entries = []
101         chunks = response['chunks']
102         qualities = list(chunks.keys())
103         for num, fragment in enumerate(zip(*chunks.values()), start=1):
104             formats = []
105             for fmt_num, fragment_fmt in enumerate(fragment):
106                 format_id = qualities[fmt_num]
107                 fmt = {
108                     'url': fragment_fmt['url'],
109                     'format_id': format_id,
110                     'quality': 1 if format_id == 'live' else 0,
111                 }
112                 m = re.search(r'^(?P<height>\d+)[Pp]', format_id)
113                 if m:
114                     fmt['height'] = int(m.group('height'))
115                 formats.append(fmt)
116             self._sort_formats(formats)
117             entry = dict(info)
118             entry['id'] = '%s_%d' % (entry['id'], num)
119             entry['title'] = '%s part %d' % (entry['title'], num)
120             entry['formats'] = formats
121             entries.append(entry)
122         return self.playlist_result(entries, info['id'], info['title'])
123
124     def _extract_info(self, info):
125         return {
126             'id': info['_id'],
127             'title': info['title'],
128             'description': info['description'],
129             'duration': info['length'],
130             'thumbnail': info['preview'],
131             'uploader': info['channel']['display_name'],
132             'uploader_id': info['channel']['name'],
133             'timestamp': parse_iso8601(info['recorded_at']),
134             'view_count': info['views'],
135         }
136
137     def _real_extract(self, url):
138         return self._extract_media(self._match_id(url))
139
140
141 class TwitchVideoIE(TwitchItemBaseIE):
142     IE_NAME = 'twitch:video'
143     _VALID_URL = r'%s/[^/]+/b/(?P<id>[^/]+)' % TwitchBaseIE._VALID_URL_BASE
144     _ITEM_TYPE = 'video'
145     _ITEM_SHORTCUT = 'a'
146
147     _TEST = {
148         'url': 'http://www.twitch.tv/riotgames/b/577357806',
149         'info_dict': {
150             'id': 'a577357806',
151             'title': 'Worlds Semifinals - Star Horn Royal Club vs. OMG',
152         },
153         'playlist_mincount': 12,
154     }
155
156
157 class TwitchChapterIE(TwitchItemBaseIE):
158     IE_NAME = 'twitch:chapter'
159     _VALID_URL = r'%s/[^/]+/c/(?P<id>[^/]+)' % TwitchBaseIE._VALID_URL_BASE
160     _ITEM_TYPE = 'chapter'
161     _ITEM_SHORTCUT = 'c'
162
163     _TESTS = [{
164         'url': 'http://www.twitch.tv/acracingleague/c/5285812',
165         'info_dict': {
166             'id': 'c5285812',
167             'title': 'ACRL Off Season - Sports Cars @ Nordschleife',
168         },
169         'playlist_mincount': 3,
170     }, {
171         'url': 'http://www.twitch.tv/tsm_theoddone/c/2349361',
172         'only_matching': True,
173     }]
174
175
176 class TwitchVodIE(TwitchItemBaseIE):
177     IE_NAME = 'twitch:vod'
178     _VALID_URL = r'%s/[^/]+/v/(?P<id>[^/]+)' % TwitchBaseIE._VALID_URL_BASE
179     _ITEM_TYPE = 'vod'
180     _ITEM_SHORTCUT = 'v'
181
182     _TEST = {
183         'url': 'http://www.twitch.tv/ksptv/v/3622000',
184         'info_dict': {
185             'id': 'v3622000',
186             'ext': 'mp4',
187             'title': '''KSPTV: Squadcast: "Everyone's on vacation so here's Dahud" Edition!''',
188             'thumbnail': 're:^https?://.*\.jpg$',
189             'duration': 6951,
190             'timestamp': 1419028564,
191             'upload_date': '20141219',
192             'uploader': 'KSPTV',
193             'uploader_id': 'ksptv',
194             'view_count': int,
195         },
196         'params': {
197             # m3u8 download
198             'skip_download': True,
199         },
200     }
201
202     def _real_extract(self, url):
203         item_id = self._match_id(url)
204         info = self._download_info(self._ITEM_SHORTCUT, item_id)
205         access_token = self._download_json(
206             '%s/api/vods/%s/access_token' % (self._API_BASE, item_id), item_id,
207             'Downloading %s access token' % self._ITEM_TYPE)
208         formats = self._extract_m3u8_formats(
209             '%s/vod/%s?nauth=%s&nauthsig=%s'
210             % (self._USHER_BASE, item_id, access_token['token'], access_token['sig']),
211             item_id, 'mp4')
212         info['formats'] = formats
213         return info
214
215
216 class TwitchPlaylistBaseIE(TwitchBaseIE):
217     _PLAYLIST_URL = '%s/kraken/channels/%%s/videos/?offset=%%d&limit=%%d' % TwitchBaseIE._API_BASE
218     _PAGE_LIMIT = 100
219
220     def _extract_playlist(self, channel_id):
221         info = self._download_json(
222             '%s/kraken/channels/%s' % (self._API_BASE, channel_id),
223             channel_id, 'Downloading channel info JSON')
224         channel_name = info.get('display_name') or info.get('name')
225         entries = []
226         offset = 0
227         limit = self._PAGE_LIMIT
228         for counter in itertools.count(1):
229             response = self._download_json(
230                 self._PLAYLIST_URL % (channel_id, offset, limit),
231                 channel_id, 'Downloading %s videos JSON page %d' % (self._PLAYLIST_TYPE, counter))
232             page_entries = self._extract_playlist_page(response)
233             if not page_entries:
234                 break
235             entries.extend(page_entries)
236             offset += limit
237         return self.playlist_result(
238             [self.url_result(entry) for entry in set(entries)],
239             channel_id, channel_name)
240
241     def _extract_playlist_page(self, response):
242         videos = response.get('videos')
243         return [video['url'] for video in videos] if videos else []
244
245     def _real_extract(self, url):
246         return self._extract_playlist(self._match_id(url))
247
248
249 class TwitchProfileIE(TwitchPlaylistBaseIE):
250     IE_NAME = 'twitch:profile'
251     _VALID_URL = r'%s/(?P<id>[^/]+)/profile/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE
252     _PLAYLIST_TYPE = 'profile'
253
254     _TEST = {
255         'url': 'http://www.twitch.tv/vanillatv/profile',
256         'info_dict': {
257             'id': 'vanillatv',
258             'title': 'VanillaTV',
259         },
260         'playlist_mincount': 412,
261     }
262
263
264 class TwitchPastBroadcastsIE(TwitchPlaylistBaseIE):
265     IE_NAME = 'twitch:past_broadcasts'
266     _VALID_URL = r'%s/(?P<id>[^/]+)/profile/past_broadcasts/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE
267     _PLAYLIST_URL = TwitchPlaylistBaseIE._PLAYLIST_URL + '&broadcasts=true'
268     _PLAYLIST_TYPE = 'past broadcasts'
269
270     _TEST = {
271         'url': 'http://www.twitch.tv/spamfish/profile/past_broadcasts',
272         'info_dict': {
273             'id': 'spamfish',
274             'title': 'Spamfish',
275         },
276         'playlist_mincount': 54,
277     }
278
279
280 class TwitchBookmarksIE(TwitchPlaylistBaseIE):
281     IE_NAME = 'twitch:bookmarks'
282     _VALID_URL = r'%s/(?P<id>[^/]+)/profile/bookmarks/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE
283     _PLAYLIST_URL = '%s/api/bookmark/?user=%%s&offset=%%d&limit=%%d' % TwitchBaseIE._API_BASE
284     _PLAYLIST_TYPE = 'bookmarks'
285
286     _TEST = {
287         'url': 'http://www.twitch.tv/ognos/profile/bookmarks',
288         'info_dict': {
289             'id': 'ognos',
290             'title': 'Ognos',
291         },
292         'playlist_mincount': 3,
293     }
294
295     def _extract_playlist_page(self, response):
296         entries = []
297         for bookmark in response.get('bookmarks', []):
298             video = bookmark.get('video')
299             if not video:
300                 continue
301             entries.append(video['url'])
302         return entries
303
304
305 class TwitchStreamIE(TwitchBaseIE):
306     IE_NAME = 'twitch:stream'
307     _VALID_URL = r'%s/(?P<id>[^/]+)/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE
308
309     _TEST = {
310         'url': 'http://www.twitch.tv/shroomztv',
311         'info_dict': {
312             'id': '12772022048',
313             'display_id': 'shroomztv',
314             'ext': 'mp4',
315             'title': 're:^ShroomzTV [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
316             'description': 'H1Z1 - lonewolfing with ShroomzTV | A3 Battle Royale later - @ShroomzTV',
317             'is_live': True,
318             'timestamp': 1421928037,
319             'upload_date': '20150122',
320             'uploader': 'ShroomzTV',
321             'uploader_id': 'shroomztv',
322             'view_count': int,
323         },
324         'params': {
325             # m3u8 download
326             'skip_download': True,
327         },
328     }
329
330     def _real_extract(self, url):
331         channel_id = self._match_id(url)
332
333         stream = self._download_json(
334             '%s/kraken/streams/%s' % (self._API_BASE, channel_id), channel_id,
335             'Downloading stream JSON').get('stream')
336
337         # Fallback on profile extraction if stream is offline
338         if not stream:
339             return self.url_result(
340                 'http://www.twitch.tv/%s/profile' % channel_id,
341                 'TwitchProfile', channel_id)
342
343         access_token = self._download_json(
344             '%s/api/channels/%s/access_token' % (self._API_BASE, channel_id), channel_id,
345             'Downloading channel access token')
346
347         query = {
348             'allow_source': 'true',
349             'p': random.randint(1000000, 10000000),
350             'player': 'twitchweb',
351             'segment_preference': '4',
352             'sig': access_token['sig'],
353             'token': access_token['token'],
354         }
355
356         formats = self._extract_m3u8_formats(
357             '%s/api/channel/hls/%s.m3u8?%s'
358             % (self._USHER_BASE, channel_id, compat_urllib_parse.urlencode(query).encode('utf-8')),
359             channel_id, 'mp4')
360
361         # prefer the 'source' stream, the others are limited to 30 fps
362         def _sort_source(f):
363             if f.get('m3u8_media') is not None and f['m3u8_media'].get('NAME') == 'Source':
364                 return 1
365             return 0
366         formats = sorted(formats, key=_sort_source)
367
368         view_count = stream.get('viewers')
369         timestamp = parse_iso8601(stream.get('created_at'))
370
371         channel = stream['channel']
372         title = self._live_title(channel.get('display_name') or channel.get('name'))
373         description = channel.get('status')
374
375         thumbnails = []
376         for thumbnail_key, thumbnail_url in stream['preview'].items():
377             m = re.search(r'(?P<width>\d+)x(?P<height>\d+)\.jpg$', thumbnail_key)
378             if not m:
379                 continue
380             thumbnails.append({
381                 'url': thumbnail_url,
382                 'width': int(m.group('width')),
383                 'height': int(m.group('height')),
384             })
385
386         return {
387             'id': compat_str(stream['_id']),
388             'display_id': channel_id,
389             'title': title,
390             'description': description,
391             'thumbnails': thumbnails,
392             'uploader': channel.get('display_name'),
393             'uploader_id': channel.get('name'),
394             'timestamp': timestamp,
395             'view_count': view_count,
396             'formats': formats,
397             'is_live': True,
398         }