• 1
  • 105
  • 106
  • 107(current)
  • 108
  • 109
  • 128
[SUPPORT] Hulu Video Plugin
(2014-03-17, 00:25)russelldub Wrote: OK. More work and I've got a full stream solution for edgecast. stream_hulu.py pasted in below. Short solution for those that understand these things : swfUrl changed for swfVerification (so turn swfvfy=true back on and use SWFPlayer = 'http://www.hulu.com/site-player/205970/player.swf?cb=205970'). Otherwise locate your stream_hulu.py and replace it with the below code.

stream_hulu.py is in your add-on directory which depends on your OS. Check google.

Code:
import xbmc
import xbmcgui
import xbmcplugin

import common
import ads
import subtitles
import sys
import binascii
import base64
import os
import hmac
import operator
import time
import urllib
import re
import md5
from array import array

from BeautifulSoup import BeautifulStoneSoup

try:
    from xml.etree import ElementTree
except:
    from elementtree import ElementTree


smildeckeys = [ common.xmldeckeys[9] ]

class Main:
    def __init__( self ):
        if 'http://' in common.args.url:
            video_id=self.getIDS4HTTP(common.args.url)
            self.queue=True
            httpplay=True
        else:
            self.queue=False
            httpplay=False
            video_id=common.args.url
        admodule = ads.Main()
        common.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
        if common.args.mode.endswith('TV_play'):
            if os.path.isfile(common.ADCACHE):
                os.remove(common.ADCACHE)
            self.NoResolve=False
            self.GUID = common.makeGUID()
            
            if common.args.mode.startswith('Captions'):
                common.settings['enable_captions']='true'
                common.settings['segmentvideos'] = 'false'
                self.NoResolve=True
            elif common.args.mode.startswith('NoCaptions'):
                common.settings['enable_captions']='false'
                common.settings['segmentvideos'] = 'false'
                self.NoResolve=True
            elif common.args.mode.startswith('Select'):
                common.settings['quality']='0'
                common.settings['segmentvideos'] = 'false'
                self.NoResolve=True
                
            if ((common.settings['segmentvideos'] == 'true') or
                (common.settings['networkpreroll'] == 'true') or
                (common.settings['prerollads'] > 0) or
                (common.settings['trailads'] > 0)):
                    common.playlist.clear()
            
            # POST VIEW
            if common.settings['enable_login']=='true' and common.settings['usertoken']:
                common.viewed(common.args.videoid)
                
            if not self.NoResolve:
                if (common.settings['networkpreroll'] == 'true'):
                    self.NetworkPreroll()
                addcount = admodule.PreRoll(video_id,self.GUID,self.queue)
                if addcount > 0:
                    self.queue=True
            else:
                addcount = 0
            if common.settings['segmentvideos'] == 'true':
                segments = self.playSegment(video_id)
                if segments:
                    adbreaks = common.settings['adbreaks']
                    for i in range(1,len(segments)+1):
                        admodule.queueAD(video_id,adbreaks+addcount,addcount)
                        addcount += adbreaks
                        self.queueVideoSegment(video_id,segment=i)
            else:
                self.play(video_id)
            admodule.Trailing(addcount,video_id,self.GUID)
            
            if common.settings['queueremove']=='true' and common.settings['enable_login']=='true' and common.settings['usertoken']:
                self.queueViewComplete()
            
            if httpplay:
                xbmc.Player().play(common.playlist)

        elif common.args.mode == 'SEGMENT_play':
            self.queue=False
            self.NoResolve=False
            self.GUID = common.args.guid
            self.playSegment(video_id,segment=int(common.args.segment))
        elif common.args.mode == 'AD_play':
            self.NoResolve=False
            self.GUID = common.args.guid
            pod = int(common.args.pod)
            admodule.playAD(video_id,pod,self.GUID)
        elif common.args.mode == 'SUBTITLE_play':
            subtitles.Main().SetSubtitles(video_id)
            
    def getIDS4HTTP(self, url):
        pagedata=common.getFEED(url)
        common.args.videoid = url.split('watch/')[1].split('/')[0]
        content_id = re.compile('so.addVariable\("content_id", (.*?)\);').findall(pagedata)[0].strip()
        common.args.eid = self.cid2eid(content_id)
        return content_id

    def cid2eid(self, content_id):
        m = md5.new()
        m.update(str(content_id) + "MAZxpK3WwazfARjIpSXKQ9cmg9nPe5wIOOfKuBIfz7bNdat6gQKHj69ZWNWNVB1")
        value = m.digest()
        return base64.encodestring(value).replace("+", "-").replace("/", "_").replace("=", "").replace('\n','')
              
    def getSMIL(self, video_id,retry=0):
        epoch = int(time.mktime(time.gmtime()))
        parameters = {'video_id'  : video_id,
                      'v'         : '888324234',
                      'ts'        : str(epoch),
                      'np'        : '1',
                      'vp'        : '1',
                      'enable_fa' : '1',
                      'device_id' : self.GUID,
                      'pp'        : 'Desktop',
                      'dp_id'     : 'Hulu',
                      'region'    : 'US',
                      'ep'        : '1',
                      'language'  : 'en'
                      }
        if retry > 0:
            parameters['retry']=str(retry)
        if common.settings['enable_login']=='true' and common.settings['enable_plus']=='true' and common.settings['usertoken']:
            parameters['token'] = common.settings['usertoken']
        smilURL = False
        for item1, item2 in parameters.iteritems():
            if not smilURL:
                smilURL = 'http://s.hulu.com/select?'+item1+'='+item2
            else:
                smilURL += '&'+item1+'='+item2
        smilURL += '&bcs='+self.content_sig(parameters)
        print 'HULU --> SMILURL: ' + smilURL
        if common.settings['proxy_enable'] == 'true':
            proxy=True
        else:
            proxy=False
        smilXML=common.getFEED(smilURL,proxy=proxy)
        if smilXML:
            smilXML=self.decrypt_SMIL(smilXML)
            print "GOT SMIL"
            if smilXML:
                smilSoup=BeautifulStoneSoup(smilXML, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
                print smilSoup.prettify()
                return smilSoup
            else:
                return False
        else:
            return False
        
    def content_sig(self, parameters):
        hmac_key = 'f6daaa397d51f568dd068709b0ce8e93293e078f7dfc3b40dd8c32d36d2b3ce1'
        sorted_parameters = sorted(parameters.iteritems(), key=operator.itemgetter(0))
        data = ''
        for item1, item2 in sorted_parameters:
            data += item1 + item2
        sig = hmac.new(hmac_key, data)
        return sig.hexdigest()

    def decrypt_SMIL(self, encsmil):
        encdata = binascii.unhexlify(encsmil)
        expire_message = 'Your access to play this content has expired.'
        plus_message = 'please close any Hulu Plus videos you may be watching on other devices'
        proxy_message = 'you are trying to access Hulu through an anonymous proxy tool'
        for key in smildeckeys[:]:
            cbc = common.AES_CBC(binascii.unhexlify(key[0]))
            smil = cbc.decrypt(encdata,key[1])
            
            print smil
            if (smil.find("<smil") == 0):
                #print key
                i = smil.rfind("</smil>")
                smil = smil[0:i+7]
                return smil
            elif expire_message in smil:
                xbmcgui.Dialog().ok('Content Expired',expire_message)
                return False
            elif plus_message in smil:
                xbmcgui.Dialog().ok('Too many sessions','please close any Hulu Plus videos','you may be watching on other devices')
                return False
            elif proxy_message in smil:
                xbmcgui.Dialog().ok('Proxy Detected','Based on your IP address we noticed','you are trying to access Hulu','through an anonymous proxy tool')
                return False
    
    def queueViewComplete(self):
        u = sys.argv[0]
        u += "?mode='viewcomplete'"
        u += '&videoid="'+urllib.quote_plus(common.args.videoid)+'"'
        item=xbmcgui.ListItem("Remove from Queue")
        common.playlist.add(url=u, listitem=item)

    def queueVideoSegment( self, video_id, segment=False):
        mode='SEGMENT_play'
        u = sys.argv[0]
        u += '?url="'+urllib.quote_plus(video_id)+'"'
        u += '&mode="'+urllib.quote_plus(mode)+'"'
        u += '&videoid="'+urllib.quote_plus(common.args.videoid)+'"'
        u += '&segment="'+urllib.quote_plus(str(segment))+'"'
        u += '&guid="'+urllib.quote_plus(self.GUID)+'"'
        item=xbmcgui.ListItem(self.displayname)
        item.setInfo( type="Video", infoLabels=self.infoLabels)
        item.setProperty('IsPlayable', 'true')
        common.playlist.add(url=u, listitem=item)

    def time2ms( self, time):
        hour,minute,seconds = time.split(';')[0].split(':')
        frame = int((float(time.split(';')[1])/24)*1000)
        milliseconds = (((int(hour)*60*60)+(int(minute)*60)+int(seconds))*1000)+frame
        return milliseconds

    def NetworkPreroll( self ):
        url = 'http://r.hulu.com/videos?eid='+common.args.eid+'&include=video_assets&include_eos=1&_language=en&_package_group_id=1&_region=US'
        data=common.getFEED(url)
        tree=BeautifulStoneSoup(data, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
        networkPreroll = tree.find('show').find('link-url').string
        if networkPreroll is not None:
            if '.flv' in networkPreroll:
                name = tree.find('channel').string
                infoLabels={ "Title":name }
                item = xbmcgui.ListItem(name+' Intro',path=networkPreroll)
                item.setInfo( type="Video", infoLabels=infoLabels)
                if self.queue:
                    item.setProperty('IsPlayable', 'true')
                    common.playlist.add(url=networkPreroll, listitem=item)
                else:
                    self.queue = True
                    xbmcplugin.setResolvedUrl(common.handle, True, item)

    def playSegment( self, video_id, segment=0):
        try:
            if segments > 0: smilSoup = self.getSMIL(video_id,retry=1)
            else: smilSoup = self.getSMIL(video_id)
        except: smilSoup = self.getSMIL(video_id,retry=1)
        if smilSoup:
            finalUrl = self.selectStream(smilSoup)
            self.displayname, self.infoLabels, segments  = self.getMeta(smilSoup)
            segmentUrl = finalUrl
            if segments:
                segmentUrl = finalUrl
                if segment > 0:
                    startseconds = self.time2ms(segments[segment-1])
                    segmentUrl += " start="+str(startseconds)
                if len(segments) > segment:
                    stopseconds = self.time2ms(segments[segment])
                    segmentUrl += " stop="+str(stopseconds)
            item = xbmcgui.ListItem(self.displayname,path=segmentUrl)
            item.setInfo( type="Video", infoLabels=self.infoLabels)
            if self.queue:
                item.setProperty('IsPlayable', 'true')
                common.playlist.add(url=segmentUrl, listitem=item)
            else:
                self.queue = True
                xbmcplugin.setResolvedUrl(common.handle, True, item)
            return segments

    def play( self, video_id):
        if (common.settings['enable_captions'] == 'true'):
            subtitles.Main().checkCaptions(video_id)
        try: smilSoup = self.getSMIL(video_id)
        except: smilSoup = self.getSMIL(video_id,retry=1)
        if smilSoup:
            finalUrl = self.selectStream(smilSoup)
            displayname, infoLabels, segments = self.getMeta(smilSoup)
            item = xbmcgui.ListItem(displayname,path=finalUrl)
            item.setInfo( type="Video", infoLabels=infoLabels)
            if self.queue:
                item.setProperty('IsPlayable', 'true')
                common.playlist.add(url=finalUrl, listitem=item)
            else:
                self.queue = True
                if self.NoResolve:
                    xbmc.sleep(5)
                    xbmc.Player().play(finalUrl,item)
                else:
                    xbmcplugin.setResolvedUrl(common.handle, True, item)
                if (common.settings['enable_captions'] == 'true'):
                    subtitles.Main().PlayWaitSubtitles(video_id)
            return segments

    def getMeta( self, smilSoup ):
        refs = smilSoup.findAll('ref')
        ref = refs[1]
        title = ref['title']
        series_title = ref['tp:series_title']
        plot = ref['abstract']
        try:season = int(ref['tp:season_number'])
        except:season = -1
        try:episode = int(ref['tp:episode_number'])
        except:episode = -1
        displayname = series_title+' - '+str(season)+'x'+str(episode)+' - '+title
        try:
            playlist = refs[0]['src']
            mpaa=re.compile('rating,([^\]]+)').findall(playlist)[0]
        except: mpaa=''
        infoLabels={ "Title":title,
                     "TVShowTitle":series_title,
                     "Plot":plot,
                     "MPAA":mpaa,
                     "Season":season,
                     "Episode":episode}
        try:
            segments = ref['tp:segments']
            if segments <> '':
                segments=segments.replace('T:','').split(',')
            else:
                segments = False
        except:segments = False
        return displayname, infoLabels, segments
                
    def selectStream( self, smilSoup ):        
        video=smilSoup.findAll('video')
        if video is None or len(video) == 0:
            xbmcgui.Dialog().ok('No Video Streams','SMIL did not contain video links','Geo-Blocked')
            return
        streams=[]
        selectedStream = None
        cdn = None
        qtypes=['ask', 'p011', 'p010', 'p009', 'p008', 'H264 Medium', 'H264 650K', 'H264 400K', 'VP6 400K']        
        qt = int(common.settings['quality'])
        if qt < 0 or qt > 8: qt = 0
        while qt < 8:
            qtext = qtypes[qt]
            for vid in video:
                streams.append([vid['profile'],vid['cdn'],vid['server'],vid['stream'],vid['token']])
                if qtext in vid['profile']:
                    if vid['cdn'] == common.settings['defaultcdn']:
                        selectedStream = [vid['server'],vid['stream'],vid['token']]
                        print selectedStream
                        cdn = vid['cdn']
                        break

            if qt == 0 or selectedStream != None: break
            qt += 1
        
        if qt == 0 or selectedStream == None:
            if selectedStream == None:
                #ask user for quality level
                quality=xbmcgui.Dialog().select('Please select a quality level:', [stream[0]+' ('+stream[1]+')' for stream in streams])
                print quality
                if quality!=-1:
                    selectedStream = [streams[quality][2], streams[quality][3], streams[quality][4]]
                    cdn = streams[quality][1]
                    print "stream url"
                    print selectedStream
            
        if selectedStream != None:
            server = selectedStream[0]
            stream = selectedStream[1]
            token = selectedStream[2]

            protocolSplit = server.split("://")
            pathSplit = protocolSplit[1].split("/")
            hostname = pathSplit[0]
            appName = protocolSplit[1].split(hostname + "/")[1]

            if "level3" in cdn:
                appName += "?sessionid=sessionId&" + token
                stream = stream[0:len(stream)-4]
                finalUrl = server + "?sessionid=sessionId&" + token + " app=" + appName

            elif "limelight" in cdn:
                appName += '?sessionid=sessionId&' + token
                stream = stream[0:len(stream)-4]
                finalUrl = server + "?sessionid=sessionId&" + token + " app=" + appName
                
            elif "akamai" in cdn:
                appName += '?sessionid=sessionId&' + token
                finalUrl = server + "?sessionid=sessionId&" + token + " app=" + appName

            elif "edgecast" in cdn:
                server=server.replace('.com','.com:80')
                appName += '?' + token
                finalUrl = server + "?" + token + " app=" + appName
                
            else:
                xbmcgui.Dialog().ok('Unsupported Content Delivery Network',cdn+' is unsupported at this time')
                return ""

            print "item url -- > " + finalUrl
            print "app name -- > " + appName
            print "playPath -- > " + stream

            #define item
            #SWFPlayer = 'http://download.hulu.com/huludesktop.swf'
            SWFPlayer = 'http://www.hulu.com/site-player/205970/player.swf?cb=205970'
            finalUrl += " playpath=" + stream + " swfurl=" + SWFPlayer + " pageurl=" + SWFPlayer
            finalUrl += " swfvfy=true"
            #if (common.settings['swfverify'] == 'true'):
#                finalUrl += " swfvfy=true"
            return finalUrl

                          
################# OLD FUNCTIONS
# might be useful        
                
                
    def decrypt_cid(self, p):
        cidkey = '48555bbbe9f41981df49895f44c83993a09334d02d17e7a76b237d04c084e342'
        v3 = binascii.unhexlify(p)
        ecb = common.AES(binascii.unhexlify(cidkey))
        return ecb.decrypt(v3).split("~")[0]

    def cid2eidOLD(self, p):
        import md5
        dec_cid = int(p.lstrip('m'), 36)
        xor_cid = dec_cid ^ 3735928559 # 0xDEADBEEF
        m = md5.new()
        m.update(str(xor_cid) + "MAZxpK3WwazfARjIpSXKQ9cmg9nPe5wIOOfKuBIfz7bNdat6gQKHj69ZWNWNVB1")
        value = m.digest()
        return base64.encodestring(value).replace("+", "-").replace("/", "_").replace("=", "").replace('/n','')

    def decrypt_pid(self, p):
        import re
        cp_strings = [
            '6fe8131ca9b01ba011e9b0f5bc08c1c9ebaf65f039e1592d53a30def7fced26c',
            'd3802c10649503a60619b709d1278ffff84c1856dfd4097541d55c6740442d8b',
            'c402fb2f70c89a0df112c5e38583f9202a96c6de3fa1aa3da6849bb317a983b3',
            'e1a28374f5562768c061f22394a556a75860f132432415d67768e0c112c31495',
            'd3802c10649503a60619b709d1278efef84c1856dfd4097541d55c6740442d8b'
        ]

        v3 = p.split("~")
        v3a = binascii.unhexlify(v3[0])
        v3b = binascii.unhexlify(v3[1])

        ecb = common.AES(v3b)
        tmp = ecb.decrypt(v3a)

        for v1 in cp_strings[:]:
            ecb = common.AES(binascii.unhexlify(v1))
            v2 = ecb.decrypt(tmp)
            if (re.match("[0-9A-Za-z_-]{32}", v2)):
                return v2

    def pid_auth(self, pid):
        import md5
        m=md5.new()
        m.update(str(pid) + "yumUsWUfrAPraRaNe2ru2exAXEfaP6Nugubepreb68REt7daS79fase9haqar9sa")
        return m.hexdigest()

(2014-03-17, 13:50)frieten Wrote: Thanks for this, would be even more awesome sauce if you could just provide edited py files to replace straight over Smile

copy and paste, or rename the file and make a new file (probably better)
Linux Mint 18 LTS 64-bit - Kodi 17 Beta6
Odroid-C2 - Libreelec v7.90.009
Reply
The spacing is important in Python, make sure the spaces / indentation line up correctly.
I can upload the modified files tonight when I get home.
Reply
Works good here on Droid. Wonder if the change has to do with http://download.hulu.com/huludesktop.swf . them no longer going to be using hulu desktop. anyway just a thought.

Thanks for the Quick Fix Bohdans!

John
Reply
Time is 2:58 am GMT.

I think AKAMAI is back again but the stream prompt list still popping up, even though AKAMAI is the default cdn in setting. This happens with both the original and russeldub's modified stream_hulu.py .Though now AKAMAI is listed as Darwin AKAMAI. Maybe this is why list pops up? I use the beta plugin


UPDATE 3:39 am GMT
Took a page from bohdans quide and only replaced the code in common.py as such:

Code:
cdns = ['level3','limelight','akamai']

with

Code:
cdns = ['level3','limelight','darwin-akamai','darwin-edgecast']

Stream prompt stopped popping up. See if it works for you.
Reply
(2014-03-18, 04:58)gjacov Wrote: Time is 2:58 am GMT.

I think AKAMAI is back again but the stream prompt list still popping up, even though AKAMAI is the default cdn in setting. This happens with both the original and russeldub's modified stream_hulu.py .Though now AKAMAI is listed as Darwin AKAMAI. Maybe this is why list pops up? I use the beta plugin

Are you using Hulu plus? Is the stream you select with Akamai playing as a 720p?
Reply
gjacov:

Which version of the SWFPlayer string is right for akamai?
Reply
(2014-03-18, 06:46)learningit Wrote:
(2014-03-18, 04:58)gjacov Wrote: Time is 2:58 am GMT.

I think AKAMAI is back again but the stream prompt list still popping up, even though AKAMAI is the default cdn in setting. This happens with both the original and russeldub's modified stream_hulu.py .Though now AKAMAI is listed as Darwin AKAMAI. Maybe this is why list pops up? I use the beta plugin

Are you using Hulu plus? Is the stream you select with Akamai playing as a 720p?

No Hulu plus,I live outside US. And as far as i know without Hulu plus you can only stream SD

(2014-03-18, 06:52)russelldub Wrote: gjacov:

Which version of the SWFPlayer string is right for akamai?

I really don't know much about these things .I just edited this one line in common.py, used your hulu_stream.py and it just worked
Reply
(2014-03-18, 07:36)gjacov Wrote: No Hulu plus,I live outside US. And as far as i know without Hulu plus you can only stream SD

So you are logging into Hulu, you have no Hulu Plus and you are seeing Darwin-Akamai as a CDN choice? Do you see just Akamai as a choice as well?

(2014-03-18, 06:52)russelldub Wrote: Which version of the SWFPlayer string is right for akamai?
'http://www.hulu.com/site-player/205970/player.swf?cb=205970' is fine.
Reply
OK. I'm not logging in. I see darwin-akamai as a choice and I'm using the newer SWFPlayer string.
Reply
(2014-03-18, 08:32)russelldub Wrote: OK. I'm not logging in. I see darwin-akamai as a choice and I'm using the newer SWFPlayer string.

If it's possible, I would really like to see a log from someone that sees the "darwin-akamai' choice and a brief description of your settings. I cannot reproduce this choice. Please do not load the log or any code into the forum pages, it is really bad form to keep posting either of them into the forum pages. Please use xbmclogs.com or pastebin or something like it for large pieces of text. The forum posts create problems with indentation, etc. if you're not very careful and lead to the creation of additional problems for others trying to understand what's going on or apply a fix. Just paste the url to the larger text from xbmclogs or pastebin here in the forum. Thanks.
Reply
(2014-03-17, 23:12)bramwell Wrote: I have tried copying and pasting this several times, and when I start the hulu addon, I get a script error. I have noticed that when I copy and paste it, after the first lines, it seems all lines are indented one space. Not sure if that makes a difference or not. Can't see any other reason that it wouldn't work. Any ideas would be appreciated. I am using XBMC with windows 8.1. I have tried using both notepad and notepad++ to copy and paste this code.

Thanks
AFAIK that space you talk about at the start of each line shouldn't be there. Try highlighting all the text in notepad++ and moving it all left one character with Shift + Tab

That worked for me.

Edit: Forgot to say before but thanks russelldub for the fix
Reply
Here I've just been using the Hulu Beta plugin with modifications mentioned earlier which has been working for me with edgecast... which seems to still be working fine for me *knocks on wood*

1) russelldub's modified steam_hulu.py
2) bodhan's mods for common.py and strings.xml (except I use 24fps p009 instead of 24fps medium... I went through and changed the strings to match the ones in the quality choices list instead of putting the first one to be 24fps medium like he suggested).
3) Shutting off Ads (and turning number of ads to 0 in preferences)

I've been wanting to stick to Hulu Beta because that's where I have all my favorites setup for currently.

If someone could modify the Hulu Beta plugin to have that baked in as well as having akamai as a fallback for just in case that would be great.
Reply
Thank you, Paul777. That did it. I am back in business. There is always so much to learn about using these programs. I appreciate you taking the time to give that tip. Thanks.
Reply
@smoke_tetsu, all

You're welcome to use GitHub or pm me fixes.
// GitHub // Repository

// USTV VoD (Video-on-Demand) / World News Live / MRT Play
Reply
I can report that they changed back the cdn to Akamai.
I used the old files of stream_hulu.py,common.py and strings.xml before they changed to edgecast

Changed the cdns to akamai and everything is awesome again Smile

I use HULU stable version not the beta.
Reply
  • 1
  • 105
  • 106
  • 107(current)
  • 108
  • 109
  • 128

Logout Mark Read Team Forum Stats Members Help
[SUPPORT] Hulu Video Plugin17