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.

415 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._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. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  241. _TESTS = [{
  242. 'url': 'https://www.netplus.tv/watch/abc/123-abc',
  243. 'only_matching': True,
  244. }]
  245. class MNetTVIE(ZattooIE):
  246. _NETRC_MACHINE = 'mnettv'
  247. _HOST = 'tvplus.m-net.de'
  248. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  249. _TESTS = [{
  250. 'url': 'https://www.tvplus.m-net.de/watch/abc/123-abc',
  251. 'only_matching': True,
  252. }]
  253. class WalyTVIE(ZattooIE):
  254. _NETRC_MACHINE = 'walytv'
  255. _HOST = 'player.waly.tv'
  256. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  257. _TESTS = [{
  258. 'url': 'https://www.player.waly.tv/watch/abc/123-abc',
  259. 'only_matching': True,
  260. }]
  261. class BBVTVIE(ZattooIE):
  262. _NETRC_MACHINE = 'bbvtv'
  263. _HOST = 'bbv-tv.net'
  264. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  265. _TESTS = [{
  266. 'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
  267. 'only_matching': True,
  268. }]
  269. class VTXTVIE(ZattooIE):
  270. _NETRC_MACHINE = 'vtxtv'
  271. _HOST = 'vtxtv.ch'
  272. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  273. _TESTS = [{
  274. 'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
  275. 'only_matching': True,
  276. }]
  277. class MyVisionTVIE(ZattooIE):
  278. _NETRC_MACHINE = 'myvisiontv'
  279. _HOST = 'myvisiontv.ch'
  280. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  281. _TESTS = [{
  282. 'url': 'https://www.myvisiontv.ch/watch/abc/123-abc',
  283. 'only_matching': True,
  284. }]
  285. class GlattvisionTVIE(ZattooIE):
  286. _NETRC_MACHINE = 'glattvisiontv'
  287. _HOST = 'iptv.glattvision.ch'
  288. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  289. _TESTS = [{
  290. 'url': 'https://www.iptv.glattvision.ch/watch/abc/123-abc',
  291. 'only_matching': True,
  292. }]
  293. class SAKTVIE(ZattooIE):
  294. _NETRC_MACHINE = 'saktv'
  295. _HOST = 'saktv.ch'
  296. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  297. _TESTS = [{
  298. 'url': 'https://www.saktv.ch/watch/abc/123-abc',
  299. 'only_matching': True,
  300. }]
  301. class EWETVIE(ZattooIE):
  302. _NETRC_MACHINE = 'ewetv'
  303. _HOST = 'tvonline.ewe.de'
  304. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  305. _TESTS = [{
  306. 'url': 'https://www.tvonline.ewe.de/watch/abc/123-abc',
  307. 'only_matching': True,
  308. }]
  309. class QuantumTVIE(ZattooIE):
  310. _NETRC_MACHINE = 'quantumtv'
  311. _HOST = 'quantum-tv.com'
  312. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  313. _TESTS = [{
  314. 'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
  315. 'only_matching': True,
  316. }]
  317. class OsnatelTVIE(ZattooIE):
  318. _NETRC_MACHINE = 'osnateltv'
  319. _HOST = 'onlinetv.osnatel.de'
  320. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  321. _TESTS = [{
  322. 'url': 'https://www.onlinetv.osnatel.de/watch/abc/123-abc',
  323. 'only_matching': True,
  324. }]
  325. class EinsUndEinsTVIE(ZattooIE):
  326. _NETRC_MACHINE = '1und1tv'
  327. _HOST = '1und1.tv'
  328. _VALID_URL = _make_valid_url(ZattooIE._VALID_URL_TEMPLATE, _HOST)
  329. _TESTS = [{
  330. 'url': 'https://www.1und1.tv/watch/abc/123-abc',
  331. 'only_matching': True,
  332. }]