Re-ordered the import lines to fit with PEP 8.
[pyTivo/wgw.git] / plugins / video / video.py
blob6727622aec3edaa1037c9407f3fa2840a1fc7c52
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['seriesTitle'] = metadata['title'] # default to the filename
163 metadata['originalAirDate'] = originalAirDate.isoformat()
165 metadata.update(self.getMetadataFromTxt(full_path))
167 return metadata
169 def metadata_full(self, full_path, tsn=''):
170 metadata = {}
172 now = datetime.utcnow()
174 duration = self.__duration(full_path)
175 duration_delta = timedelta(milliseconds = duration)
177 metadata['time'] = now.isoformat()
178 metadata['startTime'] = now.isoformat()
179 metadata['stopTime'] = (now + duration_delta).isoformat()
180 metadata['size'] = self.__est_size(full_path, tsn)
181 metadata['duration'] = duration
182 vInfo = transcode.video_info(full_path)
183 transcode_options = {}
184 if not transcode.tivo_compatible(full_path, tsn)[0]:
185 transcode_options = transcode.transcode(True, full_path, '', tsn)
186 metadata['vHost'] = [str(transcode.tivo_compatible(full_path, tsn)[1])]+\
187 ['SOURCE INFO: ']+["%s=%s" % (k, v) for k, v in sorted(transcode.video_info(full_path).items(), reverse=True)]+\
188 ['TRANSCODE OPTIONS: ']+["%s" % (v) for k, v in transcode_options.items()]+\
189 ['SOURCE FILE: ']+[str(os.path.split(full_path)[1])]
190 if not (full_path[-5:]).lower() == '.tivo':
191 if ((int(vInfo['vHeight']) >= 720 and
192 config.getTivoHeight >= 720) or
193 (int(vInfo['vWidth']) >= 1280 and
194 config.getTivoWidth >= 1280)):
195 metadata['showingBits'] = '4096'
197 metadata.update(self.metadata_basic(full_path))
199 min = duration_delta.seconds / 60
200 sec = duration_delta.seconds % 60
201 hours = min / 60
202 min = min % 60
203 metadata['iso_duration'] = 'P' + str(duration_delta.days) + \
204 'DT' + str(hours) + 'H' + str(min) + \
205 'M' + str(sec) + 'S'
206 return metadata
208 def QueryContainer(self, handler, query):
209 tsn = handler.headers.getheader('tsn', '')
210 subcname = query['Container'][0]
211 cname = subcname.split('/')[0]
213 if not handler.server.containers.has_key(cname) or \
214 not self.get_local_path(handler, query):
215 handler.send_response(404)
216 handler.end_headers()
217 return
219 container = handler.server.containers[cname]
220 precache = container.get('precache', 'False').lower() == 'true'
222 files, total, start = self.get_files(handler, query,
223 self.video_file_filter)
225 videos = []
226 local_base_path = self.get_local_base_path(handler, query)
227 for f in files:
228 mtime = os.stat(f).st_mtime
229 if (mtime < 0): mtime = 0
230 mtime = datetime.fromtimestamp(mtime)
231 video = VideoDetails()
232 video['captureDate'] = hex(int(time.mktime(mtime.timetuple())))
233 video['name'] = os.path.split(f)[1]
234 video['path'] = f
235 video['part_path'] = f.replace(local_base_path, '', 1)
236 if not video['part_path'].startswith(os.path.sep):
237 video['part_path'] = os.path.sep + video['part_path']
238 video['title'] = os.path.split(f)[1]
239 video['is_dir'] = self.__isdir(f)
240 if video['is_dir']:
241 video['small_path'] = subcname + '/' + video['name']
242 video['total_items'] = self.__total_items(f)
243 else:
244 if precache or len(files) == 1 or f in transcode.info_cache:
245 video['valid'] = transcode.supported_format(f)
246 if video['valid']:
247 video.update(self.metadata_full(f, tsn))
248 else:
249 video['valid'] = True
250 video.update(self.metadata_basic(f))
252 videos.append(video)
254 handler.send_response(200)
255 handler.end_headers()
256 t = Template(CONTAINER_TEMPLATE)
257 t.container = cname
258 t.name = subcname
259 t.total = total
260 t.start = start
261 t.videos = videos
262 t.quote = quote
263 t.escape = escape
264 t.crc = zlib.crc32
265 t.guid = config.getGUID()
266 t.tivos = handler.tivos
267 t.tivo_names = handler.tivo_names
268 handler.wfile.write(t)
270 def TVBusQuery(self, handler, query):
271 tsn = handler.headers.getheader('tsn', '')
272 f = query['File'][0]
273 path = self.get_local_path(handler, query)
274 file_path = path + f
276 file_info = VideoDetails()
277 file_info['valid'] = transcode.supported_format(file_path)
278 if file_info['valid']:
279 file_info.update(self.metadata_full(file_path, tsn))
281 handler.send_response(200)
282 handler.end_headers()
283 t = Template(TVBUS_TEMPLATE)
284 t.video = file_info
285 t.escape = escape
286 handler.wfile.write(t)
288 def XSL(self, handler, query):
289 handler.send_response(200)
290 handler.end_headers()
291 handler.wfile.write(XSL_TEMPLATE)
293 def Push(self, handler, query):
294 f = unquote(query['File'][0])
296 tsn = query['tsn'][0]
297 for key in handler.tivo_names:
298 if handler.tivo_names[key] == tsn:
299 tsn = key
300 break
302 path = self.get_local_path(handler, query)
303 file_path = path + f
305 file_info = VideoDetails()
306 file_info['valid'] = transcode.supported_format(file_path)
307 if file_info['valid']:
308 file_info.update(self.metadata_full(file_path, tsn))
310 import socket
311 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
312 s.connect(('tivo.com',123))
313 ip = s.getsockname()[0]
314 container = quote(query['Container'][0].split('/')[0])
315 port = config.getPort()
317 url = 'http://%s:%s/%s%s' % (ip, port, container, quote(f))
319 try:
320 m = mind.getMind()
321 m.pushVideo(
322 tsn = tsn,
323 url = url,
324 description = file_info['description'],
325 duration = file_info['duration'] / 1000,
326 size = file_info['size'],
327 title = file_info['title'],
328 subtitle = file_info['name'])
329 except Exception, e:
330 import traceback
331 handler.send_response(500)
332 handler.end_headers()
333 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
334 raise
336 referer = handler.headers.getheader('Referer')
337 handler.send_response(302)
338 handler.send_header('Location', referer)
339 handler.end_headers()
342 class VideoDetails(DictMixin):
344 def __init__(self, d=None):
345 if d:
346 self.d = d
347 else:
348 self.d = {}
350 def __getitem__(self, key):
351 if key not in self.d:
352 self.d[key] = self.default(key)
353 return self.d[key]
355 def __contains__(self, key):
356 return True
358 def __setitem__(self, key, value):
359 self.d[key] = value
361 def __delitem__(self):
362 del self.d[key]
364 def keys(self):
365 return self.d.keys()
367 def __iter__(self):
368 return self.d.__iter__()
370 def iteritems(self):
371 return self.d.iteritems()
373 def default(self, key):
374 defaults = {
375 'showingBits' : '0',
376 'episodeNumber' : '0',
377 'displayMajorNumber' : '0',
378 'displayMinorNumber' : '0',
379 'isEpisode' : 'true',
380 'colorCode' : ('COLOR', '4'),
381 'showType' : ('SERIES', '5'),
382 'tvRating' : ('NR', '7')
384 if key in defaults:
385 return defaults[key]
386 elif key.startswith('v'):
387 return []
388 else:
389 return ''