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