
#
#  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('&#8211;', '-')
    text = text.replace('&#8217;', '\'')
    text = text.replace('&#8220;', '"')
    text = text.replace('&#8221;', '"')
    text = text.replace('&#39;',   '\'')
    text = text.replace('<b>',     '')
    text = text.replace('</b>',    '')
    text = text.replace('&amp;',   '&')
    text = text.replace('\ufeff', '')
    return text


def PlayVideo(id):
    import xbmcgui
    import sys

    video, links = GetVideoInformation(id)

    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} )
    liz.setProperty("IsPlayable","true")

    if 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('<meta name="title" content="(.+?)">').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 &#<number>;
    txt = re.sub("(&#[0-9]+)([^;^0-9]+)", "\\1;\\2", txt)

    txt = HTMLParser.HTMLParser().unescape(txt)
    txt = txt.replace("&amp;", "&")
    return txt


def RemoveAdditionalEndingDelimiter(data):
    pos = data.find("};")
    if pos != -1:
        data = data[:pos + 1]
    return data
    
####################################################

global fullAlgoCode
global allLocalFunNamesTab
global playerData

def _jsToPy(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
    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 DecryptSignatureNew(s, playerUrl):
    if not playerUrl.startswith('http:'):
        playerUrl = 'http:' + playerUrl
        
    #print "Decrypt_signature sign_len[%d] playerUrl[%s]" % (len(s), playerUrl)

    global fullAlgoCode
    global allLocalFunNamesTab
    global playerData
    fullAlgoCode        = ''
    allLocalFunNamesTab = []
    playerData          = ''

    request = urllib2.Request(playerUrl)
    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=(\w+?)\([^)]\)", playerData)
    if match:
        mainFunName = match.group(1)
        #print('Main signature function name = "%s"' % mainFunName)
    else: 
        print('Failed to get main signature function name')
        return ''

    _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 fullAlgoCode
    
    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:
                if funName not in allLocalFunNamesTab:
                    allLocalFunNamesTab.append(funName)
                    #print 'Add local function %s to known functions' % mainFunName
                    _getfullAlgoCode(funName, recDepth+1)

        # convert code from javascript to python 
        funBody = _jsToPy(funBody)
        fullAlgoCode += '\n' + funBody + '\n'
