]>
Commit | Line | Data |
---|---|---|
dcdb292f | 1 | # coding: utf-8 |
1bf32108 S |
2 | from __future__ import unicode_literals |
3 | ||
4 | import re | |
75229763 S |
5 | import time |
6 | import hashlib | |
1bf32108 S |
7 | |
8 | from .common import InfoExtractor | |
1cc79574 PH |
9 | from ..compat import ( |
10 | compat_str, | |
2469a6ae | 11 | compat_urlparse, |
1cc79574 PH |
12 | ) |
13 | from ..utils import ( | |
c63b3090 | 14 | clean_html, |
1cc79574 | 15 | ExtractorError, |
815ac029 S |
16 | int_or_none, |
17 | float_or_none, | |
18 | parse_iso8601, | |
5c2266df | 19 | sanitized_Request, |
6e6bc8da | 20 | urlencode_postdata, |
1bf32108 S |
21 | ) |
22 | ||
23 | ||
24 | class NocoIE(InfoExtractor): | |
5886b38d | 25 | _VALID_URL = r'https?://(?:(?:www\.)?noco\.tv/emission/|player\.noco\.tv/\?idvideo=)(?P<id>\d+)' |
f3bc2812 | 26 | _LOGIN_URL = 'https://noco.tv/do.php' |
75229763 | 27 | _API_URL_TEMPLATE = 'https://api.noco.tv/1.1/%s?ts=%s&tk=%s' |
cb6444e1 | 28 | _SUB_LANG_TEMPLATE = '&sub_lang=%s' |
c63b3090 | 29 | _NETRC_MACHINE = 'noco' |
1bf32108 | 30 | |
ff9d68e7 AD |
31 | _TESTS = [ |
32 | { | |
33 | 'url': 'http://noco.tv/emission/11538/nolife/ami-ami-idol-hello-france/', | |
34 | 'md5': '0a993f0058ddbcd902630b2047ef710e', | |
35 | 'info_dict': { | |
36 | 'id': '11538', | |
37 | 'ext': 'mp4', | |
38 | 'title': 'Ami Ami Idol - Hello! France', | |
39 | 'description': 'md5:4eaab46ab68fa4197a317a88a53d3b86', | |
40 | 'upload_date': '20140412', | |
41 | 'uploader': 'Nolife', | |
42 | 'uploader_id': 'NOL', | |
43 | 'duration': 2851.2, | |
44 | }, | |
45 | 'skip': 'Requires noco account', | |
d7e7dedb | 46 | }, |
ff9d68e7 AD |
47 | { |
48 | 'url': 'http://noco.tv/emission/12610/lbl42/the-guild/s01e01-wake-up-call', | |
49 | 'md5': 'c190f1f48e313c55838f1f412225934d', | |
50 | 'info_dict': { | |
51 | 'id': '12610', | |
52 | 'ext': 'mp4', | |
53 | 'title': 'The Guild #1 - Wake-Up Call', | |
01e4b1ee | 54 | 'timestamp': 1403863200, |
ff9d68e7 AD |
55 | 'upload_date': '20140627', |
56 | 'uploader': 'LBL42', | |
57 | 'uploader_id': 'LBL', | |
58 | 'duration': 233.023, | |
59 | }, | |
60 | 'skip': 'Requires noco account', | |
61 | } | |
62 | ] | |
1bf32108 | 63 | |
c63b3090 S |
64 | def _real_initialize(self): |
65 | self._login() | |
66 | ||
67 | def _login(self): | |
68217024 | 68 | username, password = self._get_login_info() |
c63b3090 S |
69 | if username is None: |
70 | return | |
71 | ||
ad303303 | 72 | login = self._download_json( |
e4d95865 | 73 | self._LOGIN_URL, None, 'Logging in', |
ad303303 S |
74 | data=urlencode_postdata({ |
75 | 'a': 'login', | |
76 | 'cookie': '1', | |
77 | 'username': username, | |
78 | 'password': password, | |
79 | }), | |
80 | headers={ | |
81 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', | |
82 | }) | |
c63b3090 S |
83 | |
84 | if 'erreur' in login: | |
75229763 S |
85 | raise ExtractorError('Unable to login: %s' % clean_html(login['erreur']), expected=True) |
86 | ||
2469a6ae S |
87 | @staticmethod |
88 | def _ts(): | |
89 | return int(time.time() * 1000) | |
90 | ||
cb6444e1 | 91 | def _call_api(self, path, video_id, note, sub_lang=None): |
2469a6ae | 92 | ts = compat_str(self._ts() + self._ts_offset) |
f7d159cf | 93 | tk = hashlib.md5((hashlib.md5(ts.encode('ascii')).hexdigest() + '#8S?uCraTedap6a').encode('ascii')).hexdigest() |
75229763 | 94 | url = self._API_URL_TEMPLATE % (path, ts, tk) |
cb6444e1 S |
95 | if sub_lang: |
96 | url += self._SUB_LANG_TEMPLATE % sub_lang | |
75229763 | 97 | |
2469a6ae S |
98 | request = sanitized_Request(url) |
99 | request.add_header('Referer', self._referer) | |
100 | ||
101 | resp = self._download_json(request, video_id, note) | |
75229763 S |
102 | |
103 | if isinstance(resp, dict) and resp.get('error'): | |
104 | self._raise_error(resp['error'], resp['description']) | |
105 | ||
106 | return resp | |
107 | ||
108 | def _raise_error(self, error, description): | |
109 | raise ExtractorError( | |
110 | '%s returned error: %s - %s' % (self.IE_NAME, error, description), | |
111 | expected=True) | |
c63b3090 | 112 | |
1bf32108 | 113 | def _real_extract(self, url): |
7824e1f6 | 114 | video_id = self._match_id(url) |
1bf32108 | 115 | |
2469a6ae S |
116 | # Timestamp adjustment offset between server time and local time |
117 | # must be calculated in order to use timestamps closest to server's | |
067aa17e | 118 | # in all API requests (see https://github.com/ytdl-org/youtube-dl/issues/7864) |
2469a6ae S |
119 | webpage = self._download_webpage(url, video_id) |
120 | ||
121 | player_url = self._search_regex( | |
122 | r'(["\'])(?P<player>https?://noco\.tv/(?:[^/]+/)+NocoPlayer.+?\.swf.*?)\1', | |
123 | webpage, 'noco player', group='player', | |
124 | default='http://noco.tv/cdata/js/player/NocoPlayer-v1.2.40.swf') | |
125 | ||
126 | qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(player_url).query) | |
127 | ts = int_or_none(qs.get('ts', [None])[0]) | |
128 | self._ts_offset = ts - self._ts() if ts else 0 | |
129 | self._referer = player_url | |
130 | ||
75229763 S |
131 | medias = self._call_api( |
132 | 'shows/%s/medias' % video_id, | |
133 | video_id, 'Downloading video JSON') | |
134 | ||
7212560f AD |
135 | show = self._call_api( |
136 | 'shows/by_id/%s' % video_id, | |
137 | video_id, 'Downloading show JSON')[0] | |
138 | ||
6568382d S |
139 | options = self._call_api( |
140 | 'users/init', video_id, | |
141 | 'Downloading user options JSON')['options'] | |
142 | audio_lang_pref = options.get('audio_language') or options.get('language', 'fr') | |
143 | ||
144 | if audio_lang_pref == 'original': | |
145 | audio_lang_pref = show['original_lang'] | |
7212560f | 146 | if len(medias) == 1: |
6568382d S |
147 | audio_lang_pref = list(medias.keys())[0] |
148 | elif audio_lang_pref not in medias: | |
149 | audio_lang_pref = 'fr' | |
7212560f | 150 | |
75229763 S |
151 | qualities = self._call_api( |
152 | 'qualities', | |
153 | video_id, 'Downloading qualities JSON') | |
1bf32108 S |
154 | |
155 | formats = [] | |
156 | ||
6568382d S |
157 | for audio_lang, audio_lang_dict in medias.items(): |
158 | preference = 1 if audio_lang == audio_lang_pref else 0 | |
159 | for sub_lang, lang_dict in audio_lang_dict['video_list'].items(): | |
160 | for format_id, fmt in lang_dict['quality_list'].items(): | |
161 | format_id_extended = 'audio-%s_sub-%s_%s' % (audio_lang, sub_lang, format_id) | |
162 | ||
163 | video = self._call_api( | |
164 | 'shows/%s/video/%s/%s' % (video_id, format_id.lower(), audio_lang), | |
165 | video_id, 'Downloading %s video JSON' % format_id_extended, | |
166 | sub_lang if sub_lang != 'none' else None) | |
167 | ||
168 | file_url = video['file'] | |
169 | if not file_url: | |
170 | continue | |
171 | ||
172 | if file_url in ['forbidden', 'not found']: | |
173 | popmessage = video['popmessage'] | |
174 | self._raise_error(popmessage['title'], popmessage['message']) | |
175 | ||
176 | formats.append({ | |
177 | 'url': file_url, | |
178 | 'format_id': format_id_extended, | |
815ac029 S |
179 | 'width': int_or_none(fmt.get('res_width')), |
180 | 'height': int_or_none(fmt.get('res_lines')), | |
9dc1d94a S |
181 | 'abr': int_or_none(fmt.get('audiobitrate'), 1000), |
182 | 'vbr': int_or_none(fmt.get('videobitrate'), 1000), | |
815ac029 S |
183 | 'filesize': int_or_none(fmt.get('filesize')), |
184 | 'format_note': qualities[format_id].get('quality_name'), | |
185 | 'quality': qualities[format_id].get('priority'), | |
f983b875 | 186 | 'language_preference': preference, |
6568382d | 187 | }) |
1bf32108 S |
188 | |
189 | self._sort_formats(formats) | |
190 | ||
815ac029 | 191 | timestamp = parse_iso8601(show.get('online_date_start_utc'), ' ') |
01e21b89 YCH |
192 | |
193 | if timestamp is not None and timestamp < 0: | |
194 | timestamp = None | |
195 | ||
815ac029 S |
196 | uploader = show.get('partner_name') |
197 | uploader_id = show.get('partner_key') | |
198 | duration = float_or_none(show.get('duration_ms'), 1000) | |
75229763 S |
199 | |
200 | thumbnails = [] | |
201 | for thumbnail_key, thumbnail_url in show.items(): | |
202 | m = re.search(r'^screenshot_(?P<width>\d+)x(?P<height>\d+)$', thumbnail_key) | |
203 | if not m: | |
204 | continue | |
205 | thumbnails.append({ | |
206 | 'url': thumbnail_url, | |
207 | 'width': int(m.group('width')), | |
208 | 'height': int(m.group('height')), | |
209 | }) | |
1bf32108 S |
210 | |
211 | episode = show.get('show_TT') or show.get('show_OT') | |
212 | family = show.get('family_TT') or show.get('family_OT') | |
213 | episode_number = show.get('episode_number') | |
214 | ||
215 | title = '' | |
216 | if family: | |
217 | title += family | |
218 | if episode_number: | |
219 | title += ' #' + compat_str(episode_number) | |
220 | if episode: | |
1a1251e8 | 221 | title += ' - ' + compat_str(episode) |
1bf32108 S |
222 | |
223 | description = show.get('show_resume') or show.get('family_resume') | |
224 | ||
225 | return { | |
226 | 'id': video_id, | |
227 | 'title': title, | |
228 | 'description': description, | |
75229763 | 229 | 'thumbnails': thumbnails, |
815ac029 | 230 | 'timestamp': timestamp, |
1bf32108 S |
231 | 'uploader': uploader, |
232 | 'uploader_id': uploader_id, | |
233 | 'duration': duration, | |
234 | 'formats': formats, | |
5f6a1245 | 235 | } |