Change "Web Configuration" to "Settings"; navigation link for the
[pyTivo/wmcbrine/lucasnz.git] / plugins / togo / togo.py
bloba9b4c788b88d0c54bbbb7dbc4f901a304bbfd922
1 import cookielib
2 import logging
3 import os
4 import subprocess
5 import thread
6 import time
7 import urllib2
8 import urlparse
9 from urllib import quote, unquote
10 from xml.dom import minidom
11 from xml.sax.saxutils import escape
13 from Cheetah.Template import Template
15 import config
16 import metadata
17 from plugin import EncodeUnicode, Plugin
19 logger = logging.getLogger('pyTivo.togo')
20 tag_data = metadata.tag_data
22 SCRIPTDIR = os.path.dirname(__file__)
24 CLASS_NAME = 'ToGo'
26 # Some error/status message templates
28 MISSING = """<h3>Missing Data</h3> <p>You must set both "tivo_mak" and
29 "togo_path" before using this function.</p>"""
31 TRANS_QUEUE = """<h3>Queued for Transfer</h3> <p>%s</p> <p>queued for
32 transfer to:</p> <p>%s</p>"""
34 TRANS_STOP = """<h3>Transfer Stopped</h3> <p>Your transfer of:</p>
35 <p>%s</p> <p>has been stopped.</p>"""
37 UNQUEUE = """<h3>Removed from Queue</h3> <p>%s</p> <p>has been removed
38 from the queue.</p>"""
40 UNABLE = """<h3>Unable to Connect to TiVo</h3> <p>pyTivo was unable to
41 connect to the TiVo at %s.</p> <p>This is most likely caused by an
42 incorrect Media Access Key. Please return to the Settings page and
43 double check your <b>tivo_mak</b> setting.</p>"""
45 # Preload the templates
46 def tmpl(name):
47 return file(os.path.join(SCRIPTDIR, 'templates', name), 'rb').read()
49 CONTAINER_TEMPLATE_MOBILE = tmpl('npl_mob.tmpl')
50 CONTAINER_TEMPLATE = tmpl('npl.tmpl')
52 status = {} # Global variable to control download threads
53 tivo_cache = {} # Cache of TiVo NPL
54 queue = {} # Recordings to download -- list per TiVo
55 basic_meta = {} # Data from NPL, parsed, indexed by progam URL
57 def null_cookie(name, value):
58 return cookielib.Cookie(0, name, value, None, False, '', False,
59 False, '', False, False, None, False, None, None, None)
61 auth_handler = urllib2.HTTPDigestAuthHandler()
62 cj = cookielib.CookieJar()
63 cj.set_cookie(null_cookie('sid', 'ADEADDA7EDEBAC1E'))
64 tivo_opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),
65 auth_handler)
67 class ToGo(Plugin):
68 CONTENT_TYPE = 'text/html'
70 def tivo_open(self, url):
71 # Loop just in case we get a server busy message
72 while True:
73 try:
74 # Open the URL using our authentication/cookie opener
75 return tivo_opener.open(url)
77 # Do a retry if the TiVo responds that the server is busy
78 except urllib2.HTTPError, e:
79 if e.code == 503:
80 time.sleep(5)
81 continue
83 # Throw the error otherwise
84 raise
86 def NPL(self, handler, query):
87 global basic_meta
88 shows_per_page = 50 # Change this to alter the number of shows returned
89 folder = ''
90 has_tivodecode = bool(config.get_bin('tivodecode'))
92 if 'TiVo' in query:
93 tivoIP = query['TiVo'][0]
94 tsn = config.tivos_by_ip(tivoIP)
96 togo_mpegts = config.is_ts_capable(tsn)
97 useragent = handler.headers.getheader('User-Agent', '')
99 tivo_name = config.tivo_names[tsn]
100 tivo_mak = config.get_tsn('tivo_mak', tsn)
101 theurl = ('https://' + tivoIP +
102 '/TiVoConnect?Command=QueryContainer&ItemCount=' +
103 str(shows_per_page) + '&Container=/NowPlaying')
104 if 'Folder' in query:
105 folder += query['Folder'][0]
106 theurl += '/' + folder
107 if 'AnchorItem' in query:
108 theurl += '&AnchorItem=' + quote(query['AnchorItem'][0])
109 if 'AnchorOffset' in query:
110 theurl += '&AnchorOffset=' + query['AnchorOffset'][0]
112 if (theurl not in tivo_cache or
113 (time.time() - tivo_cache[theurl]['thepage_time']) >= 60):
114 # if page is not cached or old then retreive it
115 auth_handler.add_password('TiVo DVR', tivoIP, 'tivo', tivo_mak)
116 try:
117 page = self.tivo_open(theurl)
118 except IOError, e:
119 handler.redir(UNABLE % tivoIP, 10)
120 return
121 tivo_cache[theurl] = {'thepage': minidom.parse(page),
122 'thepage_time': time.time()}
123 page.close()
125 xmldoc = tivo_cache[theurl]['thepage']
126 items = xmldoc.getElementsByTagName('Item')
127 TotalItems = tag_data(xmldoc, 'TiVoContainer/Details/TotalItems')
128 ItemStart = tag_data(xmldoc, 'TiVoContainer/ItemStart')
129 ItemCount = tag_data(xmldoc, 'TiVoContainer/ItemCount')
130 FirstAnchor = tag_data(items[0], 'Links/Content/Url')
131 title = tag_data(xmldoc, 'TiVoContainer/Details/Title')
133 data = []
134 for item in items:
135 entry = {}
136 entry['ContentType'] = tag_data(item, 'Details/ContentType')
137 for tag in ('CopyProtected', 'UniqueId'):
138 value = tag_data(item, 'Details/' + tag)
139 if value:
140 entry[tag] = value
141 if entry['ContentType'] == 'x-tivo-container/folder':
142 entry['Title'] = tag_data(item, 'Details/Title')
143 entry['TotalItems'] = tag_data(item, 'Details/TotalItems')
144 lc = tag_data(item, 'Details/LastCaptureDate')
145 if not lc:
146 lc = tag_data(item, 'Details/LastChangeDate')
147 entry['LastChangeDate'] = time.strftime('%b %d, %Y',
148 time.localtime(int(lc, 16)))
149 else:
150 keys = {'Icon': 'Links/CustomIcon/Url',
151 'Url': 'Links/Content/Url',
152 'SourceSize': 'Details/SourceSize',
153 'Duration': 'Details/Duration',
154 'CaptureDate': 'Details/CaptureDate'}
155 for key in keys:
156 value = tag_data(item, keys[key])
157 if value:
158 entry[key] = value
160 entry['SourceSize'] = ( '%.3f GB' %
161 (float(entry['SourceSize']) / (1024 ** 3)) )
163 dur = int(entry['Duration']) / 1000
164 entry['Duration'] = ( '%02d:%02d:%02d' %
165 (dur / 3600, (dur % 3600) / 60, dur % 60) )
167 entry['CaptureDate'] = time.strftime('%b %d, %Y',
168 time.localtime(int(entry['CaptureDate'], 16)))
170 url = entry['Url']
171 if url in basic_meta:
172 entry.update(basic_meta[url])
173 else:
174 basic_data = metadata.from_container(item)
175 entry.update(basic_data)
176 basic_meta[url] = basic_data
178 data.append(entry)
179 else:
180 data = []
181 tivoIP = ''
182 TotalItems = 0
183 ItemStart = 0
184 ItemCount = 0
185 FirstAnchor = ''
186 title = ''
188 if useragent.lower().find('mobile') > 0:
189 t = Template(CONTAINER_TEMPLATE_MOBILE, filter=EncodeUnicode)
190 else:
191 t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode)
192 t.escape = escape
193 t.quote = quote
194 t.folder = folder
195 t.status = status
196 if tivoIP in queue:
197 t.queue = queue[tivoIP]
198 t.has_tivodecode = has_tivodecode
199 t.togo_mpegts = togo_mpegts
200 t.tname = tivo_name
201 t.tivoIP = tivoIP
202 t.container = handler.cname
203 t.data = data
204 t.len = len
205 t.TotalItems = int(TotalItems)
206 t.ItemStart = int(ItemStart)
207 t.ItemCount = int(ItemCount)
208 t.FirstAnchor = quote(FirstAnchor)
209 t.shows_per_page = shows_per_page
210 t.title = title
211 handler.send_html(str(t), refresh='300')
213 def get_tivo_file(self, tivoIP, url, mak, togo_path):
214 # global status
215 status[url].update({'running': True, 'queued': False})
217 parse_url = urlparse.urlparse(url)
219 name = unquote(parse_url[2])[10:].split('.')
220 id = unquote(parse_url[4]).split('id=')[1]
221 name.insert(-1, ' - ' + id + '.')
222 if status[url]['decode']:
223 name[-1] = 'mpg'
224 outfile = os.path.join(togo_path, ''.join(name))
226 if status[url]['save']:
227 meta = basic_meta[url]
228 details_url = 'https://%s/TiVoVideoDetails?id=%s' % (tivoIP, id)
229 try:
230 handle = self.tivo_open(details_url)
231 meta.update(metadata.from_details(handle))
232 handle.close()
233 except:
234 pass
235 metafile = open(outfile + '.txt', 'w')
236 metadata.dump(metafile, meta)
237 metafile.close()
239 auth_handler.add_password('TiVo DVR', url, 'tivo', mak)
240 try:
241 if status[url]['ts_format']:
242 handle = self.tivo_open('%s&Format=video/x-tivo-mpeg-ts' % url)
243 else:
244 handle = self.tivo_open(url)
245 except urllib2.HTTPError, e:
246 status[url]['running'] = False
247 status[url]['error'] = e.code
248 logger.error(e.code)
249 return
250 except urllib2.URLError, e:
251 status[url]['running'] = False
252 status[url]['error'] = e.reason
253 logger.error(e.reason)
254 return
256 tivo_name = config.tivo_names[config.tivos_by_ip(tivoIP)]
258 logger.info('[%s] Start getting "%s" from %s' %
259 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile, tivo_name))
261 if status[url]['decode']:
262 tivodecode_path = config.get_bin('tivodecode')
263 tcmd = [tivodecode_path, '-m', mak, '-o', outfile, '-']
264 tivodecode = subprocess.Popen(tcmd, stdin=subprocess.PIPE,
265 bufsize=(512 * 1024))
266 f = tivodecode.stdin
267 else:
268 f = open(outfile, 'wb')
269 length = 0
270 start_time = time.time()
271 last_interval = start_time
272 now = start_time
273 try:
274 while status[url]['running']:
275 output = handle.read(1024000)
276 if not output:
277 break
278 length += len(output)
279 f.write(output)
280 now = time.time()
281 elapsed = now - last_interval
282 if elapsed >= 5:
283 status[url]['rate'] = '%.2f Mb/s' % (length * 8.0 /
284 (elapsed * 1024 * 1024))
285 status[url]['size'] += length
286 length = 0
287 last_interval = now
288 if status[url]['running']:
289 status[url]['finished'] = True
290 except Exception, msg:
291 status[url]['running'] = False
292 logger.info(msg)
293 handle.close()
294 f.close()
295 status[url]['size'] += length
296 if status[url]['running']:
297 mega_elapsed = (now - start_time) * 1024 * 1024
298 if mega_elapsed < 1:
299 mega_elapsed = 1
300 size = status[url]['size']
301 rate = size * 8.0 / mega_elapsed
302 logger.info('[%s] Done getting "%s" from %s, %d bytes, %.2f Mb/s' %
303 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
304 tivo_name, size, rate))
305 status[url]['running'] = False
306 else:
307 os.remove(outfile)
308 if status[url]['save']:
309 os.remove(outfile + '.txt')
310 logger.info('[%s] Transfer of "%s" from %s aborted' %
311 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
312 tivo_name))
313 del status[url]
315 def process_queue(self, tivoIP, mak, togo_path):
316 while queue[tivoIP]:
317 time.sleep(5)
318 url = queue[tivoIP][0]
319 self.get_tivo_file(tivoIP, url, mak, togo_path)
320 queue[tivoIP].pop(0)
321 del queue[tivoIP]
323 def ToGo(self, handler, query):
324 togo_path = config.get_server('togo_path')
325 for name, data in config.getShares():
326 if togo_path == name:
327 togo_path = data.get('path')
328 if togo_path:
329 tivoIP = query['TiVo'][0]
330 tsn = config.tivos_by_ip(tivoIP)
331 tivo_mak = config.get_tsn('tivo_mak', tsn)
332 urls = query.get('Url', [])
333 decode = 'decode' in query
334 save = 'save' in query
335 ts_format = 'ts_format' in query
336 for theurl in urls:
337 status[theurl] = {'running': False, 'error': '', 'rate': '',
338 'queued': True, 'size': 0, 'finished': False,
339 'decode': decode, 'save': save, 'ts_format' : ts_format}
340 if tivoIP in queue:
341 queue[tivoIP].append(theurl)
342 else:
343 queue[tivoIP] = [theurl]
344 thread.start_new_thread(ToGo.process_queue,
345 (self, tivoIP, tivo_mak, togo_path))
346 logger.info('[%s] Queued "%s" for transfer to %s' %
347 (time.strftime('%d/%b/%Y %H:%M:%S'),
348 unquote(theurl), togo_path))
349 urlstring = '<br>'.join([unquote(x) for x in urls])
350 message = TRANS_QUEUE % (urlstring, togo_path)
351 else:
352 message = MISSING
353 handler.redir(message, 5)
355 def ToGoStop(self, handler, query):
356 theurl = query['Url'][0]
357 status[theurl]['running'] = False
358 handler.redir(TRANS_STOP % unquote(theurl))
360 def Unqueue(self, handler, query):
361 theurl = query['Url'][0]
362 tivoIP = query['TiVo'][0]
363 del status[theurl]
364 queue[tivoIP].remove(theurl)
365 logger.info('[%s] Removed "%s" from queue' %
366 (time.strftime('%d/%b/%Y %H:%M:%S'),
367 unquote(theurl)))
368 handler.redir(UNQUEUE % unquote(theurl))