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 logger
= logging
.getLogger('pyTivo.video.video')
26 SCRIPTDIR
= os
.path
.dirname(__file__
)
30 PUSHED
= '<h3>Queued for Push to %s</h3> <p>%s</p>'
32 # Preload the templates
34 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
36 HTML_CONTAINER_TEMPLATE
= tmpl('container_html.tmpl')
37 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
38 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
40 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv
41 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
42 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
43 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
44 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
45 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
46 .wm .wmd .wtv .yuv""".split()
50 assert(config
.get_bin('ffmpeg'))
52 use_extensions
= False
54 queue
= [] # Recordings to push
57 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
60 return datetime(*uniso(iso
)[:6])
63 return int(calendar
.timegm(uniso(iso
)))
65 class Pushable(object):
67 def push_one_file(self
, f
):
68 file_info
= VideoDetails()
69 file_info
['valid'] = transcode
.supported_format(f
['path'])
72 if config
.isHDtivo(f
['tsn']):
73 for m
in ['video/mp4', 'video/bif']:
74 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
78 if (mime
== 'video/mpeg' and
79 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
80 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'])
85 if file_info
['valid']:
86 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
88 url
= f
['url'] + quote(f
['name'])
90 title
= file_info
['seriesTitle']
92 title
= file_info
['title']
94 source
= file_info
['seriesId']
98 subtitle
= file_info
['episodeTitle']
100 m
= mind
.getMind(f
['tsn'])
104 description
= file_info
['description'],
105 duration
= file_info
['duration'] / 1000,
106 size
= file_info
['size'],
111 tvrating
= file_info
['tvRating'])
112 except Exception, msg
:
115 def process_queue(self
):
119 self
.push_one_file(item
)
122 """ returns your external IP address by querying dyndns.org """
123 f
= urllib
.urlopen('http://checkip.dyndns.org/')
125 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
128 def Push(self
, handler
, query
):
130 tsn
= query
['tsn'][0]
132 logger
.error('Push requires a TiVo Service Number')
133 handler
.send_error(404)
136 if not tsn
in config
.tivos
:
137 for key
, value
in config
.tivos
.items():
138 if value
.get('name') == tsn
:
142 tivo_name
= config
.tivos
[tsn
]['name']
146 container
= quote(query
['Container'][0].split('/')[0])
147 ip
= config
.get_ip(tsn
)
148 port
= config
.getPort()
150 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
151 if config
.getIsExternal(tsn
):
152 exturl
= config
.get_server('externalurl')
154 if not exturl
.endswith('/'):
156 baseurl
= exturl
+ container
159 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
161 path
= self
.get_local_base_path(handler
, query
)
163 files
= query
.get('File', [])
165 file_path
= os
.path
.normpath(path
+ '/' + f
)
166 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
169 thread
.start_new_thread(Video
.process_queue
, (self
,))
171 logger
.info('[%s] Queued "%s" for Push to %s' %
172 (time
.strftime('%d/%b/%Y %H:%M:%S'),
173 unicode(file_path
, 'utf-8'), tivo_name
))
175 files
= [unicode(f
, 'utf-8') for f
in files
]
176 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
178 class BaseVideo(Plugin
):
180 CONTENT_TYPE
= 'x-container/tivo-videos'
182 tvbus_cache
= LRUCache(1)
184 def video_file_filter(self
, full_path
, type=None):
185 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
188 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
190 return transcode
.supported_format(full_path
)
192 def send_file(self
, handler
, path
, query
):
193 mime
= 'video/x-tivo-mpeg'
194 tsn
= handler
.headers
.getheader('tsn', '')
197 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
199 tivo_name
= handler
.address_string()
201 is_tivo_file
= (path
[-5:].lower() == '.tivo')
203 if 'Format' in query
:
204 mime
= query
['Format'][0]
206 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
207 compatible
= (not needs_tivodecode
and
208 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
211 offset
= int(handler
.headers
.getheader('Range')[6:-1])
216 valid
= bool(config
.get_bin('tivodecode') and
217 config
.get_server('tivo_mak'))
222 valid
= ((compatible
and offset
< os
.path
.getsize(path
)) or
223 (not compatible
and transcode
.is_resumable(path
, offset
)))
225 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
226 faking
= (mime
== 'video/x-tivo-mpeg' and
227 not (is_tivo_file
and compatible
))
228 fname
= unicode(path
, 'utf-8')
231 thead
= self
.tivo_header(tsn
, path
, mime
)
233 size
= os
.path
.getsize(fname
) + len(thead
)
234 handler
.send_response(200)
235 handler
.send_header('Content-Length', size
- offset
)
236 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
237 (offset
, size
- offset
- 1, size
))
239 handler
.send_response(206)
240 handler
.send_header('Transfer-Encoding', 'chunked')
241 handler
.send_header('Content-Type', mime
)
242 handler
.end_headers()
244 logger
.info('[%s] Start sending "%s" to %s' %
245 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
251 if faking
and not offset
:
252 handler
.wfile
.write(thead
)
253 logger
.debug('"%s" is tivo compatible' % fname
)
254 f
= open(fname
, 'rb')
256 if mime
== 'video/mp4':
257 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
263 block
= f
.read(512 * 1024)
266 handler
.wfile
.write(block
)
268 except Exception, msg
:
272 logger
.debug('"%s" is not tivo compatible' % fname
)
274 count
= transcode
.resume_transfer(path
, handler
.wfile
,
277 count
= transcode
.transcode(False, path
, handler
.wfile
,
281 handler
.wfile
.write('0\r\n\r\n')
282 handler
.wfile
.flush()
283 except Exception, msg
:
286 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
289 rate
= count
* 8.0 / mega_elapsed
290 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
291 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
292 tivo_name
, count
, rate
))
294 if fname
.endswith('.pyTivo-temp'):
297 def __duration(self
, full_path
):
298 return transcode
.video_info(full_path
)['millisecs']
300 def __total_items(self
, full_path
):
303 full_path
= unicode(full_path
, 'utf-8')
304 for f
in os
.listdir(full_path
):
305 if f
.startswith('.'):
307 f
= os
.path
.join(full_path
, f
)
308 f2
= f
.encode('utf-8')
312 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
314 elif f2
in transcode
.info_cache
:
315 if transcode
.supported_format(f2
):
321 def __est_size(self
, full_path
, tsn
='', mime
=''):
322 # Size is estimated by taking audio and video bit rate adding 2%
324 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
325 return os
.path
.getsize(unicode(full_path
, 'utf-8'))
328 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
329 #audioBPS = config.strtod(config.getAudioBR(tsn))
330 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
331 bitrate
= audioBPS
+ videoBPS
332 return int((self
.__duration
(full_path
) / 1000) *
333 (bitrate
* 1.02 / 8))
335 def metadata_full(self
, full_path
, tsn
='', mime
='', mtime
=None):
337 vInfo
= transcode
.video_info(full_path
)
339 if ((int(vInfo
['vHeight']) >= 720 and
340 config
.getTivoHeight
>= 720) or
341 (int(vInfo
['vWidth']) >= 1280 and
342 config
.getTivoWidth
>= 1280)):
343 data
['showingBits'] = '4096'
345 data
.update(metadata
.basic(full_path
, mtime
))
346 if full_path
[-5:].lower() == '.tivo':
347 data
.update(metadata
.from_tivo(full_path
))
348 if full_path
[-4:].lower() == '.wtv':
349 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
351 if 'episodeNumber' in data
:
353 ep
= int(data
['episodeNumber'])
356 data
['episodeNumber'] = str(ep
)
358 if config
.getDebug() and 'vHost' not in data
:
359 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
361 transcode_options
= []
363 transcode_options
= transcode
.transcode(True, full_path
,
366 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
369 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
370 ['TRANSCODE OPTIONS: '] +
372 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
375 now
= datetime
.utcnow()
377 if data
['time'].lower() == 'file':
379 mtime
= os
.path
.getmtime(unicode(full_path
, 'utf-8'))
381 now
= datetime
.utcfromtimestamp(mtime
)
383 logger
.warning('Bad file time on ' + full_path
)
384 elif data
['time'].lower() == 'oad':
385 now
= isodt(data
['originalAirDate'])
388 now
= isodt(data
['time'])
390 logger
.warning('Bad time format: ' + data
['time'] +
391 ' , using current time')
393 duration
= self
.__duration
(full_path
)
394 duration_delta
= timedelta(milliseconds
= duration
)
395 min = duration_delta
.seconds
/ 60
396 sec
= duration_delta
.seconds
% 60
400 data
.update({'time': now
.isoformat(),
401 'startTime': now
.isoformat(),
402 'stopTime': (now
+ duration_delta
).isoformat(),
403 'size': self
.__est
_size
(full_path
, tsn
, mime
),
404 'duration': duration
,
405 'iso_duration': ('P%sDT%sH%sM%sS' %
406 (duration_delta
.days
, hours
, min, sec
))})
410 def QueryContainer(self
, handler
, query
):
411 tsn
= handler
.headers
.getheader('tsn', '')
412 subcname
= query
['Container'][0]
414 if not self
.get_local_path(handler
, query
):
415 handler
.send_error(404)
418 container
= handler
.container
419 force_alpha
= container
.getboolean('force_alpha')
420 ar
= container
.get('allow_recurse', 'auto').lower()
422 allow_recurse
= not tsn
or tsn
[0] < '7'
424 allow_recurse
= ar
in ('1', 'yes', 'true', 'on')
425 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
427 files
, total
, start
= self
.get_files(handler
, query
,
428 self
.video_file_filter
,
429 force_alpha
, allow_recurse
)
432 local_base_path
= self
.get_local_base_path(handler
, query
)
434 video
= VideoDetails()
437 ltime
= time
.localtime(mtime
)
439 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
441 ltime
= time
.localtime(mtime
)
442 video
['captureDate'] = hex(int(mtime
))
443 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
444 video
['name'] = os
.path
.basename(f
.name
)
445 video
['path'] = f
.name
446 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
447 if not video
['part_path'].startswith(os
.path
.sep
):
448 video
['part_path'] = os
.path
.sep
+ video
['part_path']
449 video
['title'] = os
.path
.basename(f
.name
)
450 video
['is_dir'] = f
.isdir
452 video
['small_path'] = subcname
+ '/' + video
['name']
453 video
['total_items'] = self
.__total
_items
(f
.name
)
455 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
456 video
['valid'] = transcode
.supported_format(f
.name
)
458 video
.update(self
.metadata_full(f
.name
, tsn
,
461 video
['captureDate'] = hex(isogm(video
['time']))
463 video
['valid'] = True
464 video
.update(metadata
.basic(f
.name
, mtime
))
466 if self
.use_ts(tsn
, f
.name
):
467 video
['mime'] = 'video/x-tivo-mpeg-ts'
469 video
['mime'] = 'video/x-tivo-mpeg'
471 video
['textSize'] = metadata
.human_size(f
.size
)
476 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
478 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
479 t
.container
= handler
.cname
487 t
.guid
= config
.getGUID()
488 t
.tivos
= config
.tivos
490 handler
.send_html(str(t
))
492 handler
.send_xml(str(t
))
494 def use_ts(self
, tsn
, file_path
):
495 if config
.is_ts_capable(tsn
):
496 if file_path
[-5:].lower() == '.tivo':
498 flag
= file(file_path
).read(8)
501 if ord(flag
[7]) & 0x20:
503 elif config
.has_ts_flag():
508 def get_details_xml(self
, tsn
, file_path
):
509 if (tsn
, file_path
) in self
.tvbus_cache
:
510 details
= self
.tvbus_cache
[(tsn
, file_path
)]
512 file_info
= VideoDetails()
513 file_info
['valid'] = transcode
.supported_format(file_path
)
514 if file_info
['valid']:
515 file_info
.update(self
.metadata_full(file_path
, tsn
))
517 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
520 t
.get_tv
= metadata
.get_tv
521 t
.get_mpaa
= metadata
.get_mpaa
522 t
.get_stars
= metadata
.get_stars
523 t
.get_color
= metadata
.get_color
525 self
.tvbus_cache
[(tsn
, file_path
)] = details
528 def tivo_header(self
, tsn
, path
, mime
):
529 def pad(length
, align
):
530 extra
= length
% align
532 extra
= align
- extra
535 if mime
== 'video/x-tivo-mpeg-ts':
539 details
= self
.get_details_xml(tsn
, path
)
541 chunk
= details
+ '\0' * (pad(ld
, 4) + 4)
543 blocklen
= lc
* 2 + 40
544 padding
= pad(blocklen
, 1024)
546 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
547 padding
+ blocklen
, 2),
548 struct
.pack('>LLHH', lc
+ 12, ld
, 1, 0),
550 struct
.pack('>LLHH', lc
+ 12, ld
, 2, 0),
551 chunk
, '\0' * padding
])
553 def TVBusQuery(self
, handler
, query
):
554 tsn
= handler
.headers
.getheader('tsn', '')
556 path
= self
.get_local_path(handler
, query
)
557 file_path
= os
.path
.normpath(path
+ '/' + f
)
559 details
= self
.get_details_xml(tsn
, file_path
)
561 handler
.send_xml(details
)
563 class Video(BaseVideo
, Pushable
):
566 class VideoDetails(DictMixin
):
568 def __init__(self
, d
=None):
574 def __getitem__(self
, key
):
575 if key
not in self
.d
:
576 self
.d
[key
] = self
.default(key
)
579 def __contains__(self
, key
):
582 def __setitem__(self
, key
, value
):
585 def __delitem__(self
):
592 return self
.d
.__iter
__()
595 return self
.d
.iteritems()
597 def default(self
, key
):
600 'displayMajorNumber' : '0',
601 'displayMinorNumber' : '0',
602 'isEpisode' : 'true',
604 'showType' : ('SERIES', '5')
608 elif key
.startswith('v'):