12 from UserDict
import DictMixin
13 from datetime
import datetime
, timedelta
14 from xml
.sax
.saxutils
import escape
16 from Cheetah
.Template
import Template
17 from lrucache
import LRUCache
24 from plugin
import EncodeUnicode
, Plugin
, quote
26 logger
= logging
.getLogger('pyTivo.video.video')
28 SCRIPTDIR
= os
.path
.dirname(__file__
)
32 PUSHED
= '<h3>Queued for Push to %s</h3> <p>%s</p>'
34 # Preload the templates
36 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
38 HTML_CONTAINER_TEMPLATE_MOBILE
= tmpl('container_mob.tmpl')
39 HTML_CONTAINER_TEMPLATE
= tmpl('container_html.tmpl')
40 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
41 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
43 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv
44 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
45 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
46 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
47 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
48 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
49 .wm .wmd .wtv .yuv""".split()
53 assert(config
.get_bin('ffmpeg'))
55 use_extensions
= False
57 queue
= [] # Recordings to push
60 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
63 return datetime(*uniso(iso
)[:6])
66 return int(calendar
.timegm(uniso(iso
)))
68 class Pushable(object):
70 def push_one_file(self
, f
):
71 file_info
= VideoDetails()
72 file_info
['valid'] = transcode
.supported_format(f
['path'])
74 temp_share
= config
.get_server('temp_share', '')
77 for name
, data
in config
.getShares():
78 if temp_share
== name
:
79 temp_share_path
= data
.get('path')
82 if config
.isHDtivo(f
['tsn']):
83 for m
in ['video/mp4', 'video/bif']:
84 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
88 if (mime
== 'video/mpeg' and
89 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
90 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'], temp_share_path
)
96 port
= config
.getPort()
97 container
= quote(temp_share
) + '/'
98 f
['url'] = 'http://%s:%s/%s' % (ip
, port
, container
)
100 if file_info
['valid']:
101 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
103 url
= f
['url'] + quote(f
['name'])
105 title
= file_info
['seriesTitle']
107 title
= file_info
['title']
109 source
= file_info
['seriesId']
113 subtitle
= file_info
['episodeTitle']
115 m
= mind
.getMind(f
['tsn'])
119 description
= file_info
['description'],
120 duration
= file_info
['duration'] / 1000,
121 size
= file_info
['size'],
126 tvrating
= file_info
['tvRating'])
127 except Exception, msg
:
130 def process_queue(self
):
134 self
.push_one_file(item
)
137 """ returns your external IP address by querying dyndns.org """
138 f
= urllib
.urlopen('http://checkip.dyndns.org/')
140 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
143 def Push(self
, handler
, query
):
144 tsn
= query
['tsn'][0]
145 for key
in config
.tivo_names
:
146 if config
.tivo_names
[key
] == tsn
:
149 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
151 container
= quote(query
['Container'][0].split('/')[0])
152 ip
= config
.get_ip(tsn
)
153 port
= config
.getPort()
155 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
156 if config
.getIsExternal(tsn
):
157 exturl
= config
.get_server('externalurl')
162 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
164 path
= self
.get_local_base_path(handler
, query
)
166 files
= query
.get('File', [])
168 file_path
= path
+ os
.path
.normpath(f
)
169 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
172 thread
.start_new_thread(Video
.process_queue
, (self
,))
174 logger
.info('[%s] Queued "%s" for Push to %s' %
175 (time
.strftime('%d/%b/%Y %H:%M:%S'),
176 unicode(file_path
, 'utf-8'), tivo_name
))
178 files
= [unicode(f
, 'utf-8') for f
in files
]
179 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
181 class BaseVideo(Plugin
):
183 CONTENT_TYPE
= 'x-container/tivo-videos'
185 tvbus_cache
= LRUCache(1)
187 def pre_cache(self
, full_path
):
188 if Video
.video_file_filter(self
, full_path
):
189 transcode
.supported_format(full_path
)
191 def video_file_filter(self
, full_path
, type=None):
192 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
195 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
197 return transcode
.supported_format(full_path
)
199 def send_file(self
, handler
, path
, query
):
200 mime
= 'video/x-tivo-mpeg'
201 tsn
= handler
.headers
.getheader('tsn', '')
202 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
204 is_tivo_file
= (path
[-5:].lower() == '.tivo')
206 if 'Format' in query
:
207 mime
= query
['Format'][0]
209 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
210 compatible
= (not needs_tivodecode
and
211 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
214 offset
= int(handler
.headers
.getheader('Range')[6:-1])
219 valid
= bool(config
.get_bin('tivodecode') and
220 config
.get_server('tivo_mak'))
225 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
226 (not compatible
and transcode
.is_resumable(path
, offset
)))
228 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
229 faking
= (mime
== 'video/x-tivo-mpeg' and
230 not (is_tivo_file
and compatible
))
231 fname
= unicode(path
, 'utf-8')
234 thead
= self
.tivo_header(tsn
, path
, mime
)
236 size
= os
.stat(fname
).st_size
+ len(thead
)
237 handler
.send_response(200)
238 handler
.send_header('Content-Length', size
- offset
)
239 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
240 (offset
, size
- offset
- 1, size
))
242 handler
.send_response(206)
243 handler
.send_header('Transfer-Encoding', 'chunked')
244 handler
.send_header('Content-Type', mime
)
245 handler
.send_header('Connection', 'close')
246 handler
.end_headers()
248 logger
.info('[%s] Start sending "%s" to %s' %
249 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
255 if faking
and not offset
:
256 handler
.wfile
.write(thead
)
257 logger
.debug('"%s" is tivo compatible' % fname
)
258 f
= open(fname
, 'rb')
260 if mime
== 'video/mp4':
261 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
267 block
= f
.read(512 * 1024)
270 handler
.wfile
.write(block
)
272 except Exception, msg
:
276 logger
.debug('"%s" is not tivo compatible' % fname
)
278 count
= transcode
.resume_transfer(path
, handler
.wfile
,
281 count
= transcode
.transcode(False, path
, handler
.wfile
,
285 handler
.wfile
.write('0\r\n\r\n')
286 handler
.wfile
.flush()
287 except Exception, msg
:
290 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
293 rate
= count
* 8.0 / mega_elapsed
294 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
295 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
296 tivo_name
, count
, rate
))
298 if fname
.endswith('.pyTivo-temp'):
301 def __duration(self
, full_path
):
302 return transcode
.video_info(full_path
)['millisecs']
304 def __total_items(self
, full_path
):
307 full_path
= unicode(full_path
, 'utf-8')
308 for f
in os
.listdir(full_path
):
309 if f
.startswith('.'):
311 f
= os
.path
.join(full_path
, f
)
312 f2
= f
.encode('utf-8')
316 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
318 elif f2
in transcode
.info_cache
:
319 if transcode
.supported_format(f2
):
325 def __est_size(self
, full_path
, tsn
='', mime
=''):
326 # Size is estimated by taking audio and video bit rate adding 2%
328 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
329 return int(os
.stat(unicode(full_path
, 'utf-8')).st_size
)
332 if config
.get_tsn('audio_codec', tsn
) == None:
333 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
335 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
336 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
337 bitrate
= audioBPS
+ videoBPS
338 return int((self
.__duration
(full_path
) / 1000) *
339 (bitrate
* 1.02 / 8))
341 def metadata_full(self
, full_path
, tsn
='', mime
=''):
343 vInfo
= transcode
.video_info(full_path
)
345 if ((int(vInfo
['vHeight']) >= 720 and
346 config
.getTivoHeight
>= 720) or
347 (int(vInfo
['vWidth']) >= 1280 and
348 config
.getTivoWidth
>= 1280)):
349 data
['showingBits'] = '4096'
351 data
.update(metadata
.basic(full_path
))
352 if full_path
[-5:].lower() == '.tivo':
353 data
.update(metadata
.from_tivo(full_path
))
354 if full_path
[-4:].lower() == '.wtv':
355 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
357 if 'episodeNumber' in data
:
359 ep
= int(data
['episodeNumber'])
362 data
['episodeNumber'] = str(ep
)
364 if config
.getDebug() and 'vHost' not in data
:
365 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
367 transcode_options
= {}
369 transcode_options
= transcode
.transcode(True, full_path
,
372 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
375 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
376 ['TRANSCODE OPTIONS: '] +
377 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
378 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
381 now
= datetime
.utcnow()
383 if data
['time'].lower() == 'file':
384 mtime
= os
.stat(unicode(full_path
, 'utf-8')).st_mtime
388 now
= datetime
.utcfromtimestamp(mtime
)
390 logger
.warning('Bad file time on ' + full_path
)
391 elif data
['time'].lower() == 'oad':
392 now
= isodt(data
['originalAirDate'])
395 now
= isodt(data
['time'])
397 logger
.warning('Bad time format: ' + data
['time'] +
398 ' , using current time')
400 duration
= self
.__duration
(full_path
)
401 duration_delta
= timedelta(milliseconds
= duration
)
402 min = duration_delta
.seconds
/ 60
403 sec
= duration_delta
.seconds
% 60
407 data
.update({'time': now
.isoformat(),
408 'startTime': now
.isoformat(),
409 'stopTime': (now
+ duration_delta
).isoformat(),
410 'size': self
.__est
_size
(full_path
, tsn
, mime
),
411 'duration': duration
,
412 'iso_duration': ('P%sDT%sH%sM%sS' %
413 (duration_delta
.days
, hours
, min, sec
))})
417 def QueryContainer(self
, handler
, query
):
418 tsn
= handler
.headers
.getheader('tsn', '')
419 subcname
= query
['Container'][0]
420 useragent
= handler
.headers
.getheader('User-Agent', '')
422 if not self
.get_local_path(handler
, query
):
423 handler
.send_error(404)
426 container
= handler
.container
427 precache
= container
.get('precache', 'False').lower() == 'true'
428 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
429 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
431 files
, total
, start
= self
.get_files(handler
, query
,
432 self
.video_file_filter
,
436 local_base_path
= self
.get_local_base_path(handler
, query
)
438 video
= VideoDetails()
441 ltime
= time
.localtime(mtime
)
443 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
444 mtime
= int(time
.time())
445 ltime
= time
.localtime(mtime
)
446 video
['captureDate'] = hex(mtime
)
447 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
448 video
['name'] = os
.path
.basename(f
.name
)
449 video
['path'] = f
.name
450 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
451 if not video
['part_path'].startswith(os
.path
.sep
):
452 video
['part_path'] = os
.path
.sep
+ video
['part_path']
453 video
['title'] = os
.path
.basename(f
.name
)
454 video
['is_dir'] = f
.isdir
456 video
['small_path'] = subcname
+ '/' + video
['name']
457 video
['total_items'] = self
.__total
_items
(f
.name
)
459 if precache
or len(files
) == 1 or f
.name
in transcode
.info_cache
:
460 video
['valid'] = transcode
.supported_format(f
.name
)
462 video
.update(self
.metadata_full(f
.name
, tsn
))
464 video
['captureDate'] = hex(isogm(video
['time']))
466 video
['valid'] = True
467 video
.update(metadata
.basic(f
.name
))
469 if config
.hasTStivo(tsn
):
470 video
['mime'] = 'video/x-tivo-mpeg-ts'
472 video
['mime'] = 'video/x-tivo-mpeg'
474 video
['textSize'] = ( '%.3f GB' %
475 (float(f
.size
) / (1024 ** 3)) )
479 logger
.debug('mobileagent: %d useragent: %s' % (useragent
.lower().find('mobile'), useragent
.lower()))
480 use_mobile
= useragent
.lower().find('mobile') > 0
483 t
= Template(HTML_CONTAINER_TEMPLATE_MOBILE
, filter=EncodeUnicode
)
485 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
487 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
489 t
.container
= handler
.cname
497 t
.guid
= config
.getGUID()
498 t
.tivos
= config
.tivos
499 t
.tivo_names
= config
.tivo_names
501 handler
.send_html(str(t
))
503 handler
.send_xml(str(t
))
505 def get_details_xml(self
, tsn
, file_path
):
506 if (tsn
, file_path
) in self
.tvbus_cache
:
507 details
= self
.tvbus_cache
[(tsn
, file_path
)]
509 file_info
= VideoDetails()
510 file_info
['valid'] = transcode
.supported_format(file_path
)
511 if file_info
['valid']:
512 file_info
.update(self
.metadata_full(file_path
, tsn
))
514 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
517 t
.get_tv
= metadata
.get_tv
518 t
.get_mpaa
= metadata
.get_mpaa
519 t
.get_stars
= metadata
.get_stars
521 self
.tvbus_cache
[(tsn
, file_path
)] = details
524 def tivo_header(self
, tsn
, path
, mime
):
525 if mime
== 'video/x-tivo-mpeg-ts':
529 details
= self
.get_details_xml(tsn
, path
)
531 chunklen
= ld
* 2 + 44
532 padding
= 2048 - chunklen
% 1024
534 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
535 padding
+ chunklen
, 2),
536 struct
.pack('>LLHH', ld
+ 16, ld
, 1, 0),
538 struct
.pack('>LLHH', ld
+ 19, ld
, 2, 0),
539 details
, '\0' * padding
])
541 def TVBusQuery(self
, handler
, query
):
542 tsn
= handler
.headers
.getheader('tsn', '')
544 path
= self
.get_local_path(handler
, query
)
545 file_path
= path
+ os
.path
.normpath(f
)
547 details
= self
.get_details_xml(tsn
, file_path
)
549 handler
.send_xml(details
)
551 class Video(BaseVideo
, Pushable
):
554 class VideoDetails(DictMixin
):
556 def __init__(self
, d
=None):
562 def __getitem__(self
, key
):
563 if key
not in self
.d
:
564 self
.d
[key
] = self
.default(key
)
567 def __contains__(self
, key
):
570 def __setitem__(self
, key
, value
):
573 def __delitem__(self
):
580 return self
.d
.__iter
__()
583 return self
.d
.iteritems()
585 def default(self
, key
):
588 'displayMajorNumber' : '0',
589 'displayMinorNumber' : '0',
590 'isEpisode' : 'true',
591 'colorCode' : ('COLOR', '4'),
592 'showType' : ('SERIES', '5')
596 elif key
.startswith('v'):