1 import os
, socket
, re
, sys
, ConfigParser
, config
, time
2 import urllib2
, cookielib
, thread
, buildhelp
3 from xml
.dom
import minidom
4 from ConfigParser
import NoOptionError
5 from Cheetah
.Template
import Template
6 from plugin
import Plugin
7 from urllib
import unquote_plus
, quote
, unquote
8 from urlparse
import urlparse
9 from xml
.sax
.saxutils
import escape
10 from lrucache
import LRUCache
13 SCRIPTDIR
= os
.path
.dirname(__file__
)
17 # Some error/status message templates
19 MISSING
= """<h3>Missing Data.</h3> <br>
20 You must set both "tivo_mak" and "togo_path" before using this
22 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
23 will reload in 10 seconds."""
25 RESET_MSG
= """<h3>The pyTivo Server has been soft reset.</h3> <br>
26 pyTivo has reloaded the pyTivo.conf file and all changes should now be
28 The <a href="/TiVoConnect?Command=%s&Container=%s">previous</a> page
29 will reload in 3 seconds."""
31 SETTINGS1
= """<h3>Your Settings have been saved.</h3> <br>
32 Your settings have been saved to the pyTivo.conf file. However you will
33 need to do a <b>Soft Reset</b> before these changes will take effect.<br>
34 The <a href="/TiVoConnect?Command=Admin&Container=%s">Admin</a> page
35 will reload in 10 seconds."""
37 SETTINGS2
= """<h3>Your Settings have been saved.</h3> <br>
38 Your settings have been saved to the pyTivo.conf file. pyTivo will now
39 do a <b>Soft Reset</b> to allow these changes to take effect.<br>
40 The <a href="/TiVoConnect?last_page=NPL&Command=Reset&Container=%s">Reset</a>
41 will occur in 2 seconds."""
43 TRANS_INIT
= """<h3>Transfer Initiated.</h3> <br>
44 You selected transfer has been initiated.<br>
45 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
46 will reload in 3 seconds."""
48 TRANS_STOP
= """<h3>Transfer Stopped.</h3> <br>
49 Your transfer has been stopped.<br>
50 The <a href="/TiVoConnect?Command=%s&Container=%s&TiVo=%s">ToGo</a> page
51 will reload in 3 seconds."""
53 UNABLE
= """<h3>Unable to Connect to TiVo.</h3> <br>
54 pyTivo was unable to connect to the TiVo at %s</br>
55 This most likely caused by an incorrect Media Access Key. Please return
56 to the ToGo page and double check your Media Access Key.<br>
57 The <a href="/TiVoConnect?Command=NPL&Container=%s">ToGo</a> page will
58 reload in 20 seconds."""
60 # Preload the templates
61 trname
= os
.path
.join(SCRIPTDIR
, 'templates', 'redirect.tmpl')
62 tsname
= os
.path
.join(SCRIPTDIR
, 'templates', 'settings.tmpl')
63 tnname
= os
.path
.join(SCRIPTDIR
, 'templates', 'npl.tmpl')
64 REDIRECT_TEMPLATE
= file(trname
, 'rb').read()
65 SETTINGS_TEMPLATE
= file(tsname
, 'rb').read()
66 NPL_TEMPLATE
= file(tnname
, 'rb').read()
69 TRIBUNE_CR
= ' Copyright Tribune Media Services, Inc.'
71 p
= os
.path
.dirname(__file__
)
72 p
= p
.split(os
.path
.sep
)
75 p
= os
.path
.sep
.join(p
)
76 config_file_path
= os
.path
.join(p
, 'pyTivo.conf')
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 handler
.send_response(200)
103 handler
.end_headers()
104 t
= Template(REDIRECT_TEMPLATE
)
107 t
.url
= '/TiVoConnect?Command='+ last_page
+'&Container=' + quote(cname
)
108 t
.text
= RESET_MSG
% (quote(last_page
), quote(cname
))
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
114 config
= ConfigParser
.ConfigParser()
115 config
.read(config_file_path
)
118 for section
in config
.sections():
119 if not(section
.startswith('_tivo_') or
120 section
.startswith('Server')):
121 if (not(config
.has_option(section
,'type')) or
122 config
.get(section
, 'type').lower() != 'admin'):
123 shares_data
.append((section
,
124 dict(config
.items(section
, raw
=True))))
126 subcname
= query
['Container'][0]
127 cname
= subcname
.split('/')[0]
128 handler
.send_response(200)
129 handler
.end_headers()
130 t
= Template(SETTINGS_TEMPLATE
)
133 t
.server_data
= dict(config
.items('Server', raw
=True))
134 t
.server_known
= buildhelp
.getknown('server')
135 t
.shares_data
= shares_data
136 t
.shares_known
= buildhelp
.getknown('shares')
137 t
.tivos_data
= [(section
, dict(config
.items(section
, raw
=True)))
138 for section
in config
.sections()
139 if section
.startswith('_tivo_')]
140 t
.tivos_known
= buildhelp
.getknown('tivos')
141 t
.help_list
= buildhelp
.gethelp()
142 handler
.wfile
.write(t
)
144 def UpdateSettings(self
, handler
, query
):
145 config
= ConfigParser
.ConfigParser()
146 config
.read(config_file_path
)
148 if key
.startswith('Server.'):
149 section
, option
= key
.split('.')
150 if option
== "new__setting":
151 new_setting
= query
[key
][0]
152 elif option
== "new__value":
153 new_value
= query
[key
][0]
154 elif query
[key
][0] == " ":
155 config
.remove_option(section
, option
)
157 config
.set(section
, option
, query
[key
][0])
158 if not(new_setting
== ' ' and new_value
== ' '):
159 config
.set('Server', new_setting
, new_value
)
161 sections
= query
['Section_Map'][0].split(']')
162 sections
.pop() #last item is junk
163 for section
in sections
:
164 ID
, name
= section
.split('|')
165 if query
[ID
][0] == "Delete_Me":
166 config
.remove_section(name
)
168 if query
[ID
][0] != name
:
169 config
.remove_section(name
)
170 config
.add_section(query
[ID
][0])
172 if key
.startswith(ID
+ '.'):
173 junk
, option
= key
.split('.')
174 if option
== "new__setting":
175 new_setting
= query
[key
][0]
176 elif option
== "new__value":
177 new_value
= query
[key
][0]
178 elif query
[key
][0] == " ":
179 config
.remove_option(query
[ID
][0], option
)
181 config
.set(query
[ID
][0], option
, query
[key
][0])
182 if not(new_setting
== ' ' and new_value
== ' '):
183 config
.set(query
[ID
][0], new_setting
, new_value
)
184 if query
['new_Section'][0] != " ":
185 config
.add_section(query
['new_Section'][0])
186 f
= open(config_file_path
, "w")
190 subcname
= query
['Container'][0]
191 cname
= subcname
.split('/')[0]
192 handler
.send_response(200)
193 handler
.end_headers()
194 t
= Template(REDIRECT_TEMPLATE
)
197 t
.url
= '/TiVoConnect?Command=Admin&Container=' + quote(cname
)
198 t
.text
= SETTINGS1
% quote(cname
)
199 handler
.wfile
.write(t
)
201 def NPL(self
, handler
, query
):
202 shows_per_page
= 50 #Change this to alter the number of shows returned
203 subcname
= query
['Container'][0]
204 cname
= subcname
.split('/')[0]
208 for name
, data
in config
.getShares():
210 tivo_mak
= data
.get('tivo_mak', '')
211 togo_path
= data
.get('togo_path', '')
214 tivoIP
= query
['TiVo'][0]
215 theurl
= 'https://' + tivoIP
+ \
216 '/TiVoConnect?Command=QueryContainer&ItemCount=' + \
217 str(shows_per_page
) + '&Container=/NowPlaying'
218 if 'Folder' in query
:
219 folder
+= str(query
['Folder'][0])
220 theurl
+= '/' + folder
221 if 'AnchorItem' in query
:
222 AnchorItem
+= str(query
['AnchorItem'][0])
223 theurl
+= '&AnchorItem=' + quote(AnchorItem
)
224 if 'AnchorOffset' in query
:
225 AnchorOffset
+= str(query
['AnchorOffset'][0])
226 theurl
+= '&AnchorOffset=' + AnchorOffset
228 r
=urllib2
.Request(theurl
)
229 auth_handler
= urllib2
.HTTPDigestAuthHandler()
230 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', tivo_mak
)
231 opener
= urllib2
.build_opener(auth_handler
)
232 urllib2
.install_opener(opener
)
234 if theurl
in tivo_cache
: #check if we've accessed this page before
235 if (tivo_cache
[theurl
]['thepage'] == '' or
236 (time
.time() - tivo_cache
[theurl
]['thepage_time']) >= 60):
237 #if page is empty or old then retreive it
239 handle
= urllib2
.urlopen(r
)
241 handler
.send_response(200)
242 handler
.end_headers()
243 t
= Template(REDIRECT_TEMPLATE
)
246 t
.url
= '/TiVoConnect?Command=NPL&Container=' + \
248 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
249 handler
.wfile
.write(t
)
251 tivo_cache
[theurl
]['thepage'] = handle
.read()
252 tivo_cache
[theurl
]['thepage_time'] = time
.time()
255 handle
= urllib2
.urlopen(r
)
257 handler
.send_response(200)
258 handler
.end_headers()
259 t
= Template(REDIRECT_TEMPLATE
)
262 t
.url
= '/TiVoConnect?Command=NPL&Container=' + quote(cname
)
263 t
.text
= UNABLE
% (tivoIP
, quote(cname
))
264 handler
.wfile
.write(t
)
266 tivo_cache
[theurl
] = {}
267 tivo_cache
[theurl
]['thepage'] = handle
.read()
268 tivo_cache
[theurl
]['thepage_time'] = time
.time()
270 xmldoc
= minidom
.parseString(tivo_cache
[theurl
]['thepage'])
271 items
= xmldoc
.getElementsByTagName('Item')
272 TotalItems
= tag_data(xmldoc
, 'Details/TotalItems')
273 ItemStart
= tag_data(xmldoc
, 'ItemStart')
274 ItemCount
= tag_data(xmldoc
, 'ItemCount')
275 FirstAnchor
= tag_data(items
[0], 'Links/Content/Url')
280 entry
['Title'] = tag_data(item
, 'Title')
281 entry
['ContentType'] = tag_data(item
, 'ContentType')
282 for tag
in ('CopyProtected', 'UniqueId'):
283 value
= tag_data(item
, tag
)
286 if entry
['ContentType'] == 'x-tivo-container/folder':
287 entry
['TotalItems'] = tag_data(item
, 'TotalItems')
288 lc
= int(tag_data(item
, 'LastChangeDate'), 16)
289 entry
['LastChangeDate'] = time
.strftime('%b %d, %Y',
292 icon
= tag_data(item
, 'Links/CustomIcon/Url')
295 url
= tag_data(item
, 'Links/Content/Url')
297 parse_url
= urlparse(url
)
298 entry
['Url'] = quote('http://%s%s?%s' %
299 (parse_url
[1].split(':')[0],
300 parse_url
[2], parse_url
[4]))
301 keys
= ('SourceSize', 'Duration', 'CaptureDate',
302 'EpisodeTitle', 'Description',
303 'SourceChannel', 'SourceStation')
305 entry
[key
] = tag_data(item
, key
)
307 entry
['SourceSize'] = ( '%.3f GB' %
308 (float(entry
['SourceSize']) / (1024 ** 3)) )
310 dur
= int(entry
['Duration']) / 1000
311 entry
['Duration'] = ( '%02d:%02d:%02d' %
312 (dur
/ 3600, (dur
% 3600) / 60, dur
% 60) )
314 entry
['CaptureDate'] = time
.strftime('%b %d, %Y',
315 time
.localtime(int(entry
['CaptureDate'], 16)))
317 desc
= entry
['Description']
318 entry
['Description'] = desc
.replace(TRIBUNE_CR
, '')
329 subcname
= query
['Container'][0]
330 cname
= subcname
.split('/')[0]
331 handler
.send_response(200)
332 handler
.send_header('Content-Type', 'text/html; charset=UTF-8')
333 handler
.end_headers()
334 t
= Template(NPL_TEMPLATE
)
338 t
.tivo_mak
= tivo_mak
339 t
.togo_path
= togo_path
340 t
.tivos
= handler
.tivos
341 t
.tivo_names
= handler
.tivo_names
347 t
.TotalItems
= int(TotalItems
)
348 t
.ItemStart
= int(ItemStart
)
349 t
.ItemCount
= int(ItemCount
)
350 t
.FirstAnchor
= quote(FirstAnchor
)
351 t
.shows_per_page
= shows_per_page
352 handler
.wfile
.write(unicode(t
).encode('utf-8'))
354 def get_tivo_file(self
, url
, mak
, tivoIP
, outfile
):
356 cj
= cookielib
.LWPCookieJar()
358 r
=urllib2
.Request(url
)
359 auth_handler
= urllib2
.HTTPDigestAuthHandler()
360 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', mak
)
361 opener
= urllib2
.build_opener(urllib2
.HTTPCookieProcessor(cj
),
363 urllib2
.install_opener(opener
)
366 handle
= urllib2
.urlopen(r
)
368 #If we get "Too many transfers error" try a second time.
369 #For some reason urllib2 does not properly close connections
370 #when a transfer is canceled.
373 handle
= urllib2
.urlopen(r
)
375 status
[url
]['running'] = False
376 status
[url
]['error'] = e
.code
379 status
[url
]['running'] = False
380 status
[url
]['error'] = e
.code
383 f
= open(outfile
, 'wb')
385 start_time
= time
.time()
386 output
= handle
.read(1024)
387 while status
[url
]['running'] and output
!= '':
390 if ((time
.time() - start_time
) >= 5):
391 status
[url
]['rate'] = int(kilobytes
/(time
.time() - start_time
))
393 start_time
= time
.time()
394 output
= handle
.read(1024)
395 status
[url
]['running'] = False
400 def ToGo(self
, handler
, query
):
401 subcname
= query
['Container'][0]
402 cname
= subcname
.split('/')[0]
403 tivoIP
= query
['TiVo'][0]
404 for name
, data
in config
.getShares():
406 tivo_mak
= data
.get('tivo_mak', '')
407 togo_path
= data
.get('togo_path', '')
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
))
422 handler
.send_response(200)
423 handler
.end_headers()
424 t
= Template(REDIRECT_TEMPLATE
)
425 command
= query
['Redirect'][0]
427 t
.url
= '/TiVoConnect?Command=' + command
+ '&Container=' + \
428 quote(cname
) + '&TiVo=' + tivoIP
429 t
.text
= TRANS_INIT
% (command
, quote(cname
), tivoIP
)
430 handler
.wfile
.write(t
)
432 handler
.send_response(200)
433 handler
.end_headers()
434 t
= Template(REDIRECT_TEMPLATE
)
435 command
= query
['Redirect'][0]
437 t
.url
= '/TiVoConnect?Command=' + command
+ '&Container=' + \
438 quote(cname
) + '&TiVo=' + tivoIP
439 t
.text
= MISSING
% (command
, quote(cname
), tivoIP
)
440 handler
.wfile
.write(t
)
442 def ToGoStop(self
, handler
, query
):
443 parse_url
= urlparse(str(query
['Url'][0]))
444 theurl
= 'http://%s%s?%s' % (parse_url
[1].split(':')[0],
445 parse_url
[2], parse_url
[4])
447 status
[theurl
]['running'] = False
449 subcname
= query
['Container'][0]
450 cname
= subcname
.split('/')[0]
451 tivoIP
= query
['TiVo'][0]
452 command
= query
['Redirect'][0]
453 handler
.send_response(200)
454 handler
.end_headers()
455 t
= Template(REDIRECT_TEMPLATE
)
457 t
.url
= '/TiVoConnect?Command=' + command
+ '&Container=' + \
458 quote(cname
) + '&TiVo=' + tivoIP
459 t
.text
= TRANS_STOP
% (command
, quote(cname
), tivoIP
)
460 handler
.wfile
.write(t
)
463 def SaveNPL(self
, handler
, query
):
464 config
= ConfigParser
.ConfigParser()
465 config
.read(config_file_path
)
466 if 'tivo_mak' in query
:
467 config
.set(query
['Container'][0], 'tivo_mak',
468 query
['tivo_mak'][0])
469 if 'togo_path' in query
:
470 config
.set(query
['Container'][0], 'togo_path',
471 query
['togo_path'][0])
472 f
= open(config_file_path
, "w")
476 subcname
= query
['Container'][0]
477 cname
= subcname
.split('/')[0]
478 handler
.send_response(200)
479 handler
.end_headers()
480 t
= Template(REDIRECT_TEMPLATE
)
483 t
.url
= '/TiVoConnect?last_page=NPL&Command=Reset&Container=' + \
485 t
.text
= SETTINGS2
% quote(cname
)
486 handler
.wfile
.write(t
)