Moved metadata functions to their own module.
[pyTivo/TheBayer.git] / plugins / video / video.py
blob62ac3c242b356568d8827645987baafc45f380a4
1 import cgi
2 import logging
3 import os
4 import re
5 import time
6 import traceback
7 import urllib
8 import zlib
9 from UserDict import DictMixin
10 from datetime import datetime, timedelta
11 from xml.sax.saxutils import escape
13 from Cheetah.Template import Template
15 import config
16 import metadata
17 import mind
18 import qtfaststart
19 import transcode
20 from plugin import EncodeUnicode, Plugin, quote
22 logger = logging.getLogger('pyTivo.video.video')
24 SCRIPTDIR = os.path.dirname(__file__)
26 CLASS_NAME = 'Video'
28 # Preload the templates
29 def tmpl(name):
30 return file(os.path.join(SCRIPTDIR, 'templates', name), 'rb').read()
32 CONTAINER_TEMPLATE = tmpl('container.tmpl')
33 TVBUS_TEMPLATE = tmpl('TvBus.tmpl')
34 XSL_TEMPLATE = tmpl('container.xsl')
36 extfile = os.path.join(SCRIPTDIR, 'video.ext')
37 try:
38 assert(config.get_bin('ffmpeg'))
39 extensions = file(extfile).read().split()
40 except:
41 extensions = None
43 class Video(Plugin):
45 CONTENT_TYPE = 'x-container/tivo-videos'
47 def pre_cache(self, full_path):
48 if Video.video_file_filter(self, full_path):
49 transcode.supported_format(full_path)
51 def video_file_filter(self, full_path, type=None):
52 if os.path.isdir(full_path):
53 return True
54 if extensions:
55 return os.path.splitext(full_path)[1].lower() in extensions
56 else:
57 return transcode.supported_format(full_path)
59 def send_file(self, handler, path, query):
60 mime = 'video/mpeg'
61 tsn = handler.headers.getheader('tsn', '')
63 is_tivo_file = (path[-5:].lower() == '.tivo')
65 if is_tivo_file and transcode.tivo_compatible(path, tsn, mime)[0]:
66 mime = 'video/x-tivo-mpeg'
68 if 'Format' in query:
69 mime = query['Format'][0]
71 needs_tivodecode = (is_tivo_file and mime == 'video/mpeg')
72 compatible = (not needs_tivodecode and
73 transcode.tivo_compatible(path, tsn, mime)[0])
75 offset = handler.headers.getheader('Range')
76 if offset:
77 offset = int(offset[6:-1]) # "bytes=XXX-"
79 if needs_tivodecode:
80 valid = bool(config.get_bin('tivodecode') and
81 config.get_server('tivo_mak'))
82 else:
83 valid = True
85 if valid and offset:
86 valid = ((compatible and offset < os.stat(path).st_size) or
87 (not compatible and transcode.is_resumable(path, offset)))
89 handler.send_response(206)
90 handler.send_header('Content-Type', mime)
91 handler.send_header('Connection', 'close')
92 if compatible:
93 handler.send_header('Content-Length',
94 os.stat(path).st_size - offset)
95 else:
96 handler.send_header('Transfer-Encoding', 'chunked')
97 handler.end_headers()
99 if valid:
100 if compatible:
101 logger.debug('%s is tivo compatible' % path)
102 f = open(path, 'rb')
103 try:
104 if mime == 'video/mp4':
105 qtfaststart.fast_start(f, handler.wfile, offset)
106 else:
107 if offset:
108 f.seek(offset)
109 while True:
110 block = f.read(512 * 1024)
111 if not block:
112 break
113 handler.wfile.write(block)
114 except Exception, msg:
115 logger.info(msg)
116 f.close()
117 else:
118 logger.debug('%s is not tivo compatible' % path)
119 if offset:
120 transcode.resume_transfer(path, handler.wfile, offset)
121 else:
122 transcode.transcode(False, path, handler.wfile, tsn)
123 try:
124 if not compatible:
125 handler.wfile.write('0\r\n\r\n')
126 handler.wfile.flush()
127 except Exception, msg:
128 logger.info(msg)
129 logger.debug("Finished outputing video")
131 def __duration(self, full_path):
132 return transcode.video_info(full_path)['millisecs']
134 def __total_items(self, full_path):
135 count = 0
136 try:
137 for f in os.listdir(full_path):
138 if f.startswith('.'):
139 continue
140 f = os.path.join(full_path, f)
141 if os.path.isdir(f):
142 count += 1
143 elif extensions:
144 if os.path.splitext(f)[1].lower() in extensions:
145 count += 1
146 elif f in transcode.info_cache:
147 if transcode.supported_format(f):
148 count += 1
149 except:
150 pass
151 return count
153 def __est_size(self, full_path, tsn='', mime=''):
154 # Size is estimated by taking audio and video bit rate adding 2%
156 if transcode.tivo_compatible(full_path, tsn, mime)[0]:
157 return int(os.stat(full_path).st_size)
158 else:
159 # Must be re-encoded
160 if config.get_tsn('audio_codec', tsn) == None:
161 audioBPS = config.getMaxAudioBR(tsn) * 1000
162 else:
163 audioBPS = config.strtod(config.getAudioBR(tsn))
164 videoBPS = transcode.select_videostr(full_path, tsn)
165 bitrate = audioBPS + videoBPS
166 return int((self.__duration(full_path) / 1000) *
167 (bitrate * 1.02 / 8))
169 def metadata_full(self, full_path, tsn='', mime=''):
170 data = {}
171 vInfo = transcode.video_info(full_path)
173 if ((int(vInfo['vHeight']) >= 720 and
174 config.getTivoHeight >= 720) or
175 (int(vInfo['vWidth']) >= 1280 and
176 config.getTivoWidth >= 1280)):
177 data['showingBits'] = '4096'
179 data.update(metadata.basic(full_path))
180 if full_path[-5:].lower() == '.tivo':
181 data.update(metadata.from_tivo(full_path))
183 if config.getDebug() and 'vHost' not in data:
184 compatible, reason = transcode.tivo_compatible(full_path, tsn, mime)
185 if compatible:
186 transcode_options = {}
187 else:
188 transcode_options = transcode.transcode(True, full_path,
189 '', tsn)
190 data['vHost'] = (
191 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible], reason)] +
192 ['SOURCE INFO: '] +
193 ["%s=%s" % (k, v)
194 for k, v in sorted(vInfo.items(), reverse=True)] +
195 ['TRANSCODE OPTIONS: '] +
196 ["%s" % (v) for k, v in transcode_options.items()] +
197 ['SOURCE FILE: ', os.path.split(full_path)[1]]
200 now = datetime.utcnow()
201 duration = self.__duration(full_path)
202 duration_delta = timedelta(milliseconds = duration)
203 min = duration_delta.seconds / 60
204 sec = duration_delta.seconds % 60
205 hours = min / 60
206 min = min % 60
208 data.update({'time': now.isoformat(),
209 'startTime': now.isoformat(),
210 'stopTime': (now + duration_delta).isoformat(),
211 'size': self.__est_size(full_path, tsn, mime),
212 'duration': duration,
213 'iso_duration': ('P%sDT%sH%sM%sS' %
214 (duration_delta.days, hours, min, sec))})
216 return data
218 def QueryContainer(self, handler, query):
219 tsn = handler.headers.getheader('tsn', '')
220 subcname = query['Container'][0]
221 cname = subcname.split('/')[0]
223 if (not cname in handler.server.containers or
224 not self.get_local_path(handler, query)):
225 handler.send_error(404)
226 return
228 container = handler.server.containers[cname]
229 precache = container.get('precache', 'False').lower() == 'true'
230 force_alpha = container.get('force_alpha', 'False').lower() == 'true'
232 files, total, start = self.get_files(handler, query,
233 self.video_file_filter,
234 force_alpha)
236 videos = []
237 local_base_path = self.get_local_base_path(handler, query)
238 for f in files:
239 mtime = datetime.fromtimestamp(f.mdate)
240 video = VideoDetails()
241 video['captureDate'] = hex(int(time.mktime(mtime.timetuple())))
242 video['name'] = os.path.split(f.name)[1]
243 video['path'] = f.name
244 video['part_path'] = f.name.replace(local_base_path, '', 1)
245 if not video['part_path'].startswith(os.path.sep):
246 video['part_path'] = os.path.sep + video['part_path']
247 video['title'] = os.path.split(f.name)[1]
248 video['is_dir'] = f.isdir
249 if video['is_dir']:
250 video['small_path'] = subcname + '/' + video['name']
251 video['total_items'] = self.__total_items(f.name)
252 else:
253 if precache or len(files) == 1 or f.name in transcode.info_cache:
254 video['valid'] = transcode.supported_format(f.name)
255 if video['valid']:
256 video.update(self.metadata_full(f.name, tsn))
257 else:
258 video['valid'] = True
259 video.update(metadata.basic(f.name))
261 videos.append(video)
263 t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode)
264 t.container = cname
265 t.name = subcname
266 t.total = total
267 t.start = start
268 t.videos = videos
269 t.quote = quote
270 t.escape = escape
271 t.crc = zlib.crc32
272 t.guid = config.getGUID()
273 t.tivos = config.tivos
274 t.tivo_names = config.tivo_names
275 handler.send_response(200)
276 handler.send_header('Content-Type', 'text/xml')
277 handler.end_headers()
278 handler.wfile.write(t)
280 def TVBusQuery(self, handler, query):
281 tsn = handler.headers.getheader('tsn', '')
282 f = query['File'][0]
283 path = self.get_local_path(handler, query)
284 file_path = path + os.path.normpath(f)
286 file_info = VideoDetails()
287 file_info['valid'] = transcode.supported_format(file_path)
288 if file_info['valid']:
289 file_info.update(self.metadata_full(file_path, tsn))
291 t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode)
292 t.video = file_info
293 t.escape = escape
294 handler.send_response(200)
295 handler.send_header('Content-Type', 'text/xml')
296 handler.end_headers()
297 handler.wfile.write(t)
299 def XSL(self, handler, query):
300 handler.send_response(200)
301 handler.send_header('Content-Type', 'text/xml')
302 handler.end_headers()
303 handler.wfile.write(XSL_TEMPLATE)
305 def Push(self, handler, query):
306 tsn = query['tsn'][0]
307 for key in config.tivo_names:
308 if config.tivo_names[key] == tsn:
309 tsn = key
310 break
312 container = quote(query['Container'][0].split('/')[0])
313 ip = config.get_ip()
314 port = config.getPort()
316 baseurl = 'http://%s:%s' % (ip, port)
317 if config.getIsExternal(tsn):
318 exturl = config.get_server('externalurl')
319 if exturl:
320 baseurl = exturl
321 else:
322 ip = self.readip()
323 baseurl = 'http://%s:%s' % (ip, port)
325 path = self.get_local_base_path(handler, query)
327 for f in query.get('File', []):
328 file_path = path + os.path.normpath(f)
330 file_info = VideoDetails()
331 file_info['valid'] = transcode.supported_format(file_path)
333 mime = 'video/mpeg'
334 if config.isHDtivo(tsn):
335 for m in ['video/mp4', 'video/bif']:
336 if transcode.tivo_compatible(file_path, tsn, m)[0]:
337 mime = m
338 break
340 if file_info['valid']:
341 file_info.update(self.metadata_full(file_path, tsn, mime))
343 url = baseurl + '/%s%s' % (container, quote(f))
345 title = file_info['seriesTitle']
346 if not title:
347 title = file_info['title']
349 source = file_info['seriesId']
350 if not source:
351 source = title
353 subtitle = file_info['episodeTitle']
354 logger.debug('Pushing ' + url)
355 try:
356 m = mind.getMind(tsn)
357 m.pushVideo(
358 tsn = tsn,
359 url = url,
360 description = file_info['description'],
361 duration = file_info['duration'] / 1000,
362 size = file_info['size'],
363 title = title,
364 subtitle = subtitle,
365 source = source,
366 mime = mime)
367 except Exception, e:
368 handler.send_response(500)
369 handler.end_headers()
370 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
371 raise
373 referer = handler.headers.getheader('Referer')
374 handler.send_response(302)
375 handler.send_header('Location', referer)
376 handler.end_headers()
378 def readip(self):
379 """ returns your external IP address by querying dyndns.org """
380 f = urllib.urlopen('http://checkip.dyndns.org/')
381 s = f.read()
382 m = re.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s)
383 return m.group(0)
385 class VideoDetails(DictMixin):
387 def __init__(self, d=None):
388 if d:
389 self.d = d
390 else:
391 self.d = {}
393 def __getitem__(self, key):
394 if key not in self.d:
395 self.d[key] = self.default(key)
396 return self.d[key]
398 def __contains__(self, key):
399 return True
401 def __setitem__(self, key, value):
402 self.d[key] = value
404 def __delitem__(self):
405 del self.d[key]
407 def keys(self):
408 return self.d.keys()
410 def __iter__(self):
411 return self.d.__iter__()
413 def iteritems(self):
414 return self.d.iteritems()
416 def default(self, key):
417 defaults = {
418 'showingBits' : '0',
419 'episodeNumber' : '0',
420 'displayMajorNumber' : '0',
421 'displayMinorNumber' : '0',
422 'isEpisode' : 'true',
423 'colorCode' : ('COLOR', '4'),
424 'showType' : ('SERIES', '5'),
425 'tvRating' : ('NR', '7')
427 if key in defaults:
428 return defaults[key]
429 elif key.startswith('v'):
430 return []
431 else:
432 return ''