mirror of
				https://github.com/yt-dlp/yt-dlp.git
				synced 2025-10-31 14:45:14 +00:00 
			
		
		
		
	| @@ -653,6 +653,7 @@ from .nextmedia import ( | ||||
|     AppleDailyIE, | ||||
|     NextTVIE, | ||||
| ) | ||||
| from .nexx import NexxIE | ||||
| from .nfb import NFBIE | ||||
| from .nfl import NFLIE | ||||
| from .nhk import NhkVodIE | ||||
|   | ||||
| @@ -36,6 +36,7 @@ from .brightcove import ( | ||||
|     BrightcoveLegacyIE, | ||||
|     BrightcoveNewIE, | ||||
| ) | ||||
| from .nexx import NexxIE | ||||
| from .nbc import NBCSportsVPlayerIE | ||||
| from .ooyala import OoyalaIE | ||||
| from .rutv import RUTVIE | ||||
| @@ -1549,6 +1550,22 @@ class GenericIE(InfoExtractor): | ||||
|             }, | ||||
|             'add_ie': ['BrightcoveLegacy'], | ||||
|         }, | ||||
|         # Nexx embed | ||||
|         { | ||||
|             'url': 'https://www.funk.net/serien/5940e15073f6120001657956/items/593efbb173f6120001657503', | ||||
|             'info_dict': { | ||||
|                 'id': '247746', | ||||
|                 'ext': 'mp4', | ||||
|                 'title': "Yesterday's Jam (OV)", | ||||
|                 'description': 'md5:09bc0984723fed34e2581624a84e05f0', | ||||
|                 'timestamp': 1492594816, | ||||
|                 'upload_date': '20170419', | ||||
|             }, | ||||
|             'params': { | ||||
|                 'format': 'bestvideo', | ||||
|                 'skip_download': True, | ||||
|             }, | ||||
|         }, | ||||
|         # Facebook <iframe> embed | ||||
|         { | ||||
|             'url': 'https://www.hostblogger.de/blog/archives/6181-Auto-jagt-Betonmischer.html', | ||||
| @@ -2133,6 +2150,11 @@ class GenericIE(InfoExtractor): | ||||
|         if bc_urls: | ||||
|             return self.playlist_from_matches(bc_urls, video_id, video_title, ie='BrightcoveNew') | ||||
|  | ||||
|         # Look for Nexx embeds | ||||
|         nexx_urls = NexxIE._extract_urls(webpage) | ||||
|         if nexx_urls: | ||||
|             return self.playlist_from_matches(nexx_urls, video_id, video_title, ie=NexxIE.ie_key()) | ||||
|  | ||||
|         # Look for ThePlatform embeds | ||||
|         tp_urls = ThePlatformIE._extract_urls(webpage) | ||||
|         if tp_urls: | ||||
|   | ||||
							
								
								
									
										221
									
								
								youtube_dl/extractor/nexx.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								youtube_dl/extractor/nexx.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| import hashlib | ||||
| import random | ||||
| import re | ||||
| import time | ||||
|  | ||||
| from .common import InfoExtractor | ||||
| from ..compat import compat_str | ||||
| from ..utils import ( | ||||
|     ExtractorError, | ||||
|     int_or_none, | ||||
|     parse_duration, | ||||
|     try_get, | ||||
|     urlencode_postdata, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class NexxIE(InfoExtractor): | ||||
|     _VALID_URL = r'https?://api\.nexx(?:\.cloud|cdn\.com)/v3/(?P<domain_id>\d+)/videos/byid/(?P<id>\d+)' | ||||
|     _TESTS = [{ | ||||
|         # movie | ||||
|         'url': 'https://api.nexx.cloud/v3/748/videos/byid/128907', | ||||
|         'md5': '16746bfc28c42049492385c989b26c4a', | ||||
|         'info_dict': { | ||||
|             'id': '128907', | ||||
|             'ext': 'mp4', | ||||
|             'title': 'Stiftung Warentest', | ||||
|             'alt_title': 'Wie ein Test abläuft', | ||||
|             'description': 'md5:d1ddb1ef63de721132abd38639cc2fd2', | ||||
|             'release_year': 2013, | ||||
|             'creator': 'SPIEGEL TV', | ||||
|             'thumbnail': r're:^https?://.*\.jpg$', | ||||
|             'duration': 2509, | ||||
|             'timestamp': 1384264416, | ||||
|             'upload_date': '20131112', | ||||
|         }, | ||||
|         'params': { | ||||
|             'format': 'bestvideo', | ||||
|         }, | ||||
|     }, { | ||||
|         # episode | ||||
|         'url': 'https://api.nexx.cloud/v3/741/videos/byid/247858', | ||||
|         'info_dict': { | ||||
|             'id': '247858', | ||||
|             'ext': 'mp4', | ||||
|             'title': 'Return of the Golden Child (OV)', | ||||
|             'description': 'md5:5d969537509a92b733de21bae249dc63', | ||||
|             'release_year': 2017, | ||||
|             'thumbnail': r're:^https?://.*\.jpg$', | ||||
|             'duration': 1397, | ||||
|             'timestamp': 1495033267, | ||||
|             'upload_date': '20170517', | ||||
|             'episode_number': 2, | ||||
|             'season_number': 2, | ||||
|         }, | ||||
|         'params': { | ||||
|             'format': 'bestvideo', | ||||
|             'skip_download': True, | ||||
|         }, | ||||
|     }, { | ||||
|         'url': 'https://api.nexxcdn.com/v3/748/videos/byid/128907', | ||||
|         'only_matching': True, | ||||
|     }] | ||||
|  | ||||
|     @staticmethod | ||||
|     def _extract_urls(webpage): | ||||
|         # Reference: | ||||
|         # 1. https://nx-s.akamaized.net/files/201510/44.pdf | ||||
|  | ||||
|         entries = [] | ||||
|  | ||||
|         # JavaScript Integration | ||||
|         for domain_id, video_id in re.findall( | ||||
|                 r'''(?isx) | ||||
|                     <script\b[^>]+\bsrc=["\']https?://require\.nexx(?:\.cloud|cdn\.com)/(\d+).+? | ||||
|                     onPLAYReady.+? | ||||
|                     _play\.init\s*\(.+?\s*,\s*(\d+)\s*,\s*.+?\) | ||||
|                 ''', webpage): | ||||
|             entries.append('https://api.nexx.cloud/v3/%s/videos/byid/%s' % (domain_id, video_id)) | ||||
|  | ||||
|         # TODO: support more embed formats | ||||
|  | ||||
|         return entries | ||||
|  | ||||
|     def _handle_error(self, response): | ||||
|         status = int_or_none(try_get( | ||||
|             response, lambda x: x['metadata']['status']) or 200) | ||||
|         if 200 <= status < 300: | ||||
|             return | ||||
|         raise ExtractorError( | ||||
|             '%s said: %s' % (self.IE_NAME, response['metadata']['errorhint']), | ||||
|             expected=True) | ||||
|  | ||||
|     def _call_api(self, domain_id, path, video_id, data=None, headers={}): | ||||
|         headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' | ||||
|         result = self._download_json( | ||||
|             'https://api.nexx.cloud/v3/%s/%s' % (domain_id, path), video_id, | ||||
|             'Downloading %s JSON' % path, data=urlencode_postdata(data), | ||||
|             headers=headers) | ||||
|         self._handle_error(result) | ||||
|         return result['result'] | ||||
|  | ||||
|     def _real_extract(self, url): | ||||
|         mobj = re.match(self._VALID_URL, url) | ||||
|         domain_id, video_id = mobj.group('domain_id', 'id') | ||||
|  | ||||
|         # Reverse engineered from JS code (see getDeviceID function) | ||||
|         device_id = '%d:%d:%d%d' % ( | ||||
|             random.randint(1, 4), int(time.time()), | ||||
|             random.randint(1e4, 99999), random.randint(1, 9)) | ||||
|  | ||||
|         result = self._call_api(domain_id, 'session/init', video_id, data={ | ||||
|             'nxp_devh': device_id, | ||||
|             'nxp_userh': '', | ||||
|             'precid': '0', | ||||
|             'playlicense': '0', | ||||
|             'screenx': '1920', | ||||
|             'screeny': '1080', | ||||
|             'playerversion': '6.0.00', | ||||
|             'gateway': 'html5', | ||||
|             'adGateway': '', | ||||
|             'explicitlanguage': 'en-US', | ||||
|             'addTextTemplates': '1', | ||||
|             'addDomainData': '1', | ||||
|             'addAdModel': '1', | ||||
|         }, headers={ | ||||
|             'X-Request-Enable-Auth-Fallback': '1', | ||||
|         }) | ||||
|  | ||||
|         cid = result['general']['cid'] | ||||
|  | ||||
|         # As described in [1] X-Request-Token generation algorithm is | ||||
|         # as follows: | ||||
|         #   md5( operation + domain_id + domain_secret ) | ||||
|         # where domain_secret is a static value that will be given by nexx.tv | ||||
|         # as per [1]. Here is how this "secret" is generated (reversed | ||||
|         # from _play.api.init function, search for clienttoken). So it's | ||||
|         # actually not static and not that much of a secret. | ||||
|         # 1. https://nexxtvstorage.blob.core.windows.net/files/201610/27.pdf | ||||
|         secret = result['device']['clienttoken'][int(device_id[0]):] | ||||
|         secret = secret[0:len(secret) - int(device_id[-1])] | ||||
|  | ||||
|         op = 'byid' | ||||
|  | ||||
|         # Reversed from JS code for _play.api.call function (search for | ||||
|         # X-Request-Token) | ||||
|         request_token = hashlib.md5( | ||||
|             ''.join((op, domain_id, secret)).encode('utf-8')).hexdigest() | ||||
|  | ||||
|         video = self._call_api( | ||||
|             domain_id, 'videos/%s/%s' % (op, video_id), video_id, data={ | ||||
|                 'additionalfields': 'language,channel,actors,studio,licenseby,slug,subtitle,teaser,description', | ||||
|                 'addInteractionOptions': '1', | ||||
|                 'addStatusDetails': '1', | ||||
|                 'addStreamDetails': '1', | ||||
|                 'addCaptions': '1', | ||||
|                 'addScenes': '1', | ||||
|                 'addHotSpots': '1', | ||||
|                 'addBumpers': '1', | ||||
|                 'captionFormat': 'data', | ||||
|             }, headers={ | ||||
|                 'X-Request-CID': cid, | ||||
|                 'X-Request-Token': request_token, | ||||
|             }) | ||||
|  | ||||
|         general = video['general'] | ||||
|         title = general['title'] | ||||
|  | ||||
|         stream_data = video['streamdata'] | ||||
|         language = general.get('language_raw') or '' | ||||
|  | ||||
|         # TODO: reverse more cdns and formats | ||||
|  | ||||
|         cdn = stream_data['cdnType'] | ||||
|         assert cdn == 'azure' | ||||
|  | ||||
|         azure_locator = stream_data['azureLocator'] | ||||
|  | ||||
|         AZURE_URL = 'http://nx-p%02d.akamaized.net/' | ||||
|  | ||||
|         for secure in ('s', ''): | ||||
|             cdn_shield = stream_data.get('cdnShieldHTTP%s' % secure.upper()) | ||||
|             if cdn_shield: | ||||
|                 azure_base = 'http%s://%s' % (secure, cdn_shield) | ||||
|                 break | ||||
|         else: | ||||
|             azure_base = AZURE_URL % int(stream_data['azureAccount'].replace('nexxplayplus', '')) | ||||
|  | ||||
|         is_ml = ',' in language | ||||
|         azure_m3u8_url = '%s%s/%s_src%s.ism/Manifest(format=m3u8-aapl)' % ( | ||||
|             azure_base, azure_locator, video_id, ('_manifest' if is_ml else '')) | ||||
|  | ||||
|         protection_token = try_get( | ||||
|             video, lambda x: x['protectiondata']['token'], compat_str) | ||||
|         if protection_token: | ||||
|             azure_m3u8_url += '?hdnts=%s' % protection_token | ||||
|  | ||||
|         formats = self._extract_m3u8_formats( | ||||
|             azure_m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native', | ||||
|             m3u8_id='%s-hls' % cdn) | ||||
|         self._sort_formats(formats) | ||||
|  | ||||
|         return { | ||||
|             'id': video_id, | ||||
|             'title': title, | ||||
|             'alt_title': general.get('subtitle'), | ||||
|             'description': general.get('description'), | ||||
|             'release_year': int_or_none(general.get('year')), | ||||
|             'creator': general.get('studio') or general.get('studio_adref'), | ||||
|             'thumbnail': try_get( | ||||
|                 video, lambda x: x['imagedata']['thumb'], compat_str), | ||||
|             'duration': parse_duration(general.get('runtime')), | ||||
|             'timestamp': int_or_none(general.get('uploaded')), | ||||
|             'episode_number': int_or_none(try_get( | ||||
|                 video, lambda x: x['episodedata']['episode'])), | ||||
|             'season_number': int_or_none(try_get( | ||||
|                 video, lambda x: x['episodedata']['season'])), | ||||
|             'formats': formats, | ||||
|         } | ||||
		Reference in New Issue
	
	Block a user
	 Sergey M․
					Sergey M․