zattoo.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. import re
  2. from uuid import uuid4
  3. from .common import InfoExtractor
  4. from ..compat import compat_HTTPError, compat_str
  5. from ..utils import (
  6. ExtractorError,
  7. int_or_none,
  8. join_nonempty,
  9. try_get,
  10. url_or_none,
  11. urlencode_postdata,
  12. )
  13. class ZattooPlatformBaseIE(InfoExtractor):
  14. _power_guide_hash = None
  15. def _host_url(self):
  16. return 'https://%s' % (self._API_HOST if hasattr(self, '_API_HOST') else self._HOST)
  17. def _real_initialize(self):
  18. if not self._power_guide_hash:
  19. self.raise_login_required('An account is needed to access this media', method='password')
  20. def _perform_login(self, username, password):
  21. try:
  22. data = self._download_json(
  23. '%s/zapi/v2/account/login' % self._host_url(), None, 'Logging in',
  24. data=urlencode_postdata({
  25. 'login': username,
  26. 'password': password,
  27. 'remember': 'true',
  28. }), headers={
  29. 'Referer': '%s/login' % self._host_url(),
  30. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  31. })
  32. except ExtractorError as e:
  33. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
  34. raise ExtractorError(
  35. 'Unable to login: incorrect username and/or password',
  36. expected=True)
  37. raise
  38. self._power_guide_hash = data['session']['power_guide_hash']
  39. def _initialize_pre_login(self):
  40. session_token = self._download_json(
  41. f'{self._host_url()}/token.json', None, 'Downloading session token')['session_token']
  42. # Will setup appropriate cookies
  43. self._request_webpage(
  44. '%s/zapi/v3/session/hello' % self._host_url(), None,
  45. 'Opening session', data=urlencode_postdata({
  46. 'uuid': compat_str(uuid4()),
  47. 'lang': 'en',
  48. 'app_version': '1.8.2',
  49. 'format': 'json',
  50. 'client_app_token': session_token,
  51. }))
  52. def _extract_video_id_from_recording(self, recid):
  53. playlist = self._download_json(
  54. f'{self._host_url()}/zapi/v2/playlist', recid, 'Downloading playlist')
  55. try:
  56. return next(
  57. str(item['program_id']) for item in playlist['recordings']
  58. if item.get('program_id') and str(item.get('id')) == recid)
  59. except (StopIteration, KeyError):
  60. raise ExtractorError('Could not extract video id from recording')
  61. def _extract_cid(self, video_id, channel_name):
  62. channel_groups = self._download_json(
  63. '%s/zapi/v2/cached/channels/%s' % (self._host_url(),
  64. self._power_guide_hash),
  65. video_id, 'Downloading channel list',
  66. query={'details': False})['channel_groups']
  67. channel_list = []
  68. for chgrp in channel_groups:
  69. channel_list.extend(chgrp['channels'])
  70. try:
  71. return next(
  72. chan['cid'] for chan in channel_list
  73. if chan.get('cid') and (
  74. chan.get('display_alias') == channel_name
  75. or chan.get('cid') == channel_name))
  76. except StopIteration:
  77. raise ExtractorError('Could not extract channel id')
  78. def _extract_cid_and_video_info(self, video_id):
  79. data = self._download_json(
  80. '%s/zapi/v2/cached/program/power_details/%s' % (
  81. self._host_url(), self._power_guide_hash),
  82. video_id,
  83. 'Downloading video information',
  84. query={
  85. 'program_ids': video_id,
  86. 'complete': True,
  87. })
  88. p = data['programs'][0]
  89. cid = p['cid']
  90. info_dict = {
  91. 'id': video_id,
  92. 'title': p.get('t') or p['et'],
  93. 'description': p.get('d'),
  94. 'thumbnail': p.get('i_url'),
  95. 'creator': p.get('channel_name'),
  96. 'episode': p.get('et'),
  97. 'episode_number': int_or_none(p.get('e_no')),
  98. 'season_number': int_or_none(p.get('s_no')),
  99. 'release_year': int_or_none(p.get('year')),
  100. 'categories': try_get(p, lambda x: x['c'], list),
  101. 'tags': try_get(p, lambda x: x['g'], list)
  102. }
  103. return cid, info_dict
  104. def _extract_ondemand_info(self, ondemand_id):
  105. """
  106. @returns (ondemand_token, ondemand_type, info_dict)
  107. """
  108. data = self._download_json(
  109. '%s/zapi/vod/movies/%s' % (self._host_url(), ondemand_id),
  110. ondemand_id, 'Downloading ondemand information')
  111. info_dict = {
  112. 'id': ondemand_id,
  113. 'title': data.get('title'),
  114. 'description': data.get('description'),
  115. 'duration': int_or_none(data.get('duration')),
  116. 'release_year': int_or_none(data.get('year')),
  117. 'episode_number': int_or_none(data.get('episode_number')),
  118. 'season_number': int_or_none(data.get('season_number')),
  119. 'categories': try_get(data, lambda x: x['categories'], list),
  120. }
  121. return data['terms_catalog'][0]['terms'][0]['token'], data['type'], info_dict
  122. def _extract_formats(self, cid, video_id, record_id=None, ondemand_id=None, ondemand_termtoken=None, ondemand_type=None, is_live=False):
  123. postdata_common = {
  124. 'https_watch_urls': True,
  125. }
  126. if is_live:
  127. postdata_common.update({'timeshift': 10800})
  128. url = '%s/zapi/watch/live/%s' % (self._host_url(), cid)
  129. elif record_id:
  130. url = '%s/zapi/watch/recording/%s' % (self._host_url(), record_id)
  131. elif ondemand_id:
  132. postdata_common.update({
  133. 'teasable_id': ondemand_id,
  134. 'term_token': ondemand_termtoken,
  135. 'teasable_type': ondemand_type
  136. })
  137. url = '%s/zapi/watch/vod/video' % self._host_url()
  138. else:
  139. url = '%s/zapi/v3/watch/replay/%s/%s' % (self._host_url(), cid, video_id)
  140. formats = []
  141. subtitles = {}
  142. for stream_type in ('dash', 'hls7'):
  143. postdata = postdata_common.copy()
  144. postdata['stream_type'] = stream_type
  145. data = self._download_json(
  146. url, video_id, 'Downloading %s formats' % stream_type.upper(),
  147. data=urlencode_postdata(postdata), fatal=False)
  148. if not data:
  149. continue
  150. watch_urls = try_get(
  151. data, lambda x: x['stream']['watch_urls'], list)
  152. if not watch_urls:
  153. continue
  154. for watch in watch_urls:
  155. if not isinstance(watch, dict):
  156. continue
  157. watch_url = url_or_none(watch.get('url'))
  158. if not watch_url:
  159. continue
  160. audio_channel = watch.get('audio_channel')
  161. preference = 1 if audio_channel == 'A' else None
  162. format_id = join_nonempty(stream_type, watch.get('maxrate'), audio_channel)
  163. if stream_type.startswith('dash'):
  164. this_formats, subs = self._extract_mpd_formats_and_subtitles(
  165. watch_url, video_id, mpd_id=format_id, fatal=False)
  166. self._merge_subtitles(subs, target=subtitles)
  167. elif stream_type.startswith('hls'):
  168. this_formats, subs = self._extract_m3u8_formats_and_subtitles(
  169. watch_url, video_id, 'mp4',
  170. entry_protocol='m3u8_native', m3u8_id=format_id,
  171. fatal=False)
  172. self._merge_subtitles(subs, target=subtitles)
  173. elif stream_type == 'hds':
  174. this_formats = self._extract_f4m_formats(
  175. watch_url, video_id, f4m_id=format_id, fatal=False)
  176. elif stream_type == 'smooth_playready':
  177. this_formats = self._extract_ism_formats(
  178. watch_url, video_id, ism_id=format_id, fatal=False)
  179. else:
  180. assert False
  181. for this_format in this_formats:
  182. this_format['quality'] = preference
  183. formats.extend(this_formats)
  184. return formats, subtitles
  185. def _extract_video(self, video_id, record_id=None):
  186. cid, info_dict = self._extract_cid_and_video_info(video_id)
  187. info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
  188. return info_dict
  189. def _extract_live(self, channel_name):
  190. cid = self._extract_cid(channel_name, channel_name)
  191. formats, subtitles = self._extract_formats(cid, cid, is_live=True)
  192. return {
  193. 'id': channel_name,
  194. 'title': channel_name,
  195. 'is_live': True,
  196. 'formats': formats,
  197. 'subtitles': subtitles
  198. }
  199. def _extract_record(self, record_id):
  200. video_id = self._extract_video_id_from_recording(record_id)
  201. cid, info_dict = self._extract_cid_and_video_info(video_id)
  202. info_dict['formats'], info_dict['subtitles'] = self._extract_formats(cid, video_id, record_id=record_id)
  203. return info_dict
  204. def _extract_ondemand(self, ondemand_id):
  205. ondemand_termtoken, ondemand_type, info_dict = self._extract_ondemand_info(ondemand_id)
  206. info_dict['formats'], info_dict['subtitles'] = self._extract_formats(
  207. None, ondemand_id, ondemand_id=ondemand_id,
  208. ondemand_termtoken=ondemand_termtoken, ondemand_type=ondemand_type)
  209. return info_dict
  210. def _real_extract(self, url):
  211. video_id, record_id = self._match_valid_url(url).groups()
  212. return getattr(self, f'_extract_{self._TYPE}')(video_id or record_id)
  213. def _create_valid_url(host, match, qs, base_re=None):
  214. match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
  215. return rf'''(?x)https?://(?:www\.)?{re.escape(host)}/(?:
  216. [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
  217. {match_base}
  218. )'''
  219. class ZattooBaseIE(ZattooPlatformBaseIE):
  220. _NETRC_MACHINE = 'zattoo'
  221. _HOST = 'zattoo.com'
  222. class ZattooIE(ZattooBaseIE):
  223. _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  224. _TYPE = 'video'
  225. _TESTS = [{
  226. 'url': 'https://zattoo.com/program/zdf/250170418',
  227. 'info_dict': {
  228. 'id': '250170418',
  229. 'ext': 'mp4',
  230. 'title': 'Markus Lanz',
  231. 'description': 'md5:e41cb1257de008ca62a73bb876ffa7fc',
  232. 'thumbnail': 're:http://images.zattic.com/cms/.+/format_480x360.jpg',
  233. 'creator': 'ZDF HD',
  234. 'release_year': 2022,
  235. 'episode': 'Folge 1655',
  236. 'categories': 'count:1',
  237. 'tags': 'count:2'
  238. },
  239. 'params': {'skip_download': 'm3u8'}
  240. }, {
  241. 'url': 'https://zattoo.com/program/daserste/210177916',
  242. 'only_matching': True,
  243. }, {
  244. 'url': 'https://zattoo.com/guide/german?channel=srf1&program=169860555',
  245. 'only_matching': True,
  246. }]
  247. class ZattooLiveIE(ZattooBaseIE):
  248. _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  249. _TYPE = 'live'
  250. _TESTS = [{
  251. 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
  252. 'only_matching': True,
  253. }, {
  254. 'url': 'https://zattoo.com/live/srf1',
  255. 'only_matching': True,
  256. }]
  257. @classmethod
  258. def suitable(cls, url):
  259. return False if ZattooIE.suitable(url) else super().suitable(url)
  260. class ZattooMoviesIE(ZattooBaseIE):
  261. _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\w+', 'movie_id', 'vod/movies')
  262. _TYPE = 'ondemand'
  263. _TESTS = [{
  264. 'url': 'https://zattoo.com/vod/movies/7521',
  265. 'only_matching': True,
  266. }, {
  267. 'url': 'https://zattoo.com/ondemand?movie_id=7521&term_token=9f00f43183269484edde',
  268. 'only_matching': True,
  269. }]
  270. class ZattooRecordingsIE(ZattooBaseIE):
  271. _VALID_URL = _create_valid_url('zattoo.com', r'\d+', 'recording')
  272. _TYPE = 'record'
  273. _TESTS = [{
  274. 'url': 'https://zattoo.com/recordings?recording=193615508',
  275. 'only_matching': True,
  276. }, {
  277. 'url': 'https://zattoo.com/tc/ptc_recordings_all_recordings?recording=193615420',
  278. 'only_matching': True,
  279. }]
  280. class NetPlusTVBaseIE(ZattooPlatformBaseIE):
  281. _NETRC_MACHINE = 'netplus'
  282. _HOST = 'netplus.tv'
  283. _API_HOST = 'www.%s' % _HOST
  284. class NetPlusTVIE(NetPlusTVBaseIE):
  285. _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  286. _TYPE = 'video'
  287. _TESTS = [{
  288. 'url': 'https://netplus.tv/program/daserste/210177916',
  289. 'only_matching': True,
  290. }, {
  291. 'url': 'https://netplus.tv/guide/german?channel=srf1&program=169860555',
  292. 'only_matching': True,
  293. }]
  294. class NetPlusTVLiveIE(NetPlusTVBaseIE):
  295. _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  296. _TYPE = 'live'
  297. _TESTS = [{
  298. 'url': 'https://netplus.tv/channels/german?channel=srf_zwei',
  299. 'only_matching': True,
  300. }, {
  301. 'url': 'https://netplus.tv/live/srf1',
  302. 'only_matching': True,
  303. }]
  304. @classmethod
  305. def suitable(cls, url):
  306. return False if NetPlusTVIE.suitable(url) else super().suitable(url)
  307. class NetPlusTVRecordingsIE(NetPlusTVBaseIE):
  308. _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'recording')
  309. _TYPE = 'record'
  310. _TESTS = [{
  311. 'url': 'https://netplus.tv/recordings?recording=193615508',
  312. 'only_matching': True,
  313. }, {
  314. 'url': 'https://netplus.tv/tc/ptc_recordings_all_recordings?recording=193615420',
  315. 'only_matching': True,
  316. }]
  317. class MNetTVBaseIE(ZattooPlatformBaseIE):
  318. _NETRC_MACHINE = 'mnettv'
  319. _HOST = 'tvplus.m-net.de'
  320. class MNetTVIE(MNetTVBaseIE):
  321. _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  322. _TYPE = 'video'
  323. _TESTS = [{
  324. 'url': 'https://tvplus.m-net.de/program/daserste/210177916',
  325. 'only_matching': True,
  326. }, {
  327. 'url': 'https://tvplus.m-net.de/guide/german?channel=srf1&program=169860555',
  328. 'only_matching': True,
  329. }]
  330. class MNetTVLiveIE(MNetTVBaseIE):
  331. _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  332. _TYPE = 'live'
  333. _TESTS = [{
  334. 'url': 'https://tvplus.m-net.de/channels/german?channel=srf_zwei',
  335. 'only_matching': True,
  336. }, {
  337. 'url': 'https://tvplus.m-net.de/live/srf1',
  338. 'only_matching': True,
  339. }]
  340. @classmethod
  341. def suitable(cls, url):
  342. return False if MNetTVIE.suitable(url) else super().suitable(url)
  343. class MNetTVRecordingsIE(MNetTVBaseIE):
  344. _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'recording')
  345. _TYPE = 'record'
  346. _TESTS = [{
  347. 'url': 'https://tvplus.m-net.de/recordings?recording=193615508',
  348. 'only_matching': True,
  349. }, {
  350. 'url': 'https://tvplus.m-net.de/tc/ptc_recordings_all_recordings?recording=193615420',
  351. 'only_matching': True,
  352. }]
  353. class WalyTVBaseIE(ZattooPlatformBaseIE):
  354. _NETRC_MACHINE = 'walytv'
  355. _HOST = 'player.waly.tv'
  356. class WalyTVIE(WalyTVBaseIE):
  357. _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  358. _TYPE = 'video'
  359. _TESTS = [{
  360. 'url': 'https://player.waly.tv/program/daserste/210177916',
  361. 'only_matching': True,
  362. }, {
  363. 'url': 'https://player.waly.tv/guide/german?channel=srf1&program=169860555',
  364. 'only_matching': True,
  365. }]
  366. class WalyTVLiveIE(WalyTVBaseIE):
  367. _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  368. _TYPE = 'live'
  369. _TESTS = [{
  370. 'url': 'https://player.waly.tv/channels/german?channel=srf_zwei',
  371. 'only_matching': True,
  372. }, {
  373. 'url': 'https://player.waly.tv/live/srf1',
  374. 'only_matching': True,
  375. }]
  376. @classmethod
  377. def suitable(cls, url):
  378. return False if WalyTVIE.suitable(url) else super().suitable(url)
  379. class WalyTVRecordingsIE(WalyTVBaseIE):
  380. _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'recording')
  381. _TYPE = 'record'
  382. _TESTS = [{
  383. 'url': 'https://player.waly.tv/recordings?recording=193615508',
  384. 'only_matching': True,
  385. }, {
  386. 'url': 'https://player.waly.tv/tc/ptc_recordings_all_recordings?recording=193615420',
  387. 'only_matching': True,
  388. }]
  389. class BBVTVBaseIE(ZattooPlatformBaseIE):
  390. _NETRC_MACHINE = 'bbvtv'
  391. _HOST = 'bbv-tv.net'
  392. _API_HOST = 'www.%s' % _HOST
  393. class BBVTVIE(BBVTVBaseIE):
  394. _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  395. _TYPE = 'video'
  396. _TESTS = [{
  397. 'url': 'https://bbv-tv.net/program/daserste/210177916',
  398. 'only_matching': True,
  399. }, {
  400. 'url': 'https://bbv-tv.net/guide/german?channel=srf1&program=169860555',
  401. 'only_matching': True,
  402. }]
  403. class BBVTVLiveIE(BBVTVBaseIE):
  404. _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  405. _TYPE = 'live'
  406. _TESTS = [{
  407. 'url': 'https://bbv-tv.net/channels/german?channel=srf_zwei',
  408. 'only_matching': True,
  409. }, {
  410. 'url': 'https://bbv-tv.net/live/srf1',
  411. 'only_matching': True,
  412. }]
  413. @classmethod
  414. def suitable(cls, url):
  415. return False if BBVTVIE.suitable(url) else super().suitable(url)
  416. class BBVTVRecordingsIE(BBVTVBaseIE):
  417. _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'recording')
  418. _TYPE = 'record'
  419. _TESTS = [{
  420. 'url': 'https://bbv-tv.net/recordings?recording=193615508',
  421. 'only_matching': True,
  422. }, {
  423. 'url': 'https://bbv-tv.net/tc/ptc_recordings_all_recordings?recording=193615420',
  424. 'only_matching': True,
  425. }]
  426. class VTXTVBaseIE(ZattooPlatformBaseIE):
  427. _NETRC_MACHINE = 'vtxtv'
  428. _HOST = 'vtxtv.ch'
  429. _API_HOST = 'www.%s' % _HOST
  430. class VTXTVIE(VTXTVBaseIE):
  431. _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  432. _TYPE = 'video'
  433. _TESTS = [{
  434. 'url': 'https://vtxtv.ch/program/daserste/210177916',
  435. 'only_matching': True,
  436. }, {
  437. 'url': 'https://vtxtv.ch/guide/german?channel=srf1&program=169860555',
  438. 'only_matching': True,
  439. }]
  440. class VTXTVLiveIE(VTXTVBaseIE):
  441. _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  442. _TYPE = 'live'
  443. _TESTS = [{
  444. 'url': 'https://vtxtv.ch/channels/german?channel=srf_zwei',
  445. 'only_matching': True,
  446. }, {
  447. 'url': 'https://vtxtv.ch/live/srf1',
  448. 'only_matching': True,
  449. }]
  450. @classmethod
  451. def suitable(cls, url):
  452. return False if VTXTVIE.suitable(url) else super().suitable(url)
  453. class VTXTVRecordingsIE(VTXTVBaseIE):
  454. _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'recording')
  455. _TYPE = 'record'
  456. _TESTS = [{
  457. 'url': 'https://vtxtv.ch/recordings?recording=193615508',
  458. 'only_matching': True,
  459. }, {
  460. 'url': 'https://vtxtv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
  461. 'only_matching': True,
  462. }]
  463. class GlattvisionTVBaseIE(ZattooPlatformBaseIE):
  464. _NETRC_MACHINE = 'glattvisiontv'
  465. _HOST = 'iptv.glattvision.ch'
  466. class GlattvisionTVIE(GlattvisionTVBaseIE):
  467. _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  468. _TYPE = 'video'
  469. _TESTS = [{
  470. 'url': 'https://iptv.glattvision.ch/program/daserste/210177916',
  471. 'only_matching': True,
  472. }, {
  473. 'url': 'https://iptv.glattvision.ch/guide/german?channel=srf1&program=169860555',
  474. 'only_matching': True,
  475. }]
  476. class GlattvisionTVLiveIE(GlattvisionTVBaseIE):
  477. _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  478. _TYPE = 'live'
  479. _TESTS = [{
  480. 'url': 'https://iptv.glattvision.ch/channels/german?channel=srf_zwei',
  481. 'only_matching': True,
  482. }, {
  483. 'url': 'https://iptv.glattvision.ch/live/srf1',
  484. 'only_matching': True,
  485. }]
  486. @classmethod
  487. def suitable(cls, url):
  488. return False if GlattvisionTVIE.suitable(url) else super().suitable(url)
  489. class GlattvisionTVRecordingsIE(GlattvisionTVBaseIE):
  490. _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'recording')
  491. _TYPE = 'record'
  492. _TESTS = [{
  493. 'url': 'https://iptv.glattvision.ch/recordings?recording=193615508',
  494. 'only_matching': True,
  495. }, {
  496. 'url': 'https://iptv.glattvision.ch/tc/ptc_recordings_all_recordings?recording=193615420',
  497. 'only_matching': True,
  498. }]
  499. class SAKTVBaseIE(ZattooPlatformBaseIE):
  500. _NETRC_MACHINE = 'saktv'
  501. _HOST = 'saktv.ch'
  502. _API_HOST = 'www.%s' % _HOST
  503. class SAKTVIE(SAKTVBaseIE):
  504. _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  505. _TYPE = 'video'
  506. _TESTS = [{
  507. 'url': 'https://saktv.ch/program/daserste/210177916',
  508. 'only_matching': True,
  509. }, {
  510. 'url': 'https://saktv.ch/guide/german?channel=srf1&program=169860555',
  511. 'only_matching': True,
  512. }]
  513. class SAKTVLiveIE(SAKTVBaseIE):
  514. _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  515. _TYPE = 'live'
  516. _TESTS = [{
  517. 'url': 'https://saktv.ch/channels/german?channel=srf_zwei',
  518. 'only_matching': True,
  519. }, {
  520. 'url': 'https://saktv.ch/live/srf1',
  521. 'only_matching': True,
  522. }]
  523. @classmethod
  524. def suitable(cls, url):
  525. return False if SAKTVIE.suitable(url) else super().suitable(url)
  526. class SAKTVRecordingsIE(SAKTVBaseIE):
  527. _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'recording')
  528. _TYPE = 'record'
  529. _TESTS = [{
  530. 'url': 'https://saktv.ch/recordings?recording=193615508',
  531. 'only_matching': True,
  532. }, {
  533. 'url': 'https://saktv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
  534. 'only_matching': True,
  535. }]
  536. class EWETVBaseIE(ZattooPlatformBaseIE):
  537. _NETRC_MACHINE = 'ewetv'
  538. _HOST = 'tvonline.ewe.de'
  539. class EWETVIE(EWETVBaseIE):
  540. _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  541. _TYPE = 'video'
  542. _TESTS = [{
  543. 'url': 'https://tvonline.ewe.de/program/daserste/210177916',
  544. 'only_matching': True,
  545. }, {
  546. 'url': 'https://tvonline.ewe.de/guide/german?channel=srf1&program=169860555',
  547. 'only_matching': True,
  548. }]
  549. class EWETVLiveIE(EWETVBaseIE):
  550. _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  551. _TYPE = 'live'
  552. _TESTS = [{
  553. 'url': 'https://tvonline.ewe.de/channels/german?channel=srf_zwei',
  554. 'only_matching': True,
  555. }, {
  556. 'url': 'https://tvonline.ewe.de/live/srf1',
  557. 'only_matching': True,
  558. }]
  559. @classmethod
  560. def suitable(cls, url):
  561. return False if EWETVIE.suitable(url) else super().suitable(url)
  562. class EWETVRecordingsIE(EWETVBaseIE):
  563. _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'recording')
  564. _TYPE = 'record'
  565. _TESTS = [{
  566. 'url': 'https://tvonline.ewe.de/recordings?recording=193615508',
  567. 'only_matching': True,
  568. }, {
  569. 'url': 'https://tvonline.ewe.de/tc/ptc_recordings_all_recordings?recording=193615420',
  570. 'only_matching': True,
  571. }]
  572. class QuantumTVBaseIE(ZattooPlatformBaseIE):
  573. _NETRC_MACHINE = 'quantumtv'
  574. _HOST = 'quantum-tv.com'
  575. _API_HOST = 'www.%s' % _HOST
  576. class QuantumTVIE(QuantumTVBaseIE):
  577. _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  578. _TYPE = 'video'
  579. _TESTS = [{
  580. 'url': 'https://quantum-tv.com/program/daserste/210177916',
  581. 'only_matching': True,
  582. }, {
  583. 'url': 'https://quantum-tv.com/guide/german?channel=srf1&program=169860555',
  584. 'only_matching': True,
  585. }]
  586. class QuantumTVLiveIE(QuantumTVBaseIE):
  587. _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  588. _TYPE = 'live'
  589. _TESTS = [{
  590. 'url': 'https://quantum-tv.com/channels/german?channel=srf_zwei',
  591. 'only_matching': True,
  592. }, {
  593. 'url': 'https://quantum-tv.com/live/srf1',
  594. 'only_matching': True,
  595. }]
  596. @classmethod
  597. def suitable(cls, url):
  598. return False if QuantumTVIE.suitable(url) else super().suitable(url)
  599. class QuantumTVRecordingsIE(QuantumTVBaseIE):
  600. _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'recording')
  601. _TYPE = 'record'
  602. _TESTS = [{
  603. 'url': 'https://quantum-tv.com/recordings?recording=193615508',
  604. 'only_matching': True,
  605. }, {
  606. 'url': 'https://quantum-tv.com/tc/ptc_recordings_all_recordings?recording=193615420',
  607. 'only_matching': True,
  608. }]
  609. class OsnatelTVBaseIE(ZattooPlatformBaseIE):
  610. _NETRC_MACHINE = 'osnateltv'
  611. _HOST = 'tvonline.osnatel.de'
  612. class OsnatelTVIE(OsnatelTVBaseIE):
  613. _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  614. _TYPE = 'video'
  615. _TESTS = [{
  616. 'url': 'https://tvonline.osnatel.de/program/daserste/210177916',
  617. 'only_matching': True,
  618. }, {
  619. 'url': 'https://tvonline.osnatel.de/guide/german?channel=srf1&program=169860555',
  620. 'only_matching': True,
  621. }]
  622. class OsnatelTVLiveIE(OsnatelTVBaseIE):
  623. _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  624. _TYPE = 'live'
  625. _TESTS = [{
  626. 'url': 'https://tvonline.osnatel.de/channels/german?channel=srf_zwei',
  627. 'only_matching': True,
  628. }, {
  629. 'url': 'https://tvonline.osnatel.de/live/srf1',
  630. 'only_matching': True,
  631. }]
  632. @classmethod
  633. def suitable(cls, url):
  634. return False if OsnatelTVIE.suitable(url) else super().suitable(url)
  635. class OsnatelTVRecordingsIE(OsnatelTVBaseIE):
  636. _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'recording')
  637. _TYPE = 'record'
  638. _TESTS = [{
  639. 'url': 'https://tvonline.osnatel.de/recordings?recording=193615508',
  640. 'only_matching': True,
  641. }, {
  642. 'url': 'https://tvonline.osnatel.de/tc/ptc_recordings_all_recordings?recording=193615420',
  643. 'only_matching': True,
  644. }]
  645. class EinsUndEinsTVBaseIE(ZattooPlatformBaseIE):
  646. _NETRC_MACHINE = '1und1tv'
  647. _HOST = '1und1.tv'
  648. _API_HOST = 'www.%s' % _HOST
  649. class EinsUndEinsTVIE(EinsUndEinsTVBaseIE):
  650. _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  651. _TYPE = 'video'
  652. _TESTS = [{
  653. 'url': 'https://1und1.tv/program/daserste/210177916',
  654. 'only_matching': True,
  655. }, {
  656. 'url': 'https://1und1.tv/guide/german?channel=srf1&program=169860555',
  657. 'only_matching': True,
  658. }]
  659. class EinsUndEinsTVLiveIE(EinsUndEinsTVBaseIE):
  660. _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  661. _TYPE = 'live'
  662. _TESTS = [{
  663. 'url': 'https://1und1.tv/channels/german?channel=srf_zwei',
  664. 'only_matching': True,
  665. }, {
  666. 'url': 'https://1und1.tv/live/srf1',
  667. 'only_matching': True,
  668. }]
  669. @classmethod
  670. def suitable(cls, url):
  671. return False if EinsUndEinsTVIE.suitable(url) else super().suitable(url)
  672. class EinsUndEinsTVRecordingsIE(EinsUndEinsTVBaseIE):
  673. _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'recording')
  674. _TYPE = 'record'
  675. _TESTS = [{
  676. 'url': 'https://1und1.tv/recordings?recording=193615508',
  677. 'only_matching': True,
  678. }, {
  679. 'url': 'https://1und1.tv/tc/ptc_recordings_all_recordings?recording=193615420',
  680. 'only_matching': True,
  681. }]
  682. class SaltTVBaseIE(ZattooPlatformBaseIE):
  683. _NETRC_MACHINE = 'salttv'
  684. _HOST = 'tv.salt.ch'
  685. class SaltTVIE(SaltTVBaseIE):
  686. _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
  687. _TYPE = 'video'
  688. _TESTS = [{
  689. 'url': 'https://tv.salt.ch/program/daserste/210177916',
  690. 'only_matching': True,
  691. }, {
  692. 'url': 'https://tv.salt.ch/guide/german?channel=srf1&program=169860555',
  693. 'only_matching': True,
  694. }]
  695. class SaltTVLiveIE(SaltTVBaseIE):
  696. _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
  697. _TYPE = 'live'
  698. _TESTS = [{
  699. 'url': 'https://tv.salt.ch/channels/german?channel=srf_zwei',
  700. 'only_matching': True,
  701. }, {
  702. 'url': 'https://tv.salt.ch/live/srf1',
  703. 'only_matching': True,
  704. }]
  705. @classmethod
  706. def suitable(cls, url):
  707. return False if SaltTVIE.suitable(url) else super().suitable(url)
  708. class SaltTVRecordingsIE(SaltTVBaseIE):
  709. _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'recording')
  710. _TYPE = 'record'
  711. _TESTS = [{
  712. 'url': 'https://tv.salt.ch/recordings?recording=193615508',
  713. 'only_matching': True,
  714. }, {
  715. 'url': 'https://tv.salt.ch/tc/ptc_recordings_all_recordings?recording=193615420',
  716. 'only_matching': True,
  717. }]