Kodi plugin to to play various online streams (mostly Latvian)

YouTubeVideoUrl.py 13KB

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