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("&amp;", "&")
-                newurl = newurl[newurl.find("&#39;") + 5:newurl.rfind("&#39;")]
+                newurl = newurl.replace("0; url=&#39;", "")
                 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("&amp;", "&"), 
"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("&amp;", "&"), 
"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

Reply via email to