dplay.py 38 KB

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