dplay.py 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. import json
  2. import uuid
  3. from .common import InfoExtractor
  4. from ..networking.exceptions import HTTPError
  5. from ..utils import (
  6. ExtractorError,
  7. determine_ext,
  8. float_or_none,
  9. int_or_none,
  10. remove_start,
  11. strip_or_none,
  12. try_get,
  13. unified_timestamp,
  14. )
  15. class DPlayBaseIE(InfoExtractor):
  16. _PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
  17. _auth_token_cache = {}
  18. def _get_auth(self, disco_base, display_id, realm, needs_device_id=True):
  19. key = (disco_base, realm)
  20. st = self._get_cookies(disco_base).get('st')
  21. token = (st and st.value) or self._auth_token_cache.get(key)
  22. if not token:
  23. query = {'realm': realm}
  24. if needs_device_id:
  25. query['deviceId'] = uuid.uuid4().hex
  26. token = self._download_json(
  27. disco_base + 'token', display_id, 'Downloading token',
  28. query=query)['data']['attributes']['token']
  29. # Save cache only if cookies are not being set
  30. if not self._get_cookies(disco_base).get('st'):
  31. self._auth_token_cache[key] = token
  32. return f'Bearer {token}'
  33. def _process_errors(self, e, geo_countries):
  34. info = self._parse_json(e.cause.response.read().decode('utf-8'), None)
  35. error = info['errors'][0]
  36. error_code = error.get('code')
  37. if error_code == 'access.denied.geoblocked':
  38. self.raise_geo_restricted(countries=geo_countries)
  39. elif error_code in ('access.denied.missingpackage', 'invalid.token'):
  40. raise ExtractorError(
  41. 'This video is only available for registered users. You may want to use --cookies.', expected=True)
  42. raise ExtractorError(info['errors'][0]['detail'], expected=True)
  43. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  44. headers['Authorization'] = self._get_auth(disco_base, display_id, realm, False)
  45. def _download_video_playback_info(self, disco_base, video_id, headers):
  46. streaming = self._download_json(
  47. disco_base + 'playback/videoPlaybackInfo/' + video_id,
  48. video_id, headers=headers)['data']['attributes']['streaming']
  49. streaming_list = []
  50. for format_id, format_dict in streaming.items():
  51. streaming_list.append({
  52. 'type': format_id,
  53. 'url': format_dict.get('url'),
  54. })
  55. return streaming_list
  56. def _get_disco_api_info(self, url, display_id, disco_host, realm, country, domain=''):
  57. country = self.get_param('geo_bypass_country') or country
  58. geo_countries = [country.upper()]
  59. self._initialize_geo_bypass({
  60. 'countries': geo_countries,
  61. })
  62. disco_base = f'https://{disco_host}/'
  63. headers = {
  64. 'Referer': url,
  65. }
  66. self._update_disco_api_headers(headers, disco_base, display_id, realm)
  67. try:
  68. video = self._download_json(
  69. disco_base + 'content/videos/' + display_id, display_id,
  70. headers=headers, query={
  71. 'fields[channel]': 'name',
  72. 'fields[image]': 'height,src,width',
  73. 'fields[show]': 'name',
  74. 'fields[tag]': 'name',
  75. 'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
  76. 'include': 'images,primaryChannel,show,tags',
  77. })
  78. except ExtractorError as e:
  79. if isinstance(e.cause, HTTPError) and e.cause.status == 400:
  80. self._process_errors(e, geo_countries)
  81. raise
  82. video_id = video['data']['id']
  83. info = video['data']['attributes']
  84. title = info['name'].strip()
  85. formats = []
  86. subtitles = {}
  87. try:
  88. streaming = self._download_video_playback_info(
  89. disco_base, video_id, headers)
  90. except ExtractorError as e:
  91. if isinstance(e.cause, HTTPError) and e.cause.status == 403:
  92. self._process_errors(e, geo_countries)
  93. raise
  94. for format_dict in streaming:
  95. if not isinstance(format_dict, dict):
  96. continue
  97. format_url = format_dict.get('url')
  98. if not format_url:
  99. continue
  100. format_id = format_dict.get('type')
  101. ext = determine_ext(format_url)
  102. if format_id == 'dash' or ext == 'mpd':
  103. dash_fmts, dash_subs = self._extract_mpd_formats_and_subtitles(
  104. format_url, display_id, mpd_id='dash', fatal=False)
  105. formats.extend(dash_fmts)
  106. subtitles = self._merge_subtitles(subtitles, dash_subs)
  107. elif format_id == 'hls' or ext == 'm3u8':
  108. m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
  109. format_url, display_id, 'mp4',
  110. entry_protocol='m3u8_native', m3u8_id='hls',
  111. fatal=False)
  112. formats.extend(m3u8_fmts)
  113. subtitles = self._merge_subtitles(subtitles, m3u8_subs)
  114. else:
  115. formats.append({
  116. 'url': format_url,
  117. 'format_id': format_id,
  118. })
  119. creator = series = None
  120. tags = []
  121. thumbnails = []
  122. included = video.get('included') or []
  123. if isinstance(included, list):
  124. for e in included:
  125. attributes = e.get('attributes')
  126. if not attributes:
  127. continue
  128. e_type = e.get('type')
  129. if e_type == 'channel':
  130. creator = attributes.get('name')
  131. elif e_type == 'image':
  132. src = attributes.get('src')
  133. if src:
  134. thumbnails.append({
  135. 'url': src,
  136. 'width': int_or_none(attributes.get('width')),
  137. 'height': int_or_none(attributes.get('height')),
  138. })
  139. if e_type == 'show':
  140. series = attributes.get('name')
  141. elif e_type == 'tag':
  142. name = attributes.get('name')
  143. if name:
  144. tags.append(name)
  145. return {
  146. 'id': video_id,
  147. 'display_id': display_id,
  148. 'title': title,
  149. 'description': strip_or_none(info.get('description')),
  150. 'duration': float_or_none(info.get('videoDuration'), 1000),
  151. 'timestamp': unified_timestamp(info.get('publishStart')),
  152. 'series': series,
  153. 'season_number': int_or_none(info.get('seasonNumber')),
  154. 'episode_number': int_or_none(info.get('episodeNumber')),
  155. 'creator': creator,
  156. 'tags': tags,
  157. 'thumbnails': thumbnails,
  158. 'formats': formats,
  159. 'subtitles': subtitles,
  160. 'http_headers': {
  161. 'referer': domain,
  162. },
  163. }
  164. class DPlayIE(DPlayBaseIE):
  165. _VALID_URL = r'''(?x)https?://
  166. (?P<domain>
  167. (?:www\.)?(?P<host>d
  168. (?:
  169. play\.(?P<country>dk|fi|jp|se|no)|
  170. iscoveryplus\.(?P<plus_country>dk|es|fi|it|se|no)
  171. )
  172. )|
  173. (?P<subdomain_country>es|it)\.dplay\.com
  174. )/[^/]+''' + DPlayBaseIE._PATH_REGEX
  175. _TESTS = [{
  176. # non geo restricted, via secure api, unsigned download hls URL
  177. 'url': 'https://www.dplay.se/videos/nugammalt-77-handelser-som-format-sverige/nugammalt-77-handelser-som-format-sverige-101',
  178. 'info_dict': {
  179. 'id': '13628',
  180. 'display_id': 'nugammalt-77-handelser-som-format-sverige/nugammalt-77-handelser-som-format-sverige-101',
  181. 'ext': 'mp4',
  182. 'title': 'Svensken lär sig njuta av livet',
  183. 'description': 'md5:d3819c9bccffd0fe458ca42451dd50d8',
  184. 'duration': 2649.856,
  185. 'timestamp': 1365453720,
  186. 'upload_date': '20130408',
  187. 'creator': 'Kanal 5',
  188. 'series': 'Nugammalt - 77 händelser som format Sverige',
  189. 'season_number': 1,
  190. 'episode_number': 1,
  191. },
  192. 'params': {
  193. 'skip_download': True,
  194. },
  195. }, {
  196. # geo restricted, via secure api, unsigned download hls URL
  197. 'url': 'http://www.dplay.dk/videoer/ted-bundy-mind-of-a-monster/ted-bundy-mind-of-a-monster',
  198. 'info_dict': {
  199. 'id': '104465',
  200. 'display_id': 'ted-bundy-mind-of-a-monster/ted-bundy-mind-of-a-monster',
  201. 'ext': 'mp4',
  202. 'title': 'Ted Bundy: Mind Of A Monster',
  203. 'description': 'md5:8b780f6f18de4dae631668b8a9637995',
  204. 'duration': 5290.027,
  205. 'timestamp': 1570694400,
  206. 'upload_date': '20191010',
  207. 'creator': 'ID - Investigation Discovery',
  208. 'series': 'Ted Bundy: Mind Of A Monster',
  209. 'season_number': 1,
  210. 'episode_number': 1,
  211. },
  212. 'params': {
  213. 'skip_download': True,
  214. },
  215. }, {
  216. # disco-api
  217. 'url': 'https://www.dplay.no/videoer/i-kongens-klr/sesong-1-episode-7',
  218. 'info_dict': {
  219. 'id': '40206',
  220. 'display_id': 'i-kongens-klr/sesong-1-episode-7',
  221. 'ext': 'mp4',
  222. 'title': 'Episode 7',
  223. 'description': 'md5:e3e1411b2b9aebeea36a6ec5d50c60cf',
  224. 'duration': 2611.16,
  225. 'timestamp': 1516726800,
  226. 'upload_date': '20180123',
  227. 'series': 'I kongens klær',
  228. 'season_number': 1,
  229. 'episode_number': 7,
  230. },
  231. 'params': {
  232. 'skip_download': True,
  233. },
  234. 'skip': 'Available for Premium users',
  235. }, {
  236. 'url': 'http://it.dplay.com/nove/biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij/',
  237. 'md5': '2b808ffb00fc47b884a172ca5d13053c',
  238. 'info_dict': {
  239. 'id': '6918',
  240. 'display_id': 'biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij',
  241. 'ext': 'mp4',
  242. 'title': 'Luigi Di Maio: la psicosi di Stanislawskij',
  243. 'description': 'md5:3c7a4303aef85868f867a26f5cc14813',
  244. 'thumbnail': r're:^https?://.*\.jpe?g',
  245. 'upload_date': '20160524',
  246. 'timestamp': 1464076800,
  247. 'series': 'Biografie imbarazzanti',
  248. 'season_number': 1,
  249. 'episode': 'Episode 1',
  250. 'episode_number': 1,
  251. },
  252. }, {
  253. 'url': 'https://es.dplay.com/dmax/la-fiebre-del-oro/temporada-8-episodio-1/',
  254. 'info_dict': {
  255. 'id': '21652',
  256. 'display_id': 'la-fiebre-del-oro/temporada-8-episodio-1',
  257. 'ext': 'mp4',
  258. 'title': 'Episodio 1',
  259. 'description': 'md5:b9dcff2071086e003737485210675f69',
  260. 'thumbnail': r're:^https?://.*\.png',
  261. 'upload_date': '20180709',
  262. 'timestamp': 1531173540,
  263. 'series': 'La fiebre del oro',
  264. 'season_number': 8,
  265. 'episode': 'Episode 1',
  266. 'episode_number': 1,
  267. },
  268. 'params': {
  269. 'skip_download': True,
  270. },
  271. }, {
  272. 'url': 'https://www.dplay.fi/videot/shifting-gears-with-aaron-kaufman/episode-16',
  273. 'only_matching': True,
  274. }, {
  275. 'url': 'https://www.dplay.jp/video/gold-rush/24086',
  276. 'only_matching': True,
  277. }, {
  278. 'url': 'https://www.discoveryplus.se/videos/nugammalt-77-handelser-som-format-sverige/nugammalt-77-handelser-som-format-sverige-101',
  279. 'only_matching': True,
  280. }, {
  281. 'url': 'https://www.discoveryplus.dk/videoer/ted-bundy-mind-of-a-monster/ted-bundy-mind-of-a-monster',
  282. 'only_matching': True,
  283. }, {
  284. 'url': 'https://www.discoveryplus.no/videoer/i-kongens-klr/sesong-1-episode-7',
  285. 'only_matching': True,
  286. }, {
  287. 'url': 'https://www.discoveryplus.it/videos/biografie-imbarazzanti/luigi-di-maio-la-psicosi-di-stanislawskij',
  288. 'only_matching': True,
  289. }, {
  290. 'url': 'https://www.discoveryplus.es/videos/la-fiebre-del-oro/temporada-8-episodio-1',
  291. 'only_matching': True,
  292. }, {
  293. 'url': 'https://www.discoveryplus.fi/videot/shifting-gears-with-aaron-kaufman/episode-16',
  294. 'only_matching': True,
  295. }]
  296. def _real_extract(self, url):
  297. mobj = self._match_valid_url(url)
  298. display_id = mobj.group('id')
  299. domain = remove_start(mobj.group('domain'), 'www.')
  300. country = mobj.group('country') or mobj.group('subdomain_country') or mobj.group('plus_country')
  301. host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
  302. return self._get_disco_api_info(
  303. url, display_id, host, 'dplay' + country, country, domain)
  304. class HGTVDeIE(DPlayBaseIE):
  305. _VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayBaseIE._PATH_REGEX
  306. _TESTS = [{
  307. 'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
  308. 'info_dict': {
  309. 'id': '151205',
  310. 'display_id': 'tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette',
  311. 'ext': 'mp4',
  312. 'title': 'Wer braucht schon eine Toilette',
  313. 'description': 'md5:05b40a27e7aed2c9172de34d459134e2',
  314. 'duration': 1177.024,
  315. 'timestamp': 1595705400,
  316. 'upload_date': '20200725',
  317. 'creator': 'HGTV',
  318. 'series': 'Tiny House - klein, aber oho',
  319. 'season_number': 3,
  320. 'episode_number': 3,
  321. },
  322. }]
  323. def _real_extract(self, url):
  324. display_id = self._match_id(url)
  325. return self._get_disco_api_info(
  326. url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
  327. class DiscoveryPlusBaseIE(DPlayBaseIE):
  328. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  329. headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6'
  330. def _download_video_playback_info(self, disco_base, video_id, headers):
  331. return self._download_json(
  332. disco_base + 'playback/v3/videoPlaybackInfo',
  333. video_id, headers=headers, data=json.dumps({
  334. 'deviceInfo': {
  335. 'adBlocker': False,
  336. 'drmSupported': False,
  337. },
  338. 'videoId': video_id,
  339. 'wisteriaProperties': {},
  340. }).encode())['data']['attributes']['streaming']
  341. def _real_extract(self, url):
  342. return self._get_disco_api_info(url, self._match_id(url), **self._DISCO_API_PARAMS)
  343. class GoDiscoveryIE(DiscoveryPlusBaseIE):
  344. _VALID_URL = r'https?://(?:go\.)?discovery\.com/video' + DPlayBaseIE._PATH_REGEX
  345. _TESTS = [{
  346. 'url': 'https://go.discovery.com/video/dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
  347. 'info_dict': {
  348. 'id': '4164906',
  349. 'display_id': 'dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
  350. 'ext': 'mp4',
  351. 'title': 'Rodbuster / Galvanizer',
  352. 'description': 'Mike installs rebar with a team of rodbusters, then he galvanizes steel.',
  353. 'season_number': 9,
  354. 'episode_number': 1,
  355. },
  356. 'skip': 'Available for Premium users',
  357. }, {
  358. 'url': 'https://discovery.com/video/dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
  359. 'only_matching': True,
  360. }]
  361. _PRODUCT = 'dsc'
  362. _DISCO_API_PARAMS = {
  363. 'disco_host': 'us1-prod-direct.go.discovery.com',
  364. 'realm': 'go',
  365. 'country': 'us',
  366. }
  367. class TravelChannelIE(DiscoveryPlusBaseIE):
  368. _VALID_URL = r'https?://(?:watch\.)?travelchannel\.com/video' + DPlayBaseIE._PATH_REGEX
  369. _TESTS = [{
  370. 'url': 'https://watch.travelchannel.com/video/ghost-adventures-travel-channel/ghost-train-of-ely',
  371. 'info_dict': {
  372. 'id': '2220256',
  373. 'display_id': 'ghost-adventures-travel-channel/ghost-train-of-ely',
  374. 'ext': 'mp4',
  375. 'title': 'Ghost Train of Ely',
  376. 'description': 'The crew investigates the dark history of the Nevada Northern Railway.',
  377. 'season_number': 24,
  378. 'episode_number': 1,
  379. },
  380. 'skip': 'Available for Premium users',
  381. }, {
  382. 'url': 'https://watch.travelchannel.com/video/ghost-adventures-travel-channel/ghost-train-of-ely',
  383. 'only_matching': True,
  384. }]
  385. _PRODUCT = 'trav'
  386. _DISCO_API_PARAMS = {
  387. 'disco_host': 'us1-prod-direct.watch.travelchannel.com',
  388. 'realm': 'go',
  389. 'country': 'us',
  390. }
  391. class CookingChannelIE(DiscoveryPlusBaseIE):
  392. _VALID_URL = r'https?://(?:watch\.)?cookingchanneltv\.com/video' + DPlayBaseIE._PATH_REGEX
  393. _TESTS = [{
  394. 'url': 'https://watch.cookingchanneltv.com/video/carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
  395. 'info_dict': {
  396. 'id': '2348634',
  397. 'display_id': 'carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
  398. 'ext': 'mp4',
  399. 'title': 'The Postman Always Brings Rice',
  400. 'description': 'Noah visits the Maui Fair and the Aurora Winter Festival in Vancouver.',
  401. 'season_number': 9,
  402. 'episode_number': 1,
  403. },
  404. 'skip': 'Available for Premium users',
  405. }, {
  406. 'url': 'https://watch.cookingchanneltv.com/video/carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
  407. 'only_matching': True,
  408. }]
  409. _PRODUCT = 'cook'
  410. _DISCO_API_PARAMS = {
  411. 'disco_host': 'us1-prod-direct.watch.cookingchanneltv.com',
  412. 'realm': 'go',
  413. 'country': 'us',
  414. }
  415. class HGTVUsaIE(DiscoveryPlusBaseIE):
  416. _VALID_URL = r'https?://(?:watch\.)?hgtv\.com/video' + DPlayBaseIE._PATH_REGEX
  417. _TESTS = [{
  418. 'url': 'https://watch.hgtv.com/video/home-inspector-joe-hgtv-atve-us/this-mold-house',
  419. 'info_dict': {
  420. 'id': '4289736',
  421. 'display_id': 'home-inspector-joe-hgtv-atve-us/this-mold-house',
  422. 'ext': 'mp4',
  423. 'title': 'This Mold House',
  424. 'description': 'Joe and Noel help take a familys dream home from hazardous to fabulous.',
  425. 'season_number': 1,
  426. 'episode_number': 1,
  427. },
  428. 'skip': 'Available for Premium users',
  429. }, {
  430. 'url': 'https://watch.hgtv.com/video/home-inspector-joe-hgtv-atve-us/this-mold-house',
  431. 'only_matching': True,
  432. }]
  433. _PRODUCT = 'hgtv'
  434. _DISCO_API_PARAMS = {
  435. 'disco_host': 'us1-prod-direct.watch.hgtv.com',
  436. 'realm': 'go',
  437. 'country': 'us',
  438. }
  439. class FoodNetworkIE(DiscoveryPlusBaseIE):
  440. _VALID_URL = r'https?://(?:watch\.)?foodnetwork\.com/video' + DPlayBaseIE._PATH_REGEX
  441. _TESTS = [{
  442. 'url': 'https://watch.foodnetwork.com/video/kids-baking-championship-food-network/float-like-a-butterfly',
  443. 'info_dict': {
  444. 'id': '4116449',
  445. 'display_id': 'kids-baking-championship-food-network/float-like-a-butterfly',
  446. 'ext': 'mp4',
  447. 'title': 'Float Like a Butterfly',
  448. 'description': 'The 12 kid bakers create colorful carved butterfly cakes.',
  449. 'season_number': 10,
  450. 'episode_number': 1,
  451. },
  452. 'skip': 'Available for Premium users',
  453. }, {
  454. 'url': 'https://watch.foodnetwork.com/video/kids-baking-championship-food-network/float-like-a-butterfly',
  455. 'only_matching': True,
  456. }]
  457. _PRODUCT = 'food'
  458. _DISCO_API_PARAMS = {
  459. 'disco_host': 'us1-prod-direct.watch.foodnetwork.com',
  460. 'realm': 'go',
  461. 'country': 'us',
  462. }
  463. class DestinationAmericaIE(DiscoveryPlusBaseIE):
  464. _VALID_URL = r'https?://(?:www\.)?destinationamerica\.com/video' + DPlayBaseIE._PATH_REGEX
  465. _TESTS = [{
  466. 'url': 'https://www.destinationamerica.com/video/alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
  467. 'info_dict': {
  468. 'id': '4210904',
  469. 'display_id': 'alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
  470. 'ext': 'mp4',
  471. 'title': 'Central Alaskas Bigfoot',
  472. 'description': 'A team heads to central Alaska to investigate an aggressive Bigfoot.',
  473. 'season_number': 1,
  474. 'episode_number': 1,
  475. },
  476. 'skip': 'Available for Premium users',
  477. }, {
  478. 'url': 'https://www.destinationamerica.com/video/alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
  479. 'only_matching': True,
  480. }]
  481. _PRODUCT = 'dam'
  482. _DISCO_API_PARAMS = {
  483. 'disco_host': 'us1-prod-direct.destinationamerica.com',
  484. 'realm': 'go',
  485. 'country': 'us',
  486. }
  487. class InvestigationDiscoveryIE(DiscoveryPlusBaseIE):
  488. _VALID_URL = r'https?://(?:www\.)?investigationdiscovery\.com/video' + DPlayBaseIE._PATH_REGEX
  489. _TESTS = [{
  490. 'url': 'https://www.investigationdiscovery.com/video/unmasked-investigation-discovery/the-killer-clown',
  491. 'info_dict': {
  492. 'id': '2139409',
  493. 'display_id': 'unmasked-investigation-discovery/the-killer-clown',
  494. 'ext': 'mp4',
  495. 'title': 'The Killer Clown',
  496. 'description': 'A wealthy Florida woman is fatally shot in the face by a clown at her door.',
  497. 'season_number': 1,
  498. 'episode_number': 1,
  499. },
  500. 'skip': 'Available for Premium users',
  501. }, {
  502. 'url': 'https://www.investigationdiscovery.com/video/unmasked-investigation-discovery/the-killer-clown',
  503. 'only_matching': True,
  504. }]
  505. _PRODUCT = 'ids'
  506. _DISCO_API_PARAMS = {
  507. 'disco_host': 'us1-prod-direct.investigationdiscovery.com',
  508. 'realm': 'go',
  509. 'country': 'us',
  510. }
  511. class AmHistoryChannelIE(DiscoveryPlusBaseIE):
  512. _VALID_URL = r'https?://(?:www\.)?ahctv\.com/video' + DPlayBaseIE._PATH_REGEX
  513. _TESTS = [{
  514. 'url': 'https://www.ahctv.com/video/modern-sniper-ahc/army',
  515. 'info_dict': {
  516. 'id': '2309730',
  517. 'display_id': 'modern-sniper-ahc/army',
  518. 'ext': 'mp4',
  519. 'title': 'Army',
  520. 'description': 'Snipers today face challenges their predecessors couldve only dreamed of.',
  521. 'season_number': 1,
  522. 'episode_number': 1,
  523. },
  524. 'skip': 'Available for Premium users',
  525. }, {
  526. 'url': 'https://www.ahctv.com/video/modern-sniper-ahc/army',
  527. 'only_matching': True,
  528. }]
  529. _PRODUCT = 'ahc'
  530. _DISCO_API_PARAMS = {
  531. 'disco_host': 'us1-prod-direct.ahctv.com',
  532. 'realm': 'go',
  533. 'country': 'us',
  534. }
  535. class ScienceChannelIE(DiscoveryPlusBaseIE):
  536. _VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayBaseIE._PATH_REGEX
  537. _TESTS = [{
  538. 'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
  539. 'info_dict': {
  540. 'id': '2842849',
  541. 'display_id': 'strangest-things-science-atve-us/nazi-mystery-machine',
  542. 'ext': 'mp4',
  543. 'title': 'Nazi Mystery Machine',
  544. 'description': 'Experts investigate the secrets of a revolutionary encryption machine.',
  545. 'season_number': 1,
  546. 'episode_number': 1,
  547. },
  548. 'skip': 'Available for Premium users',
  549. }, {
  550. 'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
  551. 'only_matching': True,
  552. }]
  553. _PRODUCT = 'sci'
  554. _DISCO_API_PARAMS = {
  555. 'disco_host': 'us1-prod-direct.sciencechannel.com',
  556. 'realm': 'go',
  557. 'country': 'us',
  558. }
  559. class DIYNetworkIE(DiscoveryPlusBaseIE):
  560. _VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayBaseIE._PATH_REGEX
  561. _TESTS = [{
  562. 'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
  563. 'info_dict': {
  564. 'id': '2309730',
  565. 'display_id': 'pool-kings-diy-network/bringing-beach-life-to-texas',
  566. 'ext': 'mp4',
  567. 'title': 'Bringing Beach Life to Texas',
  568. 'description': 'The Pool Kings give a family a day at the beach in their own backyard.',
  569. 'season_number': 10,
  570. 'episode_number': 2,
  571. },
  572. 'skip': 'Available for Premium users',
  573. }, {
  574. 'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
  575. 'only_matching': True,
  576. }]
  577. _PRODUCT = 'diy'
  578. _DISCO_API_PARAMS = {
  579. 'disco_host': 'us1-prod-direct.watch.diynetwork.com',
  580. 'realm': 'go',
  581. 'country': 'us',
  582. }
  583. class DiscoveryLifeIE(DiscoveryPlusBaseIE):
  584. _VALID_URL = r'https?://(?:www\.)?discoverylife\.com/video' + DPlayBaseIE._PATH_REGEX
  585. _TESTS = [{
  586. 'url': 'https://www.discoverylife.com/video/surviving-death-discovery-life-atve-us/bodily-trauma',
  587. 'info_dict': {
  588. 'id': '2218238',
  589. 'display_id': 'surviving-death-discovery-life-atve-us/bodily-trauma',
  590. 'ext': 'mp4',
  591. 'title': 'Bodily Trauma',
  592. 'description': 'Meet three people who tested the limits of the human body.',
  593. 'season_number': 1,
  594. 'episode_number': 2,
  595. },
  596. 'skip': 'Available for Premium users',
  597. }, {
  598. 'url': 'https://www.discoverylife.com/video/surviving-death-discovery-life-atve-us/bodily-trauma',
  599. 'only_matching': True,
  600. }]
  601. _PRODUCT = 'dlf'
  602. _DISCO_API_PARAMS = {
  603. 'disco_host': 'us1-prod-direct.discoverylife.com',
  604. 'realm': 'go',
  605. 'country': 'us',
  606. }
  607. class AnimalPlanetIE(DiscoveryPlusBaseIE):
  608. _VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayBaseIE._PATH_REGEX
  609. _TESTS = [{
  610. 'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
  611. 'info_dict': {
  612. 'id': '3338923',
  613. 'display_id': 'north-woods-law-animal-planet/squirrel-showdown',
  614. 'ext': 'mp4',
  615. 'title': 'Squirrel Showdown',
  616. 'description': 'A woman is suspected of being in possession of flying squirrel kits.',
  617. 'season_number': 16,
  618. 'episode_number': 11,
  619. },
  620. 'skip': 'Available for Premium users',
  621. }, {
  622. 'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
  623. 'only_matching': True,
  624. }]
  625. _PRODUCT = 'apl'
  626. _DISCO_API_PARAMS = {
  627. 'disco_host': 'us1-prod-direct.animalplanet.com',
  628. 'realm': 'go',
  629. 'country': 'us',
  630. }
  631. class TLCIE(DiscoveryPlusBaseIE):
  632. _VALID_URL = r'https?://(?:go\.)?tlc\.com/video' + DPlayBaseIE._PATH_REGEX
  633. _TESTS = [{
  634. 'url': 'https://go.tlc.com/video/my-600-lb-life-tlc/melissas-story-part-1',
  635. 'info_dict': {
  636. 'id': '2206540',
  637. 'display_id': 'my-600-lb-life-tlc/melissas-story-part-1',
  638. 'ext': 'mp4',
  639. 'title': 'Melissas Story (Part 1)',
  640. 'description': 'At 650 lbs, Melissa is ready to begin her seven-year weight loss journey.',
  641. 'season_number': 1,
  642. 'episode_number': 1,
  643. },
  644. 'skip': 'Available for Premium users',
  645. }, {
  646. 'url': 'https://go.tlc.com/video/my-600-lb-life-tlc/melissas-story-part-1',
  647. 'only_matching': True,
  648. }]
  649. _PRODUCT = 'tlc'
  650. _DISCO_API_PARAMS = {
  651. 'disco_host': 'us1-prod-direct.tlc.com',
  652. 'realm': 'go',
  653. 'country': 'us',
  654. }
  655. class MotorTrendIE(DiscoveryPlusBaseIE):
  656. _VALID_URL = r'https?://(?:watch\.)?motortrend\.com/video' + DPlayBaseIE._PATH_REGEX
  657. _TESTS = [{
  658. 'url': 'https://watch.motortrend.com/video/car-issues-motortrend-atve-us/double-dakotas',
  659. 'info_dict': {
  660. 'id': '"4859182"',
  661. 'display_id': 'double-dakotas',
  662. 'ext': 'mp4',
  663. 'title': 'Double Dakotas',
  664. 'description': 'Tylers buy-one-get-one Dakota deal has the Wizard pulling double duty.',
  665. 'season_number': 2,
  666. 'episode_number': 3,
  667. },
  668. 'skip': 'Available for Premium users',
  669. }, {
  670. 'url': 'https://watch.motortrend.com/video/car-issues-motortrend-atve-us/double-dakotas',
  671. 'only_matching': True,
  672. }]
  673. _PRODUCT = 'vel'
  674. _DISCO_API_PARAMS = {
  675. 'disco_host': 'us1-prod-direct.watch.motortrend.com',
  676. 'realm': 'go',
  677. 'country': 'us',
  678. }
  679. class MotorTrendOnDemandIE(DiscoveryPlusBaseIE):
  680. _VALID_URL = r'https?://(?:www\.)?motortrend(?:ondemand\.com|\.com/plus)/detail' + DPlayBaseIE._PATH_REGEX
  681. _TESTS = [{
  682. 'url': 'https://www.motortrendondemand.com/detail/wheelstanding-dump-truck-stubby-bobs-comeback/37699/784',
  683. 'info_dict': {
  684. 'id': '37699',
  685. 'display_id': 'wheelstanding-dump-truck-stubby-bobs-comeback/37699',
  686. 'ext': 'mp4',
  687. 'title': 'Wheelstanding Dump Truck! Stubby Bob’s Comeback',
  688. 'description': 'md5:996915abe52a1c3dfc83aecea3cce8e7',
  689. 'season_number': 5,
  690. 'episode_number': 52,
  691. 'episode': 'Episode 52',
  692. 'season': 'Season 5',
  693. 'thumbnail': r're:^https?://.+\.jpe?g$',
  694. 'timestamp': 1388534401,
  695. 'duration': 1887.345,
  696. 'creator': 'Originals',
  697. 'series': 'Roadkill',
  698. 'upload_date': '20140101',
  699. 'tags': [],
  700. },
  701. }, {
  702. 'url': 'https://www.motortrend.com/plus/detail/roadworthy-rescues-teaser-trailer/4922860/',
  703. 'info_dict': {
  704. 'id': '4922860',
  705. 'ext': 'mp4',
  706. 'title': 'Roadworthy Rescues | Teaser Trailer',
  707. 'description': 'Derek Bieri helps Freiburger and Finnegan with their \'68 big-block Dart.',
  708. 'display_id': 'roadworthy-rescues-teaser-trailer/4922860',
  709. 'creator': 'Originals',
  710. 'series': 'Roadworthy Rescues',
  711. 'thumbnail': r're:^https?://.+\.jpe?g$',
  712. 'upload_date': '20220907',
  713. 'timestamp': 1662523200,
  714. 'duration': 1066.356,
  715. 'tags': [],
  716. },
  717. }, {
  718. 'url': 'https://www.motortrend.com/plus/detail/ugly-duckling/2450033/12439',
  719. 'only_matching': True,
  720. }]
  721. _PRODUCT = 'MTOD'
  722. _DISCO_API_PARAMS = {
  723. 'disco_host': 'us1-prod-direct.motortrendondemand.com',
  724. 'realm': 'motortrend',
  725. 'country': 'us',
  726. }
  727. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  728. headers.update({
  729. 'x-disco-params': f'realm={realm}',
  730. 'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:4.39.1-gi1',
  731. 'Authorization': self._get_auth(disco_base, display_id, realm),
  732. })
  733. class DiscoveryPlusIE(DiscoveryPlusBaseIE):
  734. _VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?!it/)(?:\w{2}/)?video' + DPlayBaseIE._PATH_REGEX
  735. _TESTS = [{
  736. 'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
  737. 'info_dict': {
  738. 'id': '1140794',
  739. 'display_id': 'property-brothers-forever-home/food-and-family',
  740. 'ext': 'mp4',
  741. 'title': 'Food and Family',
  742. 'description': 'The brothers help a Richmond family expand their single-level home.',
  743. 'duration': 2583.113,
  744. 'timestamp': 1609304400,
  745. 'upload_date': '20201230',
  746. 'creator': 'HGTV',
  747. 'series': 'Property Brothers: Forever Home',
  748. 'season_number': 1,
  749. 'episode_number': 1,
  750. },
  751. 'skip': 'Available for Premium users',
  752. }, {
  753. 'url': 'https://discoveryplus.com/ca/video/bering-sea-gold-discovery-ca/goldslingers',
  754. 'only_matching': True,
  755. }]
  756. _PRODUCT = 'dplus_us'
  757. _DISCO_API_PARAMS = {
  758. 'disco_host': 'us1-prod-direct.discoveryplus.com',
  759. 'realm': 'go',
  760. 'country': 'us',
  761. }
  762. class DiscoveryPlusIndiaIE(DiscoveryPlusBaseIE):
  763. _VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayBaseIE._PATH_REGEX
  764. _TESTS = [{
  765. 'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
  766. 'info_dict': {
  767. 'id': '27104',
  768. 'ext': 'mp4',
  769. 'display_id': 'how-do-they-do-it/fugu-and-more',
  770. 'title': 'Fugu and More',
  771. 'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
  772. 'duration': 1319.32,
  773. 'timestamp': 1582309800,
  774. 'upload_date': '20200221',
  775. 'series': 'How Do They Do It?',
  776. 'season_number': 8,
  777. 'episode_number': 2,
  778. 'creator': 'Discovery Channel',
  779. 'thumbnail': r're:https://.+\.jpeg',
  780. 'episode': 'Episode 2',
  781. 'season': 'Season 8',
  782. 'tags': [],
  783. },
  784. 'params': {
  785. 'skip_download': True,
  786. },
  787. }]
  788. _PRODUCT = 'dplus-india'
  789. _DISCO_API_PARAMS = {
  790. 'disco_host': 'ap2-prod-direct.discoveryplus.in',
  791. 'realm': 'dplusindia',
  792. 'country': 'in',
  793. 'domain': 'https://www.discoveryplus.in/',
  794. }
  795. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  796. headers.update({
  797. 'x-disco-params': f'realm={realm}',
  798. 'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:17.0.0',
  799. 'Authorization': self._get_auth(disco_base, display_id, realm),
  800. })
  801. class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
  802. _VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de|dplay\.co\.uk)/(?:programme|show|sendungen)/(?P<programme>[^/]+)/(?:video/)?(?P<alternate_id>[^/]+)'
  803. _TESTS = [{
  804. 'url': 'https://dmax.de/sendungen/goldrausch-in-australien/german-gold',
  805. 'info_dict': {
  806. 'id': '4756322',
  807. 'ext': 'mp4',
  808. 'title': 'German Gold',
  809. 'description': 'md5:f3073306553a8d9b40e6ac4cdbf09fc6',
  810. 'display_id': 'goldrausch-in-australien/german-gold',
  811. 'episode': 'Episode 1',
  812. 'episode_number': 1,
  813. 'season': 'Season 5',
  814. 'season_number': 5,
  815. 'series': 'Goldrausch in Australien',
  816. 'duration': 2648.0,
  817. 'upload_date': '20230517',
  818. 'timestamp': 1684357500,
  819. 'creators': ['DMAX'],
  820. 'thumbnail': 'https://eu1-prod-images.disco-api.com/2023/05/09/f72fb510-7992-3b12-af7f-f16a2c22d1e3.jpeg',
  821. 'tags': ['schatzsucher', 'schatz', 'nugget', 'bodenschätze', 'down under', 'australien', 'goldrausch'],
  822. },
  823. 'params': {'skip_download': 'm3u8'},
  824. }, {
  825. 'url': 'https://www.tlc.de/programme/breaking-amish/video/die-welt-da-drauen/DCB331270001100',
  826. 'info_dict': {
  827. 'id': '78867',
  828. 'ext': 'mp4',
  829. 'title': 'Die Welt da draußen',
  830. 'description': 'md5:61033c12b73286e409d99a41742ef608',
  831. 'timestamp': 1554069600,
  832. 'upload_date': '20190331',
  833. 'creator': 'TLC',
  834. 'season': 'Season 1',
  835. 'series': 'Breaking Amish',
  836. 'episode_number': 1,
  837. 'tags': ['new york', 'großstadt', 'amische', 'landleben', 'modern', 'infos', 'tradition', 'herausforderung'],
  838. 'display_id': 'breaking-amish/die-welt-da-drauen',
  839. 'episode': 'Episode 1',
  840. 'duration': 2625.024,
  841. 'season_number': 1,
  842. 'thumbnail': r're:https://.+\.jpg',
  843. },
  844. 'skip': '404 Not Found',
  845. }, {
  846. 'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
  847. 'only_matching': True,
  848. }, {
  849. 'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
  850. 'only_matching': True,
  851. }, {
  852. 'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
  853. 'only_matching': True,
  854. }]
  855. def _real_extract(self, url):
  856. domain, programme, alternate_id = self._match_valid_url(url).groups()
  857. country = 'GB' if domain == 'dplay.co.uk' else 'DE'
  858. realm = 'questuk' if country == 'GB' else domain.replace('.', '')
  859. return self._get_disco_api_info(
  860. url, f'{programme}/{alternate_id}', 'eu1-prod.disco-api.com', realm, country)
  861. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  862. headers.update({
  863. 'x-disco-params': f'realm={realm}',
  864. 'x-disco-client': 'Alps:HyogaPlayer:0.0.0',
  865. 'Authorization': self._get_auth(disco_base, display_id, realm),
  866. })
  867. class DiscoveryPlusShowBaseIE(DPlayBaseIE):
  868. def _entries(self, show_name):
  869. headers = {
  870. 'x-disco-client': self._X_CLIENT,
  871. 'x-disco-params': f'realm={self._REALM}',
  872. 'referer': self._DOMAIN,
  873. 'Authentication': self._get_auth(self._BASE_API, None, self._REALM),
  874. }
  875. show_json = self._download_json(
  876. f'{self._BASE_API}cms/routes/{self._SHOW_STR}/{show_name}?include=default',
  877. video_id=show_name, headers=headers)['included'][self._INDEX]['attributes']['component']
  878. show_id = show_json['mandatoryParams'].split('=')[-1]
  879. season_url = self._BASE_API + 'content/videos?sort=episodeNumber&filter[seasonNumber]={}&filter[show.id]={}&page[size]=100&page[number]={}'
  880. for season in show_json['filters'][0]['options']:
  881. season_id = season['id']
  882. total_pages, page_num = 1, 0
  883. while page_num < total_pages:
  884. season_json = self._download_json(
  885. season_url.format(season_id, show_id, str(page_num + 1)), show_name, headers=headers,
  886. note='Downloading season {} JSON metadata{}'.format(season_id, f' page {page_num}' if page_num else ''))
  887. if page_num == 0:
  888. total_pages = try_get(season_json, lambda x: x['meta']['totalPages'], int) or 1
  889. episodes_json = season_json['data']
  890. for episode in episodes_json:
  891. video_path = episode['attributes']['path']
  892. yield self.url_result(
  893. f'{self._DOMAIN}videos/{video_path}',
  894. ie=self._VIDEO_IE.ie_key(), video_id=episode.get('id') or video_path)
  895. page_num += 1
  896. def _real_extract(self, url):
  897. show_name = self._match_valid_url(url).group('show_name')
  898. return self.playlist_result(self._entries(show_name), playlist_id=show_name)
  899. class DiscoveryPlusItalyIE(DiscoveryPlusBaseIE):
  900. _VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/it/video' + DPlayBaseIE._PATH_REGEX
  901. _TESTS = [{
  902. 'url': 'https://www.discoveryplus.com/it/video/i-signori-della-neve/stagione-2-episodio-1-i-preparativi',
  903. 'only_matching': True,
  904. }, {
  905. 'url': 'https://www.discoveryplus.com/it/video/super-benny/trailer',
  906. 'only_matching': True,
  907. }]
  908. _PRODUCT = 'dplus_us'
  909. _DISCO_API_PARAMS = {
  910. 'disco_host': 'eu1-prod-direct.discoveryplus.com',
  911. 'realm': 'dplay',
  912. 'country': 'it',
  913. }
  914. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  915. headers.update({
  916. 'x-disco-params': f'realm={realm}',
  917. 'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6',
  918. 'Authorization': self._get_auth(disco_base, display_id, realm),
  919. })
  920. class DiscoveryPlusItalyShowIE(DiscoveryPlusShowBaseIE):
  921. _VALID_URL = r'https?://(?:www\.)?discoveryplus\.it/programmi/(?P<show_name>[^/]+)/?(?:[?#]|$)'
  922. _TESTS = [{
  923. 'url': 'https://www.discoveryplus.it/programmi/deal-with-it-stai-al-gioco',
  924. 'playlist_mincount': 168,
  925. 'info_dict': {
  926. 'id': 'deal-with-it-stai-al-gioco',
  927. },
  928. }]
  929. _BASE_API = 'https://disco-api.discoveryplus.it/'
  930. _DOMAIN = 'https://www.discoveryplus.it/'
  931. _X_CLIENT = 'WEB:UNKNOWN:dplay-client:2.6.0'
  932. _REALM = 'dplayit'
  933. _SHOW_STR = 'programmi'
  934. _INDEX = 1
  935. _VIDEO_IE = DPlayIE
  936. class DiscoveryPlusIndiaShowIE(DiscoveryPlusShowBaseIE):
  937. _VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/show/(?P<show_name>[^/]+)/?(?:[?#]|$)'
  938. _TESTS = [{
  939. 'url': 'https://www.discoveryplus.in/show/how-do-they-do-it',
  940. 'playlist_mincount': 140,
  941. 'info_dict': {
  942. 'id': 'how-do-they-do-it',
  943. },
  944. }]
  945. _BASE_API = 'https://ap2-prod-direct.discoveryplus.in/'
  946. _DOMAIN = 'https://www.discoveryplus.in/'
  947. _X_CLIENT = 'WEB:UNKNOWN:dplus-india:prod'
  948. _REALM = 'dplusindia'
  949. _SHOW_STR = 'show'
  950. _INDEX = 4
  951. _VIDEO_IE = DiscoveryPlusIndiaIE
  952. class GlobalCyclingNetworkPlusIE(DiscoveryPlusBaseIE):
  953. _VALID_URL = r'https?://plus\.globalcyclingnetwork\.com/watch/(?P<id>\d+)'
  954. _TESTS = [{
  955. 'url': 'https://plus.globalcyclingnetwork.com/watch/1397691',
  956. 'info_dict': {
  957. 'id': '1397691',
  958. 'ext': 'mp4',
  959. 'title': 'The Athertons: Mountain Biking\'s Fastest Family',
  960. 'description': 'md5:75a81937fcd8b989eec6083a709cd837',
  961. 'thumbnail': 'https://us1-prod-images.disco-api.com/2021/03/04/eb9e3026-4849-3001-8281-9356466f0557.png',
  962. 'series': 'gcn',
  963. 'creator': 'Gcn',
  964. 'upload_date': '20210309',
  965. 'timestamp': 1615248000,
  966. 'duration': 2531.0,
  967. 'tags': [],
  968. },
  969. 'skip': 'Subscription required',
  970. 'params': {'skip_download': 'm3u8'},
  971. }]
  972. _PRODUCT = 'web'
  973. _DISCO_API_PARAMS = {
  974. 'disco_host': 'disco-api-prod.globalcyclingnetwork.com',
  975. 'realm': 'gcn',
  976. 'country': 'us',
  977. }
  978. def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
  979. headers.update({
  980. 'x-disco-params': f'realm={realm}',
  981. 'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:27.3.2',
  982. 'Authorization': self._get_auth(disco_base, display_id, realm),
  983. })