10 from UserDict
import DictMixin
11 from datetime
import datetime
, timedelta
12 from xml
.sax
.saxutils
import escape
14 from Cheetah
.Template
import Template
15 from lrucache
import LRUCache
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 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
33 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
35 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv
36 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
37 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
38 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
39 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
40 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
41 .wm .wmd .wtv .yuv""".split()
43 LIKELYTS
= """.ts .tp .trp .3g2 .3gp .3gp2 .3gpp .m2t .m2ts .mts .mp4
44 .m4v .flv .mkv .mov .wtv .dvr-ms .webm""".split()
48 assert(config
.get_bin('ffmpeg'))
50 use_extensions
= False
53 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
56 return datetime(*uniso(iso
)[:6])
59 return int(calendar
.timegm(uniso(iso
)))
63 CONTENT_TYPE
= 'x-container/tivo-videos'
65 tvbus_cache
= LRUCache(1)
67 def video_file_filter(self
, full_path
, type=None):
68 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
71 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
73 return transcode
.supported_format(full_path
)
75 def send_file(self
, handler
, path
, query
):
76 mime
= 'video/x-tivo-mpeg'
77 tsn
= handler
.headers
.getheader('tsn', '')
80 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
82 tivo_name
= handler
.address_string()
84 is_tivo_file
= (path
[-5:].lower() == '.tivo')
87 mime
= query
['Format'][0]
89 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
90 compatible
= (not needs_tivodecode
and
91 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
94 offset
= int(handler
.headers
.getheader('Range')[6:-1])
99 valid
= bool(config
.get_bin('tivodecode') and
100 config
.get_server('tivo_mak'))
105 valid
= ((compatible
and offset
< os
.path
.getsize(path
)) or
106 (not compatible
and transcode
.is_resumable(path
, offset
)))
108 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
109 faking
= (mime
== 'video/x-tivo-mpeg' and
110 not (is_tivo_file
and compatible
))
111 fname
= unicode(path
, 'utf-8')
114 thead
= self
.tivo_header(tsn
, path
, mime
)
116 size
= os
.path
.getsize(fname
) + len(thead
)
117 handler
.send_response(200)
118 handler
.send_header('Content-Length', size
- offset
)
119 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
120 (offset
, size
- offset
- 1, size
))
122 handler
.send_response(206)
123 handler
.send_header('Transfer-Encoding', 'chunked')
124 handler
.send_header('Content-Type', mime
)
125 handler
.end_headers()
127 logger
.info('[%s] Start sending "%s" to %s' %
128 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
134 if faking
and not offset
:
135 handler
.wfile
.write(thead
)
136 logger
.debug('"%s" is tivo compatible' % fname
)
137 f
= open(fname
, 'rb')
143 block
= f
.read(512 * 1024)
146 handler
.wfile
.write(block
)
148 except Exception, msg
:
152 logger
.debug('"%s" is not tivo compatible' % fname
)
154 count
= transcode
.resume_transfer(path
, handler
.wfile
,
157 count
= transcode
.transcode(False, path
, handler
.wfile
,
161 handler
.wfile
.write('0\r\n\r\n')
162 handler
.wfile
.flush()
163 except Exception, msg
:
166 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
169 rate
= count
* 8.0 / mega_elapsed
170 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
171 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
172 tivo_name
, count
, rate
))
174 def __duration(self
, full_path
):
175 return transcode
.video_info(full_path
)['millisecs']
177 def __total_items(self
, full_path
):
180 full_path
= unicode(full_path
, 'utf-8')
181 for f
in os
.listdir(full_path
):
182 if f
.startswith('.'):
184 f
= os
.path
.join(full_path
, f
)
185 f2
= f
.encode('utf-8')
189 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
191 elif f2
in transcode
.info_cache
:
192 if transcode
.supported_format(f2
):
198 def __est_size(self
, full_path
, tsn
='', mime
=''):
199 # Size is estimated by taking audio and video bit rate adding 2%
201 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
202 return os
.path
.getsize(unicode(full_path
, 'utf-8'))
205 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
206 #audioBPS = config.strtod(config.getAudioBR(tsn))
207 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
208 bitrate
= audioBPS
+ videoBPS
209 return int((self
.__duration
(full_path
) / 1000) *
210 (bitrate
* 1.02 / 8))
212 def metadata_full(self
, full_path
, tsn
='', mime
='', mtime
=None):
214 vInfo
= transcode
.video_info(full_path
)
216 if ((int(vInfo
['vHeight']) >= 720 and
217 config
.getTivoHeight
>= 720) or
218 (int(vInfo
['vWidth']) >= 1280 and
219 config
.getTivoWidth
>= 1280)):
220 data
['showingBits'] = '4096'
222 data
.update(metadata
.basic(full_path
, mtime
))
223 if full_path
[-5:].lower() == '.tivo':
224 data
.update(metadata
.from_tivo(full_path
))
225 if full_path
[-4:].lower() == '.wtv':
226 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
228 if 'episodeNumber' in data
:
230 ep
= int(data
['episodeNumber'])
233 data
['episodeNumber'] = str(ep
)
235 if config
.getDebug() and 'vHost' not in data
:
236 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
238 transcode_options
= []
240 transcode_options
= transcode
.transcode(True, full_path
,
243 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
246 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
247 ['TRANSCODE OPTIONS: '] +
249 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
252 now
= datetime
.utcnow()
254 if data
['time'].lower() == 'file':
256 mtime
= os
.path
.getmtime(unicode(full_path
, 'utf-8'))
258 now
= datetime
.utcfromtimestamp(mtime
)
260 logger
.warning('Bad file time on ' + full_path
)
261 elif data
['time'].lower() == 'oad':
262 now
= isodt(data
['originalAirDate'])
265 now
= isodt(data
['time'])
267 logger
.warning('Bad time format: ' + data
['time'] +
268 ' , using current time')
270 duration
= self
.__duration
(full_path
)
271 duration_delta
= timedelta(milliseconds
= duration
)
272 min = duration_delta
.seconds
/ 60
273 sec
= duration_delta
.seconds
% 60
277 data
.update({'time': now
.isoformat(),
278 'startTime': now
.isoformat(),
279 'stopTime': (now
+ duration_delta
).isoformat(),
280 'size': self
.__est
_size
(full_path
, tsn
, mime
),
281 'duration': duration
,
282 'iso_duration': ('P%sDT%sH%sM%sS' %
283 (duration_delta
.days
, hours
, min, sec
))})
287 def QueryContainer(self
, handler
, query
):
288 tsn
= handler
.headers
.getheader('tsn', '')
289 subcname
= query
['Container'][0]
291 if not self
.get_local_path(handler
, query
):
292 handler
.send_error(404)
295 container
= handler
.container
296 force_alpha
= container
.getboolean('force_alpha')
297 ar
= container
.get('allow_recurse', 'auto').lower()
299 allow_recurse
= not tsn
or tsn
[0] < '7'
301 allow_recurse
= ar
in ('1', 'yes', 'true', 'on')
303 files
, total
, start
= self
.get_files(handler
, query
,
304 self
.video_file_filter
,
305 force_alpha
, allow_recurse
)
308 local_base_path
= self
.get_local_base_path(handler
, query
)
310 video
= VideoDetails()
313 ltime
= time
.localtime(mtime
)
315 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
317 ltime
= time
.localtime(mtime
)
318 video
['captureDate'] = hex(int(mtime
))
319 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
320 video
['name'] = os
.path
.basename(f
.name
)
321 video
['path'] = f
.name
322 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
323 if not video
['part_path'].startswith(os
.path
.sep
):
324 video
['part_path'] = os
.path
.sep
+ video
['part_path']
325 video
['title'] = os
.path
.basename(f
.name
)
326 video
['is_dir'] = f
.isdir
328 video
['small_path'] = subcname
+ '/' + video
['name']
329 video
['total_items'] = self
.__total
_items
(f
.name
)
331 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
332 video
['valid'] = transcode
.supported_format(f
.name
)
334 video
.update(self
.metadata_full(f
.name
, tsn
,
337 video
['captureDate'] = hex(isogm(video
['time']))
339 video
['valid'] = True
340 video
.update(metadata
.basic(f
.name
, mtime
))
342 if self
.use_ts(tsn
, f
.name
):
343 video
['mime'] = 'video/x-tivo-mpeg-ts'
345 video
['mime'] = 'video/x-tivo-mpeg'
347 video
['textSize'] = metadata
.human_size(f
.size
)
351 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
352 t
.container
= handler
.cname
360 t
.guid
= config
.getGUID()
361 t
.tivos
= config
.tivos
362 handler
.send_xml(str(t
))
364 def use_ts(self
, tsn
, file_path
):
365 if config
.is_ts_capable(tsn
):
366 ext
= os
.path
.splitext(file_path
)[1].lower()
369 flag
= file(file_path
).read(8)
372 if ord(flag
[7]) & 0x20:
375 opt
= config
.get_ts_flag()
376 if ((opt
== 'auto' and ext
in LIKELYTS
) or
377 (opt
in ['true', 'yes', 'on'])):
382 def get_details_xml(self
, tsn
, file_path
):
383 if (tsn
, file_path
) in self
.tvbus_cache
:
384 details
= self
.tvbus_cache
[(tsn
, file_path
)]
386 file_info
= VideoDetails()
387 file_info
['valid'] = transcode
.supported_format(file_path
)
388 if file_info
['valid']:
389 file_info
.update(self
.metadata_full(file_path
, tsn
))
391 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
394 t
.get_tv
= metadata
.get_tv
395 t
.get_mpaa
= metadata
.get_mpaa
396 t
.get_stars
= metadata
.get_stars
397 t
.get_color
= metadata
.get_color
399 self
.tvbus_cache
[(tsn
, file_path
)] = details
402 def tivo_header(self
, tsn
, path
, mime
):
403 def pad(length
, align
):
404 extra
= length
% align
406 extra
= align
- extra
409 if mime
== 'video/x-tivo-mpeg-ts':
413 details
= self
.get_details_xml(tsn
, path
)
415 chunk
= details
+ '\0' * (pad(ld
, 4) + 4)
417 blocklen
= lc
* 2 + 40
418 padding
= pad(blocklen
, 1024)
420 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
421 padding
+ blocklen
, 2),
422 struct
.pack('>LLHH', lc
+ 12, ld
, 1, 0),
424 struct
.pack('>LLHH', lc
+ 12, ld
, 2, 0),
425 chunk
, '\0' * padding
])
427 def TVBusQuery(self
, handler
, query
):
428 tsn
= handler
.headers
.getheader('tsn', '')
430 path
= self
.get_local_path(handler
, query
)
431 file_path
= os
.path
.normpath(path
+ '/' + f
)
433 details
= self
.get_details_xml(tsn
, file_path
)
435 handler
.send_xml(details
)
437 class VideoDetails(DictMixin
):
439 def __init__(self
, d
=None):
445 def __getitem__(self
, key
):
446 if key
not in self
.d
:
447 self
.d
[key
] = self
.default(key
)
450 def __contains__(self
, key
):
453 def __setitem__(self
, key
, value
):
456 def __delitem__(self
):
463 return self
.d
.__iter
__()
466 return self
.d
.iteritems()
468 def default(self
, key
):
471 'displayMajorNumber' : '0',
472 'displayMinorNumber' : '0',
473 'isEpisode' : 'true',
475 'showType' : ('SERIES', '5')
479 elif key
.startswith('v'):