The branch, frodo has been updated
via 7472c9fc4966936ba62369ed21e64d3335f71a5e (commit)
via c752025cd3de245ebc288441af2b084f8ef09323 (commit)
from df00167cea8f9d94d9801262a74de1b176e34d01 (commit)
- Log -----------------------------------------------------------------
http://xbmc.git.sourceforge.net/git/gitweb.cgi?p=xbmc/plugins;a=commit;h=7472c9fc4966936ba62369ed21e64d3335f71a5e
commit 7472c9fc4966936ba62369ed21e64d3335f71a5e
Author: Martijn Kaijser <[email protected]>
Date: Sat May 24 11:57:43 2014 +0200
[plugin.video.youtube] 4.4.7
diff --git a/plugin.video.youtube/YouTubeCore.py
b/plugin.video.youtube/YouTubeCore.py
index d955e49..b1b3758 100644
--- a/plugin.video.youtube/YouTubeCore.py
+++ b/plugin.video.youtube/YouTubeCore.py
@@ -139,7 +139,7 @@ class YouTubeCore():
def del_playlist(self, params={}):
self.common.log("")
get = params.get
- url = "http://gdata.youtube.com/feeds/api/users/default/playlists/%s"
% (get("playlist"))
+ url =
u"http://gdata.youtube.com/feeds/api/users/default/playlists/{0}".format(get("playlist"))
result = self._fetchPage({"link": url, "api": "true", "login": "true",
"auth": "true", "method": "DELETE"})
return (result["content"], result["status"])
@@ -220,7 +220,7 @@ class YouTubeCore():
if title.find(": ") > 0:
title = title[title.find(": ") + 2:]
title = self.common.replaceHTMLCodes(title)
-
+
folder['Title'] = title
for tmp in self.common.parseDOM(node, "published"):
folder['published'] = tmp
@@ -397,7 +397,13 @@ class YouTubeCore():
return ret_obj
if get("url_data"):
- request = urllib2.Request(link, urllib.urlencode(get("url_data")))
+ urldata = get("url_data")
+ url_data = {}
+
+ for key in urldata:
+ url_data[key.encode('UTF-8')] = urldata[key].encode('UTF-8')
+
+ request = urllib2.Request(link, urllib.urlencode(url_data))
request.add_header('Content-Type',
'application/x-www-form-urlencoded')
elif get("request", "false") == "false":
if get("proxy"):
@@ -429,7 +435,7 @@ class YouTubeCore():
else:
request.add_header('User-Agent', self.common.USERAGENT)
- if get("no-language-cookie", "false") == "false" and False:
+ if get("no-language-cookie", "false") == "true":
cookie += "PREF=f1=50000000&hl=en; "
if get("login", "false") == "true":
@@ -455,6 +461,7 @@ class YouTubeCore():
if cookie:
self.common.log("Setting cookie: " + cookie)
+ request.add_header('Cookie', cookie)
con = urllib2.urlopen(request)
diff --git a/plugin.video.youtube/YouTubeLogin.py
b/plugin.video.youtube/YouTubeLogin.py
index b046485..3a5d0aa 100644
--- a/plugin.video.youtube/YouTubeLogin.py
+++ b/plugin.video.youtube/YouTubeLogin.py
@@ -167,7 +167,7 @@ class YouTubeLogin():
self.common.log("Use saved cookies")
return (self.settings.getSetting("cookies_saved"), 200)
- fetch_options = {"link": get("link", "http://www.youtube.com/")}
+ fetch_options = {"link": get("link", "http://www.youtube.com/"),
"no-language-cookie": "true"}
step = 0
galx = ""
@@ -188,7 +188,7 @@ class YouTubeLogin():
fetch_options = False
# Check if we are logged in.
- nick = self.common.parseDOM(ret["content"], "p", attrs={"class":
"masthead-expanded-acct-sw-id2"})
+ nick = self.common.parseDOM(ret["content"], "span", attrs={"id":
"yt-masthead-user-displayname"})
# Check if there are any errors to report
errors = self.core._findErrors(ret, silent=True)
@@ -224,40 +224,41 @@ class YouTubeLogin():
newurl = self.common.parseDOM(ret["content"], "meta",
attrs={"http-equiv": "refresh"}, ret="content")
if len(newurl) > 0:
newurl = newurl[0].replace("&", "&")
- newurl = newurl[newurl.find("'") + 5:newurl.rfind("'")]
+ newurl = newurl.replace("0; url='", "")
fetch_options = {"link": newurl, "referer": ret["location"]}
self.common.log("Part C: " + repr(fetch_options))
continue
## 2-factor login start
- if ret["content"].find("smsUserPin") > -1:
- url_data = self._fillUserPin(ret["content"])
- if len(url_data) == 0:
- return (False, 500)
+ #if ret["content"].find("smsUserPin") > -1:
+ # url_data = self._fillUserPin(ret["content"])
+ # if len(url_data) == 0:
+ # return (False, 500)
- new_part = self.common.parseDOM(ret["content"], "form",
attrs={"name": "verifyForm"}, ret="action")
- fetch_options = {"link": new_part[0].replace("&", "&"),
"url_data": url_data, "referer": ret["location"]}
+ # self.common.log("RETURNED CONTENT" + ret["content"])
+ # new_part = self.common.parseDOM(ret["content"], "input",
attrs={"name": "continue"}, ret="value")
+ # fetch_options = {"link": new_part[0].replace("&", "&"),
"url_data": url_data, "referer": ret["location"]}
- self.common.log("Part D: " + repr(fetch_options))
- continue
+ # self.common.log("Part D: " + repr(fetch_options))
+ # continue
- smsToken = self.common.parseDOM(ret["content"].replace("\n", ""),
"input", attrs={"name": "smsToken"}, ret="value")
+ #smsToken = self.common.parseDOM(ret["content"].replace("\n", ""),
"input", attrs={"name": "smsToken"}, ret="value")
- if len(smsToken) > 0 and galx != "":
- url_data = {"smsToken": smsToken[0],
- "PersistentCookie": "yes",
- "service": "youtube",
- "GALX": galx}
+ #if len(smsToken) > 0 and galx != "":
+ # url_data = {"smsToken": smsToken[0],
+ # "PersistentCookie": "yes",
+ # "service": "youtube",
+ # "GALX": galx}
- target_url = self.common.parseDOM(ret["content"], "form",
attrs={"name": "hiddenpost"}, ret="action")
- fetch_options = {"link": target_url[0], "url_data": url_data,
"referer": ret["location"]}
- self.common.log("Part E: " + repr(fetch_options))
- continue
+ # target_url = self.common.parseDOM(ret["content"], "form",
attrs={"name": "hiddenpost"}, ret="action")
+ # fetch_options = {"link": target_url[0], "url_data": url_data,
"referer": ret["location"]}
+ # self.common.log("Part E: " + repr(fetch_options))
+ # continue
## 2-factor login finish
- if not fetch_options:
+ #if not fetch_options:
# Check for errors.
- return (self.core._findErrors(ret), 303)
+ # return (self.core._findErrors(ret), 303)
return (ret, 500)
@@ -282,7 +283,7 @@ class YouTubeLogin():
def _fillUserPin(self, content):
self.common.log("")
- form = self.common.parseDOM(content, "form", attrs={"name":
"verifyForm"}, ret=True)
+ form = self.common.parseDOM(content, "form", attrs={"id":
"gaia_secondfactorform"}, ret=True)
url_data = {}
for name in self.common.parseDOM(form, "input", ret="name"):
diff --git a/plugin.video.youtube/YouTubePlayer.py
b/plugin.video.youtube/YouTubePlayer.py
index d49364c..cae01dd 100644
--- a/plugin.video.youtube/YouTubePlayer.py
+++ b/plugin.video.youtube/YouTubePlayer.py
@@ -22,6 +22,8 @@ import cgi
try: import simplejson as json
except ImportError: import json
+import urllib2, re
+
class YouTubePlayer():
fmt_value = {
5: "240p h263 flv container",
@@ -50,6 +52,9 @@ class YouTubePlayer():
121: "hd1080"
}
+ # MAX RECURSION Depth for security
+ MAX_REC_DEPTH = 5
+
# YouTube Playback Feeds
urls = {}
urls['video_stream'] = "http://www.youtube.com/watch?v=%s&safeSearch=none"
@@ -72,6 +77,9 @@ class YouTubePlayer():
self.core = sys.modules["__main__"].core
self.login = sys.modules["__main__"].login
self.subtitles = sys.modules["__main__"].subtitles
+
+ self.algoCache = {}
+ self._cleanTmpVariables()
def playVideo(self, params={}):
self.common.log(repr(params), 3)
@@ -304,7 +312,12 @@ class YouTubePlayer():
data = data[:pos + 1]
return data
- def extractFlashVars(self, data):
+ def normalizeUrl(self, url):
+ if url[0:2] == "//":
+ url = "http:" + url
+ return url
+
+ def extractFlashVars(self, data, assets):
flashvars = {}
found = False
@@ -321,7 +334,15 @@ class YouTubePlayer():
if found:
data = json.loads(data)
- flashvars = data["args"]
+ if assets:
+ flashvars = data["assets"]
+ else:
+ flashvars = data["args"]
+
+ for k in ["html", "css", "js"]:
+ if k in flashvars:
+ flashvars[k] = self.normalizeUrl(flashvars[k])
+
self.common.log("Step2: " + repr(data))
self.common.log(u"flashvars: " + repr(flashvars), 2)
@@ -331,7 +352,7 @@ class YouTubePlayer():
self.common.log(u"")
links = {}
- flashvars = self.extractFlashVars(result[u"content"])
+ flashvars = self.extractFlashVars(result[u"content"], 0)
if not flashvars.has_key(u"url_encoded_fmt_stream_map"):
return links
@@ -363,34 +384,167 @@ class YouTubePlayer():
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=" + self.decrypt_signature(sig)
+ flashvars = self.extractFlashVars(result[u"content"], 1)
+ js = flashvars[u"js"]
+ url = url + u"&signature=" + self.decrypt_signature(sig, js)
links[key] = url
return links
- def decrypt_signature(self, 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];
+ @staticmethod
+ def printDBG(s):
+ print(s)
+
+ def _cleanTmpVariables(self):
+ self.fullAlgoCode = ''
+ self.allLocalFunNamesTab = []
+ self.playerData = ''
+
+ def _jsToPy(self, jsFunBody):
+ pythonFunBody = re.sub(r'function (\w*)\$(\w*)', r'function \1_S_\2',
jsFunBody)
+ 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\(([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(self, funName):
+ # get function body
+ funName=funName.replace('$', '\\$')
+ match = re.search('(function %s\([^)]+?\){[^}]+?})' % funName,
self.playerData)
+ if match:
+ # return jsFunBody
+ return match.group(1)
+ return ''
+
+ def _getAllLocalSubFunNames(self, 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 decrypt_signature(self, s, playerUrl):
+ self.printDBG("decrypt_signature sign_len[%d] playerUrl[%s]" %
(len(s), playerUrl) )
+
+ # clear local data
+ self._cleanTmpVariables()
+
+ # use algoCache
+ if playerUrl not in self.algoCache:
+ # get player HTML 5 sript
+ request = urllib2.Request(playerUrl)
+ try:
+ self.playerData = urllib2.urlopen(request).read()
+ self.playerData = self.playerData.decode('utf-8', 'ignore')
+ except Exception as ex:
+ self.printDBG("Error: " + str(sys.exc_info()[0]) + " - " +
str(ex))
+ self.printDBG('Unable to download playerUrl webpage')
+ return ''
+
+ # get main function name
+ match = re.search("signature=(\w+?)\([^)]\)", self.playerData)
+ if match:
+ mainFunName = match.group(1)
+ self.printDBG('Main signature function name = "%s"' %
mainFunName)
+ else:
+ self.printDBG('Can not get main signature function name')
+ return ''
+
+ self._getfullAlgoCode( mainFunName )
+
+ # wrap all local algo function into one function
extractedSignatureAlgo()
+ algoLines = self.fullAlgoCode.split('\n')
+ for i in range(len(algoLines)):
+ algoLines[i] = '\t' + algoLines[i]
+ self.fullAlgoCode = 'def extractedSignatureAlgo(param):'
+ self.fullAlgoCode += '\n'.join(algoLines)
+ self.fullAlgoCode += '\n\treturn %s(param)' % mainFunName
+ self.fullAlgoCode += '\noutSignature = extractedSignatureAlgo(
inSignature )\n'
+
+ # after this function we should have all needed code in
self.fullAlgoCode
+
+ self.printDBG( "---------------------------------------" )
+ self.printDBG( "| ALGO FOR SIGNATURE DECRYPTION |" )
+ self.printDBG( "---------------------------------------" )
+ self.printDBG( self.fullAlgoCode )
+ self.printDBG( "---------------------------------------" )
+
+ try:
+ algoCodeObj = compile(self.fullAlgoCode, '', 'exec')
+ except:
+ self.printDBG('decryptSignature compile algo code EXCEPTION')
+ return ''
else:
- self.common.log(u'Unable to decrypt signature, key length %d not
supported; retrying might work' % (len(s)))
+ # get algoCodeObj from algoCache
+ self.printDBG('Algo taken from cache')
+ algoCodeObj = self.algoCache[playerUrl]
+
+ # for security alow 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:
+ self.printDBG('decryptSignature exec code EXCEPTION')
+ return ''
+
+ self.printDBG('Decrypted signature = [%s]' % vLocals['outSignature'])
+ # if algo seems ok and not in cache, add it to cache
+ if playerUrl not in self.algoCache and '' != vLocals['outSignature']:
+ self.printDBG('Algo from player [%s] added to cache' % playerUrl)
+ self.algoCache[playerUrl] = algoCodeObj
+
+ # free not needed data
+ self._cleanTmpVariables()
+
+ return vLocals['outSignature']
+
+ # Note, this method is using a recursion
+ def _getfullAlgoCode( self, mainFunName, recDepth = 0 ):
+ if self.MAX_REC_DEPTH <= recDepth:
+ self.printDBG('_getfullAlgoCode: Maximum recursion depth exceeded')
+ return
+
+ funBody = self._getLocalFunBody( mainFunName )
+ if '' != funBody:
+ funNames = self._getAllLocalSubFunNames(funBody)
+ if len(funNames):
+ for funName in funNames:
+ funName_=funName.replace('$','_S_')
+ if funName not in self.allLocalFunNamesTab:
+ funBody=funBody.replace(funName,funName_)
+ self.allLocalFunNamesTab.append(funName)
+ self.printDBG("Add local function %s to known
functions" % mainFunName)
+ self._getfullAlgoCode( funName, recDepth + 1 )
+
+ # conver code from javascript to python
+ funBody = self._jsToPy(funBody)
+ self.fullAlgoCode += '\n' + funBody + '\n'
+ return
def getVideoPageFromYoutube(self, get):
login = "false"
diff --git a/plugin.video.youtube/addon.xml b/plugin.video.youtube/addon.xml
index 036332a..31c157a 100644
--- a/plugin.video.youtube/addon.xml
+++ b/plugin.video.youtube/addon.xml
@@ -1,5 +1,5 @@
-<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
-<addon id="plugin.video.youtube" name="YouTube" provider-name="TheCollective"
version="4.4.6">
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<addon id="plugin.video.youtube" name="YouTube" provider-name="TheCollective,
Skipmode A1" version="4.4.7">
<requires>
<import addon="xbmc.python" version="2.1.0" />
<import addon="script.module.simplejson" version="2.0.10" />
@@ -117,7 +117,12 @@ Raadpleeg de lokale wetgeving voordat u deze plugin
installeert.</disclaimer>
<disclaimer lang="sv">Någon del av detta tillägg kanske är olagligt i
ditt land - kontrollera dina lokala lagar innan installation.</disclaimer>
<disclaimer
lang="th">à¸à¸²à¸à¸ªà¹à¸§à¸à¸à¸²à¸à¸ªà¹à¸§à¸à¸à¸¢à¸²à¸¢à¸à¸µà¹à¸à¸²à¸à¹à¸¡à¹à¸à¸¹à¸à¸à¹à¸à¸à¸à¸²à¸¡à¸à¸à¸«à¸¡à¸²à¸¢à¹à¸à¸à¸£à¸°à¹à¸à¸¨à¸à¸à¸à¸à¸¸à¸
à¸à¸£à¸¸à¸à¸²à¸à¸£à¸§à¸à¸ªà¸à¸à¸à¸à¸«à¸¡à¸²à¸¢à¹à¸à¸à¸£à¸°à¹à¸à¸¨à¸à¸à¸à¸à¸¸à¸à¸à¹à¸à¸à¸à¸³à¸à¸²à¸£à¸à¸´à¸à¸à¸±à¹à¸</disclaimer>
<disclaimer lang="zh">è¿ä¸ªæä»¶çæäºå
容å¯è½ä¸ç¬¦åä½
æå¨å½å®¶çæ³å¾è§å® - 请å¨å¨å®è£
å确认符åå½å°æ³å¾ã</disclaimer>
+ <language></language>
<platform>all</platform>
- <language />
+ <license>GNU GENERAL PUBLIC LICENSE. Version 2, June 1991</license>
+ <forum>http://forum.xbmc.org/showthread.php?tid=79487</forum>
+ <website>http://www.youtube.com</website>
+ <email></email>
+ <source>git://github.com/skipmodea1/plugin.video.youtube.git</source>
</extension>
</addon>
\ No newline at end of file
diff --git a/plugin.video.youtube/changelog.txt
b/plugin.video.youtube/changelog.txt
index fb0e41f..7a97afe 100644
--- a/plugin.video.youtube/changelog.txt
+++ b/plugin.video.youtube/changelog.txt
@@ -1,14 +1,13 @@
-[B]TODO:[/B]
-- Fix RTMP support.
-- UTF8/16 does not work consistently(Verify failures against minidom
implementation)
-- Embed playback fallback
+[B]Known Errors[/B]
+- Google's two-step verification is NOT working at the moment for this addon:
until it gets fixed, either create a second google account for youtube viewing
in xbmc or turn 2-step off.
-[B]Errata[/B]
-- [XBMC] Thumbnails sometimes turns into black box or Folder (XBMC not
detecting when thumbnail is set and defaulting to icon?)
-- [XBMC] When sorting items, it's impossible to get them to return to their
original order
-- [XBMC] Has Excessive Memory use after running the plugin for prolonged
periods of time
-- [RTMPDUMP] Doesn't support handshake type 10 which is required by youtube.
-- Youtube implemented encryption of signatures, without knowing the proper
decryption method this VEVO content will remain unstable
+[B]Version 4.4.7[/B]
+This version contains the sources by sogopot
(http://forum.xbmc.org/showthread.php?tid=79487&page=271) and jded
(http://forum.xbmc.org/showthread.php?tid=79487&page=276), thanks both :). I
(Skipmode A1) pushed it to the officials XMBC repo so it will install for
everybody.
+- Fixed: To solve the login problems, I added the fix from this post: Reply
#52 (http://code.google.com/p/youtubexbmc/issues/detail?id=115#c52). If you
continue to have login problems, you may have to find a different fix.
+- Fixed: To fix a problem with VEVO videos not playing, as well as certain
videos added to favorites not playing, I added the YouTubePlayer.py from this
post: Reply #217
(http://code.google.com/p/youtubexbmc/issues/detail?id=95#c217).
+- Fixed: To fix a problem with LIVE streams not playing, I compared an addon
sent to from member RedPenguin, and found a duplicated entry at line 362 & 363
and replaced with the necessary code in YouTubePlayer.py.
+- Fixed: Fix Login Problem with non English languages.
+- Fixed: When you are in a non English speaking country, youtube returns the
pages by default in your language. The plugin sometimes gave an error in this
situation.
[B]Version 3.4.6[/B]
- Fixed: Age verification working again due
@@ -372,3 +371,17 @@
- Cleanup of listFeedFolder
- Cleanup of listUserFolder
- Added Viewed and Downloaded overlay to video items.
+
+Not current anymore:
+[B]TODO:[/B]
+- Fix RTMP support.
+- UTF8/16 does not work consistently(Verify failures against minidom
implementation)
+- Embed playback fallback
+
+Not current anymore:
+[B]Errata[/B]
+- [XBMC] Thumbnails sometimes turns into black box or Folder (XBMC not
detecting when thumbnail is set and defaulting to icon?)
+- [XBMC] When sorting items, it's impossible to get them to return to their
original order
+- [XBMC] Has Excessive Memory use after running the plugin for prolonged
periods of time
+- [RTMPDUMP] Doesn't support handshake type 10 which is required by youtube.
+- Youtube implemented encryption of signatures, without knowing the proper
decryption method this VEVO content will remain unstable
diff --git a/plugin.video.youtube/default.py b/plugin.video.youtube/default.py
index 1082caa..dcfb5d0 100644
--- a/plugin.video.youtube/default.py
+++ b/plugin.video.youtube/default.py
@@ -30,9 +30,9 @@ except ImportError:
import xbmcvfsdummy as xbmcvfs
# plugin constants
-version = "4.4.6"
+version = "4.4.7"
plugin = "YouTube-" + version
-author = "TheCollective"
+author = "TheCollective, Skipmode A1"
url = "www.xbmc.com"
# xbmc hooks
http://xbmc.git.sourceforge.net/git/gitweb.cgi?p=xbmc/plugins;a=commit;h=c752025cd3de245ebc288441af2b084f8ef09323
commit c752025cd3de245ebc288441af2b084f8ef09323
Author: Martijn Kaijser <[email protected]>
Date: Sat May 24 11:55:14 2014 +0200
[plugin.video.twitch] 1.1.1
diff --git a/plugin.video.twitch/README.md b/plugin.video.twitch/README.md
index 91f5744..623ffd0 100644
--- a/plugin.video.twitch/README.md
+++ b/plugin.video.twitch/README.md
@@ -8,7 +8,13 @@ FAQ
* I can't find the Twitch.tv add-on in the xbmc add-on manager!
-> Make sure you are using at least XBMC 12 Frodo.
+> Make sure you are using at least XBMC 12 Frodo / XBMC 13 Gotham.
+
+* I'm having issues with the playback of streams (buffering, dropping,
stuttering).
+
+> This Addon does not handle any aspect of the playback of Twitch streams
(that would be the XBMC Video Player), it simply tells XBMC what to play.
+> The Addon does however provide Quality Options which may help if your
internet connection / computer specs are below requirements for HD streams.
+
What's next?
----------------
@@ -24,4 +30,4 @@ Credit where credit is due.
Thanks to all the people who contributed to this project:
-ccaspers, CDehning, Giacom, grocal, KlingOne, kokarn, Kr0nZ, MrSprigster,
stuross
+ccaspers, CDehning, Giacom, grocal, KlingOne, kokarn, Kr0nZ, Liquex,
MrSprigster, stuross
diff --git a/plugin.video.twitch/addon.xml b/plugin.video.twitch/addon.xml
index 7db5bb9..5f98a08 100644
--- a/plugin.video.twitch/addon.xml
+++ b/plugin.video.twitch/addon.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
-<addon id='plugin.video.twitch' version='1.0.8' name='TwitchTV'
provider-name='StateOfTheArt and ccaspers'>
+<addon id='plugin.video.twitch' version='1.1.1' name='TwitchTV'
provider-name='StateOfTheArt and ccaspers'>
<requires>
<import addon='xbmc.python' version='2.1.0'/>
<import addon='script.module.simplejson' version='2.0.10'/>
@@ -17,6 +17,8 @@
<description lang='de'>Schaue die besten Gaming-Streams auf
XBMC!</description>
<summary lang='en'>TwitchTV video plugin</summary>
<description lang='en'>Watch your favorite gaming streams on
XBMC!</description>
+ <summary lang='nl'>TwitchTV video plugin</summary>
+ <description lang='nl'>Bekijk je favoriete gaming-streams op
XBMC!</description>
<summary lang='pl'>TwitchTV video plugin</summary>
<description lang='pl'>OglÄ
daj ulubione programy TwitchTV na
XBMC!</description>
</extension>
diff --git a/plugin.video.twitch/changelog.txt
b/plugin.video.twitch/changelog.txt
index 078c388..e013df3 100644
--- a/plugin.video.twitch/changelog.txt
+++ b/plugin.video.twitch/changelog.txt
@@ -58,3 +58,11 @@ Added ability to play archived videos (past broadcasts) -
thx to Kr0nZ
- rearranged string ids
1.0.7
- fixed bug in quality settings
+1.0.8
+- version increment to force update on official xbmc repository
+1.0.9
+- added detection of restricted qualities (addon will now load best quality
available if preferred quality is restricted) - MrSprigster
+1.1.0
+- added 'thumbnail' argument to enable support for third party skins + changed
games list icon source for improved icons - Liquex
+1.1.1
+- various bug fixes, code improvements
diff --git a/plugin.video.twitch/converter.py b/plugin.video.twitch/converter.py
index 71c9f6b..c4f41ed 100644
--- a/plugin.video.twitch/converter.py
+++ b/plugin.video.twitch/converter.py
@@ -10,11 +10,12 @@ class JsonListItemConverter(object):
def convertGameToListItem(self, game):
name = game[Keys.NAME].encode('utf-8')
- image = game[Keys.LOGO].get(Keys.LARGE, '')
+ image = game[Keys.BOX].get(Keys.LARGE, '')
return {'label': name,
'path': self.plugin.url_for('createListForGame',
gameName=name, index='0'),
- 'icon': image
+ 'icon': image,
+ 'thumbnail': image
}
def convertTeamToListItem(self, team):
@@ -22,7 +23,8 @@ class JsonListItemConverter(object):
return {'label': name,
'path': self.plugin.url_for(endpoint='createListOfTeamStreams',
team=name),
- 'icon': team.get(Keys.LOGO, '')
+ 'icon': team.get(Keys.LOGO, ''),
+ 'thumbnail': team.get(Keys.LOGO, '')
}
def convertTeamChannelToListItem(self, teamChannel):
@@ -38,14 +40,17 @@ class JsonListItemConverter(object):
return {'label': title,
'path': self.plugin.url_for(endpoint='playLive',
name=channelname),
'is_playable': True,
- 'icon': image}
+ 'icon': image,
+ 'thumbnail': image
+ }
def convertFollowersToListItem(self, follower):
videobanner = follower.get(Keys.LOGO, '')
return {'label': follower[Keys.DISPLAY_NAME],
'path': self.plugin.url_for(endpoint='channelVideos',
name=follower[Keys.NAME]),
- 'icon': videobanner
+ 'icon': videobanner,
+ 'thumbnail': videobanner
}
def convertVideoListToListItem(self,video):
@@ -53,7 +58,8 @@ class JsonListItemConverter(object):
'path': self.plugin.url_for(endpoint='playVideo',
id=video['_id']),
'is_playable': True,
- 'icon': video.get(Keys.PREVIEW, '')
+ 'icon': video.get(Keys.PREVIEW, ''),
+ 'thumbnail': video.get(Keys.PREVIEW, '')
}
def convertStreamToListItem(self, stream):
@@ -64,7 +70,8 @@ class JsonListItemConverter(object):
'path': self.plugin.url_for(endpoint='playLive',
name=channel[Keys.NAME]),
'is_playable': True,
- 'icon': videobanner if videobanner else logo
+ 'icon': videobanner if videobanner else logo,
+ 'thumbnail': videobanner if videobanner else logo
}
def getTitleForStream(self, stream):
diff --git a/plugin.video.twitch/twitch.py b/plugin.video.twitch/twitch.py
index 6bb3947..633f955 100644
--- a/plugin.video.twitch/twitch.py
+++ b/plugin.video.twitch/twitch.py
@@ -1,7 +1,13 @@
#-*- encoding: utf-8 -*-
-import urllib2, sys
-from urllib import quote_plus
-import re, xbmcgui, xbmc
+import xbmcgui, xbmc
+import sys
+try:
+ from urllib.request import urlopen, Request
+ from urllib.parse import quote_plus
+except ImportError:
+ from urllib import quote_plus
+ from urllib2 import Request, urlopen
+
try:
import json
except:
@@ -19,14 +25,34 @@ class JSONScraper(object):
object.__init__(self)
self.logger = logger
+ '''
+ Download Data from an url and returns it as a String
+ @param url Url to download from (e.g. http://www.google.com)
+ @param headers currently unused, backwards compability
+ @returns String of data from URL
+ '''
def downloadWebData(self, url, headers=None):
- req = urllib2.Request(url)
- req.add_header(Keys.USER_AGENT, USER_AGENT)
- response = urllib2.urlopen(req)
- data = response.read()
- response.close()
+ data = ""
+ try:
+ req = Request(url)
+ req.add_header(Keys.USER_AGENT, USER_AGENT)
+ response = urlopen(req)
+
+ if sys.version_info < (3, 0):
+ data = response.read()
+ else:
+ data = response.readall().decode('utf-8')
+ response.close()
+ except:
+ raise TwitchException(TwitchException.HTTP_ERROR)
return data
-
+
+ '''
+ Download Data from an url and returns it as JSON
+ @param url Url to download from
+ @param headers currently unused, backwards compability
+ @returns JSON Object with data from URL
+ '''
def getJson(self, url, headers=None):
try:
jsonString = self.downloadWebData(url, headers)
@@ -95,12 +121,13 @@ class TwitchTV(object):
url = Urls.VIDEO_INFO.format(id)
return self._fetchItems(url, 'title')
+
def getVideoChunksPlaylist(self, id):
vidChunks = self.getVideoChunks(id)
chunks = vidChunks['chunks']['live']
title = self.getVideoTitle(id)
itemTitle = '%s - Part {0} of %s' % (title, len(chunks))
-
+
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
@@ -113,7 +140,7 @@ class TwitchTV(object):
playlist.add(chunk['url'],
xbmcgui.ListItem(itemTitle.format(curN), thumbnailImage=vidChunks['preview']))
return playlist
-
+
def getFollowingChannelNames(self, username):
quotedUsername = quote_plus(username)
url = Urls.FOLLOWED_CHANNELS.format(quotedUsername)
@@ -186,25 +213,41 @@ class TwitchVideoResolver(object):
#Split Into Multiple Lines
streamurls = data.split('\n')
#Initialize Custom Playlist Var
- playlist='#EXTM3U\n'
-
+ playlist=''
+
#Define Qualities
quality = 'Source,High,Medium,Low'
quality = quality.split(',')
-
+
+ #Initialize Var
+ unrestrictedqualities = ''
+ #Loop Through Multiple Quality Stream Playlist and Remove Any
Restricted Qualities
+ for line in range(0, (len(streamurls))):
+ if 'EXT-X-TWITCH-RESTRICTED' not in streamurls[line]:
+ unrestrictedqualities += streamurls[line] + '\n'
+
+ streamurls = unrestrictedqualities.split('\n')
+
+ self.logger.info('search for quality: ' + quality[maxQuality])
+
#Check to see if our preferred quality is available (not all
qualities are available for none partnered streams)
- if quality[maxQuality] in data:
+ if quality[maxQuality] in unrestrictedqualities:
#Preferred quality is available
#Loop Through Multiple Quality Stream Playlist Until We Find
Our Preferred Quality
- for line in range(0, (len(streamurls)-1)):
+ for line in range(0, (len(streamurls))):
if quality[maxQuality] in streamurls[line]:
+ #Add Playlist Header
+ playlist = '#EXTM3U\n'
#Add 3 Quality Specific Applicable Lines From Multiple
Quality Stream Playlist To Our Custom Playlist Var
- playlist = playlist + streamurls[line] + '\n' +
streamurls[(line + 1)] + '\n' + streamurls[(line + 2)]
- print(playlist)
+ playlist += streamurls[line] + '\n' + streamurls[(line
+ 1)] + '\n' + streamurls[(line + 2)]
+ #URL was not found where we were expecting one (rare
Twitch API bug?), lets use the raw playlist provided by the Twitch API (ignores
quality preference)
+ if 'http' not in playlist:
+ playlist = '#EXTM3U\n\n'.join(streamurls)
+ self.logger.info("URL error occurred (rare Twitch
API bug?), using raw playlist from Twitch API (ignoring quality preference)")
else:
- #Preferred quality is unavailable so let's play the raw
playlist we got from twitch (contains only 'source')
- playlist = data
- print(playlist)
+ #Preferred quality is unavailable so let's play the highest
available quality
+ playlist += '\n'.join(streamurls)
+ self.logger.info("prefered quality unavailable, using highest
available quality")
#Write Custom Playlist
text_file = open(fileName, "w")
@@ -214,12 +257,13 @@ class TwitchVideoResolver(object):
else:
raise TwitchException(TwitchException.STREAM_OFFLINE)
+
def _getSwfUrl(self, channelName):
url = Urls.TWITCH_SWF + channelName
headers = {Keys.USER_AGENT: USER_AGENT,
Keys.REFERER: Urls.TWITCH_TV + channelName}
- req = urllib2.Request(url, None, headers)
- response = urllib2.urlopen(req)
+ req = Request(url, None, headers)
+ response = urlopen(req)
return response.geturl()
def _streamIsAccessible(self, stream):
@@ -280,6 +324,7 @@ class Keys(object):
FOLLOWS = 'follows'
GAME = 'game'
LOGO = 'logo'
+ BOX = 'box'
LARGE = 'large'
NAME = 'name'
NEEDED_INFO = 'needed_info'
-----------------------------------------------------------------------
Summary of changes:
plugin.video.twitch/README.md | 10 +-
plugin.video.twitch/addon.xml | 4 +-
plugin.video.twitch/changelog.txt | 8 +
plugin.video.twitch/converter.py | 21 ++-
.../resources/language/Dutch/strings.xml | 51 +++++
plugin.video.twitch/twitch.py | 91 +++++++---
plugin.video.youtube/YouTubeCore.py | 15 +-
plugin.video.youtube/YouTubeLogin.py | 49 +++---
plugin.video.youtube/YouTubePlayer.py | 204 +++++++++++++++++---
plugin.video.youtube/addon.xml | 11 +-
plugin.video.youtube/changelog.txt | 33 +++-
plugin.video.youtube/default.py | 4 +-
12 files changed, 400 insertions(+), 101 deletions(-)
create mode 100644 plugin.video.twitch/resources/language/Dutch/strings.xml
hooks/post-receive
--
Plugins
------------------------------------------------------------------------------
"Accelerate Dev Cycles with Automated Cross-Browser Testing - For FREE
Instantly run your Selenium tests across 300+ browser/OS combos.
Get unparalleled scalability from the best Selenium testing platform available
Simple to use. Nothing to install. Get started now for free."
http://p.sf.net/sfu/SauceLabs
_______________________________________________
Xbmc-addons mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/xbmc-addons