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
):
129 tsn
= query
['tsn'][0]
130 for key
in config
.tivo_names
:
131 if config
.tivo_names
[key
] == tsn
:
134 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
136 container
= quote(query
['Container'][0].split('/')[0])
137 ip
= config
.get_ip(tsn
)
138 port
= config
.getPort()
140 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
141 if config
.getIsExternal(tsn
):
142 exturl
= config
.get_server('externalurl')
144 if not exturl
.endswith('/'):
146 baseurl
= exturl
+ container
149 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
151 path
= self
.get_local_base_path(handler
, query
)
153 files
= query
.get('File', [])
155 file_path
= os
.path
.normpath(path
+ f
)
156 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
159 thread
.start_new_thread(Video
.process_queue
, (self
,))
161 logger
.info('[%s] Queued "%s" for Push to %s' %
162 (time
.strftime('%d/%b/%Y %H:%M:%S'),
163 unicode(file_path
, 'utf-8'), tivo_name
))
165 files
= [unicode(f
, 'utf-8') for f
in files
]
166 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
168 class BaseVideo(Plugin
):
170 CONTENT_TYPE
= 'x-container/tivo-videos'
172 tvbus_cache
= LRUCache(1)
174 def video_file_filter(self
, full_path
, type=None):
175 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
178 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
180 return transcode
.supported_format(full_path
)
182 def send_file(self
, handler
, path
, query
):
183 mime
= 'video/x-tivo-mpeg'
184 tsn
= handler
.headers
.getheader('tsn', '')
185 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
187 is_tivo_file
= (path
[-5:].lower() == '.tivo')
189 if 'Format' in query
:
190 mime
= query
['Format'][0]
192 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
193 compatible
= (not needs_tivodecode
and
194 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
197 offset
= int(handler
.headers
.getheader('Range')[6:-1])
202 valid
= bool(config
.get_bin('tivodecode') and
203 config
.get_server('tivo_mak'))
208 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
209 (not compatible
and transcode
.is_resumable(path
, offset
)))
211 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
212 faking
= (mime
== 'video/x-tivo-mpeg' and
213 not (is_tivo_file
and compatible
))
214 fname
= unicode(path
, 'utf-8')
217 thead
= self
.tivo_header(tsn
, path
, mime
)
219 size
= os
.stat(fname
).st_size
+ len(thead
)
220 handler
.send_response(200)
221 handler
.send_header('Content-Length', size
- offset
)
222 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
223 (offset
, size
- offset
- 1, size
))
225 handler
.send_response(206)
226 handler
.send_header('Transfer-Encoding', 'chunked')
227 handler
.send_header('Content-Type', mime
)
228 handler
.end_headers()
230 logger
.info('[%s] Start sending "%s" to %s' %
231 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
237 if faking
and not offset
:
238 handler
.wfile
.write(thead
)
239 logger
.debug('"%s" is tivo compatible' % fname
)
240 f
= open(fname
, 'rb')
242 if mime
== 'video/mp4':
243 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
249 block
= f
.read(512 * 1024)
252 handler
.wfile
.write(block
)
254 except Exception, msg
:
258 logger
.debug('"%s" is not tivo compatible' % fname
)
260 count
= transcode
.resume_transfer(path
, handler
.wfile
,
263 count
= transcode
.transcode(False, path
, handler
.wfile
,
267 handler
.wfile
.write('0\r\n\r\n')
268 handler
.wfile
.flush()
269 except Exception, msg
:
272 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
275 rate
= count
* 8.0 / mega_elapsed
276 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
277 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
278 tivo_name
, count
, rate
))
280 if fname
.endswith('.pyTivo-temp'):
283 def __duration(self
, full_path
):
284 return transcode
.video_info(full_path
)['millisecs']
286 def __total_items(self
, full_path
):
289 full_path
= unicode(full_path
, 'utf-8')
290 for f
in os
.listdir(full_path
):
291 if f
.startswith('.'):
293 f
= os
.path
.join(full_path
, f
)
294 f2
= f
.encode('utf-8')
298 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
300 elif f2
in transcode
.info_cache
:
301 if transcode
.supported_format(f2
):
307 def __est_size(self
, full_path
, tsn
='', mime
=''):
308 # Size is estimated by taking audio and video bit rate adding 2%
310 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
311 return int(os
.stat(unicode(full_path
, 'utf-8')).st_size
)
314 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
315 #audioBPS = config.strtod(config.getAudioBR(tsn))
316 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
317 bitrate
= audioBPS
+ videoBPS
318 return int((self
.__duration
(full_path
) / 1000) *
319 (bitrate
* 1.02 / 8))
321 def metadata_full(self
, full_path
, tsn
='', mime
=''):
323 vInfo
= transcode
.video_info(full_path
)
325 if ((int(vInfo
['vHeight']) >= 720 and
326 config
.getTivoHeight
>= 720) or
327 (int(vInfo
['vWidth']) >= 1280 and
328 config
.getTivoWidth
>= 1280)):
329 data
['showingBits'] = '4096'
331 data
.update(metadata
.basic(full_path
))
332 if full_path
[-5:].lower() == '.tivo':
333 data
.update(metadata
.from_tivo(full_path
))
334 if full_path
[-4:].lower() == '.wtv':
335 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
337 if 'episodeNumber' in data
:
339 ep
= int(data
['episodeNumber'])
342 data
['episodeNumber'] = str(ep
)
344 if config
.getDebug() and 'vHost' not in data
:
345 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
347 transcode_options
= {}
349 transcode_options
= transcode
.transcode(True, full_path
,
352 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
355 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
356 ['TRANSCODE OPTIONS: '] +
357 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
358 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
361 now
= datetime
.utcnow()
363 if data
['time'].lower() == 'file':
364 mtime
= os
.stat(unicode(full_path
, 'utf-8')).st_mtime
366 now
= datetime
.utcfromtimestamp(mtime
)
368 logger
.warning('Bad file time on ' + full_path
)
369 elif data
['time'].lower() == 'oad':
370 now
= isodt(data
['originalAirDate'])
373 now
= isodt(data
['time'])
375 logger
.warning('Bad time format: ' + data
['time'] +
376 ' , using current time')
378 duration
= self
.__duration
(full_path
)
379 duration_delta
= timedelta(milliseconds
= duration
)
380 min = duration_delta
.seconds
/ 60
381 sec
= duration_delta
.seconds
% 60
385 data
.update({'time': now
.isoformat(),
386 'startTime': now
.isoformat(),
387 'stopTime': (now
+ duration_delta
).isoformat(),
388 'size': self
.__est
_size
(full_path
, tsn
, mime
),
389 'duration': duration
,
390 'iso_duration': ('P%sDT%sH%sM%sS' %
391 (duration_delta
.days
, hours
, min, sec
))})
395 def QueryContainer(self
, handler
, query
):
396 tsn
= handler
.headers
.getheader('tsn', '')
397 subcname
= query
['Container'][0]
399 if not self
.get_local_path(handler
, query
):
400 handler
.send_error(404)
403 container
= handler
.container
404 force_alpha
= container
.getboolean('force_alpha')
405 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
407 files
, total
, start
= self
.get_files(handler
, query
,
408 self
.video_file_filter
,
412 local_base_path
= self
.get_local_base_path(handler
, query
)
414 video
= VideoDetails()
417 ltime
= time
.localtime(mtime
)
419 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
420 mtime
= int(time
.time())
421 ltime
= time
.localtime(mtime
)
422 video
['captureDate'] = hex(mtime
)
423 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
424 video
['name'] = os
.path
.basename(f
.name
)
425 video
['path'] = f
.name
426 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
427 if not video
['part_path'].startswith(os
.path
.sep
):
428 video
['part_path'] = os
.path
.sep
+ video
['part_path']
429 video
['title'] = os
.path
.basename(f
.name
)
430 video
['is_dir'] = f
.isdir
432 video
['small_path'] = subcname
+ '/' + video
['name']
433 video
['total_items'] = self
.__total
_items
(f
.name
)
435 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
436 video
['valid'] = transcode
.supported_format(f
.name
)
438 video
.update(self
.metadata_full(f
.name
, tsn
))
440 video
['captureDate'] = hex(isogm(video
['time']))
442 video
['valid'] = True
443 video
.update(metadata
.basic(f
.name
))
445 if self
.use_ts(tsn
, f
.name
):
446 video
['mime'] = 'video/x-tivo-mpeg-ts'
448 video
['mime'] = 'video/x-tivo-mpeg'
450 video
['textSize'] = metadata
.human_size(f
.size
)
455 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
457 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
458 t
.container
= handler
.cname
466 t
.guid
= config
.getGUID()
467 t
.tivos
= config
.tivos
468 t
.tivo_names
= config
.tivo_names
470 handler
.send_html(str(t
))
472 handler
.send_xml(str(t
))
474 def use_ts(self
, tsn
, file_path
):
475 if config
.is_ts_capable(tsn
):
476 if file_path
[-5:].lower() == '.tivo':
478 flag
= file(file_path
).read(8)
481 if ord(flag
[7]) & 0x20:
483 elif config
.has_ts_flag():
488 def get_details_xml(self
, tsn
, file_path
):
489 if (tsn
, file_path
) in self
.tvbus_cache
:
490 details
= self
.tvbus_cache
[(tsn
, file_path
)]
492 file_info
= VideoDetails()
493 file_info
['valid'] = transcode
.supported_format(file_path
)
494 if file_info
['valid']:
495 file_info
.update(self
.metadata_full(file_path
, tsn
))
497 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
500 t
.get_tv
= metadata
.get_tv
501 t
.get_mpaa
= metadata
.get_mpaa
502 t
.get_stars
= metadata
.get_stars
504 self
.tvbus_cache
[(tsn
, file_path
)] = details
507 def tivo_header(self
, tsn
, path
, mime
):
508 if mime
== 'video/x-tivo-mpeg-ts':
512 details
= self
.get_details_xml(tsn
, path
)
514 chunklen
= ld
* 2 + 44
515 padding
= 2048 - chunklen
% 1024
517 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
518 padding
+ chunklen
, 2),
519 struct
.pack('>LLHH', ld
+ 16, ld
, 1, 0),
521 struct
.pack('>LLHH', ld
+ 19, ld
, 2, 0),
522 details
, '\0' * padding
])
524 def TVBusQuery(self
, handler
, query
):
525 tsn
= handler
.headers
.getheader('tsn', '')
527 path
= self
.get_local_path(handler
, query
)
528 file_path
= os
.path
.normpath(path
+ f
)
530 details
= self
.get_details_xml(tsn
, file_path
)
532 handler
.send_xml(details
)
534 class Video(BaseVideo
, Pushable
):
537 class VideoDetails(DictMixin
):
539 def __init__(self
, d
=None):
545 def __getitem__(self
, key
):
546 if key
not in self
.d
:
547 self
.d
[key
] = self
.default(key
)
550 def __contains__(self
, key
):
553 def __setitem__(self
, key
, value
):
556 def __delitem__(self
):
563 return self
.d
.__iter
__()
566 return self
.d
.iteritems()
568 def default(self
, key
):
571 'displayMajorNumber' : '0',
572 'displayMinorNumber' : '0',
573 'isEpisode' : 'true',
574 'colorCode' : ('COLOR', '4'),
575 'showType' : ('SERIES', '5')
579 elif key
.startswith('v'):