9 from UserDict
import DictMixin
10 from datetime
import datetime
, timedelta
11 from xml
.sax
.saxutils
import escape
13 from Cheetah
.Template
import Template
20 from plugin
import EncodeUnicode
, Plugin
, quote
22 logger
= logging
.getLogger('pyTivo.video.video')
24 SCRIPTDIR
= os
.path
.dirname(__file__
)
28 # Preload the templates
30 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
32 CONTAINER_TEMPLATE
= tmpl('container.tmpl')
33 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
34 XSL_TEMPLATE
= tmpl('container.xsl')
36 extfile
= os
.path
.join(SCRIPTDIR
, 'video.ext')
38 assert(config
.get_bin('ffmpeg'))
39 extensions
= file(extfile
).read().split()
45 CONTENT_TYPE
= 'x-container/tivo-videos'
47 def pre_cache(self
, full_path
):
48 if Video
.video_file_filter(self
, full_path
):
49 transcode
.supported_format(full_path
)
51 def video_file_filter(self
, full_path
, type=None):
52 if os
.path
.isdir(full_path
):
55 return os
.path
.splitext(full_path
)[1].lower() in extensions
57 return transcode
.supported_format(full_path
)
59 def send_file(self
, handler
, path
, query
):
61 tsn
= handler
.headers
.getheader('tsn', '')
63 is_tivo_file
= (path
[-5:].lower() == '.tivo')
65 if is_tivo_file
and transcode
.tivo_compatible(path
, tsn
, mime
)[0]:
66 mime
= 'video/x-tivo-mpeg'
69 mime
= query
['Format'][0]
71 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
72 compatible
= (not needs_tivodecode
and
73 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
75 offset
= handler
.headers
.getheader('Range')
77 offset
= int(offset
[6:-1]) # "bytes=XXX-"
80 valid
= bool(config
.get_bin('tivodecode') and
81 config
.get_server('tivo_mak'))
86 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
87 (not compatible
and transcode
.is_resumable(path
, offset
)))
89 handler
.send_response(206)
90 handler
.send_header('Content-Type', mime
)
91 handler
.send_header('Connection', 'close')
93 handler
.send_header('Content-Length',
94 os
.stat(path
).st_size
- offset
)
96 handler
.send_header('Transfer-Encoding', 'chunked')
101 logger
.debug('%s is tivo compatible' % path
)
104 if mime
== 'video/mp4':
105 qtfaststart
.fast_start(f
, handler
.wfile
, offset
)
110 block
= f
.read(512 * 1024)
113 handler
.wfile
.write(block
)
114 except Exception, msg
:
118 logger
.debug('%s is not tivo compatible' % path
)
120 transcode
.resume_transfer(path
, handler
.wfile
, offset
)
122 transcode
.transcode(False, path
, handler
.wfile
, tsn
)
125 handler
.wfile
.write('0\r\n\r\n')
126 handler
.wfile
.flush()
127 except Exception, msg
:
129 logger
.debug("Finished outputing video")
131 def __duration(self
, full_path
):
132 return transcode
.video_info(full_path
)['millisecs']
134 def __total_items(self
, full_path
):
137 for f
in os
.listdir(full_path
):
138 if f
.startswith('.'):
140 f
= os
.path
.join(full_path
, f
)
144 if os
.path
.splitext(f
)[1].lower() in extensions
:
146 elif f
in transcode
.info_cache
:
147 if transcode
.supported_format(f
):
153 def __est_size(self
, full_path
, tsn
='', mime
=''):
154 # Size is estimated by taking audio and video bit rate adding 2%
156 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
157 return int(os
.stat(full_path
).st_size
)
160 if config
.get_tsn('audio_codec', tsn
) == None:
161 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
163 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
164 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
165 bitrate
= audioBPS
+ videoBPS
166 return int((self
.__duration
(full_path
) / 1000) *
167 (bitrate
* 1.02 / 8))
169 def metadata_full(self
, full_path
, tsn
='', mime
=''):
171 vInfo
= transcode
.video_info(full_path
)
173 if ((int(vInfo
['vHeight']) >= 720 and
174 config
.getTivoHeight
>= 720) or
175 (int(vInfo
['vWidth']) >= 1280 and
176 config
.getTivoWidth
>= 1280)):
177 data
['showingBits'] = '4096'
179 data
.update(metadata
.basic(full_path
))
180 if full_path
[-5:].lower() == '.tivo':
181 data
.update(metadata
.from_tivo(full_path
))
183 if config
.getDebug() and 'vHost' not in data
:
184 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
186 transcode_options
= {}
188 transcode_options
= transcode
.transcode(True, full_path
,
191 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
194 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
195 ['TRANSCODE OPTIONS: '] +
196 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
197 ['SOURCE FILE: ', os
.path
.split(full_path
)[1]]
200 now
= datetime
.utcnow()
201 duration
= self
.__duration
(full_path
)
202 duration_delta
= timedelta(milliseconds
= duration
)
203 min = duration_delta
.seconds
/ 60
204 sec
= duration_delta
.seconds
% 60
208 data
.update({'time': now
.isoformat(),
209 'startTime': now
.isoformat(),
210 'stopTime': (now
+ duration_delta
).isoformat(),
211 'size': self
.__est
_size
(full_path
, tsn
, mime
),
212 'duration': duration
,
213 'iso_duration': ('P%sDT%sH%sM%sS' %
214 (duration_delta
.days
, hours
, min, sec
))})
218 def QueryContainer(self
, handler
, query
):
219 tsn
= handler
.headers
.getheader('tsn', '')
220 subcname
= query
['Container'][0]
221 cname
= subcname
.split('/')[0]
223 if (not cname
in handler
.server
.containers
or
224 not self
.get_local_path(handler
, query
)):
225 handler
.send_error(404)
228 container
= handler
.server
.containers
[cname
]
229 precache
= container
.get('precache', 'False').lower() == 'true'
230 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
232 files
, total
, start
= self
.get_files(handler
, query
,
233 self
.video_file_filter
,
237 local_base_path
= self
.get_local_base_path(handler
, query
)
239 mtime
= datetime
.fromtimestamp(f
.mdate
)
240 video
= VideoDetails()
241 video
['captureDate'] = hex(int(time
.mktime(mtime
.timetuple())))
242 video
['name'] = os
.path
.split(f
.name
)[1]
243 video
['path'] = f
.name
244 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
245 if not video
['part_path'].startswith(os
.path
.sep
):
246 video
['part_path'] = os
.path
.sep
+ video
['part_path']
247 video
['title'] = os
.path
.split(f
.name
)[1]
248 video
['is_dir'] = f
.isdir
250 video
['small_path'] = subcname
+ '/' + video
['name']
251 video
['total_items'] = self
.__total
_items
(f
.name
)
253 if precache
or len(files
) == 1 or f
.name
in transcode
.info_cache
:
254 video
['valid'] = transcode
.supported_format(f
.name
)
256 video
.update(self
.metadata_full(f
.name
, tsn
))
258 video
['valid'] = True
259 video
.update(metadata
.basic(f
.name
))
263 t
= Template(CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
272 t
.guid
= config
.getGUID()
273 t
.tivos
= config
.tivos
274 t
.tivo_names
= config
.tivo_names
275 handler
.send_response(200)
276 handler
.send_header('Content-Type', 'text/xml')
277 handler
.end_headers()
278 handler
.wfile
.write(t
)
280 def TVBusQuery(self
, handler
, query
):
281 tsn
= handler
.headers
.getheader('tsn', '')
283 path
= self
.get_local_path(handler
, query
)
284 file_path
= path
+ os
.path
.normpath(f
)
286 file_info
= VideoDetails()
287 file_info
['valid'] = transcode
.supported_format(file_path
)
288 if file_info
['valid']:
289 file_info
.update(self
.metadata_full(file_path
, tsn
))
291 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
294 handler
.send_response(200)
295 handler
.send_header('Content-Type', 'text/xml')
296 handler
.end_headers()
297 handler
.wfile
.write(t
)
299 def XSL(self
, handler
, query
):
300 handler
.send_response(200)
301 handler
.send_header('Content-Type', 'text/xml')
302 handler
.end_headers()
303 handler
.wfile
.write(XSL_TEMPLATE
)
305 def Push(self
, handler
, query
):
306 tsn
= query
['tsn'][0]
307 for key
in config
.tivo_names
:
308 if config
.tivo_names
[key
] == tsn
:
312 container
= quote(query
['Container'][0].split('/')[0])
314 port
= config
.getPort()
316 baseurl
= 'http://%s:%s' % (ip
, port
)
317 if config
.getIsExternal(tsn
):
318 exturl
= config
.get_server('externalurl')
323 baseurl
= 'http://%s:%s' % (ip
, port
)
325 path
= self
.get_local_base_path(handler
, query
)
327 for f
in query
.get('File', []):
328 file_path
= path
+ os
.path
.normpath(f
)
330 file_info
= VideoDetails()
331 file_info
['valid'] = transcode
.supported_format(file_path
)
334 if config
.isHDtivo(tsn
):
335 for m
in ['video/mp4', 'video/bif']:
336 if transcode
.tivo_compatible(file_path
, tsn
, m
)[0]:
340 if file_info
['valid']:
341 file_info
.update(self
.metadata_full(file_path
, tsn
, mime
))
343 url
= baseurl
+ '/%s%s' % (container
, quote(f
))
345 title
= file_info
['seriesTitle']
347 title
= file_info
['title']
349 source
= file_info
['seriesId']
353 subtitle
= file_info
['episodeTitle']
354 logger
.debug('Pushing ' + url
)
356 m
= mind
.getMind(tsn
)
360 description
= file_info
['description'],
361 duration
= file_info
['duration'] / 1000,
362 size
= file_info
['size'],
368 handler
.send_response(500)
369 handler
.end_headers()
370 handler
.wfile
.write('%s\n\n%s' % (e
, traceback
.format_exc() ))
373 referer
= handler
.headers
.getheader('Referer')
374 handler
.send_response(302)
375 handler
.send_header('Location', referer
)
376 handler
.end_headers()
379 """ returns your external IP address by querying dyndns.org """
380 f
= urllib
.urlopen('http://checkip.dyndns.org/')
382 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
385 class VideoDetails(DictMixin
):
387 def __init__(self
, d
=None):
393 def __getitem__(self
, key
):
394 if key
not in self
.d
:
395 self
.d
[key
] = self
.default(key
)
398 def __contains__(self
, key
):
401 def __setitem__(self
, key
, value
):
404 def __delitem__(self
):
411 return self
.d
.__iter
__()
414 return self
.d
.iteritems()
416 def default(self
, key
):
419 'episodeNumber' : '0',
420 'displayMajorNumber' : '0',
421 'displayMinorNumber' : '0',
422 'isEpisode' : 'true',
423 'colorCode' : ('COLOR', '4'),
424 'showType' : ('SERIES', '5'),
425 'tvRating' : ('NR', '7')
429 elif key
.startswith('v'):