diff --git a/addon.py b/addon.py index f33b139..429fa50 100644 --- a/addon.py +++ b/addon.py @@ -2,13 +2,15 @@ # default.py import xbmcgui, xbmcaddon, xbmc -import json, sys, urllib, urllib2, gzip, StringIO, re, os, time, threading, socket, base64, math, cookielib +import json, sys, urllib, urlparse, os, time, threading, socket from video_concatenate import video_concatenate try: import StorageServer except: import storageserverdummy as StorageServer +import requests + __addonid__ = "plugin.video.youkutv" __addon__ = xbmcaddon.Addon(id=__addonid__) __cwd__ = __addon__.getAddonInfo('path') @@ -26,6 +28,10 @@ HOST = 'http://tv.api.3g.youku.com/' IDS = 'pid=0ce22bfd5ef5d2c5&guid=12d60728bd267e3e0b6ab4d124d6c5f0&ngdid=357e71ee78debf7340d29408b88c85c4&ver=2.6.0&operator=T-Mobile_310260&network=WIFI&launcher=0' +headers = {'Referer': 'http://v.youku.com', + 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36', +} + #Fixed tabs Navigation = ['首页', '频道', '排行'] ContentID = [520, 560, 580] @@ -41,22 +47,23 @@ mainData = [{'title': '搜索', 'image': 'yk_search.jpg', 'mtype': 'search'}, {'title': '观看记录', 'image': 'yk_history.jpg', 'mtype': 'history'}, {'title': '收藏', 'image': 'yk_favor.jpg', 'mtype': 'favor'}] -settings_data = {'resolution':[u'1080P', u'超清', u'高清', u'标清', u'标清(3GP)'], - 'resolution_type':[['hd3','mp4hd3'], ['hd2','mp4hd2'], ['mp4','mp4hd'], ['flv','flvhd'], ['3gphd']], +settings_data = {'resolution':[u'1080P', u'超清', u'高清', u'标清'], + 'resolution_type':[['mp4hd3', 'mp4hd3v2'], ['mp4hd2', 'mp4hd2v2'], ['mp4hd', '3gphd'], ['mp4sd', 'flvhd']], 'language':[u'默认', u'国语', u'粤语', u'英语'], 'language_code':[u'', u'guoyu', u'yue', u'yingyu'], 'play':['整合(试验阶段)', '分段', '堆叠'], 'play_type':['concatenate', 'list', 'stack']} settings={'resolution':0, 'language':0, 'play':0} -resolution_map = {'3gphd': '3gp', - 'flv': 'flv', - 'flvhd': 'flv', - 'mp4': 'mp4', - 'mp4hd': 'mp4', - 'hd2': 'flv', - 'mp4hd2': 'flv', - 'hd3': 'flv', - 'mp4hd3': 'flv'} +resolution_map = { + "3gphd": "mp4", + "mp4hd2": "hd2", + "mp4hd3": "hd3", + "flvhd": "flv", + "mp4hd3v2": "hd3", + "mp4hd2v2": "hd2", + "mp4sd": "flv", + "mp4hd": "mp4" +} ACTION_MOVE_LEFT = 1 @@ -1869,92 +1876,6 @@ def getSetting(self,key,default=None): return setting == 'true' return setting -class youkuDecoder: - def __init__( self ): - return - - def getFileIDMixString(self,seed): - mixed = [] - source = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\:._-1234567890") - seed = float(seed) - for i in range(len(source)): - seed = (seed * 211 + 30031 ) % 65536 - index = math.floor(seed /65536 *len(source)) - mixed.append(source[int(index)]) - source.remove(source[int(index)]) - return mixed - - def getFileId(self,fileId,seed): - mixed = self.getFileIDMixString(seed) - ids = fileId.split('*') - realId = [] - for i in range(0,len(ids)-1): - realId.append(mixed[int(ids[i])]) - return ''.join(realId) - - def trans_e(self, a, c): - b = range(256) - f = 0 - result = '' - h = 0 - while h < 256: - f = (f + b[h] + ord(a[h % len(a)])) % 256 - b[h], b[f] = b[f], b[h] - h += 1 - q = f = h = 0 - while q < len(c): - h = (h + 1) % 256 - f = (f + b[h]) % 256 - b[h], b[f] = b[f], b[h] - if isinstance(c[q], int): - result += chr(c[q] ^ b[(b[h] + b[f]) % 256]) - else: - result += chr(ord(c[q]) ^ b[(b[h] + b[f]) % 256]) - q += 1 - return result - - def trans_f(self, a, c): - """ - :argument a: list - :param c: - :return: - """ - b = [] - for f in range(len(a)): - i = ord(a[f][0]) - 97 if "a" <= a[f] <= "z" else int(a[f]) + 26 - e = 0 - while e < 36: - if c[e] == i: - i = e - break - e += 1 - v = i - 26 if i > 25 else chr(i + 97) - b.append(str(v)) - return ''.join(b) - - f_code_1 = 'becaf9be' - f_code_2 = 'bf7e5f01' - - def _calc_ep(self, sid, fileId, token): - ep = self.trans_e(self.f_code_2, '%s_%s_%s' % (sid, fileId, token)) - return base64.b64encode(ep) - - def _calc_ep2(self, vid, ep): - e_code = self.trans_e(self.f_code_1, base64.b64decode(ep)) - sid, token = e_code.split('_') - new_ep = self.trans_e(self.f_code_2, '%s_%s_%s' % (sid, vid, token)) - return base64.b64encode(new_ep), token, sid - - def get_sid(self, ep): - e_code = self.trans_e(self.f_code_1, base64.b64decode(ep)) - return e_code.split('_') - - def generate_ep(self, no, fileid, sid, token): - ep = urllib.quote(self._calc_ep(sid, fileid, token).encode('latin1'), - safe="~()*!.'" - ) - return ep - def getNumber(data, k): try: s = data[k] @@ -2005,10 +1926,48 @@ def getProperty(item, key): return value +def fetch_cna(): + url = 'http://log.mmstat.com/eg.js' + req = requests.get(url) + cna = req.headers['Set-Cookie'].split(';')[0].split('=')[1] + if '%' not in cna: + cna = requests.compat.quote(cna) + return cna + +def youku_ups(vid, ccode='0502'): + url = 'http://ups.youku.com/ups/get.json?vid={}&ccode={}'.format(vid, ccode) + url += '&client_ip=192.168.1.1' + url += '&utid=' + fetch_cna() + url += '&client_ts=' + str(int(time.time())) + # Found in http://g.alicdn.com/player/ykplayer/0.5.28/youku-player.min.js + # grep -oE '"[0-9a-zA-Z+/=]{256}"' youku-player.min.js + ckey = 'DIl58SLFxFNndSV1GFNnMQVYkx1PP5tKe1siZu/86PR1u/Wh1Ptd+WOZsHHWxysSfAOhNJpdVWsdVJNsfJ8Sxd8WKVvNfAS8aS8fAOzYARzPyPc3JvtnPHjTdKfESTdnuTW6ZPvk2pNDh4uFzotgdMEFkzQ5wZVXl2Pf1/Y6hLK0OnCNxBj3+nb0v72gZ6b0td+WOZsHHWxysSo/0y9D2K42SaB8Y/+aD2K42SaB8Y/+ahU+WOZsHcrxysooUeND' + url += '&ckey=' + requests.compat.quote(ckey) + time.sleep(3) + return requests.get(url, headers=headers).json() + +def change_cdn(url, dispatcher_url='vali.cp31.ott.cibntv.net'): + # if the cnd_url starts with an ip addr, it should be youku's old CDN + # which rejects http requests randomly with status code > 400 + # change it to the dispatcher of aliCDN can do better + # at least a little more recoverable from HTTP 403 + if dispatcher_url in url: + return url + elif 'k.youku.com' in url: + return url + else: + url_seg_list = list(urlparse.urlparse(url)) + url_seg_list[1] = dispatcher_url + return urlparse.urlunparse(url_seg_list) + +def add_headers(url): + # http://kodi.wiki/view/HTTP + param = '&'.join(['%s=%s' % (k, headers[k]) for k in headers]) + return '%s|%s' % (url, param) + def play(vid, playContinue=False): readSettings() - playid = vid try: ret = eval(cache.get('history')) @@ -2040,42 +1999,30 @@ def play(vid, playContinue=False): xbmc.executebuiltin("ActivateWindow(busydialog)") try: - movinfo = json.loads(GetHttpData('http://play.youku.com/play/get.json?vid=%s&ct=12' % playid).replace('\r\n','')) - movdat = movinfo['data'] - movinfo = json.loads(GetHttpData('http://play.youku.com/play/get.json?vid=%s&ct=10' % playid).replace('\r\n','')) - movdat1 = movinfo['data'] + movdat = youku_ups(vid)['data'] assert 'stream' in movdat - assert 'stream' in movdat1 - except: + except Exception, e: xbmc.executebuiltin( "Dialog.Close(busydialog)" ) - xbmcgui.Dialog().ok('提示框', '解析地址异常,无法播放') + xbmcgui.Dialog().ok('提示框', '解析地址异常,无法播放。\nNo stream.') return #Select resolution. stream = {} - resolution = '' - language_code = settings_data['language_code'][settings['language']] - try: - for i in range(settings['resolution'], len(settings_data['resolution'])): - for t in settings_data['resolution_type'][i]: - for s in movdat1['stream'][::-1]: - if settings['language'] == 0 or language_code == s['audio_lang'] or s['audio_lang'] == 'default': - if t == s['stream_type']: - stream = s - resolution = settings_data['resolution_type'][i][0] - break - if stream.has_key('stream_type'): - break - if stream.has_key('stream_type'): + # resolution from higher to lower + for i in range(settings['resolution'], len(settings_data['resolution'])): + if stream: + break + for t in settings_data['resolution_type'][i]: + if stream: break - except: - xbmc.executebuiltin( "Dialog.Close(busydialog)" ) - xbmcgui.Dialog().ok('提示框', '解析地址异常,无法播放') - return + for s in movdat['stream'][::-1]: + if t == s['stream_type']: + stream = s + break - if not stream.has_key('stream_type'): + if not stream: xbmc.executebuiltin( "Dialog.Close(busydialog)" ) - xbmcgui.Dialog().ok('提示框', '解析地址异常,无法播放') + xbmcgui.Dialog().ok('提示框', '解析地址异常,无法播放。\nStream type not matched.') return @@ -2084,34 +2031,19 @@ def play(vid, playContinue=False): urls = [] segs = stream['segs'] - oip = movdat['security']['ip'] - ep = movdat['security']['encrypt_string'] - sid, token = youkuDecoder().get_sid(ep) - for no in range(len(segs)): k = segs[no]['key'] assert k != -1 - fileid = stream['segs'][no]['fileid'] - ep = youkuDecoder().generate_ep(no, fileid, sid, token) - q = urllib.urlencode(dict( - ctype = 12, - ev = 1, - K = k, - ep = urllib.unquote(ep), - oip = oip, - token = token, - yxon = 1 - )) - u = 'http://k.youku.com/player/getFlvPath/sid/{sid}_00/st/{container}/fileid/{fileid}?{q}'.format( - sid = sid, - container = resolution_map[resolution], - fileid = fileid, - q = q - ) - urls += [i['server'] for i in json.loads(GetHttpData(u))] + url = segs[no]["cdn_url"] + url = change_cdn(url) + url = add_headers(url) + urls.append(url) playlist = xbmc.PlayList(1) playlist.clear() + title = movdat['video']['title'] + listitem=xbmcgui.ListItem(title) + listitem.setInfo(type="Video",infoLabels={"Title":title}) if settings_data['play_type'][settings['play']] == 'concatenate' and resolution_map[resolution] == 'flv': vc.start(urls) @@ -2122,9 +2054,9 @@ def play(vid, playContinue=False): playlist.add('http://127.0.0.1:%d' % port, listitem) elif settings_data['play_type'][settings['play']] == 'list': for i in range(len(urls)): - title =movdat['video']['title'] + u" - 第"+str(i+1)+"/"+str(len(urls)) + u"节" - listitem=xbmcgui.ListItem(title) - listitem.setInfo(type="Video",infoLabels={"Title":title}) + _title = u"%s - 第 %s/%s 节" % (title, i+1, len(urls)) + listitem = xbmcgui.ListItem(_title) + listitem.setInfo(type="Video", infoLabels={"Title": _title}) playlist.add(urls[i], listitem) else: playurl = 'stack://' + ' , '.join(urls) @@ -2198,25 +2130,7 @@ def openWindow(window_name,session=None,**kwargs): def GetHttpData(url): log('Frech: ' + url) try: - req = urllib2.Request(url) - req.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64) {0}{1}'. - format('AppleWebKit/537.36 (KHTML, like Gecko) ', - 'Chrome/28.0.1500.71 Safari/537.36')) - req.add_header('Accept-encoding', 'gzip') - if (url.find('play.youku.com') != -1): - req.add_header('referer', 'http://static.youku.com') - response = urllib2.urlopen(req) - httpdata = response.read() - if response.headers.get('content-encoding', None) == 'gzip': - httpdata = gzip.GzipFile(fileobj=StringIO.StringIO(httpdata)).read() - response.close() - match = re.compile('encodingt=(.+?)"').findall(httpdata) - if len(match)<=0: - match = re.compile('meta charset="(.+?)"').findall(httpdata) - if len(match)>0: - charset = match[0].lower() - if (charset != 'utf-8') and (charset != 'utf8'): - httpdata = unicode(httpdata, charset).encode('utf8') + httpdata = requests.get(url, headers=headers).content except: if xbmcgui.Dialog().yesno('错误', '网络超时,是否继续?'): return GetHttpData(url) @@ -2279,8 +2193,4 @@ def clearFavor(): openWindow('mysettings') except: if __name__ == '__main__': - cj = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) - opener.addheaders = [('Cookie', '__ysuid={0}'.format(time.time()))] - urllib2.install_opener(opener) openWindow('main') diff --git a/addon.xml b/addon.xml index 18ffe00..4277824 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,8 @@  - + + video