[dplayit] Relax playback info URL extraction
[youtube-dl] / youtube_dl / extractor / dplay.py
1 # coding: utf-8
2 from __future__ import unicode_literals
3
4 import json
5 import re
6 import time
7
8 from .common import InfoExtractor
9 from ..compat import (
10     compat_urlparse,
11     compat_HTTPError,
12 )
13 from ..utils import (
14     USER_AGENTS,
15     ExtractorError,
16     int_or_none,
17     unified_strdate,
18     remove_end,
19     update_url_query,
20 )
21
22
23 class DPlayIE(InfoExtractor):
24     _VALID_URL = r'https?://(?P<domain>www\.dplay\.(?:dk|se|no))/[^/]+/(?P<id>[^/?#]+)'
25
26     _TESTS = [{
27         # non geo restricted, via secure api, unsigned download hls URL
28         'url': 'http://www.dplay.se/nugammalt-77-handelser-som-format-sverige/season-1-svensken-lar-sig-njuta-av-livet/',
29         'info_dict': {
30             'id': '3172',
31             'display_id': 'season-1-svensken-lar-sig-njuta-av-livet',
32             'ext': 'mp4',
33             'title': 'Svensken lär sig njuta av livet',
34             'description': 'md5:d3819c9bccffd0fe458ca42451dd50d8',
35             'duration': 2650,
36             'timestamp': 1365454320,
37             'upload_date': '20130408',
38             'creator': 'Kanal 5 (Home)',
39             'series': 'Nugammalt - 77 händelser som format Sverige',
40             'season_number': 1,
41             'episode_number': 1,
42             'age_limit': 0,
43         },
44     }, {
45         # geo restricted, via secure api, unsigned download hls URL
46         'url': 'http://www.dplay.dk/mig-og-min-mor/season-6-episode-12/',
47         'info_dict': {
48             'id': '70816',
49             'display_id': 'season-6-episode-12',
50             'ext': 'mp4',
51             'title': 'Episode 12',
52             'description': 'md5:9c86e51a93f8a4401fc9641ef9894c90',
53             'duration': 2563,
54             'timestamp': 1429696800,
55             'upload_date': '20150422',
56             'creator': 'Kanal 4 (Home)',
57             'series': 'Mig og min mor',
58             'season_number': 6,
59             'episode_number': 12,
60             'age_limit': 0,
61         },
62     }, {
63         # geo restricted, via direct unsigned hls URL
64         'url': 'http://www.dplay.no/pga-tour/season-1-hoydepunkter-18-21-februar/',
65         'only_matching': True,
66     }]
67
68     def _real_extract(self, url):
69         mobj = re.match(self._VALID_URL, url)
70         display_id = mobj.group('id')
71         domain = mobj.group('domain')
72
73         webpage = self._download_webpage(url, display_id)
74
75         video_id = self._search_regex(
76             r'data-video-id=["\'](\d+)', webpage, 'video id')
77
78         info = self._download_json(
79             'http://%s/api/v2/ajax/videos?video_id=%s' % (domain, video_id),
80             video_id)['data'][0]
81
82         title = info['title']
83
84         PROTOCOLS = ('hls', 'hds')
85         formats = []
86
87         def extract_formats(protocol, manifest_url):
88             if protocol == 'hls':
89                 m3u8_formats = self._extract_m3u8_formats(
90                     manifest_url, video_id, ext='mp4',
91                     entry_protocol='m3u8_native', m3u8_id=protocol, fatal=False)
92                 # Sometimes final URLs inside m3u8 are unsigned, let's fix this
93                 # ourselves. Also fragments' URLs are only served signed for
94                 # Safari user agent.
95                 query = compat_urlparse.parse_qs(compat_urlparse.urlparse(manifest_url).query)
96                 for m3u8_format in m3u8_formats:
97                     m3u8_format.update({
98                         'url': update_url_query(m3u8_format['url'], query),
99                         'http_headers': {
100                             'User-Agent': USER_AGENTS['Safari'],
101                         },
102                     })
103                 formats.extend(m3u8_formats)
104             elif protocol == 'hds':
105                 formats.extend(self._extract_f4m_formats(
106                     manifest_url + '&hdcore=3.8.0&plugin=flowplayer-3.8.0.0',
107                     video_id, f4m_id=protocol, fatal=False))
108
109         domain_tld = domain.split('.')[-1]
110         if domain_tld in ('se', 'dk', 'no'):
111             for protocol in PROTOCOLS:
112                 # Providing dsc-geo allows to bypass geo restriction in some cases
113                 self._set_cookie(
114                     'secure.dplay.%s' % domain_tld, 'dsc-geo',
115                     json.dumps({
116                         'countryCode': domain_tld.upper(),
117                         'expiry': (time.time() + 20 * 60) * 1000,
118                     }))
119                 stream = self._download_json(
120                     'https://secure.dplay.%s/secure/api/v2/user/authorization/stream/%s?stream_type=%s'
121                     % (domain_tld, video_id, protocol), video_id,
122                     'Downloading %s stream JSON' % protocol, fatal=False)
123                 if stream and stream.get(protocol):
124                     extract_formats(protocol, stream[protocol])
125
126         # The last resort is to try direct unsigned hls/hds URLs from info dictionary.
127         # Sometimes this does work even when secure API with dsc-geo has failed (e.g.
128         # http://www.dplay.no/pga-tour/season-1-hoydepunkter-18-21-februar/).
129         if not formats:
130             for protocol in PROTOCOLS:
131                 if info.get(protocol):
132                     extract_formats(protocol, info[protocol])
133
134         self._sort_formats(formats)
135
136         subtitles = {}
137         for lang in ('se', 'sv', 'da', 'nl', 'no'):
138             for format_id in ('web_vtt', 'vtt', 'srt'):
139                 subtitle_url = info.get('subtitles_%s_%s' % (lang, format_id))
140                 if subtitle_url:
141                     subtitles.setdefault(lang, []).append({'url': subtitle_url})
142
143         return {
144             'id': video_id,
145             'display_id': display_id,
146             'title': title,
147             'description': info.get('video_metadata_longDescription'),
148             'duration': int_or_none(info.get('video_metadata_length'), scale=1000),
149             'timestamp': int_or_none(info.get('video_publish_date')),
150             'creator': info.get('video_metadata_homeChannel'),
151             'series': info.get('video_metadata_show'),
152             'season_number': int_or_none(info.get('season')),
153             'episode_number': int_or_none(info.get('episode')),
154             'age_limit': int_or_none(info.get('minimum_age')),
155             'formats': formats,
156             'subtitles': subtitles,
157         }
158
159
160 class DPlayItIE(InfoExtractor):
161     _VALID_URL = r'https?://it\.dplay\.com/[^/]+/[^/]+/(?P<id>[^/?#]+)'
162     _GEO_COUNTRIES = ['IT']
163     _TEST = {
164         'url': 'http://it.dplay.com/nove/biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij/',
165         'md5': '2b808ffb00fc47b884a172ca5d13053c',
166         'info_dict': {
167             'id': '6918',
168             'display_id': 'luigi-di-maio-la-psicosi-di-stanislawskij',
169             'ext': 'mp4',
170             'title': 'Biografie imbarazzanti: Luigi Di Maio: la psicosi di Stanislawskij',
171             'description': 'md5:3c7a4303aef85868f867a26f5cc14813',
172             'thumbnail': r're:^https?://.*\.jpe?g',
173             'upload_date': '20160524',
174             'series': 'Biografie imbarazzanti',
175             'season_number': 1,
176             'episode': 'Luigi Di Maio: la psicosi di Stanislawskij',
177             'episode_number': 1,
178         },
179     }
180
181     def _real_extract(self, url):
182         display_id = self._match_id(url)
183
184         webpage = self._download_webpage(url, display_id)
185
186         info_url = self._search_regex(
187             r'url\s*:\s*["\']((?:https?:)?//[^/]+/playback/videoPlaybackInfo/\d+)',
188             webpage, 'video id')
189
190         title = remove_end(self._og_search_title(webpage), ' | Dplay')
191
192         try:
193             info = self._download_json(
194                 info_url, display_id, headers={
195                     'Authorization': 'Bearer %s' % self._get_cookies(url).get(
196                         'dplayit_token').value,
197                     'Referer': url,
198                 })
199         except ExtractorError as e:
200             if isinstance(e.cause, compat_HTTPError) and e.cause.code in (400, 403):
201                 info = self._parse_json(e.cause.read().decode('utf-8'), display_id)
202                 error = info['errors'][0]
203                 if error.get('code') == 'access.denied.geoblocked':
204                     self.raise_geo_restricted(
205                         msg=error.get('detail'), countries=self._GEO_COUNTRIES)
206                 raise ExtractorError(info['errors'][0]['detail'], expected=True)
207             raise
208
209         hls_url = info['data']['attributes']['streaming']['hls']['url']
210
211         formats = self._extract_m3u8_formats(
212             hls_url, display_id, ext='mp4', entry_protocol='m3u8_native',
213             m3u8_id='hls')
214
215         series = self._html_search_regex(
216             r'(?s)<h1[^>]+class=["\'].*?\bshow_title\b.*?["\'][^>]*>(.+?)</h1>',
217             webpage, 'series', fatal=False)
218         episode = self._search_regex(
219             r'<p[^>]+class=["\'].*?\bdesc_ep\b.*?["\'][^>]*>\s*<br/>\s*<b>([^<]+)',
220             webpage, 'episode', fatal=False)
221
222         mobj = re.search(
223             r'(?s)<span[^>]+class=["\']dates["\'][^>]*>.+?\bS\.(?P<season_number>\d+)\s+E\.(?P<episode_number>\d+)\s*-\s*(?P<upload_date>\d{2}/\d{2}/\d{4})',
224             webpage)
225         if mobj:
226             season_number = int(mobj.group('season_number'))
227             episode_number = int(mobj.group('episode_number'))
228             upload_date = unified_strdate(mobj.group('upload_date'))
229         else:
230             season_number = episode_number = upload_date = None
231
232         return {
233             'id': info_url.rpartition('/')[-1],
234             'display_id': display_id,
235             'title': title,
236             'description': self._og_search_description(webpage),
237             'thumbnail': self._og_search_thumbnail(webpage),
238             'series': series,
239             'season_number': season_number,
240             'episode': episode,
241             'episode_number': episode_number,
242             'upload_date': upload_date,
243             'formats': formats,
244         }