4 from .common
import InfoExtractor
14 from ..utils
.traversal
import traverse_obj
16 SERIES_API
= 'https://production-cdn.dr-massive.com/api/page?device=web_browser&item_detail_expand=all&lang=da&max_list_prefetch=3&path=%s'
19 class DRTVIE(InfoExtractor
):
23 (?:www\.)?dr\.dk/tv/se(?:/ondemand)?/(?:[^/?#]+/)*|
24 (?:www\.)?(?:dr\.dk|dr-massive\.com)/drtv/(?:se|episode|program)/
29 _GEO_COUNTRIES
= ['DK']
32 'url': 'https://www.dr.dk/tv/se/boern/ultra/klassen-ultra/klassen-darlig-taber-10',
33 'md5': '25e659cccc9a2ed956110a299fdf5983',
35 'id': 'klassen-darlig-taber-10',
37 'title': 'Klassen - DÃ¥rlig taber (10)',
38 'description': 'md5:815fe1b7fa656ed80580f31e8b3c79aa',
39 'timestamp': 1539085800,
40 'upload_date': '20181009',
43 'season': 'Klassen I',
45 'season_id': 'urn:dr:mu:bundle:57d7e8216187a4031cfd6f6b',
46 'episode': 'Episode 10',
50 'expected_warnings': ['Unable to download f4m manifest'],
51 'skip': 'this video has been removed',
53 # with SignLanguage formats
54 'url': 'https://www.dr.dk/tv/se/historien-om-danmark/-/historien-om-danmark-stenalder',
58 'title': 'Historien om Danmark: Stenalder',
59 'description': 'md5:8c66dcbc1669bbc6f873879880f37f2a',
60 'timestamp': 1546628400,
61 'upload_date': '20190104',
63 'formats': 'mincount:20',
65 'season_id': 'urn:dr:mu:bundle:5afc03ad6187a4065ca5fd35',
67 'season': 'Historien om Danmark',
68 'series': 'Historien om Danmark',
70 'skip': 'this video has been removed',
72 'url': 'https://www.dr.dk/drtv/se/frank-and-kastaniegaarden_71769',
76 'title': 'Frank & Kastaniegaarden',
77 'description': 'md5:974e1780934cf3275ef10280204bccb0',
78 'release_timestamp': 1546545600,
79 'release_date': '20190103',
81 'season': 'Frank & Kastaniegaarden',
84 'season_number': 2019,
85 'series': 'Frank & Kastaniegaarden',
87 'episode': 'Frank & Kastaniegaarden',
88 'thumbnail': r
're:https?://.+',
91 'skip_download': True,
94 # Foreign and Regular subtitle track
95 'url': 'https://www.dr.dk/drtv/se/spise-med-price_-pasta-selv_397445',
100 'title': 'Spise med Price: Pasta Selv',
101 'alt_title': '1. Pasta Selv',
102 'release_date': '20230807',
103 'description': 'md5:2da9060524fed707810d71080b3d0cd8',
105 'season': 'Spise med Price',
106 'release_timestamp': 1691438400,
107 'season_id': '397440',
108 'episode': 'Spise med Price: Pasta Selv',
109 'thumbnail': r
're:https?://.+',
111 'series': 'Spise med Price',
112 'release_year': 2022,
113 'subtitles': 'mincount:2',
116 'skip_download': 'm3u8',
119 'url': 'https://www.dr.dk/drtv/episode/bonderoeven_71769',
120 'only_matching': True,
122 'url': 'https://dr-massive.com/drtv/se/bonderoeven_71769',
123 'only_matching': True,
125 'url': 'https://www.dr.dk/drtv/program/jagten_220924',
126 'only_matching': True,
130 'DanishLanguageSubtitles': 'da',
131 'ForeignLanguageSubtitles': 'da_foreign',
132 'CombinedLanguageSubtitles': 'da_combined',
137 def _real_initialize(self
):
141 token_response
= self
._download
_json
(
142 'https://production.dr-massive.com/api/authorization/anonymous-sso', None,
143 note
='Downloading anonymous token', headers
={
144 'content-type': 'application/json',
146 'device': 'web_browser',
149 'supportFallbackToken': 'true',
151 'deviceId': str(uuid
.uuid4()),
152 'scopes': ['Catalog'],
156 self
._TOKEN
= traverse_obj(
157 token_response
, (lambda _
, x
: x
['type'] == 'UserAccount', 'value', {str}
), get_all
=False)
159 raise ExtractorError('Unable to get anonymous token')
161 def _real_extract(self
, url
):
162 url_slug
= self
._match
_id
(url
)
163 webpage
= self
._download
_webpage
(url
, url_slug
)
165 json_data
= self
._search
_json
(
166 r
'window\.__data\s*=', webpage
, 'data', url_slug
, fatal
=False) or {}
168 json_data
, ('cache', 'page', ..., (None, ('entries', 0)), 'item', {dict}
), get_all
=False)
170 item_id
= item
.get('id')
172 item_id
= url_slug
.rsplit('_', 1)[-1]
173 item
= self
._download
_json
(
174 f
'https://production-cdn.dr-massive.com/api/items/{item_id}', item_id
,
175 note
='Attempting to download backup item data', query
={
176 'device': 'web_browser',
180 'isDeviceAbroad': 'false',
182 'segments': 'drtv,optedout',
186 video_id
= try_call(lambda: item
['customId'].rsplit(':', 1)[-1]) or item_id
187 stream_data
= self
._download
_json
(
188 f
'https://production.dr-massive.com/api/account/items/{item_id}/videos', video_id
,
189 note
='Downloading stream data', query
={
190 'delivery': 'stream',
191 'device': 'web_browser',
194 'resolution': 'HD-1080',
196 }, headers
={'authorization': f'Bearer {self._TOKEN}
'})
200 for stream in traverse_obj(stream_data, (lambda _, x: x['url
'])):
201 format_id = stream.get('format
', 'na
')
202 access_service = stream.get('accessService
')
205 if access_service in ('SpokenSubtitles
', 'SignLanguage
', 'VisuallyInterpreted
'):
207 format_id += f'-{access_service}
'
208 subtitle_suffix = f'-{access_service}
'
209 elif access_service == 'StandardVideo
':
211 fmts, subs = self._extract_m3u8_formats_and_subtitles(
212 stream.get('url
'), video_id, ext='mp4
', preference=preference, m3u8_id=format_id, fatal=False)
215 api_subtitles = traverse_obj(stream, ('subtitles
', lambda _, v: url_or_none(v['link
']), {dict}))
216 if not api_subtitles:
217 self._merge_subtitles(subs, target=subtitles)
219 for sub_track in api_subtitles:
220 lang = sub_track.get('language
') or 'da
'
221 subtitles.setdefault(self.SUBTITLE_LANGS.get(lang, lang) + subtitle_suffix, []).append({
222 'url
': sub_track['link
'],
223 'ext
': mimetype2ext(sub_track.get('format
')) or 'vtt
'
226 if not formats and traverse_obj(item, ('season
', 'customFields
', 'IsGeoRestricted
')):
227 self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
232 'subtitles
': subtitles,
233 **traverse_obj(item, {
235 'alt_title
': 'contextualTitle
',
236 'description
': 'description
',
237 'thumbnail
': ('images
', 'wallpaper
'),
238 'release_timestamp
': ('customFields
', 'BroadcastTimeDK
', {parse_iso8601}),
239 'duration
': ('duration
', {int_or_none}),
240 'series
': ('season
', 'show
', 'title
'),
241 'season
': ('season
', 'title
'),
242 'season_number
': ('season
', 'seasonNumber
', {int_or_none}),
243 'season_id
': 'seasonId
',
244 'episode
': 'episodeName
',
245 'episode_number
': ('episodeNumber
', {int_or_none}),
246 'release_year
': ('releaseYear
', {int_or_none}),
251 class DRTVLiveIE(InfoExtractor):
252 IE_NAME = 'drtv
:live
'
253 _VALID_URL = r'https?
://(?
:www\
.)?dr\
.dk
/(?
:tv|TV
)/live
/(?P
<id>[\da
-z
-]+)'
254 _GEO_COUNTRIES = ['DK
']
256 'url
': 'https
://www
.dr
.dk
/tv
/live
/dr1
',
260 'title
': 're
:^DR1
[0-9]{4}
-[0-9]{2}
-[0-9]{2}
[0-9]{2}
:[0-9]{2}$
',
264 'skip_download
': True,
268 def _real_extract(self, url):
269 channel_id = self._match_id(url)
270 channel_data = self._download_json(
271 'https
://www
.dr
.dk
/mu
-online
/api
/1.0/channel
/' + channel_id,
273 title = channel_data['Title
']
276 for streaming_server in channel_data.get('StreamingServers
', []):
277 server = streaming_server.get('Server
')
280 link_type = streaming_server.get('LinkType
')
281 for quality in streaming_server.get('Qualities
', []):
282 for stream in quality.get('Streams
', []):
283 stream_path = stream.get('Stream
')
286 stream_url = update_url_query(
287 '%s/%s' % (server, stream_path), {'b': ''})
288 if link_type == 'HLS
':
289 formats.extend(self._extract_m3u8_formats(
290 stream_url, channel_id, 'mp4
',
291 m3u8_id=link_type, fatal=False, live=True))
292 elif link_type == 'HDS
':
293 formats.extend(self._extract_f4m_formats(update_url_query(
294 '%s/%s' % (server, stream_path), {'hdcore': '3.7.0'}),
295 channel_id, f4m_id=link_type, fatal=False))
300 'thumbnail
': channel_data.get('PrimaryImageUri
'),
306 class DRTVSeasonIE(InfoExtractor):
307 IE_NAME = 'drtv
:season
'
308 _VALID_URL = r'https?
://(?
:www\
.)?
(?
:dr\
.dk|dr
-massive\
.com
)/drtv
/saeson
/(?P
<display_id
>[\w
-]+)_(?P
<id>\d
+)'
309 _GEO_COUNTRIES = ['DK
']
311 'url
': 'https
://www
.dr
.dk
/drtv
/saeson
/frank
-and-kastaniegaarden_9008
',
314 'display_id
': 'frank
-and-kastaniegaarden
',
315 'title
': 'Frank
& Kastaniegaarden
',
316 'series
': 'Frank
& Kastaniegaarden
',
317 'season_number
': 2008,
318 'alt_title
': 'Season
2008',
320 'playlist_mincount
': 8
322 'url
': 'https
://www
.dr
.dk
/drtv
/saeson
/frank
-and-kastaniegaarden_8761
',
325 'display_id
': 'frank
-and-kastaniegaarden
',
326 'title
': 'Frank
& Kastaniegaarden
',
327 'series
': 'Frank
& Kastaniegaarden
',
328 'season_number
': 2009,
329 'alt_title
': 'Season
2009',
331 'playlist_mincount
': 19
334 def _real_extract(self, url):
335 display_id, season_id = self._match_valid_url(url).group('display_id
', 'id')
336 data = self._download_json(SERIES_API % f'/saeson
/{display_id}_{season_id}
', display_id)
340 'url
': f'https
://www
.dr
.dk
/drtv{episode["path"]}
',
341 'ie_key
': DRTVIE.ie_key(),
342 'title
': episode.get('title
'),
343 'alt_title
': episode.get('contextualTitle
'),
344 'episode
': episode.get('episodeName
'),
345 'description
': episode.get('shortDescription
'),
346 'series
': traverse_obj(data, ('entries
', 0, 'item
', 'title
')),
347 'season_number
': traverse_obj(data, ('entries
', 0, 'item
', 'seasonNumber
')),
348 'episode_number
': episode.get('episodeNumber
'),
349 } for episode in traverse_obj(data, ('entries
', 0, 'item
', 'episodes
', 'items
'))]
354 'display_id
': display_id,
355 'title
': traverse_obj(data, ('entries
', 0, 'item
', 'title
')),
356 'alt_title
': traverse_obj(data, ('entries
', 0, 'item
', 'contextualTitle
')),
357 'series
': traverse_obj(data, ('entries
', 0, 'item
', 'title
')),
359 'season_number
': traverse_obj(data, ('entries
', 0, 'item
', 'seasonNumber
'))
363 class DRTVSeriesIE(InfoExtractor):
364 IE_NAME = 'drtv
:series
'
365 _VALID_URL = r'https?
://(?
:www\
.)?
(?
:dr\
.dk|dr
-massive\
.com
)/drtv
/serie
/(?P
<display_id
>[\w
-]+)_(?P
<id>\d
+)'
366 _GEO_COUNTRIES = ['DK
']
368 'url
': 'https
://www
.dr
.dk
/drtv
/serie
/frank
-and-kastaniegaarden_6954
',
371 'display_id
': 'frank
-and-kastaniegaarden
',
372 'title
': 'Frank
& Kastaniegaarden
',
373 'series
': 'Frank
& Kastaniegaarden
',
376 'playlist_mincount
': 15
379 def _real_extract(self, url):
380 display_id, series_id = self._match_valid_url(url).group('display_id
', 'id')
381 data = self._download_json(SERIES_API % f'/serie
/{display_id}_{series_id}
', display_id)
385 'url
': f'https
://www
.dr
.dk
/drtv{season.get("path")}
',
386 'ie_key
': DRTVSeasonIE.ie_key(),
387 'title
': season.get('title
'),
388 'alt_title
': season.get('contextualTitle
'),
389 'series
': traverse_obj(data, ('entries
', 0, 'item
', 'title
')),
390 'season_number
': traverse_obj(data, ('entries
', 0, 'item
', 'seasonNumber
'))
391 } for season in traverse_obj(data, ('entries
', 0, 'item
', 'show
', 'seasons
', 'items
'))]
396 'display_id
': display_id,
397 'title
': traverse_obj(data, ('entries
', 0, 'item
', 'title
')),
398 'alt_title
': traverse_obj(data, ('entries
', 0, 'item
', 'contextualTitle
')),
399 'series
': traverse_obj(data, ('entries
', 0, 'item
', 'title
')),