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 p
= os
.path
.dirname(__file__
)
18 p
= p
.split(os
.path
.sep
)
21 p
= os
.path
.sep
.join(p
)
22 config_file_path
= os
.path
.join(p
, 'pyTivo.conf')
24 status
= {} #Global variable to control download threads
25 tivo_cache
= {} #Cache of TiVo NPL
28 CONTENT_TYPE
= 'text/html'
30 def Reset(self
, handler
, query
):
32 handler
.server
.reset()
33 if 'last_page' in query
:
34 last_page
= query
['last_page'][0]
38 subcname
= query
['Container'][0]
39 cname
= subcname
.split('/')[0]
40 handler
.send_response(200)
42 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
45 t
.url
= '/TiVoConnect?Command='+ last_page
+'&Container=' + quote(cname
)
46 t
.text
= '<h3>The pyTivo Server has been soft reset.</h3> <br>pyTivo has reloaded the pyTivo.conf'+\
47 'file and all changed should now be in effect. <br> The'+ \
48 '<a href="/TiVoConnect?Command='+ quote(last_page
) +'&Container='+ quote(cname
) +'"> previous</a> page will reload in 3 seconds.'
49 handler
.wfile
.write(t
)
50 debug
.debug_write(__name__
, debug
.fn_attr(), ['The pyTivo Server has been soft reset.'])
51 debug
.print_conf(__name__
, debug
.fn_attr())
53 def Admin(self
, handler
, query
):
54 #Read config file new each time in case there was any outside edits
55 config
= ConfigParser
.ConfigParser()
56 config
.read(config_file_path
)
59 for section
in config
.sections():
60 if not(section
.startswith('_tivo_') or section
.startswith('Server')):
61 if not(config
.has_option(section
,'type')):
62 shares_data
.append((section
, dict(config
.items(section
, raw
=True))))
63 elif config
.get(section
,'type').lower() != 'admin':
64 shares_data
.append((section
, dict(config
.items(section
, raw
=True))))
66 subcname
= query
['Container'][0]
67 cname
= subcname
.split('/')[0]
68 handler
.send_response(200)
70 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'settings.tmpl'))
73 t
.server_data
= dict(config
.items('Server', raw
=True))
74 t
.server_known
= buildhelp
.getknown('server')
75 t
.shares_data
= shares_data
76 t
.shares_known
= buildhelp
.getknown('shares')
77 t
.tivos_data
= [ (section
, dict(config
.items(section
, raw
=True))) for section
in config
.sections() \
78 if section
.startswith('_tivo_')]
79 t
.tivos_known
= buildhelp
.getknown('tivos')
80 t
.help_list
= buildhelp
.gethelp()
81 handler
.wfile
.write(t
)
83 def UpdateSettings(self
, handler
, query
):
84 config
= ConfigParser
.ConfigParser()
85 config
.read(config_file_path
)
87 if key
.startswith('Server.'):
88 section
, option
= key
.split('.')
89 if option
== "new__setting":
90 new_setting
= query
[key
][0]
92 if option
== "new__value":
93 new_value
= query
[key
][0]
95 if query
[key
][0] == " ":
96 config
.remove_option(section
, option
)
98 config
.set(section
, option
, query
[key
][0])
99 if not(new_setting
== ' ' and new_value
== ' '):
100 config
.set('Server', new_setting
, new_value
)
102 sections
= query
['Section_Map'][0].split(']')
103 sections
.pop() #last item is junk
104 for section
in sections
:
105 ID
, name
= section
.split('|')
106 if query
[ID
][0] == "Delete_Me":
107 config
.remove_section(name
)
109 if query
[ID
][0] != name
:
110 config
.remove_section(name
)
111 config
.add_section(query
[ID
][0])
113 if key
.startswith(ID
+ '.'):
114 junk
, option
= key
.split('.')
115 if option
== "new__setting":
116 new_setting
= query
[key
][0]
118 if option
== "new__value":
119 new_value
= query
[key
][0]
121 if query
[key
][0] == " ":
122 config
.remove_option(query
[ID
][0], option
)
124 config
.set(query
[ID
][0], option
, query
[key
][0])
125 if not(new_setting
== ' ' and new_value
== ' '):
126 config
.set(query
[ID
][0], new_setting
, new_value
)
127 if query
['new_Section'][0] != " ":
128 config
.add_section(query
['new_Section'][0])
129 f
= open(config_file_path
, "w")
133 subcname
= query
['Container'][0]
134 cname
= subcname
.split('/')[0]
135 handler
.send_response(200)
136 handler
.end_headers()
137 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
140 t
.url
= '/TiVoConnect?Command=Admin&Container=' + quote(cname
)
141 t
.text
= '<h3>Your Settings have been saved.</h3> <br>You settings have been saved to the pyTivo.conf file.'+\
142 'However you will need to do a <b>Soft Reset</b> before these changes will take effect.'+\
143 '<br> The <a href="/TiVoConnect?Command=Admin&Container='+ quote(cname
) +'"> Admin</a> page will reload in 10 seconds.'
144 handler
.wfile
.write(t
)
146 def NPL(self
, handler
, query
):
147 shows_per_page
= 50 #Change this to alter the number of shows returned per page
148 subcname
= query
['Container'][0]
149 cname
= subcname
.split('/')[0]
153 for name
, data
in config
.getShares():
155 if 'tivo_mak' in data
:
156 tivo_mak
= data
['tivo_mak']
159 if 'togo_path' in data
:
160 togo_path
= data
['togo_path']
165 tivoIP
= query
['TiVo'][0]
166 theurl
= 'https://'+ tivoIP
+'/TiVoConnect?Command=QueryContainer&ItemCount='+ str(shows_per_page
) +'&Container=/NowPlaying'
167 if 'Folder' in query
:
168 folder
+= str(query
['Folder'][0])
169 theurl
+= '/' + folder
170 if 'AnchorItem' in query
:
171 AnchorItem
+= str(query
['AnchorItem'][0])
172 theurl
+= '&AnchorItem=' + quote(AnchorItem
)
173 if 'AnchorOffset' in query
:
174 AnchorOffset
+= str(query
['AnchorOffset'][0])
175 theurl
+= '&AnchorOffset=' + AnchorOffset
177 password
= tivo_mak
#TiVo MAK
179 r
=urllib2
.Request(theurl
)
180 auth_handler
= urllib2
.HTTPDigestAuthHandler()
181 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', password
)
182 opener
= urllib2
.build_opener(auth_handler
)
183 urllib2
.install_opener(opener
)
185 if theurl
in tivo_cache
: #check to see if we have accessed this page before
186 if tivo_cache
[theurl
]['thepage'] == '' or (time
.time() - tivo_cache
[theurl
]['thepage_time']) >= 60: #if page is empty or old then retreive it
188 handle
= urllib2
.urlopen(r
)
190 handler
.send_response(200)
191 handler
.end_headers()
192 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
195 t
.url
= '/TiVoConnect?Command=NPL&Container=' + quote(cname
)
196 t
.text
= '<h3>Unable to Connect to TiVo.</h3> <br>pyTivo was unable to connect to the TiVo at ' + tivoIP
+\
197 '<br>This most likely caused by an incorrect Media Access Key. Please return to the ToGo page and double check your Media Access Key.' +\
198 '<br> The <a href="/TiVoConnect?Command=NPL&Container='+ quote(cname
) + '"> ToGo</a> page will reload in 20 seconds.'
199 handler
.wfile
.write(t
)
201 tivo_cache
[theurl
]['thepage'] = handle
.read()
202 tivo_cache
[theurl
]['thepage_time'] = time
.time()
205 handle
= urllib2
.urlopen(r
)
207 handler
.send_response(200)
208 handler
.end_headers()
209 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
212 t
.url
= '/TiVoConnect?Command=NPL&Container=' + quote(cname
)
213 t
.text
= '<h3>Unable to Connect to TiVo.</h3> <br>pyTivo was unable to connect to the TiVo at ' + tivoIP
+\
214 '<br>This most likely caused by an incorrect Media Access Key. Please return to the ToGo page and double check your Media Access Key.' +\
215 '<br> The <a href="/TiVoConnect?Command=NPL&Container='+ quote(cname
) + '"> ToGo</a> page will reload in 20 seconds.'
216 handler
.wfile
.write(t
)
218 tivo_cache
[theurl
] = {}
219 tivo_cache
[theurl
]['thepage'] = handle
.read()
220 tivo_cache
[theurl
]['thepage_time'] = time
.time()
222 xmldoc
= minidom
.parseString(tivo_cache
[theurl
]['thepage'])
223 items
= xmldoc
.getElementsByTagName('Item')
224 TotalItems
= xmldoc
.getElementsByTagName('Details')[0].getElementsByTagName('TotalItems')[0].firstChild
.data
225 ItemStart
= xmldoc
.getElementsByTagName('ItemStart')[0].firstChild
.data
226 ItemCount
= xmldoc
.getElementsByTagName('ItemCount')[0].firstChild
.data
227 FirstAnchor
= items
[0].getElementsByTagName("Links")[0].getElementsByTagName("Content")[0].getElementsByTagName("Url")[0].firstChild
.data
232 entry
['Title'] = item
.getElementsByTagName("Title")[0].firstChild
.data
233 entry
['ContentType'] = item
.getElementsByTagName("ContentType")[0].firstChild
.data
234 if (len(item
.getElementsByTagName("UniqueId")) >= 1):
235 entry
['UniqueId'] = item
.getElementsByTagName("UniqueId")[0].firstChild
.data
236 if entry
['ContentType'] == 'x-tivo-container/folder':
237 entry
['TotalItems'] = item
.getElementsByTagName("TotalItems")[0].firstChild
.data
238 entry
['LastChangeDate'] = item
.getElementsByTagName("LastChangeDate")[0].firstChild
.data
239 entry
['LastChangeDate'] = time
.strftime("%b %d, %Y", time
.localtime(int(entry
['LastChangeDate'], 16)))
241 link
= item
.getElementsByTagName("Links")[0]
242 if (len(link
.getElementsByTagName("CustomIcon")) >= 1):
243 entry
['Icon'] = link
.getElementsByTagName("CustomIcon")[0].getElementsByTagName("Url")[0].firstChild
.data
244 if (len(link
.getElementsByTagName("Content")) >= 1):
245 entry
['Url'] = link
.getElementsByTagName("Content")[0].getElementsByTagName("Url")[0].firstChild
.data
246 parse_url
= urlparse(entry
['Url'])
247 entry
['Url'] = quote('http://' + parse_url
[1].split(':')[0] + parse_url
[2] + "?" + parse_url
[4])
248 keys
= ['SourceSize', 'Duration', 'CaptureDate', 'EpisodeTitle', 'Description', 'SourceChannel', 'SourceStation']
251 entry
[key
] = item
.getElementsByTagName(key
)[0].firstChild
.data
254 entry
['SourceSize'] = "%.3f GB" % float(float(entry
['SourceSize'])/(1024*1024*1024))
255 entry
['Duration'] = str(int(entry
['Duration'])/(60*60*1000)).zfill(2) + ':' \
256 + str((int(entry
['Duration'])%(60*60*1000))/(60*1000)).zfill(2) + ':' \
257 + str((int(entry
['Duration'])/1000)%60).zfill(2)
258 entry
['CaptureDate'] = time
.strftime("%b %d, %Y", time
.localtime(int(entry
['CaptureDate'], 16)))
269 subcname
= query
['Container'][0]
270 cname
= subcname
.split('/')[0]
271 handler
.send_response(200)
272 handler
.end_headers()
273 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'npl.tmpl'))
277 t
.tivo_mak
= tivo_mak
278 t
.togo_path
= togo_path
279 t
.tivos
= handler
.tivos
285 t
.TotalItems
= int(TotalItems
)
286 t
.ItemStart
= int(ItemStart
)
287 t
.ItemCount
= int(ItemCount
)
288 t
.FirstAnchor
= quote(FirstAnchor
)
289 t
.shows_per_page
= shows_per_page
290 t
.redirect
= quote(unquote_plus(handler
.path
).split('/')[1])
291 o
= ''.join([i
for i
in unicode(t
) if i
not in (u
'\u200b')])
292 handler
.wfile
.write(o
.encode('latin-1'))
294 def get_tivo_file(self
, url
, mak
, tivoIP
, outfile
):
296 cj
= cookielib
.LWPCookieJar()
298 r
=urllib2
.Request(url
)
299 auth_handler
= urllib2
.HTTPDigestAuthHandler()
300 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', mak
)
301 opener
= urllib2
.build_opener(urllib2
.HTTPCookieProcessor(cj
), auth_handler
)
302 urllib2
.install_opener(opener
)
305 handle
= urllib2
.urlopen(r
)
307 #If we get "Too many transfers error" try a second time. For some reason
308 #urllib2 does not properly close connections when a transfer is canceled.
311 handle
= urllib2
.urlopen(r
)
313 status
[url
]['running'] = False
314 status
[url
]['error'] = e
.code
317 status
[url
]['running'] = False
318 status
[url
]['error'] = e
.code
321 f
= open(outfile
, 'wb')
323 start_time
= time
.time()
324 output
= handle
.read(1024)
325 while status
[url
]['running'] and output
!= '':
328 if ((time
.time() - start_time
) >= 5):
329 status
[url
]['rate'] = int(kilobytes
/(time
.time() - start_time
))
331 start_time
= time
.time()
332 output
= handle
.read(1024)
333 status
[url
]['running'] = False
338 def ToGo(self
, handler
, query
):
339 subcname
= query
['Container'][0]
340 cname
= subcname
.split('/')[0]
341 for name
, data
in config
.getShares():
343 if 'tivo_mak' in data
:
344 tivo_mak
= data
['tivo_mak']
347 if 'togo_path' in data
:
348 togo_path
= data
['togo_path']
351 if tivo_mak
!= "" and togo_path
!= "":
352 parse_url
= urlparse(str(query
['Url'][0]))
353 theurl
= 'http://' + parse_url
[1].split(':')[0] + parse_url
[2] + "?" + parse_url
[4]
354 password
= tivo_mak
#TiVo MAK
355 tivoIP
= query
['TiVo'][0]
356 name
= unquote(parse_url
[2])[10:300].split('.')
357 name
.insert(-1," - " + unquote(parse_url
[4]).split("id=")[1] + ".")
358 outfile
= os
.path
.join(togo_path
, "".join(name
))
360 status
[theurl
] = {'running':True, 'error':'', 'rate':'', 'finished':False}
362 thread
.start_new_thread(Admin
.get_tivo_file
, (self
, theurl
, password
, tivoIP
, outfile
))
364 handler
.send_response(200)
365 handler
.end_headers()
366 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
369 t
.url
= '/'+ query
['Redirect'][0]
370 t
.text
= '<h3>Transfer Initiated.</h3> <br>You selected transfer has been initiated.'+\
371 '<br> The <a href="/'+ query
['Redirect'][0] +'"> ToGo</a> page will reload in 3 seconds.'
372 handler
.wfile
.write(t
)
374 handler
.send_response(200)
375 handler
.end_headers()
376 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
379 t
.url
= '/'+ query
['Redirect'][0]
380 t
.text
= '<h3>Missing Data.</h3> <br>You must set both "tivo_mak" and "togo_path" before using this function.'+\
381 '<br> The <a href="/'+ query
['Redirect'][0] +'"> ToGo</a> page will reload in 10 seconds.'
382 handler
.wfile
.write(t
)
384 def ToGoStop(self
, handler
, query
):
385 parse_url
= urlparse(str(query
['Url'][0]))
386 theurl
= 'http://' + parse_url
[1].split(':')[0] + parse_url
[2] + "?" + parse_url
[4]
388 status
[theurl
]['running'] = False
390 subcname
= query
['Container'][0]
391 cname
= subcname
.split('/')[0]
392 handler
.send_response(200)
393 handler
.end_headers()
394 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
397 t
.url
= '/'+ query
['Redirect'][0]
398 t
.text
= '<h3>Transfer Stopped.</h3> <br>Your transfer has been stopped.'+\
399 '<br> The <a href="/'+ query
['Redirect'][0] +'"> ToGo</a> page will reload in 3 seconds.'
400 handler
.wfile
.write(t
)
403 def SaveNPL(self
, handler
, query
):
404 config
= ConfigParser
.ConfigParser()
405 config
.read(config_file_path
)
406 if 'tivo_mak' in query
:
407 config
.set(query
['Container'][0], 'tivo_mak', query
['tivo_mak'][0])
408 if 'togo_path' in query
:
409 config
.set(query
['Container'][0], 'togo_path', query
['togo_path'][0])
410 f
= open(config_file_path
, "w")
414 subcname
= query
['Container'][0]
415 cname
= subcname
.split('/')[0]
416 handler
.send_response(200)
417 handler
.end_headers()
418 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates', 'redirect.tmpl'))
421 t
.url
= '/TiVoConnect?last_page=NPL&Command=Reset&Container=' + quote(cname
)
422 t
.text
= '<h3>Your Settings have been saved.</h3> <br>You settings have been saved to the pyTivo.conf file.'+\
423 'pyTivo will now do a <b>Soft Reset</b> to allow these changes to take effect.'+\
424 '<br> The <a href="/TiVoConnect?last_page=NPL&Command=Reset&Container='+ quote(cname
) +'"> Reset</a> will occur in 2 seconds.'
425 handler
.wfile
.write(t
)