Tidy up for audio_lang. If we match more than one item we keep checking the matching...
[pyTivo/wmcbrine.git] / plugins / togo / togo.py
blobc8ada9f7166aa8be939c9e71c24ef24904a05f51
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 Web Configuration page
43 and 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 auth_handler = urllib2.HTTPDigestAuthHandler()
55 cj = cookielib.LWPCookieJar()
56 tivo_opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),
57 auth_handler)
59 class ToGo(Plugin):
60 CONTENT_TYPE = 'text/html'
62 def tivo_open(self, url):
63 # Loop just in case we get a server busy message
64 while True:
65 try:
66 # Open the URL using our authentication/cookie opener
67 return tivo_opener.open(url)
69 # Do a retry if the TiVo responds that the server is busy
70 except urllib2.HTTPError, e:
71 if e.code == 503:
72 time.sleep(5)
73 continue
75 # Throw the error otherwise
76 raise
78 def NPL(self, handler, query):
79 global basic_meta
80 shows_per_page = 50 # Change this to alter the number of shows returned
81 folder = ''
82 has_tivodecode = bool(config.get_bin('tivodecode'))
84 if 'TiVo' in query:
85 tivoIP = query['TiVo'][0]
86 tsn = config.tivos_by_ip(tivoIP)
87 tivo_name = config.tivo_names[tsn]
88 tivo_mak = config.get_tsn('tivo_mak', tsn)
89 theurl = ('https://' + tivoIP +
90 '/TiVoConnect?Command=QueryContainer&ItemCount=' +
91 str(shows_per_page) + '&Container=/NowPlaying')
92 if 'Folder' in query:
93 folder += query['Folder'][0]
94 theurl += '/' + folder
95 if 'AnchorItem' in query:
96 theurl += '&AnchorItem=' + quote(query['AnchorItem'][0])
97 if 'AnchorOffset' in query:
98 theurl += '&AnchorOffset=' + query['AnchorOffset'][0]
100 if (theurl not in tivo_cache or
101 (time.time() - tivo_cache[theurl]['thepage_time']) >= 60):
102 # if page is not cached or old then retreive it
103 auth_handler.add_password('TiVo DVR', tivoIP, 'tivo', tivo_mak)
104 try:
105 page = self.tivo_open(theurl)
106 except IOError, e:
107 handler.redir(UNABLE % tivoIP, 10)
108 return
109 tivo_cache[theurl] = {'thepage': minidom.parse(page),
110 'thepage_time': time.time()}
111 page.close()
113 xmldoc = tivo_cache[theurl]['thepage']
114 items = xmldoc.getElementsByTagName('Item')
115 TotalItems = tag_data(xmldoc, 'Details/TotalItems')
116 ItemStart = tag_data(xmldoc, 'ItemStart')
117 ItemCount = tag_data(xmldoc, 'ItemCount')
118 FirstAnchor = tag_data(items[0], 'Links/Content/Url')
120 data = []
121 for item in items:
122 entry = {}
123 entry['ContentType'] = tag_data(item, 'ContentType')
124 for tag in ('CopyProtected', 'UniqueId'):
125 value = tag_data(item, tag)
126 if value:
127 entry[tag] = value
128 if entry['ContentType'] == 'x-tivo-container/folder':
129 entry['Title'] = tag_data(item, 'Title')
130 entry['TotalItems'] = tag_data(item, 'TotalItems')
131 lc = tag_data(item, 'LastCaptureDate')
132 if not lc:
133 lc = tag_data(item, 'LastChangeDate')
134 entry['LastChangeDate'] = time.strftime('%b %d, %Y',
135 time.localtime(int(lc, 16)))
136 else:
137 keys = {'Icon': 'Links/CustomIcon/Url',
138 'Url': 'Links/Content/Url',
139 'SourceSize': 'Details/SourceSize',
140 'Duration': 'Details/Duration',
141 'CaptureDate': 'Details/CaptureDate'}
142 for key in keys:
143 value = tag_data(item, keys[key])
144 if value:
145 entry[key] = value
147 entry['SourceSize'] = ( '%.3f GB' %
148 (float(entry['SourceSize']) / (1024 ** 3)) )
150 dur = int(entry['Duration']) / 1000
151 entry['Duration'] = ( '%02d:%02d:%02d' %
152 (dur / 3600, (dur % 3600) / 60, dur % 60) )
154 entry['CaptureDate'] = time.strftime('%b %d, %Y',
155 time.localtime(int(entry['CaptureDate'], 16)))
157 url = entry['Url']
158 if url in basic_meta:
159 entry.update(basic_meta[url])
160 else:
161 basic_data = metadata.from_container(item)
162 entry.update(basic_data)
163 basic_meta[url] = basic_data
165 data.append(entry)
166 else:
167 data = []
168 tivoIP = ''
169 TotalItems = 0
170 ItemStart = 0
171 ItemCount = 0
172 FirstAnchor = ''
174 t = Template(NPL_TEMPLATE, filter=EncodeUnicode)
175 t.escape = escape
176 t.quote = quote
177 t.folder = folder
178 t.status = status
179 if tivoIP in queue:
180 t.queue = queue[tivoIP]
181 t.has_tivodecode = has_tivodecode
182 t.tname = tivo_name
183 t.tivoIP = tivoIP
184 t.container = handler.cname
185 t.data = data
186 t.len = len
187 t.TotalItems = int(TotalItems)
188 t.ItemStart = int(ItemStart)
189 t.ItemCount = int(ItemCount)
190 t.FirstAnchor = quote(FirstAnchor)
191 t.shows_per_page = shows_per_page
192 handler.send_html(str(t), refresh='300')
194 def get_tivo_file(self, tivoIP, url, mak, togo_path):
195 # global status
196 status[url].update({'running': True, 'queued': False})
198 parse_url = urlparse.urlparse(url)
200 name = unquote(parse_url[2])[10:].split('.')
201 id = unquote(parse_url[4]).split('id=')[1]
202 name.insert(-1, ' - ' + id + '.')
203 if status[url]['decode']:
204 name[-1] = 'mpg'
205 outfile = os.path.join(togo_path, ''.join(name))
207 if status[url]['save']:
208 meta = basic_meta[url]
209 details_url = 'https://%s/TiVoVideoDetails?id=%s' % (tivoIP, id)
210 try:
211 handle = self.tivo_open(details_url)
212 meta.update(metadata.from_details(handle))
213 handle.close()
214 except:
215 pass
216 metafile = open(outfile + '.txt', 'w')
217 metadata.dump(metafile, meta)
218 metafile.close()
220 auth_handler.add_password('TiVo DVR', url, 'tivo', mak)
221 try:
222 handle = self.tivo_open(url)
223 except urllib2.HTTPError, e:
224 status[url]['running'] = False
225 status[url]['error'] = e.code
226 logger.error(e.code)
227 return
228 except urllib2.URLError, e:
229 status[url]['running'] = False
230 status[url]['error'] = e.reason
231 logger.error(e.reason)
232 return
234 tivo_name = config.tivo_names[config.tivos_by_ip(tivoIP)]
236 logger.info('[%s] Start getting "%s" from %s' %
237 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile, tivo_name))
239 if status[url]['decode']:
240 tivodecode_path = config.get_bin('tivodecode')
241 tcmd = [tivodecode_path, '-m', mak, '-o', outfile, '-']
242 tivodecode = subprocess.Popen(tcmd, stdin=subprocess.PIPE,
243 bufsize=(512 * 1024))
244 f = tivodecode.stdin
245 else:
246 f = open(outfile, 'wb')
247 length = 0
248 start_time = time.time()
249 last_interval = start_time
250 now = start_time
251 try:
252 while status[url]['running']:
253 output = handle.read(1024000)
254 if not output:
255 break
256 length += len(output)
257 f.write(output)
258 now = time.time()
259 elapsed = now - last_interval
260 if elapsed >= 5:
261 status[url]['rate'] = '%.2f Mb/s' % (length * 8.0 /
262 (elapsed * 1024 * 1024))
263 status[url]['size'] += length
264 length = 0
265 last_interval = now
266 if status[url]['running']:
267 status[url]['finished'] = True
268 except Exception, msg:
269 status[url]['running'] = False
270 logger.info(msg)
271 handle.close()
272 f.close()
273 status[url]['size'] += length
274 if status[url]['running']:
275 mega_elapsed = (now - start_time) * 1024 * 1024
276 if mega_elapsed < 1:
277 mega_elapsed = 1
278 size = status[url]['size']
279 rate = size * 8.0 / mega_elapsed
280 logger.info('[%s] Done getting "%s" from %s, %d bytes, %.2f Mb/s' %
281 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
282 tivo_name, size, rate))
283 status[url]['running'] = False
284 else:
285 os.remove(outfile)
286 if status[url]['save']:
287 os.remove(outfile + '.txt')
288 logger.info('[%s] Transfer of "%s" from %s aborted' %
289 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
290 tivo_name))
291 del status[url]
293 def process_queue(self, tivoIP, mak, togo_path):
294 while queue[tivoIP]:
295 time.sleep(5)
296 url = queue[tivoIP][0]
297 self.get_tivo_file(tivoIP, url, mak, togo_path)
298 queue[tivoIP].pop(0)
299 del queue[tivoIP]
301 def ToGo(self, handler, query):
302 togo_path = config.get_server('togo_path')
303 for name, data in config.getShares():
304 if togo_path == name:
305 togo_path = data.get('path')
306 if togo_path:
307 tivoIP = query['TiVo'][0]
308 tsn = config.tivos_by_ip(tivoIP)
309 tivo_mak = config.get_tsn('tivo_mak', tsn)
310 urls = query.get('Url', [])
311 decode = 'decode' in query
312 save = 'save' in query
313 for theurl in urls:
314 status[theurl] = {'running': False, 'error': '', 'rate': '',
315 'queued': True, 'size': 0, 'finished': False,
316 'decode': decode, 'save': save}
317 if tivoIP in queue:
318 queue[tivoIP].append(theurl)
319 else:
320 queue[tivoIP] = [theurl]
321 thread.start_new_thread(ToGo.process_queue,
322 (self, tivoIP, tivo_mak, togo_path))
323 logger.info('[%s] Queued "%s" for transfer to %s' %
324 (time.strftime('%d/%b/%Y %H:%M:%S'),
325 unquote(theurl), togo_path))
326 urlstring = '<br>'.join([unquote(x) for x in urls])
327 message = TRANS_QUEUE % (urlstring, togo_path)
328 else:
329 message = MISSING
330 handler.redir(message, 5)
332 def ToGoStop(self, handler, query):
333 theurl = query['Url'][0]
334 status[theurl]['running'] = False
335 handler.redir(TRANS_STOP % unquote(theurl))
337 def Unqueue(self, handler, query):
338 theurl = query['Url'][0]
339 tivoIP = query['TiVo'][0]
340 del status[theurl]
341 queue[tivoIP].remove(theurl)
342 logger.info('[%s] Removed "%s" from queue' %
343 (time.strftime('%d/%b/%Y %H:%M:%S'),
344 unquote(theurl)))
345 handler.redir(UNQUEUE % unquote(theurl))