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 t
= Template(REDIRECT_TEMPLATE
)
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
)
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
)
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
)
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 t
= Template(REDIRECT_TEMPLATE
)
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]
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 t
= Template(REDIRECT_TEMPLATE
)
253 t
.url
= ('/TiVoConnect?Command=NPL&Container=' +
255 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
256 handler
.send_response(200)
257 handler
.end_headers()
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 t
= Template(REDIRECT_TEMPLATE
)
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
)
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 t
= Template(NPL_TEMPLATE
)
344 t
.tivo_mak
= tivo_mak
345 t
.togo_path
= togo_path
346 t
.tivos
= handler
.tivos
347 t
.tivo_names
= handler
.tivo_names
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
):
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
379 # connections 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
:
400 elapsed
= now
- start_time
402 status
[url
]['rate'] = int(kilobytes
/ elapsed
)
405 output
= handle
.read(1024)
406 status
[url
]['running'] = False
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():
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': '',
432 thread
.start_new_thread(Admin
.get_tivo_file
,
433 (self
, theurl
, tivo_mak
, tivoIP
, outfile
))
436 t
.text
= TRANS_INIT
% params
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
)
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")
479 subcname
= query
['Container'][0]
480 cname
= subcname
.split('/')[0]
481 t
= Template(REDIRECT_TEMPLATE
)
484 t
.url
= ('/TiVoConnect?last_page=NPL&Command=Reset&Container=' +
486 t
.text
= SETTINGS2
% quote(cname
)
487 handler
.send_response(200)
488 handler
.end_headers()
489 handler
.wfile
.write(t
)