1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
import resource
from database import DDBObject
from template import fillStaticTemplate
from httpclient import grabURL
from xhtmltools import urlencode
from copy import copy
import re
import app
import config
import indexes
import menu
import prefs
import threading
import urllib
import eventloop
import views
from gtcache import gettext as _
HTMLPattern = re.compile("^.*(<head.*?>.*</body\s*>)", re.S)
# Desired semantics:
# * The first time getHTML() is called (ever, across sessions), the user
# gets 'firstTimeIntroBody' above. Subsequent times, she gets the <body>
# part of the document returned from CHANNEL_GUIDE_URL (the 'network guide
# body'.)
# * The network guide body is retrieved at program startup, and once an
# hour after that, so that getHTML() can return immediately. If retrieval
# fails, we just skip that hourly update. The last-retrieved copy is
# kept in memory, and it is saved to disk across executions of the program.
# * When getHTML() is called, we just return that cached copy. That might
# be from the previous run of the program if we haven't succeeded in a
# retrieval during this session.
# * If we have *never* succeeded in retrieving the channel guide, we put up
# an error page and immediately schedule a reload. We provide a "try again"
# link that simply reloads the guide. Hopefully by the time the user
# manages to click it, the scheduled update attempt will have completed.
#
# NEEDS: Right now, there's a race between execution (and completion)
# of the first update and the first call to getHTML(). On Windows, the
# latter has a tendency to happen first, meaning that on our first
# view of the channel guide in a session we see a stale copy of the
# guide from the last run. This usually isn't a problem on the first
# run, because the load will finish while the user is messing around
# with the tutorial.
#
# Fixing this is tricky -- do we want to block waiting for the update
# to complete during that first load? Then we need to redesign some of
# the frontend semantics somewhere, because this causes nasty
# lockup-like behavior on Windows. (It might be sufficient to call
# onStartup from a new thread rather than from a JS window creation
# event handler calling into Python.)
class ChannelGuide(DDBObject):
def __init__(self, url=None):
# Delayed callback for eventloop.
self.dc = None
# True if user has seen the tutorial, or this is a non default guide.
# We don't want to display the Intro anymore.
self.sawIntro = True
# self.sawIntro = url != None
# If None, we have never successfully loaded the guide. Otherwise,
# the <body> part of the front channel guide page from the last time
# we loaded it, whenever that was (perhaps in a previous session.)
self.cachedGuideBody = None
# True if we have successfully loaded the channel guide in this
# session.
self.loadedThisSession = False
# None means this is the default channel guide.
self.url = url
self.redirectedURL = None
DDBObject.__init__(self)
# Start loading the channel guide.
self.startLoadsIfNecessary()
def __str__(self):
return "Channel Guide <%s>" % (self.url,)
##
# Called by pickle during deserialization
def onRestore(self):
self.loadedThisSession = False
self.dc = None
# Try to get a fresh version.
# NEEDS: There's a race between self.update finishing and
# getHTML() being called. If the latter happens first, we might get
# the version of the channel guide from the last time DTV was run even
# if we have a perfectly good net connection.
self.startLoadsIfNecessary()
def startLoadsIfNecessary(self):
import frontend
if frontend.getDTVAPIURL():
# Uses direct browsing. No precaching needed here.
pass
else:
# Uses precaching. Set up an initial update, plus hourly reloads..
self.startUpdates()
def setSawIntro(self):
self.sawIntro = True
self.signalChange()
def startUpdates(self):
if not self.dc:
self.dc = eventloop.addIdle (self.update, "Channel Guide Update")
# How should we load the guide? Returns (scheme, value). If scheme is
# 'url', value is a URL that should be loaded directly in the frame.
# If scheme is 'template', value is the template that should be loaded in
# the frame.
def getLocation(self):
if not self.sawIntro:
return ('template', 'first-time-intro')
import frontend
apiurl = frontend.getDTVAPIURL()
if apiurl:
# We're on a platform that uses direct loads and DTVAPI.
apiurl = urllib.quote_plus(apiurl)
apicookie = urllib.quote_plus(frontend.getDTVAPICookie())
url = "%s?dtvapiURL=%s&dtvapiCookie=%s" % (self.getURL(), apiurl, apicookie)
return ('url', url)
# We're on a platform that uses template inclusions and URL
# interception.
return ('template', 'guide')
def makeContextMenu(self, templateName, view):
menuItems = [
(lambda: app.delegate.copyTextToClipboard(self.getURL()),
_('Copy URL to clipboard')),
]
if not self.getDefault():
i = (lambda: app.controller.removeGuide(self), _('Remove'))
menuItems.append(i)
return menu.makeMenu(menuItems)
def getHTML(self):
# In the future, may want to use
# self.loadedThisSession to tell if this is a fresh
# copy of the channel guide, and/or block a bit to
# give the initial load a chance to succeed or fail
# (but this would require changing the frontend code
# to expect the template code to block, and in general
# seems like a bad idea.)
#
# A better solution would be to put up a "loading" page and
# somehow shove an event to the page when the channel guide
# load finishes that causes the browser to reload the page.
if (not self.cachedGuideBody) or (not self.loadedThisSession):
# Start a new attempt, so that clicking on the guide
# tab again has at least a chance of working
#print "DTV: No guide available! Sending apology instead."
self.startUpdates()
return fillStaticTemplate("go-to-guide", platform="", eventCookie="", id=self.getID())
else:
return self.cachedGuideBody
def processUpdate(self, info):
try:
html = info["body"]
# Put the HTML into the cache
match = HTMLPattern.match(html)
wasLoading = self.cachedGuideBody is None
if match:
self.cachedGuideBody = match.group(1)
else:
self.cachedGuideBody = html
self.redirectedURL = info['redirected-url']
selection = app.controller.selection
if wasLoading:
myTab = None
for tab in views.guideTabs:
if tab.obj is self:
myTab = tab
break
if myTab and selection.isTabSelected(myTab):
selection.displayCurrentTabContent()
self.loadedThisSession = True
self.signalChange()
finally:
self.dc = eventloop.addTimeout(3600, self.update, "Channel Guide Update")
def processUpdateErrback(self, error):
print "WARNING: HTTP error while downloading the channel guide (%s)" \
% error
self.dc = eventloop.addTimeout(3600, self.update,
"Channel Guide Update")
def update(self):
# We grab the URL and convert the HTML to JavaScript so it can
# be loaded from a plain old template. It's less elegant than
# making another kind of feed object, but it makes it easier
# for non-programmers to work with
print "DTV: updating the Guide"
self.dc = grabURL(self.getURL(), self.processUpdate, self.processUpdateErrback)
def remove(self):
self.dc.cancel()
DDBObject.remove(self)
def getURL(self):
if self.url is not None:
return self.url
else:
return config.get(prefs.CHANNEL_GUIDE_URL)
def getRedirectedURL(self):
return self.redirectedURL
def getDefault(self):
return self.url is None
# For the tabs
def getTitle(self):
if self.getDefault():
return _('Channel Guide')
else:
return self.getURL()
def getIconURL(self):
return resource.url("images/channelguide-icon-tablist.png")
def getGuideByURL(url):
return views.guides.getItemWithIndex(indexes.guidesByURL, url)
|