1 import transcode
, os
, socket
, re
, urllib
, zlib
2 from Cheetah
.Template
import Template
3 from plugin
import Plugin
, quote
, unquote
4 from urlparse
import urlparse
5 from xml
.sax
.saxutils
import escape
6 from lrucache
import LRUCache
7 from UserDict
import DictMixin
8 from datetime
import datetime
, timedelta
14 SCRIPTDIR
= os
.path
.dirname(__file__
)
18 # Preload the templates
19 tcname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.tmpl')
20 ttname
= os
.path
.join(SCRIPTDIR
, 'templates', 'TvBus.tmpl')
21 txname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.xsl')
22 CONTAINER_TEMPLATE
= file(tcname
, 'rb').read()
23 TVBUS_TEMPLATE
= file(ttname
, 'rb').read()
24 XSL_TEMPLATE
= file(txname
, 'rb').read()
26 extfile
= os
.path
.join(SCRIPTDIR
, 'video.ext')
28 extensions
= file(extfile
).read().split()
34 CONTENT_TYPE
= 'x-container/tivo-videos'
36 def pre_cache(self
, full_path
):
37 if Video
.video_file_filter(self
, full_path
):
38 transcode
.supported_format(full_path
)
40 def video_file_filter(self
, full_path
, type=None):
41 if os
.path
.isdir(full_path
):
44 return os
.path
.splitext(full_path
)[1].lower() in extensions
46 return transcode
.supported_format(full_path
)
48 def send_file(self
, handler
, container
, name
):
49 if (handler
.headers
.getheader('Range') and
50 handler
.headers
.getheader('Range') != 'bytes=0-'):
51 handler
.send_response(206)
52 handler
.send_header('Connection', 'close')
53 handler
.send_header('Content-Type', 'video/x-tivo-mpeg')
54 handler
.send_header('Transfer-Encoding', 'chunked')
56 handler
.wfile
.write("\x30\x0D\x0A")
59 tsn
= handler
.headers
.getheader('tsn', '')
61 o
= urlparse("http://fake.host" + handler
.path
)
63 handler
.send_response(200)
65 transcode
.output_video(container
['path'] + path
[len(name
) + 1:],
68 def __isdir(self
, full_path
):
69 return os
.path
.isdir(full_path
)
71 def __duration(self
, full_path
):
72 return transcode
.video_info(full_path
)['millisecs']
74 def __total_items(self
, full_path
):
77 for f
in os
.listdir(full_path
):
80 f
= os
.path
.join(full_path
, f
)
84 if os
.path
.splitext(f
)[1].lower() in extensions
:
86 elif f
in transcode
.info_cache
:
87 if transcode
.supported_format(f
):
93 def __est_size(self
, full_path
, tsn
= ''):
94 # Size is estimated by taking audio and video bit rate adding 2%
96 if transcode
.tivo_compatable(full_path
, tsn
)[0]:
97 # Is TiVo-compatible mpeg2
98 return int(os
.stat(full_path
).st_size
)
101 if config
.getAudioCodec(tsn
) == None:
102 audioBPS
= config
.getMaxAudioBR(tsn
)*1000
104 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
105 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
106 bitrate
= audioBPS
+ videoBPS
107 return int((self
.__duration
(full_path
) / 1000) *
108 (bitrate
* 1.02 / 8))
110 def getMetadataFromTxt(self
, full_path
):
113 default_meta
= os
.path
.join(os
.path
.split(full_path
)[0], 'default.txt')
114 standard_meta
= full_path
+ '.txt'
115 subdir_meta
= os
.path
.join(os
.path
.dirname(full_path
), '.meta',
116 os
.path
.basename(full_path
)) + '.txt'
118 for metafile
in (default_meta
, standard_meta
, subdir_meta
):
119 metadata
.update(self
.__getMetadataFromFile
(metafile
))
123 def __getMetadataFromFile(self
, f
):
126 if os
.path
.exists(f
):
128 if line
.strip().startswith('#'):
133 key
, value
= line
.split(':', 1)
135 value
= value
.strip()
137 if key
.startswith('v'):
139 metadata
[key
].append(value
)
141 metadata
[key
] = [value
]
143 metadata
[key
] = value
147 def metadata_basic(self
, full_path
):
150 base_path
, title
= os
.path
.split(full_path
)
151 ctime
= os
.stat(full_path
).st_ctime
152 if (ctime
< 0): ctime
= 0
153 originalAirDate
= datetime
.fromtimestamp(ctime
)
155 metadata
['title'] = '.'.join(title
.split('.')[:-1])
156 metadata
['seriesTitle'] = metadata
['title'] # default to the filename
157 metadata
['originalAirDate'] = originalAirDate
.isoformat()
159 metadata
.update(self
.getMetadataFromTxt(full_path
))
163 def metadata_full(self
, full_path
, tsn
=''):
166 now
= datetime
.utcnow()
168 duration
= self
.__duration
(full_path
)
169 duration_delta
= timedelta(milliseconds
= duration
)
171 metadata
['time'] = now
.isoformat()
172 metadata
['startTime'] = now
.isoformat()
173 metadata
['stopTime'] = (now
+ duration_delta
).isoformat()
174 metadata
['size'] = self
.__est
_size
(full_path
, tsn
)
175 metadata
['duration'] = duration
176 vInfo
= transcode
.video_info(full_path
)
177 transcode_options
= {}
178 if not transcode
.tivo_compatable(full_path
, tsn
)[0]:
179 transcode_options
= transcode
.transcode(True, full_path
, '', tsn
)
180 metadata
['vHost'] = [str(transcode
.tivo_compatable(full_path
, tsn
)[1])]+\
181 ['SOURCE INFO: ']+["%s=%s" % (k
, v
) for k
, v
in sorted(transcode
.video_info(full_path
).items(), reverse
=True)]+\
182 ['TRANSCODE OPTIONS: ']+["%s" % (v
) for k
, v
in transcode_options
.items()]+\
183 ['SOURCE FILE: ']+[str(os
.path
.split(full_path
)[1])]
184 if not (full_path
[-5:]).lower() == '.tivo':
185 if ((int(vInfo
['vHeight']) >= 720 and
186 config
.getTivoHeight
>= 720) or
187 (int(vInfo
['vWidth']) >= 1280 and
188 config
.getTivoWidth
>= 1280)):
189 metadata
['showingBits'] = '4096'
191 metadata
.update(self
.metadata_basic(full_path
))
193 min = duration_delta
.seconds
/ 60
194 sec
= duration_delta
.seconds
% 60
197 metadata
['iso_duration'] = 'P' + str(duration_delta
.days
) + \
198 'DT' + str(hours
) + 'H' + str(min) + \
202 def QueryContainer(self
, handler
, query
):
203 tsn
= handler
.headers
.getheader('tsn', '')
204 subcname
= query
['Container'][0]
205 cname
= subcname
.split('/')[0]
207 if not handler
.server
.containers
.has_key(cname
) or \
208 not self
.get_local_path(handler
, query
):
209 handler
.send_response(404)
210 handler
.end_headers()
213 container
= handler
.server
.containers
[cname
]
214 precache
= container
.get('precache', 'False').lower() == 'true'
216 files
, total
, start
= self
.get_files(handler
, query
,
217 self
.video_file_filter
)
220 local_base_path
= self
.get_local_base_path(handler
, query
)
222 mtime
= os
.stat(f
).st_mtime
223 if (mtime
< 0): mtime
= 0
224 mtime
= datetime
.fromtimestamp(mtime
)
225 video
= VideoDetails()
226 video
['captureDate'] = hex(int(time
.mktime(mtime
.timetuple())))
227 video
['name'] = os
.path
.split(f
)[1]
229 video
['part_path'] = f
.replace(local_base_path
, '', 1)
230 if not video
['part_path'].startswith(os
.path
.sep
):
231 video
['part_path'] = os
.path
.sep
+ video
['part_path']
232 video
['title'] = os
.path
.split(f
)[1]
233 video
['is_dir'] = self
.__isdir
(f
)
235 video
['small_path'] = subcname
+ '/' + video
['name']
236 video
['total_items'] = self
.__total
_items
(f
)
238 if precache
or len(files
) == 1 or f
in transcode
.info_cache
:
239 video
['valid'] = transcode
.supported_format(f
)
241 video
.update(self
.metadata_full(f
, tsn
))
243 video
['valid'] = True
244 video
.update(self
.metadata_basic(f
))
248 handler
.send_response(200)
249 handler
.end_headers()
250 t
= Template(CONTAINER_TEMPLATE
)
259 t
.guid
= config
.getGUID()
260 t
.tivos
= handler
.tivos
261 t
.tivo_names
= handler
.tivo_names
262 handler
.wfile
.write(t
)
264 def TVBusQuery(self
, handler
, query
):
265 tsn
= handler
.headers
.getheader('tsn', '')
267 path
= self
.get_local_path(handler
, query
)
270 file_info
= VideoDetails()
271 file_info
['valid'] = transcode
.supported_format(file_path
)
272 if file_info
['valid']:
273 file_info
.update(self
.metadata_full(file_path
, tsn
))
275 handler
.send_response(200)
276 handler
.end_headers()
277 t
= Template(TVBUS_TEMPLATE
)
280 handler
.wfile
.write(t
)
282 def XSL(self
, handler
, query
):
283 handler
.send_response(200)
284 handler
.end_headers()
285 handler
.wfile
.write(XSL_TEMPLATE
)
287 def Push(self
, handler
, query
):
288 f
= unquote(query
['File'][0])
290 tsn
= query
['tsn'][0]
291 for key
in handler
.tivo_names
:
292 if handler
.tivo_names
[key
] == tsn
:
296 path
= self
.get_local_path(handler
, query
)
299 file_info
= VideoDetails()
300 file_info
['valid'] = transcode
.supported_format(file_path
)
301 if file_info
['valid']:
302 file_info
.update(self
.metadata_full(file_path
, tsn
))
305 s
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
306 s
.connect(('tivo.com',123))
307 ip
= s
.getsockname()[0]
308 container
= quote(query
['Container'][0].split('/')[0])
309 port
= config
.getPort()
311 url
= 'http://%s:%s/%s%s' % (ip
, port
, container
, quote(f
))
318 description
= file_info
['description'],
319 duration
= file_info
['duration'] / 1000,
320 size
= file_info
['size'],
321 title
= file_info
['title'],
322 subtitle
= file_info
['name'])
325 handler
.send_response(500)
326 handler
.end_headers()
327 handler
.wfile
.write('%s\n\n%s' % (e
, traceback
.format_exc() ))
330 referer
= handler
.headers
.getheader('Referer')
331 handler
.send_response(302)
332 handler
.send_header('Location', referer
)
333 handler
.end_headers()
336 class VideoDetails(DictMixin
):
338 def __init__(self
, d
=None):
344 def __getitem__(self
, key
):
345 if key
not in self
.d
:
346 self
.d
[key
] = self
.default(key
)
349 def __contains__(self
, key
):
352 def __setitem__(self
, key
, value
):
355 def __delitem__(self
):
362 return self
.d
.__iter
__()
365 return self
.d
.iteritems()
367 def default(self
, key
):
370 'episodeNumber' : '0',
371 'displayMajorNumber' : '0',
372 'displayMinorNumber' : '0',
373 'isEpisode' : 'true',
374 'colorCode' : ('COLOR', '4'),
375 'showType' : ('SERIES', '5'),
376 'tvRating' : ('NR', '7')
380 elif key
.startswith('v'):