You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
9.9 KiB

  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import re
  4. from uuid import uuid4
  5. from .common import InfoExtractor
  6. from ..compat import (
  7. compat_HTTPError,
  8. compat_str,
  9. )
  10. from ..utils import (
  11. ExtractorError,
  12. int_or_none,
  13. try_get,
  14. url_or_none,
  15. urlencode_postdata,
  16. )
  17. class ZattooBaseIE(InfoExtractor):
  18. _NETRC_MACHINE = 'zattoo'
  19. _HOST_URL = 'https://zattoo.com'
  20. _power_guide_hash = None
  21. def _login(self):
  22. username, password = self._get_login_info()
  23. if not username or not password:
  24. self.raise_login_required(
  25. 'A valid %s account is needed to access this media.'
  26. % self._NETRC_MACHINE)
  27. try:
  28. data = self._download_json(
  29. '%s/zapi/v2/account/login' % self._HOST_URL, None, 'Logging in',
  30. data=urlencode_postdata({
  31. 'login': username,
  32. 'password': password,
  33. 'remember': 'true',
  34. }), headers={
  35. 'Referer': '%s/login' % self._HOST_URL,
  36. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  37. })
  38. except ExtractorError as e:
  39. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
  40. raise ExtractorError(
  41. 'Unable to login: incorrect username and/or password',
  42. expected=True)
  43. raise
  44. self._power_guide_hash = data['session']['power_guide_hash']
  45. def _real_initialize(self):
  46. webpage = self._download_webpage(
  47. self._HOST_URL, None, 'Downloading app token')
  48. app_token = self._html_search_regex(
  49. r'appToken\s*=\s*(["\'])(?P<token>(?:(?!\1).)+?)\1',
  50. webpage, 'app token', group='token')
  51. app_version = self._html_search_regex(
  52. r'<!--\w+-(.+?)-', webpage, 'app version', default='2.8.2')
  53. # Will setup appropriate cookies
  54. self._request_webpage(
  55. '%s/zapi/v2/session/hello' % self._HOST_URL, None,
  56. 'Opening session', data=urlencode_postdata({
  57. 'client_app_token': app_token,
  58. 'uuid': compat_str(uuid4()),
  59. 'lang': 'en',
  60. 'app_version': app_version,
  61. 'format': 'json',
  62. }))
  63. self._login()
  64. def _extract_cid(self, video_id, channel_name):
  65. channel_groups = self._download_json(
  66. '%s/zapi/v2/cached/channels/%s' % (self._HOST_URL,
  67. self._power_guide_hash),
  68. video_id, 'Downloading channel list',
  69. query={'details': False})['channel_groups']
  70. channel_list = []
  71. for chgrp in channel_groups:
  72. channel_list.extend(chgrp['channels'])
  73. try:
  74. return next(
  75. chan['cid'] for chan in channel_list
  76. if chan.get('cid') and (
  77. chan.get('display_alias') == channel_name or
  78. chan.get('cid') == channel_name))
  79. except StopIteration:
  80. raise ExtractorError('Could not extract channel id')
  81. def _extract_cid_and_video_info(self, video_id):
  82. data = self._download_json(
  83. '%s/zapi/program/details' % self._HOST_URL,
  84. video_id,
  85. 'Downloading video information',
  86. query={
  87. 'program_id': video_id,
  88. 'complete': True
  89. })
  90. p = data['program']
  91. cid = p['cid']
  92. info_dict = {
  93. 'id': video_id,
  94. 'title': p.get('title') or p['episode_title'],
  95. 'description': p.get('description'),
  96. 'thumbnail': p.get('image_url'),
  97. 'creator': p.get('channel_name'),
  98. 'episode': p.get('episode_title'),
  99. 'episode_number': int_or_none(p.get('episode_number')),
  100. 'season_number': int_or_none(p.get('season_number')),
  101. 'release_year': int_or_none(p.get('year')),
  102. 'categories': try_get(p, lambda x: x['categories'], list),
  103. }
  104. return cid, info_dict
  105. def _extract_formats(self, cid, video_id, record_id=None, is_live=False):
  106. postdata_common = {
  107. 'https_watch_urls': True,
  108. }
  109. if is_live:
  110. postdata_common.update({'timeshift': 10800})
  111. url = '%s/zapi/watch/live/%s' % (self._HOST_URL, cid)
  112. elif record_id:
  113. url = '%s/zapi/watch/recording/%s' % (self._HOST_URL, record_id)
  114. else:
  115. url = '%s/zapi/watch/recall/%s/%s' % (self._HOST_URL, cid, video_id)
  116. formats = []
  117. for stream_type in ('dash', 'hls', 'hls5', 'hds'):
  118. postdata = postdata_common.copy()
  119. postdata['stream_type'] = stream_type
  120. data = self._download_json(
  121. url, video_id, 'Downloading %s formats' % stream_type.upper(),
  122. data=urlencode_postdata(postdata), fatal=False)
  123. if not data:
  124. continue
  125. watch_urls = try_get(
  126. data, lambda x: x['stream']['watch_urls'], list)
  127. if not watch_urls:
  128. continue
  129. for watch in watch_urls:
  130. if not isinstance(watch, dict):
  131. continue
  132. watch_url = url_or_none(watch.get('url'))
  133. if not watch_url:
  134. continue
  135. format_id_list = [stream_type]
  136. maxrate = watch.get('maxrate')
  137. if maxrate:
  138. format_id_list.append(compat_str(maxrate))
  139. audio_channel = watch.get('audio_channel')
  140. if audio_channel:
  141. format_id_list.append(compat_str(audio_channel))
  142. preference = 1 if audio_channel == 'A' else None
  143. format_id = '-'.join(format_id_list)
  144. if stream_type in ('dash', 'dash_widevine', 'dash_playready'):
  145. this_formats = self._extract_mpd_formats(
  146. watch_url, video_id, mpd_id=format_id, fatal=False)
  147. elif stream_type in ('hls', 'hls5', 'hls5_fairplay'):
  148. this_formats = self._extract_m3u8_formats(
  149. watch_url, video_id, 'mp4',
  150. entry_protocol='m3u8_native', m3u8_id=format_id,
  151. fatal=False)
  152. elif stream_type == 'hds':
  153. this_formats = self._extract_f4m_formats(
  154. watch_url, video_id, f4m_id=format_id, fatal=False)
  155. elif stream_type == 'smooth_playready':
  156. this_formats = self._extract_ism_formats(
  157. watch_url, video_id, ism_id=format_id, fatal=False)
  158. else:
  159. assert False
  160. for this_format in this_formats:
  161. this_format['preference'] = preference
  162. formats.extend(this_formats)
  163. self._sort_formats(formats)
  164. return formats
  165. def _extract_video(self, channel_name, video_id, record_id=None, is_live=False):
  166. if is_live:
  167. cid = self._extract_cid(video_id, channel_name)
  168. info_dict = {
  169. 'id': channel_name,
  170. 'title': self._live_title(channel_name),
  171. 'is_live': True,
  172. }
  173. else:
  174. cid, info_dict = self._extract_cid_and_video_info(video_id)
  175. formats = self._extract_formats(
  176. cid, video_id, record_id=record_id, is_live=is_live)
  177. info_dict['formats'] = formats
  178. return info_dict
  179. class QuicklineBaseIE(ZattooBaseIE):
  180. _NETRC_MACHINE = 'quickline'
  181. _HOST_URL = 'https://mobiltv.quickline.com'
  182. class QuicklineIE(QuicklineBaseIE):
  183. _VALID_URL = r'https?://(?:www\.)?mobiltv\.quickline\.com/watch/(?P<channel>[^/]+)/(?P<id>[0-9]+)'
  184. _TEST = {
  185. 'url': 'https://mobiltv.quickline.com/watch/prosieben/130671867-maze-runner-die-auserwaehlten-in-der-brandwueste',
  186. 'only_matching': True,
  187. }
  188. def _real_extract(self, url):
  189. channel_name, video_id = re.match(self._VALID_URL, url).groups()
  190. return self._extract_video(channel_name, video_id)
  191. class QuicklineLiveIE(QuicklineBaseIE):
  192. _VALID_URL = r'https?://(?:www\.)?mobiltv\.quickline\.com/watch/(?P<id>[^/]+)'
  193. _TEST = {
  194. 'url': 'https://mobiltv.quickline.com/watch/srf1',
  195. 'only_matching': True,
  196. }
  197. @classmethod
  198. def suitable(cls, url):
  199. return False if QuicklineIE.suitable(url) else super(QuicklineLiveIE, cls).suitable(url)
  200. def _real_extract(self, url):
  201. channel_name = video_id = self._match_id(url)
  202. return self._extract_video(channel_name, video_id, is_live=True)
  203. class ZattooIE(ZattooBaseIE):
  204. _VALID_URL = r'https?://(?:www\.)?zattoo\.com/watch/(?P<channel>[^/]+?)/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
  205. # Since regular videos are only available for 7 days and recorded videos
  206. # are only available for a specific user, we cannot have detailed tests.
  207. _TESTS = [{
  208. 'url': 'https://zattoo.com/watch/prosieben/130671867-maze-runner-die-auserwaehlten-in-der-brandwueste',
  209. 'only_matching': True,
  210. }, {
  211. 'url': 'https://zattoo.com/watch/srf_zwei/132905652-eishockey-spengler-cup/102791477/1512211800000/1514433500000/92000',
  212. 'only_matching': True,
  213. }]
  214. def _real_extract(self, url):
  215. channel_name, video_id, record_id = re.match(self._VALID_URL, url).groups()
  216. return self._extract_video(channel_name, video_id, record_id)
  217. class ZattooLiveIE(ZattooBaseIE):
  218. _VALID_URL = r'https?://(?:www\.)?zattoo\.com/watch/(?P<id>[^/]+)'
  219. _TEST = {
  220. 'url': 'https://zattoo.com/watch/srf1',
  221. 'only_matching': True,
  222. }
  223. @classmethod
  224. def suitable(cls, url):
  225. return False if ZattooIE.suitable(url) else super(ZattooLiveIE, cls).suitable(url)
  226. def _real_extract(self, url):
  227. channel_name = video_id = self._match_id(url)
  228. return self._extract_video(channel_name, video_id, is_live=True)