If no referer for Push, return 200 OK. For compatibility with some
[pyTivo/TheBayer.git] / plugins / video / video.py
bloba46fd92831e712aae4ed7e676ccd02b15e10d357
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')
35 extfile = os.path.join(SCRIPTDIR, 'video.ext')
36 try:
37 assert(config.get_bin('ffmpeg'))
38 extensions = file(extfile).read().split()
39 except:
40 extensions = None
42 class Video(Plugin):
44 CONTENT_TYPE = 'x-container/tivo-videos'
46 def pre_cache(self, full_path):
47 if Video.video_file_filter(self, full_path):
48 transcode.supported_format(full_path)
50 def video_file_filter(self, full_path, type=None):
51 if os.path.isdir(full_path):
52 return True
53 if extensions:
54 return os.path.splitext(full_path)[1].lower() in extensions
55 else:
56 return transcode.supported_format(full_path)
58 def send_file(self, handler, path, query):
59 mime = 'video/mpeg'
60 tsn = handler.headers.getheader('tsn', '')
62 is_tivo_file = (path[-5:].lower() == '.tivo')
64 if is_tivo_file and transcode.tivo_compatible(path, tsn, mime)[0]:
65 mime = 'video/x-tivo-mpeg'
67 if 'Format' in query:
68 mime = query['Format'][0]
70 needs_tivodecode = (is_tivo_file and mime == 'video/mpeg')
71 compatible = (not needs_tivodecode and
72 transcode.tivo_compatible(path, tsn, mime)[0])
74 offset = handler.headers.getheader('Range')
75 if offset:
76 offset = int(offset[6:-1]) # "bytes=XXX-"
78 if needs_tivodecode:
79 valid = bool(config.get_bin('tivodecode') and
80 config.get_server('tivo_mak'))
81 else:
82 valid = True
84 if valid and offset:
85 valid = ((compatible and offset < os.stat(path).st_size) or
86 (not compatible and transcode.is_resumable(path, offset)))
88 handler.send_response(206)
89 handler.send_header('Content-Type', mime)
90 handler.send_header('Connection', 'close')
91 if compatible:
92 handler.send_header('Content-Length',
93 os.stat(path).st_size - offset)
94 else:
95 handler.send_header('Transfer-Encoding', 'chunked')
96 handler.end_headers()
98 if valid:
99 if compatible:
100 logger.debug('%s is tivo compatible' % path)
101 f = open(path, 'rb')
102 try:
103 if mime == 'video/mp4':
104 qtfaststart.fast_start(f, handler.wfile, offset)
105 else:
106 if offset:
107 f.seek(offset)
108 while True:
109 block = f.read(512 * 1024)
110 if not block:
111 break
112 handler.wfile.write(block)
113 except Exception, msg:
114 logger.info(msg)
115 f.close()
116 else:
117 logger.debug('%s is not tivo compatible' % path)
118 if offset:
119 transcode.resume_transfer(path, handler.wfile, offset)
120 else:
121 transcode.transcode(False, path, handler.wfile, tsn)
122 try:
123 if not compatible:
124 handler.wfile.write('0\r\n\r\n')
125 handler.wfile.flush()
126 except Exception, msg:
127 logger.info(msg)
128 logger.debug("Finished outputing video")
130 def __duration(self, full_path):
131 return transcode.video_info(full_path)['millisecs']
133 def __total_items(self, full_path):
134 count = 0
135 try:
136 for f in os.listdir(full_path):
137 if f.startswith('.'):
138 continue
139 f = os.path.join(full_path, f)
140 if os.path.isdir(f):
141 count += 1
142 elif extensions:
143 if os.path.splitext(f)[1].lower() in extensions:
144 count += 1
145 elif f in transcode.info_cache:
146 if transcode.supported_format(f):
147 count += 1
148 except:
149 pass
150 return count
152 def __est_size(self, full_path, tsn='', mime=''):
153 # Size is estimated by taking audio and video bit rate adding 2%
155 if transcode.tivo_compatible(full_path, tsn, mime)[0]:
156 return int(os.stat(full_path).st_size)
157 else:
158 # Must be re-encoded
159 if config.get_tsn('audio_codec', tsn) == None:
160 audioBPS = config.getMaxAudioBR(tsn) * 1000
161 else:
162 audioBPS = config.strtod(config.getAudioBR(tsn))
163 videoBPS = transcode.select_videostr(full_path, tsn)
164 bitrate = audioBPS + videoBPS
165 return int((self.__duration(full_path) / 1000) *
166 (bitrate * 1.02 / 8))
168 def metadata_full(self, full_path, tsn='', mime=''):
169 data = {}
170 vInfo = transcode.video_info(full_path)
172 if ((int(vInfo['vHeight']) >= 720 and
173 config.getTivoHeight >= 720) or
174 (int(vInfo['vWidth']) >= 1280 and
175 config.getTivoWidth >= 1280)):
176 data['showingBits'] = '4096'
178 data.update(metadata.basic(full_path))
179 if full_path[-5:].lower() == '.tivo':
180 data.update(metadata.from_tivo(full_path))
182 if 'episodeNumber' in data:
183 try:
184 int(data['episodeNumber'])
185 except:
186 data['episodeNumber'] = '0'
188 if config.getDebug() and 'vHost' not in data:
189 compatible, reason = transcode.tivo_compatible(full_path, tsn, mime)
190 if compatible:
191 transcode_options = {}
192 else:
193 transcode_options = transcode.transcode(True, full_path,
194 '', tsn)
195 data['vHost'] = (
196 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible], reason)] +
197 ['SOURCE INFO: '] +
198 ["%s=%s" % (k, v)
199 for k, v in sorted(vInfo.items(), reverse=True)] +
200 ['TRANSCODE OPTIONS: '] +
201 ["%s" % (v) for k, v in transcode_options.items()] +
202 ['SOURCE FILE: ', os.path.split(full_path)[1]]
205 now = datetime.utcnow()
206 if 'time' in data:
207 if data['time'].lower() == 'file':
208 mtime = os.stat(full_path).st_mtime
209 if (mtime < 0):
210 mtime = 0
211 try:
212 now = datetime.utcfromtimestamp(mtime)
213 except:
214 logger.warning('Bad file time on ' + full_path)
215 elif data['time'].lower() == 'oad':
216 now = datetime.strptime(data['originalAirDate'][:19],
217 '%Y-%m-%dT%H:%M:%S')
218 else:
219 try:
220 now = datetime.strptime(data['time'][:19],
221 '%Y-%m-%dT%H:%M:%S')
222 except:
223 logger.warning('Bad time format: ' + data['time'] +
224 ' , using current time')
226 duration = self.__duration(full_path)
227 duration_delta = timedelta(milliseconds = duration)
228 min = duration_delta.seconds / 60
229 sec = duration_delta.seconds % 60
230 hours = min / 60
231 min = min % 60
233 data.update({'time': now.isoformat(),
234 'startTime': now.isoformat(),
235 'stopTime': (now + duration_delta).isoformat(),
236 'size': self.__est_size(full_path, tsn, mime),
237 'duration': duration,
238 'iso_duration': ('P%sDT%sH%sM%sS' %
239 (duration_delta.days, hours, min, sec))})
241 return data
243 def QueryContainer(self, handler, query):
244 tsn = handler.headers.getheader('tsn', '')
245 subcname = query['Container'][0]
246 cname = subcname.split('/')[0]
248 if (not cname in handler.server.containers or
249 not self.get_local_path(handler, query)):
250 handler.send_error(404)
251 return
253 container = handler.server.containers[cname]
254 precache = container.get('precache', 'False').lower() == 'true'
255 force_alpha = container.get('force_alpha', 'False').lower() == 'true'
257 files, total, start = self.get_files(handler, query,
258 self.video_file_filter,
259 force_alpha)
261 videos = []
262 local_base_path = self.get_local_base_path(handler, query)
263 for f in files:
264 try:
265 mtime = datetime.utcfromtimestamp(f.mdate)
266 except:
267 logger.warning('Bad file time on ' + f.name)
268 mtime = datetime.utcnow()
269 video = VideoDetails()
270 video['captureDate'] = hex(int(time.mktime(mtime.timetuple())))
271 video['name'] = os.path.split(f.name)[1]
272 video['path'] = f.name
273 video['part_path'] = f.name.replace(local_base_path, '', 1)
274 if not video['part_path'].startswith(os.path.sep):
275 video['part_path'] = os.path.sep + video['part_path']
276 video['title'] = os.path.split(f.name)[1]
277 video['is_dir'] = f.isdir
278 if video['is_dir']:
279 video['small_path'] = subcname + '/' + video['name']
280 video['total_items'] = self.__total_items(f.name)
281 else:
282 if precache or len(files) == 1 or f.name in transcode.info_cache:
283 video['valid'] = transcode.supported_format(f.name)
284 if video['valid']:
285 video.update(self.metadata_full(f.name, tsn))
286 else:
287 video['valid'] = True
288 video.update(metadata.basic(f.name))
290 videos.append(video)
292 t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode)
293 t.container = cname
294 t.name = subcname
295 t.total = total
296 t.start = start
297 t.videos = videos
298 t.quote = quote
299 t.escape = escape
300 t.crc = zlib.crc32
301 t.guid = config.getGUID()
302 t.tivos = config.tivos
303 t.tivo_names = config.tivo_names
304 handler.send_response(200)
305 handler.send_header('Content-Type', 'text/xml')
306 handler.end_headers()
307 handler.wfile.write(t)
309 def TVBusQuery(self, handler, query):
310 tsn = handler.headers.getheader('tsn', '')
311 f = query['File'][0]
312 path = self.get_local_path(handler, query)
313 file_path = path + os.path.normpath(f)
315 file_info = VideoDetails()
316 file_info['valid'] = transcode.supported_format(file_path)
317 if file_info['valid']:
318 file_info.update(self.metadata_full(file_path, tsn))
320 t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode)
321 t.video = file_info
322 t.escape = escape
323 handler.send_response(200)
324 handler.send_header('Content-Type', 'text/xml')
325 handler.end_headers()
326 handler.wfile.write(t)
328 def Push(self, handler, query):
329 tsn = query['tsn'][0]
330 for key in config.tivo_names:
331 if config.tivo_names[key] == tsn:
332 tsn = key
333 break
335 container = quote(query['Container'][0].split('/')[0])
336 ip = config.get_ip()
337 port = config.getPort()
339 baseurl = 'http://%s:%s' % (ip, port)
340 if config.getIsExternal(tsn):
341 exturl = config.get_server('externalurl')
342 if exturl:
343 baseurl = exturl
344 else:
345 ip = self.readip()
346 baseurl = 'http://%s:%s' % (ip, port)
348 path = self.get_local_base_path(handler, query)
350 for f in query.get('File', []):
351 file_path = path + os.path.normpath(f)
353 file_info = VideoDetails()
354 file_info['valid'] = transcode.supported_format(file_path)
356 mime = 'video/mpeg'
357 if config.isHDtivo(tsn):
358 for m in ['video/mp4', 'video/bif']:
359 if transcode.tivo_compatible(file_path, tsn, m)[0]:
360 mime = m
361 break
363 if file_info['valid']:
364 file_info.update(self.metadata_full(file_path, tsn, mime))
366 url = baseurl + '/%s%s' % (container, quote(f))
368 title = file_info['seriesTitle']
369 if not title:
370 title = file_info['title']
372 source = file_info['seriesId']
373 if not source:
374 source = title
376 subtitle = file_info['episodeTitle']
377 logger.debug('Pushing ' + url)
378 try:
379 m = mind.getMind(tsn)
380 m.pushVideo(
381 tsn = tsn,
382 url = url,
383 description = file_info['description'],
384 duration = file_info['duration'] / 1000,
385 size = file_info['size'],
386 title = title,
387 subtitle = subtitle,
388 source = source,
389 mime = mime,
390 tvrating = file_info['tvRating'])
391 except Exception, e:
392 handler.send_response(500)
393 handler.end_headers()
394 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
395 raise
397 referer = handler.headers.getheader('Referer')
398 if referer:
399 handler.send_response(302)
400 handler.send_header('Location', referer)
401 else:
402 handler.send_response(200)
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')
453 if key in defaults:
454 return defaults[key]
455 elif key.startswith('v'):
456 return []
457 else:
458 return ''