Updated to support webvideo
[pyTivo.git] / mind.py
blob86a91340fb3fdcaa53f3ad135ef119b5af132117
1 import cookielib
2 import urllib2
3 import urllib
4 import struct
5 import httplib
6 import time
7 import warnings
8 import itertools
9 import config
11 try:
12 import xml.etree.ElementTree as ElementTree
13 except ImportError:
14 try:
15 import elementtree.ElementTree as ElementTree
16 except ImportError:
17 warnings.warn('Python 2.5 or higher or elementtree is needed to use the TivoPush')
19 if 'ElementTree' not in locals():
21 class Mind:
22 def __init__(self, *arg, **karg):
23 raise Exception('Python 2.5 or higher or elementtree is needed to use the TivoPush')
25 else:
27 class Mind:
28 def __init__(self, username, password, debug=False):
29 self.__username = username
30 self.__password = password
32 self.__debug = debug
34 self.__cj = cookielib.CookieJar()
35 self.__opener = urllib2.build_opener(urllib2.HTTPSHandler(debuglevel=1), urllib2.HTTPCookieProcessor(self.__cj))
37 self.__login()
39 if not self.__pcBodySearch():
40 self.__pcBodyStore('pyTivo', True)
42 def pushVideo(self, tsn, url, description, duration, size, title, subtitle):
43 data = {
44 'bodyId' : 'tsn:' + tsn,
45 'description' : description,
46 'duration' : duration,
47 'encodingType' : 'mpeg2ProgramStream',
48 'partnerId' : 'tivo:pt.3187',
49 'pcBodyId' : pc_body_id,
50 'publishDate' : time.strftime('%Y-%m-%d %H:%M%S', time.gmtime()),
51 'size' : size,
52 'source' : 'file:/C%3A%2FDocuments%20and%20Settings%2FStephanie%2FDesktop%2FVideo',
53 'state' : 'complete',
54 'subtitle' : subtitle,
55 'title' : title,
56 'url' : url,
59 # It looks like tivo only supports one pc per house
60 pc_body_id = self.__pcBodySearch()[0]
61 offer_id, content_id = self.__bodyOfferModify(data)
62 self.__subscribe(offer_id, content_id, tsn)
64 def getDownloadRequests(self):
65 NEEDED_VALUES = [
66 'bodyId',
67 'bodyOfferId',
68 'description',
69 'partnerId',
70 'pcBodyId',
71 'publishDate',
72 'source',
73 'state',
74 'subscriptionId',
75 'subtitle',
76 'title',
77 'url',
80 # It looks like tivo only supports one pc per house
81 pc_body_id = self.__pcBodySearch()[0]
83 requests = []
84 offer_list = self.__bodyOfferSchedule(pc_body_id)
86 for offer in offer_list.findall('bodyOffer'):
87 d = {}
88 if offer.findtext('state') != 'scheduled':
89 continue
91 for n in NEEDED_VALUES:
92 d[n] = offer.findtext(n)
93 requests.append(d)
95 return requests
97 def completeDownloadRequest(self, request):
98 request['encodingType'] = 'mpeg2ProgramStream'
99 request['state'] = 'complete'
100 request['type'] = 'bodyOfferModify'
101 request['updateDate'] = time.strftime('%Y-%m-%d %H:%M%S', time.gmtime())
103 offer_id, content_id = self.__bodyOfferModify(request)
104 self.__subscribe(offer_id, content_id, request['bodyId'][4:])
107 def getXMPPLoginInfo(self):
108 # It looks like tivo only supports one pc per house
109 pc_body_id = self.__pcBodySearch()[0]
111 xml = self.__bodyXmppInfoGet(pc_body_id)
113 results = {}
114 results['server'] = xml.findtext('server')
115 results['port'] = int(xml.findtext('port'))
116 results['username'] = xml.findtext('xmppId')
118 for sendPresence in xml.findall('sendPresence'):
119 results.setdefault('presence_list', []).append(sendPresence.text)
121 return results
123 def __log(self, message):
124 if self.__debug:
125 print message
126 print
128 def __login(self):
130 data = {
131 'cams_security_domain' : 'tivocom',
132 'cams_login_config' : 'http',
133 'cams_cb_username' : self.__username,
134 'cams_cb_password' : self.__password,
135 'cams_original_url' : '/mind/mind7?type=infoGet'
138 r = urllib2.Request(
139 'https://mind.tivo.com:8181/mind/login',
140 urllib.urlencode(data)
142 try:
143 result = self.__opener.open(r)
144 except:
145 pass
147 self.__log('__login\n%s' % (data))
149 def __bodyOfferModify(self, data):
150 """Create an offer"""
151 r = urllib2.Request(
152 'https://mind.tivo.com:8181/mind/mind7?type=bodyOfferModify&bodyId=' + data['bodyId'],
153 dictcode(data),
154 {'Content-Type' : 'x-tivo/dict-binary'}
156 result = self.__opener.open(r)
158 xml = ElementTree.parse(result).find('.')
160 self.__log('__bodyOfferModify\n%s\n\n%sg' % (data, ElementTree.tostring(xml)))
162 if xml.findtext('state') != 'complete':
163 raise Exception(ElementTree.tostring(xml))
165 offer_id = xml.findtext('offerId')
166 content_id = offer_id.replace('of','ct')
168 return offer_id, content_id
171 def __subscribe(self, offer_id, content_id, tsn):
172 """Push the offer to the tivo"""
173 data = {
174 'bodyId' : 'tsn:' + tsn,
175 'idSetSource' : {
176 'contentId': content_id,
177 'offerId' : offer_id,
178 'type' : 'singleOfferSource',
180 'title' : 'pcBodySubscription',
181 'uiType' : 'cds',
184 r = urllib2.Request(
185 'https://mind.tivo.com:8181/mind/mind7?type=subscribe&bodyId=tsn:' + tsn,
186 dictcode(data),
187 {'Content-Type' : 'x-tivo/dict-binary'}
189 result = self.__opener.open(r)
191 xml = ElementTree.parse(result).find('.')
193 self.__log('__subscribe\n%s\n\n%sg' % (data, ElementTree.tostring(xml)))
195 return xml
197 def __bodyOfferSchedule(self, pc_body_id):
198 """Get pending stuff for this pc"""
200 data = {'pcBodyId' : pc_body_id,}
201 r = urllib2.Request(
202 'https://mind.tivo.com:8181/mind/mind7?type=bodyOfferSchedule',
203 dictcode(data),
204 {'Content-Type' : 'x-tivo/dict-binary'}
206 result = self.__opener.open(r)
208 xml = ElementTree.parse(result).find('.')
210 self.__log('bodyOfferSchedule\n%s\n\n%sg' % (data, ElementTree.tostring(xml)))
212 return xml
214 def __pcBodySearch(self):
215 """Find PCS"""
217 data = {}
218 r = urllib2.Request(
219 'https://mind.tivo.com:8181/mind/mind7?type=pcBodySearch',
220 dictcode(data),
221 {'Content-Type' : 'x-tivo/dict-binary'}
223 result = self.__opener.open(r)
225 xml = ElementTree.parse(result).find('.')
228 self.__log('__pcBodySearch\n%s\n\n%sg' % (data, ElementTree.tostring(xml)))
230 return [id.text for id in xml.findall('pcBody/pcBodyId')]
232 def __collectionIdSearch(self, url):
233 """Find collection ids"""
235 data = {'url' : url}
236 r = urllib2.Request(
237 'https://mind.tivo.com:8181/mind/mind7?type=collectionIdSearch',
238 dictcode(data),
239 {'Content-Type' : 'x-tivo/dict-binary'}
241 result = self.__opener.open(r)
243 xml = ElementTree.parse( result ).find('.')
244 collection_id = xml.findtext('collectionId')
246 self.__log('__collectionIdSearch\n%s\n\n%sg' % (data, ElementTree.tostring(xml)))
248 return collection_id
250 def __pcBodyStore(self, name, replace=False):
251 """Setup a new PC"""
253 data = {
254 'name' : name,
255 'replaceExisting' : str(replace).lower(),
258 r = urllib2.Request(
259 'https://mind.tivo.com:8181/mind/mind7?type=pcBodyStore',
260 dictcode(data),
261 {'Content-Type' : 'x-tivo/dict-binary'}
263 result = self.__opener.open(r)
265 xml = ElementTree.parse(result).find('.')
267 self.__log('__pcBodySearch\n%s\n\n%s' % (data, ElementTree.tostring(xml)))
269 return xml
271 def __bodyXmppInfoGet(self, body_id):
273 data = {
274 'bodyId' : body_id,
277 r = urllib2.Request(
278 'https://mind.tivo.com:8181/mind/mind7?type=bodyXmppInfoGet&bodyId=' + body_id,
279 dictcode(data),
280 {'Content-Type' : 'x-tivo/dict-binary'}
283 result = self.__opener.open(r)
285 xml = ElementTree.parse(result).find('.')
287 self.__log('__bodyXmppInfoGe\n%s\n\n%s' % (data, ElementTree.tostring(xml)))
289 return xml
292 def dictcode(d):
293 """Helper to create x-tivo/dict-binary"""
294 output = []
296 keys = [str(k) for k in d]
297 keys.sort()
299 for k in keys:
300 v = d[k]
302 output.append( varint( len(k) ) )
303 output.append( k )
305 if isinstance(v, dict):
306 output.append( struct.pack('>B', 0x02) )
307 output.append( dictcode(v) )
309 else:
310 v = unicode(v).encode('utf-8')
311 output.append( struct.pack('>B', 0x01) )
312 output.append( varint( len(v) ) )
313 output.append( v )
315 output.append( struct.pack('>B', 0x00) )
317 output.append( struct.pack('>B', 0x80) )
319 return ''.join(output)
321 def varint(i):
322 import sys
324 bits = []
325 while i:
326 bits.append(i & 0x01)
327 i = i >> 1
329 if not bits:
330 output = [0]
331 else:
332 output = []
334 while bits:
335 byte = 0
336 mybits = bits[:7]
337 del bits[:7]
339 for bit, p in zip(mybits, itertools.count()):
340 byte += bit * (2 ** p)
342 output.append(byte)
344 output[-1] = output[-1] | 0x80
345 return ''.join([chr(b) for b in output])
348 def getMind():
349 username = config.getTivoUsername()
350 password = config.getTivoPassword()
352 if not username or not password:
353 raise Exception("tivo_username and tivo_password required")
355 m = Mind(username, password, True)
357 return m