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
21 from plugin
import Plugin
23 SCRIPTDIR
= os
.path
.dirname(__file__
)
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
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
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()
79 TRIBUNE_CR
= ' Copyright Tribune Media Services, Inc.'
81 p
= os
.path
.dirname(__file__
)
82 p
= p
.split(os
.path
.sep
)
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
)
96 element
= new_element
[0]
97 return element
.firstChild
.data
100 CONTENT_TYPE
= 'text/html'
102 def Reset(self
, handler
, query
):
104 handler
.server
.reset()
105 if 'last_page' in query
:
106 last_page
= query
['last_page'][0]
110 subcname
= query
['Container'][0]
111 cname
= subcname
.split('/')[0]
112 handler
.send_response(200)
113 handler
.end_headers()
114 t
= Template(REDIRECT_TEMPLATE
)
117 t
.url
= '/TiVoConnect?Command='+ last_page
+'&Container=' + quote(cname
)
118 t
.text
= RESET_MSG
% (quote(last_page
), quote(cname
))
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
)
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 handler
.send_response(200)
138 handler
.end_headers()
139 t
= Template(SETTINGS_TEMPLATE
)
142 t
.server_data
= dict(config
.items('Server', raw
=True))
143 t
.server_known
= buildhelp
.getknown('server')
144 t
.shares_data
= shares_data
145 t
.shares_known
= buildhelp
.getknown('shares')
146 t
.tivos_data
= [(section
, dict(config
.items(section
, raw
=True)))
147 for section
in config
.sections()
148 if section
.startswith('_tivo_')]
149 t
.tivos_known
= buildhelp
.getknown('tivos')
150 t
.help_list
= buildhelp
.gethelp()
151 handler
.wfile
.write(t
)
153 def UpdateSettings(self
, handler
, query
):
154 config
= ConfigParser
.ConfigParser()
155 config
.read(config_file_path
)
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
)
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
)
177 if query
[ID
][0] != name
:
178 config
.remove_section(name
)
179 config
.add_section(query
[ID
][0])
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
)
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")
199 subcname
= query
['Container'][0]
200 cname
= subcname
.split('/')[0]
201 handler
.send_response(200)
202 handler
.end_headers()
203 t
= Template(REDIRECT_TEMPLATE
)
206 t
.url
= '/TiVoConnect?Command=Admin&Container=' + quote(cname
)
207 t
.text
= SETTINGS1
% quote(cname
)
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]
217 for name
, data
in config
.getShares():
219 tivo_mak
= data
.get('tivo_mak', '')
220 togo_path
= data
.get('togo_path', '')
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
248 handle
= urllib2
.urlopen(r
)
250 handler
.send_response(200)
251 handler
.end_headers()
252 t
= Template(REDIRECT_TEMPLATE
)
255 t
.url
= '/TiVoConnect?Command=NPL&Container=' + \
257 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
258 handler
.wfile
.write(t
)
260 tivo_cache
[theurl
]['thepage'] = handle
.read()
261 tivo_cache
[theurl
]['thepage_time'] = time
.time()
264 handle
= urllib2
.urlopen(r
)
266 handler
.send_response(200)
267 handler
.end_headers()
268 t
= Template(REDIRECT_TEMPLATE
)
271 t
.url
= '/TiVoConnect?Command=NPL&Container=' + quote(cname
)
272 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
273 handler
.wfile
.write(t
)
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')
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
)
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',
301 icon
= tag_data(item
, 'Links/CustomIcon/Url')
304 url
= tag_data(item
, 'Links/Content/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')
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
, '')
338 subcname
= query
['Container'][0]
339 cname
= subcname
.split('/')[0]
340 handler
.send_response(200)
341 handler
.send_header('Content-Type', 'text/html; charset=UTF-8')
342 handler
.end_headers()
343 t
= Template(NPL_TEMPLATE
)
347 t
.tivo_mak
= tivo_mak
348 t
.togo_path
= togo_path
349 t
.tivos
= handler
.tivos
350 t
.tivo_names
= handler
.tivo_names
356 t
.TotalItems
= int(TotalItems
)
357 t
.ItemStart
= int(ItemStart
)
358 t
.ItemCount
= int(ItemCount
)
359 t
.FirstAnchor
= quote(FirstAnchor
)
360 t
.shows_per_page
= shows_per_page
361 handler
.wfile
.write(unicode(t
).encode('utf-8'))
363 def get_tivo_file(self
, url
, mak
, tivoIP
, outfile
):
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
),
372 urllib2
.install_opener(opener
)
375 handle
= urllib2
.urlopen(r
)
377 #If we get "Too many transfers error" try a second time.
378 #For some reason urllib2 does not properly close connections
379 #when a transfer is canceled.
382 handle
= urllib2
.urlopen(r
)
384 status
[url
]['running'] = False
385 status
[url
]['error'] = e
.code
388 status
[url
]['running'] = False
389 status
[url
]['error'] = e
.code
392 f
= open(outfile
, 'wb')
394 start_time
= time
.time()
395 output
= handle
.read(1024)
396 while status
[url
]['running'] and output
!= '':
399 if ((time
.time() - start_time
) >= 5):
400 status
[url
]['rate'] = int(kilobytes
/(time
.time() - start_time
))
402 start_time
= time
.time()
403 output
= handle
.read(1024)
404 status
[url
]['running'] = False
409 def ToGo(self
, handler
, query
):
410 subcname
= query
['Container'][0]
411 cname
= subcname
.split('/')[0]
412 tivoIP
= query
['TiVo'][0]
413 for name
, data
in config
.getShares():
415 tivo_mak
= data
.get('tivo_mak', '')
416 togo_path
= data
.get('togo_path', '')
417 if tivo_mak
and togo_path
:
418 parse_url
= urlparse(str(query
['Url'][0]))
419 theurl
= 'http://%s%s?%s' % (parse_url
[1].split(':')[0],
420 parse_url
[2], parse_url
[4])
421 name
= unquote(parse_url
[2])[10:300].split('.')
422 name
.insert(-1," - " + unquote(parse_url
[4]).split("id=")[1] + ".")
423 outfile
= os
.path
.join(togo_path
, "".join(name
))
425 status
[theurl
] = {'running': True, 'error': '', 'rate': '',
428 thread
.start_new_thread(Admin
.get_tivo_file
,
429 (self
, theurl
, tivo_mak
, tivoIP
, outfile
))
431 handler
.send_response(200)
432 handler
.end_headers()
433 t
= Template(REDIRECT_TEMPLATE
)
434 command
= query
['Redirect'][0]
436 t
.url
= '/TiVoConnect?Command=' + command
+ '&Container=' + \
437 quote(cname
) + '&TiVo=' + tivoIP
438 t
.text
= TRANS_INIT
% (command
, quote(cname
), tivoIP
)
439 handler
.wfile
.write(t
)
441 handler
.send_response(200)
442 handler
.end_headers()
443 t
= Template(REDIRECT_TEMPLATE
)
444 command
= query
['Redirect'][0]
446 t
.url
= '/TiVoConnect?Command=' + command
+ '&Container=' + \
447 quote(cname
) + '&TiVo=' + tivoIP
448 t
.text
= MISSING
% (command
, quote(cname
), tivoIP
)
449 handler
.wfile
.write(t
)
451 def ToGoStop(self
, handler
, query
):
452 parse_url
= urlparse(str(query
['Url'][0]))
453 theurl
= 'http://%s%s?%s' % (parse_url
[1].split(':')[0],
454 parse_url
[2], parse_url
[4])
456 status
[theurl
]['running'] = False
458 subcname
= query
['Container'][0]
459 cname
= subcname
.split('/')[0]
460 tivoIP
= query
['TiVo'][0]
461 command
= query
['Redirect'][0]
462 handler
.send_response(200)
463 handler
.end_headers()
464 t
= Template(REDIRECT_TEMPLATE
)
466 t
.url
= '/TiVoConnect?Command=' + command
+ '&Container=' + \
467 quote(cname
) + '&TiVo=' + tivoIP
468 t
.text
= TRANS_STOP
% (command
, quote(cname
), tivoIP
)
469 handler
.wfile
.write(t
)
472 def SaveNPL(self
, handler
, query
):
473 config
= ConfigParser
.ConfigParser()
474 config
.read(config_file_path
)
475 if 'tivo_mak' in query
:
476 config
.set(query
['Container'][0], 'tivo_mak',
477 query
['tivo_mak'][0])
478 if 'togo_path' in query
:
479 config
.set(query
['Container'][0], 'togo_path',
480 query
['togo_path'][0])
481 f
= open(config_file_path
, "w")
485 subcname
= query
['Container'][0]
486 cname
= subcname
.split('/')[0]
487 handler
.send_response(200)
488 handler
.end_headers()
489 t
= Template(REDIRECT_TEMPLATE
)
492 t
.url
= '/TiVoConnect?last_page=NPL&Command=Reset&Container=' + \
494 t
.text
= SETTINGS2
% quote(cname
)
495 handler
.wfile
.write(t
)