zattoo.py 29 KB

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