9 from urllib
import unquote_plus
, quote
, unquote
10 from urlparse
import urlparse
11 from xml
.dom
import minidom
13 from lrucache
import LRUCache
14 from Cheetah
.Template
import Template
18 from plugin
import Plugin
20 SCRIPTDIR
= os
.path
.dirname(__file__
)
24 # Some error/status message templates
26 MISSING
= """<h3>Missing Data.</h3> <br>
27 You must set both "tivo_mak" and "togo_path" before using this
29 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
30 will reload in 10 seconds."""
32 RESET_MSG
= """<h3>The pyTivo Server has been soft reset.</h3> <br>
33 pyTivo has reloaded the pyTivo.conf file and all changes should now be
35 The <a href="/TiVoConnect?Command=%s&Container=%s">previous</a> page
36 will reload in 3 seconds."""
38 SETTINGS1
= """<h3>Your Settings have been saved.</h3> <br>
39 Your settings have been saved to the pyTivo.conf file. However you will
40 need to do a <b>Soft Reset</b> before these changes will take effect.<br>
41 The <a href="/TiVoConnect?Command=Admin&Container=%s">Admin</a> page
42 will reload in 10 seconds."""
44 SETTINGS2
= """<h3>Your Settings have been saved.</h3> <br>
45 Your settings have been saved to the pyTivo.conf file. pyTivo will now
46 do a <b>Soft Reset</b> to allow these changes to take effect.<br>
47 The <a href="/TiVoConnect?last_page=NPL&Command=Reset&Container=%s">Reset</a>
48 will occur in 2 seconds."""
50 TRANS_INIT
= """<h3>Transfer Initiated.</h3> <br>
51 You selected transfer has been initiated.<br>
52 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
53 will reload in 3 seconds."""
55 TRANS_STOP
= """<h3>Transfer Stopped.</h3> <br>
56 Your transfer has been stopped.<br>
57 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
58 will reload in 3 seconds."""
60 UNABLE
= """<h3>Unable to Connect to TiVo.</h3> <br>
61 pyTivo was unable to connect to the TiVo at %s</br>
62 This most likely caused by an incorrect Media Access Key. Please return
63 to the ToGo page and double check your Media Access Key.<br>
64 The <a href="/TiVoConnect?Command=NPL&Container=%s">ToGo</a> page will
65 reload in 20 seconds."""
67 # Preload the templates
68 trname
= os
.path
.join(SCRIPTDIR
, 'templates', 'redirect.tmpl')
69 tsname
= os
.path
.join(SCRIPTDIR
, 'templates', 'settings.tmpl')
70 tnname
= os
.path
.join(SCRIPTDIR
, 'templates', 'npl.tmpl')
71 REDIRECT_TEMPLATE
= file(trname
, 'rb').read()
72 SETTINGS_TEMPLATE
= file(tsname
, 'rb').read()
73 NPL_TEMPLATE
= file(tnname
, 'rb').read()
76 TRIBUNE_CR
= ' Copyright Tribune Media Services, Inc.'
78 status
= {} # Global variable to control download threads
79 tivo_cache
= {} # Cache of TiVo NPL
81 def tag_data(element
, tag
):
82 for name
in tag
.split('/'):
83 new_element
= element
.getElementsByTagName(name
)
86 element
= new_element
[0]
87 return element
.firstChild
.data
90 CONTENT_TYPE
= 'text/html'
92 def Reset(self
, handler
, query
):
94 handler
.server
.reset()
95 if 'last_page' in query
:
96 last_page
= query
['last_page'][0]
100 subcname
= query
['Container'][0]
101 cname
= subcname
.split('/')[0]
102 t
= Template(REDIRECT_TEMPLATE
)
105 t
.url
= '/TiVoConnect?Command='+ last_page
+'&Container=' + quote(cname
)
106 t
.text
= RESET_MSG
% (quote(last_page
), quote(cname
))
107 handler
.send_response(200)
108 handler
.end_headers()
109 handler
.wfile
.write(t
)
110 logging
.getLogger('pyTivo.admin').info('pyTivo has been soft reset.')
112 def Admin(self
, handler
, query
):
113 # Read config file new each time in case there was any outside edits
117 for section
in config
.config
.sections():
118 if not section
.startswith(('_tivo_', 'Server')):
119 if (not(config
.config
.has_option(section
, 'type')) or
120 config
.config
.get(section
, 'type').lower() != 'admin'):
121 shares_data
.append((section
,
122 dict(config
.config
.items(section
,
125 subcname
= query
['Container'][0]
126 cname
= subcname
.split('/')[0]
127 t
= Template(SETTINGS_TEMPLATE
)
130 t
.server_data
= dict(config
.config
.items('Server', raw
=True))
131 t
.server_known
= buildhelp
.getknown('server')
132 t
.shares_data
= shares_data
133 t
.shares_known
= buildhelp
.getknown('shares')
134 t
.tivos_data
= [(section
, dict(config
.config
.items(section
, raw
=True)))
135 for section
in config
.config
.sections()
136 if section
.startswith('_tivo_')]
137 t
.tivos_known
= buildhelp
.getknown('tivos')
138 t
.help_list
= buildhelp
.gethelp()
139 handler
.send_response(200)
140 handler
.end_headers()
141 handler
.wfile
.write(t
)
143 def UpdateSettings(self
, handler
, query
):
146 if key
.startswith('Server.'):
147 section
, option
= key
.split('.')
148 if option
== "new__setting":
149 new_setting
= query
[key
][0]
150 elif option
== "new__value":
151 new_value
= query
[key
][0]
152 elif query
[key
][0] == " ":
153 config
.config
.remove_option(section
, option
)
155 config
.config
.set(section
, option
, query
[key
][0])
156 if not(new_setting
== ' ' and new_value
== ' '):
157 config
.config
.set('Server', new_setting
, new_value
)
159 sections
= query
['Section_Map'][0].split(']')
160 sections
.pop() # last item is junk
161 for section
in sections
:
162 ID
, name
= section
.split('|')
163 if query
[ID
][0] == "Delete_Me":
164 config
.config
.remove_section(name
)
166 if query
[ID
][0] != name
:
167 config
.config
.remove_section(name
)
168 config
.config
.add_section(query
[ID
][0])
170 if key
.startswith(ID
+ '.'):
171 junk
, option
= key
.split('.')
172 if option
== "new__setting":
173 new_setting
= query
[key
][0]
174 elif option
== "new__value":
175 new_value
= query
[key
][0]
176 elif query
[key
][0] == " ":
177 config
.config
.remove_option(query
[ID
][0], option
)
179 config
.config
.set(query
[ID
][0], option
, query
[key
][0])
180 if not(new_setting
== ' ' and new_value
== ' '):
181 config
.config
.set(query
[ID
][0], new_setting
, new_value
)
182 if query
['new_Section'][0] != " ":
183 config
.config
.add_section(query
['new_Section'][0])
186 subcname
= query
['Container'][0]
187 cname
= subcname
.split('/')[0]
188 t
= Template(REDIRECT_TEMPLATE
)
191 t
.url
= '/TiVoConnect?Command=Admin&Container=' + quote(cname
)
192 t
.text
= SETTINGS1
% quote(cname
)
193 handler
.send_response(200)
194 handler
.end_headers()
195 handler
.wfile
.write(t
)
197 def NPL(self
, handler
, query
):
198 shows_per_page
= 50 # Change this to alter the number of shows returned
199 subcname
= query
['Container'][0]
200 cname
= subcname
.split('/')[0]
204 for name
, data
in config
.getShares():
206 tivo_mak
= data
.get('tivo_mak', '')
207 togo_path
= data
.get('togo_path', '')
210 tivoIP
= query
['TiVo'][0]
211 theurl
= ('https://' + tivoIP
+
212 '/TiVoConnect?Command=QueryContainer&ItemCount=' +
213 str(shows_per_page
) + '&Container=/NowPlaying')
214 if 'Folder' in query
:
215 folder
+= str(query
['Folder'][0])
216 theurl
+= '/' + folder
217 if 'AnchorItem' in query
:
218 AnchorItem
+= str(query
['AnchorItem'][0])
219 theurl
+= '&AnchorItem=' + quote(AnchorItem
)
220 if 'AnchorOffset' in query
:
221 AnchorOffset
+= str(query
['AnchorOffset'][0])
222 theurl
+= '&AnchorOffset=' + AnchorOffset
224 r
=urllib2
.Request(theurl
)
225 auth_handler
= urllib2
.HTTPDigestAuthHandler()
226 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', tivo_mak
)
227 opener
= urllib2
.build_opener(auth_handler
)
228 urllib2
.install_opener(opener
)
230 if theurl
in tivo_cache
: #check if we've accessed this page before
231 if (tivo_cache
[theurl
]['thepage'] == '' or
232 (time
.time() - tivo_cache
[theurl
]['thepage_time']) >= 60):
233 # if page is empty or old then retreive it
235 handle
= urllib2
.urlopen(r
)
237 t
= Template(REDIRECT_TEMPLATE
)
240 t
.url
= ('/TiVoConnect?Command=NPL&Container=' +
242 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
243 handler
.send_response(200)
244 handler
.end_headers()
245 handler
.wfile
.write(t
)
247 tivo_cache
[theurl
]['thepage'] = handle
.read()
248 tivo_cache
[theurl
]['thepage_time'] = time
.time()
251 handle
= urllib2
.urlopen(r
)
253 t
= Template(REDIRECT_TEMPLATE
)
256 t
.url
= '/TiVoConnect?Command=NPL&Container=' + quote(cname
)
257 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
258 handler
.send_response(200)
259 handler
.end_headers()
260 handler
.wfile
.write(t
)
262 tivo_cache
[theurl
] = {}
263 tivo_cache
[theurl
]['thepage'] = handle
.read()
264 tivo_cache
[theurl
]['thepage_time'] = time
.time()
266 xmldoc
= minidom
.parseString(tivo_cache
[theurl
]['thepage'])
267 items
= xmldoc
.getElementsByTagName('Item')
268 TotalItems
= tag_data(xmldoc
, 'Details/TotalItems')
269 ItemStart
= tag_data(xmldoc
, 'ItemStart')
270 ItemCount
= tag_data(xmldoc
, 'ItemCount')
271 FirstAnchor
= tag_data(items
[0], 'Links/Content/Url')
276 entry
['Title'] = tag_data(item
, 'Title')
277 entry
['ContentType'] = tag_data(item
, 'ContentType')
278 for tag
in ('CopyProtected', 'UniqueId'):
279 value
= tag_data(item
, tag
)
282 if entry
['ContentType'] == 'x-tivo-container/folder':
283 entry
['TotalItems'] = tag_data(item
, 'TotalItems')
284 lc
= int(tag_data(item
, 'LastChangeDate'), 16)
285 entry
['LastChangeDate'] = time
.strftime('%b %d, %Y',
288 icon
= tag_data(item
, 'Links/CustomIcon/Url')
291 url
= tag_data(item
, 'Links/Content/Url')
293 parse_url
= urlparse(url
)
294 entry
['Url'] = quote('http://%s%s?%s' %
295 (parse_url
[1].split(':')[0],
296 parse_url
[2], parse_url
[4]))
297 keys
= ('SourceSize', 'Duration', 'CaptureDate',
298 'EpisodeTitle', 'Description',
299 'SourceChannel', 'SourceStation')
301 entry
[key
] = tag_data(item
, key
)
303 entry
['SourceSize'] = ( '%.3f GB' %
304 (float(entry
['SourceSize']) / (1024 ** 3)) )
306 dur
= int(entry
['Duration']) / 1000
307 entry
['Duration'] = ( '%02d:%02d:%02d' %
308 (dur
/ 3600, (dur
% 3600) / 60, dur
% 60) )
310 entry
['CaptureDate'] = time
.strftime('%b %d, %Y',
311 time
.localtime(int(entry
['CaptureDate'], 16)))
313 desc
= entry
['Description']
314 entry
['Description'] = desc
.replace(TRIBUNE_CR
, '')
325 subcname
= query
['Container'][0]
326 cname
= subcname
.split('/')[0]
327 t
= Template(NPL_TEMPLATE
)
331 t
.tivo_mak
= tivo_mak
332 t
.togo_path
= togo_path
333 t
.tivos
= handler
.tivos
334 t
.tivo_names
= handler
.tivo_names
340 t
.TotalItems
= int(TotalItems
)
341 t
.ItemStart
= int(ItemStart
)
342 t
.ItemCount
= int(ItemCount
)
343 t
.FirstAnchor
= quote(FirstAnchor
)
344 t
.shows_per_page
= shows_per_page
345 handler
.send_response(200)
346 handler
.send_header('Content-Type', 'text/html; charset=UTF-8')
347 handler
.end_headers()
348 handler
.wfile
.write(unicode(t
).encode('utf-8'))
350 def get_tivo_file(self
, url
, mak
, tivoIP
, outfile
):
352 cj
= cookielib
.LWPCookieJar()
354 r
= urllib2
.Request(url
)
355 auth_handler
= urllib2
.HTTPDigestAuthHandler()
356 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', mak
)
357 opener
= urllib2
.build_opener(urllib2
.HTTPCookieProcessor(cj
),
359 urllib2
.install_opener(opener
)
362 handle
= urllib2
.urlopen(r
)
364 # If we get "Too many transfers error" try a second time.
365 # For some reason urllib2 does not properly close
366 # connections when a transfer is canceled.
369 handle
= urllib2
.urlopen(r
)
371 status
[url
]['running'] = False
372 status
[url
]['error'] = e
.code
375 status
[url
]['running'] = False
376 status
[url
]['error'] = e
.code
379 f
= open(outfile
, 'wb')
381 start_time
= time
.time()
382 output
= handle
.read(1024)
383 while status
[url
]['running'] and output
:
387 elapsed
= now
- start_time
389 status
[url
]['rate'] = int(kilobytes
/ elapsed
)
392 output
= handle
.read(1024)
393 status
[url
]['running'] = False
397 def ToGo(self
, handler
, query
):
398 subcname
= query
['Container'][0]
399 cname
= subcname
.split('/')[0]
400 tivoIP
= query
['TiVo'][0]
401 for name
, data
in config
.getShares():
403 tivo_mak
= data
.get('tivo_mak', '')
404 togo_path
= data
.get('togo_path', '')
405 t
= Template(REDIRECT_TEMPLATE
)
406 command
= query
['Redirect'][0]
407 params
= (command
, quote(cname
), tivoIP
)
408 if tivo_mak
and togo_path
:
409 parse_url
= urlparse(str(query
['Url'][0]))
410 theurl
= 'http://%s%s?%s' % (parse_url
[1].split(':')[0],
411 parse_url
[2], parse_url
[4])
412 name
= unquote(parse_url
[2])[10:300].split('.')
413 name
.insert(-1," - " + unquote(parse_url
[4]).split("id=")[1] + ".")
414 outfile
= os
.path
.join(togo_path
, "".join(name
))
416 status
[theurl
] = {'running': True, 'error': '', 'rate': '',
419 thread
.start_new_thread(Admin
.get_tivo_file
,
420 (self
, theurl
, tivo_mak
, tivoIP
, outfile
))
423 t
.text
= TRANS_INIT
% params
426 t
.text
= MISSING
% params
427 t
.url
= ('/TiVoConnect?Command=' + command
+ '&Container=' +
428 quote(cname
) + '&TiVo=' + tivoIP
)
429 handler
.send_response(200)
430 handler
.end_headers()
431 handler
.wfile
.write(t
)
433 def ToGoStop(self
, handler
, query
):
434 parse_url
= urlparse(str(query
['Url'][0]))
435 theurl
= 'http://%s%s?%s' % (parse_url
[1].split(':')[0],
436 parse_url
[2], parse_url
[4])
438 status
[theurl
]['running'] = False
440 subcname
= query
['Container'][0]
441 cname
= subcname
.split('/')[0]
442 tivoIP
= query
['TiVo'][0]
443 command
= query
['Redirect'][0]
444 t
= Template(REDIRECT_TEMPLATE
)
446 t
.url
= ('/TiVoConnect?Command=' + command
+ '&Container=' +
447 quote(cname
) + '&TiVo=' + tivoIP
)
448 t
.text
= TRANS_STOP
% (command
, quote(cname
), tivoIP
)
449 handler
.send_response(200)
450 handler
.end_headers()
451 handler
.wfile
.write(t
)
453 def SaveNPL(self
, handler
, query
):
455 if 'tivo_mak' in query
:
456 config
.config
.set(query
['Container'][0], 'tivo_mak',
457 query
['tivo_mak'][0])
458 if 'togo_path' in query
:
459 config
.config
.set(query
['Container'][0], 'togo_path',
460 query
['togo_path'][0])
463 subcname
= query
['Container'][0]
464 cname
= subcname
.split('/')[0]
465 t
= Template(REDIRECT_TEMPLATE
)
468 t
.url
= ('/TiVoConnect?last_page=NPL&Command=Reset&Container=' +
470 t
.text
= SETTINGS2
% quote(cname
)
471 handler
.send_response(200)
472 handler
.end_headers()
473 handler
.wfile
.write(t
)