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
22 from plugin
import EncodeUnicode
, Plugin
, quote
24 from operator
import itemgetter
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
= 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 .f4v .vob .mp4 .m4v .mkv
43 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
44 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
45 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
46 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
47 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
48 .wm .wmd .wtv .yuv""".split()
52 assert(config
.get_bin('ffmpeg'))
54 use_extensions
= False
56 queue
= [] # Recordings to push
59 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
62 return datetime(*uniso(iso
)[:6])
65 return int(calendar
.timegm(uniso(iso
)))
67 class Pushable(object):
69 def push_one_file(self
, f
):
70 file_info
= VideoDetails()
71 file_info
['valid'] = transcode
.supported_format(f
['path'])
73 temp_share
= config
.get_server('temp_share', '')
76 for name
, data
in config
.getShares():
77 if temp_share
== name
:
78 temp_share_path
= data
.get('path')
81 if config
.isHDtivo(f
['tsn']):
82 for m
in ['video/mp4', 'video/bif']:
83 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
87 if (mime
== 'video/mpeg' and
88 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
89 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'], temp_share_path
)
95 port
= config
.getPort()
96 container
= quote(temp_share
) + '/'
97 f
['url'] = 'http://%s:%s/%s' % (ip
, port
, container
)
99 if file_info
['valid']:
100 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
102 url
= f
['url'] + quote(f
['name'])
104 title
= file_info
['seriesTitle']
106 title
= file_info
['title']
108 source
= file_info
['seriesId']
112 subtitle
= file_info
['episodeTitle']
114 m
= mind
.getMind(f
['tsn'])
118 description
= file_info
['description'],
119 duration
= file_info
['duration'] / 1000,
120 size
= file_info
['size'],
125 tvrating
= file_info
['tvRating'])
126 except Exception, msg
:
129 def process_queue(self
):
133 self
.push_one_file(item
)
136 """ returns your external IP address by querying dyndns.org """
137 f
= urllib
.urlopen('http://checkip.dyndns.org/')
139 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
142 def Push(self
, handler
, query
):
144 tsn
= query
['tsn'][0]
146 logger
.error('Push requires a TiVo Service Number')
147 handler
.send_error(404)
150 if not tsn
in config
.tivos
:
151 for key
, value
in config
.tivos
.items():
152 if value
.get('name') == tsn
:
156 tivo_name
= config
.tivos
[tsn
]['name']
160 container
= quote(query
['Container'][0].split('/')[0])
161 ip
= config
.get_ip(tsn
)
162 port
= config
.getPort()
164 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
165 if config
.getIsExternal(tsn
):
166 exturl
= config
.get_server('externalurl')
168 if not exturl
.endswith('/'):
170 baseurl
= exturl
+ container
173 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
175 path
= self
.get_local_base_path(handler
, query
)
177 files
= query
.get('File', [])
179 file_path
= os
.path
.normpath(path
+ '/' + f
)
180 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
183 thread
.start_new_thread(Video
.process_queue
, (self
,))
185 logger
.info('[%s] Queued "%s" for Push to %s' %
186 (time
.strftime('%d/%b/%Y %H:%M:%S'),
187 unicode(file_path
, 'utf-8'), tivo_name
))
189 files
= [unicode(f
, 'utf-8') for f
in files
]
190 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
192 class BaseVideo(Plugin
):
194 CONTENT_TYPE
= 'x-container/tivo-videos'
196 tvbus_cache
= LRUCache(1)
198 def video_file_filter(self
, full_path
, type=None):
199 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
202 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
204 return transcode
.supported_format(full_path
)
206 def send_file(self
, handler
, path
, query
):
207 mime
= 'video/x-tivo-mpeg'
208 tsn
= handler
.headers
.getheader('tsn', '')
211 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
213 tivo_name
= handler
.address_string()
215 is_tivo_file
= (path
[-5:].lower() == '.tivo')
217 if 'Format' in query
:
218 mime
= query
['Format'][0]
220 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
221 compatible
= (not needs_tivodecode
and
222 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
225 offset
= int(handler
.headers
.getheader('Range')[6:-1])
230 valid
= bool(config
.get_bin('tivodecode') and
231 config
.get_server('tivo_mak'))
236 valid
= ((compatible
and offset
< os
.path
.getsize(path
)) or
237 (not compatible
and transcode
.is_resumable(path
, offset
)))
239 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
240 faking
= (mime
== 'video/x-tivo-mpeg' and
241 not (is_tivo_file
and compatible
))
242 fname
= unicode(path
, 'utf-8')
245 thead
= self
.tivo_header(tsn
, path
, mime
)
247 size
= os
.path
.getsize(fname
) + len(thead
)
248 handler
.send_response(200)
249 handler
.send_header('Content-Length', size
- offset
)
250 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
251 (offset
, size
- offset
- 1, size
))
253 handler
.send_response(206)
254 handler
.send_header('Transfer-Encoding', 'chunked')
255 handler
.send_header('Content-Type', mime
)
256 handler
.end_headers()
258 logger
.info('[%s] Start sending "%s" to %s' %
259 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
265 if faking
and not offset
:
266 handler
.wfile
.write(thead
)
267 logger
.debug('"%s" is tivo compatible' % fname
)
268 f
= open(fname
, 'rb')
270 if mime
== 'video/mp4':
271 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
277 block
= f
.read(512 * 1024)
280 handler
.wfile
.write(block
)
282 except Exception, msg
:
286 logger
.debug('"%s" is not tivo compatible' % fname
)
288 count
= transcode
.resume_transfer(path
, handler
.wfile
,
291 count
= transcode
.transcode(False, path
, handler
.wfile
,
295 handler
.wfile
.write('0\r\n\r\n')
296 handler
.wfile
.flush()
297 except Exception, msg
:
300 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
303 rate
= count
* 8.0 / mega_elapsed
304 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
305 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
306 tivo_name
, count
, rate
))
308 if fname
.endswith('.pyTivo-temp'):
311 def __duration(self
, full_path
):
312 return transcode
.video_info(full_path
)['millisecs']
314 def __total_items(self
, full_path
):
317 full_path
= unicode(full_path
, 'utf-8')
318 for f
in os
.listdir(full_path
):
319 if f
.startswith('.'):
321 f
= os
.path
.join(full_path
, f
)
322 f2
= f
.encode('utf-8')
326 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
328 elif f2
in transcode
.info_cache
:
329 if transcode
.supported_format(f2
):
335 def __est_size(self
, full_path
, tsn
='', mime
=''):
336 # Size is estimated by taking audio and video bit rate adding 2%
338 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
339 return os
.path
.getsize(unicode(full_path
, 'utf-8'))
342 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
343 #audioBPS = config.strtod(config.getAudioBR(tsn))
344 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
345 bitrate
= audioBPS
+ videoBPS
346 return int((self
.__duration
(full_path
) / 1000) *
347 (bitrate
* 1.02 / 8))
349 def metadata_full(self
, full_path
, tsn
='', mime
='', mtime
=None):
351 vInfo
= transcode
.video_info(full_path
)
353 if ((int(vInfo
['vHeight']) >= 720 and
354 config
.getTivoHeight
>= 720) or
355 (int(vInfo
['vWidth']) >= 1280 and
356 config
.getTivoWidth
>= 1280)):
357 data
['showingBits'] = '4096'
359 data
.update(metadata
.basic(full_path
, mtime
))
360 if full_path
[-5:].lower() == '.tivo':
361 data
.update(metadata
.from_tivo(full_path
))
362 if full_path
[-4:].lower() == '.wtv':
363 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
365 if 'episodeNumber' in data
:
367 ep
= int(data
['episodeNumber'])
370 data
['episodeNumber'] = str(ep
)
372 if config
.getDebug() and 'vHost' not in data
:
373 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
375 transcode_options
= {}
377 transcode_options
= transcode
.transcode(True, full_path
,
380 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
383 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
384 ['TRANSCODE OPTIONS: '] +
385 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
386 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
389 now
= datetime
.utcnow()
391 if data
['time'].lower() == 'file':
393 mtime
= os
.path
.getmtime(unicode(full_path
, 'utf-8'))
395 now
= datetime
.utcfromtimestamp(mtime
)
397 logger
.warning('Bad file time on ' + full_path
)
398 elif data
['time'].lower() == 'oad':
399 now
= isodt(data
['originalAirDate'])
402 now
= isodt(data
['time'])
404 logger
.warning('Bad time format: "' + data
['time'] +
405 '", using current time')
407 duration
= self
.__duration
(full_path
)
408 duration_delta
= timedelta(milliseconds
= duration
)
409 min = duration_delta
.seconds
/ 60
410 sec
= duration_delta
.seconds
% 60
414 data
.update({'time': now
.isoformat(),
415 'startTime': now
.isoformat(),
416 'stopTime': (now
+ duration_delta
).isoformat(),
417 'size': self
.__est
_size
(full_path
, tsn
, mime
),
418 'duration': duration
,
419 'iso_duration': ('P%sDT%sH%sM%sS' %
420 (duration_delta
.days
, hours
, min, sec
))})
424 def QueryContainer(self
, handler
, query
):
425 tsn
= handler
.headers
.getheader('tsn', '')
426 subcname
= query
['Container'][0]
427 useragent
= handler
.headers
.getheader('User-Agent', '')
429 if not self
.get_local_path(handler
, query
):
430 handler
.send_error(404)
433 container
= handler
.container
434 force_alpha
= container
.getboolean('force_alpha')
435 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
437 files
, total
, start
= self
.get_files(handler
, query
,
438 self
.video_file_filter
,
442 local_base_path
= self
.get_local_base_path(handler
, query
)
445 video
= VideoDetails()
448 ltime
= time
.localtime(mtime
)
450 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
452 ltime
= time
.localtime(mtime
)
453 video
['captureDate'] = hex(int(mtime
))
454 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
455 video
['name'] = os
.path
.basename(f
.name
)
456 video
['path'] = f
.name
457 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
458 if not video
['part_path'].startswith(os
.path
.sep
):
459 video
['part_path'] = os
.path
.sep
+ video
['part_path']
460 video
['title'] = os
.path
.basename(f
.name
)
461 video
['is_dir'] = f
.isdir
463 video
['small_path'] = subcname
+ '/' + video
['name']
464 video
['total_items'] = self
.__total
_items
(f
.name
)
466 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
467 video
['valid'] = transcode
.supported_format(f
.name
)
469 video
.update(self
.metadata_full(f
.name
, tsn
,
472 video
['valid'] = True
473 video
.update(metadata
.basic(f
.name
, mtime
))
475 if 'time' in video
and video
['time'] != '':
476 if video
['time'].lower() == 'oad':
477 video
['time'] = video
['originalAirDate']
480 video
['captureDate'] = hex(isogm(video
['time']))
481 video
['textDate'] = time
.strftime('%b %d, %Y', time
.localtime(isogm(video
['time'])))
484 logger
.warning('Bad time format: "' + video
['time'] +
485 '", using current time')
487 if self
.use_ts(tsn
, f
.name
):
488 video
['mime'] = 'video/x-tivo-mpeg-ts'
490 video
['mime'] = 'video/x-tivo-mpeg'
492 video
['textSize'] = metadata
.human_size(f
.size
)
497 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
499 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
501 sortby
= query
.get('SortOrder', ['Normal'])[0].lower()
503 if use_html
and resort
:
504 if sortby
== 'capturedate':
505 logger
.info('re-sorting by captureDate, reverse=True')
506 videos
.sort(key
=itemgetter('captureDate'), reverse
=True)
507 elif sortby
== '!capturedate':
508 logger
.info('re-sorting by captureDate, reverse=False')
509 videos
.sort(key
=itemgetter('captureDate'), reverse
=False)
511 t
.container
= handler
.cname
519 t
.guid
= config
.getGUID()
520 t
.tivos
= config
.tivos
522 handler
.send_html(str(t
))
524 handler
.send_xml(str(t
))
526 def use_ts(self
, tsn
, file_path
):
527 if config
.is_ts_capable(tsn
):
528 if file_path
[-5:].lower() == '.tivo':
530 flag
= file(file_path
).read(8)
533 if ord(flag
[7]) & 0x20:
535 elif config
.has_ts_flag():
540 def get_details_xml(self
, tsn
, file_path
):
541 if (tsn
, file_path
) in self
.tvbus_cache
:
542 details
= self
.tvbus_cache
[(tsn
, file_path
)]
544 file_info
= VideoDetails()
545 file_info
['valid'] = transcode
.supported_format(file_path
)
546 if file_info
['valid']:
547 file_info
.update(self
.metadata_full(file_path
, tsn
))
549 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
552 t
.get_tv
= metadata
.get_tv
553 t
.get_mpaa
= metadata
.get_mpaa
554 t
.get_stars
= metadata
.get_stars
556 self
.tvbus_cache
[(tsn
, file_path
)] = details
559 def tivo_header(self
, tsn
, path
, mime
):
560 def pad(length
, align
):
561 extra
= length
% align
563 extra
= align
- extra
566 if mime
== 'video/x-tivo-mpeg-ts':
570 details
= self
.get_details_xml(tsn
, path
)
572 chunk
= details
+ '\0' * (pad(ld
, 4) + 4)
574 blocklen
= lc
* 2 + 40
575 padding
= pad(blocklen
, 1024)
577 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
578 padding
+ blocklen
, 2),
579 struct
.pack('>LLHH', lc
+ 12, ld
, 1, 0),
581 struct
.pack('>LLHH', lc
+ 12, ld
, 2, 0),
582 chunk
, '\0' * padding
])
584 def TVBusQuery(self
, handler
, query
):
585 tsn
= handler
.headers
.getheader('tsn', '')
587 path
= self
.get_local_path(handler
, query
)
588 file_path
= os
.path
.normpath(path
+ '/' + f
)
590 details
= self
.get_details_xml(tsn
, file_path
)
592 handler
.send_xml(details
)
594 class Video(BaseVideo
, Pushable
):
597 class VideoDetails(DictMixin
):
599 def __init__(self
, d
=None):
605 def __getitem__(self
, key
):
606 if key
not in self
.d
:
607 self
.d
[key
] = self
.default(key
)
610 def __contains__(self
, key
):
613 def __setitem__(self
, key
, value
):
616 def __delitem__(self
):
623 return self
.d
.__iter
__()
626 return self
.d
.iteritems()
628 def default(self
, key
):
631 'displayMajorNumber' : '0',
632 'displayMinorNumber' : '0',
633 'isEpisode' : 'true',
634 'colorCode' : ('COLOR', '4'),
635 'showType' : ('SERIES', '5')
639 elif key
.startswith('v'):