Enigma2 plugin to to play various online streams (mostly Latvian).

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. # -*- coding: UTF-8 -*-
  2. # This video extraction code based on youtube-dl: https://github.com/rg3/youtube-dl
  3. import codecs
  4. import json
  5. import re
  6. from urllib import urlencode
  7. from urllib2 import urlopen, URLError
  8. import sys
  9. import ssl
  10. ssl._create_default_https_context = ssl._create_unverified_context
  11. #from Components.config import config
  12. #from . import sslContext
  13. sslContext = None
  14. if sys.version_info >= (2, 7, 9):
  15. try:
  16. import ssl
  17. sslContext = ssl._create_unverified_context()
  18. except:
  19. pass
  20. from jsinterp import JSInterpreter
  21. from swfinterp import SWFInterpreter
  22. PRIORITY_VIDEO_FORMAT = []
  23. maxResolution = '22'
  24. def createPriorityFormats():
  25. global PRIORITY_VIDEO_FORMAT,maxResolution
  26. PRIORITY_VIDEO_FORMAT = []
  27. use_format = False
  28. for itag_value in ['38', '37', '96', '22', '95', '120',
  29. '35', '94', '18', '93', '5', '92', '132', '17']:
  30. if itag_value == maxResolution: #config.plugins.YouTube.maxResolution.value:
  31. use_format = True
  32. if use_format:
  33. PRIORITY_VIDEO_FORMAT.append(itag_value)
  34. createPriorityFormats()
  35. IGNORE_VIDEO_FORMAT = [
  36. '43', # webm
  37. '44', # webm
  38. '45', # webm
  39. '46', # webm
  40. '100', # webm
  41. '101', # webm
  42. '102' # webm
  43. ]
  44. def uppercase_escape(s):
  45. unicode_escape = codecs.getdecoder('unicode_escape')
  46. return re.sub(
  47. r'\\U[0-9a-fA-F]{8}',
  48. lambda m: unicode_escape(m.group(0))[0],
  49. s)
  50. def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
  51. if string == '':
  52. return string
  53. res = string.split('%')
  54. if len(res) == 1:
  55. return string
  56. if encoding is None:
  57. encoding = 'utf-8'
  58. if errors is None:
  59. errors = 'replace'
  60. # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
  61. pct_sequence = b''
  62. string = res[0]
  63. for item in res[1:]:
  64. try:
  65. if not item:
  66. raise ValueError
  67. pct_sequence += item[:2].decode('hex')
  68. rest = item[2:]
  69. if not rest:
  70. # This segment was just a single percent-encoded character.
  71. # May be part of a sequence of code units, so delay decoding.
  72. # (Stored in pct_sequence).
  73. continue
  74. except ValueError:
  75. rest = '%' + item
  76. # Encountered non-percent-encoded characters. Flush the current
  77. # pct_sequence.
  78. string += pct_sequence.decode(encoding, errors) + rest
  79. pct_sequence = b''
  80. if pct_sequence:
  81. # Flush the final pct_sequence
  82. string += pct_sequence.decode(encoding, errors)
  83. return string
  84. def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
  85. encoding='utf-8', errors='replace'):
  86. qs, _coerce_result = qs, unicode
  87. pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
  88. r = []
  89. for name_value in pairs:
  90. if not name_value and not strict_parsing:
  91. continue
  92. nv = name_value.split('=', 1)
  93. if len(nv) != 2:
  94. if strict_parsing:
  95. raise ValueError("bad query field: %r" % (name_value,))
  96. # Handle case of a control-name with no equal sign
  97. if keep_blank_values:
  98. nv.append('')
  99. else:
  100. continue
  101. if len(nv[1]) or keep_blank_values:
  102. name = nv[0].replace('+', ' ')
  103. name = compat_urllib_parse_unquote(
  104. name, encoding=encoding, errors=errors)
  105. name = _coerce_result(name)
  106. value = nv[1].replace('+', ' ')
  107. value = compat_urllib_parse_unquote(
  108. value, encoding=encoding, errors=errors)
  109. value = _coerce_result(value)
  110. r.append((name, value))
  111. return r
  112. def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
  113. encoding='utf-8', errors='replace'):
  114. parsed_result = {}
  115. pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
  116. encoding=encoding, errors=errors)
  117. for name, value in pairs:
  118. if name in parsed_result:
  119. parsed_result[name].append(value)
  120. else:
  121. parsed_result[name] = [value]
  122. return parsed_result
  123. class YouTubeVideoUrl():
  124. def _download_webpage(self, url):
  125. """ Returns a tuple (page content as string, URL handle) """
  126. try:
  127. if sslContext:
  128. urlh = urlopen(url, context = sslContext)
  129. else:
  130. urlh = urlopen(url)
  131. except URLError, e:
  132. #raise Exception(e.reason)
  133. return ""
  134. return urlh.read()
  135. def _search_regex(self, pattern, string):
  136. """
  137. Perform a regex search on the given string, using a single or a list of
  138. patterns returning the first matching group.
  139. """
  140. mobj = re.search(pattern, string, 0)
  141. if mobj:
  142. # return the first matching group
  143. return next(g for g in mobj.groups() if g is not None)
  144. else:
  145. raise Exception('Unable extract pattern from string!')
  146. def _decrypt_signature(self, s, player_url):
  147. """Turn the encrypted s field into a working signature"""
  148. if player_url is None:
  149. raise Exception('Cannot decrypt signature without player_url!')
  150. if player_url[:2] == '//':
  151. player_url = 'https:' + player_url
  152. try:
  153. func = self._extract_signature_function(player_url)
  154. return func(s)
  155. except:
  156. raise Exception('Signature extraction failed!')
  157. def _extract_signature_function(self, player_url):
  158. id_m = re.match(
  159. r'.*?-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|/base)?\.(?P<ext>[a-z]+)$',
  160. player_url)
  161. if not id_m:
  162. raise Exception('Cannot identify player %r!' % player_url)
  163. player_type = id_m.group('ext')
  164. code = self._download_webpage(player_url)
  165. if player_type == 'js':
  166. return self._parse_sig_js(code)
  167. elif player_type == 'swf':
  168. return self._parse_sig_swf(code)
  169. else:
  170. raise Exception('Invalid player type %r!' % player_type)
  171. def _parse_sig_js(self, jscode):
  172. funcname = self._search_regex(r'\.sig\|\|([a-zA-Z0-9$]+)\(', jscode)
  173. jsi = JSInterpreter(jscode)
  174. initial_function = jsi.extract_function(funcname)
  175. return lambda s: initial_function([s])
  176. def _parse_sig_swf(self, file_contents):
  177. swfi = SWFInterpreter(file_contents)
  178. TARGET_CLASSNAME = 'SignatureDecipher'
  179. searched_class = swfi.extract_class(TARGET_CLASSNAME)
  180. initial_function = swfi.extract_function(searched_class, 'decipher')
  181. return lambda s: initial_function([s])
  182. def _extract_from_m3u8(self, manifest_url):
  183. url_map = {}
  184. def _get_urls(_manifest):
  185. lines = _manifest.split('\n')
  186. urls = filter(lambda l: l and not l.startswith('#'), lines)
  187. return urls
  188. manifest = self._download_webpage(manifest_url)
  189. formats_urls = _get_urls(manifest)
  190. for format_url in formats_urls:
  191. itag = self._search_regex(r'itag/(\d+?)/', format_url)
  192. url_map[itag] = format_url
  193. return url_map
  194. def _get_ytplayer_config(self, webpage):
  195. # User data may contain arbitrary character sequences that may affect
  196. # JSON extraction with regex, e.g. when '};' is contained the second
  197. # regex won't capture the whole JSON. Yet working around by trying more
  198. # concrete regex first keeping in mind proper quoted string handling
  199. # to be implemented in future that will replace this workaround (see
  200. # https://github.com/rg3/youtube-dl/issues/7468,
  201. # https://github.com/rg3/youtube-dl/pull/7599)
  202. patterns = [
  203. r';ytplayer\.config\s*=\s*({.+?});ytplayer',
  204. r';ytplayer\.config\s*=\s*({.+?});',
  205. ]
  206. for pattern in patterns:
  207. config = self._search_regex(pattern, webpage)
  208. if config:
  209. return json.loads(uppercase_escape(config))
  210. def extract(self, video_id):
  211. url = 'https://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
  212. # Get video webpage
  213. video_webpage = self._download_webpage(url)
  214. if not video_webpage:
  215. #raise Exception('Video webpage not found!')
  216. return ""
  217. # Attempt to extract SWF player URL
  218. mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
  219. if mobj is not None:
  220. player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
  221. else:
  222. player_url = None
  223. # Get video info
  224. embed_webpage = None
  225. if re.search(r'player-age-gate-content">', video_webpage) is not None:
  226. age_gate = True
  227. # We simulate the access to the video from www.youtube.com/v/{video_id}
  228. # this can be viewed without login into Youtube
  229. url = 'https://www.youtube.com/embed/%s' % video_id
  230. embed_webpage = self._download_webpage(url)
  231. data = urlencode({
  232. 'video_id': video_id,
  233. 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
  234. 'sts': self._search_regex(r'"sts"\s*:\s*(\d+)', embed_webpage),
  235. })
  236. video_info_url = 'https://www.youtube.com/get_video_info?' + data
  237. video_info_webpage = self._download_webpage(video_info_url)
  238. video_info = compat_parse_qs(video_info_webpage)
  239. else:
  240. age_gate = False
  241. video_info = None
  242. # Try looking directly into the video webpage
  243. ytplayer_config = self._get_ytplayer_config(video_webpage)
  244. if ytplayer_config:
  245. args = ytplayer_config['args']
  246. if args.get('url_encoded_fmt_stream_map'):
  247. # Convert to the same format returned by compat_parse_qs
  248. video_info = dict((k, [v]) for k, v in args.items())
  249. if not video_info:
  250. # We also try looking in get_video_info since it may contain different dashmpd
  251. # URL that points to a DASH manifest with possibly different itag set (some itags
  252. # are missing from DASH manifest pointed by webpage's dashmpd, some - from DASH
  253. # manifest pointed by get_video_info's dashmpd).
  254. # The general idea is to take a union of itags of both DASH manifests (for example
  255. # video with such 'manifest behavior' see https://github.com/rg3/youtube-dl/issues/6093)
  256. for el_type in ['&el=info', '&el=embedded', '&el=detailpage', '&el=vevo', '']:
  257. video_info_url = (
  258. 'https://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
  259. % (video_id, el_type))
  260. video_info_webpage = self._download_webpage(video_info_url)
  261. video_info = compat_parse_qs(video_info_webpage)
  262. if 'token' in video_info:
  263. break
  264. if 'token' not in video_info:
  265. if 'reason' in video_info:
  266. print '[YouTubeVideoUrl] %s' % video_info['reason'][0]
  267. else:
  268. print '[YouTubeVideoUrl] "token" parameter not in video info for unknown reason'
  269. # Start extracting information
  270. if 'conn' in video_info and video_info['conn'][0][:4] == 'rtmp':
  271. url = video_info['conn'][0]
  272. elif len(video_info.get('url_encoded_fmt_stream_map', [''])[0]) >= 1 or \
  273. len(video_info.get('adaptive_fmts', [''])[0]) >= 1:
  274. encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + \
  275. ',' + video_info.get('adaptive_fmts', [''])[0]
  276. if 'rtmpe%3Dyes' in encoded_url_map:
  277. raise Exception('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343')
  278. # Find the best format from our format priority map
  279. encoded_url_map = encoded_url_map.split(',')
  280. url_map_str = None
  281. # If format changed in config, recreate priority list
  282. if PRIORITY_VIDEO_FORMAT[0] != maxResolution: #config.plugins.YouTube.maxResolution.value:
  283. createPriorityFormats()
  284. for our_format in PRIORITY_VIDEO_FORMAT:
  285. our_format = 'itag=' + our_format
  286. for encoded_url in encoded_url_map:
  287. if our_format in encoded_url and 'url=' in encoded_url:
  288. url_map_str = encoded_url
  289. break
  290. if url_map_str:
  291. break
  292. # If anything not found, used first in the list if it not in ignore map
  293. if not url_map_str:
  294. for encoded_url in encoded_url_map:
  295. if 'url=' in encoded_url:
  296. url_map_str = encoded_url
  297. for ignore_format in IGNORE_VIDEO_FORMAT:
  298. ignore_format = 'itag=' + ignore_format
  299. if ignore_format in encoded_url:
  300. url_map_str = None
  301. break
  302. if url_map_str:
  303. break
  304. if not url_map_str:
  305. url_map_str = encoded_url_map[0]
  306. url_data = compat_parse_qs(url_map_str)
  307. url = url_data['url'][0]
  308. if 'sig' in url_data:
  309. url += '&signature=' + url_data['sig'][0]
  310. elif 's' in url_data:
  311. encrypted_sig = url_data['s'][0]
  312. ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
  313. jsplayer_url_json = self._search_regex(ASSETS_RE,
  314. embed_webpage if age_gate else video_webpage)
  315. if not jsplayer_url_json and not age_gate:
  316. # We need the embed website after all
  317. if embed_webpage is None:
  318. embed_url = 'https://www.youtube.com/embed/%s' % video_id
  319. embed_webpage = self._download_webpage(embed_url)
  320. jsplayer_url_json = self._search_regex(ASSETS_RE, embed_webpage)
  321. player_url = json.loads(jsplayer_url_json)
  322. if player_url is None:
  323. player_url_json = self._search_regex(
  324. r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
  325. video_webpage)
  326. player_url = json.loads(player_url_json)
  327. signature = self._decrypt_signature(encrypted_sig, player_url)
  328. url += '&signature=' + signature
  329. if 'ratebypass' not in url:
  330. url += '&ratebypass=yes'
  331. elif video_info.get('hlsvp'):
  332. url = None
  333. manifest_url = video_info['hlsvp'][0]
  334. url_map = self._extract_from_m3u8(manifest_url)
  335. # Find the best format from our format priority map
  336. for our_format in PRIORITY_VIDEO_FORMAT:
  337. if url_map.get(our_format):
  338. url = url_map[our_format]
  339. break
  340. # If anything not found, used first in the list if it not in ignore map
  341. if not url:
  342. for url_map_key in url_map.keys():
  343. if url_map_key not in IGNORE_VIDEO_FORMAT:
  344. url = url_map[url_map_key]
  345. break
  346. if not url:
  347. url = url_map.values()[0]
  348. else:
  349. #raise Exception('No supported formats found in video info!')
  350. return ""
  351. return str(url)
  352. if __name__ == "__main__":
  353. #yt = YouTubeVideoUrl()
  354. if len(sys.argv)>1:
  355. video_id= sys.argv[1]
  356. else:
  357. video_id = "2rlTF6HiMGg"
  358. e = YouTubeVideoUrl().extract(video_id)
  359. print e