]>
Commit | Line | Data |
---|---|---|
b68a2613 | 1 | # coding: utf-8 |
3647136f S |
2 | from __future__ import unicode_literals |
3 | ||
864d5e72 | 4 | import re |
5 | ||
3647136f | 6 | from .common import InfoExtractor |
b68a2613 | 7 | from ..compat import ( |
cccedc1a | 8 | compat_HTTPError, |
1f048735 | 9 | compat_urllib_parse_unquote, |
b68a2613 S |
10 | ) |
11 | from ..utils import ( | |
12 | determine_ext, | |
cccedc1a | 13 | ExtractorError, |
b68a2613 S |
14 | int_or_none, |
15 | parse_iso8601, | |
5c2266df | 16 | sanitized_Request, |
30a45388 | 17 | HEADRequest, |
864d5e72 | 18 | url_basename, |
b68a2613 | 19 | ) |
3647136f S |
20 | |
21 | ||
22 | class ViewsterIE(InfoExtractor): | |
92085e70 | 23 | _VALID_URL = r'https?://(?:www\.)?viewster\.com/(?:serie|movie)/(?P<id>\d+-\d+-\d+)' |
7be5a62e | 24 | _TESTS = [{ |
b68a2613 | 25 | # movie, Type=Movie |
7be5a62e | 26 | 'url': 'http://www.viewster.com/movie/1140-11855-000/the-listening-project/', |
cb4e4219 | 27 | 'md5': 'e642d1b27fcf3a4ffa79f194f5adde36', |
7be5a62e S |
28 | 'info_dict': { |
29 | 'id': '1140-11855-000', | |
cb4e4219 | 30 | 'ext': 'mp4', |
b68a2613 S |
31 | 'title': 'The listening Project', |
32 | 'description': 'md5:bac720244afd1a8ea279864e67baa071', | |
33 | 'timestamp': 1214870400, | |
34 | 'upload_date': '20080701', | |
35 | 'duration': 4680, | |
36 | }, | |
7be5a62e | 37 | }, { |
b68a2613 S |
38 | # series episode, Type=Episode |
39 | 'url': 'http://www.viewster.com/serie/1284-19427-001/the-world-and-a-wall/', | |
cb4e4219 | 40 | 'md5': '9243079a8531809efe1b089db102c069', |
7be5a62e | 41 | 'info_dict': { |
b68a2613 | 42 | 'id': '1284-19427-001', |
cb4e4219 | 43 | 'ext': 'mp4', |
b68a2613 S |
44 | 'title': 'The World and a Wall', |
45 | 'description': 'md5:24814cf74d3453fdf5bfef9716d073e3', | |
46 | 'timestamp': 1428192000, | |
47 | 'upload_date': '20150405', | |
48 | 'duration': 1500, | |
49 | }, | |
50 | }, { | |
51 | # serie, Type=Serie | |
52 | 'url': 'http://www.viewster.com/serie/1303-19426-000/', | |
53 | 'info_dict': { | |
54 | 'id': '1303-19426-000', | |
55 | 'title': 'Is It Wrong to Try to Pick up Girls in a Dungeon?', | |
56 | 'description': 'md5:eeda9bef25b0d524b3a29a97804c2f11', | |
57 | }, | |
58 | 'playlist_count': 13, | |
59 | }, { | |
60 | # unfinished serie, no Type | |
61 | 'url': 'http://www.viewster.com/serie/1284-19427-000/baby-steps-season-2/', | |
62 | 'info_dict': { | |
63 | 'id': '1284-19427-000', | |
64 | 'title': 'Baby Steps—Season 2', | |
65 | 'description': 'md5:e7097a8fc97151e25f085c9eb7a1cdb1', | |
66 | }, | |
67 | 'playlist_mincount': 16, | |
7ce50a35 S |
68 | }, { |
69 | # geo restricted series | |
70 | 'url': 'https://www.viewster.com/serie/1280-18794-002/', | |
71 | 'only_matching': True, | |
72 | }, { | |
73 | # geo restricted video | |
74 | 'url': 'https://www.viewster.com/serie/1280-18794-002/what-is-extraterritoriality-lawo/', | |
75 | 'only_matching': True, | |
7be5a62e | 76 | }] |
3647136f S |
77 | |
78 | _ACCEPT_HEADER = 'application/json, text/javascript, */*; q=0.01' | |
79 | ||
0ba9e3ca | 80 | def _download_json(self, url, video_id, note='Downloading JSON metadata', fatal=True, query={}): |
5c2266df | 81 | request = sanitized_Request(url) |
3647136f | 82 | request.add_header('Accept', self._ACCEPT_HEADER) |
b68a2613 | 83 | request.add_header('Auth-token', self._AUTH_TOKEN) |
0ba9e3ca | 84 | return super(ViewsterIE, self)._download_json(request, video_id, note, fatal=fatal, query=query) |
3647136f | 85 | |
b68a2613 S |
86 | def _real_extract(self, url): |
87 | video_id = self._match_id(url) | |
799207e8 | 88 | # Get 'api_token' cookie |
92085e70 | 89 | self._request_webpage(HEADRequest('http://www.viewster.com/'), video_id) |
90 | cookies = self._get_cookies('http://www.viewster.com/') | |
1f048735 | 91 | self._AUTH_TOKEN = compat_urllib_parse_unquote(cookies['api_token'].value) |
7be5a62e | 92 | |
b68a2613 S |
93 | info = self._download_json( |
94 | 'https://public-api.viewster.com/search/%s' % video_id, | |
95 | video_id, 'Downloading entry JSON') | |
7be5a62e | 96 | |
b68a2613 | 97 | entry_id = info.get('Id') or info['id'] |
7be5a62e | 98 | |
b68a2613 | 99 | # unfinished serie has no Type |
d0fed4ac | 100 | if info.get('Type') in ('Serie', None): |
cccedc1a S |
101 | try: |
102 | episodes = self._download_json( | |
103 | 'https://public-api.viewster.com/series/%s/episodes' % entry_id, | |
104 | video_id, 'Downloading series JSON') | |
105 | except ExtractorError as e: | |
106 | if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404: | |
107 | self.raise_geo_restricted() | |
108 | else: | |
109 | raise | |
b68a2613 S |
110 | entries = [ |
111 | self.url_result( | |
112 | 'http://www.viewster.com/movie/%s' % episode['OriginId'], 'Viewster') | |
113 | for episode in episodes] | |
c84683c8 | 114 | title = (info.get('Title') or info['Synopsis']['Title']).strip() |
b68a2613 S |
115 | description = info.get('Synopsis', {}).get('Detailed') |
116 | return self.playlist_result(entries, video_id, title, description) | |
7be5a62e | 117 | |
b68a2613 | 118 | formats = [] |
0ba9e3ca | 119 | for language_set in info.get('LanguageSets', []): |
120 | manifest_url = None | |
121 | m3u8_formats = [] | |
122 | audio = language_set.get('Audio') or '' | |
123 | subtitle = language_set.get('Subtitle') or '' | |
124 | base_format_id = audio | |
125 | if subtitle: | |
126 | base_format_id += '-%s' % subtitle | |
127 | ||
128 | def concat(suffix, sep='-'): | |
129 | return (base_format_id + '%s%s' % (sep, suffix)) if base_format_id else suffix | |
130 | ||
131 | for media_type in ('application/f4m+xml', 'application/x-mpegURL', 'video/mp4'): | |
132 | media = self._download_json( | |
133 | 'https://public-api.viewster.com/movies/%s/video' % entry_id, | |
134 | video_id, 'Downloading %s JSON' % concat(media_type, ' '), fatal=False, query={ | |
135 | 'mediaType': media_type, | |
136 | 'language': audio, | |
137 | 'subtitle': subtitle, | |
138 | }) | |
139 | if not media: | |
c14dc00d | 140 | continue |
0ba9e3ca | 141 | video_url = media.get('Uri') |
142 | if not video_url: | |
c14dc00d | 143 | continue |
0ba9e3ca | 144 | ext = determine_ext(video_url) |
145 | if ext == 'f4m': | |
146 | manifest_url = video_url | |
147 | video_url += '&' if '?' in video_url else '?' | |
148 | video_url += 'hdcore=3.2.0&plugin=flowplayer-3.2.0.1' | |
149 | formats.extend(self._extract_f4m_formats( | |
150 | video_url, video_id, f4m_id=concat('hds'))) | |
151 | elif ext == 'm3u8': | |
152 | manifest_url = video_url | |
153 | m3u8_formats = self._extract_m3u8_formats( | |
154 | video_url, video_id, 'mp4', m3u8_id=concat('hls'), | |
155 | fatal=False) # m3u8 sometimes fail | |
156 | if m3u8_formats: | |
157 | formats.extend(m3u8_formats) | |
f1f87909 | 158 | else: |
0ba9e3ca | 159 | qualities_basename = self._search_regex( |
ec85ded8 | 160 | r'/([^/]+)\.csmil/', |
0ba9e3ca | 161 | manifest_url, 'qualities basename', default=None) |
162 | if not qualities_basename: | |
163 | continue | |
164 | QUALITIES_RE = r'((,\d+k)+,?)' | |
165 | qualities = self._search_regex( | |
166 | QUALITIES_RE, qualities_basename, | |
167 | 'qualities', default=None) | |
168 | if not qualities: | |
169 | continue | |
170 | qualities = list(map(lambda q: int(q[:-1]), qualities.strip(',').split(','))) | |
171 | qualities.sort() | |
172 | http_template = re.sub(QUALITIES_RE, r'%dk', qualities_basename) | |
173 | http_url_basename = url_basename(video_url) | |
174 | if m3u8_formats: | |
175 | self._sort_formats(m3u8_formats) | |
176 | m3u8_formats = list(filter( | |
177 | lambda f: f.get('vcodec') != 'none' and f.get('resolution') != 'multiple', | |
178 | m3u8_formats)) | |
179 | if len(qualities) == len(m3u8_formats): | |
180 | for q, m3u8_format in zip(qualities, m3u8_formats): | |
181 | f = m3u8_format.copy() | |
182 | f.update({ | |
183 | 'url': video_url.replace(http_url_basename, http_template % q), | |
184 | 'format_id': f['format_id'].replace('hls', 'http'), | |
185 | 'protocol': 'http', | |
186 | }) | |
187 | formats.append(f) | |
188 | else: | |
189 | for q in qualities: | |
190 | formats.append({ | |
191 | 'url': video_url.replace(http_url_basename, http_template % q), | |
192 | 'ext': 'mp4', | |
193 | 'format_id': 'http-%d' % q, | |
194 | 'tbr': q, | |
195 | }) | |
196 | ||
197 | if not formats and not info.get('VODSettings'): | |
9612f233 S |
198 | self.raise_geo_restricted() |
199 | ||
b68a2613 | 200 | self._sort_formats(formats) |
7be5a62e | 201 | |
485139c1 | 202 | synopsis = info.get('Synopsis') or {} |
b68a2613 | 203 | # Prefer title outside synopsis since it's less messy |
c84683c8 | 204 | title = (info.get('Title') or synopsis['Title']).strip() |
485139c1 | 205 | description = synopsis.get('Detailed') or (info.get('Synopsis') or {}).get('Short') |
b68a2613 S |
206 | duration = int_or_none(info.get('Duration')) |
207 | timestamp = parse_iso8601(info.get('ReleaseDate')) | |
208 | ||
209 | return { | |
210 | 'id': video_id, | |
211 | 'title': title, | |
212 | 'description': description, | |
213 | 'timestamp': timestamp, | |
214 | 'duration': duration, | |
215 | 'formats': formats, | |
216 | } |