Add "(TS)" to the names of transport-stream .TiVo files, and use ".ts"
[pyTivo/wmcbrine/lucasnz.git] / plugins / togo / togo.py
blob9e2ffabfe4b4beffa9aae03fd6c52af76e506a8c
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 tnname = os.path.join(SCRIPTDIR, 'templates', 'npl.tmpl')
47 NPL_TEMPLATE = file(tnname, 'rb').read()
49 status = {} # Global variable to control download threads
50 tivo_cache = {} # Cache of TiVo NPL
51 queue = {} # Recordings to download -- list per TiVo
52 basic_meta = {} # Data from NPL, parsed, indexed by progam URL
54 def null_cookie(name, value):
55 return cookielib.Cookie(0, name, value, None, False, '', False,
56 False, '', False, False, None, False, None, None, None)
58 auth_handler = urllib2.HTTPDigestAuthHandler()
59 cj = cookielib.CookieJar()
60 cj.set_cookie(null_cookie('sid', 'ADEADDA7EDEBAC1E'))
61 tivo_opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),
62 auth_handler)
64 class ToGo(Plugin):
65 CONTENT_TYPE = 'text/html'
67 def tivo_open(self, url):
68 # Loop just in case we get a server busy message
69 while True:
70 try:
71 # Open the URL using our authentication/cookie opener
72 return tivo_opener.open(url)
74 # Do a retry if the TiVo responds that the server is busy
75 except urllib2.HTTPError, e:
76 if e.code == 503:
77 time.sleep(5)
78 continue
80 # Throw the error otherwise
81 raise
83 def NPL(self, handler, query):
85 def getint(thing):
86 try:
87 result = int(thing)
88 except:
89 result = 0
90 return result
92 global basic_meta
93 shows_per_page = 50 # Change this to alter the number of shows returned
94 folder = ''
95 FirstAnchor = ''
96 has_tivodecode = bool(config.get_bin('tivodecode'))
98 if 'TiVo' in query:
99 tivoIP = query['TiVo'][0]
100 tsn = config.tivos_by_ip(tivoIP)
101 tivo_name = config.tivo_names[tsn]
102 tivo_mak = config.get_tsn('tivo_mak', tsn)
103 theurl = ('https://' + tivoIP +
104 '/TiVoConnect?Command=QueryContainer&ItemCount=' +
105 str(shows_per_page) + '&Container=/NowPlaying')
106 if 'Folder' in query:
107 folder += query['Folder'][0]
108 theurl += '/' + folder
109 if 'AnchorItem' in query:
110 theurl += '&AnchorItem=' + quote(query['AnchorItem'][0])
111 if 'AnchorOffset' in query:
112 theurl += '&AnchorOffset=' + query['AnchorOffset'][0]
114 if (theurl not in tivo_cache or
115 (time.time() - tivo_cache[theurl]['thepage_time']) >= 60):
116 # if page is not cached or old then retreive it
117 auth_handler.add_password('TiVo DVR', tivoIP, 'tivo', tivo_mak)
118 try:
119 page = self.tivo_open(theurl)
120 except IOError, e:
121 handler.redir(UNABLE % tivoIP, 10)
122 return
123 tivo_cache[theurl] = {'thepage': minidom.parse(page),
124 'thepage_time': time.time()}
125 page.close()
127 xmldoc = tivo_cache[theurl]['thepage']
128 items = xmldoc.getElementsByTagName('Item')
129 TotalItems = tag_data(xmldoc, 'TiVoContainer/Details/TotalItems')
130 ItemStart = tag_data(xmldoc, 'TiVoContainer/ItemStart')
131 ItemCount = tag_data(xmldoc, 'TiVoContainer/ItemCount')
132 title = tag_data(xmldoc, 'TiVoContainer/Details/Title')
133 if items:
134 FirstAnchor = tag_data(items[0], 'Links/Content/Url')
136 data = []
137 for item in items:
138 entry = {}
139 entry['ContentType'] = tag_data(item, 'Details/ContentType')
140 for tag in ('CopyProtected', 'UniqueId'):
141 value = tag_data(item, 'Details/' + tag)
142 if value:
143 entry[tag] = value
144 if entry['ContentType'] == 'x-tivo-container/folder':
145 entry['Title'] = tag_data(item, 'Details/Title')
146 entry['TotalItems'] = tag_data(item, 'Details/TotalItems')
147 lc = tag_data(item, 'Details/LastCaptureDate')
148 if not lc:
149 lc = tag_data(item, 'Details/LastChangeDate')
150 entry['LastChangeDate'] = time.strftime('%b %d, %Y',
151 time.localtime(int(lc, 16)))
152 else:
153 keys = {'Icon': 'Links/CustomIcon/Url',
154 'Url': 'Links/Content/Url',
155 'SourceSize': 'Details/SourceSize',
156 'Duration': 'Details/Duration',
157 'CaptureDate': 'Details/CaptureDate'}
158 for key in keys:
159 value = tag_data(item, keys[key])
160 if value:
161 entry[key] = value
163 rawsize = entry['SourceSize']
164 entry['SourceSize'] = metadata.human_size(rawsize)
166 dur = getint(entry['Duration']) / 1000
167 entry['Duration'] = ( '%d:%02d:%02d' %
168 (dur / 3600, (dur % 3600) / 60, dur % 60) )
170 entry['CaptureDate'] = time.strftime('%b %d, %Y',
171 time.localtime(int(entry['CaptureDate'], 16)))
173 url = entry['Url']
174 if url in basic_meta:
175 entry.update(basic_meta[url])
176 else:
177 basic_data = metadata.from_container(item)
178 entry.update(basic_data)
179 basic_meta[url] = basic_data
181 data.append(entry)
182 else:
183 data = []
184 tivoIP = ''
185 TotalItems = 0
186 ItemStart = 0
187 ItemCount = 0
188 title = ''
190 t = Template(NPL_TEMPLATE, filter=EncodeUnicode)
191 t.escape = escape
192 t.quote = quote
193 t.folder = folder
194 t.status = status
195 if tivoIP in queue:
196 t.queue = queue[tivoIP]
197 t.has_tivodecode = has_tivodecode
198 t.togo_mpegts = config.is_ts_capable(tsn)
199 t.tname = tivo_name
200 t.tivoIP = tivoIP
201 t.container = handler.cname
202 t.data = data
203 t.len = len
204 t.TotalItems = getint(TotalItems)
205 t.ItemStart = getint(ItemStart)
206 t.ItemCount = getint(ItemCount)
207 t.FirstAnchor = quote(FirstAnchor)
208 t.shows_per_page = shows_per_page
209 t.title = title
210 handler.send_html(str(t), refresh='300')
212 def get_tivo_file(self, tivoIP, url, mak, togo_path):
213 # global status
214 status[url].update({'running': True, 'queued': False})
216 parse_url = urlparse.urlparse(url)
218 name = unquote(parse_url[2])[10:].split('.')
219 id = unquote(parse_url[4]).split('id=')[1]
220 ts = status[url]['ts_format']
221 name.insert(-1, ' - ' + id)
222 if status[url]['decode']:
223 if ts:
224 name[-1] = 'ts'
225 else:
226 name[-1] = 'mpg'
227 elif ts:
228 name.insert(-1, ' (TS)')
229 name.insert(-1, '.')
230 outfile = os.path.join(togo_path, ''.join(name))
232 if status[url]['save']:
233 meta = basic_meta[url]
234 details_url = 'https://%s/TiVoVideoDetails?id=%s' % (tivoIP, id)
235 try:
236 handle = self.tivo_open(details_url)
237 meta.update(metadata.from_details(handle))
238 handle.close()
239 except:
240 pass
241 metafile = open(outfile + '.txt', 'w')
242 metadata.dump(metafile, meta)
243 metafile.close()
245 auth_handler.add_password('TiVo DVR', url, 'tivo', mak)
246 try:
247 if status[url]['ts_format']:
248 handle = self.tivo_open(url + '&Format=video/x-tivo-mpeg-ts')
249 else:
250 handle = self.tivo_open(url)
251 except urllib2.HTTPError, e:
252 status[url]['running'] = False
253 status[url]['error'] = e.code
254 logger.error(e.code)
255 return
256 except urllib2.URLError, e:
257 status[url]['running'] = False
258 status[url]['error'] = e.reason
259 logger.error(e.reason)
260 return
262 tivo_name = config.tivo_names[config.tivos_by_ip(tivoIP)]
264 logger.info('[%s] Start getting "%s" from %s' %
265 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile, tivo_name))
267 if status[url]['decode']:
268 tivodecode_path = config.get_bin('tivodecode')
269 tcmd = [tivodecode_path, '-m', mak, '-o', outfile, '-']
270 tivodecode = subprocess.Popen(tcmd, stdin=subprocess.PIPE,
271 bufsize=(512 * 1024))
272 f = tivodecode.stdin
273 else:
274 f = open(outfile, 'wb')
275 length = 0
276 start_time = time.time()
277 last_interval = start_time
278 now = start_time
279 try:
280 while status[url]['running']:
281 output = handle.read(1024000)
282 if not output:
283 break
284 length += len(output)
285 f.write(output)
286 now = time.time()
287 elapsed = now - last_interval
288 if elapsed >= 5:
289 status[url]['rate'] = '%.2f Mb/s' % (length * 8.0 /
290 (elapsed * 1024 * 1024))
291 status[url]['size'] += length
292 length = 0
293 last_interval = now
294 if status[url]['running']:
295 status[url]['finished'] = True
296 except Exception, msg:
297 status[url]['running'] = False
298 logger.info(msg)
299 handle.close()
300 f.close()
301 status[url]['size'] += length
302 if status[url]['running']:
303 mega_elapsed = (now - start_time) * 1024 * 1024
304 if mega_elapsed < 1:
305 mega_elapsed = 1
306 size = status[url]['size']
307 rate = size * 8.0 / mega_elapsed
308 logger.info('[%s] Done getting "%s" from %s, %d bytes, %.2f Mb/s' %
309 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
310 tivo_name, size, rate))
311 status[url]['running'] = False
312 else:
313 os.remove(outfile)
314 if status[url]['save']:
315 os.remove(outfile + '.txt')
316 logger.info('[%s] Transfer of "%s" from %s aborted' %
317 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
318 tivo_name))
319 del status[url]
321 def process_queue(self, tivoIP, mak, togo_path):
322 while queue[tivoIP]:
323 time.sleep(5)
324 url = queue[tivoIP][0]
325 self.get_tivo_file(tivoIP, url, mak, togo_path)
326 queue[tivoIP].pop(0)
327 del queue[tivoIP]
329 def ToGo(self, handler, query):
330 togo_path = config.get_server('togo_path')
331 for name, data in config.getShares():
332 if togo_path == name:
333 togo_path = data.get('path')
334 if togo_path:
335 tivoIP = query['TiVo'][0]
336 tsn = config.tivos_by_ip(tivoIP)
337 tivo_mak = config.get_tsn('tivo_mak', tsn)
338 urls = query.get('Url', [])
339 decode = 'decode' in query
340 save = 'save' in query
341 ts_format = 'ts_format' in query
342 for theurl in urls:
343 status[theurl] = {'running': False, 'error': '', 'rate': '',
344 'queued': True, 'size': 0, 'finished': False,
345 'decode': decode, 'save': save,
346 'ts_format': ts_format}
347 if tivoIP in queue:
348 queue[tivoIP].append(theurl)
349 else:
350 queue[tivoIP] = [theurl]
351 thread.start_new_thread(ToGo.process_queue,
352 (self, tivoIP, tivo_mak, togo_path))
353 logger.info('[%s] Queued "%s" for transfer to %s' %
354 (time.strftime('%d/%b/%Y %H:%M:%S'),
355 unquote(theurl), togo_path))
356 urlstring = '<br>'.join([unquote(x) for x in urls])
357 message = TRANS_QUEUE % (urlstring, togo_path)
358 else:
359 message = MISSING
360 handler.redir(message, 5)
362 def ToGoStop(self, handler, query):
363 theurl = query['Url'][0]
364 status[theurl]['running'] = False
365 handler.redir(TRANS_STOP % unquote(theurl))
367 def Unqueue(self, handler, query):
368 theurl = query['Url'][0]
369 tivoIP = query['TiVo'][0]
370 del status[theurl]
371 queue[tivoIP].remove(theurl)
372 logger.info('[%s] Removed "%s" from queue' %
373 (time.strftime('%d/%b/%Y %H:%M:%S'),
374 unquote(theurl)))
375 handler.redir(UNQUEUE % unquote(theurl))