[nrk] Update _API_HOST and relax _VALID_URL
[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': 'nrk:clip/7707d5a3-ebe7-434a-87d5-a3ebe7a34a70',
200         'only_matching': True,
201     }, {
202         'url': 'https://v8-psapi.nrk.no/mediaelement/ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
203         'only_matching': True,
204     }]
205
206
207 class NRKTVIE(NRKBaseIE):
208     IE_DESC = 'NRK TV and NRK Radio'
209     _EPISODE_RE = r'(?P<id>[a-zA-Z]{4}\d{8})'
210     _VALID_URL = r'''(?x)
211                         https?://
212                             (?:tv|radio)\.nrk(?:super)?\.no/
213                             (?:serie/[^/]+|program)/
214                             (?![Ee]pisodes)%s
215                             (?:/\d{2}-\d{2}-\d{4})?
216                             (?:\#del=(?P<part_id>\d+))?
217                     ''' % _EPISODE_RE
218     _API_HOST = 'psapi-we.nrk.no'
219
220     _TESTS = [{
221         'url': 'https://tv.nrk.no/serie/20-spoersmaal-tv/MUHH48000314/23-05-2014',
222         'md5': '4e9ca6629f09e588ed240fb11619922a',
223         'info_dict': {
224             'id': 'MUHH48000314AA',
225             'ext': 'mp4',
226             'title': '20 spørsmål 23.05.2014',
227             'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
228             'duration': 1741,
229             'series': '20 spørsmål - TV',
230             'episode': '23.05.2014',
231         },
232     }, {
233         'url': 'https://tv.nrk.no/program/mdfp15000514',
234         'info_dict': {
235             'id': 'MDFP15000514CA',
236             'ext': 'mp4',
237             'title': 'Grunnlovsjubiléet - Stor ståhei for ingenting 24.05.2014',
238             'description': 'md5:89290c5ccde1b3a24bb8050ab67fe1db',
239             'duration': 4605,
240             'series': 'Kunnskapskanalen',
241             'episode': '24.05.2014',
242         },
243         'params': {
244             'skip_download': True,
245         },
246     }, {
247         # single playlist video
248         'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015#del=2',
249         'info_dict': {
250             'id': 'MSPO40010515-part2',
251             'ext': 'flv',
252             'title': 'Tour de Ski: Sprint fri teknikk, kvinner og menn 06.01.2015 (del 2:2)',
253             'description': 'md5:238b67b97a4ac7d7b4bf0edf8cc57d26',
254         },
255         'params': {
256             'skip_download': True,
257         },
258         'expected_warnings': ['Video is geo restricted'],
259         'skip': 'particular part is not supported currently',
260     }, {
261         'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015',
262         'playlist': [{
263             'info_dict': {
264                 'id': 'MSPO40010515AH',
265                 'ext': 'mp4',
266                 'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015 (Part 1)',
267                 'description': 'md5:c03aba1e917561eface5214020551b7a',
268                 'duration': 772,
269                 'series': 'Tour de Ski',
270                 'episode': '06.01.2015',
271             },
272             'params': {
273                 'skip_download': True,
274             },
275         }, {
276             'info_dict': {
277                 'id': 'MSPO40010515BH',
278                 'ext': 'mp4',
279                 'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015 (Part 2)',
280                 'description': 'md5:c03aba1e917561eface5214020551b7a',
281                 'duration': 6175,
282                 'series': 'Tour de Ski',
283                 'episode': '06.01.2015',
284             },
285             'params': {
286                 'skip_download': True,
287             },
288         }],
289         'info_dict': {
290             'id': 'MSPO40010515',
291             'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015',
292             'description': 'md5:c03aba1e917561eface5214020551b7a',
293         },
294         'expected_warnings': ['Video is geo restricted'],
295     }, {
296         'url': 'https://tv.nrk.no/serie/anno/KMTE50001317/sesong-3/episode-13',
297         'info_dict': {
298             'id': 'KMTE50001317AA',
299             'ext': 'mp4',
300             'title': 'Anno 13:30',
301             'description': 'md5:11d9613661a8dbe6f9bef54e3a4cbbfa',
302             'duration': 2340,
303             'series': 'Anno',
304             'episode': '13:30',
305             'season_number': 3,
306             'episode_number': 13,
307         },
308         'params': {
309             'skip_download': True,
310         },
311     }, {
312         'url': 'https://tv.nrk.no/serie/nytt-paa-nytt/MUHH46000317/27-01-2017',
313         'info_dict': {
314             'id': 'MUHH46000317AA',
315             'ext': 'mp4',
316             'title': 'Nytt på Nytt 27.01.2017',
317             'description': 'md5:5358d6388fba0ea6f0b6d11c48b9eb4b',
318             'duration': 1796,
319             'series': 'Nytt på nytt',
320             'episode': '27.01.2017',
321         },
322         'params': {
323             'skip_download': True,
324         },
325     }, {
326         'url': 'https://radio.nrk.no/serie/dagsnytt/NPUB21019315/12-07-2015#',
327         'only_matching': True,
328     }]
329
330
331 class NRKTVDirekteIE(NRKTVIE):
332     IE_DESC = 'NRK TV Direkte and NRK Radio Direkte'
333     _VALID_URL = r'https?://(?:tv|radio)\.nrk\.no/direkte/(?P<id>[^/?#&]+)'
334
335     _TESTS = [{
336         'url': 'https://tv.nrk.no/direkte/nrk1',
337         'only_matching': True,
338     }, {
339         'url': 'https://radio.nrk.no/direkte/p1_oslo_akershus',
340         'only_matching': True,
341     }]
342
343
344 class NRKPlaylistBaseIE(InfoExtractor):
345     def _extract_description(self, webpage):
346         pass
347
348     def _real_extract(self, url):
349         playlist_id = self._match_id(url)
350
351         webpage = self._download_webpage(url, playlist_id)
352
353         entries = [
354             self.url_result('nrk:%s' % video_id, NRKIE.ie_key())
355             for video_id in re.findall(self._ITEM_RE, webpage)
356         ]
357
358         playlist_title = self. _extract_title(webpage)
359         playlist_description = self._extract_description(webpage)
360
361         return self.playlist_result(
362             entries, playlist_id, playlist_title, playlist_description)
363
364
365 class NRKPlaylistIE(NRKPlaylistBaseIE):
366     _VALID_URL = r'https?://(?:www\.)?nrk\.no/(?!video|skole)(?:[^/]+/)+(?P<id>[^/]+)'
367     _ITEM_RE = r'class="[^"]*\brich\b[^"]*"[^>]+data-video-id="([^"]+)"'
368     _TESTS = [{
369         'url': 'http://www.nrk.no/troms/gjenopplev-den-historiske-solformorkelsen-1.12270763',
370         'info_dict': {
371             'id': 'gjenopplev-den-historiske-solformorkelsen-1.12270763',
372             'title': 'Gjenopplev den historiske solformørkelsen',
373             'description': 'md5:c2df8ea3bac5654a26fc2834a542feed',
374         },
375         'playlist_count': 2,
376     }, {
377         'url': 'http://www.nrk.no/kultur/bok/rivertonprisen-til-karin-fossum-1.12266449',
378         'info_dict': {
379             'id': 'rivertonprisen-til-karin-fossum-1.12266449',
380             'title': 'Rivertonprisen til Karin Fossum',
381             'description': 'Første kvinne på 15 år til å vinne krimlitteraturprisen.',
382         },
383         'playlist_count': 5,
384     }]
385
386     def _extract_title(self, webpage):
387         return self._og_search_title(webpage, fatal=False)
388
389     def _extract_description(self, webpage):
390         return self._og_search_description(webpage)
391
392
393 class NRKTVEpisodesIE(NRKPlaylistBaseIE):
394     _VALID_URL = r'https?://tv\.nrk\.no/program/[Ee]pisodes/[^/]+/(?P<id>\d+)'
395     _ITEM_RE = r'data-episode=["\']%s' % NRKTVIE._EPISODE_RE
396     _TESTS = [{
397         'url': 'https://tv.nrk.no/program/episodes/nytt-paa-nytt/69031',
398         'info_dict': {
399             'id': '69031',
400             'title': 'Nytt på nytt, sesong: 201210',
401         },
402         'playlist_count': 4,
403     }]
404
405     def _extract_title(self, webpage):
406         return self._html_search_regex(
407             r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
408
409
410 class NRKTVSeriesIE(InfoExtractor):
411     _VALID_URL = r'https?://(?:tv|radio)\.nrk(?:super)?\.no/serie/(?P<id>[^/]+)'
412     _ITEM_RE = r'(?:data-season=["\']|id=["\']season-)(?P<id>\d+)'
413     _TESTS = [{
414         'url': 'https://tv.nrk.no/serie/groenn-glede',
415         'info_dict': {
416             'id': 'groenn-glede',
417             'title': 'Grønn glede',
418             'description': 'md5:7576e92ae7f65da6993cf90ee29e4608',
419         },
420         'playlist_mincount': 9,
421     }, {
422         'url': 'http://tv.nrksuper.no/serie/labyrint',
423         'info_dict': {
424             'id': 'labyrint',
425             'title': 'Labyrint',
426             'description': 'md5:58afd450974c89e27d5a19212eee7115',
427         },
428         'playlist_mincount': 3,
429     }, {
430         'url': 'https://tv.nrk.no/serie/broedrene-dal-og-spektralsteinene',
431         'only_matching': True,
432     }, {
433         'url': 'https://tv.nrk.no/serie/saving-the-human-race',
434         'only_matching': True,
435     }, {
436         'url': 'https://tv.nrk.no/serie/postmann-pat',
437         'only_matching': True,
438     }]
439
440     @classmethod
441     def suitable(cls, url):
442         return False if NRKTVIE.suitable(url) else super(NRKTVSeriesIE, cls).suitable(url)
443
444     def _real_extract(self, url):
445         series_id = self._match_id(url)
446
447         webpage = self._download_webpage(url, series_id)
448
449         entries = [
450             self.url_result(
451                 'https://tv.nrk.no/program/Episodes/{series}/{season}'.format(
452                     series=series_id, season=season_id))
453             for season_id in re.findall(self._ITEM_RE, webpage)
454         ]
455
456         title = self._html_search_meta(
457             'seriestitle', webpage,
458             'title', default=None) or self._og_search_title(
459             webpage, fatal=False)
460
461         description = self._html_search_meta(
462             'series_description', webpage,
463             'description', default=None) or self._og_search_description(webpage)
464
465         return self.playlist_result(entries, series_id, title, description)
466
467
468 class NRKSkoleIE(InfoExtractor):
469     IE_DESC = 'NRK Skole'
470     _VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'
471
472     _TESTS = [{
473         'url': 'https://www.nrk.no/skole/?page=search&q=&mediaId=14099',
474         'md5': '6bc936b01f9dd8ed45bc58b252b2d9b6',
475         'info_dict': {
476             'id': '6021',
477             'ext': 'mp4',
478             'title': 'Genetikk og eneggede tvillinger',
479             'description': 'md5:3aca25dcf38ec30f0363428d2b265f8d',
480             'duration': 399,
481         },
482     }, {
483         'url': 'https://www.nrk.no/skole/?page=objectives&subject=naturfag&objective=K15114&mediaId=19355',
484         'only_matching': True,
485     }]
486
487     def _real_extract(self, url):
488         video_id = self._match_id(url)
489
490         webpage = self._download_webpage(
491             'https://mimir.nrk.no/plugin/1.0/static?mediaId=%s' % video_id,
492             video_id)
493
494         nrk_id = self._parse_json(
495             self._search_regex(
496                 r'<script[^>]+type=["\']application/json["\'][^>]*>({.+?})</script>',
497                 webpage, 'application json'),
498             video_id)['activeMedia']['psId']
499
500         return self.url_result('nrk:%s' % nrk_id)