From 8b26641a7a62343a70a59967fc5b345bb494f200 Mon Sep 17 00:00:00 2001 From: Lunatixz Date: Thu, 9 Nov 2017 16:02:21 -0500 Subject: [plugin.video.ustvnow] 2017.10.16 recommend updates --- plugin.video.ustvnow/resources/lib/__init__.py | 0 plugin.video.ustvnow/resources/lib/net.py | 345 +++++++++++ plugin.video.ustvnow/resources/lib/ustvnow.py | 787 +++++++++++++++++++++++++ 3 files changed, 1132 insertions(+) create mode 100644 plugin.video.ustvnow/resources/lib/__init__.py create mode 100644 plugin.video.ustvnow/resources/lib/net.py create mode 100644 plugin.video.ustvnow/resources/lib/ustvnow.py (limited to 'plugin.video.ustvnow/resources/lib') diff --git a/plugin.video.ustvnow/resources/lib/__init__.py b/plugin.video.ustvnow/resources/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin.video.ustvnow/resources/lib/net.py b/plugin.video.ustvnow/resources/lib/net.py new file mode 100644 index 0000000..c6237fd --- /dev/null +++ b/plugin.video.ustvnow/resources/lib/net.py @@ -0,0 +1,345 @@ +''' + common XBMC Module + Copyright (C) 2011 t0mm0 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +import socket, urllib, urllib2, cookielib, gzip +import re, StringIO, requests, traceback +import xbmc, xbmcaddon + +# Plugin Info +ADDON_ID = 'plugin.video.ustvnow' +REAL_SETTINGS = xbmcaddon.Addon(id=ADDON_ID) +ADDON_VERSION = REAL_SETTINGS.getAddonInfo('version') +DEBUG = REAL_SETTINGS.getSetting('Enable_Debugging') == 'true' + +def log(msg, level=xbmc.LOGDEBUG): + if DEBUG == False and level != xbmc.LOGERROR: return + if level == xbmc.LOGERROR: msg += ' ,' + traceback.format_exc() + xbmc.log(ADDON_ID + '-' + ADDON_VERSION + '-' + (msg.encode("utf-8")), level) + +class HeadRequest(urllib2.Request): + '''A Request class that sends HEAD requests''' + def get_method(self): + return 'HEAD' + +socket.setdefaulttimeout(30) +class Net: + ''' + This class wraps :mod:`urllib2` and provides an easy way to make http + requests while taking care of cookies, proxies, gzip compression and + character encoding. + + Example:: + + from t0mm0.common.net import Net + net = Net() + response = net.http_GET('http://xbmc.org') + print response.content + ''' + + _cj = cookielib.LWPCookieJar() + _proxy = None + _user_agent = 'Mozilla/5.0' + _http_debug = DEBUG + + + def __init__(self, cookie_file='', proxy='', user_agent='', + http_debug=False): + ''' + Kwargs: + cookie_file (str): Full path to a file to be used to load and save + cookies to. + + proxy (str): Proxy setting (eg. + ``'http://user:pass@example.com:1234'``) + + user_agent (str): String to use as the User Agent header. If not + supplied the class will use a default user agent (chrome) + + http_debug (bool): Set ``True`` to have HTTP header info written to + the XBMC log for all requests. + ''' + if cookie_file: + self.set_cookies(cookie_file) + if proxy: + self.set_proxy(proxy) + if user_agent: + self.set_user_agent(user_agent) + self._http_debug = http_debug + self._update_opener() + + + def set_cookies(self, cookie_file): + ''' + Set the cookie file and try to load cookies from it if it exists. + + Args: + cookie_file (str): Full path to a file to be used to load and save + cookies to. + ''' + try: + self._cj.load(cookie_file, ignore_discard=True) + self._update_opener() + return True + except: + return False + + + def get_cookies(self): + '''Returns A dictionary containing all cookie information by domain.''' + return self._cj._cookies + + + def save_cookies(self, cookie_file): + ''' + Saves cookies to a file. + + Args: + cookie_file (str): Full path to a file to save cookies to. + ''' + self._cj.save(cookie_file, ignore_discard=True) + + + def set_proxy(self, proxy): + ''' + Args: + proxy (str): Proxy setting (eg. + ``'http://user:pass@example.com:1234'``) + ''' + self._proxy = proxy + self._update_opener() + + + def get_proxy(self): + '''Returns string containing proxy details.''' + return self._proxy + + + def set_user_agent(self, user_agent): + ''' + Args: + user_agent (str): String to use as the User Agent header. + ''' + self._user_agent = user_agent + + + def get_user_agent(self): + '''Returns user agent string.''' + return self._user_agent + + + def _update_opener(self): + ''' + Builds and installs a new opener to be used by all future calls to + :func:`urllib2.urlopen`. + ''' + if self._http_debug: + http = urllib2.HTTPHandler(debuglevel=1) + else: + http = urllib2.HTTPHandler() + + if self._proxy: + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cj), + urllib2.ProxyHandler({'http': + self._proxy}), + urllib2.HTTPBasicAuthHandler(), + http) + + else: + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cj), + urllib2.HTTPBasicAuthHandler(), + http) + urllib2.install_opener(opener) + + + def http_GET(self, url, headers={}, compression=True): + ''' + Perform an HTTP GET request. + + Args: + url (str): The URL to GET. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + compression (bool): If ``True`` (default), try to use gzip + compression. + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page and the page content. + ''' + return self._fetch(url, headers=headers, compression=compression) + + + def http_POST(self, url, form_data, headers={}, compression=True): + ''' + Perform an HTTP POST request. + + Args: + url (str): The URL to POST. + + form_data (dict): A dictionary of form data to POST. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + compression (bool): If ``True`` (default), try to use gzip + compression. + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page and the page content. + ''' + return self._fetch(url, form_data, headers=headers, + compression=compression) + + + def http_HEAD(self, url, headers={}): + ''' + Perform an HTTP HEAD request. + + Args: + url (str): The URL to GET. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page. + ''' + req = HeadRequest(url) + req.add_header('User-Agent', self._user_agent) + for k, v in headers.items(): + req.add_header(k, v) + response = urllib2.urlopen(req) + return HttpResponse(response) + + + def _json(self, url, form_data={}, headers={}): + if not headers: + headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', + 'X-Requested-With': 'XMLHttpRequest'} + response = requests.get(url, params=form_data, headers=headers) + return response.json() + + + def _fetch(self, url, form_data={}, headers={}, compression=True): + ''' + Perform an HTTP GET or POST request. + + Args: + url (str): The URL to GET or POST. + + form_data (dict): A dictionary of form data to POST. If empty, the + request will be a GET, if it contains form data it will be a POST. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + compression (bool): If ``True`` (default), try to use gzip + compression. + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page and the page content. + ''' + encoding = '' + try: + req = urllib2.Request(url) + if form_data: + form_data = urllib.urlencode(form_data) + req = urllib2.Request(url, form_data) + req.add_header('User-Agent', self._user_agent) + for k, v in headers.items(): + req.add_header(k, v) + if compression: + req.add_header('Accept-Encoding', 'gzip') + response = urllib2.urlopen(req) + except urllib2.HTTPError as e: + log('_fetch, failed ' + str(e.code), xbmc.LOGERROR) + log('_fetch, failed ' + str(e.msg), xbmc.LOGERROR) + return + return HttpResponse(response) + + + +class HttpResponse: + ''' + This class represents a resoponse from an HTTP request. + + The content is examined and every attempt is made to properly encode it to + Unicode. + + .. seealso:: + :meth:`Net.http_GET`, :meth:`Net.http_HEAD` and :meth:`Net.http_POST` + ''' + + content = '' + '''Unicode encoded string containing the body of the reposne.''' + + + def __init__(self, response): + ''' + Args: + response (:class:`mimetools.Message`): The object returned by a call + to :func:`urllib2.urlopen`. + ''' + self._response = response + html = response.read() + try: + if response.headers['content-encoding'].lower() == 'gzip': + html = gzip.GzipFile(fileobj=StringIO.StringIO(html)).read() + except: + pass + + try: + content_type = response.headers['content-type'] + if 'charset=' in content_type: + encoding = content_type.split('charset=')[-1] + except: + pass + + r = re.search('. + +# -*- coding: utf-8 -*- +import os, sys, datetime, re, traceback, HTMLParser, calendar +import urllib, urllib2, socket, json, collections, net, random +import xbmc, xbmcvfs, xbmcgui, xbmcplugin, xbmcaddon + +from simplecache import SimpleCache +# Plugin Info +ADDON_ID = 'plugin.video.ustvnow' +REAL_SETTINGS = xbmcaddon.Addon(id=ADDON_ID) +ADDON_NAME = REAL_SETTINGS.getAddonInfo('name') +SETTINGS_LOC = REAL_SETTINGS.getAddonInfo('profile') +ADDON_PATH = REAL_SETTINGS.getAddonInfo('path').decode('utf-8') +ADDON_VERSION = REAL_SETTINGS.getAddonInfo('version') +ICON = REAL_SETTINGS.getAddonInfo('icon') +FANART = REAL_SETTINGS.getAddonInfo('fanart') +LANGUAGE = REAL_SETTINGS.getLocalizedString + +## GLOBALS ## +TIMEOUT = 30 +CONTENT_TYPE = 'files' +USER_EMAIL = REAL_SETTINGS.getSetting('User_Email') +PASSWORD = REAL_SETTINGS.getSetting('User_Password') +LAST_TOKEN = REAL_SETTINGS.getSetting('User_Token') +LAST_PASSKEY = REAL_SETTINGS.getSetting('User_Paskey') +DEBUG = REAL_SETTINGS.getSetting('Enable_Debugging') == 'true' +PTVL_RUN = xbmcgui.Window(10000).getProperty('PseudoTVRunning') == 'True' +BASEURL = 'https://m-api.ustvnow.com/' +BASEWEB = 'https://watch.ustvnow.com' +BASEMOB = 'http://mc.ustvnow.com/' +IMG_PATH = os.path.join(ADDON_PATH,'resources','images') +IMG_BASE = 'http://m.poster.static-ustvnow.com' +IMG_SOURCE = IMG_BASE + '/%s/%s/%s/med' +IMG_CHLOGO_W = 'http://tvdata.ustvnow.com/inverse-logos/360/%s.png' +IMG_CHLOGO = 'http://m.ustvnow.com/images/%s.png' +COOKIE_JAR = xbmc.translatePath(os.path.join(SETTINGS_LOC, "cookiejar.lwp")) +MEDIA_TYPES = {'SP':'video','SH':'episode','EP':'episode','MV':'movie'} +FREE_CHANS = ['CW','ABC','FOX','PBS','CBS','NBC','MY9'] +URL_TYPE = {0:'m3u8',1:'mp4'}[int(REAL_SETTINGS.getSetting('URL_Type'))] +URL_QUALITY = int(REAL_SETTINGS.getSetting('URL_Quality')) + 1 +CHAN_NAMES = {'whtm':'ABC','ABC':'ABC','amchd':'AMC','AMC':'AMC','animalplanet':'Animal Planet','Animal Planet':'Animal Planet','bravo':'Bravo','Bravo':'Bravo','whphd':'CBS','CBS':'CBS','cnbc':'CNBC','CNBC':'CNBC','wlyh':'CW','CW':'CW','comedycentral':'Comedy Central','Comedy Central':'Comedy Central','discovery':'Discovery Channel','Discovery Channel':'Discovery Channel','espn':'ESPN','ESPN':'ESPN', + 'FOX':'FOX','wpmt':'FOX','fxhd':'FX','FX':'FX','Fox News Channel':'Fox News','foxnews':'Fox News','frfrm':'Freeform','Freeform':'Freeform','msnbc':'MSNBC','MSNBC':'MSNBC','wgal':'NBC','NBC':'NBC','natgeo':'National Geographic','National Geographic Channel':'National Geographic','nickelodeon':'Nickelodeon','Nickelodeon':'Nickelodeon','wpsu':'PBS','PBS':'PBS', + 'spiketv':'SPIKE TV','SPIKE TV':'SPIKE TV','sndnc':'SundanceTV','SundanceTV':'SundanceTV','syfy':'Syfy','Syfy':'Syfy','AE':'A&E','My9':'MY9','whvl':'MY9','bbca':'BBC America','BBCA':'BBC America','espntwo':'ESPN 2','ESPN2':'ESPN 2','nbcsnhd':'NBCSN','NBCSNHD':'NBCSN','tlchd':'TLC','The Learning Channel':'TLC','Universal HD':'Universal', + 'usahd':'USA Network','USA':'USA Network'} + +USTVNOW_MENU = [("Live" , '', 0), + ("Schedules" , '', 1), + ("Recordings" , '', 2), + ("Lineup" , '', 3), + ("Movies" , '', 5), + ("Highlights", '', 12), + ("OnDemand" , '', 13), + ("Search" , '', 11), + ("Guide" , '', 20)] + +# if xbmc.getCondVisibility('System.HasAddon(script.module.uepg)') == 1: + # USTVNOW_MENU.append(("uEPG Guide", '', 20)) + +uEPG_PARAMS = {"runtime":"duration","stream_code":"studio","description":"plot","synopsis":"plotoutline","ut_start":"starttime","orig_air_date":"firstaired"} +FILE_PARAMS = ["title", "artist", "albumartist", "genre", "year", "rating", "album", "track", "duration", "comment", "lyrics", "musicbrainztrackid", "musicbrainzartistid", "musicbrainzalbumid", "musicbrainzalbumartistid", "playcount", "fanart", "director", "trailer", "tagline", "plot", "plotoutline", "originaltitle", "lastplayed", "writer", "studio", "mpaa", "cast", "country", "imdbnumber", "premiered", "productioncode", "runtime", "set", "showlink", "streamdetails", "top250", "votes", "firstaired", "season", "episode", "showtitle", "thumbnail", "file", "resume", "artistid", "albumid", "tvshowid", "setid", "watchedepisodes", "disc", "tag", "art", "genreid", "displayartist", "albumartistid", "description", "theme", "mood", "style", "albumlabel", "sorttitle", "episodeguide", "uniqueid", "dateadded", "size", "lastmodified", "mimetype", "specialsortseason", "specialsortepisode"] +PVR_PARAMS = ["title","plot","plotoutline","starttime","endtime","runtime","progress","progresspercentage","genre","episodename","episodenum","episodepart","firstaired","hastimer","isactive","parentalrating","wasactive","thumbnail","rating","originaltitle","cast","director","writer","year","imdbnumber","hastimerrule","hasrecording","recording","isseries"] +ART_PARAMS = ["thumb","poster","fanart","banner","landscape","clearart","clearlogo"] + +def uni(string, encoding = 'utf-8'): + if isinstance(string, basestring): + if not isinstance(string, unicode): + string = unicode(string, encoding) + elif isinstance(string, unicode): + string = string.encode('ascii', 'replace') + return string + +def unescape(string): + try: + parser = HTMLParser.HTMLParser() + return (parser.unescape(string)) + except: + return string + +def log(msg, level=xbmc.LOGDEBUG): + if DEBUG == False and level != xbmc.LOGERROR: return + if level == xbmc.LOGERROR: msg += ' ,' + traceback.format_exc() + xbmc.log(ADDON_ID + '-' + ADDON_VERSION + '-' + (msg.encode("utf-8")), level) + +def inputDialog(heading=ADDON_NAME, default='', key=xbmcgui.INPUT_ALPHANUM, opt=0, close=0): + retval = xbmcgui.Dialog().input(heading, default, key, opt, close) + if len(retval) > 0: + return retval + +def okDialog(str1, str2='', str3='', header=ADDON_NAME): + xbmcgui.Dialog().ok(header, str1, str2, str3) + +def yesnoDialog(str1, str2='', str3='', header=ADDON_NAME, yes='', no='', autoclose=0): + return xbmcgui.Dialog().yesno(header, str1, str2, str3, no, yes, autoclose) + +def getParams(): + param=[] + if len(sys.argv[2])>=2: + params=sys.argv[2] + cleanedparams=params.replace('?','') + if (params[len(params)-1]=='/'): + params=params[0:len(params)-2] + pairsofparams=cleanedparams.split('&') + param={} + for i in range(len(pairsofparams)): + splitparams={} + splitparams=pairsofparams[i].split('=') + if (len(splitparams))==2: + param[splitparams[0]]=splitparams[1] + return param + +socket.setdefaulttimeout(TIMEOUT) +class USTVnow(): + def __init__(self): + log('__init__') + self.net = net.Net() + self.cache = SimpleCache() + self.isFree = REAL_SETTINGS.getSetting('User_isFree') == "True" + if self.login(USER_EMAIL, PASSWORD) == False: raise SystemExit + + + def mainMenu(self): + log('mainMenu') + for item in USTVNOW_MENU: + self.addDir(*item) + + + def buildHeader(self): + header_dict = {} + header_dict['Accept'] = 'application/json, text/javascript, */*; q=0.01' + header_dict['Host'] = 'm-api.ustvnow.com' + header_dict['Connection'] = 'keep-alive' + header_dict['Referer'] = 'http://watch.ustvnow.com' + header_dict['Origin'] = 'http://watch.ustvnow.com' + header_dict['User-Agent'] = 'Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.127 Large Screen Safari/533.4 GoogleTV/162671' + return header_dict + + + def login(self, user, password): + log('login') + if len(user) > 0: + try: xbmcvfs.rmdir(COOKIE_JAR) + except: pass + + if xbmcvfs.exists(COOKIE_JAR) == False: + try: + xbmcvfs.mkdirs(SETTINGS_LOC) + f = xbmcvfs.File(COOKIE_JAR, 'w') + f.close() + except: log('login, Unable to create the storage directory', xbmc.LOGERROR) + header_dict = self.buildHeader() + self.net.set_cookies(COOKIE_JAR) + + try: + #check token + dvrlink = None + custlink = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/getcustomerkey', form_data={'token':LAST_TOKEN}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'username': u'', u'ip': u'', u'customerkey': u''}''' + self.custkey = (custlink.get('customerkey','') or 0) + if custlink and 'username' in custlink and user.lower() == custlink['username'].lower(): + log('login, using existing token, passkey') + self.token = LAST_TOKEN + self.passkey = LAST_PASSKEY + else: + #login + loginlink = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/login', form_data={'username':user,'password':password,'device':'gtv','redir':'0'}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'token': u'', u'result': u'success'}''' + if loginlink and 'token' in loginlink and loginlink['result'].lower() == 'success': + log('login, creating new token') + self.token = loginlink['token'] + REAL_SETTINGS.setSetting('User_Token',self.token) + + #passkey + dvrlink = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/viewdvrlist', form_data={'token':self.token}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'globalparams': {u'passkey': u''}, u'results': []}''' + if dvrlink and 'globalparams' in dvrlink: + log('login, creating new passkey') + self.passkey = dvrlink['globalparams']['passkey'] + REAL_SETTINGS.setSetting('User_Paskey',self.passkey) + else: raise Exception + + #check user credentials + userlink = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/getuserbytoken', form_data={'token':self.token}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'status': u'success', u'data': {u'username': u'', u'need_account_activation': False, u'plan_id': 1, u'language': u'en', u'plan_free': 1, u'sub_id': u'7', u'lname': u'', u'currency': u'USD', u'points': 1, u'need_account_renew': False, u'fname': u'', u'plan_name': u'Free Plan'}}''' + log('login, checking user account') + if userlink and 'data' in userlink and userlink['status'].lower() == 'success': + REAL_SETTINGS.setSetting('User_Plan' ,userlink['data']['plan_name']) + expires = 'Never' if userlink['data']['plan_name'] == 'Free Plan' else '' + REAL_SETTINGS.setSetting('User_Expires' ,'%s'%expires) + REAL_SETTINGS.setSetting('User_isFree' ,str(userlink['data']['plan_name'] == 'Free Plan')) + dvrPlan = 2 if 'dvr' in (userlink['data']['plan_name']).lower() else None + REAL_SETTINGS.setSetting('User_DVRpoints' ,str(dvrPlan or userlink['data']['points'] or 0)) + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30006) + userlink['data']['fname'], ICON, 4000) + + if userlink['data']['need_account_renew'] == True: + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30016) + userlink['data']['fname'], ICON, 4000) + elif userlink['data']['need_account_activation'] == True: + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30022) + userlink['data']['fname'], ICON, 4000) + else: + if REAL_SETTINGS.getSetting('User_DVRpoints') != REAL_SETTINGS.getSetting('Last_DVRpoints'): + REAL_SETTINGS.setSetting('Last_DVRpoints',REAL_SETTINGS.getSetting('User_DVRpoints')) + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30021) + userlink['data']['fname'], ICON, 4000) + + #check subscription + try: + #error prone, isolate and debug. + sublink = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/getaccountsubscription', form_data={'username':user,'customerkey':self.custkey}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'username': u'', u'billingDatetime': u'', u'currency': u'USD', u'cgaccountstatusreason': u'', u'invoicehistory': u'', u'ocaccountstatus': u'Account active', u'ccLastFour': u'', u'lname': u'', u'x-cg-acnt-USD': False, u'fname': u'', u'sub_info': {u'cost': 0, u'plan': {u'sub_group': 4, u'plan_id': 1, u'name': u'Free Plan', u'language': u'en', u'date_expire': u'0000-00-00', u'sub_id': 7, u'price': 0, u'currency': u'USD', u'plan_code': u'7_FREETRIAL', + u'details': u'This plan lets you receive all major US terrestrial stations (ABC, CBS, CW, FOX, NBC, PBS). You can later upgrade to a paid plan with more channels and DVR.'}, u'packages': []}, u'pendinginvoices': u'', u'cgbillingstatus': u'', u'dvrpoints': 1, u'plans': {u'10': {u'price': 15, u'name': u'1 Week All Channel Plan $15 ($2.14/day)', + u'details': u'1 Week pass for all channels (No DVR)'}, u'23': {u'price': 19, u'name': u'All Channel Promo Plan $19/mo first 3 months', + u'details': u'Monthly subscription for all channels. This promotional price is for the first three months after which it will renew at $29. This plan automatically renews each month but you can cancel anytime and will not be billed again when your current 30 day period has expired. '}, u'31': {u'price': 29, u'name': u'All Channel Promo Plan w/DVR $29/mo first 3 months', + u'details': u'Monthly subscription for all channels. This promotional price is for the first three months after which it will renew at $39. This plan automatically renews each month but you can cancel anytime and will not be billed again when your current 30 day period has expired.'}, u'7': {u'price': 0, u'name': u'Free Plan', + u'details': u'This plan lets you receive all major US terrestrial stations (ABC, CBS, CW, FOX, NBC, PBS). You can later upgrade to a paid plan with more channels and DVR.'}, u'9': {u'price': 0.00, u'name': u'3 Day All Channel Plan $6.99 ($2.33/day)', + u'details': u'3 Day pass for all channels (No DVR)'}, u'8': {u'price': 2.99, u'name': u'1 Day All Channel Plan $2.99', + u'details': u'24 hour pass for all channels (No DVR)'}}, u'cgredirectUrl': u'', u'cgaccountstatus': False, u'isfacebookuser': False, u'cgkey': u'', u'ocaccountstatuscode': 0, u'packages': [], u'subscription': u'Free Plan', u'language': u'en', u'sub_id': u'7', u'dateopened': u'December 01, 2000', u'canceledDatetime': u'', u'cgbillingmethod': u''}''' + if sublink: REAL_SETTINGS.setSetting('User_Expires' ,'%s'%(sublink['sub_info']['date_expire'])) + except: pass + else: raise Exception + self.net.save_cookies(COOKIE_JAR) + + self.channels = self.cache.get(ADDON_NAME + '.channelguide') + if not self.channels: + log('login, refreshing channels') + channels = sorted(json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/channelguide', form_data={'token':self.token}, headers=header_dict).content.encode("utf-8").rstrip())['results'], key=lambda x: x['displayorder']) + '''{u'app_name': u'preview', u'stream': u'00000WXYZustvnow', u'af': u'US', u'dvraction': u'add', u'callsign': u'WHTM', u'event_inprogress': 1, + u'srsid': 3560383, u'guideremainingtime': 3660, u'scheduleid': 9952642, u'favoriteaction': u'remove', u'event_time': u'00:00:00',u'title': u'Shark Tank', + u'timemark': 1498867200, u'recordedon': u'June 30, 2017 20:00', u'prg_img': u'h3/NowShowing/9977826/p9977826_b1t_h3_aa.jpg', u'title_10': u'', + u'xcdrappname': u'livehd', u'event_date': u'2017-07-01', u'has_img': 1, u'stream_code': u'ABC', u'updated': u'2017-06-29 11:49:08', + u'description': u'Durable bags made out of the material that protects on the front lines of firefighting; a vibrating mat that helps calm babies; + an ointment made from essential oils; a natural snack made with acai.', u'actualremainingtime': 3281, u'content_allowed': False, + u'dvrtimeraction': u'add', u'mediatype': u'EP', u'auth': None, u'stream_origin': u'dne.ustvnow.com', u'dvrtimertype': 0, u'scode': u'whtm', + u'aksid': 1, u'guideheight': 120, u'orig_air_date': u'2017-02-10', u'prgsvcid': 11534, u'runtime': 3660, u'img': u'images/WHTM.png', + u'connectorid': u'SH011581290000', u'ut_start': 1498867200, u'streamname': u'00000WXYZustvnow', u'episode_title': u'', u'synopsis': + u'A vibrating mat that helps calm babies.', u't': 1, u'edgetypes': 7, u'imgmark': u'live', u'content_id': u'EP011581290171', u'order': 1, + u'displayorder': 1}''' + self.cache.set(ADDON_NAME + '.channelguide', channels, expiration=datetime.timedelta(minutes=5)) + self.channels = self.cache.get(ADDON_NAME + '.channelguide') + + self.upcoming = self.cache.get(ADDON_NAME + '.upcoming') + if not self.upcoming: + log('login, refreshing upcoming') + upcoming = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/upcoming', form_data={'token':self.token}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'prgschid': 19479379, u'dvraction': u'add', u'callsign': u'AMC', u'newtimecat': True, u'srsid': 14930, u'timecat': u'Today', u'scheduleid': 9946282, + u'img': u'images/AMC.png', u'title': u'The Fugitive', u'prg_img': u'v5/NowShowing/14930/p14930_p_v5_aa.jpg', u'has_img': 1, u't': 0, u'sname': u'AMC', + u'description': u'U.S. marshal (Tommy Lee Jones) hunts doctor (Harrison Ford) for murder of his wife (Sela Ward).', u'dvrtimeraction': u'add', + u'auth': None, u'orig_air_date': None, u'dvrtimertype': 0, u'scode': u'amchd', u'prgsvcid': 10021, u'runtime': 10800, u'connectorid': u'MV000371790000', + u'episode_title': u'', u'synopsis': u'An innocent man must evade the law as he pursues a killer.', u'event_inprogress': 1, u'ut_start': 1499009400, + u'content_allowed': False, u'imgmark': u'live', u'content_id': u'MV000371790000', u'displayorder': 100}''' + self.cache.set(ADDON_NAME + '.upcoming', upcoming, expiration=datetime.timedelta(hours=6)) + self.upcoming = self.cache.get(ADDON_NAME + '.upcoming') + + self.highlights = self.cache.get(ADDON_NAME + '.highlights') + if not self.highlights: + log('login, refreshing highlights') + highlights = json.loads(self.net.http_POST(BASEURL + 'api/1/live/highlights', form_data={'token':self.token}, headers=header_dict).content.encode("utf-8").rstrip()) + '''{u'prgschid': 114059791, u'dvraction': u'add', u'bb_content': u'How would you like having Jim Morrison as a college roommate? ', u'callsign': u'WPSU', u'newtimecat': True, + u'srsid': 185479, u'timecat': u'Tomorrow', u'scheduleid': 49826153, u'img': u'images/WPSU.png', u'title': u'Antiques Roadshow', u'prg_img': u'h3/NowShowing/185479/p185479_b_h3_ag.jpg', + u'has_img': 1, u't': 1, u'sname': u'PBS', u'description': u'A French Art Deco diamond and platinum ring; a copy of "The History of Magic" signed by Jim Morrison; four Rembrandt', + u'dvrtimeraction': u'add', u'auth': None, u'orig_air_date': u'2014-02-17', u'dvrtimertype': 0, u'scode': u'wpsu', u'prgsvcid': 11786, u'runtime': 3600, u'connectorid': u'SH002036520000', + u'episode_title': u'Baton Rouge', u'synopsis': u'Diamond and platinum ring.', u'event_inprogress': 2, u'ut_start': 1508216400, u'content_allowed': True, u'imgmark': u'', u'content_id': + u'EP002036520417', u'displayorder': 6}''' + self.cache.set(ADDON_NAME + '.highlights', highlights, expiration=datetime.timedelta(hours=6)) + self.highlights = self.cache.get(ADDON_NAME + '.highlights') + + self.recorded = self.cache.get(ADDON_NAME + '.recorded') + if not self.recorded: + log('login, refreshing recorded') + '''{u'fn1': u'20170702', u'episode_season': 0, u'filename_smil': u'20170702_213000_220000_utc_SH000000010000_11534_11534_ABC.smil', u'app_name': u'dvrrokuplay', + u'stream': u'2F1ECWHTMUSTVNOW', u'imgmark': u'rem', u'dvrtimeraction': u'add', u'mediatype': u'SH', u'orig_air_date': None, u'content_id': u'SH000000010000', + u'callsign': u'WHTM', u'ut_expires': 1500327000, u'filename_med': u'20170702_213000_220000_utc_SH000000010000_11534_11534_ABC_650.mp4', + u'filename_high': u'20170702_213000_220000_utc_SH000000010000_11534_11534_ABC_950.mp4', u'dvrlocation': u'dvr6', u'episode_number': 0, u'srsid': 459763, + u'runtime': 1800, u'description': u'Paid programming.', u'scheduleid': 9952747, u'has_img': 1, u'fn3': u'220000', u'fn2': u'213000', + u'event_time': u'21:30:00', u'title': u'Paid Programming', u'recordedonmmddyyyy': u'07/02/2017', u'connectorid': u'SH000000010000', + u'recordedon': u'July 2, 2017', u'prg_img': u'h3/NowShowing/459763/p459763_b_h3_ae.jpg', u'filename_low': u'20170702_213000_220000_utc_SH000000010000_11534_11534_ABC_350.mp4', + u'episode_title': u'', u'synopsis': u'Paid programming.', u'dvrtimertype': 0, u'content_allowed': True, u'xcdrappname': u'livehd', u'event_date': u'2017-07-02', + u'event_inprogress': 2, u'prgsvcid': 11534, u'ut_start': 1499031000, u'stream_code': u'ABC'}''' + if dvrlink: recorded = dvrlink['results'] + else: recorded = (json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/viewdvrlist', form_data={'token':self.token}, headers=header_dict).content.encode("utf-8").rstrip()))['results'] + self.cache.set(ADDON_NAME + '.recorded', recorded, expiration=datetime.timedelta(minutes=1)) + self.recorded = self.cache.get(ADDON_NAME + '.recorded') + self.names = self.getChannelNames() + return True + except Exception as e: + log('login, Unable to login ' + str(e), xbmc.LOGERROR) + REAL_SETTINGS.setSetting('User_Token','') + REAL_SETTINGS.setSetting('User_Paskey','') + REAL_SETTINGS.setSetting('User_Activated',str(False)) + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30007), ICON, 4000) + return False + else: + #firstrun wizard + if yesnoDialog(LANGUAGE(30008),no=LANGUAGE(30009), yes=LANGUAGE(30010)): + user = inputDialog(LANGUAGE(30001)) + password = inputDialog(LANGUAGE(30002),opt=xbmcgui.ALPHANUM_HIDE_INPUT) + REAL_SETTINGS.setSetting('User_Email' ,user) + REAL_SETTINGS.setSetting('User_Password',password) + return self.login(user, password) + else: + okDialog(LANGUAGE(30003)) + return False + + + def getChannelNames(self): + collect = [] + if self.channels is None: xbmc.executebuiltin("Container.Refresh") + for channel in self.channels: + try: + name = CHAN_NAMES[channel['stream_code']] + if self.isFree == True and name not in FREE_CHANS: continue + collect.append(name) + except: pass + counter = collections.Counter(collect) + names = sorted(list(set(sorted(counter.elements())))) + log('getChannelNames, names = ' + str(names)) + return names + + + def browseLive(self): + log('browseLive') + d = datetime.datetime.utcnow() + now = datetime.datetime.fromtimestamp(calendar.timegm(d.utctimetuple())) + if self.channels is None: xbmc.executebuiltin("Container.Refresh") + for channel in self.channels: + try: + name = CHAN_NAMES[channel['stream_code']] + if self.isFree == True and name not in FREE_CHANS: continue + startime = datetime.datetime.fromtimestamp(channel['ut_start']) + endtime = startime + datetime.timedelta(seconds=channel['runtime']) + if endtime > now and startime <= now: + label, url, liz = self.buildChannelListItem(name, channel, opt='Live') + self.addLink(label, url, 9, liz, len(self.channels)) + except Exception as e: + log('browseLive, failed ' + str(e), xbmc.LOGERROR) + + + def browseRecordings(self, recorded=False): + log('browseRecordings') + d = datetime.datetime.utcnow() + now = datetime.datetime.fromtimestamp(calendar.timegm(d.utctimetuple())) + if self.recorded is None: xbmc.executebuiltin("Container.Refresh") + for channel in self.recorded: + try: + startime = datetime.datetime.fromtimestamp(channel['ut_start']) + endtime = startime + datetime.timedelta(seconds=channel['runtime']) + if recorded == False and channel['event_inprogress'] == 0: continue + elif recorded == True and channel['event_inprogress'] != 0: continue + label, url, liz = self.buildRecordedListItem(CHAN_NAMES[channel['stream_code']]) + mode = 10 if recorded == True else 21 + if mode == 21: liz.setProperty("IsPlayable","false") + self.addLink(label, url, mode, liz, len(self.recorded)) + except Exception as e: + log('browseRecordings, failed ' + str(e), xbmc.LOGERROR) + + + def browseGuide(self, name=None, upcoming=False): + log('browseGuide') + d = datetime.datetime.utcnow() + now = datetime.datetime.fromtimestamp(calendar.timegm(d.utctimetuple())) + if self.channels is None: xbmc.executebuiltin("Container.Refresh") + if name is None and upcoming == False: + for channel in sorted(self.names): + icon = (os.path.join(IMG_PATH,'logos','%s.png'%channel) or ICON) + infoArt = {"thumb":icon,"poster":icon,"icon":icon,"fanart":FANART} + self.addDir(channel, channel, 4, infoArt=infoArt) + else: + for channel in self.channels: + try: + if self.isFree == True and name not in FREE_CHANS: continue + if name == CHAN_NAMES[channel['stream_code']]: + startime = datetime.datetime.fromtimestamp(channel['ut_start']) + endtime = startime + datetime.timedelta(seconds=channel['runtime']) + if endtime > now and (startime <= now): + label, url, liz = self.buildChannelListItem(name, channel) + self.addLink(label, url, 9, liz, len(self.channels)) + elif endtime > now and (startime <= now or startime > now): + label, url, liz = self.buildChannelListItem(name, channel) + mode = 9 if PTVL_RUN == True else 21 + if mode == 21: liz.setProperty("IsPlayable","false") + self.addLink(label, url, mode, liz, len(self.channels)) + except Exception as e: + log('browseGuide, failed ' + str(e), xbmc.LOGERROR) + + + def browseFeatured(self, hlt=False): + log('browseFeatured') + d = datetime.datetime.utcnow() + now = datetime.datetime.fromtimestamp(calendar.timegm(d.utctimetuple())) + CONTENT_TYPE = 'movies' if not hlt else 'episodes' + optLST = self.highlights.get('programs') if hlt else self.upcoming + if optLST is None: xbmc.executebuiltin("Container.Refresh") + for channel in optLST: + try: + name = CHAN_NAMES[channel['sname']] + startime = datetime.datetime.fromtimestamp(channel['ut_start']) + endtime = startime + datetime.timedelta(seconds=channel['runtime']) + if endtime > now and (startime <= now or startime > now): + label, url, liz = self.buildChannelListItem(name, channel, opt='Featured') + liz.setProperty("IsPlayable","false") + self.addLink(label, url, 21, liz, len(optLST)) + except Exception as e: + log('browseFeatured, failed ' + str(e), xbmc.LOGERROR) + + + def browseVOD(self, url=None, limit=24): + log('browseVOD') + ''' + {u'paging': {u'current': u'/api/1/live/vod/?role=user&limit=24&status%5B0%5D=5', u'next_query': u'role=user&limit=24&status%5B0%5D=5&page=4954', u'next': u'/api/1/live/vod/?role=user&limit=24&status%5B0%5D=5&page=4954'}, + u'data': [{u'rented': None, u'ratings': [{u'description': u'', u'id': 12585970, u'name': u'TV-Y'}], u'scheduleid': u'V4954', u'description': u"HORSELAND: FIRST LOVE - Tomboyish Alma has a crush on a boy. Chloe . (CC)", + u'language': u'en', u'title': u'Horseland: First Love', u'content_allowed': False, u'price': u'0.99', u'created': 1493301489, u'genres': [], u'licensing_start': 1494475260, u'slug': u'V4954', u'director': None, + u'closed_caption': True, u'rent_due': 2, u'actors': [{u'lname': u'Donlan', u'id': 30781624, u'fname': u'Dana'}, {u'lname': u'Hernandez', u'id': 30781625, u'fname': u'Emily'}, {u'lname': u'Heyward', u'id': 30781626, u'fname': + u'Bianca'}], u'year': 2006, u'images': {u'posters': {u'small': u'/V4954/USTVNOW/mv/small', u'large': u'/V4954/USTVNOW/mv/large', u'medium': u'/V4954/USTVNOW/mv/medium'}}, u'runtime': u'00:21:15', u'id': 4954, + u'licensing_end': 1499745540}]} + ''' + if url is None: url = BASEWEB + '/vod/all-items' + items = self.net._json(url, form_data={'limit':limit,'token':self.token}) + if items and len(items) > 0: + label = '[B]%s[/B]'%LANGUAGE(30029) + liz = xbmcgui.ListItem(label) + liz.setArt({"thumb":ICON,"poster":ICON,"fanart":FANART}) + self.addLink(label, '', 22, liz, len(items)) + next = (items.get('paging','').get('next','') or '') + for item in items['data']: + label, url, liz = self.buildVODListItem(item) + liz.setProperty("IsPlayable","true") + liz.addContextMenuItems([('Purchase','')]) + self.addLink(label, url, 22, liz, len(items)) + # self.addDir('>> Next', BASEWEB + next, 13) + + + def search(self): + log('search') + kb = xbmc.Keyboard('', LANGUAGE(30027)%ADDON_NAME) + xbmc.sleep(1000) + kb.doModal() + if kb.isConfirmed(): + try: + query = kb.getText() + items = json.loads(self.net.http_POST(BASEURL + 'gtv/1/live/search', form_data={'token':self.token,'q_title':query}, headers=self.buildHeader()).content.encode("utf-8").rstrip())['results']['programs'] + for channel in items: + try: + name = CHAN_NAMES[channel['progs']['scode']] + label, url, liz = self.buildChannelListItem(name, channel['progs'], opt='Search') + liz.setProperty("IsPlayable","false") + self.addLink(label, url, 21, liz, len(items)) + except Exception as e: + log('search, error ' + str(e), xbmc.LOGERROR) + if len(items) == 0: + label = LANGUAGE(30028)%query + liz = xbmcgui.ListItem(label) + liz.setArt({"thumb":ICON,"poster":ICON,"fanart":FANART}) + self.addLink(label, '', 0, liz, 1) + except Exception as e: + log('search, failed ' + str(e), xbmc.LOGERROR) + else: self.mainMenu() + + + def buildVODListItem(self, item): + url = '' + title = unescape(item['title']) + price = item['price'] + year = item['year'] + h, m, s = item['runtime'].split(':') + runtime = int(h) * 3600 + int(m) * 60 + int(s) + label = '%s ($%s)'%(title,price) + label2 = item['price'] + plot = unescape(item['description']) + liz = xbmcgui.ListItem(label) + thumb = IMG_BASE + item['images']['posters']['large'] + infoList = {"mediatype":'movie',"label":label,"label2":label2,"title":label,"duration":runtime,"plot":plot} + liz.setInfo(type="Video", infoLabels=infoList) + liz.setArt({"thumb":thumb,"poster":thumb,"fanart":FANART}) + liz.setProperty("IsPlayable","true") + liz.setProperty("IsInternetStream","true") + log('buildVODListItem, label = ' + label + ', url = ' + url) + return label, url, liz + + + def buildChannelListItem(self, name, channel=None, opt=''): + if channel is None: + for idx, channel in enumerate(self.channels): + if name == CHAN_NAMES[channel['stream_code']]: break + startime = datetime.datetime.fromtimestamp(channel['ut_start']) + endtime = startime + datetime.timedelta(seconds=channel['runtime']) + title = unescape(channel['title']) + mediatype = (channel.get('mediatype','') or (channel.get('connectorid',''))[:2] or (channel.get('content_id',''))[:2] or 'SP') + mtype = MEDIA_TYPES[mediatype.upper()] + label = name if PTVL_RUN == True else '%s: %s - %s'%(startime.strftime('%I:%M %p').lstrip('0'),name,title) + label = '%s - %s'%(name,title) if opt == 'Live' else label + label2 = '%s - %s'%(startime.strftime('%I:%M %p').lstrip('0'),endtime.strftime('%I:%M %p').lstrip('0')) + url = name + liz = xbmcgui.ListItem(label) + thumb = IMG_SOURCE%(str(channel['srsid']),channel['callsign'],mediatype) + thumb = thumb + '/snapshot' if opt == 'Live' else thumb + poster = (os.path.join(IMG_PATH,'%s.png'%name) or ICON) + logo = IMG_CHLOGO%(channel['callsign']) + tag = ''#urllib.quote(json.dumps({"channelname":name,"channelnumber":self.names.index(name)+1,"channellogo":logo,"label":title,"label2":self.isHD(),"starttime":channel['ut_start']})) + infoList = {"mediatype":'episode',"label":label,"label2":label2,"title":label,"tagline":tag, + "studio":name,"duration":channel['runtime'],"plotoutline":unescape(channel.get('synopsis','')), + "plot":unescape(channel['description']),"aired":startime.strftime('%Y-%m-%d')} + liz.setInfo(type="Video", infoLabels=infoList) + liz.setArt({"thumb":thumb,"poster":thumb,"fanart":FANART,"clearlogo":logo}) + liz.setProperty("IsPlayable","true") + liz.setProperty("IsInternetStream","true") + if channel['dvrtimeraction'] == 'add': + opt = '@'.join([str(channel['prgsvcid']),(channel.get('event_time','') or str(channel.get('ut_start','')))])#lazy solution rather then create additional url parameters for this single function. + contextMenu = [('Set single recording' ,'XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['scheduleid']))+"&mode="+str(6)+"&name="+urllib.quote(opt))), + ('Set recurring recording','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['connectorid']))+"&mode="+str(7)+"&name="+urllib.quote(opt)))] + else: + contextMenu = [('Remove recording','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['scheduleid']))+"&mode="+str(8)+"&name="+urllib.quote(name)))] + liz.addContextMenuItems(contextMenu) + log('buildChannelListItem, label = ' + label + ', url = ' + url) + return label, url, liz + + + def buildRecordedListItem(self, name): + for channel in self.recorded: + if name == CHAN_NAMES[channel['stream_code']] or name == str(channel['scheduleid']): + startime = datetime.datetime.fromtimestamp(channel['ut_start']) + endtime = startime + datetime.timedelta(seconds=channel['runtime']) + title = unescape(channel['title']) + label = '%s - %s: %s - %s'%(startime.strftime('%Y-%m-%d'),startime.strftime('%I:%M %p').lstrip('0'),name,title) + label2 = '%s-%s'%(startime.strftime('%I:%M %p').lstrip('0'),endtime.strftime('%I:%M %p').lstrip('0')) + url = str(channel['scheduleid']) + liz = xbmcgui.ListItem(label) + mediatype = (channel.get('mediatype','') or (channel.get('connectorid',''))[:2] or (channel.get('content_id',''))[:2] or 'SP') + mtype = MEDIA_TYPES[mediatype.upper()] + infoList = {"mediatype":mtype,"label":label,"label2":label2,"title":label,"studio":CHAN_NAMES[channel['stream_code']], + "duration":channel['runtime'],"plotoutline":unescape(channel['synopsis']),"plot":unescape(channel['description']), + "aired":startime.strftime('%Y-%m-%d')} + thumb = IMG_SOURCE%(str(channel['srsid']),channel['callsign'],mediatype) + liz.setInfo(type="Video", infoLabels=infoList) + liz.setArt({"thumb":thumb,"poster":thumb,"fanart":FANART}) + liz.setProperty("IsPlayable","true") + liz.setProperty("IsInternetStream","true") + contextMenu = [] + # if channel['dvrtimeraction'] == 'add': + # opt = '@'.join([str(channel['prgsvcid']),channel['event_time']])#lazy solution rather then create additional url parameters for this single function. + # contextMenu = [('Set single recording' ,'XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['scheduleid']))+"&mode="+str(6)+"&name="+urllib.quote(opt))), + # ('Set recurring recording','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['connectorid']))+"&mode="+str(7)+"&name="+urllib.quote(opt)))] + contextMenu.extend([('Remove recording','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['scheduleid']))+"&mode="+str(8)+"&name="+urllib.quote(name)))]) + liz.addContextMenuItems(contextMenu) + log('buildRecordedListItem, label = ' + label + ', url = ' + url) + return label, url, liz + + + def resolveURL(self, url, dvr=False): + log('resolveURL, url = ' + url + ', dvr = ' + str(dvr)) + if dvr: + for channel in self.recorded: + if url == str(channel['scheduleid']): + try: + urllink = json.loads(self.net.http_POST(BASEURL + 'stream/1/dvr/play', form_data={'token':self.token,'key':self.passkey,'scheduleid':channel['scheduleid']}, headers=self.buildHeader()).content.encode("utf-8").rstrip()) + '''{u'pr': u'll', u'domain': u'ilvc02.ll.ustvnow.com',u'stream': u'http://ilvc02.ll.ustvnow.com/ilv10/pr/xxl/smil:0B64AWHTMUSTVNOW/playlist.m3u8?', + u'streamname': u'0B64AWHTMUSTVNOW', u'tr': u'', u'up': 1, u'pd': 0, u'pl': u'vjs'}''' + stream = urllink['stream'] + # if URL_TYPE == 'm3u8': stream = urllink['stream'] + # else: stream = (urllink['stream'].replace('smil:','mp4:').replace('USTVNOW','USTVNOW%d'%URL_QUALITY)) + log('resolveURL, url = ' + stream) + return stream + except Exception as e: + if channel and channel['scheduleid']: + self.replaceToken(url, dvr) + else: + for channel in self.channels: + if url == CHAN_NAMES[channel['stream_code']]: + try: + urllink = json.loads(self.net.http_POST(BASEURL + 'stream/1/live/view', form_data={'token':self.token,'key':self.passkey,'scode':channel['scode']}, headers=self.buildHeader()).content.encode("utf-8").rstrip()) + '''{u'pr': u'll', u'domain': u'ilvc02.ll.ustvnow.com',u'stream': u'http://ilvc02.ll.ustvnow.com/ilv10/pr/xxl/smil:0B64AWHTMUSTVNOW/playlist.m3u8?', + u'streamname': u'0B64AWHTMUSTVNOW', u'tr': u'', u'up': 1, u'pd': 0, u'pl': u'vjs'}''' + if URL_TYPE == 'm3u8': stream = urllink['stream'] + else: stream = (urllink['stream'].replace('smil:','mp4:').replace('USTVNOW','USTVNOW%d'%URL_QUALITY)) + log('resolveURL, stream = ' + stream) + return stream + except Exception as e: + if channel and channel['scode']: + self.replaceToken(url, dvr) + + + def replaceToken(self, url, dvr): + #generate alternative token using website endpoint rather then googletv. + try: + #get CSRF Token + response = urllib2.urlopen(BASEWEB + "/account/signin").read() + CSRF = re.findall(r'var csrf_value = "(.*?)"', response, re.DOTALL)[0] + #get WEB Token + response = (self.net.http_POST(BASEWEB + '/account/login', form_data={'csrf_ustvnow': CSRF, 'signin_email': USER_EMAIL, 'signin_password':PASSWORD, 'signin_remember':'1'}).content.encode("utf-8").rstrip()) + altToken = re.findall(r'var token(.*?)= "(.*?)";', response, re.DOTALL)[0][1] + if altToken and altToken != 'null': + self.token = altToken + log('replaceToken, replacing existing token') + REAL_SETTINGS.setSetting('User_Token',altToken) + self.resolveURL(url, dvr) + except Exception as e: + log('replaceToken, Unable to login ' + str(e), xbmc.LOGERROR) + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30005), ICON, 4000) + raise SystemExit + + + def setRecording(self, name, url, remove=False, recurring=False): + log('setRecording, name = ' + name + ', url = ' + url + ', remove = ' + str(remove)) + if remove == True: + setlink = (self.net.http_POST(BASEURL + 'gtv/1/dvr/updatedvr', form_data={'scheduleid':url,'token':self.token,'action':'remove'}, headers=self.buildHeader()).content.encode("utf-8").rstrip()) + else: + if int(REAL_SETTINGS.getSetting('User_DVRpoints')) <= 1: + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30019), ICON, 4000) + return + opt = name.split('@')#lazy solution rather then create additional url parameters for this single function. + if recurring == True: + setlink = (self.net.http_POST(BASEURL + 'gtv/1/dvr/updatedvrtimer', form_data={'connectorid':url,'prgsvcid':opt[0],'eventtime':opt[1],'token':self.token,'action':'add'}, headers=self.buildHeader()).content.encode("utf-8").rstrip()) + else: + setlink = (self.net.http_POST(BASEURL + 'gtv/1/dvr/updatedvr' , form_data={'scheduleid':url,'token':self.token,'action':'add'}, headers=self.buildHeader()).content.encode("utf-8").rstrip()) + '''failureadd''' + action = re.findall(r'(.*?)', setlink, re.DOTALL)[0] + status = re.findall(r'(.*?)', setlink, re.DOTALL)[0] + log('setRecording, action = ' + action + ', status = ' + status) + if status == 'failure': + log('setRecording, setlink = ' + str(setlink), xbmc.LOGERROR) + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30023)%action.title(), ICON, 4000) + return + self.cache.set(ADDON_NAME + '.recorded', None, expiration=datetime.timedelta(seconds=1)) + xbmcgui.Dialog().notification(ADDON_NAME, LANGUAGE(30024)%action.title(), ICON, 4000) + # xbmc.sleep(1001) + # xbmc.executebuiltin("Container.Update(plugin://plugin.video.ustvnow/?mode=1)") + # xbmc.executebuiltin("Container.Update(plugin://plugin.video.ustvnow/?mode=2)") + + + def playVideo(self, url, dvr=False): + log('playVideo, url = ' + url + ', dvr = ' + str(dvr)) + if dvr: label, path, liz = self.buildRecordedListItem(url) + else: label, path, liz = self.buildChannelListItem(url, opt='Live') + liz.setPath(self.resolveURL(url,dvr)) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, liz) + + + def addLink(self, name, u, mode, liz, total=0): + log('addLink, name = ' + name) + u=sys.argv[0]+"?url="+urllib.quote(u)+"&mode="+str(mode)+"&name="+urllib.quote(uni(name)) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=liz,totalItems=total) + + + def addDir(self, name, u, mode, infoList=False, infoArt=False): + log('addDir, name = ' + name) + liz=xbmcgui.ListItem(name) + liz.setProperty('IsPlayable', 'false') + if infoList == False: liz.setInfo(type="Video", infoLabels={"mediatype":"video","label":name,"title":name} ) + else: liz.setInfo(type="Video", infoLabels=infoList) + if infoArt == False: liz.setArt({'thumb':ICON,'fanart':FANART}) + else: liz.setArt(infoArt) + u=sys.argv[0]+"?url="+urllib.quote(u)+"&mode="+str(mode)+"&name="+urllib.quote(uni(name)) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=liz,isFolder=True) + + + def isHD(self): + if URL_TYPE == 'm3u8': return 'HD' + elif URL_QUALITY > 2: return 'HD' + return '' + + + def hasCC(self): + if URL_TYPE == 'm3u8': return True + return False + + + def uEPG(self): + log('uEPG') + #support for upcoming uEPG universal epg framework module, module will be available from the Kodi repository. + #https://github.com/Lunatixz/XBMC_Addons/tree/master/script.module.uepg + collect = [] + if self.channels is None: xbmc.executebuiltin("Container.Refresh") + for channel in self.channels: + try: + name = CHAN_NAMES[channel['stream_code']] + if self.isFree == True and name not in FREE_CHANS: continue + collect.append(name) + except: pass + + channelNum = 0 + counter = collections.Counter(collect) + for key, value in sorted(counter.iteritems()): + newChannel = {} + guidedata = [] + channelName = key + channelNum = channelNum + 1 + isHD = self.isHD() + hasCC = self.hasCC() + newChannel['channelname'] = channelName + newChannel['channelnumber'] = channelNum + for channel in self.channels: + try: + name = CHAN_NAMES[channel['stream_code']] + if self.isFree == True and name not in FREE_CHANS: continue + if name == channelName: + tmpdata = {} + mediatype = (channel.get('mediatype','') or (channel.get('connectorid',''))[:2] or (channel.get('content_id',''))[:2] or 'SP') + mtype = MEDIA_TYPES[mediatype.upper()] + thumb = IMG_SOURCE%(str(channel['srsid']),channel['callsign'],mediatype) + poster = thumb if mediatype.lower() == 'mv' else '' + logo = IMG_CHLOGO_W%(channel['callsign']) + newChannel['channellogo'] = logo + + for key, value in channel.iteritems(): + try: + tmpdata[uEPG_PARAMS[key]] = unescape(value) + except: + if key in FILE_PARAMS + PVR_PARAMS: + tmpdata[key] = unescape(value) + + # contextMenu = [('Schedules' ,'XBMC.RunPlugin(%s)'%(sys.argv[0]+"?mode="+str(1))), + # ('Recordings','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?mode="+str(2))), + # ('OnDemand' ,'XBMC.RunPlugin(%s)'%(sys.argv[0]+"?mode="+str(13)))] + + contextMenu = [] + if channel['dvrtimeraction'] == 'add': + opt = '@'.join([str(channel['prgsvcid']),(channel.get('event_time','') or str(channel.get('ut_start','')))])#lazy solution rather then create additional url parameters for this single function. + contextMenu.append(('Set single recording' ,'XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['scheduleid']))+"&mode="+str(6)+"&name="+urllib.quote(opt)))) + contextMenu.append(('Set recurring recording','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['connectorid']))+"&mode="+str(7)+"&name="+urllib.quote(opt)))) + else: + contextMenu.append(('Remove recording','XBMC.RunPlugin(%s)'%(sys.argv[0]+"?url="+urllib.quote(str(channel['scheduleid']))+"&mode="+str(8)+"&name="+urllib.quote(name)))) + + isNew = False #todo parse startime and data + label2 = isHD + tags = {'isHD':isHD,'hasCC':hasCC,'isNew':isNew} + tmpdata['label'] = unescape(channel['title']) + tmpdata['label2'] = label2 + # tmpdata['tagline'] = json.dumps(tags) + tmpdata['art'] = {"thumb":thumb,"poster":poster,"clearlogo":logo} + tmpdata['mediatype'] = mtype + tmpdata['url'] = sys.argv[0]+'?mode=9&url=%s'%name + tmpdata['contextmenu'] = json.dumps(contextMenu) + guidedata.append(tmpdata) + except: pass + newChannel['guidedata'] = guidedata + yield newChannel + +params=getParams() +try: url=urllib.unquote(params["url"]) +except: url=None +try: name=urllib.unquote(params["name"]) +except: name=None +try: mode=int(params["mode"]) +except: mode=None + +log("Mode: "+str(mode)) +log("URL : "+str(url)) +log("Name: "+str(name)) + +if mode==None: USTVnow().mainMenu() +elif mode == 0: USTVnow().browseLive() +elif mode == 1: USTVnow().browseRecordings() +elif mode == 2: USTVnow().browseRecordings(recorded=True) +elif mode == 3: USTVnow().browseGuide() +elif mode == 4: USTVnow().browseGuide(name) +elif mode == 5: USTVnow().browseFeatured() +elif mode == 6: USTVnow().setRecording(name,url) +elif mode == 7: USTVnow().setRecording(name,url,recurring=True) +elif mode == 8: USTVnow().setRecording(name,url,remove=True) +elif mode == 9: USTVnow().playVideo(url) +elif mode == 10:USTVnow().playVideo(url,dvr=True) +elif mode == 11:USTVnow().search() +elif mode == 12: USTVnow().browseFeatured(True) +elif mode == 13:USTVnow().browseVOD(url) +elif mode == 19:xbmc.executebuiltin("RunScript(script.module.uepg,listitem=%s&skin_path=%s&refresh_path=%s&refresh_interval=%s&row_count=%s)"%(urllib.quote(sys.argv[0]+"?mode=3"),urllib.quote(json.dumps(ADDON_PATH)),urllib.quote(json.dumps(sys.argv[0]+"?mode=19")),urllib.quote(json.dumps("7200")),urllib.quote(json.dumps("7")))) +elif mode == 20:xbmc.executebuiltin("RunScript(script.module.uepg,json=%s&skin_path=%s&refresh_path=%s&refresh_interval=%s&row_count=%s)"%(urllib.quote(json.dumps(list(USTVnow().uEPG()))),urllib.quote(json.dumps(ADDON_PATH)),urllib.quote(json.dumps(sys.argv[0]+"?mode=20")),urllib.quote(json.dumps("7200")),urllib.quote(json.dumps("7")))) +elif mode == 21:xbmc.executebuiltin("action(ContextMenu)") + +xbmcplugin.setContent(int(sys.argv[1]) , CONTENT_TYPE) +xbmcplugin.addSortMethod(int(sys.argv[1]) , xbmcplugin.SORT_METHOD_UNSORTED) +xbmcplugin.addSortMethod(int(sys.argv[1]) , xbmcplugin.SORT_METHOD_NONE) +xbmcplugin.addSortMethod(int(sys.argv[1]) , xbmcplugin.SORT_METHOD_LABEL) +xbmcplugin.addSortMethod(int(sys.argv[1]) , xbmcplugin.SORT_METHOD_TITLE) +xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=True) \ No newline at end of file -- cgit v1.2.3