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
18 from plugin
import Plugin
, quote
, unquote
20 SCRIPTDIR
= os
.path
.dirname(__file__
)
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')
34 extensions
= file(extfile
).read().split()
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
):
50 return os
.path
.splitext(full_path
)[1].lower() in extensions
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')
62 handler
.wfile
.write("\x30\x0D\x0A")
65 tsn
= handler
.headers
.getheader('tsn', '')
67 o
= urlparse("http://fake.host" + handler
.path
)
69 handler
.send_response(200)
71 transcode
.output_video(container
['path'] + path
[len(name
) + 1:],
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
):
83 for f
in os
.listdir(full_path
):
86 f
= os
.path
.join(full_path
, f
)
90 if os
.path
.splitext(f
)[1].lower() in extensions
:
92 elif f
in transcode
.info_cache
:
93 if transcode
.supported_format(f
):
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
)
107 if config
.getAudioCodec(tsn
) == None:
108 audioBPS
= config
.getMaxAudioBR(tsn
)*1000
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
):
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
))
129 def __getMetadataFromFile(self
, f
):
132 if os
.path
.exists(f
):
134 if line
.strip().startswith('#'):
139 key
, value
= line
.split(':', 1)
141 value
= value
.strip()
143 if key
.startswith('v'):
145 metadata
[key
].append(value
)
147 metadata
[key
] = [value
]
149 metadata
[key
] = value
153 def metadata_basic(self
, full_path
):
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
))
168 def metadata_full(self
, full_path
, tsn
=''):
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
202 metadata
['iso_duration'] = ('P%sDT%sH%sM%sS' %
203 (duration_delta
.days
, hours
, min, sec
))
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()
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
)
225 local_base_path
= self
.get_local_base_path(handler
, query
)
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]
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
)
240 video
['small_path'] = subcname
+ '/' + video
['name']
241 video
['total_items'] = self
.__total
_items
(f
)
243 if precache
or len(files
) == 1 or f
in transcode
.info_cache
:
244 video
['valid'] = transcode
.supported_format(f
)
246 video
.update(self
.metadata_full(f
, tsn
))
248 video
['valid'] = True
249 video
.update(self
.metadata_basic(f
))
253 t
= Template(CONTAINER_TEMPLATE
)
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', '')
272 path
= self
.get_local_path(handler
, query
)
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
)
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
:
301 path
= self
.get_local_path(handler
, query
)
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
))
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
))
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'])
330 handler
.send_response(500)
331 handler
.end_headers()
332 handler
.wfile
.write('%s\n\n%s' % (e
, traceback
.format_exc() ))
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):
349 def __getitem__(self
, key
):
350 if key
not in self
.d
:
351 self
.d
[key
] = self
.default(key
)
354 def __contains__(self
, key
):
357 def __setitem__(self
, key
, value
):
360 def __delitem__(self
):
367 return self
.d
.__iter
__()
370 return self
.d
.iteritems()
372 def default(self
, key
):
375 'episodeNumber' : '0',
376 'displayMajorNumber' : '0',
377 'displayMinorNumber' : '0',
378 'isEpisode' : 'true',
379 'colorCode' : ('COLOR', '4'),
380 'showType' : ('SERIES', '5'),
381 'tvRating' : ('NR', '7')
385 elif key
.startswith('v'):