A small bit of redundant code.
[pyTivo/TheBayer.git] / plugins / video / video.py
blob5097de87ca34caff39d712b157c8ac4d10dc9cdf
1 import cgi
2 import logging
3 import os
4 import re
5 import shutil
6 import subprocess
7 import time
8 import traceback
9 import urllib
10 import zlib
11 from UserDict import DictMixin
12 from datetime import datetime, timedelta
13 from xml.sax.saxutils import escape
15 from Cheetah.Template import Template
16 from lrucache import LRUCache
17 import config
18 import mind
19 import qtfaststart
20 import transcode
21 from plugin import EncodeUnicode, Plugin, quote
23 logger = logging.getLogger('pyTivo.video.video')
25 SCRIPTDIR = os.path.dirname(__file__)
27 CLASS_NAME = 'Video'
29 # Preload the templates
30 def tmpl(name):
31 return file(os.path.join(SCRIPTDIR, 'templates', name), 'rb').read()
33 CONTAINER_TEMPLATE = tmpl('container.tmpl')
34 TVBUS_TEMPLATE = tmpl('TvBus.tmpl')
35 XSL_TEMPLATE = tmpl('container.xsl')
37 extfile = os.path.join(SCRIPTDIR, 'video.ext')
38 try:
39 assert(config.get_bin('ffmpeg'))
40 extensions = file(extfile).read().split()
41 except:
42 extensions = None
44 class Video(Plugin):
46 CONTENT_TYPE = 'x-container/tivo-videos'
48 def pre_cache(self, full_path):
49 if Video.video_file_filter(self, full_path):
50 transcode.supported_format(full_path)
52 def video_file_filter(self, full_path, type=None):
53 if os.path.isdir(full_path):
54 return True
55 if extensions:
56 return os.path.splitext(full_path)[1].lower() in extensions
57 else:
58 return transcode.supported_format(full_path)
60 def send_file(self, handler, path, query):
61 mime = 'video/mpeg'
62 tsn = handler.headers.getheader('tsn', '')
64 is_tivo_file = (path[-5:].lower() == '.tivo')
66 if is_tivo_file and transcode.tivo_compatible(path, tsn, mime)[0]:
67 mime = 'video/x-tivo-mpeg'
69 if 'Format' in query:
70 mime = query['Format'][0]
72 is_tivo_push = (mime == 'video/mpeg' and is_tivo_file)
74 compatible = transcode.tivo_compatible(path, tsn, mime)[0]
76 offset = handler.headers.getheader('Range')
77 if offset:
78 offset = int(offset[6:-1]) # "bytes=XXX-"
80 handler.send_response(206)
81 handler.send_header('Content-Type', mime)
83 if offset:
84 if ((compatible and (is_tivo_push
85 or offset >= os.stat(path).st_size)) or
86 (not compatible and not transcode.is_resumable(path, offset))):
87 handler.send_header('Connection', 'close')
88 handler.send_header('Transfer-Encoding', 'chunked')
89 handler.end_headers()
90 handler.wfile.write('0\r\n')
91 return
93 handler.end_headers()
95 if compatible:
96 logger.debug('%s is tivo compatible' % path)
97 f = open(path, 'rb')
98 try:
99 if mime == 'video/mp4':
100 qtfaststart.fast_start(f, handler.wfile, offset)
101 else:
102 if is_tivo_push:
103 tivodecode_path = config.get_bin('tivodecode')
104 tivo_mak = config.get_server('tivo_mak')
105 if tivodecode_path and tivo_mak:
106 f.close()
107 tcmd = [tivodecode_path, '-m', tivo_mak, path]
108 tivodecode = subprocess.Popen(tcmd,
109 stdout=subprocess.PIPE, bufsize=(512 * 1024))
110 f = tivodecode.stdout
111 elif offset:
112 f.seek(offset)
113 shutil.copyfileobj(f, handler.wfile)
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 logger.debug("Finished outputing video")
125 def __duration(self, full_path):
126 return transcode.video_info(full_path)['millisecs']
128 def __total_items(self, full_path):
129 count = 0
130 try:
131 for f in os.listdir(full_path):
132 if f.startswith('.'):
133 continue
134 f = os.path.join(full_path, f)
135 if os.path.isdir(f):
136 count += 1
137 elif extensions:
138 if os.path.splitext(f)[1].lower() in extensions:
139 count += 1
140 elif f in transcode.info_cache:
141 if transcode.supported_format(f):
142 count += 1
143 except:
144 pass
145 return count
147 def __est_size(self, full_path, tsn='', mime=''):
148 # Size is estimated by taking audio and video bit rate adding 2%
150 if transcode.tivo_compatible(full_path, tsn, mime)[0]:
151 return int(os.stat(full_path).st_size)
152 else:
153 # Must be re-encoded
154 if config.get_tsn('audio_codec', tsn) == None:
155 audioBPS = config.getMaxAudioBR(tsn) * 1000
156 else:
157 audioBPS = config.strtod(config.getAudioBR(tsn))
158 videoBPS = transcode.select_videostr(full_path, tsn)
159 bitrate = audioBPS + videoBPS
160 return int((self.__duration(full_path) / 1000) *
161 (bitrate * 1.02 / 8))
163 def getMetadataFromTxt(self, full_path):
164 metadata = {}
165 path, name = os.path.split(full_path)
166 for metafile in [os.path.join(path, 'default.txt'), full_path + '.txt',
167 os.path.join(path, '.meta', name) + '.txt']:
168 if os.path.exists(metafile):
169 for line in file(metafile):
170 if line.strip().startswith('#') or not ':' in line:
171 continue
172 key, value = [x.strip() for x in line.split(':', 1)]
173 if key.startswith('v'):
174 if key in metadata:
175 metadata[key].append(value)
176 else:
177 metadata[key] = [value]
178 else:
179 metadata[key] = value
180 return metadata
182 def metadata_basic(self, full_path):
183 base_path, title = os.path.split(full_path)
184 mtime = os.stat(full_path).st_mtime
185 if (mtime < 0):
186 mtime = 0
187 originalAirDate = datetime.fromtimestamp(mtime)
189 metadata = {'title': '.'.join(title.split('.')[:-1]),
190 'originalAirDate': originalAirDate.isoformat()}
192 metadata.update(self.getMetadataFromTxt(full_path))
194 return metadata
196 def metadata_full(self, full_path, tsn='', mime=''):
197 metadata = {}
198 vInfo = transcode.video_info(full_path)
199 compat = transcode.tivo_compatible(full_path, tsn, mime)
200 if not compat[0]:
201 transcode_options = transcode.transcode(True, full_path, '', tsn)
202 else:
203 transcode_options = {}
205 if config.getDebug():
206 metadata['vHost'] = (
207 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compat[0]], compat[1])] +
208 ['SOURCE INFO: '] +
209 ["%s=%s" % (k, v)
210 for k, v in sorted(vInfo.items(), reverse=True)] +
211 ['TRANSCODE OPTIONS: '] +
212 ["%s" % (v) for k, v in transcode_options.items()] +
213 ['SOURCE FILE: ', os.path.split(full_path)[1]]
216 if ((int(vInfo['vHeight']) >= 720 and
217 config.getTivoHeight >= 720) or
218 (int(vInfo['vWidth']) >= 1280 and
219 config.getTivoWidth >= 1280)):
220 metadata['showingBits'] = '4096'
222 metadata.update(self.metadata_basic(full_path))
224 now = datetime.utcnow()
225 duration = self.__duration(full_path)
226 duration_delta = timedelta(milliseconds = duration)
227 min = duration_delta.seconds / 60
228 sec = duration_delta.seconds % 60
229 hours = min / 60
230 min = min % 60
232 metadata.update({'time': now.isoformat(),
233 'startTime': now.isoformat(),
234 'stopTime': (now + duration_delta).isoformat(),
235 'size': self.__est_size(full_path, tsn, mime),
236 'duration': duration,
237 'iso_duration': ('P%sDT%sH%sM%sS' %
238 (duration_delta.days, hours, min, sec))})
240 return metadata
242 def QueryContainer(self, handler, query):
243 tsn = handler.headers.getheader('tsn', '')
244 subcname = query['Container'][0]
245 cname = subcname.split('/')[0]
247 if (not cname in handler.server.containers or
248 not self.get_local_path(handler, query)):
249 handler.send_error(404)
250 return
252 container = handler.server.containers[cname]
253 precache = container.get('precache', 'False').lower() == 'true'
254 force_alpha = container.get('force_alpha', 'False').lower() == 'true'
256 files, total, start = self.get_files(handler, query,
257 self.video_file_filter,
258 force_alpha)
260 videos = []
261 local_base_path = self.get_local_base_path(handler, query)
262 for f in files:
263 mtime = datetime.fromtimestamp(f.mdate)
264 video = VideoDetails()
265 video['captureDate'] = hex(int(time.mktime(mtime.timetuple())))
266 video['name'] = os.path.split(f.name)[1]
267 video['path'] = f.name
268 video['part_path'] = f.name.replace(local_base_path, '', 1)
269 if not video['part_path'].startswith(os.path.sep):
270 video['part_path'] = os.path.sep + video['part_path']
271 video['title'] = os.path.split(f.name)[1]
272 video['is_dir'] = f.isdir
273 if video['is_dir']:
274 video['small_path'] = subcname + '/' + video['name']
275 video['total_items'] = self.__total_items(f.name)
276 else:
277 if precache or len(files) == 1 or f.name in transcode.info_cache:
278 video['valid'] = transcode.supported_format(f.name)
279 if video['valid']:
280 video.update(self.metadata_full(f.name, tsn))
281 else:
282 video['valid'] = True
283 video.update(self.metadata_basic(f.name))
285 videos.append(video)
287 t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode)
288 t.container = cname
289 t.name = subcname
290 t.total = total
291 t.start = start
292 t.videos = videos
293 t.quote = quote
294 t.escape = escape
295 t.crc = zlib.crc32
296 t.guid = config.getGUID()
297 t.tivos = config.tivos
298 t.tivo_names = config.tivo_names
299 handler.send_response(200)
300 handler.send_header('Content-Type', 'text/xml')
301 handler.end_headers()
302 handler.wfile.write(t)
304 def TVBusQuery(self, handler, query):
305 tsn = handler.headers.getheader('tsn', '')
306 f = query['File'][0]
307 path = self.get_local_path(handler, query)
308 file_path = path + os.path.normpath(f)
310 file_info = VideoDetails()
311 file_info['valid'] = transcode.supported_format(file_path)
312 if file_info['valid']:
313 file_info.update(self.metadata_full(file_path, tsn))
315 t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode)
316 t.video = file_info
317 t.escape = escape
318 handler.send_response(200)
319 handler.send_header('Content-Type', 'text/xml')
320 handler.end_headers()
321 handler.wfile.write(t)
323 def XSL(self, handler, query):
324 handler.send_response(200)
325 handler.send_header('Content-Type', 'text/xml')
326 handler.end_headers()
327 handler.wfile.write(XSL_TEMPLATE)
329 def Push(self, handler, query):
330 tsn = query['tsn'][0]
331 for key in config.tivo_names:
332 if config.tivo_names[key] == tsn:
333 tsn = key
334 break
336 container = quote(query['Container'][0].split('/')[0])
337 ip = config.get_ip()
338 port = config.getPort()
340 baseurl = 'http://%s:%s' % (ip, port)
341 if config.getIsExternal(tsn):
342 exturl = config.get_server('externalurl')
343 if exturl:
344 baseurl = exturl
345 else:
346 ip = self.readip()
347 baseurl = 'http://%s:%s' % (ip, port)
349 path = self.get_local_base_path(handler, query)
351 for f in query.get('File', []):
352 file_path = path + os.path.normpath(f)
354 file_info = VideoDetails()
355 file_info['valid'] = transcode.supported_format(file_path)
357 mime = 'video/mpeg'
358 if config.isHDtivo(tsn):
359 for m in ['video/mp4', 'video/bif']:
360 if transcode.tivo_compatible(file_path, tsn, m)[0]:
361 mime = m
362 break
364 if file_info['valid']:
365 file_info.update(self.metadata_full(file_path, tsn, mime))
367 url = baseurl + '/%s%s' % (container, quote(f))
369 title = file_info['seriesTitle']
370 if not title:
371 title = file_info['title']
373 source = file_info['seriesId']
374 if not source:
375 source = title
377 subtitle = file_info['episodeTitle']
378 if (not subtitle and file_info['isEpisode'] != 'false' and
379 file_info['seriesTitle']):
380 subtitle = file_info['title']
381 logger.debug('Pushing ' + url)
382 try:
383 m = mind.getMind(tsn)
384 m.pushVideo(
385 tsn = tsn,
386 url = url,
387 description = file_info['description'],
388 duration = file_info['duration'] / 1000,
389 size = file_info['size'],
390 title = title,
391 subtitle = subtitle,
392 source = source,
393 mime = mime)
394 except Exception, e:
395 handler.send_response(500)
396 handler.end_headers()
397 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
398 raise
400 referer = handler.headers.getheader('Referer')
401 handler.send_response(302)
402 handler.send_header('Location', referer)
403 handler.end_headers()
405 def readip(self):
406 """ returns your external IP address by querying dyndns.org """
407 f = urllib.urlopen('http://checkip.dyndns.org/')
408 s = f.read()
409 m = re.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s)
410 return m.group(0)
412 class VideoDetails(DictMixin):
414 def __init__(self, d=None):
415 if d:
416 self.d = d
417 else:
418 self.d = {}
420 def __getitem__(self, key):
421 if key not in self.d:
422 self.d[key] = self.default(key)
423 return self.d[key]
425 def __contains__(self, key):
426 return True
428 def __setitem__(self, key, value):
429 self.d[key] = value
431 def __delitem__(self):
432 del self.d[key]
434 def keys(self):
435 return self.d.keys()
437 def __iter__(self):
438 return self.d.__iter__()
440 def iteritems(self):
441 return self.d.iteritems()
443 def default(self, key):
444 defaults = {
445 'showingBits' : '0',
446 'episodeNumber' : '0',
447 'displayMajorNumber' : '0',
448 'displayMinorNumber' : '0',
449 'isEpisode' : 'true',
450 'colorCode' : ('COLOR', '4'),
451 'showType' : ('SERIES', '5'),
452 'tvRating' : ('NR', '7')
454 if key in defaults:
455 return defaults[key]
456 elif key.startswith('v'):
457 return []
458 else:
459 return ''