[nrk] Improve geo restriction detection and use geo bypass mechanism
[youtube-dl] / youtube_dl / extractor / nrk.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import re
5
6 from .common import InfoExtractor
7 from ..compat import compat_urllib_parse_unquote
8 from ..utils import (
9     ExtractorError,
10     int_or_none,
11     parse_age_limit,
12     parse_duration,
13 )
14
15
16 class NRKBaseIE(InfoExtractor):
17     def _real_extract(self, url):
18         video_id = self._match_id(url)
19
20         data = self._download_json(
21             'http://%s/mediaelement/%s' % (self._API_HOST, video_id),
22             video_id, 'Downloading mediaelement JSON')
23
24         title = data.get('fullTitle') or data.get('mainTitle') or data['title']
25         video_id = data.get('id') or video_id
26
27         entries = []
28
29         conviva = data.get('convivaStatistics') or {}
30         live = (data.get('mediaElementType') == 'Live' or
31                 data.get('isLive') is True or conviva.get('isLive'))
32
33         def make_title(t):
34             return self._live_title(t) if live else t
35
36         media_assets = data.get('mediaAssets')
37         if media_assets and isinstance(media_assets, list):
38             def video_id_and_title(idx):
39                 return ((video_id, title) if len(media_assets) == 1
40                         else ('%s-%d' % (video_id, idx), '%s (Part %d)' % (title, idx)))
41             for num, asset in enumerate(media_assets, 1):
42                 asset_url = asset.get('url')
43                 if not asset_url:
44                     continue
45                 formats = self._extract_akamai_formats(asset_url, video_id)
46                 if not formats:
47                     continue
48                 self._sort_formats(formats)
49
50                 # Some f4m streams may not work with hdcore in fragments' URLs
51                 for f in formats:
52                     extra_param = f.get('extra_param_to_segment_url')
53                     if extra_param and 'hdcore' in extra_param:
54                         del f['extra_param_to_segment_url']
55
56                 entry_id, entry_title = video_id_and_title(num)
57                 duration = parse_duration(asset.get('duration'))
58                 subtitles = {}
59                 for subtitle in ('webVtt', 'timedText'):
60                     subtitle_url = asset.get('%sSubtitlesUrl' % subtitle)
61                     if subtitle_url:
62                         subtitles.setdefault('no', []).append({
63                             'url': compat_urllib_parse_unquote(subtitle_url)
64                         })
65                 entries.append({
66                     'id': asset.get('carrierId') or entry_id,
67                     'title': make_title(entry_title),
68                     'duration': duration,
69                     'subtitles': subtitles,
70                     'formats': formats,
71                 })
72
73         if not entries:
74             media_url = data.get('mediaUrl')
75             if media_url:
76                 formats = self._extract_akamai_formats(media_url, video_id)
77                 self._sort_formats(formats)
78                 duration = parse_duration(data.get('duration'))
79                 entries = [{
80                     'id': video_id,
81                     'title': make_title(title),
82                     'duration': duration,
83                     'formats': formats,
84                 }]
85
86         if not entries:
87             MESSAGES = {
88                 'ProgramRightsAreNotReady': 'Du kan dessverre ikke se eller høre programmet',
89                 'ProgramRightsHasExpired': 'Programmet har gått ut',
90                 'ProgramIsGeoBlocked': 'NRK har ikke rettigheter til å vise dette programmet utenfor Norge',
91             }
92             message_type = data.get('messageType', '')
93             # Can be ProgramIsGeoBlocked or ChannelIsGeoBlocked*
94             if 'IsGeoBlocked' in message_type:
95                 self.raise_geo_restricted(
96                     msg=MESSAGES.get('ProgramIsGeoBlocked'), countries=['NO'])
97             raise ExtractorError(
98                 '%s said: %s' % (self.IE_NAME, MESSAGES.get(
99                     message_type, message_type)),
100                 expected=True)
101
102         series = conviva.get('seriesName') or data.get('seriesTitle')
103         episode = conviva.get('episodeName') or data.get('episodeNumberOrDate')
104
105         season_number = None
106         episode_number = None
107         if data.get('mediaElementType') == 'Episode':
108             _season_episode = data.get('scoresStatistics', {}).get('springStreamStream') or \
109                 data.get('relativeOriginUrl', '')
110             EPISODENUM_RE = [
111                 r'/s(?P<season>\d{,2})e(?P<episode>\d{,2})\.',
112                 r'/sesong-(?P<season>\d{,2})/episode-(?P<episode>\d{,2})',
113             ]
114             season_number = int_or_none(self._search_regex(
115                 EPISODENUM_RE, _season_episode, 'season number',
116                 default=None, group='season'))
117             episode_number = int_or_none(self._search_regex(
118                 EPISODENUM_RE, _season_episode, 'episode number',
119                 default=None, group='episode'))
120
121         thumbnails = None
122         images = data.get('images')
123         if images and isinstance(images, dict):
124             web_images = images.get('webImages')
125             if isinstance(web_images, list):
126                 thumbnails = [{
127                     'url': image['imageUrl'],
128                     'width': int_or_none(image.get('width')),
129                     'height': int_or_none(image.get('height')),
130                 } for image in web_images if image.get('imageUrl')]
131
132         description = data.get('description')
133         category = data.get('mediaAnalytics', {}).get('category')
134
135         common_info = {
136             'description': description,
137             'series': series,
138             'episode': episode,
139             'season_number': season_number,
140             'episode_number': episode_number,
141             'categories': [category] if category else None,
142             'age_limit': parse_age_limit(data.get('legalAge')),
143             'thumbnails': thumbnails,
144         }
145
146         vcodec = 'none' if data.get('mediaType') == 'Audio' else None
147
148         # TODO: extract chapters when https://github.com/rg3/youtube-dl/pull/9409 is merged
149
150         for entry in entries:
151             entry.update(common_info)
152             for f in entry['formats']:
153                 f['vcodec'] = vcodec
154
155         return self.playlist_result(entries, video_id, title, description)
156
157
158 class NRKIE(NRKBaseIE):
159     _VALID_URL = r'''(?x)
160                         (?:
161                             nrk:|
162                             https?://
163                                 (?:
164                                     (?:www\.)?nrk\.no/video/PS\*|
165                                     v8-psapi\.nrk\.no/mediaelement/
166                                 )
167                             )
168                             (?P<id>[^/?#&]+)
169                         '''
170     _API_HOST = 'v8.psapi.nrk.no'
171     _TESTS = [{
172         # video
173         'url': 'http://www.nrk.no/video/PS*150533',
174         'md5': '2f7f6eeb2aacdd99885f355428715cfa',
175         'info_dict': {
176             'id': '150533',
177             'ext': 'mp4',
178             'title': 'Dompap og andre fugler i Piip-Show',
179             'description': 'md5:d9261ba34c43b61c812cb6b0269a5c8f',
180             'duration': 263,
181         }
182     }, {
183         # audio
184         'url': 'http://www.nrk.no/video/PS*154915',
185         # MD5 is unstable
186         'info_dict': {
187             'id': '154915',
188             'ext': 'flv',
189             'title': 'Slik høres internett ut når du er blind',
190             'description': 'md5:a621f5cc1bd75c8d5104cb048c6b8568',
191             'duration': 20,
192         }
193     }, {
194         'url': 'nrk:ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
195         'only_matching': True,
196     }, {
197         'url': 'https://v8-psapi.nrk.no/mediaelement/ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
198         'only_matching': True,
199     }]
200
201
202 class NRKTVIE(NRKBaseIE):
203     IE_DESC = 'NRK TV and NRK Radio'
204     _EPISODE_RE = r'(?P<id>[a-zA-Z]{4}\d{8})'
205     _VALID_URL = r'''(?x)
206                         https?://
207                             (?:tv|radio)\.nrk(?:super)?\.no/
208                             (?:serie/[^/]+|program)/
209                             (?![Ee]pisodes)%s
210                             (?:/\d{2}-\d{2}-\d{4})?
211                             (?:\#del=(?P<part_id>\d+))?
212                     ''' % _EPISODE_RE
213     _API_HOST = 'psapi-we.nrk.no'
214
215     _TESTS = [{
216         'url': 'https://tv.nrk.no/serie/20-spoersmaal-tv/MUHH48000314/23-05-2014',
217         'md5': '4e9ca6629f09e588ed240fb11619922a',
218         'info_dict': {
219             'id': 'MUHH48000314AA',
220             'ext': 'mp4',
221             'title': '20 spørsmål 23.05.2014',
222             'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
223             'duration': 1741,
224             'series': '20 spørsmål - TV',
225             'episode': '23.05.2014',
226         },
227     }, {
228         'url': 'https://tv.nrk.no/program/mdfp15000514',
229         'info_dict': {
230             'id': 'MDFP15000514CA',
231             'ext': 'mp4',
232             'title': 'Grunnlovsjubiléet - Stor ståhei for ingenting 24.05.2014',
233             'description': 'md5:89290c5ccde1b3a24bb8050ab67fe1db',
234             'duration': 4605,
235             'series': 'Kunnskapskanalen',
236             'episode': '24.05.2014',
237         },
238         'params': {
239             'skip_download': True,
240         },
241     }, {
242         # single playlist video
243         'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015#del=2',
244         'info_dict': {
245             'id': 'MSPO40010515-part2',
246             'ext': 'flv',
247             'title': 'Tour de Ski: Sprint fri teknikk, kvinner og menn 06.01.2015 (del 2:2)',
248             'description': 'md5:238b67b97a4ac7d7b4bf0edf8cc57d26',
249         },
250         'params': {
251             'skip_download': True,
252         },
253         'expected_warnings': ['Video is geo restricted'],
254         'skip': 'particular part is not supported currently',
255     }, {
256         'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015',
257         'playlist': [{
258             'info_dict': {
259                 'id': 'MSPO40010515AH',
260                 'ext': 'mp4',
261                 'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015 (Part 1)',
262                 'description': 'md5:c03aba1e917561eface5214020551b7a',
263                 'duration': 772,
264                 'series': 'Tour de Ski',
265                 'episode': '06.01.2015',
266             },
267             'params': {
268                 'skip_download': True,
269             },
270         }, {
271             'info_dict': {
272                 'id': 'MSPO40010515BH',
273                 'ext': 'mp4',
274                 'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015 (Part 2)',
275                 'description': 'md5:c03aba1e917561eface5214020551b7a',
276                 'duration': 6175,
277                 'series': 'Tour de Ski',
278                 'episode': '06.01.2015',
279             },
280             'params': {
281                 'skip_download': True,
282             },
283         }],
284         'info_dict': {
285             'id': 'MSPO40010515',
286             'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015',
287             'description': 'md5:c03aba1e917561eface5214020551b7a',
288         },
289         'expected_warnings': ['Video is geo restricted'],
290     }, {
291         'url': 'https://tv.nrk.no/serie/anno/KMTE50001317/sesong-3/episode-13',
292         'info_dict': {
293             'id': 'KMTE50001317AA',
294             'ext': 'mp4',
295             'title': 'Anno 13:30',
296             'description': 'md5:11d9613661a8dbe6f9bef54e3a4cbbfa',
297             'duration': 2340,
298             'series': 'Anno',
299             'episode': '13:30',
300             'season_number': 3,
301             'episode_number': 13,
302         },
303         'params': {
304             'skip_download': True,
305         },
306     }, {
307         'url': 'https://tv.nrk.no/serie/nytt-paa-nytt/MUHH46000317/27-01-2017',
308         'info_dict': {
309             'id': 'MUHH46000317AA',
310             'ext': 'mp4',
311             'title': 'Nytt på Nytt 27.01.2017',
312             'description': 'md5:5358d6388fba0ea6f0b6d11c48b9eb4b',
313             'duration': 1796,
314             'series': 'Nytt på nytt',
315             'episode': '27.01.2017',
316         },
317         'params': {
318             'skip_download': True,
319         },
320     }, {
321         'url': 'https://radio.nrk.no/serie/dagsnytt/NPUB21019315/12-07-2015#',
322         'only_matching': True,
323     }]
324
325
326 class NRKTVDirekteIE(NRKTVIE):
327     IE_DESC = 'NRK TV Direkte and NRK Radio Direkte'
328     _VALID_URL = r'https?://(?:tv|radio)\.nrk\.no/direkte/(?P<id>[^/?#&]+)'
329
330     _TESTS = [{
331         'url': 'https://tv.nrk.no/direkte/nrk1',
332         'only_matching': True,
333     }, {
334         'url': 'https://radio.nrk.no/direkte/p1_oslo_akershus',
335         'only_matching': True,
336     }]
337
338
339 class NRKPlaylistBaseIE(InfoExtractor):
340     def _extract_description(self, webpage):
341         pass
342
343     def _real_extract(self, url):
344         playlist_id = self._match_id(url)
345
346         webpage = self._download_webpage(url, playlist_id)
347
348         entries = [
349             self.url_result('nrk:%s' % video_id, NRKIE.ie_key())
350             for video_id in re.findall(self._ITEM_RE, webpage)
351         ]
352
353         playlist_title = self. _extract_title(webpage)
354         playlist_description = self._extract_description(webpage)
355
356         return self.playlist_result(
357             entries, playlist_id, playlist_title, playlist_description)
358
359
360 class NRKPlaylistIE(NRKPlaylistBaseIE):
361     _VALID_URL = r'https?://(?:www\.)?nrk\.no/(?!video|skole)(?:[^/]+/)+(?P<id>[^/]+)'
362     _ITEM_RE = r'class="[^"]*\brich\b[^"]*"[^>]+data-video-id="([^"]+)"'
363     _TESTS = [{
364         'url': 'http://www.nrk.no/troms/gjenopplev-den-historiske-solformorkelsen-1.12270763',
365         'info_dict': {
366             'id': 'gjenopplev-den-historiske-solformorkelsen-1.12270763',
367             'title': 'Gjenopplev den historiske solformørkelsen',
368             'description': 'md5:c2df8ea3bac5654a26fc2834a542feed',
369         },
370         'playlist_count': 2,
371     }, {
372         'url': 'http://www.nrk.no/kultur/bok/rivertonprisen-til-karin-fossum-1.12266449',
373         'info_dict': {
374             'id': 'rivertonprisen-til-karin-fossum-1.12266449',
375             'title': 'Rivertonprisen til Karin Fossum',
376             'description': 'Første kvinne på 15 år til å vinne krimlitteraturprisen.',
377         },
378         'playlist_count': 5,
379     }]
380
381     def _extract_title(self, webpage):
382         return self._og_search_title(webpage, fatal=False)
383
384     def _extract_description(self, webpage):
385         return self._og_search_description(webpage)
386
387
388 class NRKTVEpisodesIE(NRKPlaylistBaseIE):
389     _VALID_URL = r'https?://tv\.nrk\.no/program/[Ee]pisodes/[^/]+/(?P<id>\d+)'
390     _ITEM_RE = r'data-episode=["\']%s' % NRKTVIE._EPISODE_RE
391     _TESTS = [{
392         'url': 'https://tv.nrk.no/program/episodes/nytt-paa-nytt/69031',
393         'info_dict': {
394             'id': '69031',
395             'title': 'Nytt på nytt, sesong: 201210',
396         },
397         'playlist_count': 4,
398     }]
399
400     def _extract_title(self, webpage):
401         return self._html_search_regex(
402             r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
403
404
405 class NRKTVSeriesIE(InfoExtractor):
406     _VALID_URL = r'https?://(?:tv|radio)\.nrk(?:super)?\.no/serie/(?P<id>[^/]+)'
407     _ITEM_RE = r'(?:data-season=["\']|id=["\']season-)(?P<id>\d+)'
408     _TESTS = [{
409         'url': 'https://tv.nrk.no/serie/groenn-glede',
410         'info_dict': {
411             'id': 'groenn-glede',
412             'title': 'Grønn glede',
413             'description': 'md5:7576e92ae7f65da6993cf90ee29e4608',
414         },
415         'playlist_mincount': 9,
416     }, {
417         'url': 'http://tv.nrksuper.no/serie/labyrint',
418         'info_dict': {
419             'id': 'labyrint',
420             'title': 'Labyrint',
421             'description': 'md5:58afd450974c89e27d5a19212eee7115',
422         },
423         'playlist_mincount': 3,
424     }, {
425         'url': 'https://tv.nrk.no/serie/broedrene-dal-og-spektralsteinene',
426         'only_matching': True,
427     }, {
428         'url': 'https://tv.nrk.no/serie/saving-the-human-race',
429         'only_matching': True,
430     }, {
431         'url': 'https://tv.nrk.no/serie/postmann-pat',
432         'only_matching': True,
433     }]
434
435     @classmethod
436     def suitable(cls, url):
437         return False if NRKTVIE.suitable(url) else super(NRKTVSeriesIE, cls).suitable(url)
438
439     def _real_extract(self, url):
440         series_id = self._match_id(url)
441
442         webpage = self._download_webpage(url, series_id)
443
444         entries = [
445             self.url_result(
446                 'https://tv.nrk.no/program/Episodes/{series}/{season}'.format(
447                     series=series_id, season=season_id))
448             for season_id in re.findall(self._ITEM_RE, webpage)
449         ]
450
451         title = self._html_search_meta(
452             'seriestitle', webpage,
453             'title', default=None) or self._og_search_title(
454             webpage, fatal=False)
455
456         description = self._html_search_meta(
457             'series_description', webpage,
458             'description', default=None) or self._og_search_description(webpage)
459
460         return self.playlist_result(entries, series_id, title, description)
461
462
463 class NRKSkoleIE(InfoExtractor):
464     IE_DESC = 'NRK Skole'
465     _VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'
466
467     _TESTS = [{
468         'url': 'https://www.nrk.no/skole/?page=search&q=&mediaId=14099',
469         'md5': '6bc936b01f9dd8ed45bc58b252b2d9b6',
470         'info_dict': {
471             'id': '6021',
472             'ext': 'mp4',
473             'title': 'Genetikk og eneggede tvillinger',
474             'description': 'md5:3aca25dcf38ec30f0363428d2b265f8d',
475             'duration': 399,
476         },
477     }, {
478         'url': 'https://www.nrk.no/skole/?page=objectives&subject=naturfag&objective=K15114&mediaId=19355',
479         'only_matching': True,
480     }]
481
482     def _real_extract(self, url):
483         video_id = self._match_id(url)
484
485         webpage = self._download_webpage(
486             'https://mimir.nrk.no/plugin/1.0/static?mediaId=%s' % video_id,
487             video_id)
488
489         nrk_id = self._parse_json(
490             self._search_regex(
491                 r'<script[^>]+type=["\']application/json["\'][^>]*>({.+?})</script>',
492                 webpage, 'application json'),
493             video_id)['activeMedia']['psId']
494
495         return self.url_result('nrk:%s' % nrk_id)