]> asedeno.scripts.mit.edu Git - youtube-dl.git/blob - youtube_dl/extractor/zdf.py
[NHK] Use new API URL
[youtube-dl.git] / youtube_dl / extractor / zdf.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_str
8 from ..utils import (
9     determine_ext,
10     ExtractorError,
11     float_or_none,
12     int_or_none,
13     merge_dicts,
14     NO_DEFAULT,
15     orderedSet,
16     parse_codecs,
17     qualities,
18     try_get,
19     unified_timestamp,
20     update_url_query,
21     url_or_none,
22     urljoin,
23 )
24
25
26 class ZDFBaseIE(InfoExtractor):
27     _GEO_COUNTRIES = ['DE']
28     _QUALITIES = ('auto', 'low', 'med', 'high', 'veryhigh', 'hd')
29
30     def _call_api(self, url, video_id, item, api_token=None, referrer=None):
31         headers = {}
32         if api_token:
33             headers['Api-Auth'] = 'Bearer %s' % api_token
34         if referrer:
35             headers['Referer'] = referrer
36         return self._download_json(
37             url, video_id, 'Downloading JSON %s' % item, headers=headers)
38
39     @staticmethod
40     def _extract_subtitles(src):
41         subtitles = {}
42         for caption in try_get(src, lambda x: x['captions'], list) or []:
43             subtitle_url = url_or_none(caption.get('uri'))
44             if subtitle_url:
45                 lang = caption.get('language', 'deu')
46                 subtitles.setdefault(lang, []).append({
47                     'url': subtitle_url,
48                 })
49         return subtitles
50
51     def _extract_format(self, video_id, formats, format_urls, meta):
52         format_url = url_or_none(meta.get('url'))
53         if not format_url:
54             return
55         if format_url in format_urls:
56             return
57         format_urls.add(format_url)
58         mime_type = meta.get('mimeType')
59         ext = determine_ext(format_url)
60         if mime_type == 'application/x-mpegURL' or ext == 'm3u8':
61             formats.extend(self._extract_m3u8_formats(
62                 format_url, video_id, 'mp4', m3u8_id='hls',
63                 entry_protocol='m3u8_native', fatal=False))
64         elif mime_type == 'application/f4m+xml' or ext == 'f4m':
65             formats.extend(self._extract_f4m_formats(
66                 update_url_query(format_url, {'hdcore': '3.7.0'}), video_id, f4m_id='hds', fatal=False))
67         else:
68             f = parse_codecs(meta.get('mimeCodec'))
69             format_id = ['http']
70             for p in (meta.get('type'), meta.get('quality')):
71                 if p and isinstance(p, compat_str):
72                     format_id.append(p)
73             f.update({
74                 'url': format_url,
75                 'format_id': '-'.join(format_id),
76                 'format_note': meta.get('quality'),
77                 'language': meta.get('language'),
78                 'quality': qualities(self._QUALITIES)(meta.get('quality')),
79                 'preference': -10,
80             })
81             formats.append(f)
82
83     def _extract_ptmd(self, ptmd_url, video_id, api_token, referrer):
84         ptmd = self._call_api(
85             ptmd_url, video_id, 'metadata', api_token, referrer)
86
87         content_id = ptmd.get('basename') or ptmd_url.split('/')[-1]
88
89         formats = []
90         track_uris = set()
91         for p in ptmd['priorityList']:
92             formitaeten = p.get('formitaeten')
93             if not isinstance(formitaeten, list):
94                 continue
95             for f in formitaeten:
96                 f_qualities = f.get('qualities')
97                 if not isinstance(f_qualities, list):
98                     continue
99                 for quality in f_qualities:
100                     tracks = try_get(quality, lambda x: x['audio']['tracks'], list)
101                     if not tracks:
102                         continue
103                     for track in tracks:
104                         self._extract_format(
105                             content_id, formats, track_uris, {
106                                 'url': track.get('uri'),
107                                 'type': f.get('type'),
108                                 'mimeType': f.get('mimeType'),
109                                 'quality': quality.get('quality'),
110                                 'language': track.get('language'),
111                             })
112         self._sort_formats(formats)
113
114         duration = float_or_none(try_get(
115             ptmd, lambda x: x['attributes']['duration']['value']), scale=1000)
116
117         return {
118             'extractor_key': ZDFIE.ie_key(),
119             'id': content_id,
120             'duration': duration,
121             'formats': formats,
122             'subtitles': self._extract_subtitles(ptmd),
123         }
124
125     def _extract_player(self, webpage, video_id, fatal=True):
126         return self._parse_json(
127             self._search_regex(
128                 r'(?s)data-zdfplayer-jsb=(["\'])(?P<json>{.+?})\1', webpage,
129                 'player JSON', default='{}' if not fatal else NO_DEFAULT,
130                 group='json'),
131             video_id)
132
133
134 class ZDFIE(ZDFBaseIE):
135     _VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
136     _TESTS = [{
137         # Same as https://www.phoenix.de/sendungen/ereignisse/corona-nachgehakt/wohin-fuehrt-der-protest-in-der-pandemie-a-2050630.html
138         'url': 'https://www.zdf.de/politik/phoenix-sendungen/wohin-fuehrt-der-protest-in-der-pandemie-100.html',
139         'md5': '34ec321e7eb34231fd88616c65c92db0',
140         'info_dict': {
141             'id': '210222_phx_nachgehakt_corona_protest',
142             'ext': 'mp4',
143             'title': 'Wohin führt der Protest in der Pandemie?',
144             'description': 'md5:7d643fe7f565e53a24aac036b2122fbd',
145             'duration': 1691,
146             'timestamp': 1613948400,
147             'upload_date': '20210221',
148         },
149         'skip': 'No longer available: "Diese Seite wurde leider nicht gefunden"',
150     }, {
151         # Same as https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html
152         'url': 'https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html',
153         'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
154         'info_dict': {
155             'id': '141007_ab18_10wochensommer_film',
156             'ext': 'mp4',
157             'title': 'Ab 18! - 10 Wochen Sommer',
158             'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26',
159             'duration': 2660,
160             'timestamp': 1608604200,
161             'upload_date': '20201222',
162         },
163         'skip': 'No longer available: "Diese Seite wurde leider nicht gefunden"',
164     }, {
165         'url': 'https://www.zdf.de/dokumentation/terra-x/die-magie-der-farben-von-koenigspurpur-und-jeansblau-100.html',
166         'info_dict': {
167             'id': '151025_magie_farben2_tex',
168             'ext': 'mp4',
169             'title': 'Die Magie der Farben (2/2)',
170             'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
171             'duration': 2615,
172             'timestamp': 1465021200,
173             'upload_date': '20160604',
174         },
175     }, {
176         # Same as https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche
177         'url': 'https://www.zdf.de/politik/phoenix-sendungen/die-gesten-der-maechtigen-100.html',
178         'only_matching': True,
179     }, {
180         # Same as https://www.3sat.de/film/spielfilm/der-hauptmann-100.html
181         'url': 'https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html',
182         'only_matching': True,
183     }, {
184         # Same as https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html, equal media ids
185         'url': 'https://www.zdf.de/wissen/nano/nano-21-mai-2019-102.html',
186         'only_matching': True,
187     }, {
188         'url': 'https://www.zdf.de/service-und-hilfe/die-neue-zdf-mediathek/zdfmediathek-trailer-100.html',
189         'only_matching': True,
190     }, {
191         'url': 'https://www.zdf.de/filme/taunuskrimi/die-lebenden-und-die-toten-1---ein-taunuskrimi-100.html',
192         'only_matching': True,
193     }, {
194         'url': 'https://www.zdf.de/dokumentation/planet-e/planet-e-uebersichtsseite-weitere-dokumentationen-von-planet-e-100.html',
195         'only_matching': True,
196     }, {
197         'url': 'https://www.zdf.de/arte/todliche-flucht/page-video-artede-toedliche-flucht-16-100.html',
198         'info_dict': {
199             'id': 'video_artede_083871-001-A',
200             'ext': 'mp4',
201             'title': 'Tödliche Flucht (1/6)',
202             'description': 'md5:e34f96a9a5f8abd839ccfcebad3d5315',
203             'duration': 3193.0,
204             'timestamp': 1641355200,
205             'upload_date': '20220105',
206         },
207     }]
208
209     def _extract_entry(self, url, player, content, video_id):
210         title = content.get('title') or content['teaserHeadline']
211
212         t = content['mainVideoContent']['http://zdf.de/rels/target']
213
214         def get_ptmd_path(d):
215             return (
216                 d.get('http://zdf.de/rels/streams/ptmd')
217                 or d.get('http://zdf.de/rels/streams/ptmd-template',
218                          '').replace('{playerId}', 'ngplayer_2_4'))
219
220         ptmd_path = get_ptmd_path(try_get(t, lambda x: x['streams']['default'], dict) or {})
221         if not ptmd_path:
222             ptmd_path = get_ptmd_path(t)
223
224         if not ptmd_path:
225             raise ExtractorError('Could not extract ptmd_path')
226
227         info = self._extract_ptmd(
228             urljoin(url, ptmd_path), video_id, player['apiToken'], url)
229
230         thumbnails = []
231         layouts = try_get(
232             content, lambda x: x['teaserImageRef']['layouts'], dict)
233         if layouts:
234             for layout_key, layout_url in layouts.items():
235                 layout_url = url_or_none(layout_url)
236                 if not layout_url:
237                     continue
238                 thumbnail = {
239                     'url': layout_url,
240                     'format_id': layout_key,
241                 }
242                 mobj = re.search(r'(?P<width>\d+)x(?P<height>\d+)', layout_key)
243                 if mobj:
244                     thumbnail.update({
245                         'width': int(mobj.group('width')),
246                         'height': int(mobj.group('height')),
247                     })
248                 thumbnails.append(thumbnail)
249
250         return merge_dicts(info, {
251             'title': title,
252             'description': content.get('leadParagraph') or content.get('teasertext'),
253             'duration': int_or_none(t.get('duration')),
254             'timestamp': unified_timestamp(content.get('editorialDate')),
255             'thumbnails': thumbnails,
256         })
257
258     def _extract_regular(self, url, player, video_id):
259         content = self._call_api(
260             player['content'], video_id, 'content', player['apiToken'], url)
261         return self._extract_entry(player['content'], player, content, video_id)
262
263     def _extract_mobile(self, video_id):
264         video = self._download_json(
265             'https://zdf-cdn.live.cellular.de/mediathekV2/document/%s' % video_id,
266             video_id)
267
268         document = video['document']
269
270         title = document['titel']
271         content_id = document['basename']
272
273         formats = []
274         format_urls = set()
275         for f in document['formitaeten']:
276             self._extract_format(content_id, formats, format_urls, f)
277         self._sort_formats(formats)
278
279         thumbnails = []
280         teaser_bild = document.get('teaserBild')
281         if isinstance(teaser_bild, dict):
282             for thumbnail_key, thumbnail in teaser_bild.items():
283                 thumbnail_url = try_get(
284                     thumbnail, lambda x: x['url'], compat_str)
285                 if thumbnail_url:
286                     thumbnails.append({
287                         'url': thumbnail_url,
288                         'id': thumbnail_key,
289                         'width': int_or_none(thumbnail.get('width')),
290                         'height': int_or_none(thumbnail.get('height')),
291                     })
292
293         return {
294             'id': content_id,
295             'title': title,
296             'description': document.get('beschreibung'),
297             'duration': int_or_none(document.get('length')),
298             'timestamp': unified_timestamp(document.get('date')) or unified_timestamp(
299                 try_get(video, lambda x: x['meta']['editorialDate'], compat_str)),
300             'thumbnails': thumbnails,
301             'subtitles': self._extract_subtitles(document),
302             'formats': formats,
303         }
304
305     def _real_extract(self, url):
306         video_id = self._match_id(url)
307
308         webpage = self._download_webpage(url, video_id, fatal=False)
309         if webpage:
310             player = self._extract_player(webpage, url, fatal=False)
311             if player:
312                 return self._extract_regular(url, player, video_id)
313
314         return self._extract_mobile(video_id)
315
316
317 class ZDFChannelIE(ZDFBaseIE):
318     _VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)'
319     _TESTS = [{
320         'url': 'https://www.zdf.de/sport/das-aktuelle-sportstudio',
321         'info_dict': {
322             'id': 'das-aktuelle-sportstudio',
323             'title': 'das aktuelle sportstudio | ZDF',
324         },
325         'playlist_mincount': 23,
326     }, {
327         'url': 'https://www.zdf.de/dokumentation/planet-e',
328         'info_dict': {
329             'id': 'planet-e',
330             'title': 'planet e.',
331         },
332         'playlist_mincount': 50,
333     }, {
334         'url': 'https://www.zdf.de/filme/taunuskrimi/',
335         'only_matching': True,
336     }]
337
338     @classmethod
339     def suitable(cls, url):
340         return False if ZDFIE.suitable(url) else super(ZDFChannelIE, cls).suitable(url)
341
342     def _real_extract(self, url):
343         channel_id = self._match_id(url)
344
345         webpage = self._download_webpage(url, channel_id)
346
347         entries = [
348             self.url_result(item_url, ie=ZDFIE.ie_key())
349             for item_url in orderedSet(re.findall(
350                 r'data-plusbar-url=["\'](http.+?\.html)', webpage))]
351
352         return self.playlist_result(
353             entries, channel_id, self._og_search_title(webpage, fatal=False))
354
355         r"""
356         player = self._extract_player(webpage, channel_id)
357
358         channel_id = self._search_regex(
359             r'docId\s*:\s*(["\'])(?P<id>(?!\1).+?)\1', webpage,
360             'channel id', group='id')
361
362         channel = self._call_api(
363             'https://api.zdf.de/content/documents/%s.json' % channel_id,
364             player, url, channel_id)
365
366         items = []
367         for module in channel['module']:
368             for teaser in try_get(module, lambda x: x['teaser'], list) or []:
369                 t = try_get(
370                     teaser, lambda x: x['http://zdf.de/rels/target'], dict)
371                 if not t:
372                     continue
373                 items.extend(try_get(
374                     t,
375                     lambda x: x['resultsWithVideo']['http://zdf.de/rels/search/results'],
376                     list) or [])
377             items.extend(try_get(
378                 module,
379                 lambda x: x['filterRef']['resultsWithVideo']['http://zdf.de/rels/search/results'],
380                 list) or [])
381
382         entries = []
383         entry_urls = set()
384         for item in items:
385             t = try_get(item, lambda x: x['http://zdf.de/rels/target'], dict)
386             if not t:
387                 continue
388             sharing_url = t.get('http://zdf.de/rels/sharing-url')
389             if not sharing_url or not isinstance(sharing_url, compat_str):
390                 continue
391             if sharing_url in entry_urls:
392                 continue
393             entry_urls.add(sharing_url)
394             entries.append(self.url_result(
395                 sharing_url, ie=ZDFIE.ie_key(), video_id=t.get('id')))
396
397         return self.playlist_result(entries, channel_id, channel.get('title'))
398         """