Default tvRating to "No Rating" instead of "TV-NR".
[pyTivo/TheBayer.git] / plugins / video / video.py
blob50649562c7641f4a3658589b125fe0d873b3d542
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 if 'time' in data:
202 if data['time'].lower() == 'file':
203 mtime = os.stat(full_path).st_mtime
204 if (mtime < 0):
205 mtime = 0
206 now = datetime.fromtimestamp(mtime)
207 elif data['time'].lower() == 'oad':
208 now = datetime.strptime(data['originalAirDate'][:19],
209 '%Y-%m-%dT%H:%M:%S')
210 else:
211 try:
212 now = datetime.strptime(data['time'][:19],
213 '%Y-%m-%dT%H:%M:%S')
214 except:
215 logger.warning('Bad time format: ' + data['time'] +
216 ' , using current time')
218 duration = self.__duration(full_path)
219 duration_delta = timedelta(milliseconds = duration)
220 min = duration_delta.seconds / 60
221 sec = duration_delta.seconds % 60
222 hours = min / 60
223 min = min % 60
225 data.update({'time': now.isoformat(),
226 'startTime': now.isoformat(),
227 'stopTime': (now + duration_delta).isoformat(),
228 'size': self.__est_size(full_path, tsn, mime),
229 'duration': duration,
230 'iso_duration': ('P%sDT%sH%sM%sS' %
231 (duration_delta.days, hours, min, sec))})
233 return data
235 def QueryContainer(self, handler, query):
236 tsn = handler.headers.getheader('tsn', '')
237 subcname = query['Container'][0]
238 cname = subcname.split('/')[0]
240 if (not cname in handler.server.containers or
241 not self.get_local_path(handler, query)):
242 handler.send_error(404)
243 return
245 container = handler.server.containers[cname]
246 precache = container.get('precache', 'False').lower() == 'true'
247 force_alpha = container.get('force_alpha', 'False').lower() == 'true'
249 files, total, start = self.get_files(handler, query,
250 self.video_file_filter,
251 force_alpha)
253 videos = []
254 local_base_path = self.get_local_base_path(handler, query)
255 for f in files:
256 mtime = datetime.fromtimestamp(f.mdate)
257 video = VideoDetails()
258 video['captureDate'] = hex(int(time.mktime(mtime.timetuple())))
259 video['name'] = os.path.split(f.name)[1]
260 video['path'] = f.name
261 video['part_path'] = f.name.replace(local_base_path, '', 1)
262 if not video['part_path'].startswith(os.path.sep):
263 video['part_path'] = os.path.sep + video['part_path']
264 video['title'] = os.path.split(f.name)[1]
265 video['is_dir'] = f.isdir
266 if video['is_dir']:
267 video['small_path'] = subcname + '/' + video['name']
268 video['total_items'] = self.__total_items(f.name)
269 else:
270 if precache or len(files) == 1 or f.name in transcode.info_cache:
271 video['valid'] = transcode.supported_format(f.name)
272 if video['valid']:
273 video.update(self.metadata_full(f.name, tsn))
274 else:
275 video['valid'] = True
276 video.update(metadata.basic(f.name))
278 videos.append(video)
280 t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode)
281 t.container = cname
282 t.name = subcname
283 t.total = total
284 t.start = start
285 t.videos = videos
286 t.quote = quote
287 t.escape = escape
288 t.crc = zlib.crc32
289 t.guid = config.getGUID()
290 t.tivos = config.tivos
291 t.tivo_names = config.tivo_names
292 handler.send_response(200)
293 handler.send_header('Content-Type', 'text/xml')
294 handler.end_headers()
295 handler.wfile.write(t)
297 def TVBusQuery(self, handler, query):
298 tsn = handler.headers.getheader('tsn', '')
299 f = query['File'][0]
300 path = self.get_local_path(handler, query)
301 file_path = path + os.path.normpath(f)
303 file_info = VideoDetails()
304 file_info['valid'] = transcode.supported_format(file_path)
305 if file_info['valid']:
306 file_info.update(self.metadata_full(file_path, tsn))
308 t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode)
309 t.video = file_info
310 t.escape = escape
311 handler.send_response(200)
312 handler.send_header('Content-Type', 'text/xml')
313 handler.end_headers()
314 handler.wfile.write(t)
316 def XSL(self, handler, query):
317 handler.send_response(200)
318 handler.send_header('Content-Type', 'text/xml')
319 handler.end_headers()
320 handler.wfile.write(XSL_TEMPLATE)
322 def Push(self, handler, query):
323 tsn = query['tsn'][0]
324 for key in config.tivo_names:
325 if config.tivo_names[key] == tsn:
326 tsn = key
327 break
329 container = quote(query['Container'][0].split('/')[0])
330 ip = config.get_ip()
331 port = config.getPort()
333 baseurl = 'http://%s:%s' % (ip, port)
334 if config.getIsExternal(tsn):
335 exturl = config.get_server('externalurl')
336 if exturl:
337 baseurl = exturl
338 else:
339 ip = self.readip()
340 baseurl = 'http://%s:%s' % (ip, port)
342 path = self.get_local_base_path(handler, query)
344 for f in query.get('File', []):
345 file_path = path + os.path.normpath(f)
347 file_info = VideoDetails()
348 file_info['valid'] = transcode.supported_format(file_path)
350 mime = 'video/mpeg'
351 if config.isHDtivo(tsn):
352 for m in ['video/mp4', 'video/bif']:
353 if transcode.tivo_compatible(file_path, tsn, m)[0]:
354 mime = m
355 break
357 if file_info['valid']:
358 file_info.update(self.metadata_full(file_path, tsn, mime))
360 url = baseurl + '/%s%s' % (container, quote(f))
362 title = file_info['seriesTitle']
363 if not title:
364 title = file_info['title']
366 source = file_info['seriesId']
367 if not source:
368 source = title
370 subtitle = file_info['episodeTitle']
371 logger.debug('Pushing ' + url)
372 try:
373 m = mind.getMind(tsn)
374 m.pushVideo(
375 tsn = tsn,
376 url = url,
377 description = file_info['description'],
378 duration = file_info['duration'] / 1000,
379 size = file_info['size'],
380 title = title,
381 subtitle = subtitle,
382 source = source,
383 mime = mime,
384 tvrating = file_info['tvRating'])
385 except Exception, e:
386 handler.send_response(500)
387 handler.end_headers()
388 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
389 raise
391 referer = handler.headers.getheader('Referer')
392 handler.send_response(302)
393 handler.send_header('Location', referer)
394 handler.end_headers()
396 def readip(self):
397 """ returns your external IP address by querying dyndns.org """
398 f = urllib.urlopen('http://checkip.dyndns.org/')
399 s = f.read()
400 m = re.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s)
401 return m.group(0)
403 class VideoDetails(DictMixin):
405 def __init__(self, d=None):
406 if d:
407 self.d = d
408 else:
409 self.d = {}
411 def __getitem__(self, key):
412 if key not in self.d:
413 self.d[key] = self.default(key)
414 return self.d[key]
416 def __contains__(self, key):
417 return True
419 def __setitem__(self, key, value):
420 self.d[key] = value
422 def __delitem__(self):
423 del self.d[key]
425 def keys(self):
426 return self.d.keys()
428 def __iter__(self):
429 return self.d.__iter__()
431 def iteritems(self):
432 return self.d.iteritems()
434 def default(self, key):
435 defaults = {
436 'showingBits' : '0',
437 'episodeNumber' : '0',
438 'displayMajorNumber' : '0',
439 'displayMinorNumber' : '0',
440 'isEpisode' : 'true',
441 'colorCode' : ('COLOR', '4'),
442 'showType' : ('SERIES', '5')
444 if key in defaults:
445 return defaults[key]
446 elif key.startswith('v'):
447 return []
448 else:
449 return ''