]> asedeno.scripts.mit.edu Git - youtube-dl.git/blob - youtube_dl/extractor/nrk.py
[nrktv] Switch to playback endpoint
[youtube-dl.git] / youtube_dl / extractor / nrk.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import itertools
5 import random
6 import re
7
8 from .common import InfoExtractor
9 from ..compat import compat_str
10 from ..utils import (
11     determine_ext,
12     ExtractorError,
13     int_or_none,
14     parse_duration,
15     try_get,
16     urljoin,
17     url_or_none,
18 )
19
20
21 class NRKBaseIE(InfoExtractor):
22     _GEO_COUNTRIES = ['NO']
23     _CDN_REPL_REGEX = r'''(?x)://
24         (?:
25             nrkod\d{1,2}-httpcache0-47115-cacheod0\.dna\.ip-only\.net/47115-cacheod0|
26             nrk-od-no\.telenorcdn\.net|
27             minicdn-od\.nrk\.no/od/nrkhd-osl-rr\.netwerk\.no/no
28         )/'''
29
30     def _extract_nrk_formats(self, asset_url, video_id):
31         if re.match(r'https?://[^/]+\.akamaihd\.net/i/', asset_url):
32             return self._extract_akamai_formats(asset_url, video_id)
33         asset_url = re.sub(r'(?:bw_(?:low|high)=\d+|no_audio_only)&?', '', asset_url)
34         formats = self._extract_m3u8_formats(
35             asset_url, video_id, 'mp4', 'm3u8_native', fatal=False)
36         if not formats and re.search(self._CDN_REPL_REGEX, asset_url):
37             formats = self._extract_m3u8_formats(
38                 re.sub(self._CDN_REPL_REGEX, '://nrk-od-%02d.akamaized.net/no/' % random.randint(0, 99), asset_url),
39                 video_id, 'mp4', 'm3u8_native', fatal=False)
40         return formats
41
42     def _raise_error(self, data):
43         MESSAGES = {
44             'ProgramRightsAreNotReady': 'Du kan dessverre ikke se eller høre programmet',
45             'ProgramRightsHasExpired': 'Programmet har gått ut',
46             'NoProgramRights': 'Ikke tilgjengelig',
47             'ProgramIsGeoBlocked': 'NRK har ikke rettigheter til å vise dette programmet utenfor Norge',
48         }
49         message_type = data.get('messageType', '')
50         # Can be ProgramIsGeoBlocked or ChannelIsGeoBlocked*
51         if 'IsGeoBlocked' in message_type or try_get(data, lambda x: x['usageRights']['isGeoBlocked']) is True:
52             self.raise_geo_restricted(
53                 msg=MESSAGES.get('ProgramIsGeoBlocked'),
54                 countries=self._GEO_COUNTRIES)
55         message = data.get('endUserMessage') or MESSAGES.get(message_type, message_type)
56         raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)
57
58     def _call_api(self, path, video_id, item=None, note=None, fatal=True, query=None):
59         return self._download_json(
60             urljoin('http://psapi.nrk.no/', path),
61             video_id, note or 'Downloading %s JSON' % item,
62             fatal=fatal, query=query,
63             headers={'Accept-Encoding': 'gzip, deflate, br'})
64
65     def _extract_from_playback(self, video_id):
66         path_templ = 'playback/%s/' + video_id
67
68         def call_playback_api(item, query=None):
69             return self._call_api(path_templ % item, video_id, item, query=query)
70         # known values for preferredCdn: akamai, iponly, minicdn and telenor
71         manifest = call_playback_api('manifest', {'preferredCdn': 'akamai'})
72
73         if manifest.get('playability') == 'nonPlayable':
74             self._raise_error(manifest['nonPlayable'])
75
76         playable = manifest['playable']
77
78         formats = []
79         for asset in playable['assets']:
80             if not isinstance(asset, dict):
81                 continue
82             if asset.get('encrypted'):
83                 continue
84             format_url = url_or_none(asset.get('url'))
85             if not format_url:
86                 continue
87             if asset.get('format') == 'HLS' or determine_ext(format_url) == 'm3u8':
88                 formats.extend(self._extract_nrk_formats(format_url, video_id))
89         self._sort_formats(formats)
90
91         data = call_playback_api('metadata')
92
93         preplay = data['preplay']
94         titles = preplay['titles']
95         title = titles['title']
96         alt_title = titles.get('subtitle')
97
98         description = preplay.get('description')
99         duration = parse_duration(playable.get('duration')) or parse_duration(data.get('duration'))
100
101         thumbnails = []
102         for image in try_get(
103                 preplay, lambda x: x['poster']['images'], list) or []:
104             if not isinstance(image, dict):
105                 continue
106             image_url = url_or_none(image.get('url'))
107             if not image_url:
108                 continue
109             thumbnails.append({
110                 'url': image_url,
111                 'width': int_or_none(image.get('pixelWidth')),
112                 'height': int_or_none(image.get('pixelHeight')),
113             })
114
115         return {
116             'id': video_id,
117             'title': title,
118             'alt_title': alt_title,
119             'description': description,
120             'duration': duration,
121             'thumbnails': thumbnails,
122             'formats': formats,
123         }
124
125
126 class NRKIE(NRKBaseIE):
127     _VALID_URL = r'''(?x)
128                         (?:
129                             nrk:|
130                             https?://
131                                 (?:
132                                     (?:www\.)?nrk\.no/video/(?:PS\*|[^_]+_)|
133                                     v8[-.]psapi\.nrk\.no/mediaelement/
134                                 )
135                             )
136                             (?P<id>[^?\#&]+)
137                         '''
138
139     _TESTS = [{
140         # video
141         'url': 'http://www.nrk.no/video/PS*150533',
142         'md5': 'f46be075326e23ad0e524edfcb06aeb6',
143         'info_dict': {
144             'id': '150533',
145             'ext': 'mp4',
146             'title': 'Dompap og andre fugler i Piip-Show',
147             'description': 'md5:d9261ba34c43b61c812cb6b0269a5c8f',
148             'duration': 262,
149         }
150     }, {
151         # audio
152         'url': 'http://www.nrk.no/video/PS*154915',
153         # MD5 is unstable
154         'info_dict': {
155             'id': '154915',
156             'ext': 'mp4',
157             'title': 'Slik høres internett ut når du er blind',
158             'description': 'md5:a621f5cc1bd75c8d5104cb048c6b8568',
159             'duration': 20,
160         }
161     }, {
162         'url': 'nrk:ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
163         'only_matching': True,
164     }, {
165         'url': 'nrk:clip/7707d5a3-ebe7-434a-87d5-a3ebe7a34a70',
166         'only_matching': True,
167     }, {
168         'url': 'https://v8-psapi.nrk.no/mediaelement/ecc1b952-96dc-4a98-81b9-5296dc7a98d9',
169         'only_matching': True,
170     }, {
171         'url': 'https://www.nrk.no/video/dompap-og-andre-fugler-i-piip-show_150533',
172         'only_matching': True,
173     }, {
174         'url': 'https://www.nrk.no/video/humor/kommentatorboksen-reiser-til-sjos_d1fda11f-a4ad-437a-a374-0398bc84e999',
175         'only_matching': True,
176     }]
177
178     def _real_extract(self, url):
179         video_id = self._match_id(url)
180         return self._extract_from_playback(video_id)
181
182
183 class NRKTVIE(NRKBaseIE):
184     IE_DESC = 'NRK TV and NRK Radio'
185     _EPISODE_RE = r'(?P<id>[a-zA-Z]{4}\d{8})'
186     _VALID_URL = r'https?://(?:tv|radio)\.nrk(?:super)?\.no/(?:[^/]+/)*%s' % _EPISODE_RE
187     _TESTS = [{
188         'url': 'https://tv.nrk.no/program/MDDP12000117',
189         'md5': 'c4a5960f1b00b40d47db65c1064e0ab1',
190         'info_dict': {
191             'id': 'MDDP12000117AA',
192             'ext': 'mp4',
193             'title': 'Alarm Trolltunga',
194             'description': 'md5:46923a6e6510eefcce23d5ef2a58f2ce',
195             'duration': 2223.44,
196             'age_limit': 6,
197         },
198     }, {
199         'url': 'https://tv.nrk.no/serie/20-spoersmaal-tv/MUHH48000314/23-05-2014',
200         'md5': '8d40dab61cea8ab0114e090b029a0565',
201         'info_dict': {
202             'id': 'MUHH48000314AA',
203             'ext': 'mp4',
204             'title': '20 spørsmål 23.05.2014',
205             'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
206             'duration': 1741,
207             'series': '20 spørsmål',
208             'episode': '23.05.2014',
209         },
210     }, {
211         'url': 'https://tv.nrk.no/program/mdfp15000514',
212         'info_dict': {
213             'id': 'MDFP15000514CA',
214             'ext': 'mp4',
215             'title': 'Grunnlovsjubiléet - Stor ståhei for ingenting 24.05.2014',
216             'description': 'md5:89290c5ccde1b3a24bb8050ab67fe1db',
217             'duration': 4605.08,
218             'series': 'Kunnskapskanalen',
219             'episode': '24.05.2014',
220         },
221         'params': {
222             'skip_download': True,
223         },
224     }, {
225         # single playlist video
226         'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015#del=2',
227         'info_dict': {
228             'id': 'MSPO40010515AH',
229             'ext': 'mp4',
230             'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015',
231             'description': 'md5:c03aba1e917561eface5214020551b7a',
232         },
233         'params': {
234             'skip_download': True,
235         },
236         'expected_warnings': ['Failed to download m3u8 information'],
237         'skip': 'particular part is not supported currently',
238     }, {
239         'url': 'https://tv.nrk.no/serie/tour-de-ski/MSPO40010515/06-01-2015',
240         'info_dict': {
241             'id': 'MSPO40010515AH',
242             'ext': 'mp4',
243             'title': 'Sprint fri teknikk, kvinner og menn 06.01.2015',
244             'description': 'md5:c03aba1e917561eface5214020551b7a',
245         },
246         'expected_warnings': ['Failed to download m3u8 information'],
247     }, {
248         'url': 'https://tv.nrk.no/serie/anno/KMTE50001317/sesong-3/episode-13',
249         'info_dict': {
250             'id': 'KMTE50001317AA',
251             'ext': 'mp4',
252             'title': 'Anno 13:30',
253             'description': 'md5:11d9613661a8dbe6f9bef54e3a4cbbfa',
254             'duration': 2340,
255             'series': 'Anno',
256             'episode': '13:30',
257             'season_number': 3,
258             'episode_number': 13,
259         },
260         'params': {
261             'skip_download': True,
262         },
263     }, {
264         'url': 'https://tv.nrk.no/serie/nytt-paa-nytt/MUHH46000317/27-01-2017',
265         'info_dict': {
266             'id': 'MUHH46000317AA',
267             'ext': 'mp4',
268             'title': 'Nytt på Nytt 27.01.2017',
269             'description': 'md5:5358d6388fba0ea6f0b6d11c48b9eb4b',
270             'duration': 1796,
271             'series': 'Nytt på nytt',
272             'episode': '27.01.2017',
273         },
274         'params': {
275             'skip_download': True,
276         },
277         'skip': 'ProgramRightsHasExpired',
278     }, {
279         'url': 'https://radio.nrk.no/serie/dagsnytt/NPUB21019315/12-07-2015#',
280         'only_matching': True,
281     }, {
282         'url': 'https://tv.nrk.no/serie/lindmo/2018/MUHU11006318/avspiller',
283         'only_matching': True,
284     }, {
285         'url': 'https://radio.nrk.no/serie/dagsnytt/sesong/201507/NPUB21019315',
286         'only_matching': True,
287     }]
288
289     def _real_extract(self, url):
290         video_id = self._match_id(url)
291         return self._extract_from_playback(video_id)
292
293
294 class NRKTVEpisodeIE(InfoExtractor):
295     _VALID_URL = r'https?://tv\.nrk\.no/serie/(?P<id>[^/]+/sesong/\d+/episode/\d+)'
296     _TESTS = [{
297         'url': 'https://tv.nrk.no/serie/hellums-kro/sesong/1/episode/2',
298         'info_dict': {
299             'id': 'MUHH36005220BA',
300             'ext': 'mp4',
301             'title': 'Kro, krig og kjærlighet 2:6',
302             'description': 'md5:b32a7dc0b1ed27c8064f58b97bda4350',
303             'duration': 1563,
304             'series': 'Hellums kro',
305             'season_number': 1,
306             'episode_number': 2,
307             'episode': '2:6',
308             'age_limit': 6,
309         },
310         'params': {
311             'skip_download': True,
312         },
313     }, {
314         'url': 'https://tv.nrk.no/serie/backstage/sesong/1/episode/8',
315         'info_dict': {
316             'id': 'MSUI14000816AA',
317             'ext': 'mp4',
318             'title': 'Backstage 8:30',
319             'description': 'md5:de6ca5d5a2d56849e4021f2bf2850df4',
320             'duration': 1320,
321             'series': 'Backstage',
322             'season_number': 1,
323             'episode_number': 8,
324             'episode': '8:30',
325         },
326         'params': {
327             'skip_download': True,
328         },
329         'skip': 'ProgramRightsHasExpired',
330     }]
331
332     def _real_extract(self, url):
333         display_id = self._match_id(url)
334
335         webpage = self._download_webpage(url, display_id)
336
337         info = self._search_json_ld(webpage, display_id, default={})
338         nrk_id = info.get('@id') or self._html_search_meta(
339             'nrk:program-id', webpage, default=None) or self._search_regex(
340             r'data-program-id=["\'](%s)' % NRKTVIE._EPISODE_RE, webpage,
341             'nrk id')
342         assert re.match(NRKTVIE._EPISODE_RE, nrk_id)
343
344         info.update({
345             '_type': 'url_transparent',
346             'id': nrk_id,
347             'url': 'nrk:%s' % nrk_id,
348             'ie_key': NRKIE.ie_key(),
349         })
350         return info
351
352
353 class NRKTVSerieBaseIE(NRKBaseIE):
354     def _extract_entries(self, entry_list):
355         if not isinstance(entry_list, list):
356             return []
357         entries = []
358         for episode in entry_list:
359             nrk_id = episode.get('prfId') or episode.get('episodeId')
360             if not nrk_id or not isinstance(nrk_id, compat_str):
361                 continue
362             if not re.match(NRKTVIE._EPISODE_RE, nrk_id):
363                 continue
364             entries.append(self.url_result(
365                 'nrk:%s' % nrk_id, ie=NRKIE.ie_key(), video_id=nrk_id))
366         return entries
367
368     _ASSETS_KEYS = ('episodes', 'instalments',)
369
370     def _extract_assets_key(self, embedded):
371         for asset_key in self._ASSETS_KEYS:
372             if embedded.get(asset_key):
373                 return asset_key
374
375     def _entries(self, data, display_id):
376         for page_num in itertools.count(1):
377             embedded = data.get('_embedded') or data
378             if not isinstance(embedded, dict):
379                 break
380             assets_key = self._extract_assets_key(embedded)
381             if not assets_key:
382                 break
383             # Extract entries
384             entries = try_get(
385                 embedded,
386                 (lambda x: x[assets_key]['_embedded'][assets_key],
387                  lambda x: x[assets_key]),
388                 list)
389             for e in self._extract_entries(entries):
390                 yield e
391             # Find next URL
392             next_url_path = try_get(
393                 data,
394                 (lambda x: x['_links']['next']['href'],
395                  lambda x: x['_embedded'][assets_key]['_links']['next']['href']),
396                 compat_str)
397             if not next_url_path:
398                 break
399             data = self._call_api(
400                 next_url_path, display_id,
401                 note='Downloading %s JSON page %d' % (assets_key, page_num),
402                 fatal=False)
403             if not data:
404                 break
405
406
407 class NRKTVSeasonIE(NRKTVSerieBaseIE):
408     _VALID_URL = r'https?://(?P<domain>tv|radio)\.nrk\.no/serie/(?P<serie>[^/]+)/(?:sesong/)?(?P<id>\d+)'
409     _TESTS = [{
410         'url': 'https://tv.nrk.no/serie/backstage/sesong/1',
411         'info_dict': {
412             'id': 'backstage/1',
413             'title': 'Sesong 1',
414         },
415         'playlist_mincount': 30,
416     }, {
417         # no /sesong/ in path
418         'url': 'https://tv.nrk.no/serie/lindmo/2016',
419         'info_dict': {
420             'id': 'lindmo/2016',
421             'title': '2016',
422         },
423         'playlist_mincount': 29,
424     }, {
425         # weird nested _embedded in catalog JSON response
426         'url': 'https://radio.nrk.no/serie/dickie-dick-dickens/sesong/1',
427         'info_dict': {
428             'id': 'dickie-dick-dickens/1',
429             'title': 'Sesong 1',
430         },
431         'playlist_mincount': 11,
432     }, {
433         # 841 entries, multi page
434         'url': 'https://radio.nrk.no/serie/dagsnytt/sesong/201509',
435         'info_dict': {
436             'id': 'dagsnytt/201509',
437             'title': 'September 2015',
438         },
439         'playlist_mincount': 841,
440     }, {
441         # 180 entries, single page
442         'url': 'https://tv.nrk.no/serie/spangas/sesong/1',
443         'only_matching': True,
444     }]
445
446     @classmethod
447     def suitable(cls, url):
448         return (False if NRKTVIE.suitable(url) or NRKTVEpisodeIE.suitable(url)
449                 else super(NRKTVSeasonIE, cls).suitable(url))
450
451     def _real_extract(self, url):
452         domain, serie, season_id = re.match(self._VALID_URL, url).groups()
453         display_id = '%s/%s' % (serie, season_id)
454
455         data = self._call_api(
456             '%s/catalog/series/%s/seasons/%s' % (domain, serie, season_id),
457             display_id, 'season', query={'pageSize': 50})
458
459         title = try_get(data, lambda x: x['titles']['title'], compat_str) or display_id
460         return self.playlist_result(
461             self._entries(data, display_id),
462             display_id, title)
463
464
465 class NRKTVSeriesIE(NRKTVSerieBaseIE):
466     _VALID_URL = r'https?://(?P<domain>(?:tv|radio)\.nrk|(?:tv\.)?nrksuper)\.no/serie/(?P<id>[^/]+)'
467     _TESTS = [{
468         # new layout, instalments
469         'url': 'https://tv.nrk.no/serie/groenn-glede',
470         'info_dict': {
471             'id': 'groenn-glede',
472             'title': 'Grønn glede',
473             'description': 'md5:7576e92ae7f65da6993cf90ee29e4608',
474         },
475         'playlist_mincount': 90,
476     }, {
477         # new layout, instalments, more entries
478         'url': 'https://tv.nrk.no/serie/lindmo',
479         'only_matching': True,
480     }, {
481         'url': 'https://tv.nrk.no/serie/blank',
482         'info_dict': {
483             'id': 'blank',
484             'title': 'Blank',
485             'description': 'md5:7664b4e7e77dc6810cd3bca367c25b6e',
486         },
487         'playlist_mincount': 30,
488     }, {
489         # new layout, seasons
490         'url': 'https://tv.nrk.no/serie/backstage',
491         'info_dict': {
492             'id': 'backstage',
493             'title': 'Backstage',
494             'description': 'md5:63692ceb96813d9a207e9910483d948b',
495         },
496         'playlist_mincount': 60,
497     }, {
498         # old layout
499         'url': 'https://tv.nrksuper.no/serie/labyrint',
500         'info_dict': {
501             'id': 'labyrint',
502             'title': 'Labyrint',
503             'description': 'I Daidalos sin undersjøiske Labyrint venter spennende oppgaver, skumle robotskapninger og slim.',
504         },
505         'playlist_mincount': 3,
506     }, {
507         'url': 'https://tv.nrk.no/serie/broedrene-dal-og-spektralsteinene',
508         'only_matching': True,
509     }, {
510         'url': 'https://tv.nrk.no/serie/saving-the-human-race',
511         'only_matching': True,
512     }, {
513         'url': 'https://tv.nrk.no/serie/postmann-pat',
514         'only_matching': True,
515     }, {
516         'url': 'https://radio.nrk.no/serie/dickie-dick-dickens',
517         'info_dict': {
518             'id': 'dickie-dick-dickens',
519             'title': 'Dickie Dick Dickens',
520             'description': 'md5:19e67411ffe57f7dce08a943d7a0b91f',
521         },
522         'playlist_mincount': 8,
523     }, {
524         'url': 'https://nrksuper.no/serie/labyrint',
525         'only_matching': True,
526     }]
527
528     @classmethod
529     def suitable(cls, url):
530         return (
531             False if any(ie.suitable(url)
532                          for ie in (NRKTVIE, NRKTVEpisodeIE, NRKTVSeasonIE))
533             else super(NRKTVSeriesIE, cls).suitable(url))
534
535     def _real_extract(self, url):
536         site, series_id = re.match(self._VALID_URL, url).groups()
537         is_radio = site == 'radio.nrk'
538         domain = 'radio' if is_radio else 'tv'
539
540         size_prefix = 'p' if is_radio else 'embeddedInstalmentsP'
541         series = self._call_api(
542             '%s/catalog/series/%s' % (domain, series_id),
543             series_id, 'serie', query={size_prefix + 'ageSize': 50})
544         titles = try_get(series, [
545             lambda x: x['titles'],
546             lambda x: x[x['type']]['titles'],
547             lambda x: x[x['seriesType']]['titles'],
548         ]) or {}
549
550         entries = []
551         entries.extend(self._entries(series, series_id))
552         embedded = series.get('_embedded') or {}
553         linked_seasons = try_get(series, lambda x: x['_links']['seasons']) or []
554         embedded_seasons = embedded.get('seasons') or []
555         if len(linked_seasons) > len(embedded_seasons):
556             for season in linked_seasons:
557                 season_name = season.get('name')
558                 if season_name and isinstance(season_name, compat_str):
559                     entries.append(self.url_result(
560                         'https://%s.nrk.no/serie/%s/sesong/%s'
561                         % (domain, series_id, season_name),
562                         ie=NRKTVSeasonIE.ie_key(),
563                         video_title=season.get('title')))
564         else:
565             for season in embedded_seasons:
566                 entries.extend(self._entries(season, series_id))
567         entries.extend(self._entries(
568             embedded.get('extraMaterial') or {}, series_id))
569
570         return self.playlist_result(
571             entries, series_id, titles.get('title'), titles.get('subtitle'))
572
573
574 class NRKTVDirekteIE(NRKTVIE):
575     IE_DESC = 'NRK TV Direkte and NRK Radio Direkte'
576     _VALID_URL = r'https?://(?:tv|radio)\.nrk\.no/direkte/(?P<id>[^/?#&]+)'
577
578     _TESTS = [{
579         'url': 'https://tv.nrk.no/direkte/nrk1',
580         'only_matching': True,
581     }, {
582         'url': 'https://radio.nrk.no/direkte/p1_oslo_akershus',
583         'only_matching': True,
584     }]
585
586
587 class NRKPlaylistBaseIE(InfoExtractor):
588     def _extract_description(self, webpage):
589         pass
590
591     def _real_extract(self, url):
592         playlist_id = self._match_id(url)
593
594         webpage = self._download_webpage(url, playlist_id)
595
596         entries = [
597             self.url_result('nrk:%s' % video_id, NRKIE.ie_key())
598             for video_id in re.findall(self._ITEM_RE, webpage)
599         ]
600
601         playlist_title = self. _extract_title(webpage)
602         playlist_description = self._extract_description(webpage)
603
604         return self.playlist_result(
605             entries, playlist_id, playlist_title, playlist_description)
606
607
608 class NRKPlaylistIE(NRKPlaylistBaseIE):
609     _VALID_URL = r'https?://(?:www\.)?nrk\.no/(?!video|skole)(?:[^/]+/)+(?P<id>[^/]+)'
610     _ITEM_RE = r'class="[^"]*\brich\b[^"]*"[^>]+data-video-id="([^"]+)"'
611     _TESTS = [{
612         'url': 'http://www.nrk.no/troms/gjenopplev-den-historiske-solformorkelsen-1.12270763',
613         'info_dict': {
614             'id': 'gjenopplev-den-historiske-solformorkelsen-1.12270763',
615             'title': 'Gjenopplev den historiske solformørkelsen',
616             'description': 'md5:c2df8ea3bac5654a26fc2834a542feed',
617         },
618         'playlist_count': 2,
619     }, {
620         'url': 'http://www.nrk.no/kultur/bok/rivertonprisen-til-karin-fossum-1.12266449',
621         'info_dict': {
622             'id': 'rivertonprisen-til-karin-fossum-1.12266449',
623             'title': 'Rivertonprisen til Karin Fossum',
624             'description': 'Første kvinne på 15 år til å vinne krimlitteraturprisen.',
625         },
626         'playlist_count': 2,
627     }]
628
629     def _extract_title(self, webpage):
630         return self._og_search_title(webpage, fatal=False)
631
632     def _extract_description(self, webpage):
633         return self._og_search_description(webpage)
634
635
636 class NRKTVEpisodesIE(NRKPlaylistBaseIE):
637     _VALID_URL = r'https?://tv\.nrk\.no/program/[Ee]pisodes/[^/]+/(?P<id>\d+)'
638     _ITEM_RE = r'data-episode=["\']%s' % NRKTVIE._EPISODE_RE
639     _TESTS = [{
640         'url': 'https://tv.nrk.no/program/episodes/nytt-paa-nytt/69031',
641         'info_dict': {
642             'id': '69031',
643             'title': 'Nytt på nytt, sesong: 201210',
644         },
645         'playlist_count': 4,
646     }]
647
648     def _extract_title(self, webpage):
649         return self._html_search_regex(
650             r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
651
652
653 class NRKSkoleIE(InfoExtractor):
654     IE_DESC = 'NRK Skole'
655     _VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'
656
657     _TESTS = [{
658         'url': 'https://www.nrk.no/skole/?page=search&q=&mediaId=14099',
659         'md5': '18c12c3d071953c3bf8d54ef6b2587b7',
660         'info_dict': {
661             'id': '6021',
662             'ext': 'mp4',
663             'title': 'Genetikk og eneggede tvillinger',
664             'description': 'md5:3aca25dcf38ec30f0363428d2b265f8d',
665             'duration': 399,
666         },
667     }, {
668         'url': 'https://www.nrk.no/skole/?page=objectives&subject=naturfag&objective=K15114&mediaId=19355',
669         'only_matching': True,
670     }]
671
672     def _real_extract(self, url):
673         video_id = self._match_id(url)
674
675         nrk_id = self._download_json(
676             'https://nrkno-skole-prod.kube.nrk.no/skole/api/media/%s' % video_id,
677             video_id)['psId']
678
679         return self.url_result('nrk:%s' % nrk_id)