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')
159 if not exturl
.endswith('/'):
161 baseurl
= exturl
+ container
164 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
166 path
= self
.get_local_base_path(handler
, query
)
168 files
= query
.get('File', [])
170 file_path
= path
+ os
.path
.normpath(f
)
171 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
174 thread
.start_new_thread(Video
.process_queue
, (self
,))
176 logger
.info('[%s] Queued "%s" for Push to %s' %
177 (time
.strftime('%d/%b/%Y %H:%M:%S'),
178 unicode(file_path
, 'utf-8'), tivo_name
))
180 files
= [unicode(f
, 'utf-8') for f
in files
]
181 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
183 class BaseVideo(Plugin
):
185 CONTENT_TYPE
= 'x-container/tivo-videos'
187 tvbus_cache
= LRUCache(1)
189 def pre_cache(self
, full_path
):
190 if Video
.video_file_filter(self
, full_path
):
191 transcode
.supported_format(full_path
)
193 def video_file_filter(self
, full_path
, type=None):
194 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
197 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
199 return transcode
.supported_format(full_path
)
201 def send_file(self
, handler
, path
, query
):
202 mime
= 'video/x-tivo-mpeg'
203 tsn
= handler
.headers
.getheader('tsn', '')
204 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
206 is_tivo_file
= (path
[-5:].lower() == '.tivo')
208 if 'Format' in query
:
209 mime
= query
['Format'][0]
211 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
212 compatible
= (not needs_tivodecode
and
213 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
216 offset
= int(handler
.headers
.getheader('Range')[6:-1])
221 valid
= bool(config
.get_bin('tivodecode') and
222 config
.get_server('tivo_mak'))
227 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
228 (not compatible
and transcode
.is_resumable(path
, offset
)))
230 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
231 faking
= (mime
== 'video/x-tivo-mpeg' and
232 not (is_tivo_file
and compatible
))
233 fname
= unicode(path
, 'utf-8')
236 thead
= self
.tivo_header(tsn
, path
, mime
)
238 size
= os
.stat(fname
).st_size
+ len(thead
)
239 handler
.send_response(200)
240 handler
.send_header('Content-Length', size
- offset
)
241 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
242 (offset
, size
- offset
- 1, size
))
244 handler
.send_response(206)
245 handler
.send_header('Transfer-Encoding', 'chunked')
246 handler
.send_header('Content-Type', mime
)
247 handler
.send_header('Connection', 'close')
248 handler
.end_headers()
250 logger
.info('[%s] Start sending "%s" to %s' %
251 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
257 if faking
and not offset
:
258 handler
.wfile
.write(thead
)
259 logger
.debug('"%s" is tivo compatible' % fname
)
260 f
= open(fname
, 'rb')
262 if mime
== 'video/mp4':
263 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
269 block
= f
.read(512 * 1024)
272 handler
.wfile
.write(block
)
274 except Exception, msg
:
278 logger
.debug('"%s" is not tivo compatible' % fname
)
280 count
= transcode
.resume_transfer(path
, handler
.wfile
,
283 count
= transcode
.transcode(False, path
, handler
.wfile
,
287 handler
.wfile
.write('0\r\n\r\n')
288 handler
.wfile
.flush()
289 except Exception, msg
:
292 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
295 rate
= count
* 8.0 / mega_elapsed
296 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
297 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
298 tivo_name
, count
, rate
))
300 if fname
.endswith('.pyTivo-temp'):
303 def __duration(self
, full_path
):
304 return transcode
.video_info(full_path
)['millisecs']
306 def __total_items(self
, full_path
):
309 full_path
= unicode(full_path
, 'utf-8')
310 for f
in os
.listdir(full_path
):
311 if f
.startswith('.'):
313 f
= os
.path
.join(full_path
, f
)
314 f2
= f
.encode('utf-8')
318 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
320 elif f2
in transcode
.info_cache
:
321 if transcode
.supported_format(f2
):
327 def __est_size(self
, full_path
, tsn
='', mime
=''):
328 # Size is estimated by taking audio and video bit rate adding 2%
330 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
331 return int(os
.stat(unicode(full_path
, 'utf-8')).st_size
)
334 if config
.get_tsn('audio_codec', tsn
) == None:
335 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
337 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
338 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
339 bitrate
= audioBPS
+ videoBPS
340 return int((self
.__duration
(full_path
) / 1000) *
341 (bitrate
* 1.02 / 8))
343 def metadata_full(self
, full_path
, tsn
='', mime
=''):
345 vInfo
= transcode
.video_info(full_path
)
347 if ((int(vInfo
['vHeight']) >= 720 and
348 config
.getTivoHeight
>= 720) or
349 (int(vInfo
['vWidth']) >= 1280 and
350 config
.getTivoWidth
>= 1280)):
351 data
['showingBits'] = '4096'
353 data
.update(metadata
.basic(full_path
))
354 if full_path
[-5:].lower() == '.tivo':
355 data
.update(metadata
.from_tivo(full_path
))
356 if full_path
[-4:].lower() == '.wtv':
357 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
359 if 'episodeNumber' in data
:
361 ep
= int(data
['episodeNumber'])
364 data
['episodeNumber'] = str(ep
)
366 if config
.getDebug() and 'vHost' not in data
:
367 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
369 transcode_options
= {}
371 transcode_options
= transcode
.transcode(True, full_path
,
374 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
377 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
378 ['TRANSCODE OPTIONS: '] +
379 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
380 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
383 now
= datetime
.utcnow()
385 if data
['time'].lower() == 'file':
386 mtime
= os
.stat(unicode(full_path
, 'utf-8')).st_mtime
390 now
= datetime
.utcfromtimestamp(mtime
)
392 logger
.warning('Bad file time on ' + full_path
)
393 elif data
['time'].lower() == 'oad':
394 now
= isodt(data
['originalAirDate'])
397 now
= isodt(data
['time'])
399 logger
.warning('Bad time format: ' + data
['time'] +
400 ' , using current time')
402 duration
= self
.__duration
(full_path
)
403 duration_delta
= timedelta(milliseconds
= duration
)
404 min = duration_delta
.seconds
/ 60
405 sec
= duration_delta
.seconds
% 60
409 data
.update({'time': now
.isoformat(),
410 'startTime': now
.isoformat(),
411 'stopTime': (now
+ duration_delta
).isoformat(),
412 'size': self
.__est
_size
(full_path
, tsn
, mime
),
413 'duration': duration
,
414 'iso_duration': ('P%sDT%sH%sM%sS' %
415 (duration_delta
.days
, hours
, min, sec
))})
419 def QueryContainer(self
, handler
, query
):
420 tsn
= handler
.headers
.getheader('tsn', '')
421 subcname
= query
['Container'][0]
422 useragent
= handler
.headers
.getheader('User-Agent', '')
424 if not self
.get_local_path(handler
, query
):
425 handler
.send_error(404)
428 container
= handler
.container
429 precache
= container
.get('precache', 'False').lower() == 'true'
430 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
431 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
433 files
, total
, start
= self
.get_files(handler
, query
,
434 self
.video_file_filter
,
438 local_base_path
= self
.get_local_base_path(handler
, query
)
440 video
= VideoDetails()
443 ltime
= time
.localtime(mtime
)
445 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
446 mtime
= int(time
.time())
447 ltime
= time
.localtime(mtime
)
448 video
['captureDate'] = hex(mtime
)
449 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
450 video
['name'] = os
.path
.basename(f
.name
)
451 video
['path'] = f
.name
452 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
453 if not video
['part_path'].startswith(os
.path
.sep
):
454 video
['part_path'] = os
.path
.sep
+ video
['part_path']
455 video
['title'] = os
.path
.basename(f
.name
)
456 video
['is_dir'] = f
.isdir
458 video
['small_path'] = subcname
+ '/' + video
['name']
459 video
['total_items'] = self
.__total
_items
(f
.name
)
461 if precache
or len(files
) == 1 or f
.name
in transcode
.info_cache
:
462 video
['valid'] = transcode
.supported_format(f
.name
)
464 video
.update(self
.metadata_full(f
.name
, tsn
))
466 video
['captureDate'] = hex(isogm(video
['time']))
468 video
['valid'] = True
469 video
.update(metadata
.basic(f
.name
))
471 if self
.use_ts(tsn
, f
.name
):
472 video
['mime'] = 'video/x-tivo-mpeg-ts'
474 video
['mime'] = 'video/x-tivo-mpeg'
476 video
['textSize'] = ( '%.3f GB' %
477 (float(f
.size
) / (1024 ** 3)) )
481 logger
.debug('mobileagent: %d useragent: %s' % (useragent
.lower().find('mobile'), useragent
.lower()))
482 use_mobile
= useragent
.lower().find('mobile') > 0
485 t
= Template(HTML_CONTAINER_TEMPLATE_MOBILE
, filter=EncodeUnicode
)
487 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
489 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
491 t
.container
= handler
.cname
499 t
.guid
= config
.getGUID()
500 t
.tivos
= config
.tivos
501 t
.tivo_names
= config
.tivo_names
503 handler
.send_html(str(t
))
505 handler
.send_xml(str(t
))
507 def use_ts(self
, tsn
, file_path
):
508 if config
.is_ts_capable(tsn
):
509 if file_path
[-5:].lower() == '.tivo':
511 flag
= file(file_path
).read(8)
514 if ord(flag
[7]) & 0x20:
516 elif config
.has_ts_flag():
521 def get_details_xml(self
, tsn
, file_path
):
522 if (tsn
, file_path
) in self
.tvbus_cache
:
523 details
= self
.tvbus_cache
[(tsn
, file_path
)]
525 file_info
= VideoDetails()
526 file_info
['valid'] = transcode
.supported_format(file_path
)
527 if file_info
['valid']:
528 file_info
.update(self
.metadata_full(file_path
, tsn
))
530 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
533 t
.get_tv
= metadata
.get_tv
534 t
.get_mpaa
= metadata
.get_mpaa
535 t
.get_stars
= metadata
.get_stars
537 self
.tvbus_cache
[(tsn
, file_path
)] = details
540 def tivo_header(self
, tsn
, path
, mime
):
541 if mime
== 'video/x-tivo-mpeg-ts':
545 details
= self
.get_details_xml(tsn
, path
)
547 chunklen
= ld
* 2 + 44
548 padding
= 2048 - chunklen
% 1024
550 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
551 padding
+ chunklen
, 2),
552 struct
.pack('>LLHH', ld
+ 16, ld
, 1, 0),
554 struct
.pack('>LLHH', ld
+ 19, ld
, 2, 0),
555 details
, '\0' * padding
])
557 def TVBusQuery(self
, handler
, query
):
558 tsn
= handler
.headers
.getheader('tsn', '')
560 path
= self
.get_local_path(handler
, query
)
561 file_path
= path
+ os
.path
.normpath(f
)
563 details
= self
.get_details_xml(tsn
, file_path
)
565 handler
.send_xml(details
)
567 class Video(BaseVideo
, Pushable
):
570 class VideoDetails(DictMixin
):
572 def __init__(self
, d
=None):
578 def __getitem__(self
, key
):
579 if key
not in self
.d
:
580 self
.d
[key
] = self
.default(key
)
583 def __contains__(self
, key
):
586 def __setitem__(self
, key
, value
):
589 def __delitem__(self
):
596 return self
.d
.__iter
__()
599 return self
.d
.iteritems()
601 def default(self
, key
):
604 'displayMajorNumber' : '0',
605 'displayMinorNumber' : '0',
606 'isEpisode' : 'true',
607 'colorCode' : ('COLOR', '4'),
608 'showType' : ('SERIES', '5')
612 elif key
.startswith('v'):