#
# Copyright (C) 2013 Sean Poyser
#
#
# This code is a derivative of the YouTube plugin for XBMC
# released under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3
# Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen
#
# This Program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# This Program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with XBMC; see the file COPYING. If not, write to
# the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
# http://www.gnu.org/copyleft/gpl.html
#
# 5: "240p h263 flv container",
# 18: "360p h264 mp4 container | 270 for rtmpe?",
# 22: "720p h264 mp4 container",
# 26: "???",
# 33: "???",
# 34: "360p h264 flv container",
# 35: "480p h264 flv container",
# 37: "1080p h264 mp4 container",
# 38: "720p vp8 webm container",
# 43: "360p h264 flv container",
# 44: "480p vp8 webm container",
# 45: "720p vp8 webm container",
# 46: "520p vp8 webm stereo",
# 59: "480 for rtmpe",
# 78: "seems to be around 400 for rtmpe",
# 82: "360p h264 stereo",
# 83: "240p h264 stereo",
# 84: "720p h264 stereo",
# 85: "520p h264 stereo",
# 100: "360p vp8 webm stereo",
# 101: "480p vp8 webm stereo",
# 102: "720p vp8 webm stereo",
# 120: "hd720",
# 121: "hd1080"
import re
import urllib2
import urllib
import cgi
import HTMLParser
try: import simplejson as json
except ImportError: import json
MAX_REC_DEPTH = 5
def Clean(text):
text = text.replace('–', '-')
text = text.replace('’', '\'')
text = text.replace('“', '"')
text = text.replace('”', '"')
text = text.replace(''', '\'')
text = text.replace('', '')
text = text.replace('', '')
text = text.replace('&', '&')
text = text.replace('\ufeff', '')
return text
def PlayVideo(id):
import xbmcgui
import sys
import utils
busy = utils.showBusy()
video, links = GetVideoInformation(id)
if busy:
busy.close()
if 'best' not in video:
return False
url = video['best']
title = video['title']
image = video['thumbnail']
liz = xbmcgui.ListItem(title, iconImage=image, thumbnailImage=image)
liz.setInfo( type="Video", infoLabels={ "Title": title} )
if len(sys.argv) < 2 or int(sys.argv[1]) == -1:
import xbmc
pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
pl.clear()
pl.add(url, liz)
xbmc.Player().play(pl)
else:
import xbmcplugin
liz.setPath(url)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, liz)
return True
def GetVideoInformation(id):
#id = 'H7iQ4sAf0OE' #test for HLSVP
#id = 'ofHlUJuw8Ak' #test for stereo
#id = 'ifZkeuSrNRc' #account closed
#id = 'M7FIvfx5J10'
#id = 'n-D1EB74Ckg' #vevo
#id = 'lVMWEheQ2hU' #vevo
video = {}
links = []
try: video, links = GetVideoInfo(id)
except : pass
return video, links
def GetVideoInfo(id):
url = 'http://www.youtube.com/watch?v=%s&safeSearch=none' % id
html = FetchPage(url)
video, links = Scrape(html)
video['videoid'] = id
video['thumbnail'] = "http://i.ytimg.com/vi/%s/0.jpg" % video['videoid']
video['title'] = GetVideoTitle(html)
if len(links) == 0:
if 'hlsvp' in video:
video['best'] = video['hlsvp']
else:
video['best'] = links[0][1]
return video, links
def GetVideoTitle(html):
try: return Clean(re.compile('').search(html).groups(1)[0])
except: pass
return 'YouTube Video'
def Scrape(html):
stereo = [82, 83, 84, 85, 100, 101, 102]
video = {}
links = []
flashvars = ExtractFlashVars(html)
if not flashvars.has_key(u"url_encoded_fmt_stream_map"):
return video, links
if flashvars.has_key(u"ttsurl"):
video[u"ttsurl"] = flashvars[u"ttsurl"]
if flashvars.has_key(u"hlsvp"):
video[u"hlsvp"] = flashvars[u"hlsvp"]
for url_desc in flashvars[u"url_encoded_fmt_stream_map"].split(u","):
url_desc_map = cgi.parse_qs(url_desc)
if not (url_desc_map.has_key(u"url") or url_desc_map.has_key(u"stream")):
continue
key = int(url_desc_map[u"itag"][0])
url = u""
if url_desc_map.has_key(u"url"):
url = urllib.unquote(url_desc_map[u"url"][0])
elif url_desc_map.has_key(u"conn") and url_desc_map.has_key(u"stream"):
url = urllib.unquote(url_desc_map[u"conn"][0])
if url.rfind("/") < len(url) -1:
url = url + "/"
url = url + urllib.unquote(url_desc_map[u"stream"][0])
elif url_desc_map.has_key(u"stream") and not url_desc_map.has_key(u"conn"):
url = urllib.unquote(url_desc_map[u"stream"][0])
if url_desc_map.has_key(u"sig"):
url = url + u"&signature=" + url_desc_map[u"sig"][0]
elif url_desc_map.has_key(u"s"):
sig = url_desc_map[u"s"][0]
#url = url + u"&signature=" + DecryptSignature(sig)
flashvars = ExtractFlashVars(html, assets=True)
js = flashvars[u"js"]
url += u"&signature=" + DecryptSignatureNew(sig, js)
if key not in stereo:
links.append([key, url])
#links.sort(reverse=True)
return video, links
def DecryptSignature(s):
''' use decryption solution by Youtube-DL project '''
if len(s) == 88:
return s[48] + s[81:67:-1] + s[82] + s[66:62:-1] + s[85] + s[61:48:-1] + s[67] + s[47:12:-1] + s[3] + s[11:3:-1] + s[2] + s[12]
elif len(s) == 87:
return s[62] + s[82:62:-1] + s[83] + s[61:52:-1] + s[0] + s[51:2:-1]
elif len(s) == 86:
return s[2:63] + s[82] + s[64:82] + s[63]
elif len(s) == 85:
return s[76] + s[82:76:-1] + s[83] + s[75:60:-1] + s[0] + s[59:50:-1] + s[1] + s[49:2:-1]
elif len(s) == 84:
return s[83:36:-1] + s[2] + s[35:26:-1] + s[3] + s[25:3:-1] + s[26]
elif len(s) == 83:
return s[6] + s[3:6] + s[33] + s[7:24] + s[0] + s[25:33] + s[53] + s[34:53] + s[24] + s[54:]
elif len(s) == 82:
return s[36] + s[79:67:-1] + s[81] + s[66:40:-1] + s[33] + s[39:36:-1] + s[40] + s[35] + s[0] + s[67] + s[32:0:-1] + s[34]
elif len(s) == 81:
return s[6] + s[3:6] + s[33] + s[7:24] + s[0] + s[25:33] + s[2] + s[34:53] + s[24] + s[54:81]
elif len(s) == 92:
return s[25] + s[3:25] + s[0] + s[26:42] + s[79] + s[43:79] + s[91] + s[80:83];
#else:
# print ('Unable to decrypt signature, key length %d not supported; retrying might work' % (len(s)))
def ExtractFlashVars(data, assets=False):
flashvars = {}
found = False
for line in data.split("\n"):
if line.strip().find(";ytplayer.config = ") > 0:
found = True
p1 = line.find(";ytplayer.config = ") + len(";ytplayer.config = ") - 1
p2 = line.rfind(";")
if p1 <= 0 or p2 <= 0:
continue
data = line[p1 + 1:p2]
break
data = RemoveAdditionalEndingDelimiter(data)
if found:
data = json.loads(data)
if assets:
flashvars = data['assets']
else:
flashvars = data['args']
return flashvars
def FetchPage(url):
req = urllib2.Request(url)
req.add_header('User-Agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3')
req.add_header('Referer', 'http://www.youtube.com/')
return urllib2.urlopen(req).read().decode("utf-8")
def replaceHTMLCodes(txt):
# Fix missing ; in ;
txt = re.sub("([0-9]+)([^;^0-9]+)", "\\1;\\2", txt)
txt = HTMLParser.HTMLParser().unescape(txt)
txt = txt.replace("&", "&")
return txt
def RemoveAdditionalEndingDelimiter(data):
pos = data.find("};")
if pos != -1:
data = data[:pos + 1]
return data
####################################################
global playerData
global allLocalFunNamesTab
global allLocalVarNamesTab
def _extractVarLocalFuns(match):
varName, objBody = match.groups()
output = ''
for func in objBody.split( '},' ):
output += re.sub(
r'^([^:]+):function\(([^)]*)\)',
r'function %s__\1(\2,*args)' % varName,
func
) + '\n'
return output
def _jsToPy(jsFunBody):
pythonFunBody = re.sub(r'var ([^=]+)={(.*?)}};', _extractVarLocalFuns, jsFunBody)
pythonFunBody = re.sub(r'function (\w*)\$(\w*)', r'function \1_S_\2', pythonFunBody)
pythonFunBody = pythonFunBody.replace('function', 'def').replace('{', ':\n\t').replace('}', '').replace(';', '\n\t').replace('var ', '')
pythonFunBody = pythonFunBody.replace('.reverse()', '[::-1]')
lines = pythonFunBody.split('\n')
for i in range(len(lines)):
# a.split("") -> list(a)
match = re.search('(\w+?)\.split\(""\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), 'list(' + match.group(1) + ')')
# a.length -> len(a)
match = re.search('(\w+?)\.length', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), 'len(' + match.group(1) + ')')
# a.slice(3) -> a[3:]
match = re.search('(\w+?)\.slice\((\w+?)\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), match.group(1) + ('[%s:]' % match.group(2)) )
# a.join("") -> "".join(a)
match = re.search('(\w+?)\.join\(("[^"]*?")\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), match.group(2) + '.join(' + match.group(1) + ')' )
# a.splice(b,c) -> del a[b:c]
match = re.search('(\w+?)\.splice\(([^,]+),([^)]+)\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), 'del ' + match.group(1) + '[' + match.group(2) + ':' + match.group(3) + ']' )
pythonFunBody = "\n".join(lines)
pythonFunBody = re.sub(r'(\w+)\.(\w+)\(', r'\1__\2(', pythonFunBody)
pythonFunBody = re.sub(r'([^=])(\w+)\[::-1\]', r'\1\2.reverse()', pythonFunBody)
return pythonFunBody
def _jsToPy1(jsFunBody):
pythonFunBody = jsFunBody.replace('function', 'def').replace('{', ':\n\t').replace('}', '').replace(';', '\n\t').replace('var ', '')
pythonFunBody = pythonFunBody.replace('.reverse()', '[::-1]')
lines = pythonFunBody.split('\n')
for i in range(len(lines)):
# a.split("") -> list(a)
match = re.search('(\w+?)\.split\(""\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), 'list(' + match.group(1) + ')')
# a.length -> len(a)
match = re.search('(\w+?)\.length', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), 'len(' + match.group(1) + ')')
# a.slice(3) -> a[3:]
match = re.search('(\w+?)\.slice\(([0-9]+?)\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), match.group(1) + ('[%s:]' % match.group(2)) )
# a.join("") -> "".join(a)
match = re.search('(\w+?)\.join\(("[^"]*?")\)', lines[i])
if match:
lines[i] = lines[i].replace( match.group(0), match.group(2) + '.join(' + match.group(1) + ')' )
return "\n".join(lines)
def _getLocalFunBody(funName):
# get function body
funName = funName.replace('$', '\\$')
match = re.search('(function %s\([^)]+?\){[^}]+?})' % funName, playerData)
if match:
return match.group(1)
return ''
def _getAllLocalSubFunNames(mainFunBody):
match = re.compile('[ =(,](\w+?)\([^)]*?\)').findall( mainFunBody )
if len(match):
# first item is name of main function, so omit it
funNameTab = set( match[1:] )
return funNameTab
return set()
def _extractLocalVarNames(mainFunBody):
valid_funcs = ( 'reverse', 'split', 'splice', 'slice', 'join' )
match = re.compile( r'[; =(,](\w+)\.(\w+)\(' ).findall( mainFunBody )
local_vars = []
for name in match:
if name[1] not in valid_funcs:
local_vars.append( name[0] )
return set(local_vars)
def _getLocalVarObjBody(varName):
match = re.search( r'var %s={.*?}};' % varName, playerData )
if match:
return match.group(0)
return ''
def DecryptSignatureNew(s, playerUrl):
if not playerUrl.startswith('http:'):
playerUrl = 'http:' + playerUrl
#print "Decrypt_signature sign_len[%d] playerUrl[%s]" % (len(s), playerUrl)
global allLocalFunNamesTab
global allLocalVarNamesTab
global playerData
allLocalFunNamesTab = []
allLocalVarNamesTab = []
playerData = ''
request = urllib2.Request(playerUrl)
#res = core._fetchPage({u"link": playerUrl})
#playerData = res["content"]
try:
playerData = urllib2.urlopen(request).read()
playerData = playerData.decode('utf-8', 'ignore')
except Exception, e:
#print str(e)
print 'Failed to decode playerData'
return ''
# get main function name
match = re.search("signature=([$a-zA-Z]+)\([^)]\)", playerData)
if match:
mainFunName = match.group(1)
else:
print('Failed to get main signature function name')
return ''
_mainFunName = mainFunName.replace('$','_S_')
fullAlgoCode = _getfullAlgoCode(mainFunName)
# wrap all local algo function into one function extractedSignatureAlgo()
algoLines = fullAlgoCode.split('\n')
for i in range(len(algoLines)):
algoLines[i] = '\t' + algoLines[i]
fullAlgoCode = 'def extractedSignatureAlgo(param):'
fullAlgoCode += '\n'.join(algoLines)
fullAlgoCode += '\n\treturn %s(param)' % _mainFunName
fullAlgoCode += '\noutSignature = extractedSignatureAlgo( inSignature )\n'
# after this function we should have all needed code in fullAlgoCode
#print '---------------------------------------'
#print '| ALGO FOR SIGNATURE DECRYPTION |'
#print '---------------------------------------'
#print fullAlgoCode
#print '---------------------------------------'
try:
algoCodeObj = compile(fullAlgoCode, '', 'exec')
except:
print 'Failed to obtain decryptSignature code'
return ''
# for security allow only flew python global function in algo code
vGlobals = {"__builtins__": None, 'len': len, 'list': list}
# local variable to pass encrypted sign and get decrypted sign
vLocals = { 'inSignature': s, 'outSignature': '' }
# execute prepared code
try:
exec(algoCodeObj, vGlobals, vLocals)
except:
print 'decryptSignature code failed to exceute correctly'
return ''
#print 'Decrypted signature = [%s]' % vLocals['outSignature']
return vLocals['outSignature']
# Note, this method is using a recursion
def _getfullAlgoCode(mainFunName, recDepth=0):
global playerData
global allLocalFunNamesTab
global allLocalVarNamesTab
if MAX_REC_DEPTH <= recDepth:
print '_getfullAlgoCode: Maximum recursion depth exceeded'
return
funBody = _getLocalFunBody(mainFunName)
if funBody != '':
funNames = _getAllLocalSubFunNames(funBody)
if len(funNames):
for funName in funNames:
funName_ = funName.replace('$','_S_')
if funName not in allLocalFunNamesTab:
funBody=funBody.replace(funName,funName_)
allLocalFunNamesTab.append(funName)
#print 'Add local function %s to known functions' % mainFunName
funbody = _getfullAlgoCode(funName, recDepth+1) + "\n" + funBody
varNames = _extractLocalVarNames(funBody)
if len(varNames):
for varName in varNames:
if varName not in allLocalVarNamesTab:
allLocalVarNamesTab.append(varName)
funBody = _getLocalVarObjBody(varName) + "\n" + funBody
# convert code from javascript to python
funBody = _jsToPy(funBody)
return '\n' + funBody + '\n'
return funBody