Python module (submodule repositary), which provides content (video streams) from various online stream sources to corresponding Enigma2, Kodi, Plex plugins

youtuberesolver.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. # -*- coding: UTF-8 -*-
  2. import urllib2
  3. # source from https://github.com/rg3/youtube-dl/issues/1208
  4. # removed some unnecessary debug messages..
  5. class CVevoSignAlgoExtractor:
  6. # MAX RECURSION Depth for security
  7. MAX_REC_DEPTH = 5
  8. def __init__(self):
  9. self.algoCache = {}
  10. self._cleanTmpVariables()
  11. def _cleanTmpVariables(self):
  12. self.fullAlgoCode = ''
  13. self.allLocalFunNamesTab = []
  14. self.playerData = ''
  15. def _jsToPy(self, jsFunBody):
  16. pythonFunBody = jsFunBody.replace('function', 'def').replace('{', ':\n\t').replace('}', '').replace(';', '\n\t').replace('var ', '')
  17. pythonFunBody = pythonFunBody.replace('.reverse()', '[::-1]')
  18. lines = pythonFunBody.split('\n')
  19. for i in range(len(lines)):
  20. # a.split("") -> list(a)
  21. match = re.search('(\w+?)\.split\(""\)', lines[i])
  22. if match:
  23. lines[i] = lines[i].replace(match.group(0), 'list(' + match.group(1) + ')')
  24. # a.length -> len(a)
  25. match = re.search('(\w+?)\.length', lines[i])
  26. if match:
  27. lines[i] = lines[i].replace(match.group(0), 'len(' + match.group(1) + ')')
  28. # a.slice(3) -> a[3:]
  29. match = re.search('(\w+?)\.slice\(([0-9]+?)\)', lines[i])
  30. if match:
  31. lines[i] = lines[i].replace(match.group(0), match.group(1) + ('[%s:]' % match.group(2)))
  32. # a.join("") -> "".join(a)
  33. match = re.search('(\w+?)\.join\(("[^"]*?")\)', lines[i])
  34. if match:
  35. lines[i] = lines[i].replace(match.group(0), match.group(2) + '.join(' + match.group(1) + ')')
  36. return "\n".join(lines)
  37. def _getLocalFunBody(self, funName):
  38. # get function body
  39. match = re.search('(function %s\([^)]+?\){[^}]+?})' % funName, self.playerData)
  40. if match:
  41. # return jsFunBody
  42. return match.group(1)
  43. return ''
  44. def _getAllLocalSubFunNames(self, mainFunBody):
  45. match = re.compile('[ =(,](\w+?)\([^)]*?\)').findall(mainFunBody)
  46. if len(match):
  47. # first item is name of main function, so omit it
  48. funNameTab = set(match[1:])
  49. return funNameTab
  50. return set()
  51. def decryptSignature(self, s, playerUrl):
  52. playerUrl = playerUrl[:4] != 'http' and 'http:' + playerUrl or playerUrl
  53. util.debug("decrypt_signature sign_len[%d] playerUrl[%s]" % (len(s), playerUrl))
  54. # clear local data
  55. self._cleanTmpVariables()
  56. # use algoCache
  57. if playerUrl not in self.algoCache:
  58. # get player HTML 5 sript
  59. request = urllib2.Request(playerUrl)
  60. try:
  61. self.playerData = urllib2.urlopen(request).read()
  62. self.playerData = self.playerData.decode('utf-8', 'ignore')
  63. except:
  64. util.debug('Unable to download playerUrl webpage')
  65. return ''
  66. # get main function name
  67. match = re.search("signature=(\w+?)\([^)]\)", self.playerData)
  68. if match:
  69. mainFunName = match.group(1)
  70. util.debug('Main signature function name = "%s"' % mainFunName)
  71. else:
  72. util.debug('Can not get main signature function name')
  73. return ''
  74. self._getfullAlgoCode(mainFunName)
  75. # wrap all local algo function into one function extractedSignatureAlgo()
  76. algoLines = self.fullAlgoCode.split('\n')
  77. for i in range(len(algoLines)):
  78. algoLines[i] = '\t' + algoLines[i]
  79. self.fullAlgoCode = 'def extractedSignatureAlgo(param):'
  80. self.fullAlgoCode += '\n'.join(algoLines)
  81. self.fullAlgoCode += '\n\treturn %s(param)' % mainFunName
  82. self.fullAlgoCode += '\noutSignature = extractedSignatureAlgo( inSignature )\n'
  83. # after this function we should have all needed code in self.fullAlgoCode
  84. try:
  85. algoCodeObj = compile(self.fullAlgoCode, '', 'exec')
  86. except:
  87. util.debug('decryptSignature compile algo code EXCEPTION')
  88. return ''
  89. else:
  90. # get algoCodeObj from algoCache
  91. util.debug('Algo taken from cache')
  92. algoCodeObj = self.algoCache[playerUrl]
  93. # for security alow only flew python global function in algo code
  94. vGlobals = {"__builtins__": None, 'len': len, 'list': list}
  95. # local variable to pass encrypted sign and get decrypted sign
  96. vLocals = { 'inSignature': s, 'outSignature': '' }
  97. # execute prepared code
  98. try:
  99. exec(algoCodeObj, vGlobals, vLocals)
  100. except:
  101. util.debug('decryptSignature exec code EXCEPTION')
  102. return ''
  103. util.debug('Decrypted signature = [%s]' % vLocals['outSignature'])
  104. # if algo seems ok and not in cache, add it to cache
  105. if playerUrl not in self.algoCache and '' != vLocals['outSignature']:
  106. util.debug('Algo from player [%s] added to cache' % playerUrl)
  107. self.algoCache[playerUrl] = algoCodeObj
  108. # free not needed data
  109. self._cleanTmpVariables()
  110. return vLocals['outSignature']
  111. # Note, this method is using a recursion
  112. def _getfullAlgoCode(self, mainFunName, recDepth=0):
  113. if self.MAX_REC_DEPTH <= recDepth:
  114. util.debug('_getfullAlgoCode: Maximum recursion depth exceeded')
  115. return
  116. funBody = self._getLocalFunBody(mainFunName)
  117. if '' != funBody:
  118. funNames = self._getAllLocalSubFunNames(funBody)
  119. if len(funNames):
  120. for funName in funNames:
  121. if funName not in self.allLocalFunNamesTab:
  122. self.allLocalFunNamesTab.append(funName)
  123. util.debug("Add local function %s to known functions" % mainFunName)
  124. self._getfullAlgoCode(funName, recDepth + 1)
  125. # conver code from javascript to python
  126. funBody = self._jsToPy(funBody)
  127. self.fullAlgoCode += '\n' + funBody + '\n'
  128. return
  129. decryptor = CVevoSignAlgoExtractor()
  130. '''
  131. YouTube plugin for XBMC
  132. Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen
  133. This program is free software: you can redistribute it and/or modify
  134. it under the terms of the GNU General Public License as published by
  135. the Free Software Foundation, either version 3 of the License, or
  136. (at your option) any later version.
  137. This program is distributed in the hope that it will be useful,
  138. but WITHOUT ANY WARRANTY; without even the implied warranty of
  139. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  140. GNU General Public License for more details.
  141. You should have received a copy of the GNU General Public License
  142. along with this program. If not, see <http://www.gnu.org/licenses/>.
  143. '''
  144. import sys
  145. import urllib
  146. import cgi
  147. import simplejson as json
  148. class YoutubePlayer(object):
  149. fmt_value = {
  150. 5: "240p",
  151. 18: "360p",
  152. 22: "720p",
  153. 26: "???",
  154. 33: "???",
  155. 34: "360p",
  156. 35: "480p",
  157. 37: "1080p",
  158. 38: "720p",
  159. 43: "360p",
  160. 44: "480p",
  161. 45: "720p",
  162. 46: "520p",
  163. 59: "480",
  164. 78: "400",
  165. 82: "360p",
  166. 83: "240p",
  167. 84: "720p",
  168. 85: "520p",
  169. 100: "360p",
  170. 101: "480p",
  171. 102: "720p",
  172. 120: "hd720",
  173. 121: "hd1080"
  174. }
  175. # YouTube Playback Feeds
  176. urls = {}
  177. urls['video_stream'] = "http://www.youtube.com/watch?v=%s&safeSearch=none"
  178. urls['embed_stream'] = "http://www.youtube.com/get_video_info?video_id=%s"
  179. urls['video_info'] = "http://gdata.youtube.com/feeds/api/videos/%s"
  180. def __init__(self):
  181. pass
  182. def removeAdditionalEndingDelimiter(self, data):
  183. pos = data.find("};")
  184. if pos != -1:
  185. data = data[:pos + 1]
  186. return data
  187. def extractFlashVars(self, data, assets):
  188. flashvars = {}
  189. found = False
  190. for line in data.split("\n"):
  191. if line.strip().find(";ytplayer.config = ") > 0:
  192. found = True
  193. p1 = line.find(";ytplayer.config = ") + len(";ytplayer.config = ") - 1
  194. p2 = line.rfind(";")
  195. if p1 <= 0 or p2 <= 0:
  196. continue
  197. data = line[p1 + 1:p2]
  198. break
  199. data = self.removeAdditionalEndingDelimiter(data)
  200. if found:
  201. data = json.loads(data)
  202. if assets:
  203. flashvars = data["assets"]
  204. else:
  205. flashvars = data["args"]
  206. return flashvars
  207. def scrapeWebPageForVideoLinks(self, result, video):
  208. links = {}
  209. flashvars = self.extractFlashVars(result, 0)
  210. if not flashvars.has_key(u"url_encoded_fmt_stream_map"):
  211. return links
  212. if flashvars.has_key(u"ttsurl"):
  213. video[u"ttsurl"] = flashvars[u"ttsurl"]
  214. if flashvars.has_key("title"):
  215. video["title"] = flashvars["title"]
  216. for url_desc in flashvars[u"url_encoded_fmt_stream_map"].split(u","):
  217. url_desc_map = cgi.parse_qs(url_desc)
  218. if not (url_desc_map.has_key(u"url") or url_desc_map.has_key(u"stream")):
  219. continue
  220. key = int(url_desc_map[u"itag"][0])
  221. url = u""
  222. if url_desc_map.has_key(u"url"):
  223. url = urllib.unquote(url_desc_map[u"url"][0])
  224. elif url_desc_map.has_key(u"conn") and url_desc_map.has_key(u"stream"):
  225. url = urllib.unquote(url_desc_map[u"conn"][0])
  226. if url.rfind("/") < len(url) - 1:
  227. url = url + "/"
  228. url = url + urllib.unquote(url_desc_map[u"stream"][0])
  229. elif url_desc_map.has_key(u"stream") and not url_desc_map.has_key(u"conn"):
  230. url = urllib.unquote(url_desc_map[u"stream"][0])
  231. if url_desc_map.has_key(u"sig"):
  232. url = url + u"&signature=" + url_desc_map[u"sig"][0]
  233. elif url_desc_map.has_key(u"s"):
  234. sig = url_desc_map[u"s"][0]
  235. flashvars = self.extractFlashVars(result, 1)
  236. js = flashvars[u"js"]
  237. url = url + u"&signature=" + self.decrypt_signature(sig, js)
  238. links[key] = url
  239. return links
  240. def decrypt_signature(self, s, js):
  241. return decryptor.decryptSignature(s, js)
  242. def extractVideoLinksFromYoutube(self, url, videoid, video):
  243. result = util.request(self.urls[u"video_stream"] % videoid)
  244. links = self.scrapeWebPageForVideoLinks(result, video)
  245. if len(links) == 0:
  246. util.error(u"Couldn't find video url- or stream-map.")
  247. return links
  248. # /*
  249. # * Copyright (C) 2011 Libor Zoubek
  250. # *
  251. # *
  252. # * This Program is free software; you can redistribute it and/or modify
  253. # * it under the terms of the GNU General Public License as published by
  254. # * the Free Software Foundation; either version 2, or (at your option)
  255. # * any later version.
  256. # *
  257. # * This Program is distributed in the hope that it will be useful,
  258. # * but WITHOUT ANY WARRANTY; without even the implied warranty of
  259. # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  260. # * GNU General Public License for more details.
  261. # *
  262. # * You should have received a copy of the GNU General Public License
  263. # * along with this program; see the file COPYING. If not, write to
  264. # * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
  265. # * http://www.gnu.org/copyleft/gpl.html
  266. # *
  267. # */
  268. import re, util, urllib
  269. __name__ = 'youtube'
  270. def supports(url):
  271. return not _regex(url) == None
  272. def resolve(url):
  273. m = _regex(url)
  274. if not m == None:
  275. player = YoutubePlayer()
  276. video = {'title':'žádný název'}
  277. index = url.find('&') # strip out everytihing after &
  278. if index > 0:
  279. url = url[:index]
  280. links = player.extractVideoLinksFromYoutube(url, m.group('id'), video)
  281. resolved = []
  282. for q in links:
  283. if q in player.fmt_value.keys():
  284. quality = player.fmt_value[q]
  285. item = {}
  286. item['name'] = __name__
  287. item['url'] = links[q]
  288. item['quality'] = quality
  289. item['surl'] = url
  290. item['subs'] = ''
  291. item['title'] = video['title']
  292. item['fmt'] = q
  293. resolved.append(item)
  294. return resolved
  295. def _regex(url):
  296. return re.search('www\.youtube\.com/(watch\?v=|v/|embed/)(?P<id>.+?)(\?|$|&)', url, re.IGNORECASE | re.DOTALL)