11 from UserDict
import DictMixin
12 from datetime
import datetime
, timedelta
13 from xml
.sax
.saxutils
import escape
15 from Cheetah
.Template
import Template
16 from lrucache
import LRUCache
23 from plugin
import EncodeUnicode
, Plugin
, quote
25 logger
= logging
.getLogger('pyTivo.video.video')
27 SCRIPTDIR
= os
.path
.dirname(__file__
)
31 PUSHED
= '<h3>Queued for Push to %s</h3> <p>%s</p>'
33 # Preload the templates
35 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
37 HTML_CONTAINER_TEMPLATE_MOBILE
= tmpl('container_mob.tmpl')
38 HTML_CONTAINER_TEMPLATE
= tmpl('container_html.tmpl')
39 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
40 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
42 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .vob .mp4 .m4v .mkv .ts
43 .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf .dat
44 .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf .m1v
45 .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21 .mpe
46 .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb .rts
47 .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm .wm
52 assert(config
.get_bin('ffmpeg'))
54 use_extensions
= False
56 queue
= [] # Recordings to push
60 CONTENT_TYPE
= 'x-container/tivo-videos'
62 tvbus_cache
= LRUCache(1)
64 def pre_cache(self
, full_path
):
65 if Video
.video_file_filter(self
, full_path
):
66 transcode
.supported_format(full_path
)
68 def video_file_filter(self
, full_path
, type=None):
69 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
72 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
74 return transcode
.supported_format(full_path
)
76 def send_file(self
, handler
, path
, query
):
78 tsn
= handler
.headers
.getheader('tsn', '')
79 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
81 is_tivo_file
= (path
[-5:].lower() == '.tivo')
83 if is_tivo_file
and transcode
.tivo_compatible(path
, tsn
, mime
)[0]:
84 mime
= 'video/x-tivo-mpeg'
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])
93 offset
= handler
.headers
.getheader('Range')
95 offset
= int(offset
[6:-1]) # "bytes=XXX-"
98 valid
= bool(config
.get_bin('tivodecode') and
99 config
.get_server('tivo_mak'))
104 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
105 (not compatible
and transcode
.is_resumable(path
, offset
)))
107 fname
= unicode(path
, 'utf-8')
108 handler
.send_response(206)
109 handler
.send_header('Content-Type', mime
)
110 handler
.send_header('Connection', 'close')
112 handler
.send_header('Content-Length',
113 os
.stat(fname
).st_size
- offset
)
115 handler
.send_header('Transfer-Encoding', 'chunked')
116 handler
.end_headers()
118 logger
.info('[%s] Start sending "%s" to %s' %
119 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
125 logger
.debug('"%s" is tivo compatible' % fname
)
126 f
= open(fname
, 'rb')
128 if mime
== 'video/mp4':
129 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
134 block
= f
.read(512 * 1024)
137 handler
.wfile
.write(block
)
139 except Exception, msg
:
143 logger
.debug('"%s" is not tivo compatible' % fname
)
145 count
= transcode
.resume_transfer(path
, handler
.wfile
,
148 count
= transcode
.transcode(False, path
,
152 handler
.wfile
.write('0\r\n\r\n')
153 handler
.wfile
.flush()
154 except Exception, msg
:
157 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
160 rate
= count
* 8.0 / mega_elapsed
161 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
162 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
163 tivo_name
, count
, rate
))
165 if fname
.endswith('.pyTivo-temp'):
168 def __duration(self
, full_path
):
169 return transcode
.video_info(full_path
)['millisecs']
171 def __total_items(self
, full_path
):
174 full_path
= unicode(full_path
, 'utf-8')
175 for f
in os
.listdir(full_path
):
176 if f
.startswith('.'):
178 f
= os
.path
.join(full_path
, f
)
179 f2
= f
.encode('utf-8')
183 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
185 elif f2
in transcode
.info_cache
:
186 if transcode
.supported_format(f2
):
192 def __est_size(self
, full_path
, tsn
='', mime
=''):
193 # Size is estimated by taking audio and video bit rate adding 2%
195 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
196 return int(os
.stat(unicode(full_path
, 'utf-8')).st_size
)
199 if config
.get_tsn('audio_codec', tsn
) == None:
200 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
202 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
203 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
204 bitrate
= audioBPS
+ videoBPS
205 return int((self
.__duration
(full_path
) / 1000) *
206 (bitrate
* 1.02 / 8))
208 def metadata_full(self
, full_path
, tsn
='', mime
=''):
210 vInfo
= transcode
.video_info(full_path
)
212 if ((int(vInfo
['vHeight']) >= 720 and
213 config
.getTivoHeight
>= 720) or
214 (int(vInfo
['vWidth']) >= 1280 and
215 config
.getTivoWidth
>= 1280)):
216 data
['showingBits'] = '4096'
218 data
.update(metadata
.basic(full_path
))
219 if full_path
[-5:].lower() == '.tivo':
220 data
.update(metadata
.from_tivo(full_path
))
222 if 'episodeNumber' in data
:
224 ep
= int(data
['episodeNumber'])
227 data
['episodeNumber'] = str(ep
)
229 if config
.getDebug() and 'vHost' not in data
:
230 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
232 transcode_options
= {}
234 transcode_options
= transcode
.transcode(True, full_path
,
237 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
240 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
241 ['TRANSCODE OPTIONS: '] +
242 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
243 ['SOURCE FILE: ', os
.path
.split(full_path
)[1]]
246 now
= datetime
.utcnow()
248 if data
['time'].lower() == 'file':
249 mtime
= os
.stat(unicode(full_path
, 'utf-8')).st_mtime
253 now
= datetime
.utcfromtimestamp(mtime
)
255 logger
.warning('Bad file time on ' + full_path
)
256 elif data
['time'].lower() == 'oad':
257 now
= datetime
.strptime(data
['originalAirDate'][:19],
261 now
= datetime
.strptime(data
['time'][:19],
264 logger
.warning('Bad time format: ' + data
['time'] +
265 ' , using current time')
267 duration
= self
.__duration
(full_path
)
268 duration_delta
= timedelta(milliseconds
= duration
)
269 min = duration_delta
.seconds
/ 60
270 sec
= duration_delta
.seconds
% 60
274 data
.update({'time': now
.isoformat(),
275 'startTime': now
.isoformat(),
276 'stopTime': (now
+ duration_delta
).isoformat(),
277 'size': self
.__est
_size
(full_path
, tsn
, mime
),
278 'duration': duration
,
279 'iso_duration': ('P%sDT%sH%sM%sS' %
280 (duration_delta
.days
, hours
, min, sec
))})
284 def QueryContainer(self
, handler
, query
):
285 tsn
= handler
.headers
.getheader('tsn', '')
286 subcname
= query
['Container'][0]
287 cname
= subcname
.split('/')[0]
288 useragent
= handler
.headers
.getheader('User-Agent', '')
290 if (not cname
in handler
.server
.containers
or
291 not self
.get_local_path(handler
, query
)):
292 handler
.send_error(404)
295 container
= handler
.server
.containers
[cname
]
296 precache
= container
.get('precache', 'False').lower() == 'true'
297 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
299 files
, total
, start
= self
.get_files(handler
, query
,
300 self
.video_file_filter
,
304 local_base_path
= self
.get_local_base_path(handler
, query
)
306 video
= VideoDetails()
309 ltime
= time
.localtime(mtime
)
311 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
312 mtime
= int(time
.time())
313 ltime
= time
.localtime(mtime
)
314 video
['captureDate'] = hex(mtime
)
315 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
316 video
['name'] = os
.path
.split(f
.name
)[1]
317 video
['path'] = f
.name
318 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
319 if not video
['part_path'].startswith(os
.path
.sep
):
320 video
['part_path'] = os
.path
.sep
+ video
['part_path']
321 video
['title'] = os
.path
.split(f
.name
)[1]
322 video
['is_dir'] = f
.isdir
324 video
['small_path'] = subcname
+ '/' + video
['name']
325 video
['total_items'] = self
.__total
_items
(f
.name
)
327 if precache
or len(files
) == 1 or f
.name
in transcode
.info_cache
:
328 video
['valid'] = transcode
.supported_format(f
.name
)
330 video
.update(self
.metadata_full(f
.name
, tsn
))
332 video
['valid'] = True
333 video
.update(metadata
.basic(f
.name
))
335 video
['textSize'] = ( '%.3f GB' %
336 (float(f
.size
) / (1024 ** 3)) )
340 logger
.debug('mobileagent: %d useragent: %s' % (useragent
.lower().find('mobile'), useragent
.lower()))
342 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
343 elif useragent
.lower().find('mobile') > 0:
344 t
= Template(HTML_CONTAINER_TEMPLATE_MOBILE
, filter=EncodeUnicode
)
346 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
355 t
.guid
= config
.getGUID()
356 t
.tivos
= config
.tivos
357 t
.tivo_names
= config
.tivo_names
358 handler
.send_response(200)
360 handler
.send_header('Content-Type', 'text/xml')
362 handler
.send_header('Content-Type', 'text/html; charset=utf-8')
363 handler
.send_header('Expires', '0')
364 handler
.end_headers()
365 handler
.wfile
.write(t
)
367 def get_details_xml(self
, tsn
, file_path
):
368 if (tsn
, file_path
) in self
.tvbus_cache
:
369 details
= self
.tvbus_cache
[(tsn
, file_path
)]
371 file_info
= VideoDetails()
372 file_info
['valid'] = transcode
.supported_format(file_path
)
373 if file_info
['valid']:
374 file_info
.update(self
.metadata_full(file_path
, tsn
))
376 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
380 self
.tvbus_cache
[(tsn
, file_path
)] = details
383 def tivo_header(self
, tsn
, path
, flag
=13):
384 details
= self
.get_details_xml(tsn
, path
)
386 chunklen
= ld
* 2 + 44
387 padding
= 2048 - chunklen
% 1024
389 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
390 padding
+ chunklen
, 2),
391 struct
.pack('>LLHH', ld
+ 16, ld
, 1, 0),
393 struct
.pack('>LLHH', ld
+ 19, ld
, 2, 0),
394 details
, '\0' * padding
])
396 def TVBusQuery(self
, handler
, query
):
397 tsn
= handler
.headers
.getheader('tsn', '')
399 path
= self
.get_local_path(handler
, query
)
400 file_path
= path
+ os
.path
.normpath(f
)
402 details
= self
.get_details_xml(tsn
, file_path
)
404 handler
.send_response(200)
405 handler
.send_header('Content-Type', 'text/xml')
406 handler
.send_header('Content-Length', len(details
))
407 handler
.send_header('Connection', 'close')
408 handler
.send_header('Expires', '0')
409 handler
.end_headers()
410 handler
.wfile
.write(details
)
412 def push_one_file(self
, f
):
413 file_info
= VideoDetails()
414 file_info
['valid'] = transcode
.supported_format(f
['path'])
416 temp_share
= config
.get_server('temp_share', '')
419 for name
, data
in config
.getShares():
420 if temp_share
== name
:
421 temp_share_path
= data
.get('path')
424 if config
.isHDtivo(f
['tsn']):
425 for m
in ['video/mp4', 'video/bif']:
426 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
430 if (mime
== 'video/mpeg' and
431 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
432 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'], temp_share_path
)
438 port
= config
.getPort()
439 container
= quote(temp_share
) + '/'
440 f
['url'] = 'http://%s:%s/%s' % (ip
, port
, container
)
442 if file_info
['valid']:
443 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
445 url
= f
['url'] + quote(f
['name'])
447 title
= file_info
['seriesTitle']
449 title
= file_info
['title']
451 source
= file_info
['seriesId']
455 subtitle
= file_info
['episodeTitle']
457 m
= mind
.getMind(f
['tsn'])
461 description
= file_info
['description'],
462 duration
= file_info
['duration'] / 1000,
463 size
= file_info
['size'],
468 tvrating
= file_info
['tvRating'])
469 except Exception, msg
:
472 def process_queue(self
):
476 self
.push_one_file(item
)
478 def Push(self
, handler
, query
):
479 tsn
= query
['tsn'][0]
480 for key
in config
.tivo_names
:
481 if config
.tivo_names
[key
] == tsn
:
484 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
486 container
= quote(query
['Container'][0].split('/')[0])
488 port
= config
.getPort()
490 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
491 if config
.getIsExternal(tsn
):
492 exturl
= config
.get_server('externalurl')
497 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
499 path
= self
.get_local_base_path(handler
, query
)
501 files
= query
.get('File', [])
503 file_path
= path
+ os
.path
.normpath(f
)
504 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
507 thread
.start_new_thread(Video
.process_queue
, (self
,))
509 logger
.info('[%s] Queued "%s" for Push to %s' %
510 (time
.strftime('%d/%b/%Y %H:%M:%S'),
511 unicode(file_path
, 'utf-8'), tivo_name
))
513 files
= [unicode(f
, 'utf-8') for f
in files
]
514 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
517 """ returns your external IP address by querying dyndns.org """
518 f
= urllib
.urlopen('http://checkip.dyndns.org/')
520 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
523 class VideoDetails(DictMixin
):
525 def __init__(self
, d
=None):
531 def __getitem__(self
, key
):
532 if key
not in self
.d
:
533 self
.d
[key
] = self
.default(key
)
536 def __contains__(self
, key
):
539 def __setitem__(self
, key
, value
):
542 def __delitem__(self
):
549 return self
.d
.__iter
__()
552 return self
.d
.iteritems()
554 def default(self
, key
):
557 'episodeNumber' : '0',
558 'displayMajorNumber' : '0',
559 'displayMinorNumber' : '0',
560 'isEpisode' : 'true',
561 'colorCode' : ('COLOR', '4'),
562 'showType' : ('SERIES', '5')
566 elif key
.startswith('v'):