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')
35 extfile
= os
.path
.join(SCRIPTDIR
, 'video.ext')
37 assert(config
.get_bin('ffmpeg'))
38 extensions
= file(extfile
).read().split()
44 CONTENT_TYPE
= 'x-container/tivo-videos'
46 def pre_cache(self
, full_path
):
47 if Video
.video_file_filter(self
, full_path
):
48 transcode
.supported_format(full_path
)
50 def video_file_filter(self
, full_path
, type=None):
51 if os
.path
.isdir(full_path
):
54 return os
.path
.splitext(full_path
)[1].lower() in extensions
56 return transcode
.supported_format(full_path
)
58 def send_file(self
, handler
, path
, query
):
60 tsn
= handler
.headers
.getheader('tsn', '')
62 is_tivo_file
= (path
[-5:].lower() == '.tivo')
64 if is_tivo_file
and transcode
.tivo_compatible(path
, tsn
, mime
)[0]:
65 mime
= 'video/x-tivo-mpeg'
68 mime
= query
['Format'][0]
70 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
71 compatible
= (not needs_tivodecode
and
72 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
74 offset
= handler
.headers
.getheader('Range')
76 offset
= int(offset
[6:-1]) # "bytes=XXX-"
79 valid
= bool(config
.get_bin('tivodecode') and
80 config
.get_server('tivo_mak'))
85 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
86 (not compatible
and transcode
.is_resumable(path
, offset
)))
88 handler
.send_response(206)
89 handler
.send_header('Content-Type', mime
)
90 handler
.send_header('Connection', 'close')
92 handler
.send_header('Content-Length',
93 os
.stat(path
).st_size
- offset
)
95 handler
.send_header('Transfer-Encoding', 'chunked')
100 logger
.debug('%s is tivo compatible' % path
)
103 if mime
== 'video/mp4':
104 qtfaststart
.fast_start(f
, handler
.wfile
, offset
)
109 block
= f
.read(512 * 1024)
112 handler
.wfile
.write(block
)
113 except Exception, msg
:
117 logger
.debug('%s is not tivo compatible' % path
)
119 transcode
.resume_transfer(path
, handler
.wfile
, offset
)
121 transcode
.transcode(False, path
, handler
.wfile
, tsn
)
124 handler
.wfile
.write('0\r\n\r\n')
125 handler
.wfile
.flush()
126 except Exception, msg
:
128 logger
.debug("Finished outputing video")
130 def __duration(self
, full_path
):
131 return transcode
.video_info(full_path
)['millisecs']
133 def __total_items(self
, full_path
):
136 for f
in os
.listdir(full_path
):
137 if f
.startswith('.'):
139 f
= os
.path
.join(full_path
, f
)
143 if os
.path
.splitext(f
)[1].lower() in extensions
:
145 elif f
in transcode
.info_cache
:
146 if transcode
.supported_format(f
):
152 def __est_size(self
, full_path
, tsn
='', mime
=''):
153 # Size is estimated by taking audio and video bit rate adding 2%
155 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
156 return int(os
.stat(full_path
).st_size
)
159 if config
.get_tsn('audio_codec', tsn
) == None:
160 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
162 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
163 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
164 bitrate
= audioBPS
+ videoBPS
165 return int((self
.__duration
(full_path
) / 1000) *
166 (bitrate
* 1.02 / 8))
168 def metadata_full(self
, full_path
, tsn
='', mime
=''):
170 vInfo
= transcode
.video_info(full_path
)
172 if ((int(vInfo
['vHeight']) >= 720 and
173 config
.getTivoHeight
>= 720) or
174 (int(vInfo
['vWidth']) >= 1280 and
175 config
.getTivoWidth
>= 1280)):
176 data
['showingBits'] = '4096'
178 data
.update(metadata
.basic(full_path
))
179 if full_path
[-5:].lower() == '.tivo':
180 data
.update(metadata
.from_tivo(full_path
))
182 if 'episodeNumber' in data
:
184 int(data
['episodeNumber'])
186 data
['episodeNumber'] = '0'
188 if config
.getDebug() and 'vHost' not in data
:
189 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
191 transcode_options
= {}
193 transcode_options
= transcode
.transcode(True, full_path
,
196 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
199 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
200 ['TRANSCODE OPTIONS: '] +
201 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
202 ['SOURCE FILE: ', os
.path
.split(full_path
)[1]]
205 now
= datetime
.utcnow()
207 if data
['time'].lower() == 'file':
208 mtime
= os
.stat(full_path
).st_mtime
212 now
= datetime
.utcfromtimestamp(mtime
)
214 logger
.warning('Bad file time on ' + full_path
)
215 elif data
['time'].lower() == 'oad':
216 now
= datetime
.strptime(data
['originalAirDate'][:19],
220 now
= datetime
.strptime(data
['time'][:19],
223 logger
.warning('Bad time format: ' + data
['time'] +
224 ' , using current time')
226 duration
= self
.__duration
(full_path
)
227 duration_delta
= timedelta(milliseconds
= duration
)
228 min = duration_delta
.seconds
/ 60
229 sec
= duration_delta
.seconds
% 60
233 data
.update({'time': now
.isoformat(),
234 'startTime': now
.isoformat(),
235 'stopTime': (now
+ duration_delta
).isoformat(),
236 'size': self
.__est
_size
(full_path
, tsn
, mime
),
237 'duration': duration
,
238 'iso_duration': ('P%sDT%sH%sM%sS' %
239 (duration_delta
.days
, hours
, min, sec
))})
243 def QueryContainer(self
, handler
, query
):
244 tsn
= handler
.headers
.getheader('tsn', '')
245 subcname
= query
['Container'][0]
246 cname
= subcname
.split('/')[0]
248 if (not cname
in handler
.server
.containers
or
249 not self
.get_local_path(handler
, query
)):
250 handler
.send_error(404)
253 container
= handler
.server
.containers
[cname
]
254 precache
= container
.get('precache', 'False').lower() == 'true'
255 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
257 files
, total
, start
= self
.get_files(handler
, query
,
258 self
.video_file_filter
,
262 local_base_path
= self
.get_local_base_path(handler
, query
)
265 mtime
= datetime
.utcfromtimestamp(f
.mdate
)
267 logger
.warning('Bad file time on ' + f
.name
)
268 mtime
= datetime
.utcnow()
269 video
= VideoDetails()
270 video
['captureDate'] = hex(int(time
.mktime(mtime
.timetuple())))
271 video
['name'] = os
.path
.split(f
.name
)[1]
272 video
['path'] = f
.name
273 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
274 if not video
['part_path'].startswith(os
.path
.sep
):
275 video
['part_path'] = os
.path
.sep
+ video
['part_path']
276 video
['title'] = os
.path
.split(f
.name
)[1]
277 video
['is_dir'] = f
.isdir
279 video
['small_path'] = subcname
+ '/' + video
['name']
280 video
['total_items'] = self
.__total
_items
(f
.name
)
282 if precache
or len(files
) == 1 or f
.name
in transcode
.info_cache
:
283 video
['valid'] = transcode
.supported_format(f
.name
)
285 video
.update(self
.metadata_full(f
.name
, tsn
))
287 video
['valid'] = True
288 video
.update(metadata
.basic(f
.name
))
292 t
= Template(CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
301 t
.guid
= config
.getGUID()
302 t
.tivos
= config
.tivos
303 t
.tivo_names
= config
.tivo_names
304 handler
.send_response(200)
305 handler
.send_header('Content-Type', 'text/xml')
306 handler
.end_headers()
307 handler
.wfile
.write(t
)
309 def TVBusQuery(self
, handler
, query
):
310 tsn
= handler
.headers
.getheader('tsn', '')
312 path
= self
.get_local_path(handler
, query
)
313 file_path
= path
+ os
.path
.normpath(f
)
315 file_info
= VideoDetails()
316 file_info
['valid'] = transcode
.supported_format(file_path
)
317 if file_info
['valid']:
318 file_info
.update(self
.metadata_full(file_path
, tsn
))
320 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
323 handler
.send_response(200)
324 handler
.send_header('Content-Type', 'text/xml')
325 handler
.end_headers()
326 handler
.wfile
.write(t
)
328 def Push(self
, handler
, query
):
329 tsn
= query
['tsn'][0]
330 for key
in config
.tivo_names
:
331 if config
.tivo_names
[key
] == tsn
:
335 container
= quote(query
['Container'][0].split('/')[0])
337 port
= config
.getPort()
339 baseurl
= 'http://%s:%s' % (ip
, port
)
340 if config
.getIsExternal(tsn
):
341 exturl
= config
.get_server('externalurl')
346 baseurl
= 'http://%s:%s' % (ip
, port
)
348 path
= self
.get_local_base_path(handler
, query
)
350 for f
in query
.get('File', []):
351 file_path
= path
+ os
.path
.normpath(f
)
353 file_info
= VideoDetails()
354 file_info
['valid'] = transcode
.supported_format(file_path
)
357 if config
.isHDtivo(tsn
):
358 for m
in ['video/mp4', 'video/bif']:
359 if transcode
.tivo_compatible(file_path
, tsn
, m
)[0]:
363 if file_info
['valid']:
364 file_info
.update(self
.metadata_full(file_path
, tsn
, mime
))
366 url
= baseurl
+ '/%s%s' % (container
, quote(f
))
368 title
= file_info
['seriesTitle']
370 title
= file_info
['title']
372 source
= file_info
['seriesId']
376 subtitle
= file_info
['episodeTitle']
377 logger
.debug('Pushing ' + url
)
379 m
= mind
.getMind(tsn
)
383 description
= file_info
['description'],
384 duration
= file_info
['duration'] / 1000,
385 size
= file_info
['size'],
390 tvrating
= file_info
['tvRating'])
392 handler
.send_response(500)
393 handler
.end_headers()
394 handler
.wfile
.write('%s\n\n%s' % (e
, traceback
.format_exc() ))
397 referer
= handler
.headers
.getheader('Referer')
399 handler
.send_response(302)
400 handler
.send_header('Location', referer
)
402 handler
.send_response(200)
403 handler
.end_headers()
406 """ returns your external IP address by querying dyndns.org """
407 f
= urllib
.urlopen('http://checkip.dyndns.org/')
409 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
412 class VideoDetails(DictMixin
):
414 def __init__(self
, d
=None):
420 def __getitem__(self
, key
):
421 if key
not in self
.d
:
422 self
.d
[key
] = self
.default(key
)
425 def __contains__(self
, key
):
428 def __setitem__(self
, key
, value
):
431 def __delitem__(self
):
438 return self
.d
.__iter
__()
441 return self
.d
.iteritems()
443 def default(self
, key
):
446 'episodeNumber' : '0',
447 'displayMajorNumber' : '0',
448 'displayMinorNumber' : '0',
449 'isEpisode' : 'true',
450 'colorCode' : ('COLOR', '4'),
451 'showType' : ('SERIES', '5')
455 elif key
.startswith('v'):