[vk] Fix authentication on python3
[youtube-dl] / youtube_dl / extractor / vk.py
1 # encoding: utf-8
2 from __future__ import unicode_literals
3
4 import re
5 import json
6 import sys
7
8 from .common import InfoExtractor
9 from ..compat import (
10     compat_str,
11     compat_urlparse,
12 )
13 from ..utils import (
14     clean_html,
15     ExtractorError,
16     get_element_by_class,
17     int_or_none,
18     orderedSet,
19     parse_duration,
20     remove_start,
21     str_to_int,
22     unescapeHTML,
23     unified_strdate,
24     urlencode_postdata,
25 )
26 from .vimeo import VimeoIE
27 from .pladform import PladformIE
28
29
30 class VKBaseIE(InfoExtractor):
31     _NETRC_MACHINE = 'vk'
32
33     def _login(self):
34         (username, password) = self._get_login_info()
35         if username is None:
36             return
37
38         login_page, url_handle = self._download_webpage_handle(
39             'https://vk.com', None, 'Downloading login page')
40
41         login_form = self._hidden_inputs(login_page)
42
43         login_form.update({
44             'email': username.encode('cp1251'),
45             'pass': password.encode('cp1251'),
46         })
47
48         # https://new.vk.com/ serves two same remixlhk cookies in Set-Cookie header
49         # and expects the first one to be set rather than second (see
50         # https://github.com/rg3/youtube-dl/issues/9841#issuecomment-227871201).
51         # As of RFC6265 the newer one cookie should be set into cookie store
52         # what actually happens.
53         # We will workaround this VK issue by resetting the remixlhk cookie to
54         # the first one manually.
55         for header, cookies in url_handle.headers.items():
56             if header.lower() != 'set-cookie':
57                 continue
58             if sys.version_info[0] >= 3:
59                 cookies = cookies.encode('iso-8859-1')
60             cookies = cookies.decode('utf-8')
61             remixlhk = re.search(r'remixlhk=(.+?);.*?\bdomain=(.+?)(?:[,;]|$)', cookies)
62             if remixlhk:
63                 value, domain = remixlhk.groups()
64                 self._set_cookie(domain, 'remixlhk', value)
65                 break
66
67         login_page = self._download_webpage(
68             'https://login.vk.com/?act=login', None,
69             note='Logging in as %s' % username,
70             data=urlencode_postdata(login_form))
71
72         if re.search(r'onLoginFailed', login_page):
73             raise ExtractorError(
74                 'Unable to login, incorrect username and/or password', expected=True)
75
76     def _real_initialize(self):
77         self._login()
78
79
80 class VKIE(VKBaseIE):
81     IE_NAME = 'vk'
82     IE_DESC = 'VK'
83     _VALID_URL = r'''(?x)
84                     https?://
85                         (?:
86                             (?:
87                                 (?:(?:m|new)\.)?vk\.com/video_|
88                                 (?:www\.)?daxab.com/
89                             )
90                             ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)|
91                             (?:
92                                 (?:(?:m|new)\.)?vk\.com/(?:.+?\?.*?z=)?video|
93                                 (?:www\.)?daxab.com/embed/
94                             )
95                             (?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>[\da-f]+))?
96                         )
97                     '''
98     _TESTS = [
99         {
100             'url': 'http://vk.com/videos-77521?z=video-77521_162222515%2Fclub77521',
101             'md5': '0deae91935c54e00003c2a00646315f0',
102             'info_dict': {
103                 'id': '162222515',
104                 'ext': 'flv',
105                 'title': 'ProtivoGunz - Хуёвая песня',
106                 'uploader': 're:(?:Noize MC|Alexander Ilyashenko).*',
107                 'duration': 195,
108                 'upload_date': '20120212',
109                 'view_count': int,
110             },
111         },
112         {
113             'url': 'http://vk.com/video205387401_165548505',
114             'md5': '6c0aeb2e90396ba97035b9cbde548700',
115             'info_dict': {
116                 'id': '165548505',
117                 'ext': 'mp4',
118                 'uploader': 'Tom Cruise',
119                 'title': 'No name',
120                 'duration': 9,
121                 'upload_date': '20130721',
122                 'view_count': int,
123             }
124         },
125         {
126             'note': 'Embedded video',
127             'url': 'http://vk.com/video_ext.php?oid=32194266&id=162925554&hash=7d8c2e0d5e05aeaa&hd=1',
128             'md5': 'c7ce8f1f87bec05b3de07fdeafe21a0a',
129             'info_dict': {
130                 'id': '162925554',
131                 'ext': 'mp4',
132                 'uploader': 'Vladimir Gavrin',
133                 'title': 'Lin Dan',
134                 'duration': 101,
135                 'upload_date': '20120730',
136                 'view_count': int,
137             },
138             'skip': 'This video has been removed from public access.',
139         },
140         {
141             # VIDEO NOW REMOVED
142             # please update if you find a video whose URL follows the same pattern
143             'url': 'http://vk.com/video-8871596_164049491',
144             'md5': 'a590bcaf3d543576c9bd162812387666',
145             'note': 'Only available for registered users',
146             'info_dict': {
147                 'id': '164049491',
148                 'ext': 'mp4',
149                 'uploader': 'Триллеры',
150                 'title': '► Бойцовский клуб / Fight Club 1999 [HD 720]',
151                 'duration': 8352,
152                 'upload_date': '20121218',
153                 'view_count': int,
154             },
155             'skip': 'Requires vk account credentials',
156         },
157         {
158             'url': 'http://vk.com/hd_kino_mania?z=video-43215063_168067957%2F15c66b9b533119788d',
159             'md5': '4d7a5ef8cf114dfa09577e57b2993202',
160             'info_dict': {
161                 'id': '168067957',
162                 'ext': 'mp4',
163                 'uploader': 'Киномания - лучшее из мира кино',
164                 'title': ' ',
165                 'duration': 7291,
166                 'upload_date': '20140328',
167             },
168             'skip': 'Requires vk account credentials',
169         },
170         {
171             'url': 'http://m.vk.com/video-43215063_169084319?list=125c627d1aa1cebb83&from=wall-43215063_2566540',
172             'md5': '0c45586baa71b7cb1d0784ee3f4e00a6',
173             'note': 'ivi.ru embed',
174             'info_dict': {
175                 'id': '60690',
176                 'ext': 'mp4',
177                 'title': 'Книга Илая',
178                 'duration': 6771,
179                 'upload_date': '20140626',
180                 'view_count': int,
181             },
182             'skip': 'Only works from Russia',
183         },
184         {
185             # video (removed?) only available with list id
186             'url': 'https://vk.com/video30481095_171201961?list=8764ae2d21f14088d4',
187             'md5': '091287af5402239a1051c37ec7b92913',
188             'info_dict': {
189                 'id': '171201961',
190                 'ext': 'mp4',
191                 'title': 'ТюменцевВВ_09.07.2015',
192                 'uploader': 'Anton Ivanov',
193                 'duration': 109,
194                 'upload_date': '20150709',
195                 'view_count': int,
196             },
197         },
198         {
199             # youtube embed
200             'url': 'https://vk.com/video276849682_170681728',
201             'info_dict': {
202                 'id': 'V3K4mi0SYkc',
203                 'ext': 'webm',
204                 'title': "DSWD Awards 'Children's Joy Foundation, Inc.' Certificate of Registration and License to Operate",
205                 'description': 'md5:d9903938abdc74c738af77f527ca0596',
206                 'duration': 178,
207                 'upload_date': '20130116',
208                 'uploader': "Children's Joy Foundation",
209                 'uploader_id': 'thecjf',
210                 'view_count': int,
211             },
212         },
213         {
214             # video key is extra_data not url\d+
215             'url': 'http://vk.com/video-110305615_171782105',
216             'md5': 'e13fcda136f99764872e739d13fac1d1',
217             'info_dict': {
218                 'id': '171782105',
219                 'ext': 'mp4',
220                 'title': 'S-Dance, репетиции к The way show',
221                 'uploader': 'THE WAY SHOW | 17 апреля',
222                 'upload_date': '20160207',
223                 'view_count': int,
224             },
225         },
226         {
227             # removed video, just testing that we match the pattern
228             'url': 'http://vk.com/feed?z=video-43215063_166094326%2Fbb50cacd3177146d7a',
229             'only_matching': True,
230         },
231         {
232             # age restricted video, requires vk account credentials
233             'url': 'https://vk.com/video205387401_164765225',
234             'only_matching': True,
235         },
236         {
237             # pladform embed
238             'url': 'https://vk.com/video-76116461_171554880',
239             'only_matching': True,
240         },
241         {
242             'url': 'http://new.vk.com/video205387401_165548505',
243             'only_matching': True,
244         }
245     ]
246
247     def _real_extract(self, url):
248         mobj = re.match(self._VALID_URL, url)
249         video_id = mobj.group('videoid')
250
251         if video_id:
252             info_url = 'https://vk.com/al_video.php?act=show&al=1&module=video&video=%s' % video_id
253             # Some videos (removed?) can only be downloaded with list id specified
254             list_id = mobj.group('list_id')
255             if list_id:
256                 info_url += '&list=%s' % list_id
257         else:
258             info_url = 'http://vk.com/video_ext.php?' + mobj.group('embed_query')
259             video_id = '%s_%s' % (mobj.group('oid'), mobj.group('id'))
260
261         info_page = self._download_webpage(info_url, video_id)
262
263         error_message = self._html_search_regex(
264             [r'(?s)<!><div[^>]+class="video_layer_message"[^>]*>(.+?)</div>',
265                 r'(?s)<div[^>]+id="video_ext_msg"[^>]*>(.+?)</div>'],
266             info_page, 'error message', default=None)
267         if error_message:
268             raise ExtractorError(error_message, expected=True)
269
270         if re.search(r'<!>/login\.php\?.*\bact=security_check', info_page):
271             raise ExtractorError(
272                 'You are trying to log in from an unusual location. You should confirm ownership at vk.com to log in with this IP.',
273                 expected=True)
274
275         ERRORS = {
276             r'>Видеозапись .*? была изъята из публичного доступа в связи с обращением правообладателя.<':
277             'Video %s has been removed from public access due to rightholder complaint.',
278
279             r'<!>Please log in or <':
280             'Video %s is only available for registered users, '
281             'use --username and --password options to provide account credentials.',
282
283             r'<!>Unknown error':
284             'Video %s does not exist.',
285
286             r'<!>Видео временно недоступно':
287             'Video %s is temporarily unavailable.',
288
289             r'<!>Access denied':
290             'Access denied to video %s.',
291         }
292
293         for error_re, error_msg in ERRORS.items():
294             if re.search(error_re, info_page):
295                 raise ExtractorError(error_msg % video_id, expected=True)
296
297         youtube_url = self._search_regex(
298             r'<iframe[^>]+src="((?:https?:)?//www.youtube.com/embed/[^"]+)"',
299             info_page, 'youtube iframe', default=None)
300         if youtube_url:
301             return self.url_result(youtube_url, 'Youtube')
302
303         vimeo_url = VimeoIE._extract_vimeo_url(url, info_page)
304         if vimeo_url is not None:
305             return self.url_result(vimeo_url)
306
307         pladform_url = PladformIE._extract_url(info_page)
308         if pladform_url:
309             return self.url_result(pladform_url)
310
311         m_rutube = re.search(
312             r'\ssrc="((?:https?:)?//rutube\.ru\\?/(?:video|play)\\?/embed(?:.*?))\\?"', info_page)
313         if m_rutube is not None:
314             rutube_url = self._proto_relative_url(
315                 m_rutube.group(1).replace('\\', ''))
316             return self.url_result(rutube_url)
317
318         m_opts = re.search(r'(?s)var\s+opts\s*=\s*({.+?});', info_page)
319         if m_opts:
320             m_opts_url = re.search(r"url\s*:\s*'((?!/\b)[^']+)", m_opts.group(1))
321             if m_opts_url:
322                 opts_url = m_opts_url.group(1)
323                 if opts_url.startswith('//'):
324                     opts_url = 'http:' + opts_url
325                 return self.url_result(opts_url)
326
327         data_json = self._search_regex(r'var\s+vars\s*=\s*({.+?});', info_page, 'vars')
328         data = json.loads(data_json)
329
330         # Extract upload date
331         upload_date = None
332         mobj = re.search(r'id="mv_date(?:_views)?_wrap"[^>]*>([a-zA-Z]+ [0-9]+), ([0-9]+) at', info_page)
333         if mobj is not None:
334             mobj.group(1) + ' ' + mobj.group(2)
335             upload_date = unified_strdate(mobj.group(1) + ' ' + mobj.group(2))
336
337         view_count = None
338         views = self._html_search_regex(
339             r'"mv_views_count_number"[^>]*>(.+?\bviews?)<',
340             info_page, 'view count', default=None)
341         if views:
342             view_count = str_to_int(self._search_regex(
343                 r'([\d,.]+)', views, 'view count', fatal=False))
344
345         formats = []
346         for k, v in data.items():
347             if not k.startswith('url') and not k.startswith('cache') and k != 'extra_data' or not v:
348                 continue
349             height = int_or_none(self._search_regex(
350                 r'^(?:url|cache)(\d+)', k, 'height', default=None))
351             formats.append({
352                 'format_id': k,
353                 'url': v,
354                 'height': height,
355             })
356         self._sort_formats(formats)
357
358         return {
359             'id': compat_str(data['vid']),
360             'formats': formats,
361             'title': unescapeHTML(data['md_title']),
362             'thumbnail': data.get('jpg'),
363             'uploader': data.get('md_author'),
364             'duration': data.get('duration'),
365             'upload_date': upload_date,
366             'view_count': view_count,
367         }
368
369
370 class VKUserVideosIE(VKBaseIE):
371     IE_NAME = 'vk:uservideos'
372     IE_DESC = "VK - User's Videos"
373     _VALID_URL = r'https?://(?:(?:m|new)\.)?vk\.com/videos(?P<id>-?[0-9]+)(?!\?.*\bz=video)(?:[/?#&]|$)'
374     _TEMPLATE_URL = 'https://vk.com/videos'
375     _TESTS = [{
376         'url': 'http://vk.com/videos205387401',
377         'info_dict': {
378             'id': '205387401',
379             'title': "Tom Cruise's Videos",
380         },
381         'playlist_mincount': 4,
382     }, {
383         'url': 'http://vk.com/videos-77521',
384         'only_matching': True,
385     }, {
386         'url': 'http://vk.com/videos-97664626?section=all',
387         'only_matching': True,
388     }, {
389         'url': 'http://m.vk.com/videos205387401',
390         'only_matching': True,
391     }, {
392         'url': 'http://new.vk.com/videos205387401',
393         'only_matching': True,
394     }]
395
396     def _real_extract(self, url):
397         page_id = self._match_id(url)
398
399         webpage = self._download_webpage(url, page_id)
400
401         entries = [
402             self.url_result(
403                 'http://vk.com/video' + video_id, 'VK', video_id=video_id)
404             for video_id in orderedSet(re.findall(r'href="/video(-?[0-9_]+)"', webpage))]
405
406         title = unescapeHTML(self._search_regex(
407             r'<title>\s*([^<]+?)\s+\|\s+\d+\s+videos',
408             webpage, 'title', default=page_id))
409
410         return self.playlist_result(entries, page_id, title)
411
412
413 class VKWallPostIE(VKBaseIE):
414     IE_NAME = 'vk:wallpost'
415     _VALID_URL = r'https?://(?:(?:(?:(?:m|new)\.)?vk\.com/(?:[^?]+\?.*\bw=)?wall(?P<id>-?\d+_\d+)))'
416     _TESTS = [{
417         # public page URL, audio playlist
418         'url': 'https://vk.com/bs.official?w=wall-23538238_35',
419         'info_dict': {
420             'id': '23538238_35',
421             'title': 'Black Shadow - Wall post 23538238_35',
422             'description': 'md5:3f84b9c4f9ef499731cf1ced9998cc0c',
423         },
424         'playlist': [{
425             'md5': '5ba93864ec5b85f7ce19a9af4af080f6',
426             'info_dict': {
427                 'id': '135220665_111806521',
428                 'ext': 'mp3',
429                 'title': 'Black Shadow - Слепое Верование',
430                 'duration': 370,
431                 'uploader': 'Black Shadow',
432                 'artist': 'Black Shadow',
433                 'track': 'Слепое Верование',
434             },
435         }, {
436             'md5': '4cc7e804579122b17ea95af7834c9233',
437             'info_dict': {
438                 'id': '135220665_111802303',
439                 'ext': 'mp3',
440                 'title': 'Black Shadow - Война - Негасимое Бездны Пламя!',
441                 'duration': 423,
442                 'uploader': 'Black Shadow',
443                 'artist': 'Black Shadow',
444                 'track': 'Война - Негасимое Бездны Пламя!',
445             },
446             'params': {
447                 'skip_download': True,
448             },
449         }],
450         'skip': 'Requires vk account credentials',
451     }, {
452         # single YouTube embed, no leading -
453         'url': 'https://vk.com/wall85155021_6319',
454         'info_dict': {
455             'id': '85155021_6319',
456             'title': 'Sergey Gorbunov - Wall post 85155021_6319',
457         },
458         'playlist_count': 1,
459         'skip': 'Requires vk account credentials',
460     }, {
461         # wall page URL
462         'url': 'https://vk.com/wall-23538238_35',
463         'only_matching': True,
464     }, {
465         # mobile wall page URL
466         'url': 'https://m.vk.com/wall-23538238_35',
467         'only_matching': True,
468     }]
469
470     def _real_extract(self, url):
471         post_id = self._match_id(url)
472
473         wall_url = 'https://vk.com/wall%s' % post_id
474
475         post_id = remove_start(post_id, '-')
476
477         webpage = self._download_webpage(wall_url, post_id)
478
479         error = self._html_search_regex(
480             r'>Error</div>\s*<div[^>]+class=["\']body["\'][^>]*>([^<]+)',
481             webpage, 'error', default=None)
482         if error:
483             raise ExtractorError('VK said: %s' % error, expected=True)
484
485         description = clean_html(get_element_by_class('wall_post_text', webpage))
486         uploader = clean_html(get_element_by_class(
487             'fw_post_author', webpage)) or self._og_search_description(webpage)
488         thumbnail = self._og_search_thumbnail(webpage)
489
490         entries = []
491
492         for audio in re.finditer(r'''(?sx)
493                             <input[^>]+
494                                 id=(?P<q1>["\'])audio_info(?P<id>\d+_\d+).*?(?P=q1)[^>]+
495                                 value=(?P<q2>["\'])(?P<url>http.+?)(?P=q2)
496                                 .+?
497                             </table>''', webpage):
498             audio_html = audio.group(0)
499             audio_id = audio.group('id')
500             duration = parse_duration(get_element_by_class('duration', audio_html))
501             track = self._html_search_regex(
502                 r'<span[^>]+id=["\']title%s[^>]*>([^<]+)' % audio_id,
503                 audio_html, 'title', default=None)
504             artist = self._html_search_regex(
505                 r'>([^<]+)</a></b>\s*&ndash', audio_html,
506                 'artist', default=None)
507             entries.append({
508                 'id': audio_id,
509                 'url': audio.group('url'),
510                 'title': '%s - %s' % (artist, track) if artist and track else audio_id,
511                 'thumbnail': thumbnail,
512                 'duration': duration,
513                 'uploader': uploader,
514                 'artist': artist,
515                 'track': track,
516             })
517
518         for video in re.finditer(
519                 r'<a[^>]+href=(["\'])(?P<url>/video(?:-?[\d_]+).*?)\1', webpage):
520             entries.append(self.url_result(
521                 compat_urlparse.urljoin(url, video.group('url')), VKIE.ie_key()))
522
523         title = 'Wall post %s' % post_id
524
525         return self.playlist_result(
526             orderedSet(entries), post_id,
527             '%s - %s' % (uploader, title) if uploader else title,
528             description)