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