Kill this awful kludge at last.
[pyTivo/wgw.git] / plugins / video / video.py
blob3015c7e16c55ee433f8a52e331d7307c1bc646d1
1 import logging
2 import os
3 import re
4 import socket
5 import time
6 import urllib
7 import zlib
8 from UserDict import DictMixin
9 from datetime import datetime, timedelta
10 from urlparse import urlparse
11 from xml.sax.saxutils import escape
13 from Cheetah.Template import Template
14 from lrucache import LRUCache
15 import config
16 import mind
17 import transcode
18 from plugin import Plugin, quote, unquote
20 SCRIPTDIR = os.path.dirname(__file__)
22 CLASS_NAME = 'Video'
24 # Preload the templates
25 tcname = os.path.join(SCRIPTDIR, 'templates', 'container.tmpl')
26 ttname = os.path.join(SCRIPTDIR, 'templates', 'TvBus.tmpl')
27 txname = os.path.join(SCRIPTDIR, 'templates', 'container.xsl')
28 CONTAINER_TEMPLATE = file(tcname, 'rb').read()
29 TVBUS_TEMPLATE = file(ttname, 'rb').read()
30 XSL_TEMPLATE = file(txname, 'rb').read()
32 extfile = os.path.join(SCRIPTDIR, 'video.ext')
33 try:
34 extensions = file(extfile).read().split()
35 except:
36 extensions = None
38 class Video(Plugin):
40 CONTENT_TYPE = 'x-container/tivo-videos'
42 def pre_cache(self, full_path):
43 if Video.video_file_filter(self, full_path):
44 transcode.supported_format(full_path)
46 def video_file_filter(self, full_path, type=None):
47 if os.path.isdir(full_path):
48 return True
49 if extensions:
50 return os.path.splitext(full_path)[1].lower() in extensions
51 else:
52 return transcode.supported_format(full_path)
54 def send_file(self, handler, container, name):
55 if (handler.headers.getheader('Range') and
56 handler.headers.getheader('Range') != 'bytes=0-'):
57 handler.send_response(206)
58 handler.send_header('Connection', 'close')
59 handler.send_header('Content-Type', 'video/x-tivo-mpeg')
60 handler.send_header('Transfer-Encoding', 'chunked')
61 handler.end_headers()
62 handler.wfile.write("\x30\x0D\x0A")
63 return
65 tsn = handler.headers.getheader('tsn', '')
67 o = urlparse("http://fake.host" + handler.path)
68 path = unquote(o[2])
69 handler.send_response(200)
70 handler.end_headers()
71 transcode.output_video(container['path'] + path[len(name) + 1:],
72 handler.wfile, tsn)
74 def __isdir(self, full_path):
75 return os.path.isdir(full_path)
77 def __duration(self, full_path):
78 return transcode.video_info(full_path)['millisecs']
80 def __total_items(self, full_path):
81 count = 0
82 try:
83 for f in os.listdir(full_path):
84 if f.startswith('.'):
85 continue
86 f = os.path.join(full_path, f)
87 if os.path.isdir(f):
88 count += 1
89 elif extensions:
90 if os.path.splitext(f)[1].lower() in extensions:
91 count += 1
92 elif f in transcode.info_cache:
93 if transcode.supported_format(f):
94 count += 1
95 except:
96 pass
97 return count
99 def __est_size(self, full_path, tsn = ''):
100 # Size is estimated by taking audio and video bit rate adding 2%
102 if transcode.tivo_compatible(full_path, tsn)[0]:
103 # Is TiVo-compatible mpeg2
104 return int(os.stat(full_path).st_size)
105 else:
106 # Must be re-encoded
107 if config.getAudioCodec(tsn) == None:
108 audioBPS = config.getMaxAudioBR(tsn)*1000
109 else:
110 audioBPS = config.strtod(config.getAudioBR(tsn))
111 videoBPS = transcode.select_videostr(full_path, tsn)
112 bitrate = audioBPS + videoBPS
113 return int((self.__duration(full_path) / 1000) *
114 (bitrate * 1.02 / 8))
116 def getMetadataFromTxt(self, full_path):
117 metadata = {}
119 default_meta = os.path.join(os.path.split(full_path)[0], 'default.txt')
120 standard_meta = full_path + '.txt'
121 subdir_meta = os.path.join(os.path.dirname(full_path), '.meta',
122 os.path.basename(full_path)) + '.txt'
124 for metafile in (default_meta, standard_meta, subdir_meta):
125 metadata.update(self.__getMetadataFromFile(metafile))
127 return metadata
129 def __getMetadataFromFile(self, f):
130 metadata = {}
132 if os.path.exists(f):
133 for line in open(f):
134 if line.strip().startswith('#'):
135 continue
136 if not ':' in line:
137 continue
139 key, value = line.split(':', 1)
140 key = key.strip()
141 value = value.strip()
143 if key.startswith('v'):
144 if key in metadata:
145 metadata[key].append(value)
146 else:
147 metadata[key] = [value]
148 else:
149 metadata[key] = value
151 return metadata
153 def metadata_basic(self, full_path):
154 metadata = {}
156 base_path, title = os.path.split(full_path)
157 ctime = os.stat(full_path).st_ctime
158 if (ctime < 0): ctime = 0
159 originalAirDate = datetime.fromtimestamp(ctime)
161 metadata['title'] = '.'.join(title.split('.')[:-1])
162 metadata['originalAirDate'] = originalAirDate.isoformat()
164 metadata.update(self.getMetadataFromTxt(full_path))
166 return metadata
168 def metadata_full(self, full_path, tsn=''):
169 metadata = {}
171 now = datetime.utcnow()
173 duration = self.__duration(full_path)
174 duration_delta = timedelta(milliseconds = duration)
176 metadata['time'] = now.isoformat()
177 metadata['startTime'] = now.isoformat()
178 metadata['stopTime'] = (now + duration_delta).isoformat()
179 metadata['size'] = self.__est_size(full_path, tsn)
180 metadata['duration'] = duration
181 vInfo = transcode.video_info(full_path)
182 transcode_options = {}
183 if not transcode.tivo_compatible(full_path, tsn)[0]:
184 transcode_options = transcode.transcode(True, full_path, '', tsn)
185 metadata['vHost'] = [str(transcode.tivo_compatible(full_path, tsn)[1])]+\
186 ['SOURCE INFO: ']+["%s=%s" % (k, v) for k, v in sorted(transcode.video_info(full_path).items(), reverse=True)]+\
187 ['TRANSCODE OPTIONS: ']+["%s" % (v) for k, v in transcode_options.items()]+\
188 ['SOURCE FILE: ']+[str(os.path.split(full_path)[1])]
189 if not (full_path[-5:]).lower() == '.tivo':
190 if ((int(vInfo['vHeight']) >= 720 and
191 config.getTivoHeight >= 720) or
192 (int(vInfo['vWidth']) >= 1280 and
193 config.getTivoWidth >= 1280)):
194 metadata['showingBits'] = '4096'
196 metadata.update(self.metadata_basic(full_path))
198 min = duration_delta.seconds / 60
199 sec = duration_delta.seconds % 60
200 hours = min / 60
201 min = min % 60
202 metadata['iso_duration'] = ('P%sDT%sH%sM%sS' %
203 (duration_delta.days, hours, min, sec))
205 return metadata
207 def QueryContainer(self, handler, query):
208 tsn = handler.headers.getheader('tsn', '')
209 subcname = query['Container'][0]
210 cname = subcname.split('/')[0]
212 if (not cname in handler.server.containers or
213 not self.get_local_path(handler, query)):
214 handler.send_response(404)
215 handler.end_headers()
216 return
218 container = handler.server.containers[cname]
219 precache = container.get('precache', 'False').lower() == 'true'
221 files, total, start = self.get_files(handler, query,
222 self.video_file_filter)
224 videos = []
225 local_base_path = self.get_local_base_path(handler, query)
226 for f in files:
227 mtime = os.stat(f).st_mtime
228 if (mtime < 0): mtime = 0
229 mtime = datetime.fromtimestamp(mtime)
230 video = VideoDetails()
231 video['captureDate'] = hex(int(time.mktime(mtime.timetuple())))
232 video['name'] = os.path.split(f)[1]
233 video['path'] = f
234 video['part_path'] = f.replace(local_base_path, '', 1)
235 if not video['part_path'].startswith(os.path.sep):
236 video['part_path'] = os.path.sep + video['part_path']
237 video['title'] = os.path.split(f)[1]
238 video['is_dir'] = self.__isdir(f)
239 if video['is_dir']:
240 video['small_path'] = subcname + '/' + video['name']
241 video['total_items'] = self.__total_items(f)
242 else:
243 if precache or len(files) == 1 or f in transcode.info_cache:
244 video['valid'] = transcode.supported_format(f)
245 if video['valid']:
246 video.update(self.metadata_full(f, tsn))
247 else:
248 video['valid'] = True
249 video.update(self.metadata_basic(f))
251 videos.append(video)
253 t = Template(CONTAINER_TEMPLATE)
254 t.container = cname
255 t.name = subcname
256 t.total = total
257 t.start = start
258 t.videos = videos
259 t.quote = quote
260 t.escape = escape
261 t.crc = zlib.crc32
262 t.guid = config.getGUID()
263 t.tivos = handler.tivos
264 t.tivo_names = handler.tivo_names
265 handler.send_response(200)
266 handler.end_headers()
267 handler.wfile.write(t)
269 def TVBusQuery(self, handler, query):
270 tsn = handler.headers.getheader('tsn', '')
271 f = query['File'][0]
272 path = self.get_local_path(handler, query)
273 file_path = path + f
275 file_info = VideoDetails()
276 file_info['valid'] = transcode.supported_format(file_path)
277 if file_info['valid']:
278 file_info.update(self.metadata_full(file_path, tsn))
280 t = Template(TVBUS_TEMPLATE)
281 t.video = file_info
282 t.escape = escape
283 handler.send_response(200)
284 handler.end_headers()
285 handler.wfile.write(t)
287 def XSL(self, handler, query):
288 handler.send_response(200)
289 handler.end_headers()
290 handler.wfile.write(XSL_TEMPLATE)
292 def Push(self, handler, query):
293 f = unquote(query['File'][0])
295 tsn = query['tsn'][0]
296 for key in handler.tivo_names:
297 if handler.tivo_names[key] == tsn:
298 tsn = key
299 break
301 path = self.get_local_path(handler, query)
302 file_path = path + f
304 file_info = VideoDetails()
305 file_info['valid'] = transcode.supported_format(file_path)
306 if file_info['valid']:
307 file_info.update(self.metadata_full(file_path, tsn))
309 import socket
310 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
311 s.connect(('tivo.com',123))
312 ip = s.getsockname()[0]
313 container = quote(query['Container'][0].split('/')[0])
314 port = config.getPort()
316 url = 'http://%s:%s/%s%s' % (ip, port, container, quote(f))
318 try:
319 m = mind.getMind()
320 m.pushVideo(
321 tsn = tsn,
322 url = url,
323 description = file_info['description'],
324 duration = file_info['duration'] / 1000,
325 size = file_info['size'],
326 title = file_info['title'],
327 subtitle = file_info['name'])
328 except Exception, e:
329 import traceback
330 handler.send_response(500)
331 handler.end_headers()
332 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
333 raise
335 referer = handler.headers.getheader('Referer')
336 handler.send_response(302)
337 handler.send_header('Location', referer)
338 handler.end_headers()
341 class VideoDetails(DictMixin):
343 def __init__(self, d=None):
344 if d:
345 self.d = d
346 else:
347 self.d = {}
349 def __getitem__(self, key):
350 if key not in self.d:
351 self.d[key] = self.default(key)
352 return self.d[key]
354 def __contains__(self, key):
355 return True
357 def __setitem__(self, key, value):
358 self.d[key] = value
360 def __delitem__(self):
361 del self.d[key]
363 def keys(self):
364 return self.d.keys()
366 def __iter__(self):
367 return self.d.__iter__()
369 def iteritems(self):
370 return self.d.iteritems()
372 def default(self, key):
373 defaults = {
374 'showingBits' : '0',
375 'episodeNumber' : '0',
376 'displayMajorNumber' : '0',
377 'displayMinorNumber' : '0',
378 'isEpisode' : 'true',
379 'colorCode' : ('COLOR', '4'),
380 'showType' : ('SERIES', '5'),
381 'tvRating' : ('NR', '7')
383 if key in defaults:
384 return defaults[key]
385 elif key.startswith('v'):
386 return []
387 else:
388 return ''