[googledrive] Add support for subtitles (fixes #13619)
[youtube-dl] / youtube_dl / extractor / googledrive.py
1 from __future__ import unicode_literals
2
3 import re
4
5 from .common import InfoExtractor
6 from ..utils import (
7     ExtractorError,
8     int_or_none,
9     lowercase_escape,
10     error_to_compat_str,
11     update_url_query,
12 )
13
14
15 class GoogleDriveIE(InfoExtractor):
16     _VALID_URL = r'https?://(?:(?:docs|drive)\.google\.com/(?:uc\?.*?id=|file/d/)|video\.google\.com/get_player\?.*?docid=)(?P<id>[a-zA-Z0-9_-]{28,})'
17     _TESTS = [{
18         'url': 'https://drive.google.com/file/d/0ByeS4oOUV-49Zzh4R1J6R09zazQ/edit?pli=1',
19         'md5': 'd109872761f7e7ecf353fa108c0dbe1e',
20         'info_dict': {
21             'id': '0ByeS4oOUV-49Zzh4R1J6R09zazQ',
22             'ext': 'mp4',
23             'title': 'Big Buck Bunny.mp4',
24             'duration': 45,
25         }
26     }, {
27         # video id is longer than 28 characters
28         'url': 'https://drive.google.com/file/d/1ENcQ_jeCuj7y19s66_Ou9dRP4GKGsodiDQ/edit',
29         'md5': 'c230c67252874fddd8170e3fd1a45886',
30         'info_dict': {
31             'id': '1ENcQ_jeCuj7y19s66_Ou9dRP4GKGsodiDQ',
32             'ext': 'mp4',
33             'title': 'Andreea Banica feat Smiley - Hooky Song (Official Video).mp4',
34             'duration': 189,
35         },
36         'only_matching': True
37     }]
38     _FORMATS_EXT = {
39         '5': 'flv',
40         '6': 'flv',
41         '13': '3gp',
42         '17': '3gp',
43         '18': 'mp4',
44         '22': 'mp4',
45         '34': 'flv',
46         '35': 'flv',
47         '36': '3gp',
48         '37': 'mp4',
49         '38': 'mp4',
50         '43': 'webm',
51         '44': 'webm',
52         '45': 'webm',
53         '46': 'webm',
54         '59': 'mp4',
55     }
56     _BASE_URL_CAPTIONS = 'https://drive.google.com/timedtext'
57     _CAPTIONS_ENTRY_TAG = {
58         'subtitles': 'track',
59         'automatic_captions': 'target',
60     }
61     _caption_formats_ext = []
62     _captions_by_country_xml = None
63
64     @staticmethod
65     def _extract_url(webpage):
66         mobj = re.search(
67             r'<iframe[^>]+src="https?://(?:video\.google\.com/get_player\?.*?docid=|(?:docs|drive)\.google\.com/file/d/)(?P<id>[a-zA-Z0-9_-]{28,})',
68             webpage)
69         if mobj:
70             return 'https://drive.google.com/file/d/%s' % mobj.group('id')
71
72     def _set_captions_data(self, video_id, video_subtitles_id, hl):
73         try:
74             self._captions_by_country_xml = self._download_xml(self._BASE_URL_CAPTIONS, video_id, query={
75                 'id': video_id,
76                 'vid': video_subtitles_id,
77                 'hl': hl,
78                 'v': video_id,
79                 'type': 'list',
80                 'tlangs': '1',
81                 'fmts': '1',
82                 'vssids': '1',
83             })
84         except ExtractorError as ee:
85             self.report_warning('unable to download video subtitles: %s' % error_to_compat_str(ee))
86         if self._captions_by_country_xml is not None:
87             caption_available_extensions = self._captions_by_country_xml.findall('format')
88             for caption_extension in caption_available_extensions:
89                 if caption_extension.attrib.get('fmt_code') and not caption_extension.attrib.get('default'):
90                     self._caption_formats_ext.append(caption_extension.attrib['fmt_code'])
91
92     def _get_captions_by_type(self, video_id, video_subtitles_id, caption_type, caption_original_lang_code=None):
93         if not video_subtitles_id or not caption_type:
94             return None
95         captions = {}
96         for caption_entry in self._captions_by_country_xml.findall(self._CAPTIONS_ENTRY_TAG[caption_type]):
97             caption_lang_code = caption_entry.attrib.get('lang_code')
98             if not caption_lang_code:
99                 continue
100             caption_format_data = []
101             for caption_format in self._caption_formats_ext:
102                 query = {
103                     'vid': video_subtitles_id,
104                     'v': video_id,
105                     'fmt': caption_format,
106                     'lang': caption_lang_code if caption_original_lang_code is None else caption_original_lang_code,
107                     'type': 'track',
108                     'name': '',
109                     'kind': '',
110                 }
111                 if caption_original_lang_code is not None:
112                     query.update({'tlang': caption_lang_code})
113                 caption_format_data.append({
114                     'url': update_url_query(self._BASE_URL_CAPTIONS, query),
115                     'ext': caption_format,
116                 })
117             captions[caption_lang_code] = caption_format_data
118         if not captions:
119             self.report_warning('video doesn\'t have %s' % caption_type.replace('_', ' '))
120         return captions
121
122     def _get_subtitles(self, video_id, video_subtitles_id, hl):
123         if not video_subtitles_id or not hl:
124             return None
125         if self._captions_by_country_xml is None:
126             self._set_captions_data(video_id, video_subtitles_id, hl)
127             if self._captions_by_country_xml is None:
128                 return None
129         return self._get_captions_by_type(video_id, video_subtitles_id, 'subtitles')
130
131     def _get_automatic_captions(self, video_id, video_subtitles_id, hl):
132         if not video_subtitles_id or not hl:
133             return None
134         if self._captions_by_country_xml is None:
135             self._set_captions_data(video_id, video_subtitles_id, hl)
136             if self._captions_by_country_xml is None:
137                 return None
138         self.to_screen('%s: Looking for automatic captions' % video_id)
139         subtitle_original_track = self._captions_by_country_xml.find('track')
140         if subtitle_original_track is None:
141             return None
142         subtitle_original_lang_code = subtitle_original_track.attrib.get('lang_code')
143         if not subtitle_original_lang_code:
144             return None
145         return self._get_captions_by_type(video_id, video_subtitles_id, 'automatic_captions', subtitle_original_lang_code)
146
147     def _real_extract(self, url):
148         video_id = self._match_id(url)
149         webpage = self._download_webpage(
150             'http://docs.google.com/file/d/%s' % video_id, video_id)
151
152         reason = self._search_regex(r'"reason"\s*,\s*"([^"]+)', webpage, 'reason', default=None)
153         if reason:
154             raise ExtractorError(reason)
155
156         title = self._search_regex(r'"title"\s*,\s*"([^"]+)', webpage, 'title')
157         duration = int_or_none(self._search_regex(
158             r'"length_seconds"\s*,\s*"([^"]+)', webpage, 'length seconds', default=None))
159         fmt_stream_map = self._search_regex(
160             r'"fmt_stream_map"\s*,\s*"([^"]+)', webpage, 'fmt stream map').split(',')
161         fmt_list = self._search_regex(r'"fmt_list"\s*,\s*"([^"]+)', webpage, 'fmt_list').split(',')
162
163         resolutions = {}
164         for fmt in fmt_list:
165             mobj = re.search(
166                 r'^(?P<format_id>\d+)/(?P<width>\d+)[xX](?P<height>\d+)', fmt)
167             if mobj:
168                 resolutions[mobj.group('format_id')] = (
169                     int(mobj.group('width')), int(mobj.group('height')))
170
171         formats = []
172         for fmt_stream in fmt_stream_map:
173             fmt_stream_split = fmt_stream.split('|')
174             if len(fmt_stream_split) < 2:
175                 continue
176             format_id, format_url = fmt_stream_split[:2]
177             f = {
178                 'url': lowercase_escape(format_url),
179                 'format_id': format_id,
180                 'ext': self._FORMATS_EXT[format_id],
181             }
182             resolution = resolutions.get(format_id)
183             if resolution:
184                 f.update({
185                     'width': resolution[0],
186                     'height': resolution[1],
187                 })
188             formats.append(f)
189         self._sort_formats(formats)
190
191         hl = self._search_regex(
192             r'"hl"\s*,\s*"([^"]+)', webpage, 'hl', default=None)
193         video_subtitles_id = None
194         ttsurl = self._search_regex(
195             r'"ttsurl"\s*,\s*"([^"]+)', webpage, 'ttsurl', default=None)
196         if ttsurl:
197             # the video Id for subtitles will be the last value in the ttsurl query string
198             video_subtitles_id = ttsurl.encode('utf-8').decode('unicode_escape').split('=')[-1]
199
200         return {
201             'id': video_id,
202             'title': title,
203             'thumbnail': self._og_search_thumbnail(webpage, default=None),
204             'duration': duration,
205             'formats': formats,
206             'subtitles': self.extract_subtitles(video_id, video_subtitles_id, hl),
207             'automatic_captions': self.extract_automatic_captions(video_id, video_subtitles_id, hl),
208         }