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