Some additional fields are missing from empty folders on the Premiere
[pyTivo/wmcbrine.git] / plugins / togo / togo.py
blobccddfce5d55196da2b8364f8117d42b68f678c5a
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.tname = tivo_name
199 t.tivoIP = tivoIP
200 t.container = handler.cname
201 t.data = data
202 t.len = len
203 t.TotalItems = getint(TotalItems)
204 t.ItemStart = getint(ItemStart)
205 t.ItemCount = getint(ItemCount)
206 t.FirstAnchor = quote(FirstAnchor)
207 t.shows_per_page = shows_per_page
208 t.title = title
209 handler.send_html(str(t), refresh='300')
211 def get_tivo_file(self, tivoIP, url, mak, togo_path):
212 # global status
213 status[url].update({'running': True, 'queued': False})
215 parse_url = urlparse.urlparse(url)
217 name = unquote(parse_url[2])[10:].split('.')
218 id = unquote(parse_url[4]).split('id=')[1]
219 name.insert(-1, ' - ' + id + '.')
220 if status[url]['decode']:
221 name[-1] = 'mpg'
222 outfile = os.path.join(togo_path, ''.join(name))
224 if status[url]['save']:
225 meta = basic_meta[url]
226 details_url = 'https://%s/TiVoVideoDetails?id=%s' % (tivoIP, id)
227 try:
228 handle = self.tivo_open(details_url)
229 meta.update(metadata.from_details(handle))
230 handle.close()
231 except:
232 pass
233 metafile = open(outfile + '.txt', 'w')
234 metadata.dump(metafile, meta)
235 metafile.close()
237 auth_handler.add_password('TiVo DVR', url, 'tivo', mak)
238 try:
239 handle = self.tivo_open(url)
240 except urllib2.HTTPError, e:
241 status[url]['running'] = False
242 status[url]['error'] = e.code
243 logger.error(e.code)
244 return
245 except urllib2.URLError, e:
246 status[url]['running'] = False
247 status[url]['error'] = e.reason
248 logger.error(e.reason)
249 return
251 tivo_name = config.tivo_names[config.tivos_by_ip(tivoIP)]
253 logger.info('[%s] Start getting "%s" from %s' %
254 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile, tivo_name))
256 if status[url]['decode']:
257 tivodecode_path = config.get_bin('tivodecode')
258 tcmd = [tivodecode_path, '-m', mak, '-o', outfile, '-']
259 tivodecode = subprocess.Popen(tcmd, stdin=subprocess.PIPE,
260 bufsize=(512 * 1024))
261 f = tivodecode.stdin
262 else:
263 f = open(outfile, 'wb')
264 length = 0
265 start_time = time.time()
266 last_interval = start_time
267 now = start_time
268 try:
269 while status[url]['running']:
270 output = handle.read(1024000)
271 if not output:
272 break
273 length += len(output)
274 f.write(output)
275 now = time.time()
276 elapsed = now - last_interval
277 if elapsed >= 5:
278 status[url]['rate'] = '%.2f Mb/s' % (length * 8.0 /
279 (elapsed * 1024 * 1024))
280 status[url]['size'] += length
281 length = 0
282 last_interval = now
283 if status[url]['running']:
284 status[url]['finished'] = True
285 except Exception, msg:
286 status[url]['running'] = False
287 logger.info(msg)
288 handle.close()
289 f.close()
290 status[url]['size'] += length
291 if status[url]['running']:
292 mega_elapsed = (now - start_time) * 1024 * 1024
293 if mega_elapsed < 1:
294 mega_elapsed = 1
295 size = status[url]['size']
296 rate = size * 8.0 / mega_elapsed
297 logger.info('[%s] Done getting "%s" from %s, %d bytes, %.2f Mb/s' %
298 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
299 tivo_name, size, rate))
300 status[url]['running'] = False
301 else:
302 os.remove(outfile)
303 if status[url]['save']:
304 os.remove(outfile + '.txt')
305 logger.info('[%s] Transfer of "%s" from %s aborted' %
306 (time.strftime('%d/%b/%Y %H:%M:%S'), outfile,
307 tivo_name))
308 del status[url]
310 def process_queue(self, tivoIP, mak, togo_path):
311 while queue[tivoIP]:
312 time.sleep(5)
313 url = queue[tivoIP][0]
314 self.get_tivo_file(tivoIP, url, mak, togo_path)
315 queue[tivoIP].pop(0)
316 del queue[tivoIP]
318 def ToGo(self, handler, query):
319 togo_path = config.get_server('togo_path')
320 for name, data in config.getShares():
321 if togo_path == name:
322 togo_path = data.get('path')
323 if togo_path:
324 tivoIP = query['TiVo'][0]
325 tsn = config.tivos_by_ip(tivoIP)
326 tivo_mak = config.get_tsn('tivo_mak', tsn)
327 urls = query.get('Url', [])
328 decode = 'decode' in query
329 save = 'save' in query
330 for theurl in urls:
331 status[theurl] = {'running': False, 'error': '', 'rate': '',
332 'queued': True, 'size': 0, 'finished': False,
333 'decode': decode, 'save': save}
334 if tivoIP in queue:
335 queue[tivoIP].append(theurl)
336 else:
337 queue[tivoIP] = [theurl]
338 thread.start_new_thread(ToGo.process_queue,
339 (self, tivoIP, tivo_mak, togo_path))
340 logger.info('[%s] Queued "%s" for transfer to %s' %
341 (time.strftime('%d/%b/%Y %H:%M:%S'),
342 unquote(theurl), togo_path))
343 urlstring = '<br>'.join([unquote(x) for x in urls])
344 message = TRANS_QUEUE % (urlstring, togo_path)
345 else:
346 message = MISSING
347 handler.redir(message, 5)
349 def ToGoStop(self, handler, query):
350 theurl = query['Url'][0]
351 status[theurl]['running'] = False
352 handler.redir(TRANS_STOP % unquote(theurl))
354 def Unqueue(self, handler, query):
355 theurl = query['Url'][0]
356 tivoIP = query['TiVo'][0]
357 del status[theurl]
358 queue[tivoIP].remove(theurl)
359 logger.info('[%s] Removed "%s" from queue' %
360 (time.strftime('%d/%b/%Y %H:%M:%S'),
361 unquote(theurl)))
362 handler.redir(UNQUEUE % unquote(theurl))