More fiddly bits that probably no one else cares about.
[pyTivo/wgw.git] / plugins / admin / admin.py
blob5bcd9119eca6fdff5a931a3002cd71b65eb561e7
1 import ConfigParser
2 import cookielib
3 import os
4 import socket
5 import re
6 import sys
7 import thread
8 import time
9 import urllib2
10 from ConfigParser import NoOptionError
11 from urllib import unquote_plus, quote, unquote
12 from urlparse import urlparse
13 from xml.dom import minidom
14 from xml.sax.saxutils import escape
16 from lrucache import LRUCache
17 from Cheetah.Template import Template
18 import buildhelp
19 import config
20 import logging
21 from plugin import Plugin
23 SCRIPTDIR = os.path.dirname(__file__)
25 CLASS_NAME = 'Admin'
27 # Some error/status message templates
29 MISSING = """<h3>Missing Data.</h3> <br>
30 You must set both "tivo_mak" and "togo_path" before using this
31 function.<br>
32 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
33 will reload in 10 seconds."""
35 RESET_MSG = """<h3>The pyTivo Server has been soft reset.</h3> <br>
36 pyTivo has reloaded the pyTivo.conf file and all changes should now be
37 in effect. <br>
38 The <a href="/TiVoConnect?Command=%s&Container=%s">previous</a> page
39 will reload in 3 seconds."""
41 SETTINGS1 = """<h3>Your Settings have been saved.</h3> <br>
42 Your settings have been saved to the pyTivo.conf file. However you will
43 need to do a <b>Soft Reset</b> before these changes will take effect.<br>
44 The <a href="/TiVoConnect?Command=Admin&Container=%s">Admin</a> page
45 will reload in 10 seconds."""
47 SETTINGS2 = """<h3>Your Settings have been saved.</h3> <br>
48 Your settings have been saved to the pyTivo.conf file. pyTivo will now
49 do a <b>Soft Reset</b> to allow these changes to take effect.<br>
50 The <a href="/TiVoConnect?last_page=NPL&Command=Reset&Container=%s">Reset</a>
51 will occur in 2 seconds."""
53 TRANS_INIT = """<h3>Transfer Initiated.</h3> <br>
54 You selected transfer has been initiated.<br>
55 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
56 will reload in 3 seconds."""
58 TRANS_STOP = """<h3>Transfer Stopped.</h3> <br>
59 Your transfer has been stopped.<br>
60 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
61 will reload in 3 seconds."""
63 UNABLE = """<h3>Unable to Connect to TiVo.</h3> <br>
64 pyTivo was unable to connect to the TiVo at %s</br>
65 This most likely caused by an incorrect Media Access Key. Please return
66 to the ToGo page and double check your Media Access Key.<br>
67 The <a href="/TiVoConnect?Command=NPL&Container=%s">ToGo</a> page will
68 reload in 20 seconds."""
70 # Preload the templates
71 trname = os.path.join(SCRIPTDIR, 'templates', 'redirect.tmpl')
72 tsname = os.path.join(SCRIPTDIR, 'templates', 'settings.tmpl')
73 tnname = os.path.join(SCRIPTDIR, 'templates', 'npl.tmpl')
74 REDIRECT_TEMPLATE = file(trname, 'rb').read()
75 SETTINGS_TEMPLATE = file(tsname, 'rb').read()
76 NPL_TEMPLATE = file(tnname, 'rb').read()
78 # Something to strip
79 TRIBUNE_CR = ' Copyright Tribune Media Services, Inc.'
81 p = os.path.dirname(__file__)
82 p = p.split(os.path.sep)
83 p.pop()
84 p.pop()
85 p = os.path.sep.join(p)
86 config_file_path = os.path.join(p, 'pyTivo.conf')
88 status = {} # Global variable to control download threads
89 tivo_cache = {} # Cache of TiVo NPL
91 def tag_data(element, tag):
92 for name in tag.split('/'):
93 new_element = element.getElementsByTagName(name)
94 if not new_element:
95 return ''
96 element = new_element[0]
97 return element.firstChild.data
99 class Admin(Plugin):
100 CONTENT_TYPE = 'text/html'
102 def Reset(self, handler, query):
103 config.reset()
104 handler.server.reset()
105 if 'last_page' in query:
106 last_page = query['last_page'][0]
107 else:
108 last_page = 'Admin'
110 subcname = query['Container'][0]
111 cname = subcname.split('/')[0]
112 t = Template(REDIRECT_TEMPLATE)
113 t.container = cname
114 t.time = '3'
115 t.url = '/TiVoConnect?Command='+ last_page +'&Container=' + quote(cname)
116 t.text = RESET_MSG % (quote(last_page), quote(cname))
117 handler.send_response(200)
118 handler.end_headers()
119 handler.wfile.write(t)
120 logging.getLogger('pyTivo.admin').info('pyTivo has been soft reset.')
122 def Admin(self, handler, query):
123 # Read config file new each time in case there was any outside edits
124 config = ConfigParser.ConfigParser()
125 config.read(config_file_path)
127 shares_data = []
128 for section in config.sections():
129 if not section.startswith(('_tivo_', 'Server')):
130 if (not(config.has_option(section,'type')) or
131 config.get(section, 'type').lower() != 'admin'):
132 shares_data.append((section,
133 dict(config.items(section, raw=True))))
135 subcname = query['Container'][0]
136 cname = subcname.split('/')[0]
137 t = Template(SETTINGS_TEMPLATE)
138 t.container = cname
139 t.quote = quote
140 t.server_data = dict(config.items('Server', raw=True))
141 t.server_known = buildhelp.getknown('server')
142 t.shares_data = shares_data
143 t.shares_known = buildhelp.getknown('shares')
144 t.tivos_data = [(section, dict(config.items(section, raw=True)))
145 for section in config.sections()
146 if section.startswith('_tivo_')]
147 t.tivos_known = buildhelp.getknown('tivos')
148 t.help_list = buildhelp.gethelp()
149 handler.send_response(200)
150 handler.end_headers()
151 handler.wfile.write(t)
153 def UpdateSettings(self, handler, query):
154 config = ConfigParser.ConfigParser()
155 config.read(config_file_path)
156 for key in query:
157 if key.startswith('Server.'):
158 section, option = key.split('.')
159 if option == "new__setting":
160 new_setting = query[key][0]
161 elif option == "new__value":
162 new_value = query[key][0]
163 elif query[key][0] == " ":
164 config.remove_option(section, option)
165 else:
166 config.set(section, option, query[key][0])
167 if not(new_setting == ' ' and new_value == ' '):
168 config.set('Server', new_setting, new_value)
170 sections = query['Section_Map'][0].split(']')
171 sections.pop() # last item is junk
172 for section in sections:
173 ID, name = section.split('|')
174 if query[ID][0] == "Delete_Me":
175 config.remove_section(name)
176 continue
177 if query[ID][0] != name:
178 config.remove_section(name)
179 config.add_section(query[ID][0])
180 for key in query:
181 if key.startswith(ID + '.'):
182 junk, option = key.split('.')
183 if option == "new__setting":
184 new_setting = query[key][0]
185 elif option == "new__value":
186 new_value = query[key][0]
187 elif query[key][0] == " ":
188 config.remove_option(query[ID][0], option)
189 else:
190 config.set(query[ID][0], option, query[key][0])
191 if not(new_setting == ' ' and new_value == ' '):
192 config.set(query[ID][0], new_setting, new_value)
193 if query['new_Section'][0] != " ":
194 config.add_section(query['new_Section'][0])
195 f = open(config_file_path, "w")
196 config.write(f)
197 f.close()
199 subcname = query['Container'][0]
200 cname = subcname.split('/')[0]
201 t = Template(REDIRECT_TEMPLATE)
202 t.container = cname
203 t.time = '10'
204 t.url = '/TiVoConnect?Command=Admin&Container=' + quote(cname)
205 t.text = SETTINGS1 % quote(cname)
206 handler.send_response(200)
207 handler.end_headers()
208 handler.wfile.write(t)
210 def NPL(self, handler, query):
211 shows_per_page = 50 # Change this to alter the number of shows returned
212 subcname = query['Container'][0]
213 cname = subcname.split('/')[0]
214 folder = ''
215 AnchorItem = ''
216 AnchorOffset = ''
217 for name, data in config.getShares():
218 if cname == name:
219 tivo_mak = data.get('tivo_mak', '')
220 togo_path = data.get('togo_path', '')
222 if 'TiVo' in query:
223 tivoIP = query['TiVo'][0]
224 theurl = ('https://' + tivoIP +
225 '/TiVoConnect?Command=QueryContainer&ItemCount=' +
226 str(shows_per_page) + '&Container=/NowPlaying')
227 if 'Folder' in query:
228 folder += str(query['Folder'][0])
229 theurl += '/' + folder
230 if 'AnchorItem' in query:
231 AnchorItem += str(query['AnchorItem'][0])
232 theurl += '&AnchorItem=' + quote(AnchorItem)
233 if 'AnchorOffset' in query:
234 AnchorOffset += str(query['AnchorOffset'][0])
235 theurl += '&AnchorOffset=' + AnchorOffset
237 r=urllib2.Request(theurl)
238 auth_handler = urllib2.HTTPDigestAuthHandler()
239 auth_handler.add_password('TiVo DVR', tivoIP, 'tivo', tivo_mak)
240 opener = urllib2.build_opener(auth_handler)
241 urllib2.install_opener(opener)
243 if theurl in tivo_cache: #check if we've accessed this page before
244 if (tivo_cache[theurl]['thepage'] == '' or
245 (time.time() - tivo_cache[theurl]['thepage_time']) >= 60):
246 # if page is empty or old then retreive it
247 try:
248 handle = urllib2.urlopen(r)
249 except IOError, e:
250 t = Template(REDIRECT_TEMPLATE)
251 t.container = cname
252 t.time = '20'
253 t.url = ('/TiVoConnect?Command=NPL&Container=' +
254 quote(cname))
255 t.text = UNABLE % (tivoIP, quote(cname))
256 handler.send_response(200)
257 handler.end_headers()
258 handler.wfile.write(t)
259 return
260 tivo_cache[theurl]['thepage'] = handle.read()
261 tivo_cache[theurl]['thepage_time'] = time.time()
262 else: # not in cache
263 try:
264 handle = urllib2.urlopen(r)
265 except IOError, e:
266 t = Template(REDIRECT_TEMPLATE)
267 t.container = cname
268 t.time = '20'
269 t.url = '/TiVoConnect?Command=NPL&Container=' + quote(cname)
270 t.text = UNABLE % (tivoIP, quote(cname))
271 handler.send_response(200)
272 handler.end_headers()
273 handler.wfile.write(t)
274 return
275 tivo_cache[theurl] = {}
276 tivo_cache[theurl]['thepage'] = handle.read()
277 tivo_cache[theurl]['thepage_time'] = time.time()
279 xmldoc = minidom.parseString(tivo_cache[theurl]['thepage'])
280 items = xmldoc.getElementsByTagName('Item')
281 TotalItems = tag_data(xmldoc, 'Details/TotalItems')
282 ItemStart = tag_data(xmldoc, 'ItemStart')
283 ItemCount = tag_data(xmldoc, 'ItemCount')
284 FirstAnchor = tag_data(items[0], 'Links/Content/Url')
286 data = []
287 for item in items:
288 entry = {}
289 entry['Title'] = tag_data(item, 'Title')
290 entry['ContentType'] = tag_data(item, 'ContentType')
291 for tag in ('CopyProtected', 'UniqueId'):
292 value = tag_data(item, tag)
293 if value:
294 entry[tag] = value
295 if entry['ContentType'] == 'x-tivo-container/folder':
296 entry['TotalItems'] = tag_data(item, 'TotalItems')
297 lc = int(tag_data(item, 'LastChangeDate'), 16)
298 entry['LastChangeDate'] = time.strftime('%b %d, %Y',
299 time.localtime(lc))
300 else:
301 icon = tag_data(item, 'Links/CustomIcon/Url')
302 if icon:
303 entry['Icon'] = icon
304 url = tag_data(item, 'Links/Content/Url')
305 if url:
306 parse_url = urlparse(url)
307 entry['Url'] = quote('http://%s%s?%s' %
308 (parse_url[1].split(':')[0],
309 parse_url[2], parse_url[4]))
310 keys = ('SourceSize', 'Duration', 'CaptureDate',
311 'EpisodeTitle', 'Description',
312 'SourceChannel', 'SourceStation')
313 for key in keys:
314 entry[key] = tag_data(item, key)
316 entry['SourceSize'] = ( '%.3f GB' %
317 (float(entry['SourceSize']) / (1024 ** 3)) )
319 dur = int(entry['Duration']) / 1000
320 entry['Duration'] = ( '%02d:%02d:%02d' %
321 (dur / 3600, (dur % 3600) / 60, dur % 60) )
323 entry['CaptureDate'] = time.strftime('%b %d, %Y',
324 time.localtime(int(entry['CaptureDate'], 16)))
326 desc = entry['Description']
327 entry['Description'] = desc.replace(TRIBUNE_CR, '')
329 data.append(entry)
330 else:
331 data = []
332 tivoIP = ''
333 TotalItems = 0
334 ItemStart = 0
335 ItemCount = 0
336 FirstAnchor = ''
338 subcname = query['Container'][0]
339 cname = subcname.split('/')[0]
340 t = Template(NPL_TEMPLATE)
341 t.quote = quote
342 t.folder = folder
343 t.status = status
344 t.tivo_mak = tivo_mak
345 t.togo_path = togo_path
346 t.tivos = handler.tivos
347 t.tivo_names = handler.tivo_names
348 t.tivoIP = tivoIP
349 t.container = cname
350 t.data = data
351 t.unquote = unquote
352 t.len = len
353 t.TotalItems = int(TotalItems)
354 t.ItemStart = int(ItemStart)
355 t.ItemCount = int(ItemCount)
356 t.FirstAnchor = quote(FirstAnchor)
357 t.shows_per_page = shows_per_page
358 handler.send_response(200)
359 handler.send_header('Content-Type', 'text/html; charset=UTF-8')
360 handler.end_headers()
361 handler.wfile.write(unicode(t).encode('utf-8'))
363 def get_tivo_file(self, url, mak, tivoIP, outfile):
364 # global status
365 cj = cookielib.LWPCookieJar()
367 r = urllib2.Request(url)
368 auth_handler = urllib2.HTTPDigestAuthHandler()
369 auth_handler.add_password('TiVo DVR', tivoIP, 'tivo', mak)
370 opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),
371 auth_handler)
372 urllib2.install_opener(opener)
374 try:
375 handle = urllib2.urlopen(r)
376 except IOError, e:
377 # If we get "Too many transfers error" try a second time.
378 # For some reason urllib2 does not properly close
379 # connections when a transfer is canceled.
380 if e.code == 503:
381 try:
382 handle = urllib2.urlopen(r)
383 except IOError, e:
384 status[url]['running'] = False
385 status[url]['error'] = e.code
386 return
387 else:
388 status[url]['running'] = False
389 status[url]['error'] = e.code
390 return
392 f = open(outfile, 'wb')
393 kilobytes = 0
394 start_time = time.time()
395 output = handle.read(1024)
396 while status[url]['running'] and output:
397 kilobytes += 1
398 f.write(output)
399 now = time.time()
400 elapsed = now - start_time
401 if elapsed >= 5:
402 status[url]['rate'] = int(kilobytes / elapsed)
403 kilobytes = 0
404 start_time = now
405 output = handle.read(1024)
406 status[url]['running'] = False
407 handle.close()
408 f.close()
410 def ToGo(self, handler, query):
411 subcname = query['Container'][0]
412 cname = subcname.split('/')[0]
413 tivoIP = query['TiVo'][0]
414 for name, data in config.getShares():
415 if cname == name:
416 tivo_mak = data.get('tivo_mak', '')
417 togo_path = data.get('togo_path', '')
418 t = Template(REDIRECT_TEMPLATE)
419 command = query['Redirect'][0]
420 params = (command, quote(cname), tivoIP)
421 if tivo_mak and togo_path:
422 parse_url = urlparse(str(query['Url'][0]))
423 theurl = 'http://%s%s?%s' % (parse_url[1].split(':')[0],
424 parse_url[2], parse_url[4])
425 name = unquote(parse_url[2])[10:300].split('.')
426 name.insert(-1," - " + unquote(parse_url[4]).split("id=")[1] + ".")
427 outfile = os.path.join(togo_path, "".join(name))
429 status[theurl] = {'running': True, 'error': '', 'rate': '',
430 'finished': False}
432 thread.start_new_thread(Admin.get_tivo_file,
433 (self, theurl, tivo_mak, tivoIP, outfile))
435 t.time = '3'
436 t.text = TRANS_INIT % params
437 else:
438 t.time = '10'
439 t.text = MISSING % params
440 t.url = ('/TiVoConnect?Command=' + command + '&Container=' +
441 quote(cname) + '&TiVo=' + tivoIP)
442 handler.send_response(200)
443 handler.end_headers()
444 handler.wfile.write(t)
446 def ToGoStop(self, handler, query):
447 parse_url = urlparse(str(query['Url'][0]))
448 theurl = 'http://%s%s?%s' % (parse_url[1].split(':')[0],
449 parse_url[2], parse_url[4])
451 status[theurl]['running'] = False
453 subcname = query['Container'][0]
454 cname = subcname.split('/')[0]
455 tivoIP = query['TiVo'][0]
456 command = query['Redirect'][0]
457 t = Template(REDIRECT_TEMPLATE)
458 t.time = '3'
459 t.url = ('/TiVoConnect?Command=' + command + '&Container=' +
460 quote(cname) + '&TiVo=' + tivoIP)
461 t.text = TRANS_STOP % (command, quote(cname), tivoIP)
462 handler.send_response(200)
463 handler.end_headers()
464 handler.wfile.write(t)
466 def SaveNPL(self, handler, query):
467 config = ConfigParser.ConfigParser()
468 config.read(config_file_path)
469 if 'tivo_mak' in query:
470 config.set(query['Container'][0], 'tivo_mak',
471 query['tivo_mak'][0])
472 if 'togo_path' in query:
473 config.set(query['Container'][0], 'togo_path',
474 query['togo_path'][0])
475 f = open(config_file_path, "w")
476 config.write(f)
477 f.close()
479 subcname = query['Container'][0]
480 cname = subcname.split('/')[0]
481 t = Template(REDIRECT_TEMPLATE)
482 t.container = cname
483 t.time = '2'
484 t.url = ('/TiVoConnect?last_page=NPL&Command=Reset&Container=' +
485 quote(cname))
486 t.text = SETTINGS2 % quote(cname)
487 handler.send_response(200)
488 handler.end_headers()
489 handler.wfile.write(t)