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.

433 lines
14 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 ZattooPlatformBaseIE(InfoExtractor):
  18. _power_guide_hash = None
  19. def _host_url(self):
  20. return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)
  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/v2/cached/program/power_details/%s' % (
  84. self._host_url(), self._power_guide_hash),
  85. video_id,
  86. 'Downloading video information',
  87. query={
  88. 'program_ids': video_id,
  89. 'complete': True,
  90. })
  91. p = data['programs'][0]
  92. cid = p['cid']
  93. info_dict = {
  94. 'id': video_id,
  95. 'title': p.get('t') or p['et'],
  96. 'description': p.get('d'),
  97. 'thumbnail': p.get('i_url'),
  98. 'creator': p.get('channel_name'),
  99. 'episode': p.get('et'),
  100. 'episode_number': int_or_none(p.get('e_no')),
  101. 'season_number': int_or_none(p.get('s_no')),
  102. 'release_year': int_or_none(p.get('year')),
  103. 'categories': try_get(p, lambda x: x['c'], list),
  104. 'tags': try_get(p, lambda x: x['g'], list)
  105. }
  106. return cid, info_dict
  107. def _extract_formats(self, cid, video_id, record_id=None, is_live=False):
  108. postdata_common = {
  109. 'https_watch_urls': True,
  110. }
  111. if is_live:
  112. postdata_common.update({'timeshift': 10800})
  113. url = '%s/zapi/watch/live/%s' % (self._host_url(), cid)
  114. elif record_id:
  115. url = '%s/zapi/watch/recording/%s' % (self._host_url(), record_id)
  116. else:
  117. url = '%s/zapi/watch/recall/%s/%s' % (self._host_url(), cid, video_id)
  118. formats = []
  119. for stream_type in ('dash', 'hls', 'hls5', 'hds'):
  120. postdata = postdata_common.copy()
  121. postdata['stream_type'] = stream_type
  122. data = self._download_json(
  123. url, video_id, 'Downloading %s formats' % stream_type.upper(),
  124. data=urlencode_postdata(postdata), fatal=False)
  125. if not data:
  126. continue
  127. watch_urls = try_get(
  128. data, lambda x: x['stream']['watch_urls'], list)
  129. if not watch_urls:
  130. continue
  131. for watch in watch_urls:
  132. if not isinstance(watch, dict):
  133. continue
  134. watch_url = url_or_none(watch.get('url'))
  135. if not watch_url:
  136. continue
  137. format_id_list = [stream_type]
  138. maxrate = watch.get('maxrate')
  139. if maxrate:
  140. format_id_list.append(compat_str(maxrate))
  141. audio_channel = watch.get('audio_channel')
  142. if audio_channel:
  143. format_id_list.append(compat_str(audio_channel))
  144. preference = 1 if audio_channel == 'A' else None
  145. format_id = '-'.join(format_id_list)
  146. if stream_type in ('dash', 'dash_widevine', 'dash_playready'):
  147. this_formats = self._extract_mpd_formats(
  148. watch_url, video_id, mpd_id=format_id, fatal=False)
  149. elif stream_type in ('hls', 'hls5', 'hls5_fairplay'):
  150. this_formats = self._extract_m3u8_formats(
  151. watch_url, video_id, 'mp4',
  152. entry_protocol='m3u8_native', m3u8_id=format_id,
  153. fatal=False)
  154. elif stream_type == 'hds':
  155. this_formats = self._extract_f4m_formats(
  156. watch_url, video_id, f4m_id=format_id, fatal=False)
  157. elif stream_type == 'smooth_playready':
  158. this_formats = self._extract_ism_formats(
  159. watch_url, video_id, ism_id=format_id, fatal=False)
  160. else:
  161. assert False
  162. for this_format in this_formats:
  163. this_format['preference'] = preference
  164. formats.extend(this_formats)
  165. self._sort_formats(formats)
  166. return formats
  167. def _extract_video(self, channel_name, video_id, record_id=None, is_live=False):
  168. if is_live:
  169. cid = self._extract_cid(video_id, channel_name)
  170. info_dict = {
  171. 'id': channel_name,
  172. 'title': self._live_title(channel_name),
  173. 'is_live': True,
  174. }
  175. else:
  176. cid, info_dict = self._extract_cid_and_video_info(video_id)
  177. formats = self._extract_formats(
  178. cid, video_id, record_id=record_id, is_live=is_live)
  179. info_dict['formats'] = formats
  180. return info_dict
  181. class QuicklineBaseIE(ZattooPlatformBaseIE):
  182. _NETRC_MACHINE = 'quickline'
  183. _HOST = 'mobiltv.quickline.com'
  184. class QuicklineIE(QuicklineBaseIE):
  185. _VALID_URL = r'https?://(?:www\.)?%s/watch/(?P<channel>[^/]+)/(?P<id>[0-9]+)' % re.escape(QuicklineBaseIE._HOST)
  186. _TEST = {
  187. 'url': 'https://mobiltv.quickline.com/watch/prosieben/130671867-maze-runner-die-auserwaehlten-in-der-brandwueste',
  188. 'only_matching': True,
  189. }
  190. def _real_extract(self, url):
  191. channel_name, video_id = re.match(self._VALID_URL, url).groups()
  192. return self._extract_video(channel_name, video_id)
  193. class QuicklineLiveIE(QuicklineBaseIE):
  194. _VALID_URL = r'https?://(?:www\.)?%s/watch/(?P<id>[^/]+)' % re.escape(QuicklineBaseIE._HOST)
  195. _TEST = {
  196. 'url': 'https://mobiltv.quickline.com/watch/srf1',
  197. 'only_matching': True,
  198. }
  199. @classmethod
  200. def suitable(cls, url):
  201. return False if QuicklineIE.suitable(url) else super(QuicklineLiveIE, cls).suitable(url)
  202. def _real_extract(self, url):
  203. channel_name = video_id = self._match_id(url)
  204. return self._extract_video(channel_name, video_id, is_live=True)
  205. class ZattooBaseIE(ZattooPlatformBaseIE):
  206. _NETRC_MACHINE = 'zattoo'
  207. _HOST = 'zattoo.com'
  208. def _make_valid_url(tmpl, host):
  209. return tmpl % re.escape(host)
  210. class ZattooIE(ZattooBaseIE):
  211. _VALID_URL_TEMPLATE = r'https?://(?:www\.)?%s/watch/(?P<channel>[^/]+?)/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
  212. _VALID_URL = _make_valid_url(_VALID_URL_TEMPLATE, ZattooBaseIE._HOST)
  213. # Since regular videos are only available for 7 days and recorded videos
  214. # are only available for a specific user, we cannot have detailed tests.
  215. _TESTS = [{
  216. 'url': 'https://zattoo.com/watch/prosieben/130671867-maze-runner-die-auserwaehlten-in-der-brandwueste',
  217. 'only_matching': True,
  218. }, {
  219. 'url': 'https://zattoo.com/watch/srf_zwei/132905652-eishockey-spengler-cup/102791477/1512211800000/1514433500000/92000',
  220. 'only_matching': True,
  221. }]
  222. def _real_extract(self, url):
  223. channel_name, video_id, record_id = re.match(self._VALID_URL, url).groups()
  224. return self._extract_video(channel_name, video_id, record_id)
  225. class ZattooLiveIE(ZattooBaseIE):
  226. _VALID_URL = r'https?://(?:www\.)?zattoo\.com/watch/(?P<id>[^/]+)'
  227. _TEST = {
  228. 'url': 'https://zattoo.com/watch/srf1',
  229. 'only_matching': True,
  230. }
  231. @classmethod
  232. def suitable(cls, url):
  233. return False if ZattooIE.suitable(url) else super(ZattooLiveIE, cls).suitable(url)
  234. def _real_extract(self, url):
  235. channel_name = video_id = self._match_id(url)
  236. return self._extract_video(channel_name, video_id, is_live=True)
  237. class NetPlusIE(ZattooIE):
  238. _NETRC_MACHINE = 'netplus'
  239. _HOST = 'netplus.tv'
  240. _API_HOST = 'www.%s' % _HOST
  241. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  242. _TESTS = [{
  243. 'url': 'https://www.netplus.tv/watch/abc/123-abc',
  244. 'only_matching': True,
  245. }]
  246. class MNetTVIE(ZattooIE):
  247. _NETRC_MACHINE = 'mnettv'
  248. _HOST = 'tvplus.m-net.de'
  249. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  250. _TESTS = [{
  251. 'url': 'https://tvplus.m-net.de/watch/abc/123-abc',
  252. 'only_matching': True,
  253. }]
  254. class WalyTVIE(ZattooIE):
  255. _NETRC_MACHINE = 'walytv'
  256. _HOST = 'player.waly.tv'
  257. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  258. _TESTS = [{
  259. 'url': 'https://player.waly.tv/watch/abc/123-abc',
  260. 'only_matching': True,
  261. }]
  262. class BBVTVIE(ZattooIE):
  263. _NETRC_MACHINE = 'bbvtv'
  264. _HOST = 'bbv-tv.net'
  265. _API_HOST = 'www.%s' % _HOST
  266. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  267. _TESTS = [{
  268. 'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
  269. 'only_matching': True,
  270. }]
  271. class VTXTVIE(ZattooIE):
  272. _NETRC_MACHINE = 'vtxtv'
  273. _HOST = 'vtxtv.ch'
  274. _API_HOST = 'www.%s' % _HOST
  275. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  276. _TESTS = [{
  277. 'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
  278. 'only_matching': True,
  279. }]
  280. class MyVisionTVIE(ZattooIE):
  281. _NETRC_MACHINE = 'myvisiontv'
  282. _HOST = 'myvisiontv.ch'
  283. _API_HOST = 'www.%s' % _HOST
  284. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  285. _TESTS = [{
  286. 'url': 'https://www.myvisiontv.ch/watch/abc/123-abc',
  287. 'only_matching': True,
  288. }]
  289. class GlattvisionTVIE(ZattooIE):
  290. _NETRC_MACHINE = 'glattvisiontv'
  291. _HOST = 'iptv.glattvision.ch'
  292. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  293. _TESTS = [{
  294. 'url': 'https://iptv.glattvision.ch/watch/abc/123-abc',
  295. 'only_matching': True,
  296. }]
  297. class SAKTVIE(ZattooIE):
  298. _NETRC_MACHINE = 'saktv'
  299. _HOST = 'saktv.ch'
  300. _API_HOST = 'www.%s' % _HOST
  301. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  302. _TESTS = [{
  303. 'url': 'https://www.saktv.ch/watch/abc/123-abc',
  304. 'only_matching': True,
  305. }]
  306. class EWETVIE(ZattooIE):
  307. _NETRC_MACHINE = 'ewetv'
  308. _HOST = 'tvonline.ewe.de'
  309. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  310. _TESTS = [{
  311. 'url': 'https://tvonline.ewe.de/watch/abc/123-abc',
  312. 'only_matching': True,
  313. }]
  314. class QuantumTVIE(ZattooIE):
  315. _NETRC_MACHINE = 'quantumtv'
  316. _HOST = 'quantum-tv.com'
  317. _API_HOST = 'www.%s' % _HOST
  318. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  319. _TESTS = [{
  320. 'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
  321. 'only_matching': True,
  322. }]
  323. class OsnatelTVIE(ZattooIE):
  324. _NETRC_MACHINE = 'osnateltv'
  325. _HOST = 'tvonline.osnatel.de'
  326. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  327. _TESTS = [{
  328. 'url': 'https://tvonline.osnatel.de/watch/abc/123-abc',
  329. 'only_matching': True,
  330. }]
  331. class EinsUndEinsTVIE(ZattooIE):
  332. _NETRC_MACHINE = '1und1tv'
  333. _HOST = '1und1.tv'
  334. _API_HOST = 'www.%s' % _HOST
  335. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  336. _TESTS = [{
  337. 'url': 'https://www.1und1.tv/watch/abc/123-abc',
  338. 'only_matching': True,
  339. }]
  340. class SaltTVIE(ZattooIE):
  341. _NETRC_MACHINE = 'salttv'
  342. _HOST = 'tv.salt.ch'
  343. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  344. _TESTS = [{
  345. 'url': 'https://tv.salt.ch/watch/abc/123-abc',
  346. 'only_matching': True,
  347. }]